Open Tin Tower and Whirl Islands: Suicune, Ho-Oh and Lugia (#32)

Tin Tower's floors and roof and the Whirl Islands are open. The Clear Bell
gets the player past the sages to Suicune in Tin Tower; after the League,
the Rainbow Wing leads up to Ho-Oh, and the Silver Wing to Lugia.

New specials: MonCheck and BeastsCheck (CheckOwnMonAnywhere, with its
five-letter OT comparison and no Day-Care search).

The playthrough driver plans ledge jumps for the later stages, waterfall
descents, warp panels and Surf between warps; the run gains two stages,
Routes 45 and 46 back to Ecruteak and Tin Tower's Suicune.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kawaandClaude Opus 5.5 committed 2026-10-13 14:47:45 +02:00
1 parent 9c3579937c
commit 3fbc6798eb
10 files changed
+369 -21

No files matched your search

+20
View File
@@ -1069,6 +1069,8 @@ export const JOHTO = {
FindPartyMonThatSpecies: 0xc276,
PlayCurMonCry: 0xc472,
GiveDratini: 0x8b170,
BeastsCheck: 0x4a6e8,
MonCheck: 0x4a711,
"GiveDratini.Movesets": 0x8b1c4,
MoveDeletion: 0x2c547,
"MoveDeletion.DeleterIntroText": 0x2c5ef,
@@ -1328,6 +1330,24 @@ export const NATIVE_MAPS: Record<string, string> = {
"3:58": "Mt. Mortar",
"3:59": "Mt. Mortar 2F",
"3:60": "Mt. Mortar B1F",
"3:4": "Tin Tower 1F",
"3:5": "Tin Tower 2F",
"3:6": "Tin Tower 3F",
"3:7": "Tin Tower 4F",
"3:8": "Tin Tower 5F",
"3:9": "Tin Tower 6F",
"3:10": "Tin Tower 7F",
"3:11": "Tin Tower 8F",
"3:12": "Tin Tower 9F",
"15:12": "Tin Tower roof",
"3:66": "Whirl Islands",
"3:67": "Whirl Islands",
"3:68": "Whirl Islands",
"3:69": "Whirl Islands",
"3:70": "Whirl Islands",
"3:71": "Whirl Islands B1F",
"3:72": "Whirl Islands B2F",
"3:73": "Lugia’s chamber",
"3:61": "Ice Path 1F",
"3:62": "Ice Path B1F",
"3:63": "Ice Path B2F",
+5
View File
@@ -73,6 +73,7 @@ import { displayUnownWords, hoOhChamber, omanyteChamber } from "./ruins";
import { slotMachine } from "./slot-machine";
import { cardFlip } from "./card-flip";
import { giveDratini, moveDeletion } from "./blackthorn";
import { beastsCheck, monCheck } from "./tin-tower";
import {
checkMagikarpLength,
findPartyMonThatSpecies,
@@ -3426,6 +3427,10 @@ export class ScriptRun {
this.specialContext(pages, take),
this.state.magikarpRecord,
);
case JOHTO.MonCheck:
return monCheck(this.specialContext(pages, take), value);
case JOHTO.BeastsCheck:
return beastsCheck(this.specialContext(pages, take));
case JOHTO.GiveDratini:
giveDratini(this.specialContext(pages, take), value);
return value;
+39
View File
@@ -0,0 +1,39 @@
import type { SpecialContext } from "./special-context";
const RAIKOU = 243,
ENTEI = 244,
SUICUNE = 245;
/** CheckOwnMon's OT comparison: the first five characters, stopping at a
* terminator both names share (NAME_LENGTH_JAPANESE - 2 compares, then
* one more). */
const sameOT = (ot: string, player: string) => {
const a = `${ot}@`,
b = `${player}@`;
for (let i = 0; i < 4; i++) {
if (a[i] !== b[i]) return false;
if (a[i] === "@") return true;
}
return a[4] === b[4];
};
/** CheckOwnMonAnywhere (engine/pokemon/search_owned.asm): a Pokémon of
* `species` with the player's ID and OT in the party or a box. An empty
* party finds nothing; the Day-Care is not searched. The party struct's
* species is read, so an Egg counts as its species. */
export function ownsMon(c: SpecialContext, species: number) {
const { party, storage } = c.state;
if (!party.length) return false;
const player = c.player();
const owns = (mon: { species: number; otId: number; ot: string }) =>
mon.species === species && mon.otId === player.id && sameOT(mon.ot, player.name);
return party.some(owns) || storage.boxes.some((box) => box.some(owns));
}
/** MonCheck: 1 when the player owns a Pokémon of the species. */
export const monCheck = (c: SpecialContext, species: number) =>
ownsMon(c, species) ? 1 : 0;
/** BeastsCheck: 1 when the player owns Raikou, Entei and Suicune. */
export const beastsCheck = (c: SpecialContext) =>
[RAIKOU, ENTEI, SUICUNE].every((species) => ownsMon(c, species)) ? 1 : 0;