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:
kawaandClaude Opus 5.5 committed 2026-10-13 14:19:02 +02:00
1 parent 061ca4d59d
commit 9c3579937c
15 files changed
+855 -43

No files matched your search

+30
View File
@@ -1068,6 +1068,17 @@ export const JOHTO = {
MagikarpHouseSign: 0xfbcd2,
FindPartyMonThatSpecies: 0xc276,
PlayCurMonCry: 0xc472,
GiveDratini: 0x8b170,
"GiveDratini.Movesets": 0x8b1c4,
MoveDeletion: 0x2c547,
"MoveDeletion.DeleterIntroText": 0x2c5ef,
"MoveDeletion.DeleterAskWhichMonText": 0x2c5f4,
"MoveDeletion.DeleterAskWhichMoveText": 0x2c5ea,
"MoveDeletion.AskDeleteMoveText": 0x2c5d6,
"MoveDeletion.DeleterForgotMoveText": 0x2c5db,
"MoveDeletion.MailEggText": 0x2c5e0,
"MoveDeletion.DeleterNoComeAgainText": 0x2c5e5,
"MoveDeletion.MoveKnowsOneText": 0x2c5d1,
"CheckMagikarpLength.MagikarpGuruMeasureText": 0xfbba9,
"MagikarpHouseSign.KarpGuruRecordText": 0xfbce8,
"GiveShuckle": 0x7305,
@@ -1317,6 +1328,25 @@ export const NATIVE_MAPS: Record<string, string> = {
"3:58": "Mt. Mortar",
"3:59": "Mt. Mortar 2F",
"3:60": "Mt. Mortar B1F",
"3:61": "Ice Path 1F",
"3:62": "Ice Path B1F",
"3:63": "Ice Path B2F",
"3:64": "Ice Path B2F",
"3:65": "Ice Path B3F",
"3:79": "Dark Cave",
"3:80": "Dragon’s Den 1F",
"3:81": "Dragon’s Den B1F",
"3:82": "Dragon Shrine",
"5:1": "Blackthorn Gym 1F",
"5:2": "Blackthorn Gym 2F",
"5:3": "Dragon speech house",
"5:4": "Emy’s house",
"5:5": "Blackthorn Mart",
"5:6": "Pokémon Center",
"5:7": "Move Deleter’s house",
"5:8": "Route 45",
"5:9": "Route 46",
"5:10": "Blackthorn City",
"9:1": "Hidden Power house",
"9:2": "Fishing guru’s house",
"9:3": "Route 43 Mahogany gate",
+69
View File
@@ -0,0 +1,69 @@
import { JOHTO, RAM } from "../rom/profile";
import type { Gen, SpecialContext } from "./special-context";
const DRATINI = 147;
/** GiveDratini (engine/events/dratini.asm): wScriptVar 0 (the shrine quiz
* answered without a mistake) gives the last Dratini in the party WRAP,
* THUNDER WAVE, TWISTER and EXTREMESPEED, 1 its normal level 15 moves;
* each move's PP is its base PP. Other values change nothing. */
export function giveDratini(c: SpecialContext, value: number) {
if (value >= 2) return;
const party = c.state.party;
// .CheckForDratini: from the last party slot backwards, by species.
let slot = party.length - 1;
while (slot >= 0 && party[slot].species !== DRATINI) slot--;
if (slot < 0) return;
const mon = party[slot];
// .Movesets: four moves and a 0 each; .GiveMoves stops at the 0.
const set = JOHTO["GiveDratini.Movesets"] + value * 5;
for (let i = 0; i < 4; i++) {
const move = c.reader.byte(set + i);
if (!move) break;
mon.moves[i] = move;
mon.pp[i] = c.data.move(move).pp;
}
}
/** MoveDeletion (engine/events/move_deleter.asm): the Move Deleter. A
* Pokémon with one move, an Egg and every refusal end with their own
* text; otherwise the chosen move goes and the later moves and their PP
* bytes (PP Ups included) move up a slot. */
export function* moveDeletion(c: SpecialContext): Gen {
const say = function* (address: number): Gen {
c.write(address);
yield* c.flush();
};
c.write(JOHTO["MoveDeletion.DeleterIntroText"]);
if (!(yield { kind: "yesno", pages: c.take() }))
return yield* say(JOHTO["MoveDeletion.DeleterNoComeAgainText"]);
c.write(JOHTO["MoveDeletion.DeleterAskWhichMonText"]);
const slot = yield* c.selectMon();
if (slot < 0) return yield* say(JOHTO["MoveDeletion.DeleterNoComeAgainText"]);
const mon = c.state.party[slot];
if (mon.egg) return yield* say(JOHTO["MoveDeletion.MailEggText"]);
if (!mon.moves[1]) return yield* say(JOHTO["MoveDeletion.MoveKnowsOneText"]);
c.write(JOHTO["MoveDeletion.DeleterAskWhichMoveText"]);
const moves = mon.moves.filter(Boolean);
// ChooseMoveToDelete: the move list, A on a move or B to cancel.
const index = yield {
kind: "menu",
pages: c.take(),
items: moves.map((move) => c.data.moveName(move)),
selected: 0,
};
if (typeof index !== "number" || index < 0 || index >= moves.length)
return yield* say(JOHTO["MoveDeletion.DeleterNoComeAgainText"]);
c.strings[RAM.stringBuffer1] = c.data.moveName(moves[index]);
c.write(JOHTO["MoveDeletion.AskDeleteMoveText"]);
if (!(yield { kind: "yesno", pages: c.take() }))
return yield* say(JOHTO["MoveDeletion.DeleterNoComeAgainText"]);
// .DeleteMove: shift the moves and PP after the slot up; clear the last.
for (let i = index; i < 3; i++) {
mon.moves[i] = mon.moves[i + 1];
mon.pp[i] = mon.pp[i + 1];
}
mon.moves[3] = 0;
mon.pp[3] = 0;
yield* say(JOHTO["MoveDeletion.DeleterForgotMoveText"]);
}
+15 -3
View File
@@ -72,6 +72,7 @@ import { unownPuzzle } from "./unown-puzzle";
import { displayUnownWords, hoOhChamber, omanyteChamber } from "./ruins";
import { slotMachine } from "./slot-machine";
import { cardFlip } from "./card-flip";
import { giveDratini, moveDeletion } from "./blackthorn";
import {
checkMagikarpLength,
findPartyMonThatSpecies,
@@ -1584,9 +1585,14 @@ export class ScriptRun {
};
} else throw new UnsupportedScript(offset, opcode, " (map setup)");
// The script resumes where it stopped; a caller's stack is not
// carried over to the new map here.
if (stack.length)
throw new UnsupportedScript(offset, opcode, " (inside a call)");
// carried over to the new map here. Script_Whiteout's newloadmap
// can run inside a call (a battle lost in an scall'd script jumps
// there without unwinding), but its endall then drops the stack.
if (stack.length) {
if (r.byte(p) !== 0x93)
throw new UnsupportedScript(offset, opcode, " (inside a call)");
stack.length = 0;
}
// MAPSETUP_DOOR's bytes fall through into MAPSETUP_TRAIN's
// UpdateRoamMons; MAPSETUP_TELEPORT and MAPSETUP_FLY fall
// through into MAPSETUP_WARP's bytes, but only after their own
@@ -3420,6 +3426,12 @@ export class ScriptRun {
this.specialContext(pages, take),
this.state.magikarpRecord,
);
case JOHTO.GiveDratini:
giveDratini(this.specialContext(pages, take), value);
return value;
case JOHTO.MoveDeletion:
yield* moveDeletion(this.specialContext(pages, take));
return value;
case JOHTO.MagikarpHouseSign:
magikarpHouseSign(this.specialContext(pages, take), this.state.magikarpRecord);
return value;
+41 -11
View File
@@ -149,8 +149,18 @@ export interface NativeSave {
clock: { minuteOffset: number; weekdayOffset: number; dst: boolean };
/** The introduction has not finished: it starts again on load. */
intro: boolean;
/** Current-map objects moved or turned by scripts, as saved object structs. */
objects: { id: number; x: number; y: number; direction: Direction }[];
/** Current-map objects moved or turned by scripts, as saved object structs.
* `hidden` marks an object whose flags would show it but that a script
* made disappear (one without an event flag); `shown`, one that appeared
* although its flag is set. Continue keeps the object structs as saved. */
objects: {
id: number;
x: number;
y: number;
direction: Direction;
hidden?: true;
shown?: true;
}[];
/** wPlayerID, drawn at a new game. */
playerId: number;
party: PartyMon[];
@@ -269,6 +279,9 @@ interface Actor {
hidden: boolean;
fixedFacing: boolean;
moved: boolean;
/** An appear or disappear this visit: its visibility, not its flag,
* is what a save keeps. */
toggled?: boolean;
/** OBJECT_ACTION_FISHING, from fish_cast_rod or fish_got_bite until
* PutTheRodAway: the fishing frames replace the standing ones. */
fishing?: boolean;
@@ -2548,6 +2561,7 @@ export class GameWorld {
script.run.state.events.delete(object.eventFlag);
else script.run.state.events.add(object.eventFlag);
}
this.actor(object).toggled = true;
if (effect.kind === "appear") {
this.actor(object).hidden = false;
if (this.assets.sprite(this.spriteOf(object)))
@@ -4507,14 +4521,25 @@ export class GameWorld {
rivalName: state.rivalName,
clock: { ...state.clock },
intro: this.introPending,
objects: [...this.actors]
.filter(([, actor]) => actor.moved)
.map(([id, actor]) => ({
id,
x: actor.x,
y: actor.y,
direction: actor.direction,
}))
objects: this.map.objects
.flatMap((object) => {
const actor = this.actors.get(object.id);
const spawned = this.spawned.has(object.id),
flagged = this.visible(object, state),
differs = Boolean(actor?.toggled) && spawned !== flagged;
if (!actor?.moved && !differs) return [];
const { x, y, direction } = actor ?? spawnActor(object);
return [
{
id: object.id,
x,
y,
direction,
...(differs && !spawned && { hidden: true as const }),
...(differs && spawned && { shown: true as const }),
},
];
})
.sort((a, b) => a.id - b.id),
playerId: this.playerId,
party: structuredClone(state.party),
@@ -4746,7 +4771,9 @@ export class GameWorld {
!map.objects.some((candidate) => candidate.id === object.id) ||
!integer(object.x, 0, 127) ||
!integer(object.y, 0, 127) ||
!DIRECTION_ORDER.includes(object.direction)
!DIRECTION_ORDER.includes(object.direction) ||
(Object.hasOwn(object, "hidden") && object.hidden !== true) ||
(Object.hasOwn(object, "shown") && object.shown !== true)
)
throw new Error(
"Invalid native save. A saved object is not on this map.",
@@ -4853,6 +4880,9 @@ export class GameWorld {
// Saved objects were placed by scripts; they roam from there.
if (actor.npc)
Object.assign(actor.npc, { homeX: object.x, homeY: object.y });
if (object.hidden) this.spawned.delete(object.id);
if (object.shown) this.spawned.add(object.id);
if (object.hidden || object.shown) actor.toggled = true;
}
this.player = {
x: save.x,