Open the Radio Tower takeover, Ice Path, Blackthorn and Dragon's Den (#32)
The Rocket-held Radio Tower, the Underground's switch room and warehouse, Ice Path, Blackthorn City and its gym, Routes 45 and 46, Dark Cave's Blackthorn entrance, Dragon's Den and the Dragon Shrine are open. The story plays through the Basement Key, the switches and the director, the Clear Bell, Ice Path's falling boulder, Clair's gym and the shrine's test for the RISINGBADGE, TM24 and the elder's Dratini. New specials: GiveDratini and MoveDeletion. A save keeps an object that a script made disappear (or appear against its flag) as Continue does, so the Radio Tower director no longer blocks the 5F stairs after a reload. A whiteout from a battle started in an scall no longer refuses. The playthrough driver pushes boulders into their holes (planned breadth first), clears whirlpools, chooses the move a TM replaces and heals with the strongest potion; the run gains six stages to the Rising Badge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
061ca4d59d
commit
9c3579937c
15 files changed
+855
-43
No files matched your search
+123
-13
@@ -8,7 +8,7 @@ import type { Direction } from "../src/game/world/types";
|
||||
import { C } from "../src/game/battle/constants";
|
||||
import { applyTypeMatchups } from "../src/game/battle/battle";
|
||||
import { BattleData } from "../src/game/battle/data";
|
||||
import { afterRestore, findPath, landing } from "./world-driver";
|
||||
import { afterRestore, findPath, isWhirlpool, landing } from "./world-driver";
|
||||
|
||||
const STEP: Record<Direction, [number, number]> = {
|
||||
up: [0, -1],
|
||||
@@ -93,8 +93,8 @@ export class Playthrough {
|
||||
* the bag through the battle's own item action (BattleMenu_Pack), as a
|
||||
* cautious player who restocked before a hard fight would: a carried
|
||||
* Awakening wakes the active Pokémon before Sleep/Dream Eater can lock
|
||||
* it out of a whole fight, and a carried Super Potion heals it below
|
||||
* half HP. New moves are learned when there is room, and other
|
||||
* it out of a whole fight, and the strongest carried potion heals it
|
||||
* below half HP. New moves are learned when there is room, and other
|
||||
* questions take the queued answers or their defaults. */
|
||||
play = (answers: (number | string)[] = []) => {
|
||||
const world = this.world,
|
||||
@@ -112,9 +112,10 @@ export class Playthrough {
|
||||
const ball = world.liveState.bag.balls.findIndex(
|
||||
(slot) => slot.item === C.POKE_BALL || slot.item === C.GREAT_BALL,
|
||||
);
|
||||
const potion = world.liveState.bag.items.findIndex(
|
||||
(slot) => slot.item === C.SUPER_POTION,
|
||||
);
|
||||
// The strongest potion carried: Max, then Hyper, then Super.
|
||||
const potion = [C.MAX_POTION, C.HYPER_POTION, C.SUPER_POTION]
|
||||
.map((item) => world.liveState.bag.items.findIndex((slot) => slot.item === item))
|
||||
.find((index) => index >= 0) ?? -1;
|
||||
const awakening = world.liveState.bag.items.findIndex(
|
||||
(slot) => slot.item === C.AWAKENING,
|
||||
);
|
||||
@@ -142,7 +143,11 @@ export class Playthrough {
|
||||
mon.hp * 2 < mon.maxHp &&
|
||||
potion >= 0
|
||||
)
|
||||
world.battleAction({ action: "item", item: C.SUPER_POTION, slot: potion });
|
||||
world.battleAction({
|
||||
action: "item",
|
||||
item: world.liveState.bag.items[potion].item,
|
||||
slot: potion,
|
||||
});
|
||||
else
|
||||
world.battleAction({
|
||||
action: "fight",
|
||||
@@ -240,18 +245,27 @@ export class Playthrough {
|
||||
);
|
||||
};
|
||||
/** Walk and Surf to a tile: where a step leaves the shore, face the
|
||||
* water and answer AskSurfScript's question with YES. */
|
||||
sail = (x: number, y: number) => {
|
||||
* water and answer AskSurfScript's question with YES; a whirlpool in
|
||||
* the way is faced and cleared (TryWhirlpoolOW, then YES) when
|
||||
* `whirlpools` is set. */
|
||||
sail = (x: number, y: number, { whirlpools = false } = {}) => {
|
||||
const world = this.world,
|
||||
map = world.mapId;
|
||||
for (let attempt = 0; attempt < 40; attempt++) {
|
||||
this.play();
|
||||
expect(world.mapId, `sailing to ${x},${y}`).toBe(map);
|
||||
if (world.x === x && world.y === y) return;
|
||||
for (const direction of findPath(world, x, y, { water: true })) {
|
||||
for (const direction of findPath(world, x, y, { water: true, whirlpools })) {
|
||||
const [dx, dy] = STEP[direction];
|
||||
const from = [world.x, world.y];
|
||||
const ahead = world.assets.collision(world.map, from[0] + dx, from[1] + dy);
|
||||
if (isWhirlpool(ahead)) {
|
||||
// Stepping onto a whirlpool spins the player back: turn first.
|
||||
if (world.save().direction !== direction) world.move(direction);
|
||||
world.interact();
|
||||
this.play([1]);
|
||||
break;
|
||||
}
|
||||
if (world.state.playerState !== PLAYER_SURF && world.assets.permission(ahead) === 1) {
|
||||
world.move(direction); // only turns to face the water
|
||||
world.interact();
|
||||
@@ -596,8 +610,8 @@ export class Playthrough {
|
||||
this.play();
|
||||
};
|
||||
/** PACK, the TM/HM pocket's `item`, USE, YES, then party slot `slot`;
|
||||
* a full move list forgets the first move. */
|
||||
teach = (item: number, slot: number) => {
|
||||
* a full move list forgets move `forget` (the first by default). */
|
||||
teach = (item: number, slot: number, forget = 0) => {
|
||||
const world = this.world;
|
||||
expect(world.openStartMenu()).toBe(true);
|
||||
this.choose("PACK");
|
||||
@@ -612,7 +626,7 @@ export class Playthrough {
|
||||
expect(this.toChoice().kind).toBe("yesno");
|
||||
world.choose(1);
|
||||
this.toChoice();
|
||||
world.choose(0);
|
||||
world.choose(forget);
|
||||
}
|
||||
for (let i = 0; i < 10 && world.dialogue?.choice?.kind !== "pack"; i++)
|
||||
this.toChoice();
|
||||
@@ -623,6 +637,102 @@ export class Playthrough {
|
||||
}
|
||||
this.play();
|
||||
};
|
||||
/** Push Strength boulder `id` one tile in `direction` from its far
|
||||
* side: the first bump only turns the player, a boulder Strength is not
|
||||
* yet active for asks first (AskStrengthScript, then YES), and the
|
||||
* boulder moves on its own movement function a few frames later. */
|
||||
push = (id: number, direction: Direction) => {
|
||||
const world = this.world;
|
||||
const boulder = () => world.objects().find((object) => object.id === id);
|
||||
const from = boulder()!;
|
||||
const [dx, dy] = STEP[direction];
|
||||
this.walk(from.x - dx, from.y - dy);
|
||||
const moved = () => {
|
||||
const now = boulder();
|
||||
return !now || now.x !== from.x || now.y !== from.y;
|
||||
};
|
||||
for (let attempt = 0; attempt < 3 && !moved(); attempt++) {
|
||||
world.move(direction);
|
||||
for (let tick = 0; tick < 40; tick++) world.tick();
|
||||
this.play();
|
||||
if (moved()) break;
|
||||
world.interact();
|
||||
this.play([1]);
|
||||
}
|
||||
expect(moved(), `boulder ${id} pushed ${direction}`).toBe(true);
|
||||
};
|
||||
/** Push Strength boulders into their holes: each goal boulder must reach
|
||||
* its own stone-table warp (a boulder on another pit only stops there),
|
||||
* and `movable` boulders may be pushed out of the way. The pushes are
|
||||
* planned breadth first over boulder positions and the player's reachable
|
||||
* area, then made one by one with `push`. */
|
||||
boulders = (goals: { id: number; x: number; y: number }[], movable: number[] = []) => {
|
||||
const world = this.world,
|
||||
map = world.map;
|
||||
const ids = [...goals.map((goal) => goal.id), ...movable];
|
||||
const land = (x: number, y: number) =>
|
||||
x >= 0 &&
|
||||
y >= 0 &&
|
||||
x < map.width &&
|
||||
y < map.height &&
|
||||
world.assets.permission(world.assets.collision(map, x, y)) === 0;
|
||||
const warps = new Set(map.warps.map((warp) => `${warp.x},${warp.y}`));
|
||||
const fixed = new Set(
|
||||
world
|
||||
.objects()
|
||||
.filter((object) => !ids.includes(object.id))
|
||||
.map((object) => `${object.x},${object.y}`),
|
||||
);
|
||||
const at = (id: number) => {
|
||||
const object = world.objects().find((candidate) => candidate.id === id);
|
||||
return object ? `${object.x},${object.y}` : "gone";
|
||||
};
|
||||
/** The player's reachable tiles, walking around boulders and pits. */
|
||||
const reach = (x: number, y: number, boulders: string[]) => {
|
||||
const blocked = new Set([...boulders, ...fixed]);
|
||||
const seen = new Set([`${x},${y}`]);
|
||||
const queue = [[x, y]];
|
||||
for (let i = 0; i < queue.length; i++)
|
||||
for (const [dx, dy] of Object.values(STEP)) {
|
||||
const nx = queue[i][0] + dx,
|
||||
ny = queue[i][1] + dy,
|
||||
key = `${nx},${ny}`;
|
||||
if (seen.has(key) || !land(nx, ny) || blocked.has(key) || warps.has(key)) continue;
|
||||
seen.add(key);
|
||||
queue.push([nx, ny]);
|
||||
}
|
||||
return seen;
|
||||
};
|
||||
type Node = { boulders: string[]; x: number; y: number; pushes: [number, Direction][] };
|
||||
const queue: Node[] = [{ boulders: ids.map(at), x: world.x, y: world.y, pushes: [] }];
|
||||
const seen = new Set<string>();
|
||||
let plan: Node | undefined;
|
||||
for (let i = 0; i < queue.length && i < 500_000 && !plan; i++) {
|
||||
const node = queue[i];
|
||||
const area = reach(node.x, node.y, node.boulders);
|
||||
const key = `${node.boulders.join(";")}|${[...area].sort()[0]}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
if (goals.every((_, index) => node.boulders[index] === "gone")) plan = node;
|
||||
node.boulders.forEach((boulder, index) => {
|
||||
if (boulder === "gone") return;
|
||||
const [bx, by] = boulder.split(",").map(Number);
|
||||
for (const [direction, [dx, dy]] of Object.entries(STEP) as [Direction, [number, number]][]) {
|
||||
const target = `${bx + dx},${by + dy}`;
|
||||
if (!area.has(`${bx - dx},${by - dy}`)) continue;
|
||||
if (!land(bx + dx, by + dy) || node.boulders.includes(target) || fixed.has(target)) continue;
|
||||
const goal = goals[index];
|
||||
const falls = goal && goal.x === bx + dx && goal.y === by + dy;
|
||||
if (warps.has(target) && !falls) continue;
|
||||
const boulders = [...node.boulders];
|
||||
boulders[index] = falls ? "gone" : target;
|
||||
queue.push({ boulders, x: bx, y: by, pushes: [...node.pushes, [ids[index], direction]] });
|
||||
}
|
||||
});
|
||||
}
|
||||
expect(plan, "a way to push the boulders into their holes").toBeDefined();
|
||||
for (const [id, direction] of plan!.pushes) this.push(id, direction);
|
||||
};
|
||||
/** Cut the tree at (x, y) from a walkable side (TryCutOW, then YES). */
|
||||
cut = (x: number, y: number) => {
|
||||
const world = this.world;
|
||||
|
||||
Reference in new issue
Block a user