Checks / web (push) Canceled after 0s
Port forced movement (turn delays, bumps, ice, currents, pits), dynamic return warps, elevators, Surf, Cut, Strength and cycling rules, working map blocks with changeblock/changemapblocks, and the remaining NPC movement types and movement commands. Add tileset animation from the ROM's command tables, grass sprite priority and special map palettes; room decorations with ownership, the bedroom PC and save migration; and Key Items access to the Bicycle. Fourteen original-engine traces compare positions, facing, actors, event flags and scripted poses. Tile animation matches original VRAM for four tilesets, and LoadMapPals matches 192 palette cases. skyfall_top now keeps the $60 sprite offset its step function leaves behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
456 lines
19 KiB
TypeScript
456 lines
19 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { CrystalAssets } from "../src/game/rom/assets";
|
|
import { GameWorld } from "../src/game/world/world";
|
|
import { emptyScriptState } from "../src/game/scripts/interpreter";
|
|
import { generatePartyMon } from "../src/game/party/pokemon";
|
|
import type { Direction } from "../src/game/world/types";
|
|
import { settle, walkTo } from "./world-driver";
|
|
|
|
const romPath =
|
|
process.env.CRYSTAL_ROM_PATH ??
|
|
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
|
|
const BURNED_TOWER_1F = "3:13",
|
|
BURNED_TOWER_B1F = "3:14";
|
|
const EVENT_HOLE_IN_BURNED_TOWER = 818;
|
|
|
|
/** A world on maps that normal play does not enable yet. */
|
|
function fixture(maps: string[]) {
|
|
const assets = new CrystalAssets(new Uint8Array(readFileSync(romPath)));
|
|
const ported = assets.isPorted.bind(assets);
|
|
assets.isPorted = (id) => ported(id) || maps.includes(id);
|
|
const world = new GameWorld(assets, () => new Date("2026-09-22T12:00:00"), () => 0);
|
|
world.state = emptyScriptState();
|
|
return { assets, world };
|
|
}
|
|
|
|
function hold(world: GameWorld, direction: Direction | undefined, ticks: number) {
|
|
for (let tick = 0; tick < ticks; tick++) world.tick(direction);
|
|
}
|
|
|
|
describe.skipIf(!existsSync(romPath))("overworld foundation with the local ROM", () => {
|
|
it("changeblock in Burned Tower's TILES callback closes the hole until the event is set", () => {
|
|
const { assets, world } = fixture([BURNED_TOWER_1F, BURNED_TOWER_B1F]);
|
|
world.state.scenes[BURNED_TOWER_1F] = 2; // SCENE_BURNEDTOWER1F_NOOP
|
|
world.warpTo(BURNED_TOWER_1F, 10, 10);
|
|
expect(assets.permission(assets.collision(world.map, 10, 9))).toBe(0);
|
|
expect(assets.collision(world.map, 10, 9)).not.toBe(0x60);
|
|
// The ROM's own block data still has the hole; only the working copy changed.
|
|
expect(assets.collision(assets.map(BURNED_TOWER_1F), 10, 9)).toBe(0x60);
|
|
world.state.events.add(EVENT_HOLE_IN_BURNED_TOWER);
|
|
world.warpTo(BURNED_TOWER_1F, 10, 10);
|
|
expect(assets.collision(world.map, 10, 9)).toBe(0x60);
|
|
});
|
|
|
|
it("falls through a pit with FallIntoMapScript's skyfall and earthquake", () => {
|
|
const { world } = fixture([BURNED_TOWER_1F, BURNED_TOWER_B1F]);
|
|
world.state.scenes[BURNED_TOWER_1F] = 2;
|
|
world.state.scenes[BURNED_TOWER_B1F] = 1;
|
|
world.state.events.add(EVENT_HOLE_IN_BURNED_TOWER);
|
|
world.warpTo(BURNED_TOWER_1F, 10, 10);
|
|
// Turn, then walk up onto the hole.
|
|
hold(world, "up", 4);
|
|
hold(world, "up", 8);
|
|
expect([world.mapId, world.x, world.y]).toEqual([BURNED_TOWER_1F, 10, 9]);
|
|
// The arrival tick falls through; the next starts the rest of the script.
|
|
hold(world, undefined, 2);
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
expect([world.mapId, world.x, world.y]).toEqual([BURNED_TOWER_B1F, 10, 9]);
|
|
expect(world.activity).toBe("scene");
|
|
// Invisible above the screen at first, then falling into view.
|
|
expect(world.poses().some((pose) => pose.id === 0)).toBe(false);
|
|
let shook = false;
|
|
for (let tick = 0; tick < 200 && world.busy; tick++) {
|
|
world.tick();
|
|
if (world.screenShake) shook = true;
|
|
}
|
|
expect(world.busy).toBe(false);
|
|
expect(shook).toBe(true);
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
expect(world.poses().find((pose) => pose.id === 0)).toMatchObject({ x: 160, y: 144 });
|
|
});
|
|
|
|
it("rides the Goldenrod Dept. Store elevator through its menu and -1 warps", () => {
|
|
const [store1F, store3F, elevator] = ["11:11", "11:13", "11:17"];
|
|
const { world } = fixture([store1F, store3F, elevator]);
|
|
world.warpTo(store1F, 2, 1);
|
|
walkTo(world, 2, 0);
|
|
expect([world.mapId, world.x, world.y]).toEqual([elevator, 1, 3]);
|
|
// The arrival warp is numbered -1, so the entrance became the backup.
|
|
expect(world.state.backupWarp).toEqual({ map: store1F, warp: 4 });
|
|
walkTo(world, 3, 1);
|
|
world.move("up");
|
|
world.interact();
|
|
const menu = world.dialogue!;
|
|
const shown = settle(world, [3]);
|
|
expect(shown.join("\n")).toContain("Now on: 1F");
|
|
expect(menu.choice).toMatchObject({ kind: "menu" });
|
|
expect((menu.choice as { items: string[] }).items).toEqual([
|
|
"B1F", "1F", "2F", "3F", "4F", "5F", "6F",
|
|
]);
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
expect(world.state.backupWarp).toEqual({ map: store3F, warp: 3 });
|
|
walkTo(world, 2, 3);
|
|
world.move("down");
|
|
settle(world);
|
|
// Arrival at warp 3 (2, 0), then the doorway's forced step down.
|
|
expect([world.mapId, world.x, world.y]).toEqual([store3F, 2, 1]);
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
});
|
|
|
|
it("keeps the floor when the elevator menu is cancelled or the same floor is chosen", () => {
|
|
const [store1F, elevator] = ["11:11", "11:17"];
|
|
const { world } = fixture([store1F, elevator]);
|
|
world.warpTo(store1F, 2, 1);
|
|
walkTo(world, 2, 0);
|
|
walkTo(world, 3, 1);
|
|
world.move("up");
|
|
for (const answer of [-1, 1]) {
|
|
world.interact();
|
|
settle(world, [answer]);
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
expect(world.state.backupWarp).toEqual({ map: store1F, warp: 4 });
|
|
}
|
|
});
|
|
|
|
describe("wild encounters (CanEncounterWildMon and ChooseWildEncounter)", () => {
|
|
const UNION_CAVE_1F = "3:37";
|
|
const cave = () => {
|
|
const { assets, world } = fixture([UNION_CAVE_1F]);
|
|
world.state.party = [
|
|
generatePartyMon(assets.pokemon, {
|
|
species: 155, level: 20, item: 0, otId: 1, ot: "CHRIS", dvs: 0, caughtData: 0,
|
|
}),
|
|
];
|
|
return { assets, world };
|
|
};
|
|
/** Walk back and forth until a battle starts or the steps run out. */
|
|
const pace = (world: GameWorld, steps: number, directions: Direction[]) => {
|
|
for (let step = 0; step < steps && !world.battle; step++) {
|
|
world.move(directions[step % directions.length]);
|
|
for (let tick = 0; tick < 4 && !world.battle; tick++) world.tick();
|
|
}
|
|
return world.battle?.engine;
|
|
};
|
|
|
|
it("starts on any cave floor after the five-step cooldown", () => {
|
|
const { world } = cave();
|
|
world.warpTo(UNION_CAVE_1F, 9, 6);
|
|
const battle = pace(world, 12, ["right", "left"]);
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
expect(battle).toBeDefined();
|
|
expect(world.tilesWalked).toBe(5);
|
|
});
|
|
|
|
it("stays off with wildoff", () => {
|
|
const { world } = cave();
|
|
world.state.noWildEncounters = true;
|
|
world.warpTo(UNION_CAVE_1F, 9, 6);
|
|
expect(pace(world, 12, ["right", "left"])).toBeUndefined();
|
|
});
|
|
|
|
it("uses the water table's rate and slots while surfing", () => {
|
|
const { assets, world } = cave();
|
|
const sea = assets.waterWildMons(UNION_CAVE_1F)!;
|
|
expect(sea).toBeDefined();
|
|
world.state.playerState = 4; // PLAYER_SURF
|
|
world.restore({ ...world.save(), map: UNION_CAVE_1F, x: 7, y: 4, direction: "right", objects: [] });
|
|
const battle = pace(world, 12, ["right", "left"]);
|
|
expect(battle).toBeDefined();
|
|
// Random is always 0: the first slot, with no level variety.
|
|
expect(battle!.enemy.mon).toMatchObject({
|
|
species: sea.slots[0].species,
|
|
level: sea.slots[0].level,
|
|
});
|
|
});
|
|
});
|
|
|
|
it("draws overworld Pokémon from their party menu icons (GetMonSprite)", () => {
|
|
const { assets } = fixture([]);
|
|
const unown = assets.sprite(0x80)!; // SPRITE_UNOWN
|
|
expect(unown.walking).toBe(true);
|
|
expect(unown.palette).toBe(0);
|
|
expect(unown.graphics).toHaveLength(8 * 16);
|
|
expect(assets.sprite(0x80 + 35)).toBeUndefined();
|
|
expect(assets.sprite(103)).toBeUndefined();
|
|
});
|
|
|
|
it("surfing onto a whirlpool spins the player back with Script_ForcedMovement", () => {
|
|
const { assets, world } = fixture([]);
|
|
// Find a whirlpool with water on the side a surfer comes from.
|
|
let found: { map: string; x: number; y: number } | undefined;
|
|
for (let group = 1; group <= 26 && !found; group++)
|
|
for (let number = 1; number <= 40 && !found; number++) {
|
|
let map;
|
|
try {
|
|
map = assets.map(`${group}:${number}`);
|
|
} catch {
|
|
continue;
|
|
}
|
|
for (let y = 1; y < map.height - 1 && !found; y++)
|
|
for (let x = 0; x < map.width && !found; x++)
|
|
if (
|
|
assets.collision(map, x, y) === 0x24 &&
|
|
assets.permission(assets.collision(map, x, y + 1)) === 1 &&
|
|
assets.permission(assets.collision(map, x, y + 2)) === 1 &&
|
|
!map.objects.some((o) => o.x === x && (o.y === y + 1 || o.y === y + 2))
|
|
)
|
|
found = { map: map.id, x, y };
|
|
}
|
|
expect(found).toBeDefined();
|
|
const { map, x, y } = found!;
|
|
const ported = assets.isPorted.bind(assets);
|
|
assets.isPorted = (id) => ported(id) || id === map;
|
|
world.state.playerState = 4; // PLAYER_SURF
|
|
world.restore({ ...world.save(), map, x, y: y + 2, direction: "up", objects: [] });
|
|
hold(world, "up", 8);
|
|
expect([world.x, world.y]).toEqual([x, y + 1]);
|
|
hold(world, "up", 9);
|
|
expect([world.x, world.y]).toEqual([x, y]);
|
|
expect(world.activity).toBe("scene");
|
|
const directions = new Set<string>();
|
|
for (let tick = 0; tick < 120 && world.busy; tick++) {
|
|
world.tick();
|
|
directions.add(world.direction);
|
|
}
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
// .MovementData_up: spin, turn_in DOWN back off the whirlpool, spin,
|
|
// then face down.
|
|
expect([world.x, world.y, world.direction]).toEqual([x, y + 1, "down"]);
|
|
expect(directions.size).toBe(4);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!existsSync(romPath))("field moves with the local ROM", () => {
|
|
const TOTODILE = 158;
|
|
const partyWith = (assets: CrystalAssets, moves: number[], species = TOTODILE) => {
|
|
const mon = generatePartyMon(assets.pokemon, {
|
|
species, level: 30, item: 0, otId: 1, ot: "CHRIS", dvs: 0, caughtData: 0,
|
|
});
|
|
mon.moves = [...moves, 0, 0, 0, 0].slice(0, 4) as typeof mon.moves;
|
|
mon.pp = [10, 10, 10, 10];
|
|
mon.nickname = "SWIMMY";
|
|
return [mon];
|
|
};
|
|
/** Find a tile on some map, searching every group. */
|
|
const find = (
|
|
assets: CrystalAssets,
|
|
test: (map: ReturnType<CrystalAssets["map"]>, x: number, y: number) => boolean,
|
|
) => {
|
|
for (let group = 1; group <= 26; group++)
|
|
for (let number = 1; number <= 60; number++) {
|
|
let map;
|
|
try {
|
|
map = assets.map(`${group}:${number}`);
|
|
} catch {
|
|
continue;
|
|
}
|
|
for (let y = 1; y < map.height - 1; y++)
|
|
for (let x = 1; x < map.width - 1; x++) if (test(map, x, y)) return { map: map.id, x, y };
|
|
}
|
|
throw new Error("No suitable tile.");
|
|
};
|
|
|
|
it("surfs from New Bark Town's shore through AskSurfScript and lands again", () => {
|
|
const { assets, world } = fixture([]);
|
|
world.state.party = partyWith(assets, [0x39]); // SURF
|
|
world.state.flags.add(30); // ENGINE_FOGBADGE
|
|
world.state.scenes["24:4"] = 1;
|
|
world.restore({ ...world.save(), map: "24:4", x: 17, y: 7, direction: "right", objects: [] });
|
|
world.interact();
|
|
const shown = settle(world).join("\n");
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
expect(shown).toContain("SWIMMY used\nSURF!");
|
|
expect(world.state.playerState).toBe(4);
|
|
expect([world.x, world.y]).toEqual([18, 7]);
|
|
// The surfer sprite (SPRITE_SURF) replaces Chris.
|
|
expect(world.poses().find((pose) => pose.id === 0)?.sprite).toBe(0x53);
|
|
// Water saves and reloads with the surfer on it.
|
|
world.restore(world.save());
|
|
expect(world.state.playerState).toBe(4);
|
|
world.move("left");
|
|
expect([world.x, world.y, world.state.playerState]).toEqual([17, 7, 0]);
|
|
});
|
|
|
|
it("does not offer Surf without the Fog Badge or a Pokémon that knows it", () => {
|
|
const { assets, world } = fixture([]);
|
|
world.state.party = partyWith(assets, [0x39]);
|
|
world.state.scenes["24:4"] = 1;
|
|
world.restore({ ...world.save(), map: "24:4", x: 17, y: 7, direction: "right", objects: [] });
|
|
world.interact();
|
|
expect(world.dialogue).toBeUndefined();
|
|
world.state.flags.add(30);
|
|
world.state.party = partyWith(assets, [0x0f]);
|
|
world.interact();
|
|
expect(world.dialogue).toBeUndefined();
|
|
});
|
|
|
|
it("cuts a tree with AskCutScript until the map loads again", () => {
|
|
const { assets, world } = fixture([]);
|
|
const spot = find(assets, (map, x, y) =>
|
|
[1, 2, 3, 12, 25].includes(map.tileset) &&
|
|
assets.collision(map, x, y) === 0x12 &&
|
|
assets.permission(assets.collision(map, x, y + 1)) === 0 &&
|
|
!map.objects.some((o) => o.x === x && o.y === y + 1) &&
|
|
map.callbacks.length === 0,
|
|
);
|
|
const ported = assets.isPorted.bind(assets);
|
|
assets.isPorted = (id) => ported(id) || id === spot.map;
|
|
world.state.party = partyWith(assets, [0x0f]); // CUT
|
|
world.state.flags.add(28); // ENGINE_HIVEBADGE
|
|
world.warpTo(spot.map, spot.x, spot.y + 1);
|
|
world.restore({ ...world.save(), direction: "up" });
|
|
const before = world.map.blocks.slice();
|
|
world.interact();
|
|
const shown = settle(world).join("\n");
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
expect(shown).toContain("SWIMMY used\nCUT!");
|
|
expect(world.map.blocks).not.toEqual(before);
|
|
expect(assets.permission(assets.collision(world.map, spot.x, spot.y))).toBe(0);
|
|
world.move("up");
|
|
expect([world.x, world.y]).toEqual([spot.x, spot.y]);
|
|
world.warpTo(spot.map, spot.x, spot.y + 1);
|
|
expect(world.map.blocks).toEqual(before);
|
|
});
|
|
|
|
it("explains a Cut tree without the move", () => {
|
|
const { assets, world } = fixture([]);
|
|
const spot = find(assets, (map, x, y) =>
|
|
assets.collision(map, x, y) === 0x12 &&
|
|
assets.permission(assets.collision(map, x, y + 1)) === 0 &&
|
|
!map.objects.some((o) => o.x === x && o.y === y + 1) &&
|
|
map.callbacks.length === 0,
|
|
);
|
|
const ported = assets.isPorted.bind(assets);
|
|
assets.isPorted = (id) => ported(id) || id === spot.map;
|
|
world.state.party = partyWith(assets, [0x21]);
|
|
world.warpTo(spot.map, spot.x, spot.y + 1);
|
|
world.restore({ ...world.save(), direction: "up" });
|
|
world.interact();
|
|
const shown = settle(world).join("\n");
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
expect(shown.length).toBeGreaterThan(0);
|
|
expect(assets.permission(assets.collision(world.map, spot.x, spot.y))).not.toBe(0);
|
|
});
|
|
|
|
it("uses Strength through AskStrengthScript and pushes a boulder", () => {
|
|
const { assets, world } = fixture([]);
|
|
// A Strength boulder object with floor below it and two floor tiles above.
|
|
const spot = find(assets, (map, x, y) => {
|
|
const boulder = map.objects.find(
|
|
(o) => o.x === x && o.y === y && o.movement === 25 && o.eventFlag === 0xffff,
|
|
);
|
|
return Boolean(
|
|
boulder &&
|
|
map.callbacks.length === 0 &&
|
|
[y + 1, y - 1, y - 2].every(
|
|
(row) =>
|
|
assets.permission(assets.collision(map, x, row)) === 0 &&
|
|
!map.objects.some((o) => o !== boulder && o.x === x && o.y === row),
|
|
),
|
|
);
|
|
});
|
|
const ported = assets.isPorted.bind(assets);
|
|
assets.isPorted = (id) => ported(id) || id === spot.map;
|
|
world.state.party = partyWith(assets, [0x46]); // STRENGTH
|
|
world.state.flags.add(29); // ENGINE_PLAINBADGE
|
|
world.warpTo(spot.map, spot.x, spot.y + 1);
|
|
world.restore({ ...world.save(), direction: "up" });
|
|
world.interact();
|
|
const shown = settle(world).join("\n");
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
expect(shown).toContain("SWIMMY used\nSTRENGTH!");
|
|
expect(world.state.flags.has(24)).toBe(true); // ENGINE_STRENGTH_ACTIVE
|
|
for (let tick = 0; tick < 20; tick++) world.tick("up");
|
|
const boulder = world.objects().find((o) => o.movement === 25 && o.x === spot.x);
|
|
expect(boulder?.y).toBe(spot.y - 1);
|
|
// Talking to it again: "Boulders may now be moved!"
|
|
settle(world);
|
|
});
|
|
|
|
it("rides the Bicycle on a route and gets off indoors", () => {
|
|
const { assets, world } = fixture([]);
|
|
world.state.party = partyWith(assets, [0x21]);
|
|
world.state.bag.keyItems.push(7); // BICYCLE
|
|
world.state.scenes["24:4"] = 1;
|
|
world.restore({ ...world.save(), map: "24:4", x: 13, y: 7, direction: "down", objects: [] });
|
|
expect(world.useBicycle()).toBe(true);
|
|
const shown = settle(world).join("\n");
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
expect(shown).toContain("got on the\nBICYCLE.");
|
|
expect(world.state.playerState).toBe(1);
|
|
expect(world.poses().find((pose) => pose.id === 0)?.sprite).toBe(2); // SPRITE_CHRIS_BIKE
|
|
// Turn left (four ticks), then four ticks per tile on the bike.
|
|
for (let tick = 0; tick < 4; tick++) world.tick("left");
|
|
world.tick("left");
|
|
expect(world.x).toBe(12);
|
|
for (let tick = 0; tick < 4; tick++) world.tick("left");
|
|
expect(world.x).toBe(11);
|
|
// Entering the house (indoors) gets the player off (CheckUpdatePlayerSprite).
|
|
settle(world);
|
|
walkTo(world, 13, 5);
|
|
expect(world.mapId).toBe("24:6");
|
|
expect(world.state.playerState).toBe(0);
|
|
expect(world.useBicycle()).toBe(false);
|
|
});
|
|
|
|
it("offers Whirlpool and Waterfall only with the move and badge", () => {
|
|
const { assets, world } = fixture([]);
|
|
world.state.party = partyWith(assets, [0x39]);
|
|
const whirlpool = find(assets, (map, x, y) =>
|
|
assets.collision(map, x, y) === 0x24 &&
|
|
assets.permission(assets.collision(map, x, y + 1)) === 1 &&
|
|
!map.objects.some((o) => o.x === x && o.y === y + 1),
|
|
);
|
|
const ported = assets.isPorted.bind(assets);
|
|
assets.isPorted = (id) => ported(id) || id === whirlpool.map;
|
|
world.state.playerState = 4;
|
|
world.restore({ ...world.save(), map: whirlpool.map, x: whirlpool.x, y: whirlpool.y + 1, direction: "up", objects: [] });
|
|
world.interact();
|
|
const mighty = settle(world).join("\n");
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
expect(mighty.length).toBeGreaterThan(0);
|
|
world.state.party = partyWith(assets, [0x39, 250]); // SURF, WHIRLPOOL
|
|
world.state.flags.add(33); // ENGINE_GLACIERBADGE
|
|
const before = world.map.blocks.slice();
|
|
world.interact();
|
|
const used = settle(world).join("\n");
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
expect(used).toContain("used\nWHIRLPOOL!");
|
|
expect(world.map.blocks).not.toEqual(before);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!existsSync(romPath))("Strength boulders and stone tables with the local ROM", () => {
|
|
it("drops a boulder pushed into Ice Path B1F's hole through its stone table script", () => {
|
|
const ICE_PATH_B1F = "3:62";
|
|
const EVENT_BOULDER_IN_ICE_PATH_2A = 1806;
|
|
const { assets, world } = fixture([ICE_PATH_B1F]);
|
|
const mon = generatePartyMon(assets.pokemon, {
|
|
species: 158, level: 30, item: 0, otId: 1, ot: "CHRIS", dvs: 0, caughtData: 0,
|
|
});
|
|
world.state.party = [mon];
|
|
world.warpTo(ICE_PATH_B1F, 4, 5);
|
|
world.state.flags.add(24); // ENGINE_STRENGTH_ACTIVE
|
|
world.state.events.add(EVENT_BOULDER_IN_ICE_PATH_2A);
|
|
world.restore({
|
|
...world.save(),
|
|
x: 4,
|
|
y: 5,
|
|
direction: "down",
|
|
objects: [{ id: 3, x: 4, y: 6, direction: "down" }],
|
|
});
|
|
world.state.flags.add(24);
|
|
expect(world.objects().some((o) => o.id === 3)).toBe(true);
|
|
world.tick("down");
|
|
const shown: string[] = [];
|
|
for (let tick = 0; tick < 40 && world.activity !== "scene"; tick++) world.tick();
|
|
expect(world.activity).toBe("scene");
|
|
shown.push(...settle(world));
|
|
expect(world.notice, world.diagnostic).toBe("");
|
|
expect(shown.join("\n")).toMatch(/fell\s+through/i);
|
|
expect(world.objects().some((o) => o.id === 3)).toBe(false);
|
|
expect(world.state.events.has(EVENT_BOULDER_IN_ICE_PATH_2A)).toBe(false);
|
|
});
|
|
});
|