Day night cycle
This commit is contained in:
@@ -497,6 +497,8 @@ interface ServerWorld {
|
||||
drop_item(x: number, y: number, z: number, item: ItemStack): void; // pops out of the block, like drops
|
||||
is_loaded(x: number, z: number): boolean;
|
||||
readonly seed: string;
|
||||
readonly time: number; // world time, see below
|
||||
set_time(time: number): void; // synced to every player
|
||||
}
|
||||
|
||||
interface Player {
|
||||
@@ -533,6 +535,12 @@ interface ModStorage {
|
||||
}
|
||||
```
|
||||
|
||||
**World time** counts ticks since 06:00 of day 1 and never wraps around. A day (06:00 to 18:00) is 36000 ticks, 30
|
||||
minutes, and a night (18:00 to 06:00) is 12000 ticks, 10 minutes, so in game hours are shorter at night. Dusk is the
|
||||
first minute of the night and dawn the last, so it's bright all day. `common/time.ts` has `clock`, `format_clock`,
|
||||
`is_day` and `daylight` for turning it into something readable. Client scripts can read it as `ctx.world.time`, and
|
||||
players can use `/time`, `/time set <ticks|day|noon|sunset|night|midnight>` and `/time add <ticks>`.
|
||||
|
||||
The server loads chunks around every player (its simulation distance) and generates them itself with the same generator
|
||||
as the clients. Blocks in unloaded chunks can't be read or changed, and don't tick.
|
||||
|
||||
@@ -549,7 +557,7 @@ ctx.commands.register("heal", {
|
||||
```
|
||||
|
||||
Commands run on the server when a player types `/name` in chat. Two mods using the same name, or a mod using one of the
|
||||
engine's (`give` and `tps`), is a load error. `/copper_tools:heal` always works as the unambiguous form.
|
||||
engine's (`give`, `time` and `tps`), is a load error. `/copper_tools:heal` always works as the unambiguous form.
|
||||
|
||||
## Client scripts
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ export class Client {
|
||||
this.#on_disconnect = on_disconnect;
|
||||
|
||||
this.level = new ClientLevel(connection.seed);
|
||||
this.level.time = connection.time;
|
||||
for (const [x, y, z, id, state] of connection.initial_changes) {
|
||||
this.level.record_change(x, y, z, id, state);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { Client } from "$/client/client.ts";
|
||||
import { DebugUI } from "$/client/debug_ui.ts";
|
||||
import { format_clock } from "$/common/time.ts";
|
||||
|
||||
// f3: every entity's fields, editable
|
||||
export class DebugOverlay {
|
||||
render(client: Client) {
|
||||
DebugUI.begin("Entities", 10, 10, 300);
|
||||
DebugUI.text(`${format_clock(client.level.time)} (tick ${client.level.time})`);
|
||||
DebugUI.separator();
|
||||
|
||||
for (const entity of client.level.entities.values()) {
|
||||
if (DebugUI.collapsing_header(`${entity.constructor.name} - ${entity.id}`)) {
|
||||
|
||||
@@ -87,6 +87,8 @@ export class ClientLevel {
|
||||
second_timer = 0;
|
||||
tick_timer = 0;
|
||||
seed: string;
|
||||
// world time in ticks, counted here between the server's updates. see common/time.ts
|
||||
time = 0;
|
||||
|
||||
// blocks players changed from the generated terrain, per chunk, so they survive reloading chunks
|
||||
changes = new Map<string, Map<string, BlockChange>>();
|
||||
@@ -129,6 +131,7 @@ export class ClientLevel {
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.time += 1;
|
||||
for (const entity of this.entities.values()) {
|
||||
entity.save_previous_position();
|
||||
entity.tick();
|
||||
|
||||
@@ -100,6 +100,9 @@ function client_context(listing: ModListing): ClientContext {
|
||||
if (nid === AIR) return AIR_ID;
|
||||
return EverythingRegistry.get_by_id<BlockRegistry>("blocks", nid)?.id;
|
||||
},
|
||||
get time() {
|
||||
return need_client().level.time;
|
||||
},
|
||||
},
|
||||
log: (...args) => console.log(`[${mod}]`, ...args),
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ export class Connection {
|
||||
initial_changes: BlockChange[];
|
||||
spawn: { x: number; y: number; z: number; yaw: number; pitch: number };
|
||||
selected_slot: number;
|
||||
time: number;
|
||||
// who was already there when we joined, they become entities in the level
|
||||
initial_players: PlayerInfo[];
|
||||
initial_entities: EntityInfo[];
|
||||
@@ -24,6 +25,7 @@ export class Connection {
|
||||
this.initial_changes = join.changes;
|
||||
this.spawn = join.spawn;
|
||||
this.selected_slot = join.selected_slot;
|
||||
this.time = join.time;
|
||||
this.initial_players = join.players;
|
||||
this.initial_entities = join.entities;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ export class ClientPacketListener {
|
||||
case "player_join":
|
||||
level.add_entity(new RemotePlayer(level, message.player));
|
||||
break;
|
||||
case "time":
|
||||
level.time = message.time;
|
||||
break;
|
||||
case "player_leave":
|
||||
level.remove_entity(message.id);
|
||||
break;
|
||||
|
||||
@@ -1,19 +1,41 @@
|
||||
import type { Client } from "$/client/client.ts";
|
||||
import { begin_mode_3d, end_mode_3d } from "$/client/renderer/mod.ts";
|
||||
import { begin_mode_3d, clear_background, end_mode_3d, update_lightmap } from "$/client/renderer/mod.ts";
|
||||
import { daylight } from "$/common/time.ts";
|
||||
import { Hud } from "$/client/gui/hud.ts";
|
||||
import { DebugOverlay } from "$/client/gui/debug_overlay.ts";
|
||||
import { LevelRenderer } from "./level_renderer.ts";
|
||||
|
||||
// the sky at noon and in the middle of the night
|
||||
const DAY_SKY = [0.69, 0.8, 1];
|
||||
const NIGHT_SKY = [0.02, 0.03, 0.08];
|
||||
// minecraft's darkest sky: moonlight still lights things a little
|
||||
const NIGHT_SKY_LIGHT = 0.2;
|
||||
|
||||
// draws a frame, like minecraft's GameRenderer: the level from the camera, then the hud and screens on top
|
||||
export class GameRenderer {
|
||||
level_renderer = new LevelRenderer();
|
||||
hud = new Hud();
|
||||
debug_overlay = new DebugOverlay();
|
||||
// what the lightmap was last made for, so it's only rebuilt when the light changes
|
||||
#lightmap_daylight = -1;
|
||||
|
||||
// sky color and sky light for the time of day
|
||||
#setup_sky(client: Client, partial_tick: number) {
|
||||
const light = daylight(client.level.time + partial_tick);
|
||||
const [r, g, b] = DAY_SKY.map((day, i) => NIGHT_SKY[i] + (day - NIGHT_SKY[i]) * light);
|
||||
clear_background(r, g, b, 1);
|
||||
|
||||
if (Math.abs(light - this.#lightmap_daylight) > 0.001) {
|
||||
this.#lightmap_daylight = light;
|
||||
update_lightmap(NIGHT_SKY_LIGHT + (1 - NIGHT_SKY_LIGHT) * light);
|
||||
}
|
||||
}
|
||||
|
||||
// partial_tick is how far this frame is between the last tick and the next, entities are drawn in between
|
||||
render(client: Client, partial_tick: number) {
|
||||
const camera = client.camera;
|
||||
camera.setup(client.player, partial_tick);
|
||||
this.#setup_sky(client, partial_tick);
|
||||
|
||||
begin_mode_3d(camera);
|
||||
this.level_renderer.render_opaque(client.level, camera);
|
||||
|
||||
@@ -11,7 +11,11 @@ export interface ClientContext {
|
||||
net: ClientNet;
|
||||
player: { readonly name: string; readonly position: Readonly<Position> };
|
||||
// read only, what this client sees
|
||||
world: { get_block(x: number, y: number, z: number): Id | undefined };
|
||||
world: {
|
||||
get_block(x: number, y: number, z: number): Id | undefined;
|
||||
// ticks since 06:00 of day 1, see ServerWorld.time
|
||||
readonly time: number;
|
||||
};
|
||||
log(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,9 @@ export interface ServerWorld {
|
||||
drop_item(x: number, y: number, z: number, item: ItemStack): void;
|
||||
is_loaded(x: number, z: number): boolean;
|
||||
readonly seed: string;
|
||||
// ticks since 06:00 of day 1. a day is 36000 ticks (06:00 to 18:00), a night 12000
|
||||
readonly time: number;
|
||||
set_time(time: number): void;
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
|
||||
+6
-2
@@ -7,7 +7,7 @@ export const AIR_ID = "bworld:air";
|
||||
|
||||
// bump when a client and server of different versions can't play together.
|
||||
// the server rejects a different version before the client downloads anything
|
||||
export const PROTOCOL_VERSION = 2;
|
||||
export const PROTOCOL_VERSION = 3;
|
||||
|
||||
export interface PlayerInfo {
|
||||
id: string;
|
||||
@@ -101,6 +101,8 @@ export type ServerMessage =
|
||||
changes: BlockChange[];
|
||||
spawn: { x: number; y: number; z: number; yaw: number; pitch: number };
|
||||
selected_slot: number;
|
||||
// world time in ticks, see common/time.ts
|
||||
time: number;
|
||||
}
|
||||
| { type: "player_join"; player: PlayerInfo }
|
||||
| { type: "player_leave"; id: string }
|
||||
@@ -120,7 +122,9 @@ export type ServerMessage =
|
||||
| { type: "cursor"; item: ItemData | null }
|
||||
| { type: "open_screen"; layout: ScreenLayout; properties: Record<string, number> }
|
||||
| { type: "screen_properties"; properties: Record<string, number> }
|
||||
| { type: "close_screen" };
|
||||
| { type: "close_screen" }
|
||||
// the world time, every second and when it's changed. clients count ticks in between
|
||||
| { type: "time"; time: number };
|
||||
|
||||
export const MAX_NAME_LENGTH = 16;
|
||||
export const MAX_CHAT_LENGTH = 256;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// the day and night cycle. world time counts ticks since 06:00 of day 1, it never wraps around
|
||||
import { TICKS_PER_SECOND } from "./constants.ts";
|
||||
|
||||
// 30 minutes of day, 06:00 to 18:00
|
||||
export const DAY_TICKS = 30 * 60 * TICKS_PER_SECOND;
|
||||
// 10 minutes of night, 18:00 to 06:00
|
||||
export const NIGHT_TICKS = 10 * 60 * TICKS_PER_SECOND;
|
||||
export const CYCLE_TICKS = DAY_TICKS + NIGHT_TICKS;
|
||||
// dusk right after sunset and dawn right before sunrise, so the whole day is bright
|
||||
export const TWILIGHT_TICKS = 60 * TICKS_PER_SECOND;
|
||||
|
||||
// in game hours are longer during the day than at night, since both halves are 12 hours
|
||||
const DAY_TICKS_PER_HOUR = DAY_TICKS / 12;
|
||||
const NIGHT_TICKS_PER_HOUR = NIGHT_TICKS / 12;
|
||||
// from midnight to 06:00, to count days from midnight
|
||||
const TICKS_BEFORE_SIX = 6 * NIGHT_TICKS_PER_HOUR;
|
||||
|
||||
// named times of day, as ticks into the cycle
|
||||
export const TIMES_OF_DAY: Record<string, number> = {
|
||||
day: 0,
|
||||
noon: 6 * DAY_TICKS_PER_HOUR,
|
||||
sunset: DAY_TICKS,
|
||||
night: DAY_TICKS + TWILIGHT_TICKS,
|
||||
midnight: DAY_TICKS + 6 * NIGHT_TICKS_PER_HOUR,
|
||||
};
|
||||
|
||||
// ticks into the current cycle, 0 is 06:00
|
||||
export function time_of_day(time: number): number {
|
||||
return ((time % CYCLE_TICKS) + CYCLE_TICKS) % CYCLE_TICKS;
|
||||
}
|
||||
|
||||
export function is_day(time: number): boolean {
|
||||
return time_of_day(time) < DAY_TICKS;
|
||||
}
|
||||
|
||||
// the day number, starting at 1 and going up at midnight, and the time on a 24 hour clock
|
||||
export function clock(time: number): { day: number; hour: number; minute: number } {
|
||||
const t = time_of_day(time);
|
||||
const hours = t < DAY_TICKS ? 6 + t / DAY_TICKS_PER_HOUR : 18 + (t - DAY_TICKS) / NIGHT_TICKS_PER_HOUR;
|
||||
const total_minutes = Math.floor(hours * 60) % (24 * 60);
|
||||
return {
|
||||
day: Math.floor((time + TICKS_BEFORE_SIX) / CYCLE_TICKS) + 1,
|
||||
hour: Math.floor(total_minutes / 60),
|
||||
minute: total_minutes % 60,
|
||||
};
|
||||
}
|
||||
|
||||
// like "Day 3, 14:05"
|
||||
export function format_clock(time: number): string {
|
||||
const { day, hour, minute } = clock(time);
|
||||
return `Day ${day}, ${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
// how much daylight there is, 1 all day and 0 in the middle of the night, fading through dusk and dawn.
|
||||
// time can be fractional, for drawing between ticks
|
||||
export function daylight(time: number): number {
|
||||
const t = time_of_day(time);
|
||||
if (t < DAY_TICKS) return 1;
|
||||
const into_night = t - DAY_TICKS;
|
||||
const until_day = CYCLE_TICKS - t;
|
||||
const fade = Math.min(1, into_night / TWILIGHT_TICKS, until_day / TWILIGHT_TICKS);
|
||||
return 1 - smoothstep(fade);
|
||||
}
|
||||
|
||||
function smoothstep(x: number) {
|
||||
return x * x * (3 - 2 * x);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "$/common/protocol.ts";
|
||||
import type { AtlasListing, ModListing, RecipeBook } from "$/common/mod_loader.ts";
|
||||
import type { WorldgenSetup } from "$/common/generation.ts";
|
||||
import { CYCLE_TICKS, format_clock, time_of_day, TIMES_OF_DAY } from "$/common/time.ts";
|
||||
import { ModRuntime, run_guarded } from "./mod_runtime.ts";
|
||||
import type { GameLoop } from "./game_loop.ts";
|
||||
import { consume_recipe_items, update_crafting_result } from "./crafting.ts";
|
||||
@@ -62,6 +63,8 @@ export interface GameHost {
|
||||
export interface SavedWorld {
|
||||
version: 3;
|
||||
seed: string;
|
||||
// world time in ticks, 0 in saves from before the day and night cycle
|
||||
time?: number;
|
||||
changes: BlockChange[];
|
||||
tiles: SavedTile[];
|
||||
// every ctx.containers container, by id
|
||||
@@ -124,6 +127,7 @@ export class GameServer {
|
||||
this.world = new ServerWorld(saved?.seed ?? default_seed, mods.worldgen);
|
||||
this.mods.storage = saved?.mod_storage ?? {};
|
||||
this.world.load_changes(saved?.changes ?? []);
|
||||
this.world.time = saved?.time ?? 0;
|
||||
for (const [id, items] of Object.entries(saved?.containers ?? {})) {
|
||||
const container = new Container(items.length);
|
||||
container.load(items);
|
||||
@@ -188,7 +192,11 @@ export class GameServer {
|
||||
|
||||
tick() {
|
||||
this.#tick += 1;
|
||||
this.world.time += 1;
|
||||
const second = this.#tick % TICKS_PER_SECOND === 0;
|
||||
if (second) {
|
||||
this.#broadcast({ type: "time", time: this.world.time });
|
||||
}
|
||||
|
||||
for (const tile of [...this.world.tiles.values()]) {
|
||||
if (tile.mod_data === undefined) {
|
||||
@@ -236,6 +244,7 @@ export class GameServer {
|
||||
const saved: SavedWorld = {
|
||||
version: 3,
|
||||
seed: this.world.seed,
|
||||
time: this.world.time,
|
||||
changes: this.world.all_changes(),
|
||||
tiles: [...this.world.tiles.values()].map((tile) => ({
|
||||
id: tile.id,
|
||||
@@ -342,6 +351,12 @@ export class GameServer {
|
||||
this.#broadcast({ type: "player_move", id: player.id, x, y, z, yaw: player.yaw, pitch: player.pitch }, player);
|
||||
}
|
||||
|
||||
set_time(time: number) {
|
||||
this.world.time = time;
|
||||
this.world.dirty = true;
|
||||
this.#broadcast({ type: "time", time });
|
||||
}
|
||||
|
||||
// changes a block's state bits without anything else happening, see ServerWorld.get_block_value
|
||||
set_block_state(x: number, y: number, z: number, state: number) {
|
||||
const id = this.world.get_block_id(x, y, z);
|
||||
@@ -511,6 +526,7 @@ export class GameServer {
|
||||
entities: [...this.#entities.values()].map((entity) => entity.info()),
|
||||
spawn: { x: player.x, y: player.y, z: player.z, yaw: player.yaw, pitch: player.pitch },
|
||||
selected_slot: player.selected_slot,
|
||||
time: this.world.time,
|
||||
});
|
||||
this.#players.set(conn, player);
|
||||
this.#sync(player);
|
||||
@@ -820,6 +836,10 @@ export class GameServer {
|
||||
this.give(player, item_id, amount);
|
||||
return;
|
||||
}
|
||||
if (command === "time") {
|
||||
this.#time_command(player, args);
|
||||
return;
|
||||
}
|
||||
if (command === "tps") {
|
||||
if (!this.loop) {
|
||||
this.#send(player, { type: "chat", text: "The game loop isn't running" });
|
||||
@@ -841,6 +861,30 @@ export class GameServer {
|
||||
this.#send(player, { type: "chat", text: `Unknown command /${command}` });
|
||||
}
|
||||
|
||||
// /time shows the time, /time set <ticks or day, noon, sunset, night, midnight> changes it, /time add <ticks>
|
||||
#time_command(player: ServerPlayer, [action, value]: string[]) {
|
||||
const usage = "Usage: /time [set <ticks|day|noon|sunset|night|midnight> | add <ticks>]";
|
||||
if (action === undefined) {
|
||||
this.#send(player, { type: "chat", text: `${format_clock(this.world.time)} (tick ${this.world.time})` });
|
||||
return;
|
||||
}
|
||||
const ticks = Number(value);
|
||||
if (action === "set" && value !== undefined && value in TIMES_OF_DAY) {
|
||||
// the next time it's that time of day, so days keep counting up
|
||||
const now = this.world.time;
|
||||
const target = now - time_of_day(now) + TIMES_OF_DAY[value];
|
||||
this.set_time(target >= now ? target : target + CYCLE_TICKS);
|
||||
} else if (action === "set" && Number.isInteger(ticks) && ticks >= 0) {
|
||||
this.set_time(ticks);
|
||||
} else if (action === "add" && Number.isInteger(ticks) && this.world.time + ticks >= 0) {
|
||||
this.set_time(this.world.time + ticks);
|
||||
} else {
|
||||
this.#send(player, { type: "chat", text: usage });
|
||||
return;
|
||||
}
|
||||
this.#broadcast({ type: "chat", text: `${player.name} set the time to ${format_clock(this.world.time)}` });
|
||||
}
|
||||
|
||||
#close_screen(player: ServerPlayer) {
|
||||
const screen = player.screen;
|
||||
player.screen = undefined;
|
||||
|
||||
@@ -310,7 +310,9 @@ export class ModRuntime {
|
||||
if (!/^[a-z0-9_]+$/.test(name)) {
|
||||
throw new ModLoadError(mod, `command name "${name}" must be a-z, 0-9 and _`);
|
||||
}
|
||||
if (["give", "tps"].includes(name)) throw new ModLoadError(mod, `/${name} belongs to the engine`);
|
||||
if (["give", "tps", "time"].includes(name)) {
|
||||
throw new ModLoadError(mod, `/${name} belongs to the engine`);
|
||||
}
|
||||
const existing = this.commands.get(name);
|
||||
if (existing) {
|
||||
throw new ModLoadError(mod, `/${name} is also registered by ${existing.mod}`);
|
||||
@@ -363,6 +365,15 @@ export class ModRuntime {
|
||||
get seed() {
|
||||
return game().world.seed;
|
||||
},
|
||||
get time() {
|
||||
return game().world.time;
|
||||
},
|
||||
set_time: (time) => {
|
||||
if (!Number.isInteger(time) || time < 0) {
|
||||
throw new Error(`time must be a whole number of ticks, not ${time}`);
|
||||
}
|
||||
game().set_time(time);
|
||||
},
|
||||
},
|
||||
players: {
|
||||
all: () => game().players().map((p) => this.player(p)),
|
||||
|
||||
@@ -41,6 +41,9 @@ export class ServerWorld {
|
||||
#changes = new Map<number, Map<string, BlockChange>>();
|
||||
tiles = new Map<string, Tile>();
|
||||
|
||||
// ticks since 06:00 of day 1, see common/time.ts
|
||||
time = 0;
|
||||
|
||||
// set when anything that gets saved changes
|
||||
dirty = false;
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { assert, assertEquals } from "@std/assert";
|
||||
import { clock, CYCLE_TICKS, DAY_TICKS, daylight, format_clock, is_day, NIGHT_TICKS } from "$/common/time.ts";
|
||||
import { test_game } from "./helpers.ts";
|
||||
|
||||
Deno.test("a day is 30 minutes and a night 10, tick 0 is 06:00 of day 1", () => {
|
||||
assertEquals(DAY_TICKS, 30 * 60 * 20);
|
||||
assertEquals(NIGHT_TICKS, 10 * 60 * 20);
|
||||
assertEquals(format_clock(0), "Day 1, 06:00");
|
||||
assertEquals(format_clock(DAY_TICKS / 2), "Day 1, 12:00");
|
||||
assertEquals(format_clock(DAY_TICKS), "Day 1, 18:00");
|
||||
// midnight is halfway through the night, and the day number goes up there
|
||||
assertEquals(format_clock(DAY_TICKS + NIGHT_TICKS / 2 - 1), "Day 1, 23:59");
|
||||
assertEquals(format_clock(DAY_TICKS + NIGHT_TICKS / 2), "Day 2, 00:00");
|
||||
assertEquals(format_clock(CYCLE_TICKS), "Day 2, 06:00");
|
||||
assertEquals(clock(3 * CYCLE_TICKS + DAY_TICKS / 4), { day: 4, hour: 9, minute: 0 });
|
||||
assert(is_day(DAY_TICKS - 1) && !is_day(DAY_TICKS) && is_day(CYCLE_TICKS));
|
||||
});
|
||||
|
||||
Deno.test("it's bright all day, dark in the middle of the night, and fades in between", () => {
|
||||
assertEquals(daylight(0), 1);
|
||||
assertEquals(daylight(DAY_TICKS - 1), 1);
|
||||
assertEquals(daylight(DAY_TICKS + NIGHT_TICKS / 2), 0);
|
||||
const dusk = daylight(DAY_TICKS + 600);
|
||||
const dawn = daylight(CYCLE_TICKS - 600);
|
||||
assert(dusk > 0 && dusk < 1 && Math.abs(dusk - dawn) < 1e-9, `dusk ${dusk}, dawn ${dawn}`);
|
||||
// smooth between ticks too
|
||||
assert(daylight(DAY_TICKS + 600.5) < dusk);
|
||||
});
|
||||
|
||||
Deno.test("the server keeps the time, tells players, saves it, and /time changes it", async () => {
|
||||
const { game, join, take, send } = await test_game("mods");
|
||||
assertEquals(join(1, "alice").time, 0);
|
||||
|
||||
for (let i = 0; i < 40; i++) game.tick();
|
||||
assertEquals(take(1).filter((m) => m.type === "time").map((m) => m.time), [20, 40]);
|
||||
|
||||
send(1, { type: "chat", text: "/time" });
|
||||
assertEquals(take(1).find((m) => m.type === "chat").text, "Day 1, 06:00 (tick 40)");
|
||||
|
||||
send(1, { type: "chat", text: "/time set midnight" });
|
||||
let messages = take(1);
|
||||
assertEquals(messages.find((m) => m.type === "time").time, DAY_TICKS + NIGHT_TICKS / 2);
|
||||
assert(messages.some((m) => m.text === "alice set the time to Day 2, 00:00"));
|
||||
|
||||
// named times are the next one, so time never goes backwards
|
||||
send(1, { type: "chat", text: "/time set day" });
|
||||
assertEquals(game.world.time, CYCLE_TICKS);
|
||||
send(1, { type: "chat", text: "/time add 100" });
|
||||
assertEquals(game.world.time, CYCLE_TICKS + 100);
|
||||
send(1, { type: "chat", text: "/time set nope" });
|
||||
messages = take(1);
|
||||
assert(messages.at(-1).text.startsWith("Usage: /time"));
|
||||
|
||||
const loaded = await test_game("mods", game.save());
|
||||
assertEquals(loaded.game.world.time, CYCLE_TICKS + 100);
|
||||
assertEquals(loaded.join(1, "alice").time, CYCLE_TICKS + 100);
|
||||
});
|
||||
Reference in New Issue
Block a user