Compare commits

..

155 Commits

Author SHA1 Message Date
bryanthaboi ae6cac89e1 G2 support 2026-08-11 11:53:30 -04:00
bryanthaboi 79ed37699e Merge pull request #1055 from AverageConsumer/codex/mod-grid-navigation-hooks
Mod API: allow alternate battle grids to own navigation
2026-08-10 17:08:24 -04:00
bryanthaboi abf0f9e98a Merge pull request #1057 from ShaneMcGovernIE/shanemcgovernie-fix-index-unexpected-character
Handle non-JSON update responses
2026-08-10 17:07:43 -04:00
bryanthaboi 3955721bfc Merge pull request #1058 from castdrian/ios-documents
feat(ios): expose app data in Documents dir
2026-08-10 17:07:05 -04:00
bryanthaboi c3855cebec more buggies CLOSES #960, CLOSES #961, CLOSES #968, CLOSES #995, CLOSES #1006, CLOSES #1009, CLOSES #1013, CLOSES #1021, CLOSES #1031, CLOSES #1044, CLOSES #1049, CLOSES #1050, CLOSES #1045,, 2026-08-10 17:06:24 -04:00
Adrian Castro 41e6507665 docs(ios): document public Documents storage 2026-08-10 22:56:15 +02:00
Adrian Castro 5482ac590d fix(ios): guard missing game payload 2026-08-10 22:56:15 +02:00
Adrian Castro 28440eb936 fix(ios): expose runtime data in Documents 2026-08-10 22:56:15 +02:00
Shane McGovern f606840216 Handle non-JSON update responses
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-10 20:51:57 +01:00
AverageConsumer 6a990bf79f feat(mods): allow alternate battle grids to own navigation 2026-08-10 21:22:09 +02:00
bryanthaboi 7fa758602a Merge branch 'dev' of https://github.com/bryanthaboi/gen1recomp into dev 2026-08-10 14:59:06 -04:00
bryanthaboi bb979911c8 Update common.sh 2026-08-10 14:59:04 -04:00
bryanthaboi f225f8a6e7 Merge pull request #918 from KikiManjaro/pok-nickname-changer-in-save
Add nickname editing to the save editor's mon inspector
2026-08-10 14:32:14 -04:00
bryanthaboi ee891fb8fa Merge pull request #993 from MaxTomahawk/feat/checkpoint-restore-event
feat(mods): signal verified checkpoint restores
2026-08-10 14:31:45 -04:00
bryanthaboi 9d73ff4110 yellow copyright diff 2026-08-10 14:27:00 -04:00
bryanthaboi 0b7ecf1599 Merge pull request #985 from steve1337/issue_#969_linux_app_image
[#969] Fixes linux AppImage
2026-08-10 14:21:57 -04:00
bryanthaboi aa45f05c63 Merge pull request #1038 from dlloa/upstream-pr/per-category-game-speed
Per-category GAME SPEED (overworld/battle/menu) + core.logic_speed hook
2026-08-10 14:15:59 -04:00
bryanthaboi 9234f12ed0 Merge remote-tracking branch 'origin/dev' into fix1038
# Conflicts:
#	docs/modding.md
2026-08-10 14:14:39 -04:00
bryanthaboi 26e0bd227e Merge pull request #1037 from dlloa/upstream-pr/platform-lifecycle-hooks
Add core.update / core.quit_to_launcher platform lifecycle hooks
2026-08-10 14:14:17 -04:00
bryanthaboi 64ce66a619 Merge pull request #1023 from AverageConsumer/feature/battle-ui-visibility
Mod API: expose battle UI visibility hooks
2026-08-10 14:12:57 -04:00
bryanthaboi 9302bbcbf3 Merge branch 'fix1037' into fix1038
# Conflicts:
#	docs/modding.md
2026-08-10 14:12:21 -04:00
bryanthaboi 1855f838ef Merge remote-tracking branch 'origin/dev' into fix1037
# Conflicts:
#	docs/modding.md
2026-08-10 14:11:45 -04:00
bryanthaboi 11af06a752 Merge remote-tracking branch 'origin/dev' into fix1023
# Conflicts:
#	docs/modding.md
2026-08-10 14:10:52 -04:00
bryanthaboi 05668591ef Merge pull request #965 from crusty/feat/worldapi-start-wild-battle
Add mod.world:startWildBattle
2026-08-10 14:09:28 -04:00
bryanthaboi e74beec32d Merge pull request #970 from swuff-star/modded-evo-item
add modding support for custom item effects
2026-08-10 14:09:23 -04:00
bryanthaboi 3efb2c9a6c Merge pull request #996 from thibautbus/fix/modkit-dump-dataset-utf8-decode
Fix: tools/modkit.py crashes on Windows when dumped text isn't representable in the system codepage
2026-08-10 14:09:19 -04:00
bryanthaboi d3af63e013 Merge pull request #1020 from ArmstrongThomas/agent/variable-size-overworld-sprites
Support variable-size anchored overworld sprites
2026-08-10 14:09:13 -04:00
bryanthaboi ca6bee64b9 Merge pull request #1042 from thibautbus/fix/android-ttf-dpi
Fix custom TTF rendering on Android
2026-08-10 14:09:07 -04:00
bryanthaboi b52d183dba Merge pull request #1024 from AverageConsumer/feature/semantic-pc-list-kinds
Mod API: expose semantic PC list kinds
2026-08-10 14:09:01 -04:00
bryanthaboi 23e1877a62 Merge pull request #1025 from AverageConsumer/feature/map-overview-detail-markers
Mod API: add detailed map overview data and POIs
2026-08-10 14:08:58 -04:00
bryanthaboi d8736fe33e Merge pull request #1015 from ShaneMcGovernIE/shanemcgovernie-fix-substitute-fainting
Fix Substitute exact-quarter-HP trainer softlock
2026-08-10 14:08:53 -04:00
bryanthaboi a74365a4fd Merge pull request #1043 from ShaneMcGovernIE/shanemcgovernie-fix-rival-collision
Fix champion rival walk-out route
2026-08-10 14:08:49 -04:00
bryanthaboi 1c2163bb5f Merge pull request #1046 from ShaneMcGovernIE/shanemcgovernie-fix-encounter-rate-grace-period
Fix wild encounter grace period
2026-08-10 14:08:46 -04:00
bryanthaboi 12c2677dc2 title screen issues, audio issues, and replacing gf c 2026-08-10 14:00:26 -04:00
Shane McGovern ae1b59ce7f fix: restore wild encounter grace period
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-10 12:15:18 +01:00
Shane McGovern 02c4de897d Fix champion rival walk-out route
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-10 10:59:06 +01:00
thibautbus 3c6242717e Use 1x TTF rasterizer on Android 2026-08-10 08:43:46 +02:00
david 3c71afb9fa Per-category GAME SPEED: overworld/battle/menu + core.logic_speed hook (RFC 0007)
GameSpeed is a single fast-forward multiplier applied uniformly to the
whole logic clock -- overworld walking, menu navigation and battle turns
all scale together. A player who wants 4X battles but 1X overworld (so a
cutscene or NPC dialogue doesn't blur past) has no way to get both.

Splits save.options.speed into speedOverworld/speedBattle/speedMenu, each
cycling independently, with an automatic migration so an existing save's
speed choice carries over. Game.speedCategoryInStack resolves which
category is active by walking the state stack (the same idiom
wideBattleInStack/fillScaleInStack already use), so a menu opened mid-
battle inherits battle speed rather than resetting to whatever "menu"
defaults to. Adds a new core.logic_speed hook so a mod can read or
override the resolved multiplier for the current frame regardless of
which category produced it, sitting after the link-play and run-argument
overrides so neither is a seam a mod can defeat.

RFC 0007 status: Proposed.
2026-08-09 20:13:21 -07:00
david e79107c644 Add core.update/core.quit_to_launcher platform lifecycle hooks (RFC 0006)
A platform-specific launcher wrapper (a native shell embedding this engine,
owning its own UI around the game window) needs to pause the simulation
while its own UI is on top, live-reload options it wrote outside any Lua
UI, and veto main.lua's "closing the window returns to the Lua launcher"
behavior when it owns that job itself. Implementing this by hand-patching
main.lua's love.update/love.quit directly ties every such integration to
editing the one file every other engine change also touches, guaranteeing
merge conflicts. No existing hook covers "should the per-frame simulation
step run" or "should closing the window return to the Lua launcher."

Adds two generic, additive hooks (src/core/PlatformHooks.lua): core.update
and core.quit_to_launcher, replacing what would otherwise be inline
main.lua special-casing. Also adds Manifest.force_enable_env, letting a
mod that cannot function disabled on the one build where its env var is
set (a platform-bridge mod bundled only with that build) re-enable itself
regardless of a saved disable.

RFC 0006 status: Proposed.
2026-08-09 20:05:06 -07:00
steve1337 0afce96c8d [#969] Fixes linux AppImage
StartupWMClass matches what SDL reports: AppRun execs bin/love, so the
window's WM_CLASS / Wayland app_id is "love" and without this the taskbar
entry never resolves back to this desktop file (no name, no icon).
2026-08-09 22:52:38 +07:00
AverageConsumer d881997acd feat(mods): add detailed map overview POIs 2026-08-09 15:57:17 +02:00
AverageConsumer 8906133b93 feat(ui): expose semantic PC list kinds 2026-08-09 15:49:28 +02:00
AverageConsumer b9e3c4a689 feat(mods): expose battle UI visibility hooks 2026-08-09 15:46:33 +02:00
Thomas Armstrong 81f18e244d Support variable-size anchored overworld sprites 2026-08-09 04:10:39 -04:00
Shane McGovern ff91481018 Fix Substitute zero-HP boundary
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-09 00:17:51 +01:00
thibautbus fdb3184c71 Fix Windows crash: decode LuaJIT dump output as UTF-8, not the locale codepage
subprocess.run(..., capture_output=True, text=True) with no explicit
encoding falls back to locale.getpreferredencoding(False) -- the OS
default codepage. On Windows that's a legacy single-byte codepage
(e.g. cp1252), never UTF-8. When the LuaJIT dump contains a byte with
no mapping in that codepage, subprocess's internal _readerthread
crashes with an uncaught UnicodeDecodeError in a background thread; the
thread dies silently and the caller gets back stdout=None instead of a
string, crashing one line later with
AttributeError: 'NoneType' object has no attribute 'splitlines'.

Concretely, the Yellow-side imported dataset contains:
  "_ColosseumHeightText" -> "...6’8” tall!"
The right double quotation mark (U+201D) encodes in UTF-8 as E2 80 9D;
0x9D has no defined character in cp1252, so decoding as cp1252 fails
outright. Verified against the real imported dataset: the Red/Blue-only
dump has zero bytes outside cp1252's defined range; the Yellow dump has
exactly one, at this row.

UTF-8 is the actual encoding these dumps are produced in -- the driver
Lua sources are read/written as UTF-8 throughout this file, and LuaJIT
writes those source strings' bytes back out verbatim -- so passing
encoding="utf-8" explicitly at the three affected call sites
(run_loader, check_data_dump, dump_dataset) is a no-op on platforms
whose default codepage is already UTF-8 (Linux/macOS) and a correctness
fix on Windows.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 16:14:07 +02:00
MaxTomahawk aa3b2a18ec feat(mods): signal verified checkpoint restores 2026-08-08 13:28:56 +02:00
Jakub Lisicki d498016a47 Merge remote-tracking branch 'origin/dev' into feat/worldapi-start-wild-battle 2026-08-08 12:42:10 +02:00
Jakub Lisicki 492e0344b5 Fold the startWildBattle coverage into mod_world_tests
mod_world_tests is already the mod.world suite and already the T3 tier,
with the same off-the-world refusal pattern the standalone file was
duplicating. Reuses its liveWorld fixture instead of standing up a
second one.
2026-08-08 12:35:06 +02:00
Jakub Lisicki a710c64909 Refuse re-entrant, fractional-level and no-party wild battles
overworld() resolves the world from under the stack, so a call from a
battle hook stacked a second battle over the live one -- on a loss its
afterBattle blacked out and warped with the outer battle still up.

newWild marks the species SEEN before it reports an empty party, so a
refused call still wrote the Pokedex; test the party before building it.

tonumber accepts 5.5, which Pokemon.new writes straight into the stat
calc and the exp curve.
2026-08-08 12:23:05 +02:00
bryanthaboi 943ba5dcbf Merge pull request #991 from AverageConsumer/codex/mod-world-map-tiles
Mod API: expose visual tile data in map overviews
2026-08-08 06:16:24 -04:00
AverageConsumer 371edd1a02 feat(mods): expose map tile shading 2026-08-08 11:59:57 +02:00
Jakub Lisicki 9fc6ba5b01 Add mod.world:startWildBattle
Starting a wild encounter had no supported entry point, so mods built a
BattleState and pushed it themselves -- silently losing onFinish (and
with it evolutions and blackout-on-loss) and pushBattle (entry wipe,
battle theme). Neither failure raises.

Also covers awardExp -> leveledUp -> afterBattle -> checkParty, which
parity_trainer_evolution_order stubs BattleState out of.
2026-08-08 11:58:10 +02:00
bryanthaboi 983bea61aa Merge pull request #986 from MaxTomahawk/feat/mod-battle-checkpoints
feat: add persistent battle safe-point checkpoints
2026-08-08 05:46:34 -04:00
bryanthaboi ab74f7adf7 Merge pull request #974 from hernan0078/mods/spanish-ui
mods: a Spanish translation of the app's own text
2026-08-08 05:45:29 -04:00
bryanthaboi bdd0bf45bd Merge pull request #973 from hernan0078/options/cancel-through-strings
OPTIONS: route CANCEL through Strings so a translation mod can reach it
2026-08-08 05:44:34 -04:00
bryanthaboi 4b950fc23d Merge pull request #972 from hernan0078/ios/stadium-picker-kind
iOS picker: a "stadium" kind, and a way to ask which kinds exist
2026-08-08 05:44:22 -04:00
bryanthaboi af183d95ad Merge pull request #962 from AverageConsumer/feature/mod-world-map-overview
Mod API: expose a read-only active map overview
2026-08-08 05:42:55 -04:00
bryanthaboi 1d4d5d2337 Merge pull request #940 from Yukitty/patch-title_player
Allow TitleState player overrides
2026-08-08 05:42:34 -04:00
bryanthaboi 653b72d86a Merge pull request #925 from Bortlesboat/multiplayer-session-foundation
Add a reusable multiplayer session layer
2026-08-08 05:42:23 -04:00
MaxTomahawk 12b6ef6350 test: prove complete overworld progress fidelity 2026-08-08 09:53:41 +02:00
MaxTomahawk c979685a1f test: cover switched battle party fidelity 2026-08-08 09:53:41 +02:00
MaxTomahawk 5b1d0261ff test: prove public battle checkpoint roundtrip 2026-08-08 09:53:41 +02:00
MaxTomahawk 5bffc89a2f docs: specify battle checkpoint extension 2026-08-08 09:53:41 +02:00
MaxTomahawk 2ce612d5b1 test: prove deterministic battle checkpoint fidelity 2026-08-08 09:53:41 +02:00
MaxTomahawk 6999e5aecf feat: preserve overworld checkpoint RNG 2026-08-08 09:53:41 +02:00
MaxTomahawk 44a7910c69 feat: reconstruct standard battle continuations 2026-08-08 09:53:41 +02:00
MaxTomahawk 85c6fde443 feat: reconstruct battle checkpoints from data 2026-08-08 09:53:41 +02:00
MaxTomahawk 67b6dcc293 feat: capture data-only battle checkpoints 2026-08-08 09:53:41 +02:00
MaxTomahawk 24d9f279ec feat: expose settled battle checkpoint boundary 2026-08-08 09:53:41 +02:00
hernan 83f93c27a1 mods: a Spanish translation of the app's own text
A LANGUAGE-profile mod filling the `strings` registry -- the seam #791 opened
up, used for the thing it was opened for. 566 keys: the launcher, OPTIONS,
the save-slot and profile screens, the mod manager, the naming screen, and
the battle menu.

WHAT IT DOES NOT TOUCH is the cartridge. Dialogue, species, items, moves and
places all come out of the player's ROM and stay exactly as they are, so an
English cartridge is still an English adventure with Spanish menus around
it. The lang/ tables for those ship empty on purpose rather than absent:
they are where a full translation would go, and an empty value falls through
to English, so anyone continuing this can fill one row at a time and the
game stays playable throughout.

THE FONT IS THE CONSTRAINT, and it decided the wording. The 8x8 charmap has
no N-tilde, no accented vowels and no inverted punctuation -- the sole
exception in the whole atlas is the small e-acute of POKeMON. So every value
on an 8x8 surface is plain A-Z: DISENO COMBATE, MESETA ANIL, SEGURO? OTRA
VEZ. Not a spelling preference; a glyph that is missing renders as a hole,
which is how the first cut of this shipped "ESPA OL" to a phone. The
launcher draws with a real font and keeps proper Spanish, inverted marks and
all -- the split is by surface, not by taste.

Adding the glyphs to the atlas would let the 8x8 side read properly too, and
lang/font.lua and lang/charmap.lua are already the place that would hook
into. I have not done it here: it is a separate change with its own taste
questions, and it should not ride in on a catalog.

Tested end to end on iOS and macOS.
2026-08-07 22:16:06 -04:00
hernan d756b7cd43 OPTIONS: route CANCEL through Strings so a translation mod can reach it
CANCEL is appended after the `ui.options.rows` hook, deliberately -- that is
what stops a mod from orphaning the exit. But it also means no translation
mod can ever see it: there is no row for one to rewrite, and the hook has
already run by the time it is added.

The result is that a fully translated OPTIONS menu has exactly one English
word left on it, and it is the way out. I hit this with a Spanish catalog
where every row translated and the exit did not.

One call, matching how every other label on this screen is already built.
Nothing changes without a catalog loaded: Strings is an identity function
until a mod supplies one.

Follows the same reasoning as #791.
2026-08-07 22:14:29 -04:00
hernan 1e6f504a84 iOS picker: a "stadium" kind, and a way to ask which kinds exist
Two things, the second of which is the reason the first is safe.

A NEW KIND. pickFile("stadium") opens the document picker for a Nintendo 64
cartridge and lands it as picked_stadium.z64. The caller I wrote it for is
the Dramatic Shape voxel mod, which builds Pokemon Stadium battle models out
of the player's own cartridge -- on desktop it opens a dialog for that, and
on iOS it could only print a sandbox path to a screen and ask the player to
put a file somewhere they cannot reach from a phone.

Its own filename, not picked_rom.gb, because that is the name the Game Boy
importer watches: a 32 MB N64 ROM landing there is deleted and then reported
to the player as a broken cartridge.

WHICH IS ALSO WHAT AN UNKNOWN KIND USED TO DO. The switch's default case
treated anything it did not recognise as a Game Boy ROM, so a caller asking
for a kind the build predates lost the player's file -- the worst available
answer to "I have not heard of that one". Unknown kinds are refused now.

That refusal is invisible on its own: pickFile returns false, which is also
what "the picker would not open" returns, and a mod cannot tell them apart.
So the host says what it knows -- love.system.pickFileKinds() returns
"rom,mod,sav,stadium", or nil where there is no bridge. A caller asks first
and keeps whatever fallback it had; the voxel mod shows its folder note
again, which is what it did before any picker existed.

Tested on an iPhone 17 Pro: a Stadium cartridge imports from the picker and
the models build.
2026-08-07 22:13:50 -04:00
Hanan Rodebaugh abb4f3f9f0 better mod item registry 2026-08-07 20:24:11 -04:00
Yukita Mayako 5b0209a7ce feat(TitleState): add field.title.player 2026-08-07 14:28:25 -04:00
AverageConsumer c1e0685e97 feat(mods): expose read-only map overview 2026-08-07 20:20:57 +02:00
Bortlesboat 3c57b28c6d ci: retrigger cancelled checks 2026-08-07 13:16:06 -04:00
bryanthaboi cab62ff7b3 Merge pull request #903 from martin2844/martin2844/fix-blue-rom-setup
Fix Blue ROM source setup
2026-08-07 12:50:26 -04:00
bryanthaboi 5b6dfed359 Merge pull request #959 from MaxTomahawk/feat/reproducible-mod-packages
fix: make modkit packages reproducible with SOURCE_DATE_EPOCH
2026-08-07 12:43:50 -04:00
bryanthaboi 91a85a065b Merge pull request #927 from AverageConsumer/feature/second-screen-touch
Mod API: expose Android second-screen touch input
2026-08-07 12:41:48 -04:00
bryanthaboi 32e2c51365 Merge pull request #957 from ShaneMcGovernIE/shanemcgovernie-issue-916-trainer-sprite-at-end-of-fly-animation-9e5f77
Hide trainer sprite through the Fly/Dig warp fade (#916)
2026-08-07 12:41:05 -04:00
bryanthaboi 1992fdf398 Merge pull request #955 from ShaneMcGovernIE/shanemcgovernie-issue-945-cannot-edit-trainer-class-s-battle-theme-ad701b
Honor trainer battleTheme override (fixes #945)
2026-08-07 12:40:55 -04:00
bryanthaboi 59ac27971a Merge pull request #953 from ShaneMcGovernIE/shanemcgovernie-fix-reset-settings-bugs
saveOptions three-way merge: stop partial writes dropping launcher keys (#932)
2026-08-07 12:39:56 -04:00
bryanthaboi cd0ace2a39 Merge pull request #952 from MaxTomahawk/feat/mod-state-checkpoints
Add playthrough-scoped storage and stable overworld checkpoints
2026-08-07 12:39:32 -04:00
bryanthaboi 274c728e77 Merge pull request #950 from jherediagu/fix/translation-readiness
Translation-readiness: route the last hardcoded labels through the Strings catalog.
2026-08-07 12:36:47 -04:00
bryanthaboi 8547127f54 Merge pull request #948 from ShaneMcGovernIE/shanemcgovernie-issue-915-wrong-sprite-used-in-opening-2f6ea5
Fix Yellow Oak speech showing Nidorino instead of Pikachu (#915)
2026-08-07 12:36:37 -04:00
bryanthaboi 45d799aa98 Merge pull request #946 from ShaneMcGovernIE/shanemcgovernie-issue-931-mod-update-checks-fail-594ba9
Fix mod update checks failing on non-JSON responses (#931)
2026-08-07 12:36:25 -04:00
bryanthaboi 660c90626d Merge pull request #937 from Yukitty/patch-oak_speech_shrink
feat(OakSpeech): use field.playerSprites.walk in shrink step
2026-08-07 12:36:14 -04:00
AverageConsumer d9dc42895f chore: retrigger CI 2026-08-07 18:10:22 +02:00
MaxTomahawk 02fd21bcdb fix: honor source date in mod packages 2026-08-07 17:54:06 +02:00
MaxTomahawk af00d6ad14 feat: expose storage engine compatibility context 2026-08-07 17:14:42 +02:00
Shane McGovern 0d4a518a08 saveOptions three-way merge: stop partial writes dropping launcher keys (#932)
options.lua is a whole-file rewrite, so a caller handing saveOptions a
partial table (only the keys it changed) silently dropped every key it did
not mention: launcher-only keys like lastVersion, and keys the launcher set
(battleBg, tilt) all fell back to defaults.

saveOptions now reads the on-disk file first and folds caller-absent
values underneath before mergeOptions backfills defaults.  A table holding
every defaultOptions key is a full snapshot and stays authoritative, so the
fold is inert for all in-repo writers (every one passes loadOptions-ed
tables) and cannot resurrect the bindings/activeProfile deletions the
RESET REBINDS and mod-manager paths make on full tables.

Adds a regression suite (options_partial_write_bug932.lua) pinning the
merge, and updates the #828 suite's partial-write assertion, which now
expects lastVersion to survive a delta write.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-07 14:55:55 +01:00
MaxTomahawk 05b43ca258 feat: include engine version in checkpoints 2026-08-07 15:21:16 +02:00
MaxTomahawk 9d6ea845d7 fix: reject invalid checkpoint content 2026-08-07 15:09:16 +02:00
MaxTomahawk bbca4e8e39 docs: specify mod storage and checkpoint APIs 2026-08-07 15:05:43 +02:00
MaxTomahawk 49954ec4ad fix: allocate playthrough identity only on demand 2026-08-07 15:01:18 +02:00
MaxTomahawk 6e94625f2a feat: expose stable overworld checkpoints to mods 2026-08-07 14:49:45 +02:00
MaxTomahawk 0399ad040f feat: add playthrough-scoped mod storage 2026-08-07 14:43:04 +02:00
MaxTomahawk 726ed1102e feat: add opaque playthrough identity 2026-08-07 14:43:04 +02:00
Juan Heredia 5bc7d203a5 Route the last hardcoded UI labels through the Strings catalog
Wrap the battle stat box, the PC quantity footer and the dex metric
labels in Strings() (SummaryMenu -- and MoveEffects since #811 --
already do this); give the battle menu a 'battle' lookup context so a
translation can shorten FIGHT/ITEM/RUN independently of the pause menu;
align the metric dex rows with the imperial ones and make the No.
prefix translatable; make the title menu's recolor zone follow Menu's
auto-grown width; honor the declared-but-unread boot.title seam,
drawing an explicit versionRibbon as one centered piece. With an empty
catalog every path is pixel-identical to vanilla.
2026-08-07 13:59:01 +02:00
Shane McGovern ce204aaf16 CLOSES #916: hide trainer sprite through the Fly/Dig warp fade
After the Fly departure animation finished (bird off-screen) and during
Dig/teleport, the trainer sprite popped back in standing at the old cell
for the whole 32-frame black fade-out before the transition.  The
player-hide guard only held while a departure animation was live:
flyAnim went nil the instant path2 completed and the teleportOut
countdown cleared the spin fields at 0, but startWarpTo's Transition
(not isOpaque) keeps the overworld drawing beneath the veil, and the
arrival animation is not armed until setMap's midpoint.

Add a playerHidden flag on OverworldState that bridges the gap:
- set when each departure completes (flyAnim path2 / teleportOut hit 0),
  immediately before the warp starts;
- cleared in startWarpTo's Transition enter callback, synchronously
  after setMap and before the arrival arms flyArrive / spinDrop, so the
  player is never drawable mid-fade and never bare on the landing frame;
- folded into both player-draw guards.

ROM-free regression test (tests/engine/warp_sprite_hidden_bug916.lua)
drives the REAL Transition + setMap headlessly for Dig and Fly and
asserts zero fade frames leave the player drawable bare (would have
observed 31/32 gap frames before the fix).  Runs in the CI headless T2
tier.

Dig spin timing/lift and the black fade color are left as-is (fade is
intentional per #607).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-07 11:14:20 +01:00
Shane McGovern ed8a89c5ce Honor trainer battleTheme override (fixes #945)
trainers.battleTheme validated and merged onto the trainer record but was
never read: battle music came solely from data.audio.battle[kind] where
kind is computeMusicKind()'s final/gym/trainer/wild.  Route both battle-
theme start sites through a single choke point:

- BattleState:playBattleTheme() cues Music.playBattle with the override
  (self.trainer.battleTheme via battleTheme()), defaulting to the kind
  when unset, so vanilla fights and #782's non-gym Giovanni are unchanged.
- BattleState:enter() and OverworldController:pushBattle() both call it.
- Music.playBattle gains an optional 4th song arg that overrides the kind
  default, and real call sites now populate the music.select trainerId.
- Victory jingles stay kind-based: a custom battle theme has no derivable
  win-variant.

New ROM-free T2 suite tests/engine/trainer_battle_theme_bug945.lua covers
mod load, override resolution, the choke point, and the nil-override
parity gate.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-07 10:53:42 +01:00
Shane McGovern 66e7d9432e Fix Yellow Oak speech showing Nidorino instead of Pikachu (#915)
Yellow's field.oakSpeech manifest carried only shrink frames, so
OakSpeech.lua's `oakGfx.demoSpecies or "NIDORINO"` fallback fired and
the opening speech showed Nidorino's sprite and cry instead of the
player's Pikachu.

- Stamp demoSpecies "PIKACHU" in the Yellow import manifest (source of
  truth for fresh ROM imports and developer builds).
- Stamp it in make_yellow_manifest.py so regeneration keeps the value.
- Repair stale Yellow caches in Data:applyVersionedFieldData() with a
  fill-if-absent block, matching the #617 oldManBattle RATTATA pattern.
- Add parity test (manifest carries PIKACHU; stale cache filled;
  pre-stamped value left alone).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-07 10:51:50 +01:00
Shane McGovern d59a9522ee Fix mod update checks failing on non-JSON responses (#931)
The mod update and Find Mods feeds feed the raw HTTP body straight to
Json.decode. When the endpoint hands back something that is not JSON
(an HTML error page, a proxy/captive prompt, or a plain-text outage
message like "Exceeded secondary rate limit" -- usually still HTTP 200),
the decoder's "unexpected character 'E'" assert escaped through the
pcall and became the error message, blaming the parser instead of the
response.

Add Json.describeUnexpected() as a pre-decode content-type guard: it
returns nil for body shapes the endpoints actually publish (JSON object
or array) and otherwise a short message naming what the server sent
(HTML page / plain text / empty, with a preview). Wire it into
ModUpdate.parseReleases and ModIndex.parse, so both the sync and async
update-check paths surface the real answer instead of the parse error.
HTTP status was already checked upstream by HostShell.httpGet (non-2xx
becomes "HTTP <code> from <url> (...)"); this closes the remaining
"2xx but not JSON" gap everywhere, including bridge platforms that
expose no status or headers.

Add regression tests for plain-text, HTML, and empty bodies; strengthen
the ModIndex HTML soft-fail test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-07 10:49:10 +01:00
Yukita Mayako f1661b62b3 feat(OakSpeech): use field.playerSprites.walk in shrink step 2026-08-07 00:28:24 -04:00
AverageConsumer a98562f46b feat(android): expose secondary-screen touch events 2026-08-06 22:56:13 +02:00
Andrew Barnes 365bd24740 Document multiplayer session boundary 2026-08-06 16:50:30 -04:00
Andrew Barnes d50ea4d067 Route tournaments through multiplayer sessions 2026-08-06 16:50:30 -04:00
Andrew Barnes 52c96437fa Route link play through multiplayer sessions 2026-08-06 16:50:29 -04:00
Andrew Barnes 2a7c8ec81b Harden multiplayer session terminal handling 2026-08-06 16:50:29 -04:00
Andrew Barnes bd6c606303 Add multiplayer session FIFO facade 2026-08-06 16:50:28 -04:00
bryanthaboi 112120e8fe Merge branch 'dev' of https://github.com/bryanthaboi/gen1recomp into dev 2026-08-06 14:36:03 -04:00
bryanthaboi 00f6e3a7b4 CLOSES #889 2026-08-06 14:36:01 -04:00
kikimanjaro 9cc72f951b Add nickname editing to the save editor's mon inspector
The mon inspector now has a NICKNAME section: a text field with Set / Clear, commit-on-Enter and discard-on-Escape.

Input is gated at the field instead of refused at commit. Only glyphs the game
font can actually draw AND that round-trip through a .sav are accepted, capped
at the naming screen's 10 glyphs, so a bad keystroke or paste never appears and
Set always succeeds. Chars like "@" (the Gen1 string terminator), "#" and the
dakuten kana have codec entries but no font tile so they are blocked rather than silently mangled.
2026-08-06 18:52:24 +02:00
Martin 493caee715 Merge dev and fix headless ROM builder dependency 2026-08-06 16:22:34 +02:00
bryanthaboi aa6217e581 Merge pull request #910 from andrewqsantos/feat/switch-ota-unified-launcher
Feat Switch OTA Update
2026-08-06 10:20:23 -04:00
bryanthaboi a640f5504f Merge pull request #906 from caorthann-celt/dev
Fix Xbox mod downloads and internal ROM imports
2026-08-06 10:20:00 -04:00
Caorthann e01672b97d Update Xbox UWP assets to match upstream 2026-08-06 15:15:07 +01:00
Caorthann 9e3d487a08 Add UWP LocalState baserom discovery 2026-08-06 15:15:07 +01:00
Caorthann 506cd2c5c5 Enable UWP mod downloads 2026-08-06 15:13:54 +01:00
bryanthaboi 6cb0ac5666 Merge pull request #908 from johnjohto/fix-red-cache-folder-899
Move Red's extracted cache under red/ with a legacy migration
2026-08-06 10:09:34 -04:00
bryanthaboi b7d0ff2e16 Merge pull request #904 from ShaneMcGovernIE/shanemcgovernie-hit-sfx-pitch-regression-test
Guard hit-sfx noise pitch with a CI regression test (#902)
2026-08-06 10:09:08 -04:00
bryanthaboi 984cefdc7b launcher updates 2026-08-06 09:57:31 -04:00
johnjohto 6c892cb7c1 Move Red's extracted cache under red/ with a legacy migration
Importing Red unpacked data/generated, assets/generated and
rom-cache.complete straight into the save-dir root, while Blue and
Yellow land under blue/ and yellow/. Red now uses cachePrefix red/
like the others.

CacheFs.migrateLegacyRedCache moves a pre-existing root cache into
red/ on first boot, from RomImporter.new before the readiness loop
and from mountVersion, so existing installs keep their import instead
of being asked for the ROM again. The move only runs when the root
marker resolves to the save directory, so a dev checkout's source
tree is never touched, and the portable game folder is skipped when
it is the physfs source.

Closes #899
2026-08-06 09:55:28 -04:00
Andrew Quenehen 52d1ddc5af Fix Switch OTA clean and NX version chip test after launcher layout changes. 2026-08-06 10:29:43 -03:00
Andrew Quenehen 75cd28435c Update build scripts and tests for ports/switch OTA paths.
Point launcher, fused build, docs, and gitignore at the relocated Switch port tree.
2026-08-06 10:24:04 -03:00
Andrew Quenehen 9aa8b4e371 Move Switch OTA launcher sources under ports/switch.
Consolidate the native OTA launcher, bootstrap, and assets into the Switch port tree.
2026-08-06 10:22:51 -03:00
Andrew Quenehen e94d8e32ef Drop stb_image from Switch OTA launcher with a pre-baked logo asset. 2026-08-06 10:14:11 -03:00
Andrew Quenehen 3fae3088ca Fix Switch OTA launcher self-update via a bootstrap NRO.
A running launcher cannot replace its own NRO on sdmc/FAT; stage the new
binary and chainload a tiny helper from romfs to swap it in before loading
the game.
2026-08-06 09:59:01 -03:00
Shane McGovern cbb08f76ae Add ROM-free regression test for hit-sfx noise pitch (#826/#902)
The super-effective / not-very-effective hit sounds were only covered by
the manual ears-only driver (tests/drivers/hit_sfx_bug826_test.lua), so
the wFrequencyModifier wiring that fixed #826 (and that #902 reports as
a swap had no CI guard.  This suite pins the polynomial-counter bytes
for all three hit sounds against the real audio/sfx programs: bare they
read swapped (super effective ends duller), and pitched they read
correct (super effective 4 shift 1 crack, not very effective
shift 10 thud).  A port that drops the modifier fails 11 of 24 checks.

CLOSES #902

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
EOF
)
2026-08-06 13:35:07 +01:00
Andrew Quenehen dd503706b0 Merge origin/dev (v0.1.74) into feat/switch-ota-unified-launcher.
Keep Switch version chip alongside upstream's new quit button in the launcher header.
2026-08-06 09:28:26 -03:00
Andrew Quenehen eeb181bcc0 Wrap Switch OTA button labels in parentheses so A/B/+ read as controls. 2026-08-06 09:17:49 -03:00
Andrew Quenehen 287534cf26 Mount romfs before the quiet OTA release check so HTTPS can load cacert.pem.
ota_net_init() now mounts romfs and initializes curl before the GitHub check runs, fixing silent update-check failures after TLS verification was enabled.
2026-08-06 09:14:35 -03:00
Andrew Quenehen d08fce5fd5 Fix fused Switch build polluting GAME_LOVE path capture.
Route common.sh say() to stderr so sourcing it before pack_game_love does not
mix progress lines into command substitution output.
2026-08-06 09:07:58 -03:00
Martin 0ae29744c8 Fix Blue ROM source setup 2026-08-06 14:06:55 +02:00
Andrew Quenehen 84e1a5177b Harden Switch OTA install with TLS verification and reliable NRO replace.
Enable HTTPS peer checks via a romfs CA bundle, remove existing NROs before rename on sdmc, and surface launcher replace failures before writing version.txt.
2026-08-06 08:58:22 -03:00
Andrew Quenehen c7e72149f1 Tighten Switch docs prose and drop filler patterns. 2026-08-06 08:43:32 -03:00
Andrew Quenehen ecfca19f11 Consolidate Switch docs into three production guides.
Remove internal development and hardware-evidence docs, drop issue references, and update cross-links and doc gates.
2026-08-06 08:36:43 -03:00
Andrew Quenehen 90a53e509a Remove duplicate Switch port credits from the README footer. 2026-08-06 08:29:03 -03:00
Andrew Quenehen 15fc97a996 Unify --fused with OTA launcher and require DEVKITPRO preflight for release builds. 2026-08-06 08:28:31 -03:00
bryanthaboi 6f75a64c46 deleting stale tests 2026-08-06 07:11:16 -04:00
Andrew Quenehen dc04a4e848 Improve Switch OTA flow with download progress and clearer error screens. 2026-08-06 07:54:28 -03:00
bryanthaboi cd37aa175c Merge branch 'dev' of https://github.com/bryanthaboi/gen1recomp into dev 2026-08-06 06:36:46 -04:00
bryanthaboi cb6cfb5556 CLOSES #883, CLOSES #887, CLOSES #894 2026-08-06 06:36:12 -04:00
bryanthaboi 5528525888 Merge pull request #892 from MyFriendPKMN/dev 2026-08-06 05:18:15 -04:00
bryanthaboi a49ba66c36 Merge pull request #871 from ratherDashing/linux-arm64-appimage 2026-08-06 05:17:55 -04:00
Marcelo Machado dbc48f377a feat(trainerCard): enhance player portrait handling with trueColor support 2026-08-06 00:15:07 -03:00
bryanthaboi 1d77d42fe2 Merge pull request #881 from AverageConsumer/codex/upstream-screen-render-visible
Mod API: allow active screen states to be hidden from the main render
2026-08-05 17:13:14 -04:00
AverageConsumer b9e8b00af0 feat(mods): add screen render visibility hook 2026-08-05 22:42:37 +02:00
ratherDashing f92364a002 Build SDL2, OpenAL and the codecs from source for the arm64 AppImage
CI on a headless ubuntu-24.04-arm runner caught what a desktop Pi could not:
the AppImage only started on a machine that already had a full desktop stack
installed. Three distinct causes, all from bundling Debian's builds of
libraries that Debian builds for a co-versioned system, which is the opposite
of an AppImage's situation.

1. Hard-linked backends. Debian's libSDL2 lists libpulse, libasound, libX11
   and libwayland-client as DT_NEEDED rather than dlopening them, so the
   loader demanded all four at startup; the CI job failed with
   "libpulse.so.0 => not found". Debian's OpenAL does the same through
   libsndio, which itself hard-links libasound. Built from source with
   --enable-*-shared and ALSOFT_DLOPEN, both dlopen their backends, so the
   image now runs on a Wayland-only session, a KMSDRM handheld with no X
   server, or a box with ALSA and no PulseAudio.

2. A stray link. Debian's libtheoradec is linked against libcairo, which
   drags in X11, xcb, fontconfig and freetype for a video decoder.
   --disable-examples leaves it needing only libogg.

3. SONAME collision with the host. OpenAL dlopens ALSA, ALSA's config loads
   its PulseAudio hook plugin, and that plugin pulls the host's libsndfile
   into the process. libsndfile links libogg, libvorbis and libmpg123 -- the
   same three we bundle -- and since the loader resolves a SONAME once per
   process it bound to our bullseye copies. A bullseye libmpg123 has no
   mpg123_info2 (added in 1.32), so the plugin failed to relocate, ALSA
   config collapsed, and the game ran with no audio device at all. Building
   them current means our copies satisfy the host's libsndfile instead of
   starving it.

The general rule, now stated as an assertion instead of a comment: never
bundle a library the host's own stack may also load unless ours is at least
as new as theirs. build_appimage.sh fails if any shipped object hard-requires
anything beyond glibc, libstdc++ and the font stack, and CI re-checks it on
the extracted artifact.

Host requirements drop from "a working desktop" to glibc 2.29+, libstdc++,
libfreetype6 and zlib. Bundled libraries drop from 13 to 10: libcairo,
libpixman and libsndio are gone entirely.

Verified on a Raspberry Pi 5 (trixie, Wayland): boots, imports, plays, and
audio works -- SDL 2.30 now picks the native Wayland backend rather than
falling back to XWayland as bullseye's 2.0.14 did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:24:53 -04:00
Andrew Quenehen 1bfc89fdd4 Fix Switch OTA parser for GitHub releases/latest JSON.
Scan asset objects by brace bounds instead of an 800-byte window so
browser_download_url survives fat uploader blocks; mirror logic in Lua
and add realistic API fixtures.
2026-08-05 15:15:44 -03:00
ratherDashing 24c5114745 Ship a Linux arm64 (aarch64) AppImage
scripts/build.sh's `linux` target only ever produces x86_64: it unpacks
LOVE's official love-11.5-x86_64.AppImage and re-fuses game.love into it.
There is no aarch64 equivalent to unpack -- LOVE 11.5 publishes win32,
win64, macOS, Android, iOS and exactly one x86_64 AppImage -- so arm64
desktop Linux (Raspberry Pi 4/5, Armbian, arm64 VMs on Apple Silicon) had
no artifact at all.

Compile LOVE 11.5 from the official linux-src tarball instead, inside a
Debian bullseye arm64 container, and assemble the AppImage from scratch.
Both pinned inputs (the LOVE source tarball and the AppImage type-2
runtime, on a dated tag rather than `continuous`) are SHA-256 verified on
the host, so the container runs with no network access.

Bullseye is the compile environment, not a claim about where the artifact
runs: glibc is backward but not forward compatible, so linking against the
oldest supported glibc is the only thing that makes one artifact work
everywhere. The binaries come out needing only glibc 2.29 / GLIBCXX_3.4.21,
covering Raspberry Pi OS bullseye through trixie and Ubuntu 20.04 onward.

The dependency walker copies in LOVE's own libraries and leaves the
driver-coupled, loader-coupled and font-stack libraries to the host. That
last category is not cosmetic: Debian's libtheoradec is linked against
libcairo, so a host cairo gets loaded into the process, and because the
loader resolves one SONAME once per process it then binds to whatever
libfreetype we bundled -- bullseye's 2.10.4 has no FT_Get_Transform, which
cairo 1.18 needs, and the game died at startup with a symbol lookup error.
Excluding the whole font stack makes the process self-consistent.

CI gets three path-gated jobs: an offline selftest on ubuntu-latest (pins,
the host-arch guard, the exclude list, the AppRun fusion contract), a real
build on ubuntu-24.04-arm that asserts the layout, that every bundled
object resolves under AppRun's LD_LIBRARY_PATH, and that the glibc floor is
still <= 2.31, and a release job that reuses the shared game.love payload.
None of it needs secrets or self-hosted hardware, so it runs on fork PRs.

Verified end to end on a Raspberry Pi 5 (Debian trixie, Wayland): the
launcher boots from the AppImage and renders correctly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:39:48 -04:00
Andrew Quenehen ae5276b2d1 Show the running app version on the Switch launcher header.
Add an NX-only yellow version chip so players can confirm which build is on the microSD after OTA or zip updates.
2026-08-05 02:08:20 -03:00
Andrew Quenehen 4dd4c5c463 Add launcher-styled framebuffer UI for Switch OTA prompts.
Replace the console flash with a quiet branded screen (RGB rail, logo, A/B buttons) that only appears when an update needs confirm.
2026-08-05 02:03:51 -03:00
Andrew Quenehen cafa6f3d61 Add Switch OTA launcher with unified SD zip and quiet UI.
Ships a dual-NRO native launcher that checks GitHub Releases, updates both NROs from gen1recomp-*-switch.zip with matching NACP versions, and stays silent unless an update needs confirm.
2026-08-05 01:48:39 -03:00
691 changed files with 246504 additions and 5521 deletions
+3 -1
View File
@@ -22,12 +22,14 @@ body:
id: game
attributes:
label: Which game were you playing
description: Pick every version you saw the bug in.
description: Pick every version you saw the bug in. Use N/A if it isn't game-specific.
multiple: true
options:
- Red
- Blue
- Yellow
- Gold
- N/A
validations:
required: true
+3 -2
View File
@@ -30,13 +30,14 @@ body:
id: game
attributes:
label: Which game is this about
description: Pick every version it applies to.
description: Pick every version it applies to. Use N/A if it isn't game-specific.
multiple: true
options:
- Red
- Blue
- Yellow
- Not version-specific
- Gold
- N/A
validations:
required: true
+3 -2
View File
@@ -27,13 +27,14 @@ body:
id: game
attributes:
label: Which game is this for
description: Pick every version the mod should cover.
description: Pick every version the mod should cover. Use N/A if it isn't game-specific.
multiple: true
options:
- Red
- Blue
- Yellow
- Not version-specific
- Gold
- N/A
validations:
required: true
+115 -1
View File
@@ -199,7 +199,7 @@ jobs:
echo "changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(ports/uwp/|scripts/build_xbox_uwp\.sh$|scripts/xbox-uwp/|scripts/pack_love\.sh$|\.github/workflows/(ci|release)\.yml$|src/core/Platform\.lua$|src/import/(CacheFs|RomImporter)\.lua$|src/update/Check\.lua$|tests/engine/(platform_nx|uwp_native_picker)_test\.lua$|tests/rom_importer_double_pick_test\.lua$)'; then
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(ports/uwp/|scripts/build_xbox_uwp\.sh$|scripts/xbox-uwp/|scripts/pack_love\.sh$|\.github/workflows/(ci|release)\.yml$|src/core/Platform\.lua$|src/import/(CacheFs|LauncherView|RomImporter)\.lua$|src/update/Check\.lua$|tests/engine/(platform_nx|uwp_baseroms|uwp_native_picker)_test\.lua$|tests/rom_importer_double_pick_test\.lua$)'; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
@@ -260,6 +260,117 @@ jobs:
if-no-files-found: error
retention-days: 7
linux-arm64-changes:
name: detect Linux arm64 changes
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.paths.outputs.changed }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- id: paths
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.sha }}
run: |
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(scripts/build_linux_arm64\.sh$|scripts/linux-arm64/|scripts/pack_love\.sh$|docs/linux-arm64-build\.md$|\.github/workflows/(ci|release)\.yml$)'; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
linux-arm64-selftest:
name: Linux arm64 offline selftest
needs: linux-arm64-changes
if: needs.linux-arm64-changes.outputs.changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# Deliberately on x86_64: everything this gate checks (pins, the
# host-arch guard, the dependency exclude list, the AppRun fusion
# contract) is answerable without a container or an aarch64 machine,
# so the slow native job below only ever starts on a sane tree.
- name: Linux arm64 offline selftest
run: bash scripts/linux-arm64/selftest_build_linux_arm64.sh
linux-arm64-build:
name: Linux arm64 AppImage build
needs: [linux-arm64-changes, linux-arm64-selftest]
if: |
always()
&& needs.linux-arm64-changes.outputs.changed == 'true'
&& needs.linux-arm64-selftest.result == 'success'
# No fork restriction, unlike switch-build: this needs no secrets and no
# self-hosted hardware, just GitHub's free arm64 runner for public repos,
# so contributors get the same coverage on their own PRs.
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v7
- name: Build the aarch64 AppImage
run: |
set -euo pipefail
scripts/build_linux_arm64.sh --version 0.0.0
- name: Verify the AppImage is self-contained and bullseye-compatible
run: |
set -euo pipefail
image="dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage"
# --appimage-extract needs no FUSE, so this works on a runner
# without /dev/fuse and still exercises the real payload.
"$image" --appimage-extract >/dev/null
for required in AppRun bin/love game.love lib/liblove-11.5.so; do
[ -e "squashfs-root/$required" ] \
|| { echo "::error::AppImage is missing $required"; exit 1; }
done
# Every bundled object must resolve once AppRun's LD_LIBRARY_PATH is
# applied; an unresolved soname here is a user-visible launch crash.
#
# This runs on a HEADLESS runner on purpose, and that is the point.
# The first version of this build bundled Debian's SDL2, which
# hard-links libpulse/libasound/libX11/libwayland, so it only ever
# started on a full desktop -- a bare runner is what exposed it.
missing="$(LD_LIBRARY_PATH="$PWD/squashfs-root/lib" \
ldd squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \
| grep 'not found' || true)"
[ -z "$missing" ] || { echo "::error::unresolved deps:"; echo "$missing"; exit 1; }
# Nothing may hard-link a driver, session or audio-stack library:
# those must be reached through dlopen so the AppImage runs on a box
# with only ALSA, only Wayland, or only KMSDRM.
linked="$(for f in squashfs-root/bin/love squashfs-root/lib/*.so*; do
objdump -p "$f" 2>/dev/null | awk '/NEEDED/{print $2}'
done | sort -u | grep -E '^lib(pulse|asound|X11|wayland|GL|EGL|drm|gbm|xcb|cairo|sndio|dbus)' || true)"
[ -z "$linked" ] \
|| { echo "::error::these must be dlopened, not linked:"; echo "$linked"; exit 1; }
# The whole point of compiling on bullseye. If a future change moves
# the builder to a newer base, the glibc floor silently rises and
# every user on an older distro gets "GLIBC_2.xx not found" -- catch
# it here instead of in a release.
floor="$(objdump -T squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \
| grep -o 'GLIBC_[0-9.]*' | sort -V | tail -1)"
echo "highest required glibc symbol version: $floor"
[ -n "$floor" ] \
|| { echo "::error::found no versioned glibc symbols -- objdump read nothing"; exit 1; }
highest="$(printf '%s\n' "$floor" "GLIBC_2.31" | sort -V | tail -1)"
[ "$highest" = "GLIBC_2.31" ] \
|| { echo "::error::AppImage requires $floor, above the bullseye 2.31 floor"; exit 1; }
- name: Upload the AppImage
uses: actions/upload-artifact@v7
with:
name: gen1recomp-linux-arm64
path: |
dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage
dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage.sha256
if-no-files-found: error
retention-days: 7
headless:
name: headless suites (no ROM)
runs-on: ubuntu-latest
@@ -272,6 +383,9 @@ jobs:
- name: install luajit
run: sudo apt-get update && sudo apt-get install -y luajit
- name: install Pillow
run: python3 -m pip install --upgrade pillow
- name: interpreter version
run: luajit -v
+48 -1
View File
@@ -140,6 +140,36 @@ jobs:
if-no-files-found: error
retention-days: 1
linux-arm64:
name: build Linux arm64 AppImage
needs: [version, love-payload]
# GitHub's free arm64 runner for public repos. It has to be arm64: the
# AppImage compiles LÖVE natively inside a Debian bullseye arm64
# container, and the qemu-emulated alternative takes hours.
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v7
- name: Download shared payload
uses: actions/download-artifact@v8
with:
name: gen1recomp-release-love
path: .bazinga/work
- name: Build Linux arm64 AppImage
run: |
set -euo pipefail
scripts/build_linux_arm64.sh \
--version "${{ needs.version.outputs.version }}" \
--game-love .bazinga/work/game.love
- name: Upload Linux arm64 release
uses: actions/upload-artifact@v7
with:
name: gen1recomp-linux-arm64-release
path: |
dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage
dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage.sha256
if-no-files-found: error
retention-days: 1
xbox-uwp:
name: build Xbox UWP release
needs: [version, love-payload]
@@ -222,7 +252,7 @@ jobs:
}
release:
needs: [version, xbox-uwp]
needs: [version, xbox-uwp, linux-arm64]
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
steps:
@@ -367,6 +397,13 @@ jobs:
name: gen1recomp-xbox-uwp-release
path: dist/xbox-uwp
- name: Download Linux arm64 release
if: github.repository == 'bryanthaboi/gen1recomp'
uses: actions/download-artifact@v8
with:
name: gen1recomp-linux-arm64-release
path: dist/linux-arm64
- name: Stage release assets
if: github.repository == 'bryanthaboi/gen1recomp'
id: assets
@@ -379,6 +416,15 @@ jobs:
cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip"
cp "dist/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.zip"
# arm64 desktop Linux (Raspberry Pi, Armbian, arm64 VMs). Built on
# its own runner because LÖVE publishes no aarch64 binary and the
# AppImage has to be compiled natively; ships as a runnable
# AppImage rather than a zip so `chmod +x && ./it` just works.
arm64_appimage="dist/linux-arm64/gen1recomp-${v}-linux-arm64.AppImage"
[ -f "$arm64_appimage" ] || { echo "::error::$arm64_appimage not found (expected from the linux-arm64 job)"; exit 1; }
cp "$arm64_appimage" "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
chmod +x "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
apk="$(find dist/android/debug -name '*.apk' | head -1)"
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/debug"; exit 1; }
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
@@ -512,6 +558,7 @@ jobs:
"dist/release/gen1recomp-${v}-macos.zip"
"dist/release/gen1recomp-${v}-windows.zip"
"dist/release/gen1recomp-${v}-linux.zip"
"dist/release/gen1recomp-${v}-linux-arm64.AppImage"
"dist/release/gen1recomp-${v}-android.apk"
"dist/release/gen1recomp-${v}-ios.ipa"
"dist/release/gen1recomp-${v}-switch.zip"
+18 -1
View File
@@ -31,12 +31,29 @@ mobile/ios/love-src/
mobile/ios/cache/
mobile/ios/build/
# love-nx vendor binaries (fetch per docs/switch-development.md; also covered by .*)
# love-nx vendor binaries (fetch per docs/switch-build.md; also covered by .*)
.bazinga/love-nx/
# Final packaged build artifacts (mac/win/web/android/ios/switch) — see scripts/build.sh
/dist/
# Switch OTA launcher build outputs
ports/switch/ota-launcher/build/
ports/switch/ota-launcher/build-host/
ports/switch/ota-launcher/*.nro
ports/switch/ota-launcher/*.nacp
ports/switch/ota-launcher/*.elf
ports/switch/ota-launcher/*.map
ports/switch/ota-launcher/romfs/logo.rgba
ports/switch/ota-launcher/romfs/logo.png
ports/switch/ota-launcher/romfs/cacert.pem
ports/switch/ota-launcher/romfs/ota-bootstrap.nro
ports/switch/ota-bootstrap/build/
ports/switch/ota-bootstrap/*.nro
ports/switch/ota-bootstrap/*.nacp
ports/switch/ota-bootstrap/*.elf
ports/switch/ota-bootstrap/*.map
# Legacy manual convenience-copy location (superseded by /dist/android/)
mobile/dist/
+113 -6
View File
@@ -138,7 +138,111 @@ qualifies only if every record it writes lands in `text`, `strings` or
permission. Anything else and it is an ordinary content mod that happens to
ship text.
### 4. `mod.card`
### 4. `games` (and the legacy `gen2compat`)
Pokemon Gold is Gen 2, and it runs its own battle engine, overworld, script
VM and save format. The mod API is shared across both generations (same hook
names, same event names, same registry names) but Gold cannot serve all of it
yet, so Gen 2 is opt-in. Say which games the mod is for:
```json
"games": ["gen1", "gen2"]
```
Each entry is a version id (`"red"`, `"blue"`, `"yellow"`, `"gold"`), a
generation (`"gen1"`, `"gen2"`) or `"all"`;
`src/mods/ModTargets.lua` resolves them off `GameVersion.ORDER` so nothing
restates the game list. `python3 tools/modkit.py scaffold my_mod --games
gen1,gen2` writes the key for you. The mod still installs to one directory,
`mods/<id>/`, shared by every game -- targeting is declared, never filed.
Absent means Gen 1 only, which is what every mod written before the key existed
was tested as. `"gen2compat": true` is the legacy spelling, still accepted and
purely additive (it *adds* the Gen 2 games), so no manifest can lose a game it
already ran on. On a Gold boot a mod claiming no Gen 2 game is not loaded at
all: the manager lists it as `ENABLED (NOT THIS GAME)` and says why, because a
mod that half-applies reads as a broken mod. Claim Gen 2 once you have actually
run your mod on Gold.
Every token is enforced, per game: the loader gates on the same
`ModTargets.supports` answer both mod surfaces draw, so `"games": ["blue"]`
really does not load on Red and the skip line is the launcher's line, `For
Blue, not Red`, and `"games": ["gold"]` alone does not load on Red either. A
manifest with neither key still covers every Gen 1 game, so nothing written
before the key existed changes behavior; list both generations or say `"all"`
when you mean everywhere.
`docs/mod-api-gen2-compat.md` is the compatibility matrix: what works on Gold
today (40 of the 46 registries, 40 event and 43 hook names shared with Gen 1,
and 24 Gen 2-only ones), which registries have no Gen 2 home and drop their
writes with a report, and which hooks and events are still to come.
`docs/preparing-your-mod-for-gen2.md` is the step-by-step migration guide for a
Gen 1 mod, and it is the one to start from.
Two consequences worth knowing before you claim Gen 2.
**Dependencies are contagious.** A mod whose hard dependency does not run here
is left out too, with the dependency's own wording (`depends on X, which does
not run here (For Blue, not Red)`). It is reported as a skip, not as a failure,
and neither mod lands on the boot error list, but the mod does not run, so
every hard dependency has to cover the same games.
**The player can override you.** The claim is yours, and a mod written before
the key existed can never carry one, so the manager's detail pane offers
`TRY HERE ANYWAY` for any mod that does not claim the game being played. It
persists per game in `options.modsGen2[id][version]` and takes effect on the
next boot; forcing a mod onto Red does not force it onto Gold. A forced mod
loads normally and keeps a note saying its author never verified it here.
**Prefer the API on Gold, but the Gen 1 names still work.** Gen 2 is a
parallel module tree behind `src/core/Game2.lua`. In new code take the live
game from `mod.game` (or the `game.ready` payload, or any `ui.*` hook's first
argument) and the world from `mod.world`; both resolve per generation, and
neither needs `engine_internals`.
For the mods written before Gold existed, a require made from a mod's own file
is answered on a Gold boot by an adapter presenting the Gen 1 API over Gen 2
internals. Fifteen names are served -- `src.core.Game`,
`src.world.OverworldController`, `src.world.Map`, `src.world.NPC`,
`src.world.Collision`, `src.world.WorldAPI`, `src.world.PikachuFollower`,
`src.world.FieldDefaults`, `src.pokemon.Boxes`, `src.script.ScriptRunner`,
`src.ui.PartyMenu`, `src.ui.StartMenu`, `src.ui.OptionsMenu`, `src.ui.BoxMenu`
and `src.battle.BattleState`. `src/mods/Gen2Compat.lua` is the full table and
publishes what it covers through `Gen2Compat.coverage(name)`, whose members are
`backed`, `warned` or `absent`. A name with no adapter (`src.script.Commands`,
`src.ui.OptionRows`) is reported against the mod that required it, and a member
an adapter cannot back is absent or logs once rather than answering wrongly.
Things no adapter can fix, all mod-side: a hardcoded version allow-list
(`GameVersion.get() == "red" or ...`) excludes you from Gold by construction;
Gold's builtin screen ids carry a `Gen2` prefix, so a string match on
`"BoxMenu"` matches nothing there; a write to a field on a live Gen 2 menu
instance is inert; and `map.warpAt` is a table on Gen 1 and a method on Gold,
so indexing it raises. Each has a route that works on both generations, in
`docs/preparing-your-mod-for-gen2.md`.
Check it statically, then load it headless:
```sh
python3 tools/modkit.py gen2check mods/my_mod
```
```lua
local run = T.sdk.loadMod("mods/my_mod", { generation = 2 })
T.eq(run.mod and run.mod.state, "loaded",
"runs on gen 2: " .. tostring(run.mod and run.mod.skipReason))
T.eq(#run.errors, 0, "and loads with no boot errors")
```
Assert the state, not only the error count: a gate skip is deliberately not an
error, so `#run.errors == 0` passes for a mod that never ran a line.
`gen2check` answers `will load`, `will load but degrade` or `will not work`,
with a `MK4xx` finding per site and an `unresolved:` note, with a file and a
line, for every reach a static scan could not follow. Neither substitutes for a
real Gold boot.
### 5. `mod.card`
The manifest is the *engine's* contract: identity, load order, dependencies,
permissions, profile. The card is the *human-facing* one: who made this,
@@ -159,7 +263,7 @@ Two fields deserve their own note:
distributed mod never carries ROM-derived bytes, not even in its preview
images.
### 5. Tags
### 6. Tags
Lowercase kebab strings, open vocabulary. The showcase generator
lowercases and de-dupes. A recommended starting set: `beginner`,
@@ -217,12 +321,15 @@ registry or a new schema field lands with its catalog entry in the same PR
and the generator runs clean:
```sh
luajit tools/gen_registry_docs.lua # in-repo default
luajit tools/gen_registry_docs.lua ../project.wiki # the wiki checkout
luajit tools/gen_registry_docs.lua # docs/modding/reference/registries.md
luajit tools/gen_registry_docs.lua ../project.wiki # Reference-Registries.md in a wiki checkout
```
The prose reference lives in the GitHub wiki; the generated pages are
written into a checkout of it, so they cannot drift from the engine.
With no argument it writes inside the repo, which is the copy `python3
tools/modkit.py docs` regenerates and `--out` copies from. Pass a directory
(or set `POKEPORT_DOCS_DIR`) to write the wiki's flat page name into a wiki
checkout instead. The prose reference lives in the GitHub wiki; both copies
come off `src/mods/Schemas.lua`, so neither can drift from the engine.
### 5. Deprecation etiquette
+275
View File
@@ -0,0 +1,275 @@
# Gold port: what is still open after the 2026-08-09 asm parity runs
Two multi-agent runs diffed the Gold port against the `pokegold` disassembly
(`../pokegold`) and fixed what diverged. Round 1 confirmed 51 defects across 14
paired clusters; round 2 confirmed 41 more across 13, including one crash. A
third round (2026-08-10) closed the mod-API boundary: see "The 2026-08-10
mod-API round" below for what that opened and closed.
Every tier is green at the end of all three: `run_engine` 131/131, `run_tests`
ALL TESTS PASSED, `run_modkit` 6/6, `run_save_editor_tests` 650/0,
`run_link_tests` ALL PASSED.
This file records only what is NOT done, so nobody re-derives it. It is a
working handoff doc in the shape of `KANTO-CONTINUE.md`, not user documentation.
## Do this first or none of it is visible
**Re-import the Gold cache.** Seven fixes live in the extractor and the manifest
tool, so they do nothing until the ROM is re-imported. This was verified still
outstanding on 2026-08-10: the default identity's
`gold/data/generated/menu_gfx.lua` has no `trainerPics` key at all, so a trainer
battle still opens on the mon rather than the trainer even though the Lua half
is fixed and correct.
- trainer frontpics (`TrainerPicPointers` walk in `extractMenuGfx`), which is
the missing trainer intro image at the start of a trainer battle. The Lua
side is now right and re-import is the ONLY thing left: `Trainers.lookup`
returns a numeric `class` (36) beside a `classId` (`BUG_CATCHER`), and both
`menu_gfx.trainerPics` and `palettes.trainers` are keyed by the constant, so
the lookup had to move to `classId`. On a cache that carries the pics the
frontpic renders in the class's own palette.
- egg assets: `battle/front/egg.png` and `menu/egg_hatch.png`, feeding the
hatch cutscene and the summary screen's egg page. A fallback landed in the
meantime (`ICON_EGG` frame 0, which resolves to a real 16x16 frame), so the
egg page draws SOMETHING on a stale cache; `tools/rom_manifest_gold.json`
carries `EggPic` at `[20, 31363]` and that stream decompresses to exactly 400
bytes (a 5x5-tile pic), so a fresh import takes the real path.
- the Pokegear phone icon. `_LoadFontsExtra` (`engine/gfx/load_font.asm:8-15`)
puts `FontsExtra_SolidBlackAndUpArrowGFX` at `$60`/`$61` and
`PokegearPhoneIconGFX` at `$62`, ON TOP of `FontExtra`'s BOLD_A/B/C.
`src/import/RomExtractorGen2.lua:342-356` reads `FontExtra` straight in and
then blits only `Frames` over `$79-$7E`, so `$62` is still a bold "C" -- which
is what the new caller box draws where the telephone glyph belongs. Fix is two
files: add the two GFX symbols to `tools/rom_manifest_gold.json`, then blit
them at `$60`/`$61`/`$62` in the loop that already handles `Frames`.
- `PREDEFPAL_UNOWN_PUZZLE = 76`, which is why the Ruins of Alph puzzle rendered
grayscale instead of brown
- the tilemap pad `0x7f -> 0x4f` in `readTilemapRLE` / `readFlatTilemap`
- held-item icons (`HeldItemIcons` in the manifest, `out.heldItem` in
`extractIcons`), which is the held-item marker in the party menu
- the NPC-trade rows' `item` field, which is what made the Violet City Onix's
Bitter Berry print as "83"
## Open: real unknowns, each needs a cache and a driver run
1. **Magnet train renders a blank field.** Pushing `World:magnetTrain` directly
shows nothing for 90+ frames (see shot `03-magnet-train.png`). The screen's
surround is correct now; what was never investigated is whether
`drawBackground` needs setup that the direct push skips. This cannot be
settled by reading source.
2. **The NPC that introduces Unown never appears.** Round 1 could not produce a
port ref that diverges from the asm, so it is filed `cannot_locate`, not
fixed. The cart chain is: solve a chamber puzzle, then
`setmapscene RUINS_OF_ALPH_INNER_CHAMBER, SCENE_RUINSOFALPHINNERCHAMBER_STRANGE_PRESENCE`
from `maps/RuinsOfAlphKabutoChamber.asm`. Walk that chain live rather than
statically: the static read came back clean twice.
3. **Two pic sites still resolve an Unown off the species.**
`src/ui/gen2/PhotoStudio.lua:102` and `src/ui/gen2/EvolutionAnim.lua:460` use
`def.spriteFront` without going through `Unown.formSprite`. For EvolutionAnim
this is almost certainly dead (Unown does not evolve). For PhotoStudio it is
genuinely unclear: the cart's `engine/events/print_photo.asm` does not call
`GetUnownLetter` at all, so the port may already match. Check before changing.
4. **Unown dex registration on obtain paths.** `engine/pokemon/evolve.asm:310`
runs `GetUnownLetter` then `callfar UpdateUnownDex` after
`SetSeenAndCaughtMon`, guarded by `cp UNOWN`. That is dex bookkeeping, not
rendering, and it was never audited across every path that gives the player a
mon. `GetUnownLetter` has 20 call sites on the cart; the port covers the pic
ones.
## The 2026-08-10 mod-API round
Six lanes closed the Gen 1 / Gen 2 mod-API boundary and a batch of reported
gameplay bugs. What that round **closed**, so nobody re-opens it:
- Fifteen seams that had a Gen 2 site but no entry in the parity gate are now
listed and asserted: the four `intro.oak_speech.*` events plus the
`intro.oak_speech.build` hook, `battle.overlay`,
`battle.low_health_alarm`, `battle.catch_exp`, `pokemon.sprite`,
`input.step`, `input.pointer`, `render.zones`, `render.compose`,
`render.letterbox`, `render.hud`. Five more (`intro.boot.*`) are listed as
Gen 2-only. `tests/engine/gate_gen2_mod_api.lua` is 893/893.
- Two registries un-gated on Gold after their consumer landed:
`battle_sprite_scales` (`src/ui/gen2/BattleState.lua:imageScale`) and
`render_pipelines` (`src/core/Game2.lua:load` installs
`src/render/Pipelines.lua` on the merged dataset after `mods:load`). Both
keep the SHARED Gen 1 target, so one mod record serves both games. 40 of the
46 registries are now available on Gold.
- `hook:render.zones` came off the `gate_meta_coverage.lua` DEBT ledger, which
is now four entries, all M7/M12 link and give-mon seams.
What that round **left open**, each verified on 2026-08-10:
1. **`render_pipelines`' `drawWorld` half is inert on Gold.** Gold's overworld
draws straight to the window rather than into a canvas the way
`src/world/OverworldController.lua` hands one to `Pipelines.drawWorld`, so a
drawWorld-only pipeline renders nothing. It is not left switched on
pretending: `Game2:load` retires a restored level for one and re-applies
Tilt from the option the exclusion just cleared, leaving
`options.pipelines` untouched so the mode returns the day Gold grows a world
canvas. Related: `Pipelines.rows` is read only from
`src/ui/OptionsMenu.lua`, so a pipeline on Gold has a hotkey and no OPTION
row.
2. **`transitions` stays gated and should.** `src/ui/gen2/BattleTransition.lua`
keys `STYLES` as a boolean SET of the four cart wipes (`spin`, `speckle`,
`zoom`, `sine`), not the `{ frames, draw, sound, flash }` record the
registry carries, and there is no styleDef lookup a mod id could reach --
a registered style would fail the membership test and fall back to vanilla.
Un-gating it before that changes would be the silent no-op the routing table
exists to prevent. Same for `rulesets`, `field`, `text_pointers`,
`link_fields` and `map_scripts`; `docs/mod-api-gen2-compat.md` carries the
per-registry reason.
3. **`src/core/Game2.lua:joystickremoved` is half of Gen 1's.**
`src/core/Game.lua:867` does `self:recoverInput("joystickremoved", joystick)`
AND `TouchControls:joystickremoved()`; Game2 does only the pad half, so a
controller unplugged mid-hold leaves Gold's Input state stranded and a held
direction walks forever. It was a total noop before, so this is not a
regression -- it is a seam finished halfway. One line.
4. **`Game2` noops five joystick callbacks.** `joystickpressed`,
`joystickreleased`, `joystickaxis`, `joystickhat` and `joystickadded` are
assigned `noop`, so a stick with no SDL game-controller-database entry
reaches neither Input nor `GamepadMap.RAW_BUTTON_BINDINGS` on Gold. Gen 1
handles all four (`src/core/Game.lua:764-797`). A DualSense is
SDL-recognized and takes the gamepad path, so this is not the reported
SELECT bug -- it is the raw-stick fallback.
5. **`Game2:gamepadpressed` has no SELECT-held guard.** Gen 1
(`src/core/Game.lua:686-694`) suppresses the shoulder GAME SPEED cycle while
SELECT is held, because Select+L is a display chord on NX. Harmless today
(Gold has no display chord) but the two paths have diverged.
6. **`hideCallerBox`'s fallback path skips the screen seams.**
`src/script/gen2/CallAsm.lua:217-231` pops through `stack:pop` when the
caller box is top (which it always is on a normal call, since `PhoneRing`
runs `closetext` before `InitCallReceiveDelay`) but falls back to a bare
`table.remove(states, index)` otherwise, which raises neither `exit` nor
`screen.popped`. Only a mod screen pushed over the box can reach it, and
then a listener that saw `screen.pushed` never sees the pop.
7. **`src/core/gen2/Roamers.lua:338` says the wrong thing.** Its comment says
"the shared `encounter.species` hook still runs downstream and is where a
mod changes what appears". It does not: `World:tryWildEncounter` calls
`startBattle` on a roamer hit and RETURNS before it reaches
`World:rollEncounter`. The doc's partial-coverage list is right and the
comment is wrong; correct the comment, not the doc.
8. **`src/world/gen2/World.lua:4318` still calls the pack's SEL row something
"this port has not built".** The pack submenu now offers SEL. Stale comment.
Three partial coverages that are documented and still true, repeated here
because "the hook exists" is not "the hook sees everything":
`encounter.roll` / `encounter.species` are not wired into `World:tryHeadbutt`,
`World:rockMonEncounter` or the roamer path; `src/ui/gen2/BattleState.lua`
builds a flat `opts` for `Catching.attempt` with no `data` in it, so a
mod-registered ball is readable through `Catching.recordFor` but is not
resolved at the real throw site; and no Gen 2 UI file reads
`Battle.statusRecordFor(data, status).hudLabel`, so a mod status shows no label
in the battle HUD, the party menu or the summary page.
## Open: cleanups declined on purpose
These were skipped with reasons during the wiring pass. They are listed so the
reasons survive, not because they are pending work.
1. **`Vm:showRaw(body, stay)`** was proposed and declined. `showRawHeld` in
`Specials.lua` already answers the text lookahead correctly, so this trades
one working shape for another.
2. **`tests/drivers/gold_egg_hatch.lua` summary push** was declined as
redundant: `gold_egg_hatch_shots.lua` already photographs both the cutscene
and the summary page.
3. **Two `Battle.lua` divergences** were filed as notes by the file's own owner
rather than fixed, because no reported symptom drives either: text printed
for an already-statused target, and Future Sight taking STAB, type
effectiveness and weather. Both are real divergences from the cart. Fix them
when something actually depends on them.
## Reported, investigated, and NOT defects: do not re-chase these
Each of these came in as a bug report and came back disproved against the asm.
Re-opening one costs another full investigation, so the reasoning is kept here.
- **SFX pointer table misalignment.** The table is aligned end to end. 188 `dba`
rows with no skips or padding, 188 constants, verified through the manifest
scraper, the extractor's `*3` stride, and 8 ids spot-checked against the live
cache. Two things that look like misalignment are how the cart is built:
`SFX_GET_EGG_UNUSED` and `SFX_GET_EGG` both point at `Sfx_GetEgg`, and
`Sfx_ReadText` / `Sfx_ReadText2` share one header address. **Do not repoint,
re-stride, or add an offset to the SFX, music, or cry tables.**
- **Bind letting you pick other moves.** This is Gen 1 behavior and does not
hold for Gen 2. `BattleCommand_TrapTarget` (`effect_commands.asm:5568-5605`)
writes only the TARGET's wrap count and trapping move. It touches nothing on
the user, so the user is free to switch moves. The port is correct. The real
Gen 2 move locks (Rollout, Thrash, Petal Dance) were genuinely missing and
have been added.
- **The Slowpoke Tail salesman not blocking progression.** He is Route 32's
`FISHER4` at (7,70), not an Azalea Town object, and the cart's refusal arm
only prints. `_OfferToSellSlowpokeTail` runs `setscene SCENE_ROUTE32_NOOP`
first, so the coord event never fires again. There is no pushback to port.
- **The rival being named BLUE.** Not reproducible. The whole chain is intact:
special 36 resolves to `H.NameRival`, `NamingScreen:accept` hands the typed
string through, and BLUE is the Gen 1 default that Gold never reaches. A
separate real defect on this seam (the rival being pre-named SILVER before the
officer asks, and the blank-entry fallback) was found and fixed.
- **Not Very Effective dealing 0 damage**, **status not shown in the battle
UI**, and **the Pokedex missing from the Start Menu** all came back
`already_fixed`: the damage floor is `MIN_DAMAGE` added after the cap and
before the type multiply, the HUD prints the status tag where the level goes,
and the menu row is gated on `save.engineFlags[11]` correctly.
## Standing risks, not breakage
- **Four Gold test files are wired into no runner**, so a regression in them
will not turn `run_tests.lua` red: `gold_flag_names_test`,
`gold_route_validate_test`, `gen2_pokegear_unlock_test`,
`gen2_save_export_test`. All four pass standalone. The first two are the
pre-run route validators documented in `KANTO-CONTINUE.md`; the last two need
a `GOLD_CACHE` and are excluded by an in-file comment. This is deliberate, but
it is a coverage hole and worth knowing.
- **`mods/` must stay clean.** The 2026-08-10 round left temporary probe mods
behind mid-run (`mods/tmp_menus`, `mods/zz_verify_seams`); both are gone now
and `mods/` holds only `example_mew_starter`, `example_silly_oak`, `examples`
and `nuzlocke`. A stray `mods/tmp_*` loads on every Gold boot and changes what
a driver measures. `mods/example_silly_oak` carries only a `.modkitignore` and
warns "manifest.json does not exist" on every boot; that is pre-existing
(27 Jul) and unrelated.
- **`tests/drivers/gold_opaque_surround.lua` is stale.** It reads
`game.stack._items`; `src/core/StateStack.lua` has only `.states`. That branch
is covered instead by `gold_center_pc` and the letterbox-per-frame count in
`gold_frame_seams`.
- **`luac` on this machine is Lua 5.5**, so `luac -p` is a weak proxy for the
LuaJIT/5.1 semantics this engine targets. Use `luajit -b <file> /dev/null` for
a real syntax gate.
- **`docs/rfcs/0001-surfing-pikachu-sprite.md` and `0002-screen-render-visible.md`
are deleted** in the working tree, and both are still referenced by name from
live modkit case files that pass. Those deletions predate both parity runs.
Worth resolving before a commit.
## The failure mode that actually bit, worth remembering
Round 1 reported the Yes/No dialogue fix as landed. It was not. Two of its three
lanes wrote their half, and the third left the hook closure at
`World.lua:828` declared as `function(body, onDone)`, so the third argument
`Vm:resume` passes was silently discarded and the entire `stay` implementation
in `World:showText` and `World:askYesNo` was dead code. Lua drops extra
arguments without complaint, so nothing failed and every suite stayed green.
Round 2 caught it only because an investigator probed the boundary empirically
instead of reading both sides and assuming they met. When work is split across
agents by file, **the seams between the files are where the bugs live**, and a
green suite does not prove a seam is connected.
+457
View File
@@ -0,0 +1,457 @@
# KANTO-CONTINUE: executing the never-run half of Gold (sections 19-32)
Deferred from the 2026-08-09 fix campaign by request. This doc is self-contained: the
audit documents it was distilled from are being retired, so everything a future session
needs is here.
## What this is
The Johto main line (walkthrough sections 00-18) is proven: the route bot beats Champion
Lance with every row passing. Kanto (sections 19-32) is extracted and wired but has
NEVER been executed by any bot row, driver, or human. Every entry below reads clean in
static review (cache + code + cart source were cross-checked adversarially); the risk is
purely runtime. The job: make each beat actually run, fix what breaks, and leave
executable evidence (bot rows or assertion drivers) behind.
## State after the 2026-08-09 campaign (what changed under you)
- ~80 defects fixed across the port; 91/91 gen2 suites, engine 128/128, T3 all green.
- The post-game handoff WORKS now: post-E4 credits end at the title screen, and
CONTINUE consumes the post-game spawn (HallOfFame.consumePostGameSpawn): after Lance
you continue in NEW_BARK_TOWN, after Red at SILVER_CAVE_OUTSIDE (23,20). Section 19
is reachable by normal play for the first time.
- The Snorlax wake chain is fully functional (engine-flag bridge unlocks the Pokegear
and EXPN card; the tuned radio song persists after closing the gear; SnorlaxAwake
fires; proven live by tests/drivers/gold_radio_persist.lua).
- Landed systems Kanto beats depend on: phone random/outgoing calls, field item use
(Escape Rope/Dig via save.backupWarp), Repel, swarms, Rock Smash encounters,
waterfall current tiles, roamer scatter on CONTINUE, trap volatiles + FORCESHINY
battle types (beast catching in section 32 is now mechanically possible), badge
boosts, held items, specialty balls (Lure Ball needs battleType 'fish', already
stamped by updateFishing).
- NPC HOUR WINDOWS are now enforced (they were not when the Johto route was authored).
Any Kanto row that talks to a time-gated NPC must pin the clock (POKEPORT_GOLD_HOUR /
POKEPORT_GOLD_DAY) or handle absence. Only 11 hour-gated objects exist on the whole
cart: CELADON_GAME_CORNER 6/7, GOLDENROD_GAME_CORNER 4/5, MOUNT_MOON_GIFT_SHOP 1-4,
and the three Moms in PLAYERS_HOUSE_1F; Celadon and Mt Moon are the Kanto exposures.
- The game clock now anchors on every new game (cart InitClock semantics), so time of
day no longer follows the host wall clock in driver runs; POKEPORT_GOLD_HOUR pins
the anchored base too. Runs are reproducible across day/night; a pre-existing flake
class (night runs catching HOOTHOOT where day runs catch PIDGEY, shifting grind
levels) died with it.
- Battles hit harder both ways since the cart MIN_DAMAGE floor landed; grind budgets
tuned for the old numbers may need a nudge.
## Known bot weaknesses to expect (verified pre-existing, not campaign regressions)
- Pathfinding pocket: travel into ILEX_FOREST / SLOWPOKE_WELL bounces (probe
CHERRYGROVE_CITY>ILEX_FOREST reports no route), and route row 16.54 gives up the
walk to VICTORY_ROAD_GATE with the identical TELEPORT signature in every logged run.
Kanto routing should waypoint doors explicitly per the ops recipes above.
- Optional buy rows 04.5d and 18.g4/18.g5 fail whenever the wallet is empty at that
point in the run; they are budgeting gaps in the route, not engine bugs.
- Grind row 08.g (Route 41 water) has almost no margin; with the clock now anchored
its inputs are stable, but a low-level POLIWAG catch can still stretch it.
## How to run things
Identity/cache: gold-dev (~/Library/Application Support/LOVE/gold-dev/gold), rebuilt
2026-08-09 with swarm/rock/roam tables. Checkpoints gold-ckpt-NN.lua exist per section;
gold-ckpt-18-pristine.lua is the post-E4 seed. ROM: ../decprep/Pokemon - Gold
Version.gbc. Cart source: ../pokegold (cite it in any hand-ported code).
Headless love needs a pty on macOS; wrap every run:
perl -e 'alarm 900; exec @ARGV' python3 -c "import pty; pty.spawn(['love','.'])"
Full bot run: POKEPORT_IDENTITY=<id> POKEPORT_GAME=gold POKEPORT_SPEED=200 \
POKEPORT_GOLD_CKPT=1 POKEPORT_GOLD_STALL=20000 POKEPORT_GOLD_LOG=/tmp/b.log \
POKEPORT_DRIVER=tests/drivers/gold_bot.lua love .
Resume a section: POKEPORT_GOLD_RESUME=NN (same identity, prior CKPT=1 run).
Travel probe: POKEPORT_GOLD_PROBE="MAP_A>MAP_B" with tests/drivers/gold_travel_probe.lua.
Map graph: luajit tools/goldwalk/mapgraph.lua path|map|reach|audit; regenerate
tests/drivers/gold/map_regions.lua with `mapgraph.lua graph >` after ANY extractor map
change, then re-check route region indices.
Route validation (before any run): luajit tests/gold_route_validate_test.lua and
luajit tests/gold_flag_names_test.lua.
Suites: GOLD_CACHE="$HOME/Library/Application Support/LOVE/gold-dev/gold" luajit
tests/gen2_<name>_test.lua; tiers run_engine/run_modkit/run_tests.
Reimport after extractor/manifest changes: python3 tools/make_gold_manifest.py, then
POKEPORT_IMPORT_TRACE=1 POKEPORT_IDENTITY=gold-dev POKEPORT_IMPORT_ROM="../decprep/\
Pokemon - Gold Version.gbc" POKEPORT_IMPORT_ONLY=1 POKEPORT_FORCE_IMPORT=1 \
POKEPORT_GAME=gold love .
## Recommended shape of the work
1. Extend tests/drivers/gold/route.lua with sections 19-32 (the row format, ops and
evidence rules are documented in the file head and bot.lua). Seed runs from
gold-ckpt-18-pristine plus the now-working post-credits CONTINUE. `expect` flags are
the only oracles; never make a row load-bearing on a soft-failing map.
2. Highest-risk multi-map scripted chains first: S.S. Aqua voyage, Machine Part chain,
PASS/Magnet Train chain. Each deserves an assertion driver even if bot rows also
cover it.
3. Time-gated beats (S.S. Aqua sailing days, Indigo rival Mon/Wed, Dragon's Den
Tue/Thu, Clefairy Monday night, TM03 night gate) need POKEPORT_GOLD_DAY/HOUR pins.
4. Johto stragglers are folded in below (lighthouse descent, TM08 flake, and the
optional gift beats); they ride the same route-extension pass.
5. Section 32 (beasts) last: it needs long roaming play; trap volatiles and FORCESHINY
landed this campaign, so Mean Look + Heavy/Fast Ball plans are now viable.
## Also deferred (deliberate stubs, do not re-flag)
Time Capsule trading, Trainer House CAL2/Mystery Gift visitor, Silver-version import,
Cable Club link rooms, and Game Boy Printer output are deliberate product-level stubs,
not campaign work.
## The beats, in walkthrough order
### Runtime coverage ends at the Hall of Fame: no bot row, driver, or human has run any Kanto beat (sections 19-32) or the section 14 Tin Tower/Whirl Islands content
walkthrough section(s) 19,20,21,22,23,24,27,28,29,30,31,14 | missing
One root cause: tests/drivers/gold/route.lua (1264 lines) ends at row 18.20 (settle HALL_OF_FAME, expect EVENT_BEAT_ELITE_FOUR); the campaign covers asm-walk sections 00-18 only and none of the 24/36 gold_* drivers reaches any Kanto map, so sections 19-32 have zero execution proof. Extend route.lua into sections 19-32 and/or add targeted drivers. Per section pair: (s19/20) next rows are the Elm ticket (needs the post-credits spawn fix or a fly detour from Indigo), the ship crossing as far as the docking events, the Surge fight, and a Saffron Gym pad-chain assertion. (s21/22) All 24 route/gym trainers resolve in cache with correct parties: Route 9 six (Edna, Sid, Dean, Sidney, Tim, Heidi); Route 25 seven plus Kevin, whose Nugget-then-battle script 50:488d is hand-shaped (a full bag aborts before the battle, cart behavior); Cerulean's three swimmers; Route 10 South's Jim and Robert (Robert's Quagsire holds a BERRY via TRAINERTYPE_ITEM); Route 8's five; Celadon Gym's five including the twins' shared beat flag. Sight-line engagement is Trainers.sees (proven by the Johto bot); prize money base x level x 4 is Prize.lua; Erika's class item extracts as HYPER_POTION (the walkthrough's 'three Full Restores' is a FAQ error, not a port gap); Gold-only wilds (Mankey line Route 9, Growlithe Routes 7/8) come from the ROM version branch; Route 8's PRZCureBerry fruit tree is a fruittree op (Vm.lua:1218) into Apricorns.lua daily reset. One Kanto smoke-run converts the whole block. (s23/24) Nothing touches Routes 11-19, Fuchsia City/gym, Vermilion Snorlax, Diglett's Cave, Route 2, Pewter; the report's three blocker findings (START menu row, Pokegear card flags, radio persistence) are exactly the UI-plumbing breaks data audits miss: a gold_snorlax_wake.lua that grants the gear and EXPN card the way the game does (setflag 4, setflag 0, setflag 3 through the VM, not by poking save fields), tunes the radio, closes the gear, and talks to the Snorlax would currently fail at three separate steps; gold_cycling_road.lua and gold_fuchsia_gym.lua are the other two cheap proofs (gold_menu_shots.lua:72-73 only ever hand-seeded save fields). (s27/28) Drive first: (1) Cinnabar Blue talk -> disappear -> clearevent 1910 -> Viridian Gym populated -> Blue beaten -> ENGINE_EARTHBADGE set -> VAR_BADGES answers 16 (a break anywhere makes the 16th badge unobtainable and the Mt. Silver gate unpassable); cache chain exists: CINNABAR_ISLAND obj 1 SPRITE_BLUE at (9,6) scriptKey 4e:4985 eventFlag 1909; VIRIDIAN_GYM objs 1-2 gated on 1910; (2) the ROUTE_20 MAPCALLBACK_NEWMAP 4e:4cfa setevent 215 / ROUTE_19 MAPCALLBACK_TILES 4e:4f09 changeblock unseal pair (regression walls off Route 19). Driver: surf Pallet -> Route 21 -> Cinnabar -> talk to Blue -> Route 20 -> beat Blaine -> fly to Viridian -> beat Blue. (s29/30) No rows for Kanto OAKS_LAB, ROUTE_22, ROUTE_28, SILVER_CAVE_OUTSIDE, or the Union Cave B2F Lapras revisit (route.lua's only Union Cave rows are 03.29/04.g); risk concentrates on composites never run together: the 16-badge Oak visit with ProfOaksPCBoot text chain, the double Cut approach to the Route 28 west strip, surf-to-talk on the drifting SWIM_WANDER Lapras (Lake of Rage Gyarados proves the shape via gold_lake_probe.lua), and the daily-flag consume-and-respawn cycle across a save reload; a targeted gold_lapras_probe driver (clockDay=5, walk in, assert object present, catch, walk out, re-enter, assert gone) plus route extension converts inspected to proven. (s31) All six Silver Cave maps decode (warps, item balls flags 1689-1693, hidden items, SILVER_CAVE_OUTSIDE flypoint callback, PALETTE_DARK on Room 1, grass/water tables including Room 2 water); Flash lifts PALETTE_DARK via Palettes.isDarkness + World flashUsed; Surf/Waterfall live in FieldMoves with the badge table; gen2 ledges landed via Permissions.ledgeFacings / World:tryLedgeJump (the asm-walk's 'ledge hop missing' row is stale, GOLD-NEXT-RUN records the fix); the flypoint is FieldMoves.lua:386 flag 75; but no driver has entered Silver Cave, used Waterfall on Room 2's two bands, hopped Room 1's Escape Rope ledge, or talked to Red (closest: gold_halloffame_shots, gold_hm07_probe which proved Waterfall only at Tohjo Falls). (s14) The bot covers sections 10,11,12,13,16,17,18 and never section 14 (Ho-Oh is optional); untested: the 4F-9F ladder/hop mazes (depend on the recently added ledge-hop and one-way-wall support), the Ecruteak entrance sage scene retarget from Morty's setmapscene, the 1F sage unmasking off EVENT_TEAM_ROCKET_DISBANDED (set on the Gold arm of the Radio Tower boss script), the roof save/reset loop, and the entire Gold-side Whirl Islands dive; needs one probe driver carrying a RAINBOW_WING save up the tower and one diving to the Lugia chamber.
*How to prove it:* Extend tests/drivers/gold/route.lua past row 18.20 into sections 19-32, and/or write the named targeted drivers: gold_snorlax_wake.lua, gold_cycling_road.lua, gold_fuchsia_gym.lua, a gold_lapras_probe (clockDay=5 present/absent cycle), a Cinnabar-to-Viridian 16th-badge driver, a Route 20/19 unseal driver, a Silver Cave/Red driver, a Tin Tower RAINBOW_WING probe, and a Whirl Islands Lugia dive probe.
*Notes:* Merged from the parent heading plus six 'Also filed as' sub-findings (all one cause). Doc's own ratings: parent missing/minor (s19,20, already in ledger); s21,22 unverified/minor; s23,24 unverified/minor; s27,28 missing/major; s29,30 unverified/minor; s31 unverified/minor; s14 unverified/minor. The adversarial re-check ran on the s27/28 sub-claim, tried to refute and could not; its one correction: tests/drivers/gold/flag_names.lua and map_regions.lua DO name the Kanto maps (map_regions even carries CINNABAR/SEAFOAM/VIRIDIAN region graphs, so bot travel there is data-ready) but they are lookup tables, not executed coverage. 'Implemented statically, executed by nobody' matches the project's Roamers/Events:restore failure mode. GOLD-WALK-HANDOFF sections 1 and 4 already document the Johto-only scope.
### Olivine Port pier and Fast Ship boarding gate: the only road into Kanto, fully extracted, never executed
walkthrough section(s) 19 | unverified
Coord event at (7,15) (cache 5b:407d) does the full cart sequence: temporary-event bails, first-time bypass on event 48, readvar VAR_WEEKDAY branch, yesorno, checkitem S_S_TICKET (item 68), setevent 1, 7-step applymovement, sjump into gangway script 5b:401e which plays SFX, disappears/appears the sailor, runs special 46 = FadeOutToWhite (specialOrder[46+1], implemented Specials.lua:998), clears the eight eastbound EVENT_BEAT_* rematch flags on repeat trips, sets FAST_SHIP_1F scene 1, and warps to (25,1). Port sprite toggles for the post-Hall-of-Fame sailor (events 1847/1848) are armed by the HOF script. All data extracted, every op has a proven Johto call site; matched op-for-op against pokegold maps/OlivinePort.asm. Needs one live run or driver.
*How to prove it:* A driver or bot section that reaches OLIVINE_PORT post-HOF with the S_S_TICKET, boards, and asserts the warp to FAST_SHIP_1F (25,1) with scene 1 set. No gold_* driver currently reaches OLIVINE_PORT post-HOF.
*Notes:* Doc rating: unverified/blocker, new. If this chain fails, the walkthrough stops here (Kanto unreachable).
### S.S. Aqua maiden voyage: trainer gauntlet, lazy sailor, granddaughter, Metal Coat, docking; extracted end to end, never run
walkthrough section(s) 19 | unverified
Verified in cache, never executed: FastShip1F enter scene 5b:48a8 sdefer 5b:48ad (SFX_BOAT, earthquake 30, blackoutmod to cabin map, clearevent 49, scene 2 on first trip); grandpa bump coord events at (24,6)/(25,6); maiden-trip trainer objects behind flag 1849 with parties matching data/trainers/parties.asm exactly (NOLAND Sandslash31/Golem33, LYLE Koffing28/Flareon31/Koffing28, COLIN Delibird32 holding BERRY, MEG&PEG, FRITZ Mr.Mime/Magmar/Machoke, JEFF 2x Raticate32, DEBRA Seaking33, STANLY Machop31/Machoke33/Psyduck26, all in trainers.lua); B1F blocking-sailor coord scripts 5b:5e0c/5b:5e21 plus shared talk script 5b:5e37 clearing event 1837 to spawn the lazy sailor; FastShipLazySailorScript 5b:4d68 (playmusic, loadtrainer SAILOR 9, reloadmap, special 27 = HealParty, setmapscene B1F NOOP, readvar VAR_FACING walk-out); bed script 5b:521b (HealParty, fades, ReloadSpritesNoPalettes, RestartMapMusic, dock check on events 49/50/48 so sleeping cannot dock the maiden voyage); granddaughter payoff 5b:55c6 (fade, disappear/moveobject/appear teleport, showemote, sjump 5b:5633 = Metal Coat verbosegiveitem plus EVENT_FAST_SHIP_HAS_ARRIVED/FOUND_GIRL); door guard 5b:48d2 with arrived arm 5b:48ec (scall facing arm, setmapscene VERMILION_PORT scene 1, warp to VERMILION_PORT 7,17). Specials 27/46/47/49/50/60 resolve to HealParty/FadeOutToWhite/FadeOutToBlack/FadeInFromBlack/ReloadSpritesNoPalettes/RestartMapMusic, all implemented. Nothing in the chain touches a stubbed special, callasm, or VAR. Matched against pokegold maps/FastShip*.asm.
*How to prove it:* A bot section or driver for the full crossing (board, fight or dodge the gauntlet, sleep check, granddaughter Metal Coat, dock at VERMILION_PORT 7,17).
*Notes:* Doc rating: unverified/blocker, new. If any link breaks, arrival in Kanto is impossible.
### Machine Part quest chain (Power Plant manager, Cerulean Gym grunt scene, Route 24 grunt, part return, TM07) never executed
walkthrough section(s) 21,22 | unverified
The spine of sections 21-22: manager talk sets EVENT_MET_MANAGER_AT_POWER_PLANT 202, clears EVENT_CERULEAN_GYM_ROCKET 1901 and EVENT_FOUND_MACHINE_PART_IN_CERULEAN_GYM 251, arms CERULEAN_GYM scene 1 and the POWER_PLANT guard phone-call scene; grunt runs out of the gym (sets 203, clears 1900/1902, arms ROUTE_25 scene 1); beat the Route 24 grunt (GRUNTM 31, L30 Golbat); return the part for TM07 Zap Cannon plus EVENT_RESTORED_POWER_TO_KANTO 205 and the Saffron station population flip (clears 1906). Statically verified: cache holds full bytecode for PowerPlantManager 54:4dbd with both branches (54:4deb takeitem MACHINE_PART / 54:4e04 TM give guarded by event 223), gym scene script 54:4332 sdefer 54:4336 with all 34 rows, Route 24 grunt script 50:4407; initial_events.lua seeds flags 251/1900/1901/1902/1903/1906/1907 at new game exactly as InitializeEventsScript does (pokegold engine/events/std_scripts.asm:546-550); World.lua:607 applies the seed while EVENT_INITIALIZED_EVENTS is clear. Engine deps all have call sites: World:trySceneScript (World.lua:5369, called at :6893), setmapscene/setscene (Vm.lua:274-284), specials RestartMapMusic/FadeOutToBlack/FadeInFromBlack/ReloadSpritesNoPalettes/FadeOutMusic (Specials.lua:999-1072). Missing only run-time proof: route bot covers sections 00-18, no Kanto driver exists.
*How to prove it:* Route extension or a driver walking manager -> gym grunt scene -> Route 24 grunt battle -> part return, asserting events 202/203/205 set and 1900/1901/1902/251/1906 cleared and TM07 in bag.
*Notes:* Doc rating: unverified/blocker, new.
### Route 25 Misty date scene, Cerulean gym population, and Misty fight never executed (and Misty gives no TM, contrary to walkthrough)
walkthrough section(s) 21 | unverified
The date cutscene at Route 25 cells (42,6)/(42,7) is what clears EVENT_TRAINERS_IN_CERULEAN_GYM 1903 and populates the gym with Misty, three swimmers, and the guide; without it Misty never appears and the Cascade Badge is unreachable. Cache has both coord events with sceneId=1 (scripts 18305/18369) and full scripts 50:4781/50:47c1 (34-row cutscene: heart emote, boyfriend flees, Misty approach movement, clearevent 1903, setscene 0, special 60=RestartMapMusic), armed by gym grunt script 54:4336 row 27 (setmapscene group=7 map=16 scene=1). Engine deps: World:tryCoordScript (World.lua:5006) filters on the map scene id; mapScenes persist on the save (GOLD-INDEX tier note on World:loadPlayerData). CeruleanGymMistyScript 54:438a is in the cache; MISTY trainer 1 party (L42 Golduck, L42 Quagsire, L44 Lapras, L47 Starmie with full movesets) extracts correctly; baseMoney 25 so the 4700G payout follows from Prize.reward.
*How to prove it:* Driver: arm the scene via the gym grunt, trigger the date coord event at (42,6)/(42,7), assert 1903 cleared and the gym populated, then fight Misty and assert the Cascade Badge and 4700G.
*Notes:* Doc rating: unverified/blocker, new. Correction embedded in the doc: Misty gives NO TM on the Gold cart, only the badge plus retroactive swimmer beat flags; the walkthrough and the lead's 'TM from Misty' are wrong about the cart, nothing is missing in the port there.
### PASS quest: Copycat lost item and Fan Club doll chain fully extracted, never run
walkthrough section(s) 22 | unverified
Copycat (COPYCATS_HOUSE_2F object 1, variable sprite slot 251) talk after the machine part return sets EVENT_MET_COPYCAT_FOUND_OUT_ABOUT_LOST_ITEM 206; Fan Club Clefairy guy (POKEMON_FAN_CLUB object 3, script 59:437b) then gives LOST_ITEM 130 and disappears the doll (flag 1908, correctly NOT in the initial seed; doc evidence line also cites POKEMON_FAN_CLUB object 5 flag 1908); returning it (Copycat branch 61:528b: takeitem 130, setevent 208, clearevent 1907 to re-show her shelf doll, then PASS 134 via 61:529d) yields the PASS. All bytecode in cache (61:5235/61:5270/61:528b); the mimicry gag's variablesprite slot 11 plus special 93 resolves to LoadUsedSpritesGFX (specialOrder[94], Specials.lua:1033); the Copycat's variable sprite slot is seeded through World.initialSprites/findInitialSprites (World.lua:1530, the fix GOLD-INDEX credits for the Copycat existing at all). Flag 1907 is in the initial seed so the shelf doll is hidden until the return; 1906 is seeded so the Saffron station stays empty until power is restored.
*How to prove it:* Driver: after machine-part return, talk to Copycat (assert 206), get LOST_ITEM at the Fan Club (assert doll flag 1908 set), return it (assert 208 set, 1907 cleared, PASS 134 in bag).
*Notes:* Doc rating: unverified/major, new. Correction embedded: the walkthrough's 'Rail Pass then Magnet Train Pass' is one item (PASS) on the cart, not a port gap. Merge-with hint: the s20 entry 'Copycat lost doll quest chain toward the rail PASS' (copycat-doll-pass-s20) describes the same quest from the section-20 audit with slightly different event ids cited (210/201 gate checks there vs 206/208 here).
### Magnet Train ride wired end to end; arrival behavior needs a human eye, never ridden since
walkthrough section(s) 22 | unverified
SaffronMagnetTrainStationOfficerScript (cache 61:4bc2/61:4bd0, rows 11-14) checks EVENT_RESTORED_POWER_TO_KANTO 205 then checkitem PASS 134, does setval 1, special id=35 which resolves to MagnetTrain (specialOrder[36]), then warpcheck and newloadmap 249 (MAPSETUP_TRAIN). All implemented: H.MagnetTrain (Specials.lua:2119) reads scriptVar for direction and blocks on World:magnetTrain (World.lua:2393) which pushes Gen2MagnetTrainRide; Vm handles warpcheck (:1135) and newloadmap (:1177); MAPSETUP_TRAIN defined at World.lua:175. Gating events (205 set by the Power Plant manager, PASS from the Copycat) are this section's other findings.
*How to prove it:* WHATS-NEXT item 8: a human rides both ways and presses a direction on arrival; the conversation must start on the first step because south is the only legal step out of the doorway cell. Nothing has run this since.
*Notes:* Doc rating: unverified/major, already named in the ledger.
### Silver Wing in Gold: Pewter City gramps script extracted, but Kanto (and the Whirl Islands dive) never run
walkthrough section(s) 14 | unverified
For a Gold cart, Lugia needs the SILVER_WING carried in the bag (WhirlIslandLugiaChamberLugiaCallback does checkevent EVENT_FOUGHT_LUGIA then checkitem SILVER_WING), and in Gold the wing comes from the Pewter City gramps (pokegold maps/PewterCity.asm PewterCityGrampsScript: checkver, Silver arm branches away, Gold falls through to verbosegiveitem SILVER_WING + setevent). Verified in cache: PEWTER_CITY object 3 (SPRITE_GRAMPS at 29,17) scriptKey 4d:583e = faceplayer / opentext / checkver / iftrue 4d:585c / checkevent 121 / verbosegiveitem item 71 / setevent 121 / closetext / end, all generic opcodes. The Lugia chamber map, its SPRITE_LUGIA object (scriptKey 47:41a0, eventFlag 1853), and its MAPCALLBACK_OBJECTS callback (47:418c) are in the cache using only implemented machinery. Route bot has no section 14/25 rows (grep 'id = "1[0-9]' shows 10,11,12,13,16,17,18). One mechanical gap on the dive path: the waterfall forced-down item, reported separately in the doc.
*How to prove it:* Driver or bot rows: reach Pewter in Gold, talk to the gramps (assert SILVER_WING item 71 given, event 121 set), then the full Whirl Islands dive to the Lugia chamber.
*Notes:* Doc rating: unverified/major, new. The waterfall forced-down item on the dive path is a separate finding elsewhere in the doc, not part of this entry.
### Vermilion Port arrival bookkeeping, Vermilion flypoint, and Kanto music never executed
walkthrough section(s) 19 | unverified
Arrival scene (VERMILION_PORT scene 1, sdefer 5b:450e) extracted intact: one step up, appear the gangway sailor, setscene 0, end-of-voyage flags (1841/1840 hide grandpa and granddaughter for good, 1849 retires the maiden-trip trainers, clear 1843 to reveal the Olivine passage Pokefan, set 48 EVENT_FAST_SHIP_FIRST_TIME), and blackoutmod VERMILION_CITY. ENGINE_FLYPOINT_VERMILION comes from MAPCALLBACK_NEWMAP on both VERMILION_PORT and VERMILION_CITY (setflag 57); the port's fly table carries the matching row (FieldMoves.lua:392) and World:runMapCallback has a live call site (World.lua:5659 plus the MAPCALLBACK_OBJECTS call in setMap). Fly deliberately stays region-locked exactly like the cart (FieldMoves.flyPoints, FieldMoves.lua:442: Kanto page only after SPAWN_INDIGO, no cross-region fly, verified against pokegold engine/pokegear/pokegear.asm .KantoFlyMap). Kanto music extracted: audio.lua songs has 92 entries, musicOrder includes Music_VermilionCity and Music_ViridianCity (musicOrder[63]=Music_VermilionCity), mapSongs carries all 362 map rows.
*How to prove it:* Driver: dock at Vermilion, assert flags 1841/1840/1849/48 set and 1843 cleared, flypoint flag 57 set on map entry, and Vermilion/Viridian city music playing.
*Notes:* Doc rating: unverified/major, new.
### Pokemon Fan Club chairman Rare Candy give never run
walkthrough section(s) 19 | unverified
Chairman script 59:4340: checkevent 212 (already heard), checkevent 211 (bag-full retry), yesorno where answering No gives nothing (cart behavior), two promptbutton speech pages, verbosegiveitem item 32 = RARE_CANDY, setevent 212. The cart quirk that nothing ever sets the bag-full retry event 211 is faithfully carried in the extracted bytecode. Same map also holds the Clefairy doll object (flag 1908) and Clefairy Guy script 59:437b that the later Copycat quest reads. yesorno, verbosegiveitem, and multi-page writetext all have proven Johto call sites. Matched against pokegold maps/PokemonFanClub.asm; RARE_CANDY present in items.lua.
*How to prove it:* Covered by any Kanto smoke-run that talks to the chairman and asserts RARE_CANDY given and event 212 set.
*Notes:* Doc rating: unverified/minor, new.
### Lt. Surge gym: trainers, Thunder Badge, AI item use fully extracted, never fought
walkthrough section(s) 19 | unverified
Surge script 59:4bfc: checkflag 36 re-fight guard, loadtrainer class 19 member 1, winlosstext, reloadmapafterbattle (whose loss-abort semantics were fixed during the Johto campaign), retroactive setevent of the three gym trainers, setflag 36 = ENGINE_THUNDERBADGE which routes to save.player.kantoBadges.THUNDER. Parties in trainers.lua match data/trainers/parties.asm exactly: Surge is RAICHU44/ELECTRODE40/MAGNETON40/ELECTRODE40/ELECTABUZZ46 with full TRAINERTYPE_MOVES movesets; Vincent, Horton, Gregory all match (GUITARIST 2/JUGGLER 3/GENTLEMAN 3). Trainer AI item use implemented: Ai.ITEM_ORDER/Ai.chooseItem (Ai.lua:1368-1400) dispatched from Battle.lua:1790 (AI_SwitchOrTryItem) off the extracted class attributes. The gym has no puzzle in Gen 2, matching the walkthrough's power-outage line.
*How to prove it:* Driver or route rows: fight the gym trainers and Surge, assert ENGINE_THUNDERBADGE (flag 36) set into save.player.kantoBadges.THUNDER and the AI HYPER_POTION use path exercised.
*Notes:* Doc rating: unverified/major, new. Correction embedded: the walkthrough's claim that Surge holds a Full Restore is a FAQ error; pokegold data/trainers/attributes.asm line 114 (block 113-117) gives the Lt Surge class HYPER_POTION and the cache carries exactly that (LT_SURGE items=HYPER_POTION, baseMoney 25).
### Route 6 Underground Path blocker and Saffron gate guard extracted, never walked
walkthrough section(s) 20 | unverified
The walkthrough's 'large man who blocks the entire entrance' is the ROUTE6_POKEFAN_M object at (17,4) whose event flag EVENT_ROUTE_5_6_POKEFAN_M_BLOCKS_UNDERGROUND_PATH hides him only when SET (set by the Power Plant script in a later section); the port's object-visibility rule 'flag set hides object' is implemented and documented at Events.lua:1-2, and his script is a bare jumptextfaceplayer about the Power Plant. The Route 6 Saffron gate guard at (0,4) stands off the walking lane and is dialogue only (branches on EVENT_RETURNED_MACHINE_PART). ROUTE_6 grass and water encounter tables (Abra/Magnemite slots) are present in encounters.lua. Nothing gated on engine work; it has just never been walked.
*How to prove it:* Covered by any Kanto walk through Route 6 before and after the Power Plant script sets the blocker flag.
*Notes:* Doc rating: unverified/minor, new.
### Mr. Psychic's TM29 Psychic give extracted verbatim, Kanto walk to the door unproven
walkthrough section(s) 20 | unverified
Cache 61:4b15: checkevent 227, writetext, promptbutton, verbosegiveitem item 221 = TM_PSYCHIC_M, iffalse full-bag fall-through WITHOUT setting the flag (cart behavior, re-talk after making room), setevent 227. TM_PSYCHIC_M is in items.lua with pocket TM_HM and price 2000; the four-pocket bag and TM teaching were exercised in the Johto run (TM08 ROCK SMASH teach passed on merit per GOLD-WALK-HANDOFF). Matched against pokegold maps/MrPsychicsHouse.asm.
*How to prove it:* Any Kanto run that walks to the house, talks, and asserts TM29 given and event 227 set (plus the full-bag retry arm).
*Notes:* Doc rating: unverified/minor, new.
### Silph Co. 1F Up-Grade give extracted, never run
walkthrough section(s) 20 | unverified
Cache 61:4f81: same shape as Mr. Psychic, verbosegiveitem item 172 = UP_GRADE guarded by event 222 with the full-bag fall-through. The officer at (13,1) has event flag -1 so he never moves, and SILPH_CO_1F has only the two street-door warps: there is no upstairs in Gen 2, so the 'man blocking the stairway' never opens, matching the cart (verified against pokegold maps/SilphCo1F.asm). Generic mechanisms only. UP_GRADE present in items.lua.
*How to prove it:* Any Kanto run that enters Silph Co. 1F and asserts UP_GRADE given behind event 222.
*Notes:* Doc rating: unverified/minor, new.
### Saffron Gym teleporter maze (30 same-map warp pads) never exercised; same-map warp chains untested anywhere
walkthrough section(s) 20 | unverified
The gym's whole puzzle is 30 warp pads (cache SAFFRON_GYM warps 3-32) that all target SAFFRON_GYM itself; the only route to Sabrina is entrance pad (11,15) -> ... -> (1,5) -> (11,9). The engine mechanism exists and reads correct: pads are COLL_WARP_PANEL 0x7c, an immediate warp (Permissions.lua:311); World:takeWarp (World.lua:6022-6040) handles a destination equal to the current map through setMap; warpCooldown keyed on the landing cell (World.lua:6135 set, 6217-6222, 6240 suppression check) stops the landing pad from re-firing until the player steps off, exactly what a pad-to-pad maze needs. But the asm-walk's own port-coverage table flags that no driver has ever exercised a same-map warp chain, and no Kanto run exists. If this breaks, Sabrina is unreachable and the 10th badge (and eventually Red's gate) is lost.
*How to prove it:* Targeted driver: enter the gym, ride pad (11,15), assert arrival at (19,17), then the full 5-hop chain to (11,9).
*Notes:* Doc rating: unverified/major, new.
### Sabrina battle and Marsh Badge fully extracted, never fought
walkthrough section(s) 20 | unverified
Sabrina script (cache 61:40cf): checkflag 39 guard, loadtrainer class 35 member 1, reloadmapafterbattle, force-sets the four gym trainer flags (so the trainers are skippable, as the walkthrough implies), setflag 39 = ENGINE_MARSHBADGE routed to save.player.kantoBadges.MARSH. Party matches the cart exactly (trainers.lua SABRINA 1: ESPEON46 Sand-Attack/Quick Attack/Swift/Psychic, MR__MIME46 Barrier/Reflect/Baton Pass/Psychic, ALAKAZAM48 Recover/Future Sight/Psychic/Reflect; class item HYPER_POTION, base reward 25, AI attribute words extracted; attributes.asm:209-213). The four trainers (Rebecca, Franklin, Doris, Jared; MEDIUM 6,7 / PSYCHIC_T 2,11; parties verified including the cart's L35 third Exeggcute that the FAQ gets wrong) are OBJECTTYPE_TRAINER with SPINRANDOM movement, which Npc.lua supports, and sight/battle handoff is the proven Trainers.lua path. Prize money base x last-level x4 is implemented in Prize.lua (matches the walkthrough's 4800G).
*How to prove it:* Driver through the warp maze to Sabrina; assert ENGINE_MARSHBADGE (flag 39) into save.player.kantoBadges.MARSH, the four trainer flags force-set, and the 4800G payout.
*Notes:* Doc rating: unverified/major, new. Corrections embedded: the L35 third Exeggcute is cart-correct (the FAQ is wrong); Sabrina's class item is HYPER_POTION.
### Copycat lost doll quest chain toward the rail PASS (section-20 filing): all pieces present and generic, never run
walkthrough section(s) 20 | unverified
Spans this section's maps (Copycat's house is Saffron warp 8, the doll source is the Vermilion Fan Club) though the walkthrough text defers it to after the Power Plant (EVENT_RETURNED_MACHINE_PART gates the dialogue arms). Copycat's 2F object uses variable sprite 251 (SPRITE_COPYCAT), and the new-game variablesprite seeding that used to leave Copycat despawned was fixed during the Johto campaign (tests/gen2_variable_sprites_test.lua, GOLD-WALK-HANDOFF section 5); her script (cache 61:5235) is extracted bytecode whose only special, LoadUsedSpritesGFX, is implemented (Specials.lua:1033); the Fan Club Clefairy Guy (59:437b) checks events 210/201 and gives LOST_ITEM; the doll object (flag 1907) and the PASS handover (verbosegiveitem PASS, setevent EVENT_GOT_PASS_FROM_COPYCAT per pokegold maps/CopycatsHouse2F.asm:61-73) are ordinary ops; PASS is a KEY_ITEM in items.lua. Nothing engine-shaped is missing.
*How to prove it:* Same driver as the s22 PASS quest entry: full Copycat/Fan Club round trip asserting LOST_ITEM, doll flags, and PASS.
*Notes:* Doc rating: unverified/minor, new, named in the leads. Merge-with hint: same quest chain as pass-quest-copycat-fanclub (s22); kept separate because the doc filed them as distinct headings from two section audits. This filing cites Clefairy Guy gate events 210/201 and doll flag 1907; the s22 filing cites 206/208 and doll-disappear flag 1908.
### Celadon side items: TM03 Curse night gate, Leftovers trash can, hidden PP Up (plus Berserk Gene, Route 9 Ether, Route 25 Potion) never run
walkthrough section(s) 22 | unverified
TM03 Curse: roof house pharmacist script 5e:5083 in cache with checktime mask 4 (NITE) at row 7; the Vm checktime opcode (Vm.lua:788) maps NITE to bit 4 off World:timeOfDayId, so the night-only gate should behave; the give branch 5e:509a is guarded by EVENT_GOT_TM03_CURSE 218. Leftovers: CeladonCafeTrashcan bg event at (7,1) key 5e:648c present, plain giveitem bytecode with the flag only set on success so a full pack retries, exactly the cart. Hidden PP Up at Celadon (37,21): BGEVENT_ITEM row present, handled by HiddenItems (fixed PP_UP; the walkthrough's 'random PP Up' is wrong on the cart too). Berserk Gene at Cerulean (2,12), hidden Ether Route 9 (10,5), and hidden Potion Route 25 (4,5) likewise extract. All generic and statically sound, none ever run.
*How to prove it:* Kanto run collecting each: night visit for TM03 (checktime NITE), cafe trash can Leftovers with full-pack retry, hidden-item picks via HiddenItems.
*Notes:* Doc rating: unverified/minor, new. Correction embedded: the PP Up is fixed, not random, on the cart.
### Mt. Moon Square Clefairy dance (Monday night) + hidden Moon Stone + Rock Smash rock: all dependencies present, sequence never executed
walkthrough section(s) 26 | unverified
Coord event at (7,11) scene 0 runs 5b:676a, whose 45 rows extract completely: checkflag 87 (ENGINE_MT_MOON_SQUARE_CLEFAIRY), readvar var 11 (VAR_WEEKDAY, real in the port) ifnotequal 1 (MONDAY), checktime NITE, then the appear/follow/applymovement/cry/showemote dance, clearevent 236 (the hidden Moon Stone flag) and setflag 87. Both callbacks extracted (type 5 NEWMAP 5b:6763 re-hides the stone on entry; type 2 OBJECTS 5b:6767 hides the rock). The rock object (SPRITE_ROCK at 7,7, scriptKey 5b:67ee = jumpstd SmashRockScript) resolves to the real SmashRock/AskRockSmash bodies GOLD-INDEX.md:377-382 documents; follow/stopfollow are implemented (Vm.lua:1009,1026). Flag semantics match the cart: ENGINE_MT_MOON_SQUARE_CLEFAIRY lives in wDailyFlags2 (pokegold constants/engine_flags.asm:105-106) and the port clears id 87 in Apricorns.DAILY_ENGINE_FLAGS, so the dance is once per day, i.e. once per Monday night. Unproven in-game: (a) whether the appeared rock correctly blocks/receives the A press on the Moon Stone tile at (7,7) and Rock Smash then exposes the hidden item in the same visit, and (b) the full follow-chain animation. The stubbed RockMonEncounter (CallAsm STUB_ROWS) does NOT affect this rock: MOUNT_MOON_SQUARE is not in the cart's RockMonMaps (pokegold data/wild/treemon_maps.asm:43-48), so no encounter roll is owed here.
*How to prove it:* Driver: force clockDay=MONDAY clockHour=NITE, walk to (7,11), watch the dance, smash the rock, take the Moon Stone.
*Notes:* Doc rating: unverified/minor, new.
### Rival re-encounters after Mt Moon (Indigo Plateau Mon/Wed rematch, Dragon's Den Tue/Thu cameo) never run
walkthrough section(s) 26 | unverified
Both beats are pure extracted content keyed on EVENT_BEAT_RIVAL_IN_MT_MOON (set by the implemented Mt Moon script) and VAR_WEEKDAY (real in the port; World:readVar VAR_WEEKDAY at World.lua:1289): the cache carries INDIGO_PLATEAU_POKECENTER_1F's NEWMAP callback 5a:48b6 (23 rows of setmapscene/clearevent resets) plus its two coord events, and DRAGONS_DEN_B1F's callback 47:44e1 (checkevent 793 -> disappear object 4). The daily rematch flag ENGINE_INDIGO_PLATEAU_RIVAL_FIGHT (id 92) is in Apricorns.DAILY_ENGINE_FLAGS so it clears on the daily rollover. The rematch parties RIVAL2 members 4-6 (L45-50 with CROBAT) are in trainers.lua. Nothing looks missing, but the coord-event choreography in the Pokecenter (rival walks in as you leave) is the kind of scene worth a driver before trusting; filed unverified rather than implemented for that reason.
*How to prove it:* Driver: set EVENT_BEAT_RIVAL_IN_MT_MOON via the Mt Moon fight, force weekday Monday/Wednesday, enter the Indigo lobby and assert the rematch triggers; force Tuesday/Thursday and assert the Dragon's Den object 4 state.
*Notes:* Doc rating: unverified/minor, new. Merge-with hint: overlaps indigo-lobby-rival-rematch (s18), the same Indigo Plateau rematch filed from the section-18 audit; that filing cites World:weekday at World.lua:1247 and scriptKeys 5a:48ff/5a:4940.
### Indigo lobby rival rematch (Monday/Wednesday, RIVAL2, post Mt. Moon) armed on tiles the walkthrough crosses, never run
walkthrough section(s) 18 | unverified
Not in the walkthrough text but armed on the lobby tiles it crosses: coord events at (16,4)/(17,4) run PlateauRivalBattle1/2 (cache scriptKeys 5a:48ff/5a:4940), which fight the RIVAL2 second-tier party when EVENT_BEAT_RIVAL_IN_MT_MOON is set, ENGINE_INDIGO_PLATEAU_RIVAL_FIGHT is clear, and VAR_WEEKDAY is Monday or Wednesday (pokegold maps/IndigoPlateauPokecenter1F.asm). Every dependency exists: coord events and scripts extracted, VAR_WEEKDAY answers the real weekday (World:weekday, World.lua:1247), the daily engine flag is registered for midnight reset (Apricorns.lua:88, id 92 ENGINE_INDIGO_PLATEAU_RIVAL_FIGHT), and RIVAL2 parties 4-6 (SNEASEL/45, CROBAT/48, MAGNETON/45, GENGAR/46, ALAKAZAM/46, starter/50) are in the trainers cache. But the gate flag EVENT_BEAT_RIVAL_IN_MT_MOON is Kanto content no run has ever set.
*How to prove it:* Should be picked up when section 26 is audited/run: same driver as rival-rematch-mtmoon, entering the lobby on Monday/Wednesday with the Mt Moon flag set and asserting the coord-event battle fires.
*Notes:* Doc rating: unverified/minor, new. Merge-with hint: same beat as rival-rematch-mtmoon (s26); kept separate because the doc filed both headings. Line-cite difference between filings: World.lua:1247 (weekday) here vs World.lua:1289 (readVar VAR_WEEKDAY) there.
### Lighthouse descent and swim to Cianwood (route rows 08.50/08.51) never walked; collision data matches the cart, blame points at the bot's region graph
walkthrough section(s) 08 | unverified
The route bot has never completed the descent from Jasmine to Route 40, so the Surf crossing, all fourteen Route 40/41 swimmers, and Cianwood arrival are engine-unwalked (runs reach section 08+ by teleport; run 28 still failed rows 08.50/08.51). The handoff narrows it to the 3F seven-cell pocket: warps 8/9 at (8,3)/(9,3) 'do not take' when stood on. Verified in the current cache: OLIVINE_LIGHTHOUSE_3F (8,3)/(9,3) decode to COLL_FLOOR 0x00, and pokegold data/tilesets/lighthouse_collision.asm block $27 is FLOOR,FLOOR,FLOOR,FLOOR, so those two warp rows are one-way landing anchors for the 4F pits at (8,3)/(9,3) (which decode to COLL_PIT 0x60) and never fire on the cart either. The pocket's real exit is warp 3, the staircase at (9,5), which decodes to 0x72 and does fire ((13,3) also 0x72). Fix shape: correct the bot's region graph, which treats landing-anchor warp coordinates as exits (the exact trap GOLD-WALK-HANDOFF.md:593 documents), not the engine; a human following the walkthrough (drop beside Connie, fight Terrell, take the Ether, head up the stairs) uses the (9,5) staircase and should be fine.
*How to prove it:* Something must walk 6F down to CIANWOOD_CITY in the engine (bot rows 08.50/08.51 passing, or a driver doing the descent and the Route 40/41 Surf crossing); until then the second half of section 08 stays a beat-completeness check rather than a proven path.
*Notes:* Doc rating: unverified/major, already named in the ledger. The doc's own re-check corrected the suspicion of an engine defect: the cache collision matches the cart exactly, so the failing rows indict the bot's region graph, not the engine or the warp data.
### TM08 Rock Smash give (route row 06.21) flaky across runs; ledger contradicts itself and the failure is unresolved
walkthrough section(s) 06 | unverified
The Route 36 fisher gives TM08 after EVENT_FOUGHT_SUDOWOODO (Route36RockSmashGuyScript, generic extracted bytecode with verbosegiveitem guarded by iffalse .NoRoomForTM). The ledger contradicts itself across runs: GOLD-WALK-HANDOFF section 8 says 'Everything before it now passes on merit: Sudowoodo, TM08, the ROCK SMASH teach', but run 28's failure table lists '06.21 ROUTE_36 EVENT_GOT_TM08_ROCK_SMASH not set' while the Sudowoodo row itself passed. Plausible causes: full TM pocket taking the iffalse arm, or the bot failing to stand adjacent. The script path is generic and the same give shape works everywhere else, so this is most likely route flake, but it is unresolved in the latest continuous run and TM08 gates all Rock Smash content downstream (Burned Tower rocks, Route 40/Cianwood rock items).
*How to prove it:* Re-run route row 06.21 in a continuous run and assert EVENT_GOT_TM08_ROCK_SMASH gets set; instrument for TM-pocket fullness and adjacency to distinguish the two suspected causes.
*Notes:* Doc rating: unverified/minor, already named in the ledger. Not given severity unverified-risk because the problem is a reproduced-once run failure (a live flake), not purely never-executed content.
### New Bark rival push scene (talk to rival by the lab) unproven; low risk, zero coverage
walkthrough section(s) 00 | unverified
Walkthrough: 'If you talk to him, he'll push you out of the way.' NewBarkTownRivalScript is optional, repeatable (sets no flag) and uses follow PLAYER, applymovement with turn_head/step/fix_facing/jump_step, and SFX_TACKLE. Every opcode involved is implemented (follow/stopfollow in the VM, jump_step in Movement.lua which the ledge fixes exercised), and the sibling teacher-drag scene has a dedicated driver (tests/drivers/gold_teacher_scene.lua), but no driver or test touches this specific scene and the route bot never talks to optional NPCs. Cache maps.lua NEW_BARK_TOWN objects include the rival with a scriptKey and eventFlag.
*How to prove it:* A short driver that talks to the rival by the lab and observes the push movement chain, mirroring gold_teacher_scene.lua.
*Notes:* Doc rating: unverified/minor, new.
### Kiyo battle and Tyrogue gift (Mount Mortar B1F): fully wired, never exercised by any driver or bot run
walkthrough section(s) 15 | unverified
The game's only Tyrogue. Cache scripts['46:5eec'] holds all 29 rows of MountMortarB1FKiyoScript (checkevent 97 / 1193, winlosstext, loadtrainer class BLACKBELT_T member 6, startbattle, reloadmapafterbattle, setevent 1193, readvar var=1 VAR_PARTYCOUNT, ifequal 6, givepoke species 236 level 10, setevent 97); trainers cache has BLACKBELT_T[6] KIYO = L34 HITMONLEE + L34 HITMONCHAN (KIYO object at 13,4 with scriptKey 46:5eec); readvar VAR_PARTYCOUNT is a real read (World.lua:1271); givepoke is implemented at Vm.lua:443. The full dependency chain has never run: Surf across the 1F Outside lake, the in-cave Waterfall climb to warp 4 at (17,5), the 2F/1F-inside/B1F warp descent, the battle, and the gift. The waterfall climb mechanic itself is bot-verified only at Tohjo Falls; route.lua contains no MORTAR/KIYO/DARK_CAVE rows.
*How to prove it:* A Mount Mortar driver: Surf the outside lake, Waterfall to warp 4 (17,5), descend to B1F, beat Kiyo, assert Tyrogue (species 236, L10) given with a party of <6 and events 97/1193 set.
*Notes:* Doc rating: unverified/major, new. Correction embedded: GOLD-INDEX's claim that only VAR_WEEKDAY and VAR_FACING answer readvar is STALE; World.lua:1265-1330 now answers essentially every VAR_*.
### Blackglasses pharmacist in Dark Cave Blackthorn entrance extracted exactly, never run (same for TM13 Snore and Revive balls)
walkthrough section(s) 15 | unverified
The script is fully extracted (cache scripts['47:436c'], 13 rows matching pokegold maps/DarkCaveBlackthornEntrance.asm exactly: checkevent / verbosegiveitem / iffalse pack-full arm / setevent), the spinning NPC object (SPRITE_PHARMACIST) is in the cache at (7,3) with that scriptKey, and verbosegiveitem is implemented in the VM. Reaching him also needs Flash plus Surf inside the cave, both implemented. The same unverified state covers the TM13 Snore and Revive item balls beside him (item ball arm is implemented and tested). No driver enters Dark Cave and the bot route bypasses it (grep DARK_CAVE in route.lua is empty).
*How to prove it:* A Dark Cave driver: Flash + Surf to the Blackthorn entrance, talk to the pharmacist (assert Blackglasses given), pick up TM13 Snore and the Revives.
*Notes:* Doc rating: unverified/minor, new.
### Master Ball from Elm after eighth badge: route rows exist but optional, no run log confirms the event ever set
walkthrough section(s) 16 | unverified
ProfElmScript's ElmCheckMasterBall gate is checkflag ENGINE_RISINGBADGE then verbosegiveitem MASTER_BALL then setevent EVENT_GOT_MASTER_BALL_FROM_ELM (pokegold maps/ElmsLab.asm:57-61). All generic extracted bytecode; checkflag over ENGINE_* badges works since the badge-store fix (GOLD-WALK-HANDOFF.md:502, badges routed into save.player.badges). The route bot has rows 16.24/16.24b for this beat but both are marked optional = true, and no run log or handoff note confirms EVENT_GOT_MASTER_BALL_FROM_ELM was ever actually set in a live run (no MASTER hits in GOLD-NEXT-RUN.md run logs). A full bag silently skips the setevent (verbosegiveitem iffalse), same as the cart.
*How to prove it:* Make route rows 16.24/16.24b non-optional (or run them) and assert EVENT_GOT_MASTER_BALL_FROM_ELM set with bag space available.
*Notes:* Doc rating: unverified/major, new.
### Everstone from Elm by showing a self-hatched Togepi: pieces individually proven, full chain never exercised end to end
walkthrough section(s) 16 | unverified
The gating special FindPartyMonThatSpeciesYourTrainerID is a real handler with the own-OT check (Specials.lua:1134), and the phone-call prerequisite is NOT required: pokegold maps/ElmsLab.asm ElmCheckTogepiEgg -> EVENT_TOGEPI_HATCHED -> ElmEggHatchedScript re-runs the same species scan, so a hatched Togepi in the party is sufficient (maps/ElmsLab.asm:62-95, engine/phone/scripts/elm.asm:46). Egg hatching has a driver (tests/drivers/gold_egg_hatch.lua) and PC withdraw exists (BoxMenu.lua / PcMenu.lua). The full chain (hatch with your OT, withdraw, talk, EVENT_SHOWED_TOGEPI_TO_ELM, verbosegiveitem EVERSTONE) has never run end to end. The .egghatched arm of ElmPhoneCalleeScript (sets EVENT_TOLD_ELM_ABOUT_TOGEPI_OVER_THE_PHONE) lives in the bank $41 callee scripts and only matters for the alternate entry path. No route row exists for the Everstone.
*How to prove it:* A driver chaining gold_egg_hatch-style hatching (own OT), PC withdraw, and the Elm talk, asserting EVENT_SHOWED_TOGEPI_TO_ELM and EVERSTONE in bag.
*Notes:* Doc rating: unverified/minor, new.
### TM37 Sandstorm happiness gate at the Route 27 house never exercised; gate is first-slot happiness >= 150
walkthrough section(s) 16 | unverified
The granny's script is extracted (cache scripts['60:6352'], 9 rows: checkevent, special id 88, ifgreater, sjump into the loyal/disloyal arms) and special id 88 resolves through constants.specialOrder with the VM's order[id+1] rule (Vm.lua:1773) to specialOrder[89] = GetFirstPokemonHappiness, a real handler at Specials.lua:1506 reading the first non-egg party member. The cart gate is happiness >= 150 on the FIRST party slot (ifgreater 150-1), not 'one of your Pokemon'. Happiness accrues via Happiness.lua stepped from StepEvents. No driver or bot row enters ROUTE_27_SANDSTORM_HOUSE.
*How to prove it:* Driver: enter ROUTE_27_SANDSTORM_HOUSE with a first-slot mon at happiness >= 150 and assert TM37 given; also assert the disloyal arm below 150.
*Notes:* Doc rating: unverified/minor, new.
### Route 27 whirlpool island (Bird Keeper Jose, TM22 Solarbeam, Rare Candy) and Tohjo Falls Moon Stone never run; whirlpool mechanic only proven on Route 41
walkthrough section(s) 16 | unverified
The whirlpool mechanic is implemented and driver-verified, but on Route 41, not here: tests/drivers/gold_water_moves.lua:105-124 clears a real whirlpool block with GLACIERBADGE gating and asserts the block replacement. The Route 27 island content is all in the cache (TM_SOLARBEAM ball at (60,12) item id 213 with event flag, Rare Candy ball at (53,12), BIRD_KEEPER[14] JOSE = L35 FARFETCH_D with phone contact rows; maps.ROUTE_27 objects rows 6-8) and the item ball arm is implemented, but the bot route crosses Route 27 only via Tohjo Falls and never surfs south past Gilbert (route rows 16.42-16.43 skip the south water), so this specific whirlpool, the Jose fight, and the TM22 pickup have never run. Same for the Tohjo Falls MOON_STONE ball at (2,6) (maps.TOHJO_FALLS object 1), which the walkthrough itself never mentions.
*How to prove it:* Driver or route rows: surf south past Gilbert, clear the Route 27 whirlpool, fight Jose, collect TM22 and the Rare Candy; separately pick up the Tohjo Falls Moon Stone.
*Notes:* Doc rating: unverified/minor, new.
### Route 26 heal house free heal extracted with all specials real, never entered
walkthrough section(s) 16 | unverified
The teacher's script is extracted (cache scripts['60:60e6'], 17 rows) and every special id in it resolves via order[id+1] to a real handler: 47 FadeOutToBlack, 50 ReloadSpritesNoPalettes, 27 HealParty, 49 FadeInFromBlack, 60 RestartMapMusic (Specials.lua:450, :999, :1025, and the fade-in/restart pair at :1059). Unconditional and repeatable like the cart. No driver or bot row ever enters ROUTE_26_HEAL_HOUSE; the bot route's Route 26 leg is a single travel row straight to the gate.
*How to prove it:* Driver: enter ROUTE_26_HEAL_HOUSE with a damaged party, talk, assert full heal and that the scene is repeatable.
*Notes:* Doc rating: unverified/minor, new.
### Victory Road item pickups (TM26 Earthquake pit, Full Restore shelf, Max Revive, Full Heal, X Special, two hidden items) never exercised
walkthrough section(s) 17 | unverified
The walkthrough collects five Poke Ball items (TM26 Earthquake via the one-way pit at (0,11), Full Restore on the ledge shelf via the (17,19) ladder, Max Revive and Full Heal in the entrance region, X Special on the HOP_LEFT column) plus two hidden items (Max Potion at (3,29), Full Heal at (3,65)). All pieces exist: cache maps.VICTORY_ROAD carries 6 objects (5 with itemball item/qty payloads), 2 bgEvents with hiddenItem tables, 10 warps, 2 coord events; World:interact consumes obj.itemball (World.lua:5483-5490, an earlier bot-found fix); HiddenItems handles BGEVENT_ITEM; Permissions models the COLL_PIT one-way warp, the six ladders, ledge hops (isLedge/ledgeFacings, Gold's own direction order where $a0 is HOP_RIGHT), and COLL_UP_WALL side walls (Permissions.lua:170-260 LEDGE_FACINGS/SIDE_BLOCKS/NEIGHBOR_ARM), with a test section at tests/gen2_world_test.lua:2936. But nothing has walked these routes: the bot's section 17 is only travel/walk rows 17.2, 17.11, 17.15 (no pickup rows), so the pit drop, the two HOP exits from the TM pocket, the HOP_DOWN off the shelf, and the X Special ball sitting ON a HOP_LEFT tile (the one placement the asm-walk flags as needing hardware confirmation) are all unproven end to end.
*How to prove it:* Driver or route rows through Victory Road collecting all five balls and both hidden items, specifically exercising the (0,11) pit drop, the (17,19) ladder shelf with HOP_DOWN exit, the two HOP exits from the TM pocket, and interaction with the X Special ball on its HOP_LEFT tile.
*Notes:* Doc rating: unverified/minor, new.
+84 -24
View File
@@ -53,13 +53,14 @@ supplied by the player.
This project does not include a ROM, emulate the Game Boy, transpile assembly,
or download a disassembly. A canonical US Poke Red, Blue, or Yellow ROM is the
only game content input.
or download a disassembly. A canonical US Poke Red, Blue, Yellow, or Gold ROM
is the only game content input.
The ROM is verified, used during import, and then released from memory. It is
not copied into the cache. Later launches load the private generated cache and
do not ask for the ROM again. Red, Blue, and Yellow can all be imported and
played side by side.
do not ask for the ROM again. Red, Blue, Yellow, and Gold can all be imported
side by side. Gold is Gen 2 Phase 1 (import + launcher; see
`docs/gold-phase1.md`): the Gen 2 engine is still under construction.
## Quick Start
@@ -67,12 +68,13 @@ Open the desktop app. On first boot, choose your legally obtained `.gb` /
`.gbc` file or drop it onto the window. Import takes a few seconds and the
game starts automatically.
Only the canonical 1 MiB US Red, Blue, and Yellow ROMs are accepted. The
importer verifies SHA-1 before creating any game data:
Only the canonical US Red, Blue, Yellow (1 MiB), and Gold (2 MiB) ROMs are
accepted. The importer verifies SHA-1 before creating any game data:
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
- Yellow: `cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1`
- Gold: `d8b8a3600a465308c9953dfa04f0081c05bdcb94`
The packaged app contains neither a ROM nor pre-extracted game data. Music,
sound effects, and cries are synthesized while the game runs from compact
@@ -205,6 +207,39 @@ even on a different computer, as long as the same folder comes along.
already written to either location is touched automatically, so copy files
over yourself if you want to carry existing progress across the switch.
## Launch Options
By default the app opens the launcher so you can pick a game. Launch options
skip it and start one game directly, which is what you want for a one-click
entry: a desktop shortcut per game, a Steam entry, or a handheld frontend.
| Option | Effect |
| --- | --- |
| `--game=red` | boot Red, skipping the launcher (`blue` and `yellow` too, or just `r` / `b` / `y`) |
| `--slot=2` | load that save slot; takes a slot number or a slot id |
| `--launcher` | open the launcher anyway, so you can edit a shortcut you already made |
## Linux on arm64 (Raspberry Pi)
Alongside the x86_64 `gen1recomp-*-linux.zip`, every release ships
`gen1recomp-*-linux-arm64.AppImage` for 64-bit ARM desktop Linux — Raspberry
Pi 4/5, Armbian and other SBC distros, and arm64 VMs on Apple Silicon:
```sh
chmod +x gen1recomp-*-linux-arm64.AppImage
./gen1recomp-*-linux-arm64.AppImage
```
LÖVE publishes no aarch64 binary of any kind, so this artifact compiles the
engine — and SDL2, OpenAL and the codecs — from source inside a Debian
bullseye arm64 container. It needs only glibc 2.29+, libstdc++, freetype and
zlib on the host; OpenGL, X11, Wayland, KMSDRM, ALSA and PulseAudio are all
dlopened, so the same image runs on a full desktop, a Wayland-only session or
a KMSDRM handheld with no X server. Build instructions and the reasoning are
in [docs/linux-arm64-build.md](docs/linux-arm64-build.md).
## iOS
Every release ships `gen1recomp-*-ios.ipa`. Sideload it with AltStore
@@ -222,6 +257,40 @@ build and install from source on a Mac instead, see
<a href="https://github.com/bryanthaboi/gen1recomp/releases/latest"><img src="./.github/resources/github-badge.png" alt="Download from GitHub" height="60"></a>
</div>
## Xbox Dev Mode
Every release ships `gen1recomp-*-xbox-uwp.zip` for Xbox One and Xbox Series
consoles in Developer Mode. It cannot be installed in retail mode.
Extract the archive, then use Xbox Device Portal to install the `.msix` and
the x64 package under `Dependencies`.
### External setup
1. Put your legally obtained Red, Blue, or Yellow ROMs on an external drive.
Mod ZIPs can go on the same drive.
2. Connect the drive to the Xbox and open Gen1Recomp.
3. Select **Import ROM** or **Import Mod**, then choose the file with the Xbox
file picker.
4. Repeat the ROM import for each version you want to use.
### Internal setup
1. Create a folder named `baseroms` on your PC and place your legally obtained
Red, Blue, or Yellow ROMs inside it.
2. ZIP the folder, keeping `baseroms` at the top level of the archive.
3. Launch Gen1Recomp once, then close it.
4. Open Xbox Device Portal and upload the ZIP to
`Gen1Recomp/LocalState/pokemon-love2d/`.
5. Choose **Yes** when Device Portal asks whether to extract the archive.
6. Open Gen1Recomp. The launcher checks baseroms once at startup. When it finds a compatible ROM, that games tab shows ROM FOUND and an Import detected ROM button.
ROMs, generated game data, saves, and mods remain in LocalState and are not
included in the app.
Source builds and package details are covered in
[the Xbox UWP build notes](ports/uwp/BUILD.md).
## Handhelds
A PortMaster-style port for the **Anbernic RG34XXSP** on Stock OS 64-bit MOD
@@ -231,24 +300,18 @@ Install steps, controls, and troubleshooting live in
## Nintendo Switch
Releases ship an SD-ready `gen1recomp-*-switch.zip` (issue
[#531](https://github.com/bryanthaboi/gen1recomp/issues/531)). Runtime target
is pinned [love-nx](https://github.com/retronx-team/love-nx) `11.5-nx1`.
Requires a console that can run Switch homebrew. Hardware evidence: **OLED**
(author) and **V1 / Erista** boot (community).
Releases ship an SD-ready `gen1recomp-*-switch.zip`. Runtime target is pinned
[love-nx](https://github.com/retronx-team/love-nx) `11.5-nx1`. Requires a
console that can run Switch homebrew.
- Players: [docs/switch-install.md](docs/switch-install.md) — download the
- Players: [docs/switch-install.md](docs/switch-install.md). Download the
zip, extract at the microSD root (install or update), title-override
launch, import your own legal ROM, Joy-Con controls and shortcuts.
- Builders: [docs/switch-build.md](docs/switch-build.md) `--fetch` /
`--loose` / `--fused`, toolchain, Docker fallback, and **CI vs release**
(path-gated ubuntu selftest, canonical fused PR artifact, release hard-fail).
Limitations, Dusklight-derived method, and how we tested:
[docs/switch-development.md](docs/switch-development.md) and
[docs/switch-hardware-evidence.md](docs/switch-hardware-evidence.md). Community
help — especially HOS / love-nx packaging and broader hardware coverage — is
welcome.
- Builders: [docs/switch-build.md](docs/switch-build.md). `--fetch` /
`--loose` / `--fused`, toolchain, Docker fallback, and CI vs release
(path-gated ubuntu selftest, fused PR artifact on the main repo, release
hard-fail).
- File transfer (MTP / SD / FTP): [docs/switch-transfer.md](docs/switch-transfer.md).
## Modding
@@ -297,7 +360,4 @@ This project would not be possible without [pret](https://github.com/pret) >
the pret band of decompiling maniacs > and their
[pokered](https://github.com/pret/pokered) disassembly.
Nintendo Switch port: [andrewqsantos](https://github.com/andrewqsantos).
Switch hardware testing (V1 boot): [booshankles](https://github.com/booshankles).
<p align="center"><a href="https://boisclub.games"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/bcg.png"></a></p>
Executable → Regular
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 96 KiB

+9 -6
View File
@@ -45,6 +45,8 @@ return {
{ "stop_music" },
{ "play_music", "Music_MeetRival" },
{ "show_text", "_OaksLabRivalGrampsText" },
-- callfar OaksLabPikachuMovementScript, before ShowObject (#1021)
{ "pikachu_make_way" },
{ "show_object", "OAKS_LAB", "OAKSLAB_RIVAL" },
{ "place_npc", RIVAL, 4, 7, "up" },
{ "move_npc_to", RIVAL, 4, 3 },
@@ -192,12 +194,13 @@ return {
end
rows[#rows + 1] = { "face_player_dir", "up" }
rows[#rows + 1] = { "face_object", OAK1, "down" }
-- OaksLabPlayerReceivedMonText: no nickname prompt -- the starter
-- Pikachu keeps its species name
-- OaksLabPlayerReceivedMonText clears wMonDataLocation, so AskName runs (#1013)
rows[#rows + 1] = { "show_text", "_OaksLabOakGivesText" }
rows[#rows + 1] = { "play_sound", "Get_Key_Item" }
rows[#rows + 1] = { "show_text", "_OaksLabReceivedText", { RAM = "PIKACHU" } }
rows[#rows + 1] = { "give_pokemon", "PIKACHU", 5, true }
rows[#rows + 1] = { "give_pokemon", "PIKACHU", 5 }
-- DisablePikachuOverworldSpriteDrawing keeps it in the ball (#1009)
rows[#rows + 1] = { "set_field", "pikachuInBall", true }
rows[#rows + 1] = { "set_flag", "EVENT_GOT_STARTER" }
rows[#rows + 1] = { "set_flag", "EVENT_CHOSE_PIKACHU" }
ow.runner:run(rows, { npc = npc, onDone = done })
@@ -296,9 +299,9 @@ return {
table.insert(rows, { "move_npc_to", RIVAL, 4, 11 })
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
table.insert(rows, { "play_music", "Music_OaksLab" })
-- OaksLabPikachuEscapesPokeballScript: Pikachu hates its ball.
-- The overworld follower itself is still an open port
-- (docs/yellow-version.md runtime backlog); the story beat plays.
-- OaksLabPikachuEscapesPokeballScript: the follower reaches the map (#1009)
table.insert(rows, { "face_player_dir", "up" })
table.insert(rows, { "set_field", "pikachuInBall", false })
table.insert(rows, { "play_cry", "PIKACHU" })
table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText1" })
table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText2" })
+10 -15
View File
@@ -1067,23 +1067,18 @@ local championsRoomRivalScript = {
{ "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 23
{ "move_npc", 2, "up", 2 }, -- 24 OakExitChampionsRoomMovement
{ "hide_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 25
-- ChampionsRoomPlayerFollowsOakScript / WalkToHallOfFame_RLEMovement
-- (PAD_UP 4, PAD_LEFT 1): the player walks out after Oak instead of the
-- screen just fading on the spot (#704). The entrance walk leaves the
-- player at (4,3) and both north-wall warps sit on row 0, so the original
-- only ever spends three of those simulated steps -- CheckWarpsNoCollision
-- takes the HALL_OF_FAME warp the moment the walk lands on (4,0) and the
-- trailing UP/LEFT are dropped. Scripted steps ignore collision here just
-- as they do in the original (CollisionCheckOnLand skips its checks while
-- wSimulatedJoypadStatesIndex is non-zero), so stepping through the
-- rival's cell at (4,2) is the ported behavior, not a clip. Re-reported
-- as a clip in #847 and re-checked against home/overworld.asm
-- CollisionCheckOnLand, which is still the authority: do not "fix" it.
{ "move_player", "up", 3 }, -- 26
-- ChampionsRoomPlayerFollowsOakScript / WalkToHallOfFame_RLEMovement.
-- The player walks out after Oak instead of the screen just fading on the
-- spot (#704). Route one tile right before walking north so the player
-- reaches the north-wall HALL_OF_FAME warp without sharing the rival's
-- (4,2) cell. The original simulated movement bypasses entity collision,
-- but this scene should not visibly walk through the defeated rival.
{ "move_player", "right", 1 }, -- 26
{ "move_player", "up", 3 }, -- 27
-- hand the induction off to the HALL_OF_FAME room (consumed by its
-- onEnter), then warp up into it (destWarp 1 lands at (4,7) facing up)
{ "set_field", "pendingHallOfFame", true }, -- 27
{ "warp", "HALL_OF_FAME", 4, 7, "up" }, -- 28
{ "set_field", "pendingHallOfFame", true }, -- 28
{ "warp", "HALL_OF_FAME", 4, 7, "up" }, -- 29
}
M.CHAMPIONS_ROOM = {
+16 -7
View File
@@ -400,15 +400,24 @@ M.POKEMON_FAN_CLUB = {
TEXT_POKEMONFANCLUB_CHAIRMAN = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 2
{ "jump_if_true", 9 }, -- 3
{ "show_text", "_PokemonFanClubChairmanIntroText" }, -- 4
{ "show_text", "_PokemonFanClubChairmanStoryText" }, -- 5
{ "jump_if_true", "nothing_left" }, -- 3
-- YesNoChoice (scripts/PokemonFanClub.asm): NO forfeits the voucher (#1050)
{ "ask", "_PokemonFanClubChairmanIntroText" }, -- 4
{ "jump_if_false", "no_story" }, -- 5
{ "show_text", "_PokemonFanClubChairmanStoryText" }, -- 6
-- give-then-print like scripts/PokemonFanClub.asm (GiveItem
-- fills wStringBuffer; the received text reads it)
{ "give_item", "BIKE_VOUCHER", 1, false }, -- 6
{ "show_text", "_PokemonFanClubReceivedBikeVoucherText" }, -- 7
{ "set_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 8
{ "show_text", "_PokemonFanClubExplainBikeVoucherText" }, -- 9
{ "give_item", "BIKE_VOUCHER", 1, false }, -- 7
{ "show_text", "_PokemonFanClubReceivedBikeVoucherText" }, -- 8
{ "set_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 9
{ "show_text", "_PokemonFanClubExplainBikeVoucherText" }, -- 10
{ "jump", "end" }, -- 11
{ "label", "no_story" }, -- 12
{ "show_text", "_PokemonFanClubNoStoryText" }, -- 13
{ "jump", "end" }, -- 14
-- .nothingleft: the gift is done, he only reminisces now
{ "label", "nothing_left" }, -- 15
{ "show_text", "_PokemonFanClubChairFinalText" }, -- 16
},
},
}
+38 -25
View File
@@ -27,11 +27,16 @@ local function ask(game, s, cb)
game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
end
-- fill the extracted text placeholders ({NUM:...}, {RAM:...}, {PLAYER})
-- fill text placeholders; key on the hram/wram symbol first, since one
-- string can carry two different NUM slots (#1006)
local function fill(s, subs)
s = s:gsub("{PLAYER}", subs.player or "")
s = s:gsub("{NUM:[^}]*}", function() return tostring(subs.num or "") end)
s = s:gsub("{RAM:[^}]*}", function() return subs.ram or "" end)
s = s:gsub("{NUM:([%w_]*)[^}]*}", function(name)
return tostring(subs[name] or subs.num or "")
end)
s = s:gsub("{RAM:([%w_]*)[^}]*}", function(name)
return subs[name] or subs.ram or ""
end)
return s
end
@@ -82,9 +87,12 @@ local function oaksAide(threshold, itemId, repeatText)
{ ram = itemName, player = game.save.player.name }), done)
end)
else
-- .notEnoughOwnedMons prints owned then requirement, two counts
push(game, fill(t._OaksAideUhOhText or
"You have only\ncaught {NUM:}!",
{ num = owned, ram = itemName }), done)
{ num = owned, ram = itemName,
hOaksAideNumMonsOwned = owned,
hOaksAideRequirement = threshold }), done)
end
end)
end
@@ -245,27 +253,32 @@ M.FIGHTING_DOJO = {
M.SILPH_CO_7F = {
talk = {
TEXT_SILPHCO7F_SILPH_WORKER_M1 = function(game, ow, npc, done)
local t = text(game)
if game.save.flags.EVENT_GOT_LAPRAS then
push(game, t._SilphCo7FSilphWorkerM1LaprasDescriptionText
or "How is LAPRAS\ndoing?", done)
return
end
push(game, t._SilphCo7FSilphWorkerM1ThankYouText
or "Thank you for\nsaving us!\fI want you to\nhave this LAPRAS!",
function()
game.save.flags.EVENT_GOT_LAPRAS = true
local Commands = require("src.script.Commands")
Commands.give_pokemon({ save = game.save, game = game, overworld = ow },
"LAPRAS", 15)
push(game, ("%s got\nLAPRAS!"):format(game.save.player.name),
function()
push(game, t._SilphCo7FSilphWorkerM1LaprasDescriptionText
or "It's a good\nswimmer!", done)
end)
end)
end,
-- command rows, not a Lua handler: give_pokemon needs a runner to AskName (#1049)
TEXT_SILPHCO7F_SILPH_WORKER_M1 = {
{ "face_player" },
{ "check_flag", "EVENT_GOT_LAPRAS" },
{ "jump_if_true", "has_lapras" },
{ "show_text", "_SilphCo7FSilphWorkerM1HaveThisPokemonText" },
{ "give_pokemon", "LAPRAS", 15 },
{ "jump_if_false", "box_full" },
-- flag ahead of the jingle, like the Celadon EEVEE (#426)
{ "set_flag", "EVENT_GOT_LAPRAS" },
{ "play_sound", "Get_Item1" },
{ "show_text", "_GotMonText", { RAM = "LAPRAS" } },
{ "show_text", "_SilphCo7FSilphWorkerM1LaprasDescriptionText" },
{ "jump", "end" },
{ "label", "box_full" },
{ "show_text", "_BoxIsFullText" },
{ "jump", "end" },
-- SilphCo7F.asm .saved_silph gates the thanks on Giovanni
{ "label", "has_lapras" },
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
{ "jump_if_true", "saved" },
{ "show_text", "_SilphCo7FSilphWorkerM1IsOurPresidentOkText" },
{ "jump", "end" },
{ "label", "saved" },
{ "show_text", "_SilphCo7FSilphWorkerM1SavedText" },
},
},
}
+1 -1
View File
@@ -38,7 +38,7 @@ the same core data and graphics into the source tree for verification.
| | `src/core/SaveData.lua` | Lua-serialized save in the LÖVE save dir |
| render | `src/render/Renderer.lua` | 160x144 canvas, integer nearest scaling |
| | `src/render/TileRenderer.lua` | one SpriteBatch per map (8x8 quads) + border-block ring |
| | `src/render/SpriteRenderer.lua` | 6-frame walker sheets, flipped right facing |
| | `src/render/SpriteRenderer.lua` | variable-size anchored sprite sheets, 6-frame walkers and flipped right facing |
| | `src/render/Font.lua` | glyph rendering via charmap (greedy longest match) |
| | `src/render/TextBox.lua` | dialogue box: typewriter, `\n` line, `\v` scroll, `\f` page |
| | `src/render/Camera.lua`, `Transition.lua` | follow camera, warp fades |
@@ -0,0 +1,615 @@
# Pokémon Gold, Silver, and Crystal: A Systematic Engine, ASM, and Behavioral Comparison
## Executive summary
Pokémon Gold and Silver are best understood as **two data-configured builds of one engine**, whereas Pokémon Crystal is an **expanded, reorganized, Color-only derivative of that engine**. The `pret/pokegold` repository reproducibly builds the international Gold and Silver ROMs byte-for-byte, while `pret/pokecrystal` separately builds multiple international Crystal revisions and debug artifacts. This makes the two repositories unusually strong primary evidence: their output hashes correspond to known retail binaries rather than merely reimplementing observed behavior.
The largest technical conclusion is that Crystal is **not a ground-up engine rewrite**. Core subsystems (the frame-driven sound interpreter, hardware-derived RNG, synchronized link-battle PRNG, wild-encounter probability machinery, party-mon record size, and much of the RTC pipeline) remain recognizably inherited from Gold/Silver. Crystal instead adds new ROM banks, reorganizes routines, introduces Color-specific rendering paths, fills formerly reserved fields in Pokémon records, redesigns SRAM placement, and layers Battle Tower, Mobile System GB, animated sprites, the female protagonist, Buena, Move Tutor, Suicune story logic, and Virtual Console hooks around the inherited core.
The most important behavioral differences are therefore caused less by wholesale algorithm replacement than by **changed control flow and data**:
| Area | Gold and Silver | Crystal | Technical consequence |
|---|---|---|---|
| Edition structure | One shared engine with edition-selected encounter, sprite, title, and other data | One enhanced-version build with regional/revision conditionals | Gold-to-Silver ports are mostly data substitutions; Gold/Silver-to-Crystal ports are architectural merges |
| Display target | Runtime distinguishes DMG/SGB/CGB modes | Explicit Color-only path | Crystal code can assume CGB palettes, VRAM banking, and related presentation behavior more aggressively |
| RNG core | Divider-register accumulator plus synchronized link PRNG | Same algorithms | Different outcomes normally arise from different call timing or paths, not a different generator |
| Party records | Two bytes after Pokérus are reserved | Those same bytes become caught-time/gender/level/location metadata | Pokémon records remain structurally compatible despite Crystal gaining metadata |
| Save layout | Main and backup data are fragmented among SRAM sections | Main and backup records become more contiguous; Crystal/Battle Tower/Mobile blocks are added | Whole `.sav` files are not drop-in interchangeable |
| Battles | Standard Gen II battle engine | Inherited engine plus Battle Tower branches and selective bug fixes | Link-compatible behavior is intentionally retained in some otherwise-fixed cases |
| Roaming Pokémon | Raikou, Entei, and Suicune use the roaming system | Only Raikou and Entei roam; Suicune becomes a scripted encounter | Encounter-state conversion requires special handling |
| Graphics | Static Pokémon fronts and DMG/SGB-compatible layouts | Animated fronts, revised sprites, richer Color layouts | Animation introduces new graphics data, decompression, frame, and timing requirements |
| Audio | Shared four-channel Game Boy music interpreter | Same interpreter plus new songs and call sites | Music ports are mostly data/pointer work rather than synthesizer rewrites |
| Major fixed defects | Coin Case arbitrary code execution, first-save Hall of Fame corruption, Lucky Number box limit, and several map/text defects | Corrected | Crystal is safer, but not "bug-fixed Gen II" in the broad sense |
| Persistent defects | Many Gen II battle, capture, item, and badge defects | Many remain | Mechanical compatibility often outweighed cleanup |
Gold and Silver themselves have almost no meaningful engine-level divergence. Their scientifically interesting comparison is principally **which tables and assets are selected at assembly time**. Crystal is the meaningful engine comparison.
The report assumes a technically literate audience familiar with low-level programming but not necessarily Game Boy internals. Unless explicitly stated, "Crystal" means the international English code represented by `pret/pokecrystal`; Japanese Mobile System GB and Australian/revision-specific behavior are treated as regional variants. "Scientific" here means reproducible binary-derived comparison, explicit source hierarchy, and separation of direct evidence from inference, not laboratory experimentation involving original development source code, which has not been publicly released.
## Scope, evidence, and reproducibility
### Source hierarchy
The strongest sources are the two pret disassemblies:
- [`pret/pokegold`](https://github.com/pret/pokegold), which builds exact Gold and Silver international ROM images.
- [`pret/pokecrystal`](https://github.com/pret/pokecrystal), which builds Crystal international v1.0, v1.1, Australian, and debug images.
The repositories identify exact SHA-1 outputs. Gold builds to `d8b8a3600a465308c9953dfa04f0081c05bdcb94`, Silver to `49b163f7e57702bc939d642a18f591de55d92dae`, Crystal international v1.0 to `f4cd194bdee0d04ca4eac29e09b8e4e9d818c133`, and Crystal international v1.1 to `f2f52230b536214ef7c9924f483392993e226cfb`. Those reproducible identities are critical: a label such as `BadgeStatBoosts` is a reverse-engineered name, but the emitted bytes and resulting behavior are those of the retail ROM.
Secondary sources such as Bulbapedia and Serebii are useful for player-visible cross-checking, release differences, encounter availability, and historical context. The pret bug documents are stronger than ordinary wiki summaries for defects because they identify the responsible instructions and provide corrective diffs. Official Nintendo and Pokémon sources are most valuable for release/Virtual Console behavior and supported transfer paths, but they do not publish the games' assembly architecture.
### Reproducible comparison method
A rigorous comparison should operate at four levels:
| Level | Method | What it establishes | Main limitation |
|---|---|---|---|
| Binary identity | Build each repository with its pinned RGBDS-compatible toolchain and verify SHA-1 | The source corresponds to the target retail ROM | Does not explain why bytes differ |
| Source topology | Compare `main.asm`, `home.asm`, bank sections, include graphs, WRAM/SRAM declarations | Architectural additions, removals, and relocation | File organization can change without behavioral change |
| Routine semantics | Compare labels, branches, register use, calls, data offsets, and side effects | Control-flow and algorithmic differences | Labels and comments are community-derived |
| Behavioral validation | Run controlled emulator tests with trace logging and fixed input timing | Observable consequences and RNG/cycle sensitivity | Emulator accuracy and RTC state must be controlled |
For a formal experiment, the recommended independent variables are edition, ROM revision, initial SRAM, RTC register state, boot hardware mode, input sequence, and link mode. Dependent variables include RNG bytes, encounter choice, battle result, SRAM writes, audio commands, VRAM/OAM writes, and cycle/frame number. Because the ordinary RNG reads the divider register, deterministic comparisons must start from a controlled boot state and reproduce input timing at frame or cycle precision.
### Gold versus Silver as a build-configuration comparison
The fact that `pokegold` emits both Gold and Silver from one repository is itself strong evidence that the two versions share the same broad code architecture. Edition differences are implemented through conditional assembly and alternate data or assets rather than through separate engines. The obvious behavioral results are version-specific wild Pokémon, title graphics, mascot-facing presentation, and selected NPC or table data; battle arithmetic, saves, RTC processing, audio interpretation, and link handling come from the same shared source.
This distinction matters for analysis. Treating Gold, Silver, and Crystal as three equally separate engines exaggerates Gold/Silver differences and understates Crystal's structural changes. A better model is:
```mermaid
flowchart TD
GSC[Generation II design and data model]
GS[Gold/Silver shared engine]
GD[Gold-selected data and assets]
SD[Silver-selected data and assets]
C[Crystal derivative engine]
CF[Crystal-only feature modules]
CR[Crystal revisions and regional flags]
GSC --> GS
GS --> GD
GS --> SD
GS --> C
C --> CF
C --> CR
```
## ROM and engine architecture
### Banked execution model
The games execute on the Game Boy's LR35902-class CPU and are organized around a fixed home bank plus switchable ROM banks. Calls across banks generally pass through wrappers that save the current bank, select the target bank, call the routine, and restore the original bank. The `BattleRandom` wrapper is a concise example: `_BattleRandom` resides outside the home bank, so the wrapper saves `hROMBank`, uses the bank-switch restart vector, calls the routine, preserves its return value, and restores the previous bank.
```asm
BattleRandom::
ldh a, [hROMBank]
push af
ld a, BANK(_BattleRandom)
rst Bankswitch
call _BattleRandom
...
pop af
rst Bankswitch
ret
```
Relevant sources: [Gold/Silver `home/random.asm`](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/home/random.asm#L29-L46) and [Crystal `home/random.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/home/random.asm#L29-L46).
The repositories' top-level `main.asm` files are effectively link manifests for this architecture. Gold/Silver's file groups the battle core, encounters, party menu, RTC, phone, Pokégear, sprite animation, graphics loading, and other systems into named ROM sections. Crystal retains those categories but adds explicitly named feature sections and many additional modules.
### Crystal as an extension and bank-layout rewrite
Crystal's `main.asm` introduces a conspicuous `Crystal Features 1` section containing gender initialization, Kris-specific bag handling, Move Tutor, Crystal layouts, Celebi logic, the redesigned main menu, Mobile menu code, owned-Pokémon search, and Buena's menu. Nearby banks add Battle Tower trainer logic, caught-data handling, a revised stats screen, sliding battle intros, battle-scene checks, and the Color-only startup screen.
That include topology supports three conclusions.
First, Crystal preserves **vertical subsystem continuity**. The battle engine is still `engine/battle/core.asm`; encounters remain `engine/overworld/wildmons.asm`; Pokémon records still derive from the same party/box macros; and audio is still driven by the same interpreter.
Second, Crystal adds **horizontal feature coupling**. Battle Tower touches battle setup, stat boosts, experience rules, saving, menus, SRAM, trainers, music, and link-like constraints. The female protagonist touches initialization, sprites, bag graphics, caught metadata, and menu presentation. Mobile support touches interrupts, SRAM, menus, communications, rankings, and audio.
Third, code and data relocation is substantial enough that raw ROM addresses from Gold/Silver cannot generally be transplanted into Crystal. A symbolic port based on labels and structures is practical; a patch based on absolute offsets is brittle.
### File and routine comparison
| Subsystem | Gold/Silver source | Crystal source | ASM-level difference | Porting significance |
|---|---|---|---|---|
| Top-level bank graph | [`main.asm`](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/main.asm) | [`main.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/main.asm) | Crystal adds feature, Battle Tower, Mobile, gender, animation, and caught-data modules and relocates shared includes | Port by symbol and feature dependency, not ROM offset |
| Ordinary RNG | [`home/random.asm`](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/home/random.asm) | [`home/random.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/home/random.asm) | Essentially identical routine and rejection-sampling helper | RNG-sensitive ports must preserve call timing |
| Battle core | [`engine/battle/core.asm`](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/engine/battle/core.asm) | [`engine/battle/core.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/engine/battle/core.asm) | Crystal adds Battle Tower guards and conditional fixes while retaining link behavior | Mechanical changes can desynchronize unmodified peers |
| Encounters | [`engine/overworld/wildmons.asm`](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/engine/overworld/wildmons.asm) | [`engine/overworld/wildmons.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/engine/overworld/wildmons.asm) | Same rate/slot pipeline; different swarm state and two rather than three roamers | World-state conversion needs special Suicune handling |
| SRAM declaration | [`ram/sram.asm`](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/ram/sram.asm) | [`ram/sram.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/ram/sram.asm) | Crystal consolidates save/backup regions and adds GS Ball, Battle Tower, rankings, and Mobile blocks | Whole-save binary compatibility is lost |
| Pokémon structures | [`pokemon_data_constants.asm`](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/constants/pokemon_data_constants.asm#L69-L103) | [`pokemon_data_constants.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/constants/pokemon_data_constants.asm#L69-L108) | Crystal assigns caught metadata to two bytes reserved in Gold/Silver | Record length remains compatible |
| RTC/home time | [`home/time.asm`](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/home/time.asm) | [`home/time.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/home/time.asm) | Same RTC model; Crystal timer interrupt can dispatch Mobile timing | Emulator ports must emulate RTC and, for Japanese features, mobile timing assumptions |
| Picture loading | [`engine/gfx/load_pics.asm`](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/engine/gfx/load_pics.asm) | [`engine/gfx/load_pics.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/engine/gfx/load_pics.asm) | Crystal has additional front-picture and animation-oriented handling | Static sprite replacement is insufficient |
| Sound interpreter | [`audio/engine.asm`](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/audio/engine.asm) | [`audio/engine.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/audio/engine.asm) | Core interpreter is structurally the same | New Crystal music is primarily sequenced data and pointers |
| Song table | [`audio/music_pointers.asm`](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/audio/music_pointers.asm) | [`audio/music_pointers.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/audio/music_pointers.asm#L97-L108) | Crystal appends ten named songs | Existing song IDs remain stable through the Gold/Silver range |
The line counts of several Crystal files are larger, but line count alone is not a behavioral metric; comments, label quality, and source refactoring affect it. The stronger evidence is the presence of new branches, fields, tables, and externally observable side effects.
### Hardware-mode divergence
Gold/Silver initialization tests whether the console is a Game Boy Color and maintains non-CGB paths, including Super Game Boy initialization. Crystal includes an explicit `gbc_only.asm` module that displays the incompatibility message on non-CGB hardware. That is not merely marketing metadata: it changes what assumptions later rendering code may safely make about palettes, VRAM banking, and Color hardware.
For a reverse port of Crystal features into Gold/Silver, every CGB-only feature falls into one of three categories: provide a monochrome/SGB fallback, disable it outside CGB mode, or intentionally convert the resulting ROM into a Color-only build. Skipping that decision produces subtle failures rather than one clean compile error: palette attributes, second-bank VRAM data, and tile-upload timing can all be implicated.
## Save, RNG, clock, and entity state
### Save architecture
Gold/Silver divide their main game state into options, three player-data fragments, current-map data, Pokémon data, and a checksum. Backup pieces are distributed among several separately placed SRAM sections. The current PC box is separate, the fourteen inactive boxes occupy exactly two SRAM banks, and mail and Mystery Gift have dedicated regions.
Crystal instead defines a contiguous `sGameData` consisting of player, map, and Pokémon data, pads the region, and stores a checksum. Its backup is similarly grouped. Crystal then adds a GS Ball flag, `sCrystalData`, Battle Tower progress and recent-trainer state, ranking records, Mobile communication data, offers, credentials, and Japanese Mobile-specific buffers. The source explicitly notes that the international `sCrystalData` location differs from Japanese Crystal.
| Save component | Gold/Silver | Crystal |
|---|---|---|
| Options and corruption sentinels | Present | Present |
| Main player/map/Pokémon data | Split into labeled player fragments inside the save region | Grouped into one contiguous main-game block |
| Main checksum | 16-bit checksum after game data | 16-bit checksum after padded game-data region |
| Backup | Fragmented among multiple SRAM sections | Consolidated backup game-data block |
| Current box | Separate `curbox` structure | Separate full `box` structure plus padding |
| Inactive boxes | Fourteen boxes over two SRAM banks | Same fourteen-box capacity over two banks |
| Party and mailbox mail | Dedicated primary and backup regions | Same broad model |
| GS Ball state | Absent | Primary and backup flags |
| Battle Tower | Absent | Challenge state, streak, previous teams, reward |
| Mobile/rankings | Absent | Several additional SRAM sections |
| Full-file compatibility | Gold and Silver are closely related but still edition-specific | Not layout-compatible with Gold/Silver |
Crystal's save routine makes the transactional sequence visible:
```asm
_SaveGameData:
farcall StageRTCTimeForSave
farcall BackupMysteryGift
call ValidateSave
call SaveOptions
call SavePlayerData
call SavePokemonData
call SaveBox
call SaveChecksum
call ValidateBackupSave
...
farcall BackupGSBallFlag
farcall SaveRTC
```
Source: [Crystal `engine/menus/save.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/engine/menus/save.asm#L255-L284). The routine stages RTC state, writes the primary data and box, computes the primary checksum, writes backup data and checksum, backs up party mail and GS Ball state, persists RTC state, and normalizes a completed Battle Tower reward state.
The practical conclusion is precise: **do not migrate a Gold/Silver `.sav` to Crystal by copying the file or the main save block**. A converter should parse symbolic fields, validate checksums and sentinels, copy player/map/Pokémon/box/mail data field by field, initialize Crystal-only state, and regenerate Crystal checksums. Gold-to-Silver conversion is less structurally disruptive, but version-dependent event and encounter state should still be treated deliberately.
### Party and boxed-Pokémon records
One of Crystal's most elegant compatibility decisions is visible at the structure-definition level. In Gold/Silver, the two bytes after `MON_POKERUS` are simply reserved:
```asm
DEF MON_HAPPINESS rb
DEF MON_POKERUS rb
rb_skip 2
DEF MON_LEVEL rb
```
In Crystal, the same positions are assigned to a two-byte caught-data field:
```asm
DEF MON_HAPPINESS rb
DEF MON_POKERUS rb
DEF MON_CAUGHTDATA rw
DEF MON_LEVEL rb
```
Sources: [Gold/Silver structure](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/constants/pokemon_data_constants.asm#L83-L101) and [Crystal structure](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/constants/pokemon_data_constants.asm#L83-L107).
Crystal packs caught time and trainer gender into one byte and caught level and location into another. Because Game Freak repurposed reserved bytes instead of extending the record, `BOXMON_STRUCT_LENGTH` and `PARTYMON_STRUCT_LENGTH` remain compatible. Gold/Silver can carry those bytes without understanding them; Crystal can interpret them when present. This is a major reason Gen II Pokémon can move among Gold, Silver, and Crystal without a record-size translation layer.
A second structure change appears in the base-species TM/HM compatibility bitset. Gold/Silver size it for `NUM_TM_HM`; Crystal sizes it for `NUM_TM_HM_TUTOR`, accommodating Move Tutor compatibility in the same species-data model. This means a straight copy of Gold/Silver base-stat records into Crystal must account for Crystal's expanded learnability domain even if the visible species stats are unchanged.
### Item and party handling
The basic inventory model (separate item pockets, held-item byte in each Pokémon record, party length of six, PC boxes, mail records, and item-dispatch routines) is inherited. Crystal's changes are concentrated in new consumers and UI behavior: Move Tutor eligibility, gender-specific player presentation, caught metadata, Battle Tower restrictions, and additional event items such as GS Ball state.
Crystal did **not** comprehensively repair the item mechanics. Its documented defects still include Moon Ball not applying its intended multiplier, Love Ball checking the wrong gender relationship, Fast Ball applying to only a few species, three incorrect Heavy Ball weight cases, and status conditions failing to affect capture rate as intended. Those defects live in shared mechanical paths and are important when evaluating a "faithful" engine port: correcting them changes gameplay and may alter deterministic test vectors.
### Ordinary RNG
Gold/Silver and Crystal use the same ordinary RNG routine. It samples the hardware divider register and updates two one-byte accumulators, one by addition-with-carry and one by subtraction-with-carry:
```asm
ldh a, [rDIV]
ld b, a
ldh a, [hRandomAdd]
adc b
ldh [hRandomAdd], a
ldh a, [rDIV]
ld b, a
ldh a, [hRandomSub]
sbc b
ldh [hRandomSub], a
```
Sources: [Gold/Silver](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/home/random.asm#L13-L27) and [Crystal](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/home/random.asm#L13-L27). The files are effectively identical, including the `RandomRange` rejection-sampling routine used to avoid simple modulo bias.
This generator is not a self-contained seeded PRNG in the modern sense. Its output depends on divider phase, previous accumulator state, carry state, VBlank updates, and when the routine is reached. Consequently, adding an animation, menu delay, conditional call, or extra random-consuming feature can alter later outcomes even while the RNG instructions remain unchanged.
### Link-battle PRNG
Battles route randomness through `BattleRandom`. In non-link play, `_BattleRandom` ultimately uses the ordinary RNG. In linked battles, both systems consume a shared ten-byte random sequence and advance bytes with the recurrence:
```
x[n+1] = (5 * x[n] + 1) mod 256
```
The Gold/Silver and Crystal battle cores contain the same broad synchronization mechanism.
This has a major compatibility consequence: a battle patch can be logically correct in isolation yet break link play if it causes one participant to make a different number or order of RNG calls. That is why Crystal sometimes preserves Gold/Silver behavior specifically in link battles. Synchronization depends on matching control flow, not merely using the same random-number formula.
### RTC and time-of-day behavior
Both codebases latch the MBC3-style real-time clock, read seconds, minutes, hours, and the low/high day registers, normalize time, add the player's selected starting offset, and derive the current time-of-day state. Crystal's `UpdateTime` remains a short chain of `GetClock`, `FixDays`, `FixTime`, and `GetTimeOfDay`.
Crystal's source shows the day counter reduced modulo 140 for the game's weekly/event model, with status flags distinguishing an RTC count beyond 139 days and a hardware day-high overflow beyond 255 days. The displayed game time is formed by adding the new-game start offsets to RTC values with carry through seconds, minutes, hours, and day.
The notable architectural addition is the Crystal timer interrupt's Mobile dispatch:
```asm
Timer::
push af
ldh a, [hMobile]
and a
jr z, .not_mobile
call MobileTimer
.not_mobile
pop af
reti
```
Source: [Crystal `home/time.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/home/time.asm#L2-L11). The ordinary RTC logic remains inherited; Japanese/mobile Crystal adds another timing consumer.
For emulators, flash cartridges, and ports, SRAM alone is insufficient. RTC registers, halt/carry state, elapsed host time, and game offsets must be preserved consistently. A save imported without corresponding RTC state may be structurally valid yet produce incorrect daily events, phone behavior, berries, swarms, day-of-week encounters, or time-dependent evolutions.
## Battles and encounters
### Battle-engine inheritance
The core Gen II battle model is shared: turn selection, speed ordering, accuracy, damage, stat stages, held items, status, volatile effects, experience, capture, AI, and link synchronization follow the same broad engine. Crystal's battle core is larger principally because it adds Battle Tower integration, additional presentation paths, and selective corrections.
Crystal is therefore mechanically closer to a patched and extended Gold/Silver than to a later-generation ruleset. There are no abilities, natures, modern physical/special move split, or rewritten damage model. Its new content operates within Gen II's existing move and Pokémon structures.
### Control-flow difference: Battle Tower exclusions
Gold/Silver's `BadgeStatBoosts` exits for link battles and otherwise applies badge boosts. Crystal adds a second early return when `wInBattleTowerBattle` is nonzero:
```asm
ld a, [wLinkMode]
and a
ret nz
ld a, [wInBattleTowerBattle]
and a
ret nz
```
Source: [Crystal `BadgeStatBoosts`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/engine/battle/core.asm#L6526-L6532). Gold/Silver have only the link-mode guard at the corresponding point.
This tiny six-instruction addition is representative of Crystal's design. The stat algorithm was not replaced; Crystal introduced a new battle context and inserted a guard so adventure-only badge advantages do not leak into the standardized Battle Tower. Similar context checks appear around experience and other post-battle handling.
### Selective fixes constrained by link compatibility
The pret Crystal bug documentation explicitly states that Crystal fixed Gold/Silver's Reflect/Light Screen overflow and Present damage behavior only where doing so would not break ordinary cross-version link battles. Link-mode behavior retains the compatible path.
That is an important historical engineering tradeoff. A fully corrected Crystal battle engine would disagree with Gold/Silver on intermediate values and potentially on RNG consumption, damage, fainting, or message sequence. Compatibility with the installed base was treated as a protocol requirement. Consequently, "Crystal fixed the bug" can mean **fixed in single-player but deliberately retained in linked simulation**.
### Shared and persistent battle defects
Crystal retains a substantial Gen II defect surface. Documented examples include:
| Defect | Mechanical effect | Crystal status |
|---|---|---|
| "100%" secondary effects | Fail in 1/256 qualifying cases | Retained |
| Belly Drum | May maximize Attack even when the HP requirement is not properly met | Retained |
| Berserk Gene | Confusion duration can become 256 turns or inherit stale state | Retained |
| Confusion damage | Can receive type-item and Explosion/Self-Destruct modifiers | Retained |
| Beat Up | Can behave incorrectly and can desynchronize link battles | Retained |
| Return/Frustration edge | Can produce zero damage at extreme happiness values | Retained |
| Dragon boosting item | Dragon Scale is checked instead of Dragon Fang | Retained |
| Glacier Badge | Special Defense boost depends incorrectly on overwritten accumulator state | Retained |
| Capture status bonus | Burn, poison, and paralysis do not contribute as intended | Retained |
| Specialty Balls | Multiple Apricorn-ball formulas target the wrong condition or table | Retained |
These are documented against original Crystal code, not inferred from modern competitive summaries.
The Glacier Badge defect is especially instructive at ASM level. The routine shifts a badge bitfield through register `b`, calls `BoostStat`, and later reuses register `a` as if it still contained the expected badge value. `BoostStat` can overwrite `a`, so whether Special Defense receives its boost depends on the Special Attack calculation's resulting register state. The bug survived Crystal even though the source gained an explicit comment identifying it.
### Gold/Silver defects corrected in Crystal
The `pokegold` bug document separates defects fixed in Crystal from defects still shared with it. It identifies seven clear Gold/Silver corrections: Coin Case arbitrary code execution, Hall of Fame corruption when no prior save exists, Lucky Number failure to inspect boxes 10-14, Present text overflow, surfing onto NPCs, fishing inside Cerulean Gym, and Route 15 capitalization.
The Coin Case correction is one bytecode-level terminator change:
```diff
text "Coins:"
line "@"
text_decimal wCoins, 2, 4
-done
+text_end
```
Source: [`pokegold/docs/bugs_and_glitches.md`](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/docs/bugs_and_glitches.md#L20-L34). The Gold/Silver terminator permits text-command execution to continue into unintended memory under exploitable conditions; Crystal ends the text stream correctly.
The Hall of Fame correction adds a saved-at-least-once check and erases/initializes previous-save structures before attempting the Hall of Fame write. The Lucky Number correction changes the loop bound from the Japanese box count constant to the international fourteen-box count. Both are good examples of Crystal correcting localization-sensitive state assumptions rather than changing game design.
### Encounter-rate algorithm
Gold/Silver and Crystal use essentially the same high-level encounter pipeline:
```mermaid
flowchart TD
STEP[Eligible movement step]
RATE[Read morning/day/night or water rate]
MOD1[Apply radio modifier]
MOD2[Apply Cleanse Tag modifier]
ROLL[Call Random and compare with rate]
SLOT[Choose encounter slot]
ROAM[Check roaming Pokémon]
REPEL[Apply Repel level check]
BATTLE[Stage wild battle]
STEP --> RATE --> MOD1 --> MOD2 --> ROLL
ROLL -->|pass| SLOT
SLOT --> ROAM
ROAM --> REPEL
REPEL --> BATTLE
```
In both engines, `TryWildEncounter` gets the map rate, applies Pokémon March/Ruins of Alph doubling or Pokémon Lullaby halving, applies Cleanse Tag halving, draws an RNG byte, chooses a slot from grass or water probability tables, and checks Repel.
Grass records contain separate morning, day, and night slot blocks; the active block is selected through `wTimeOfDay`. Water uses a separate rate and three-slot table. The structure constants remain seven grass slots and three water slots in both codebases.
### Encounter-table differences
Gold and Silver primarily diverge through edition-conditioned tables. Crystal has its own consolidated tables and changes both availability and placement. It makes several former Gold/Silver exclusives obtainable in one edition, while removing some species available in both base versions; Mareep's evolutionary family is a prominent Crystal omission. Crystal also moves species such as Sneasel to different locations.
The engine/table distinction is important. An encounter may differ because:
1. the map's encounter-rate byte changed;
2. a morning/day/night slot changed;
3. the slot's level changed;
4. a swarm override changed;
5. a roaming Pokémon was removed from the roaming subsystem;
6. the map itself gained or lost encounter-enabled tiles.
A robust diff should therefore compare map headers, wild tables, swarm flags, and roaming initialization, not merely produce a Pokédex availability list.
### Swarms and roaming state
Gold/Silver's swarm lookup is comparatively generic: it compares the current map against `wSwarmMapGroup` and `wSwarmMapNumber`. Crystal's code has explicit Dunsparce and Yanma swarm flags and corresponding map state. That is a data-model specialization, not just a changed encounter table.
The roaming difference is even clearer. Gold/Silver initialize three records:
```asm
ld a, RAIKOU
ld [wRoamMon1Species], a
ld a, ENTEI
ld [wRoamMon2Species], a
ld a, SUICUNE
ld [wRoamMon3Species], a
```
Crystal initializes only Raikou and Entei:
```asm
ld a, RAIKOU
ld [wRoamMon1Species], a
ld a, ENTEI
ld [wRoamMon2Species], a
```
Sources: [Gold/Silver `InitRoamMons`](https://github.com/pret/pokegold/blob/a0dad0957ac8a9ffa67e950ee3ab6715a212ded5/engine/overworld/wildmons.asm#L471-L511) and [Crystal `InitRoamMons`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/engine/overworld/wildmons.asm#L476-L506).
Gold/Silver's subsequent selection logic describes an equal choice among three beasts after the roaming check succeeds; Crystal changes the corresponding comment and index range to two. Suicune's removal is tied to Crystal's expanded Eusine/Suicune plot and scripted Tin Tower encounter.
For save conversion, a Gold/Silver Suicune roaming record cannot simply remain active in Crystal. The converter must map capture/defeat/event state into Crystal's scripted Suicune flags or deliberately define a hybrid behavior.
## Graphics and audio
### Tile, palette, and sprite model
All three games retain the Game Boy's tile-oriented rendering model: graphics are stored as compact tile data, decompressed or copied into VRAM, arranged through background/window tilemaps, and supplemented by hardware sprites through shadow OAM. Pokémon and many UI images use four-color source palettes, with separate palette data determining their actual colors. The pokecrystal FAQ explicitly notes the four-color paletted-PNG convention and the distinction between image data and palette data.
Gold/Silver must support original Game Boy-style output and Super Game Boy behavior as well as Game Boy Color enhancements. Crystal's explicit Color-only startup path lets it make stronger use of CGB layout and palette machinery. Its top-level architecture adds `crystal_layouts.asm`, dedicated player graphics, revised map and battle presentation, and other CGB-focused modules.
### Animated Pokémon sprites
Crystal's headline renderer change is animated front sprites. Every Pokémon receives an entrance animation, and the status/profile viewer can play a longer animation; several designs, palettes, and back sprites were also revised. Crystal additionally gives the legendary beasts distinct overworld sprites and introduces richer trade-screen presentation.
At the engine level, this requires more than storing extra frames. A full animation path needs:
- a base front picture and additional frame or bitmask data;
- frame sequencing and duration state;
- tile-buffer reconstruction or differential tile updates;
- synchronization with battle intro control flow;
- palette and VRAM updates during safe LCD periods;
- fallback behavior for static contexts such as link displays or icons.
Crystal's `load_pics.asm` is correspondingly larger and is integrated with dedicated animation and battle-intro modules, while Gold/Silver's loader primarily services static front/back pictures. The exact file-size difference should not be treated as a performance measurement, but the added code paths confirm a broader graphics pipeline.
Animated fronts can also alter RNG-observable timing indirectly. The ordinary RNG is updated from divider timing and VBlank activity; therefore, any test that compares post-animation random outcomes must control whether animations are enabled and how many frames elapsed. This is an inference from the animation and RNG architectures, not evidence that every animation directly invokes `Random`.
### Color and protagonist handling
Crystal adds player-gender initialization and Kris-specific bag/player graphics. The Pokémon record's caught-data bits can record whether the catcher was the boy or girl protagonist. This is an example of a feature spanning UI, overworld sprites, menus, Pokémon metadata, and save state rather than existing in one isolated "female player" switch.
A Crystal-to-Gold/Silver backport must decide how to handle caught-by-girl metadata when Gold/Silver have no female protagonist UI. Structurally the bytes can survive because they were reserved, but Gold/Silver will not natively display or generate the metadata.
### Sound-engine architecture
The most striking audio result is how little the interpreter changed. Both repositories' `audio/engine.asm` identify themselves as the entire sound engine, update once per frame, parse music commands, maintain eight software channel structures (four music and four sound-effect channels), and ultimately drive the four Game Boy audio channels. Both implementations handle duty, envelope, frequency, vibrato, noise sampling, channel muting, low-health sound, and fades through the same broad code.
```mermaid
flowchart LR
SONG[Song bytecode]
PARSE[ParseMusic]
STATE[Per-channel WRAM state]
FX[Vibrato / pitch / envelope / noise]
MIX[Music-SFX priority and routing]
APU[Game Boy audio registers]
SONG --> PARSE --> STATE --> FX --> MIX --> APU
```
Crystal therefore does not introduce a new synthesizer. It extends the music corpus and invokes new songs in new contexts.
### Crystal's additional music
Gold/Silver's song pointer table ends at `Music_PostCredits`. Crystal retains that ordering and appends ten entries:
| Crystal-only pointer entry | Use |
|---|---|
| `Music_Clair` | Clair-related scene |
| `Music_MobileAdapterMenu` | Japanese Mobile menu |
| `Music_MobileAdapter` | Mobile connectivity |
| `Music_BuenasPassword` | Buena's Password |
| `Music_LookMysticalMan` | Eusine encounter |
| `Music_CrystalOpening` | Revised opening |
| `Music_BattleTowerTheme` | Battle Tower battle/context |
| `Music_SuicuneBattle` | Legendary-beast/Suicune battle theme |
| `Music_BattleTowerLobby` | Battle Tower lobby |
| `Music_MobileCenter` | Japanese Mobile Center |
The pointer table labels these explicitly as "new to Crystal."
Bulbapedia independently notes that Crystal gives the legendary beasts a unique battle theme and presents it as the first core-series special legendary battle music.
From a porting perspective, bringing Crystal music into Gold/Silver primarily requires assigning ROM space, importing sequence data, adding pointer/constants entries, and adding selection call sites. Replacing the audio engine is generally unnecessary. However, preserving existing numeric song IDs is wise because map headers, scripts, battle setup, radio state, and fades refer to those constants.
## Bugs, compatibility, porting, and reverse-engineering timeline
### Bug and quirk matrix
| Category | Gold/Silver | Crystal | Engineering interpretation |
|---|---|---|---|
| Coin Case text terminator | Exploitable continuation can permit arbitrary code execution | Correct terminator | Parser/data correction |
| First-save Hall of Fame | Can corrupt PC boxes | Guard and initialization added | Save-state precondition correction |
| Lucky Number boxes | International boxes 10-14 omitted | Uses full box count | Localization constant corrected |
| Surf onto NPC | Possible | Facing-object check added | Collision precondition corrected |
| Cerulean Gym fishing | Enabled by map fish group | Disabled | Map-header data correction |
| Reflect/Light Screen overflow | Incorrect | Fixed outside compatible link path | Context-dependent battle fix |
| Present damage | Incorrect | Fixed outside compatible link path | Protocol-preserving fix |
| Glacier Badge Special Defense | Bugged | Still bugged | Shared inherited defect |
| Apricorn specialty balls | Multiple formula defects | Still defective | Shared inherited mechanics |
| Beat Up synchronization | Vulnerable | Still vulnerable | Shared link-engine defect |
| Secondary-effect 1/256 failure | Present | Present | Shared probability comparison defect |
| Animated-sprite quirks | Not applicable | New animation-specific defects possible | Feature expansion creates new failure surface |
The Gold/Silver and Crystal bug documents should be read together. The Gold/Silver document intentionally lists only bugs that Crystal fixed; any shared defects are documented in the Crystal repository instead.
Crystal is thus more polished but not mechanically "corrected" in a comprehensive sense. A modern source port must choose a compatibility target:
- **Retail-faithful:** preserve all version-specific bugs and timing.
- **Crystal-faithful:** preserve Crystal's selective fixes and its link-mode exceptions.
- **Corrected Gen II:** repair documented defects, accepting that link compatibility and historical RNG traces may change.
- **Hybrid:** gate corrections behind flags or negotiate them between identical modified link peers.
### ROM revisions and regional code
The Crystal repository builds international v1.0, v1.1, and Australian releases. Its FAQ states that v1.1 corrected some issues in the initial international release, while the Australian build is based on v1.1 and censors gambling references. Thus "Crystal behavior" is not completely singular even within English-language retail ROMs.
A serious test report should always state the target hash rather than merely "Pokémon Crystal." Otherwise, an observed difference may be edition-level, v1.0/v1.1-level, Australian localization, Japanese Mobile code, Virtual Console patching, emulator behavior, or an altered ROM.
### Link and trade compatibility
Gold, Silver, and Crystal share the Gen II Pokémon record length and battle/link architecture, allowing ordinary same-generation trading and battling. The retained two-byte record size, shared `REDMON_STRUCT_LENGTH` conversion constant for Time Capsule interaction, and synchronized battle RNG are all source-level evidence of compatibility-oriented design.
However, three distinct notions of compatibility must not be conflated:
| Compatibility type | Status |
|---|---|
| Pokémon record compatibility | Strong: same record length; Crystal fills reserved bytes |
| Link protocol/battle simulation compatibility | Strong for retail games, partly because Crystal preserves old link behavior |
| Whole-save-file compatibility | Weak: Crystal's SRAM organization and added state differ substantially |
| ROM patch-address compatibility | Weak: bank layout and code placement differ |
| Feature-source portability | Moderate: shared architecture helps, but dependencies are broad |
| Gen I Time Capsule data compatibility | Supported through dedicated conversion structures and restrictions |
| Modern transfer compatibility | Supported from 3DS VC releases through Poké Transporter and Pokémon Bank |
Nintendo's support documentation lists the Virtual Console releases of Gold, Silver, and Crystal as compatible with Poké Transporter. Transfers proceed into Pokémon Bank and can then move onward to Pokémon HOME, but that modern path is not a raw Gen II save conversion and is effectively one-way at later stages.
### Virtual Console modifications
Crystal's save code contains an explicit `vc_hook` after Hall of Fame insertion. The hook sets the primary and backup GS Ball flags to make the GS Ball quest and Celebi encounter available, with compile-time assertions pinning the expected SRAM addresses and flag value.
```asm
vc_hook Enable_GS_Ball_mobile_event
vc_assert BANK(sGSBallFlag) == $1
vc_assert BANK(sGSBallFlagBackup) == $1
```
Source: [Crystal `engine/menus/save.asm`](https://github.com/pret/pokecrystal/blob/8e8f7e20052a596371a77022f0392c285e51bbf1/engine/menus/save.asm#L157-L167).
This is a fascinating compatibility shim: the original international cartridge retained GS Ball-related structures but lacked the original Japanese distribution path, so the Virtual Console wrapper activates the event through a targeted runtime hook. The Pokémon Company confirms that the Virtual Console version permits the Celebi encounter at Ilex Forest's shrine.
The pret repositories also contain `vc` build material, and community GitHub discussion has addressed preserving or generating Virtual Console patches. A port targeting 3DS VC behavior should therefore compare not only the retail ROM but the external patch/hook layer.
### Japanese Crystal and mapper/SRAM concerns
Crystal's SRAM source reserves several Mobile sections and explicitly comments that a Mobile Easy Chat initialization routine uses an "MBC30 bank" available to Japanese Crystal but inaccessible with ordinary MBC3 behavior. This is direct evidence that regional hardware assumptions matter when emulating or reproducing Japanese Mobile features.
An implementation that supports only the common international MBC3-style SRAM/RTC configuration may run the international game correctly yet fail Japanese Crystal's extended Mobile storage accesses. Conversely, allocating the larger memory blindly does not implement Mobile Adapter protocols, timer behavior, ranking checksums, or server-era workflows.
### Porting guidance
The safest migration strategy between the two disassemblies is subsystem-oriented:
| Port direction | Recommended approach | Main hazard |
|---|---|---|
| Gold <-> Silver data | Preserve shared engine; switch edition tables/assets | Conditional data references and event assumptions |
| Crystal feature -> Gold/Silver | Import feature plus all WRAM/SRAM, graphics, script, audio, and menu dependencies | DMG/SGB fallback and bank-space pressure |
| Gold/Silver behavior -> Crystal | Replace data or selectively restore old branch | Crystal story/event state may expect new semantics |
| Gold/Silver save -> Crystal | Parse and rebuild symbolic fields | Relocated blocks and Crystal-only state |
| Crystal save -> Gold/Silver | Strip Crystal state and preserve common Pokémon/player data | Caught metadata becomes opaque; Suicune/Battle Tower state has no target |
| Retail battle fix | Gate by non-link mode or require identical modified peers | RNG/control-flow desynchronization |
| Crystal graphics -> DMG-compatible engine | Create monochrome layouts or make build CGB-only | Palette attributes and animation timing |
| Japanese Mobile feature port | Emulate mapper, extended SRAM, timer, and communication assumptions | Hardware and defunct-service dependencies |
Bank space is a concrete constraint. The pokecrystal FAQ describes the international ROM as 2 MiB divided across banks and notes that adding features can overflow fixed bank placement, requiring sections to be moved through the linker script. Crystal's extra features already consume carefully arranged banks, so a port that compiles at the object level can still fail at link time.
The deepest practical rule is: **port invariants before routines**. Before copying code, define the target's expected record lengths, bank-switch convention, WRAM variables, SRAM addresses, script command set, palette mode, RNG call contract, and link-mode semantics. Once those invariants match, most inherited Gen II routines are straightforward. Without them, a perfectly copied routine can read the wrong field, switch to the wrong bank, write outside a save block, or desynchronize a link battle.
### Release and reverse-engineering evidence timeline
```mermaid
timeline
title Gold, Silver, and Crystal technical lineage
1999-11-21 : Gold and Silver released in Japan
2000-10-15 : Gold and Silver released in North America
2000-12-14 : Crystal released in Japan
2001 : International Crystal releases
: Crystal adds Color-only presentation, animations, Battle Tower, and expanded Suicune story
2017-09-22 : Gold and Silver released for Nintendo 3DS Virtual Console
2018-01-26 : Crystal released for Nintendo 3DS Virtual Console
: VC hook enables the GS Ball and Celebi event
2026 : pret repositories reproducibly build exact retail revisions
: ASM-level bug catalogs and symbolic layouts support controlled comparison
```
Gold and Silver launched in Japan on November 21, 1999 and in North America on October 15, 2000; Crystal followed in Japan on December 14, 2000 and internationally in 2001. Gold/Silver reached 3DS Virtual Console in September 2017 and Crystal followed in January 2018.
The current reverse-engineering record is better described as an evolving evidence base than as a single "discovery date." pret's labels, comments, and bug documentation have accumulated over thousands of repository commits, while the exact-build hashes provide a stable anchor to the retail binaries. Crystal's repository currently has substantially more history and feature documentation than `pokegold`, reflecting its role as the community's principal Gen II hacking platform rather than evidence that Crystal's retail source was inherently better documented.
### Final technical assessment
Gold and Silver are functionally sibling configurations of one engine. Their meaningful differences are overwhelmingly table-, asset-, and version-flag-driven. Crystal preserves that engine's defining architecture but turns it into a more specialized platform: Color-only graphics, animated Pokémon, richer event scripting, expanded persistent state, Mobile-era infrastructure, Battle Tower contexts, caught metadata, additional music, and selective defect correction.
At ASM level, the most consequential changes are not usually exotic algorithms. They are small branches with large semantic reach:
- a Battle Tower early return suppresses badge boosts;
- two reserved Pokémon bytes acquire caught metadata without changing record size;
- Suicune disappears from `InitRoamMons`;
- save blocks become reorganized and gain new persistent domains;
- a text terminator closes the Coin Case execution path;
- link-mode checks preserve old arithmetic to avoid desynchronization;
- a Virtual Console hook activates an otherwise inaccessible event;
- a Mobile timer branch attaches a new subsystem to the interrupt path.
That pattern explains both Crystal's compatibility and its porting difficulty. It remains recognizably the Gold/Silver engine, but its new features are woven through banks, state structures, control-flow conditions, graphics timing, SRAM, scripts, and presentation. The code is cousin-shaped, not copy-paste-shaped: the classic reverse-engineering booby trap wearing a tiny Suicune hat.
+316
View File
@@ -0,0 +1,316 @@
# Gen 2 link play: what it takes
This is a design document, not a feature announcement. Gold cannot link today
and nothing in this document changes that on its own. What it does is state
honestly what Gen 2 link play requires, where the Gen 1 protocol in `src/link/`
stops working on a Gen 2 record, what the cart itself did, and which pieces of
the work are self-contained enough to have been built already.
The short version: the transport, the handshake and the fingerprint are
generation-agnostic or nearly so, and those are done. The party wire format is
half done (a codec exists, nothing sends it). The trade session, the trade UI
and the lockstep battle are not started, and each is a real piece of work.
Read `docs/mod-api-gen2-compat.md` beside this: it is the reference for what a
Gold mod may touch, and the link fingerprint's whole job is to hash exactly that
surface.
## 1. What Gen 1 link play is
`src/link/` is nine files and about four thousand lines:
| File | Job |
| --- | --- |
| `Net.lua` | lua-enet transport, LAN host/join plus a relay for online play. JSON messages, no game types. |
| `CodeEntry.lua` | the online join code widget. |
| `Handshake.lua` | the `hello` both peers exchange and the compatibility verdict drawn from the two of them. |
| `Fingerprint.lua` | a deterministic digest of the link surface: the slice of merged Data whose value decides whether two lockstep simulations stay identical. |
| `Protocol.lua` | mon serialization, the record-subset negotiation, and the trade session state machine. |
| `LinkState.lua` | the LINK menu, pairing, and the trade/battle hand-off. |
| `LinkBattle.lua` | lockstep battle: both machines run `src/battle/BattleState.lua` from mirrored perspectives on a shared seed, exchanging one action per turn and a per-turn state hash. |
| `Tournament.lua` | bracketed online play over the relay. |
| `Json.lua` | the encoder the wire and several unrelated callers share. |
Only three of those nine are shaped around Gen 1 game content: `Fingerprint`,
`Protocol` and `LinkBattle`. `Net`, `Json`, `CodeEntry` and most of `Handshake`
never look at a Pokemon.
## 2. What the cart did
pokegold's link code is `engine/link/link.asm` (2508 lines), and it is worth
being precise about, because several of the port's design questions have a cart
answer.
**Three rooms, one wire.** `wLinkMode` is `LINK_TIMECAPSULE`,
`LINK_TRADECENTER` or `LINK_COLOSSEUM` (`constants/serial_constants.asm`).
`LinkCommunications` branches once, at the top, on whether the mode is the Time
Capsule: `Gen2ToGen1LinkComms` for the Time Capsule and `Gen2ToGen2LinkComms`
for everything else (`engine/link/link.asm:33`). Everything after that branch is
shared -- the same byte exchange serves trading and battling, and the mode only
decides what is in the buffer and what the game does afterwards.
**What crosses the wire, in order** (`Gen2ToGen2LinkComms`,
`engine/link/link.asm:202`):
1. `SERIAL_RN_PREAMBLE_LENGTH + SERIAL_RNS_LENGTH` bytes: the shared battle
RNG state. Seven preamble bytes and ten seeds.
2. `SERIAL_PREAMBLE_LENGTH + NAME_LENGTH + (1 + PARTY_LENGTH + 1) + 2 +
(PARTYMON_STRUCT_LENGTH + NAME_LENGTH * 2) * PARTY_LENGTH + 3` bytes: the
player name, the party count and species list, the **trainer ID**, six party
structs, six OT names and six nicknames.
3. `SERIAL_PATCH_LIST_LENGTH` bytes: the patch list (see below).
4. In the Trade Center only, the mail block: six mail messages then six mail
metadata structs (`Link_PrepPartyData_Gen2`, `engine/link/link.asm:810`).
**The shared PRNG is the whole trick behind lockstep.** `_BattleRandom`
(`engine/battle/core.asm:6650`) refuses the normal RNG whenever `wLinkMode` is
non-zero and pulls from `wLinkBattleRNs` instead: ten seeds, each advanced by
`a[n+1] = (a[n] * 5 + 1) % 256` when the stream runs out, with the count in
`wLinkBattleRNCount`. Both machines exchange those ten bytes once, before the
battle, and never again -- the external-clock side adopts the internal-clock
side's numbers (`Link_CopyRandomNumbers`, `engine/link/link.asm:1115`). The port
does the same thing with a Park-Miller stream seeded by the host
(`LinkBattle.makeRng`), which is the same idea in a different arithmetic.
**The patch list exists because the wire has reserved bytes.**
`SERIAL_NO_DATA_BYTE` (`$fe`) may not appear in the payload, so
`FixDataForLinkTransfer` (`engine/link/link.asm:556`) walks the party block,
replaces every `$fe` with `$ff`, and records the offsets it touched in a
200-byte list that ships alongside. Mail gets its own smaller version of this in
`Link_PrepPartyData_Gen2`. The port's wire is JSON over enet and has no reserved
bytes, so this whole mechanism has no analogue and needs none. It is worth
knowing about only so nobody reimplements it by accident.
**The Time Capsule is the cart's own answer to a cross-generation link.**
`CheckTimeCapsuleCompatibility` (`engine/link/link.asm:1970`) refuses the party
outright for exactly three reasons, and returns which one in `wScriptVar`:
1. a species at or above `JOHTO_POKEMON` (`constants/pokemon_constants.asm:168`,
the 152 boundary),
2. a move above `STRUGGLE`, i.e. any move Gen 1 does not have,
3. any party member holding mail.
If the party passes, `Link_PrepPartyData_Gen1` (`engine/link/link.asm:640`)
rewrites every mon into the 44-byte `REDMON_STRUCT_LENGTH` layout:
`ConvertMon_2to1` remaps the species index through `Pokered_MonIndices`
(`engine/link/time_capsule_2.asm:1`), the Special stat is recomputed from
`KantoMonSpecials` because Gen 1 has one Special and Gen 2 has two, and
Magnemite and Magneton are shipped as pure Electric because their typing changed
(`engine/link/link.asm:731`). Coming the other way, `Link_ConvertPartyStruct1to2`
(`engine/link/link.asm:930`) reads the Gen 1 catch rate byte as the held item
slot -- garbage, which is why `TimeCapsule_ReplaceTeruSama`
(`engine/link/link.asm:1078`) maps the handful of catch rates that collide with
real items through `data/items/catch_rate_items.asm` and turns everything else
into a Berry. `ValidateOTTrademon` (`engine/link/time_capsule.asm:1`) then
re-checks the incoming mon's types against the local base data and refuses
anything that does not agree, with Magnemite and Magneton carved out again.
The lesson the port should take from all of that: a cross-generation link is not
a compatibility mode, it is a lossy conversion with a validator on each end, and
the cart wrote roughly 400 lines for it. Refusing the pairing is the honest
default until somebody wants to write those 400 lines.
## 3. Where the Gen 1 protocol breaks on a Gen 2 record
`Protocol.packMon` / `Protocol.unpackMon` are shaped around
`src/pokemon/Pokemon.lua`. Against `src/battle/gen2/Mon.lua`, every one of these
is wrong:
| Gen 1 assumption | Gen 2 reality | Where |
| --- | --- | --- |
| `mon.exp` | `mon.experience` | `Mon.new`, `Mon.gainExperience` |
| five DVs including `special` | four rolled DVs (`attack`/`defense`/`speed`/`special`) with `hp` **derived** from their low bits | `Mon.hpDV` |
| `special` is one stat | `specialAttack` and `specialDefense` are two stats off one DV | `Mon.stats` |
| `statExp` has five words | five words still, but the fifth feeds both special stats | `Mon.stats` |
| no held item | `mon.item` decides damage, healing and flinching | `src/battle/gen2/Battle.lua` `heldEffect` |
| stats recomputed with `src/pokemon/Stats.lua` | must go through `Mon.stats`, which is a different formula shape | `Mon.stats` |
| exp curve via `src/pokemon/Growth.lua` (code) | curve coefficients are **data**, at `data.pokemon.growthRates` | `Mon.experienceForLevel` |
| `mon.status` is `"SLP"` / `"PSN"` / `"BRN"` / `"PAR"` / `"FRZ"` | `"sleep"` / `"poison"` / `"burn"` / `"paralyze"` / `"freeze"` / `"toxic"` | `src/battle/Status.lua:62` vs `src/battle/gen2/Battle.lua:65` |
| `mv.ppUps` | Gen 2 has no PP Ups modelled yet; moves carry `id`, `pp`, `maxPp` | `Mon.movesAtLevel` |
| nothing else on the mon | `happiness`, `pokerus`, `caughtLevel`, `isEgg`/`eggSteps`, and the derived `shiny` / `gender` / `unownLetter` | `Mon.new` |
| mail does not exist | mail is a **separate** block keyed by party slot, not a mon field | `src/core/gen2/Mail.lua:84`, and the cart agrees (`Link_PrepPartyData_Gen2`) |
The status spelling is the sharpest of these. `packMon` puts `mon.status` on the
wire verbatim and `unpackMon` keeps it verbatim, so a shared codec would move
`"PSN"` onto a Gold party where nothing recognises it -- a mon that arrives
"poisoned" and never takes poison damage. Any Gen 2 codec has to be a separate
function with the generation baked into it, not a Gen 1 function with extra
optional keys.
Two things are *not* a problem, and it is worth saying so:
- **Species and move ids are shared name spaces.** Both generations key
`data.pokemon` and `data.moves` by the same `TOTODILE` / `TACKLE` strings, so
the wire never needs the cart's index remap (`ConvertMon_2to1`). Ids, not
bytes, is what makes the port's protocol simpler than the cable's.
- **The derived fields do not travel.** `shiny`, `gender` and `unownLetter` are
all functions of the DVs (`Mon.isShiny`, `Mon.gender`, `Unown.letterFromDVs`),
so the receiver recomputes them and a tampered packet cannot claim a shiny it
did not roll. Same reasoning as `unpackMon` recomputing stats.
`LinkBattle.lua` breaks in a larger way. It calls
`BattleState.makeBattler`, `src/battle/TurnOrder.lua` and Gen 1's damage
pipeline directly. Gold's battle is `src/battle/gen2/Battle.lua`, a different
object with a different event shape. The one piece of good news is that
`Battle.new` already takes `opts.random` (`src/battle/gen2/Battle.lua:220`) --
the injection seam a lockstep battle needs is there and does not have to be cut.
## 4. The wire format for Gen 2
Message types stay the ones `Protocol.lua` already documents (`hello`,
`records`, `party`, `pick`, `confirm`, `action`, `event`, `bye`). Gen 2 adds
fields, never renames -- the same rule the mod seams follow.
**`hello`** gains one field, `generation` (1 or 2). A peer that omits it is
generation 1 by construction: no released build has ever had Gen 2 link, so
absent means Red/Blue/Yellow. This is implemented.
**`party`** carries a list of packed mons whose shape is the Gen 2 one:
```
{ species, level, experience, hp, status, nickname,
dvs = { attack, defense, speed, special }, -- hp derived
statExp = { hp, attack, defense, speed, special },
moves = { { id, pp } ... },
item, happiness, pokerus, caughtLevel,
ot, otId, isEgg, eggSteps, extra }
```
and, in a trade, a parallel `mail` array indexed by the same wire position --
separate exactly as `Link_PrepPartyData_Gen2` keeps it separate, because mail is
SRAM the cart copies out of `sPartyMail`, not part of the party struct. The
codec for the mon half is implemented; the mail half is not.
**`records`** gains a third map, `heldItems`, beside `pokemon` and `moves`, so a
subset trade can refuse a mon whose held item the other game would rebuild
differently. Implemented.
**`action` / `event`** for a lockstep Gen 2 battle are not designed here, because
the design is downstream of a decision nobody has made yet: whether
`src/battle/gen2/Battle.lua` grows a "replay these two actions" entry point or
whether the link battle drives it through the same UI path a local battle uses.
Guessing at a message shape before that is exactly the half-built protocol this
document is meant to avoid.
## 5. The fingerprint on Gold
The fingerprint is the one part of the design that is fully answerable today,
because it depends only on what a Gold mod can reach, and `src/mods/Schemas.lua`
answers that exhaustively.
The Gen 1 surface is species, moves, the type chart, statuses, move effects,
constants and mod-declared link fields. The Gen 2 surface is the same idea over
different tables, plus one registry Gen 1 does not have:
| What | Gen 1 | Gen 2 | Why it is surface |
| --- | --- | --- | --- |
| species | `data.pokemon` | `data.pokemon` (Gen 2 shape) | base stats decide every damage roll; `evolutions` decides what a traded mon becomes |
| exp curves | code (`src/pokemon/Growth.lua`) | `data.pokemon.growthRates` | the level a traded mon's experience buys |
| moves | `data.moves` | `data.moves`, **plus `effectChance`** | Gen 2 stores the secondary-effect odds per move rather than per effect |
| type chart | `data.type_chart` | same, **plus `foresightMatchups`** | Foresight rewrites Ghost's immunities mid-battle |
| statuses | `data.statuses` | `data.gen2Statuses` | `statPenalty`, `cureOnSwitch`, `beforeMovePriority` |
| move effects | `data.move_effects` | `data.gen2MoveEffects` | which effect is primary, secondary, or accuracy-checked |
| held items | -- | `data.gen2HeldItems` | Leftovers, King's Rock, the type boosters: pure battle math, and the item travels with a traded mon |
| link fields | `data.link_fields` | none (gated) | a mod-declared extra mon field; see below |
Deliberately **not** hashed on either generation, and the reasoning is the same
in both:
- Names, dex entries, learnsets, TM/HM lists, sprite paths and `source`. None of
them changes a battle turn or a trade rebuild, and sprite paths differ between
two identical installs.
- `catchRate` (#511). A ball thrown in a link battle is refused; hashing it split
Red/Blue from Yellow over two bytes.
- Balls and item effects. Link battles allow no bag items on either cart.
- `data.gen2Constants`. It is the ROM's ordered name lists -- `mapOrder`,
`spriteOrder`, `speciesOrder`, `heldEffectOrder` -- and it is an *index* space.
Every dispatch in the Gen 2 simulation goes by name (`Battle.heldEffect`
compares `record.heldEffect` strings, `moveEffectRecordFor` keys by
`EFFECT_*`), so reordering a constants list moves no battle math. Hashing it
would split two peers over a list neither of them dispatches on.
- Breeding data (`eggGroups`, `eggMoves`, `eggSteps`). The Day-Care is local.
There is no link breeding and an egg's contents are decided before it is
traded.
- Everything in the Gen 2-only registries that is not `held_items`:
`phone_contacts`, `decorations`, `apricorns`, `landmarks`, `radio_channels`.
None of them can be observed from inside a link session.
`genderRatio` **is** hashed, unlike anything in the breeding block, because Gen 2
has Attract and a gender disagreement is a battle-math disagreement.
`link_fields` stays gated on Gen 2 (`Schemas.GEN2`). It is a mod's declaration
that an extra mon field must survive the wire, and it can only be un-gated once
there is a Gen 2 mon wire format with a session behind it. A registry that hashes
into the fingerprint but that no packer reads would be a lie the fingerprint
tells.
The hook keeps its Gen 1 name and its Gen 1 arity. `link.fingerprint` is called
as `(data, mods)` on both generations, and the generation is captured in the
vanilla closure rather than passed as a third argument -- a Gen 1 mod that wraps
the hook and forwards `nxt(data, mods)` therefore works verbatim on Gold, which
a third argument would have quietly broken.
## 6. Cross-generation pairing
`Handshake.checkCompat` refuses a pairing whose two `generation` fields differ,
with reason `generation_mismatch`, before any other check. This is deliberately
a refusal and not a Time Capsule: section 2 costs roughly 400 lines of lossy
conversion plus two validators, and shipping a menu that pairs a Gold game with a
Red one and then rebuilds a Cyndaquil as whatever species index 155 happens to
be in Red would be worse than refusing.
If somebody does want the Time Capsule later, the cart's own rules are the spec,
and the port has an advantage the cart did not: ids rather than indices, so
`ConvertMon_2to1`'s remap table is unnecessary. What remains is real work:
refuse species above the Kanto 151, refuse moves Gen 1 lacks, refuse held items
and mail, fold `specialAttack`/`specialDefense` back into one Special
(`KantoMonSpecials`, `data/pokemon/gen1_base_special.asm`), and re-validate types
on arrival with Magnemite and Magneton carved out.
## 7. What is built, and what is left
**Built (this change).**
- `Fingerprint.generationOf(data)`: reads `data.type_chart.generation`, then the
presence of the Gen 2-only Data namespaces, so the digest never has to be told
which game it is running in.
- A Gen 2 link surface in `Fingerprint.lua`, tagged `[gen2]` so a Gen 2 digest
can never collide with a Gen 1 one, covering the table in section 5.
- `Fingerprint.records(data, kind, generation)` for `pokemon`, `moves` and
`held_items` on Gold, which is what a subset trade negotiates on.
- `Handshake.hello` carries `generation`; `checkCompat` refuses a cross-generation
pairing; `describe` explains it in the screen's own voice.
- `held_items` counts as link surface for `Handshake.linkModified`, so a Gold mod
that gives Leftovers a different heal is correctly locked out of online play.
`growth_rates` counts too, and for a reason worth stating: a curve is an
`expForLevel` function, so the fingerprint serializes it as `?` and cannot
hash it at all. `Mon.growthFor` prefers the merged registry over the
extractor's coefficient rows, so a mod declaring `affects_link = false` could
otherwise rewrite every curve -- changing what level a traded mon's experience
buys -- and be caught by neither the digest nor the online gate.
- `Protocol.packMon2` / `unpackMon2`: the Gen 2 party-struct codec, with the
receiver recomputing stats, experience, shininess and gender from real species
data the way `unpackMon` does. Nothing sends it yet.
- `Protocol.recordsMessage` and `eligibleParty` understand held items, and
`TradeSession:_negotiate` builds our own side of the comparison by calling
`recordsMessage` rather than open-coding a subset of it. That is not a
refactor: the open-coded version left `heldItems` off our side only, and both
held-item arms of `eligibleParty` are guarded on our own map, so the whole
check was unreachable from the only caller that matters.
**Not built.** Sized as honestly as I can:
| Piece | Size | Why it is not here |
| --- | --- | --- |
| Mail on the wire | small (1-2 days) | needs `Mail.state(save).party` packed as a parallel array and the receiver writing it into its own slot; no consumer until the trade session exists |
| Gen 2 trade session | medium (about a week) | `TradeSession` is generic in shape but `apply` calls `src/world/PikachuFollower`, Gen 1 pokedex fields and Gen 1 trade evolutions; Gold needs `src/core/gen2/TradeAnim.lua`, `src/core/gen2/Evolution.lua` (`EVOLVE_TRADE`) and the Gen 2 pokedex |
| Gold LINK menu and trade UI | medium (about a week) | Gold's menus are `src/ui/gen2/`, its text box is Gen 2 chrome, and the Cable Club script needs the receptionist path; `LinkState.lua` draws with Gen 1 `TextBox` throughout |
| Gen 2 lockstep battle | large (several weeks) | needs a deterministic-replay entry point into `src/battle/gen2/Battle.lua`, a Gen 2 action/event encoding, and a state hash over a much larger volatile-status set than Gen 1's; the `opts.random` seam exists, nothing else does |
| Time Capsule | large | section 6 |
| `link_fields` on Gen 2 | small, but blocked | needs the trade session first |
The order matters: mail, session and UI are the trade half and can ship without
the battle half, exactly as the cart's Trade Center and Colosseum are separate
rooms. A Gold LINK menu that offers only TRADE is a complete feature. A Gold
LINK menu that offers BATTLE before the lockstep battle exists is the failure
this document was written to prevent.
+188
View File
@@ -0,0 +1,188 @@
# Pokemon Gold: Gen 2 import, colour, menus, saving and battles
Support for a canonical **Pokemon Gold** ROM
(SHA-1 `d8b8a3600a465308c9953dfa04f0081c05bdcb94`, 2 MiB). Gen 2 extraction
and overworld are separate from Red/Blue/Yellow -- never branched into
`RomExtractor.lua` / Gen 1 `Map.lua`.
## Pipeline
| Piece | Role |
| --- | --- |
| `src/import/Rom.lua` (`decompressLz3`) | Gen 2 graphics compression (`home/decompress.asm`) |
| `tools/make_gold_manifest.py``tools/rom_manifest_gold.json` | pret/pokegold constants + `pokegold.sym` |
| `src/import/RomExtractorGen2.lua` | Gen 2 extract (dispatched from `RomImporter` when `version == "gold"`) |
| `src/core/GameVersion.lua` | `gold` entry (`gold/` cache, `_gold` saves) |
| `src/world/gen2/` | COLL_* world, NPCs, events, roofs |
| `src/script/gen2/` | Opcodes + talk-oriented VM over extracted cmds |
| `src/core/Game2.lua` | Gold's service owner (the Gen 2 peer of `src/core/Game.lua`): boot cinema, intro menu, world, START menu |
| `src/core/gen2/Save.lua` | `save_gold.lua`, beside the Gen 1 saves |
| `src/ui/gen2/` | Intro menu, naming keyboard, START menu + every submenu, battle screen |
| `src/battle/gen2/` | Damage, stats/experience, HP bar, turn engine, catching, encounters |
| `src/render/GbcPalette.lua` | One shader: a 4-shade sheet through a GBC palette |
## What extracts
| Area | Status |
| --- | --- |
| Constants | species/map/tileset/move/type/sprite/environment/palette/fish orders |
| Font | `Font` (1bpp ink-on-transparent), `FontExtra` + `Frames` borders at $79$7E, `FontBattleExtra` |
| Tilesets | lz3 GFX → PNG; raw Meta (128×16) + Coll (128×4 COLL_* quads); Anim/PalMap pointers |
| Roofs | 5 roof sheets + `MapGroupRoofs` (runtime: outdoor tilesets only) |
| Maps | all 368: blocks, connections, warps, coord/bg/object events (+ `scriptKey`) |
| Pokemon | `BaseData` (32B), names, front/back pics, plus Unown's 26 letter forms from `UnownPicPointers` |
| Items | Names, prices, pockets, held effects, field/battle use, descriptions, TM/HM numbers and the move each teaches |
| Marts | `Marts`' 34 shelves in MART_* order plus `BargainShopData`'s own item/price rows (`marts.lua`) |
| Overworld sprites | `OverworldSprites` → PNG sheets + `sprites.lua` |
| Scripts / text | disassembled cmds + decoded strings (`scripts.lua` / `text.lua`) |
| Initial events | `InitializeEventsScript``initial_events.lua` |
| Title | Tilemap + GBC tint; cloud band; Ho-Oh frames 15; trail; copyright splash |
| Oak speech | `_OakText17`, `PokemonProfPic` / `CalPic`, Marill front; `oak_speech.lua` |
| Pokemon / trainer pics | lz3 + column-major → row (`ImageWriter.columnsToRows`) |
| Audio | banks dumped to `programs.bin`; Gen 2 ChipSynth driver (`runtime = true`) |
| Palettes | `TilesetBGPalette`, `EnvironmentColorsPointers`, `MapObjectPals`, `RoofPals`, mon/trainer/HP-bar/exp-bar palettes, per-tileset `PalMap` |
| Moves | `Moves` + names + descriptions, effect names, real percentages |
| Type chart | `TypeMatchups` (plus the Foresight rows) and `TypeNames` with physical/special |
| Pokemon | Base stats, evolutions and level-up moves (`EvosAttacks`), TM/HM lists, growth rates, egg groups, gender ratio |
| Encounters | Grass (three time-of-day slot lists), water, fishing groups, headbutt tree sets |
| Trainers | Every class's parties with moves/items, class names, attributes, `TrainerEncounterMusic` |
| Pokedex | Entries (kind, height, weight, text) + the New and A-Z orderings |
| Landmarks | Town-map positions and names, plus `SpawnPoints` |
| Icons | Party-menu mon icons and the species → icon map |
| Menu / HUD gfx | Naming-screen chrome, battle HUD border and bar tiles, exp bar |
| Intro movie | The three acts' composed backgrounds, sprite sheets and fire frames |
| Battle anims | all 428 scripts, 188 objects, 185 framesets, 216 OAM sets, 40 object sheets, the six OBJ palettes |
| Field anims | stubs |
Verified against a real Gold ROM: New Bark Town warps/objects/connections
match `pokegold/maps/NewBarkTown.asm`. Retail EVENT_* numeric ids differ
from prets current `const_def` order -- always take flags from the cart.
## Play
Launcher → Gold tab → Play opens `Game2` (`src/core/Game2.lua`):
- Copyright → GameFreak Presents → GS intro stub → title (colored tilemap,
scrolling clouds, Ho-Oh flap, trails) → Oak speech (Marill cry + shrink)
→ name pick → `src/world/gen2/World`
- Drivers (`POKEPORT_DRIVER`) skip cinema straight to the map
- New Bark Town with Johto tileset + roof overlay (outdoors only)
- Walk on `COLL_*` permissions; doors / stairs / carpets
- Elm's lab: scene walk-in, starter balls (`givepoke` + cutscene moves),
Elm phone number, aide Potion on exit (`verbosegiveitem`)
- Connected neighbor strips + seamless edge crossings (Route 29 / 27)
- Survey zoom (`-` / `=` / wheel / `4`)
- Chris + map/neighbor NPCs; `SPRITEMOVEDATA_*` walk / spin / stand
- New-game flags hide story NPCs (e.g. lab cop)
- A: talk to facing NPC or read `BGEVENT_READ` signs via Gen 2 VM + TextBox
Escape is START, not quit: quitting is the START menu's QUIT row and the
intro menu's EXIT GAME. Title / map music and script SFX/cries use the Gen 2
channel driver.
## Colour
Gen 2 is CGB-native, so colour is part of a tile's identity rather than a tint
over a 4-shade image. `engine/gfx/color.asm` LoadMapPals is ported whole in
`src/world/gen2/Palettes.lua`:
1. the clock hour picks a daytime (`engine/rtc/rtc.asm` TimesOfDay), which a
map's own `PALETTE_*` can override (`ReplaceTimeOfDayPals`)
2. `EnvironmentColorsPointers[environment][daytime]` names eight entries in the
shared `TilesetBGPalette` pool
3. outdoors only, `RoofPals[mapGroup]` overwrites `PAL_BG_ROOF` colours 1-2 --
one roof tile sheet, a different colour per town
4. each tile's slot comes from its tileset's `PalMap`, and each OW sprite's OBJ
palette from `MapObjectPals[daytime]` plus its own `PAL_OW_*`
The map bake runs one shader pass per BG slot (eight per map) rather than one
per tile, and re-bakes when the clock rolls into a new daytime.
`POKEPORT_GOLD_HOUR=21` pins the hour for screenshots.
## Still to do
About fifty of AI_Smart's seventy per-effect handlers are unwritten, fishing
and headbutt have engine support but no input path, and Mart, the summary
screen, `.sav` interchange, Silver and mod-replaceable Gen 2 screens are all
still open.
## Verify
```sh
python3 tools/make_gold_manifest.py
for t in rom_lz3 gen2_world gen2_audio gen2_oak_speech gen2_vm \
gen2_palettes gen2_battle gen2_menus gen2_save; do
luajit tests/${t}_test.lua
done
POKEPORT_IMPORT_TRACE=1 POKEPORT_IMPORT_ROM=/path/to/pokegold.gbc \
POKEPORT_IMPORT_ONLY=1 POKEPORT_FORCE_IMPORT=1 POKEPORT_GAME=gold love .
POKEPORT_GAME=gold love .
# copyright → GF presents → GS intro → title → CONTINUE/NEW GAME/OPTION
# → Oak → name → the bedroom. START opens the menu; grass starts a battle.
# Screenshot drivers (need a display):
POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/gold_menu_shots.lua love .
POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/gold_palette_shots.lua love .
POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/gold_battle_smoke.lua love .
POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/gold_transition_shots.lua love .
POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/gold_teacher_scene.lua love .
POKEPORT_GAME=gold POKEPORT_BOOT_CINEMA=1 \
POKEPORT_DRIVER=tests/drivers/gold_boot_smoke.lua love .
```
`POKEPORT_IMPORT_TRACE=1` prints each of the 26 stages as it starts, and a
headless import that fails now says so and exits non-zero rather than sitting
in an error state that looks exactly like a hang.
A Gold cache without `palettes.lua`, `moves.lua`, `encounters.lua`,
`trainers.lua`, `pokedex.lua`, `landmarks.lua`, `icons.lua`, `menu_gfx.lua`,
`intro.lua`, `std_scripts.lua` or `battle/hud/*.png` needs a re-import -- as
does one whose back pics are still the front pic's size rather than 48x48 (see
below), and one whose trainer classes have no `encounterMusic` (the sixth
pass added `TrainerEncounterMusic` to the manifest, so the manifest has to be
regenerated as well). So does one whose `text.lua` has no `labels` table: that
is the by-name seed for the text no script pointer reaches (the Day-Care and
breeding block, the mart conversation, the Hall of Fame headers), and it needs
a regenerated manifest too, because the seed reads those symbols by name.
## Traps this port has already fallen into
Worth knowing before touching the extractor:
- **Back pics are always 6x6 (48x48).** `BASE_PIC_SIZE`'s low nibble describes
the *front* pic only, so decoding a back at it reads the wrong tile count in
the wrong number of columns and produces garbage.
- **`#`, `<POKE>` and `<PKMN>` have no font tile.** They are compression bytes
($54/$24/$4a) that PlaceString expands; the manifest charmap has to expand
them or "#DEX" renders as "DEX".
- **FishGroups has no row for FISHGROUP_NONE**, so a group's row is its id minus
one. Walking the constant list from the top reads every row shifted.
- **Trainer groups have no end marker** -- the next group's label follows
immediately -- so a party scan needs the class's own member list to bound it.
- **Pokedex entries are spread over four banks** and the cart derives the bank
arithmetically; take each species' own symbol instead.
- **`EvosAttacksPointers` is `dw`, not `dba`**: the blobs share the table's bank.
- Item ids past NUM_ITEMS (the TMs and HMs) have no ItemNames row; their name is
their TM number.
- **`TrainerClassAttributes` rows are SEVEN bytes**, not eight:
`NUM_TRAINER_ATTRIBUTES` is `_RS` after three `rb` and two `rw`. An
eight-byte stride walks one byte further off with every class, so the AI
flags come out as noise for everything past the first trainer -- and it
produces plausible values rather than an error, which is how it survived two
passes. The row is {item1, item2, baseMoney, aiLo, aiHi, switchLo,
switchHi}: the base money is byte THREE.
- **`dba_pic` does not store the real bank.** For the three "Pics" sections
that sit above the 8-bit-friendly range it writes `$13`, `$14` or `$1f`, and
`FixPicBank` (engine/gfx/load_pics.asm) maps those back to `$1f`, `$20` and
`$2e`. Any table of pic pointers -- `PokemonPicPointers`,
`UnownPicPointers`, `TrainerPicPointers` -- needs that mapping, or the read
lands in a completely different bank.
- **A pic can run over the top of its bank.** `GetLZByte` bumps the bank and
drops back to `$4000` when the read pointer passes `$8000`, so a compressed
pic near the top of a bank keeps going into the next one; slicing only to
`$8000` stops short of nine of the Unown letters.
- **`BattleAnimObjects` rows are SIX bytes**, because `BATTLEANIMOBJ_LENGTH` is
`_RS - 1` -- the struct's runtime INDEX byte is not in the table.
- **Unown has no `PokemonPicPointers` row of its own**; its twenty-six forms
come out of `UnownPicPointers` instead, and letter A stands in for the
species.
File diff suppressed because it is too large Load Diff
+126
View File
@@ -0,0 +1,126 @@
# ASM Walk output format
Every `section-NN-*.md` in this folder follows the structure below. The goal is a
single place a bot author can look at to answer "for this stretch of the
walkthrough, which maps, warps, objects, scripts, flags, items and trainers are
involved, and where in the pokegold disassembly do they live?"
Conventions:
- Disassembly paths are written relative to the pokegold checkout root, e.g.
`maps/NewBarkTown.asm`, `data/maps/maps.asm`, `constants/event_flags.asm`.
- Port paths are written relative to this repo root, e.g. `src/world/gen2/Map.lua`.
- Coordinates are the raw asm values (map-block coordinates as written in the
`warp_event` / `object_event` / `bg_event` rows), not screen pixels.
- Anything not verified by opening the file is omitted rather than guessed. If a
walkthrough claim could not be located in the asm, it goes under
"Unresolved / verify by hand".
---
# Section NN - <title>
Source: `../section-NN-<slug>.txt`
Maps covered: `MAP_A`, `MAP_B`, ...
Badges / key milestones in this section: ...
## 1. Route order
Ordered list of map transitions the walkthrough takes, one row per hop.
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
## 2. Maps
Repeat this block per map.
### MAP_<NAME>
- Script: `maps/<Name>.asm`
- Blocks: `maps/<Name>.blk` (if present)
- Header: `data/maps/maps.asm` -> tileset, environment, location, music, phone, palette
- Dimensions / attributes: `constants/map_constants.asm` (`map_const <NAME>, W, H`)
- Connections: north/south/east/west map constants
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
**Coord events** (`def_coord_events`) - the scripted trip-wires a bot will hit
| scene | x | y | script label | effect |
|---|---|---|---|---|
**BG events** (`def_bg_events`) - signs, hidden items
| x | y | type | script/item |
|---|---|---|---|
**Object events** (`def_object_events`) - NPCs, item balls, trainers
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
**Scripts of interest**
For each script label the walkthrough actually depends on: label, a short prose
summary of what the opcodes do, and the flags/items/battles it touches. Cite the
label name so it can be grepped.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
Include `EVENT_*` (`constants/event_flags.asm`), `ENGINE_*`
(`constants/engine_flags.asm`), and any `SCENE_*` values
(`constants/map_setup_constants.asm` / the map's own `object_const_def` block).
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
**Wild encounters**
Table entry in `data/wild/johto_grass.asm` / `johto_water.asm` /
`kanto_grass.asm` / `kanto_water.asm`, plus fishing (`data/wild/fish.asm`),
headbutt (`data/wild/treemons.asm`) and rock smash where relevant. Note the
morn/day/nite split.
## 3. Blockers and gates
What actually stops forward progress in this section, and the precise check.
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
Cover badge checks, HM field-move checks (`engine/overworld/`), key items,
`EVENT_*` guards, and NPC-blocked tiles.
## 4. Bot checklist
The machine-actionable version: an ordered list of steps a scripted playthrough
would execute. Each step names the map, the target coordinates or object const,
the input intent (talk / walk / use item / battle), the precondition flag and the
postcondition flag. Keep it literal enough that someone can turn a row into a
driver command without reopening the asm.
## 5. Port coverage
Where this section's mechanics land in this repo, and what is missing.
| Beat | Port file | Status |
|---|---|---|
## 6. Unresolved / verify by hand
Anything the walkthrough asserts that could not be pinned to a specific asm
location, plus any contradictions found between the two.
@@ -0,0 +1,535 @@
# Section 00 - Chikorita, Cyndaquil, or Totodile?
Source: `../section-00-chikorita-cyndaquil-or-totodile.txt`
Maps covered: `MAP_PLAYERS_HOUSE_2F`, `MAP_PLAYERS_HOUSE_1F`, `MAP_NEW_BARK_TOWN`, `MAP_ELMS_LAB`
Badges / key milestones in this section: no badge. Milestones are the new-game
clock set, the #GEAR from Mom (`ENGINE_POKEGEAR`), the first rival encounter,
and the starter (`EVENT_GOT_A_POKEMON_FROM_ELM`), which is what un-gates the
walk west out of New Bark Town.
Coordinate note: every `warp_event` / `coord_event` / `bg_event` /
`object_event` x,y below is copied verbatim from the map asm. Those are 0-based
walk-grid cells (2 per map block, so a `W`-block-wide map runs x = 0..2W-1),
which is the same grid `src/world/gen2/Map.lua` calls a cell.
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 0 | (no map) | `engine/menus/intro_menu.asm` `NewGame` / `OakSpeech`, `engine/rtc/timeset.asm` `InitClock` | title screen -> NEW GAME | `InitializeWorld` -> `SPAWN_HOME` | "Choose new game and start… Professor Oak will ask you the time… he asks you what your name is" |
| 1 | `MAP_PLAYERS_HOUSE_2F` | `maps/PlayersHouse2F.asm` | spawn `SPAWN_HOME` at (3,3), `data/maps/spawn_points.asm` | warp 1 at (7,0) | "You are in your room at the beginning. Exit the room" |
| 2 | `MAP_PLAYERS_HOUSE_1F` | `maps/PlayersHouse1F.asm` | warp 3 at (9,0) | warp 1 (6,7) or warp 2 (7,7) | "you will meet your mom… You get the Pokegear… input the day of the week" |
| 3 | `MAP_NEW_BARK_TOWN` | `maps/NewBarkTown.asm` | warp 2 at (13,5) | warp 1 at (6,3) | "Leave your house and head left to Professor Elm's lab. Notice the guy standing left of it" (rival at (3,2)) |
| 4 | `MAP_ELMS_LAB` | `maps/ElmsLab.asm` | warp 1 (4,11) / warp 2 (5,11) | warp 1/2 back to New Bark Town | "go into Elm's lab… You get to pick your starter Pokémon" |
| 5 | `MAP_NEW_BARK_TOWN` | `maps/NewBarkTown.asm` | warp 1 at (6,3) | west connection to `ROUTE_29` (`data/maps/attributes.asm`) | section ends here; the walk west to Route 29 / Cherrygrove belongs to section 01 |
## 2. Maps
### MAP_PLAYERS_HOUSE_2F
- Script: `maps/PlayersHouse2F.asm`
- Blocks: `maps/PlayersHouse2F.blk`
- Header (`data/maps/maps.asm:478`): `map PlayersHouse2F, TILESET_PLAYERS_ROOM, INDOOR, LANDMARK_NEW_BARK_TOWN, MUSIC_NEW_BARK_TOWN, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions (`constants/map_constants.asm:443`): `map_const PLAYERS_HOUSE_2F, 4, 3` (4x3 blocks = 8x6 cells)
- Connections: none (indoor)
- Scene var: `wPlayersHouse2FSceneID` is *not* in `data/maps/scenes.asm`; the map has an empty `def_scene_scripts`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 7 | 0 | `PLAYERS_HOUSE_1F` | 3 |
**Coord events** (`def_coord_events`)
None (the block is empty).
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 2 | 1 | `BGEVENT_UP` | `PlayersHousePCScript` |
| 3 | 1 | `BGEVENT_READ` | `PlayersHouseRadioScript` |
| 5 | 1 | `BGEVENT_READ` | `PlayersHouseBookshelfScript` |
| 6 | 0 | `BGEVENT_IFSET` | `PlayersHousePosterScript` (guarded by `EVENT_PLAYERS_ROOM_POSTER`) |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `PLAYERSHOUSE2F_CONSOLE` | `SPRITE_CONSOLE` | 4 | 2 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_SCRIPT` | `PlayersHouseGameConsoleScript` | `EVENT_PLAYERS_HOUSE_2F_CONSOLE` |
| `PLAYERSHOUSE2F_DOLL_1` | `SPRITE_DOLL_1` | 4 | 4 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_SCRIPT` | `PlayersHouseDoll1Script` | `EVENT_PLAYERS_HOUSE_2F_DOLL_1` |
| `PLAYERSHOUSE2F_DOLL_2` | `SPRITE_DOLL_2` | 5 | 4 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_SCRIPT` | `PlayersHouseDoll2Script` | `EVENT_PLAYERS_HOUSE_2F_DOLL_2` |
| `PLAYERSHOUSE2F_BIG_DOLL` | `SPRITE_BIG_DOLL` | 0 | 1 | `SPRITEMOVEDATA_BIGDOLL` | `OBJECTTYPE_SCRIPT` | `PlayersHouseBigDollScript` | `EVENT_PLAYERS_HOUSE_2F_BIG_DOLL` |
**Scripts of interest**
- `PlayersHouse2FInitializeRoomCallback` (`callback MAPCALLBACK_NEWMAP`): runs
`special ToggleDecorationsVisibility`, sets
`EVENT_TEMPORARY_UNTIL_MAP_RELOAD_8` (read by `engine/phone/scripts/mom.asm`),
then `checkevent EVENT_INITIALIZED_EVENTS` -> if clear,
`jumpstd InitializeEventsScript`. This is the one-time world seeding for a new
game: `engine/events/std_scripts.asm:438` sets ~70 `EVENT_*` bits including
`EVENT_COP_IN_ELMS_LAB` (which *hides* the officer), `EVENT_RIVAL_CHERRYGROVE_CITY`,
`EVENT_ROUTE_30_YOUNGSTER_JOEY`, and finally `EVENT_INITIALIZED_EVENTS`
(`engine/events/std_scripts.asm:557`) so it never runs again.
- `PlayersHouse2FSetUpTileDecorationsCallback` (`callback MAPCALLBACK_TILES`):
`special ToggleMaptileDecorations`.
- `PlayersHouseRadioScript`: three-branch. Before `EVENT_GOT_A_POKEMON_FROM_ELM`
it plays `MUSIC_POKEMON_TALK`, prints four texts and sets
`EVENT_LISTENED_TO_INITIAL_RADIO`; after that flag it prints only
`PlayersRadioText4`; after `EVENT_GOT_A_POKEMON_FROM_ELM` it becomes the normal
`jumpstd Radio1Script`. Purely flavour, no gate.
- `PlayersHousePCScript`: `special PlayersHousePC`, `iftrue .Warp` -> `warp NONE, 0, 0`
(the "leave the PC by warping in place" reload).
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_INITIALIZED_EVENTS` | `constants/event_flags.asm:63` | read by `PlayersHouse2FInitializeRoomCallback`, set by `InitializeEventsScript` | first-load-only world seed; must be clear on a fresh save |
| `EVENT_TEMPORARY_UNTIL_MAP_RELOAD_8` | `constants/event_flags.asm` | set here, read by `engine/phone/scripts/mom.asm:13` | "player is at home right now" |
| `EVENT_LISTENED_TO_INITIAL_RADIO` | `constants/event_flags.asm:468` | `PlayersHouseRadioScript` | shortens the radio text on repeat |
| `EVENT_PLAYERS_ROOM_POSTER` | `constants/event_flags.asm:378` | `conditional_event` in `PlayersHousePosterScript` | decoration present |
**Items**: none on this map.
**Trainers**: none.
**Wild encounters**: none (indoor).
### MAP_PLAYERS_HOUSE_1F
- Script: `maps/PlayersHouse1F.asm`
- Blocks: `maps/PlayersHouse1F.blk`
- Header (`data/maps/maps.asm:477`): `map PlayersHouse1F, TILESET_PLAYERS_HOUSE, INDOOR, LANDMARK_NEW_BARK_TOWN, MUSIC_NEW_BARK_TOWN, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions (`constants/map_constants.asm:442`): `map_const PLAYERS_HOUSE_1F, 5, 4` (10x8 cells)
- Connections: none (indoor)
- Scene var: `wPlayersHouse1FSceneID` (`data/maps/scenes.asm:30`). Scene ids come
from declaration order in `def_scene_scripts` (`macros/scripts/maps.asm:25`):
`SCENE_PLAYERSHOUSE1F_MEET_MOM` = 0, `SCENE_PLAYERSHOUSE1F_NOOP` = 1. A new game
zeroes the scene, so scene 0 is live on the first visit.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 6 | 7 | `NEW_BARK_TOWN` | 2 |
| 2 | 7 | 7 | `NEW_BARK_TOWN` | 2 |
| 3 | 9 | 0 | `PLAYERS_HOUSE_2F` | 1 |
**Coord events** (`def_coord_events`)
None. The Mom cutscene is a **scene script**, not a coord event: `scene_script
PlayersHouse1FMeetMomScene, SCENE_PLAYERSHOUSE1F_MEET_MOM` -> `sdefer MeetMomScript`,
so it fires on map entry from the stairs while the scene id is still 0.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 0 | 1 | `BGEVENT_READ` | `PlayersHouse1FStoveScript` |
| 1 | 1 | `BGEVENT_READ` | `PlayersHouse1FSinkScript` |
| 2 | 1 | `BGEVENT_READ` | `PlayersHouse1FFridgeScript` |
| 4 | 1 | `BGEVENT_READ` | `PlayersHouse1FTVScript` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `PLAYERSHOUSE1F_MOM1` | `SPRITE_MOM` | 7 | 3 | `SPRITEMOVEDATA_STANDING_LEFT` (time `-1`) | `OBJECTTYPE_SCRIPT` | `MomScript` | `EVENT_PLAYERS_HOUSE_MOM_1` |
| `PLAYERSHOUSE1F_MOM2` | `SPRITE_MOM` | 2 | 2 | `SPRITEMOVEDATA_STANDING_UP` (time `MORN`) | `OBJECTTYPE_SCRIPT` | `MomScript` | `EVENT_PLAYERS_HOUSE_MOM_2` |
| `PLAYERSHOUSE1F_MOM3` | `SPRITE_MOM` | 7 | 3 | `SPRITEMOVEDATA_STANDING_LEFT` (time `DAY`) | `OBJECTTYPE_SCRIPT` | `MomScript` | `EVENT_PLAYERS_HOUSE_MOM_2` |
| `PLAYERSHOUSE1F_MOM4` | `SPRITE_MOM` | 0 | 2 | `SPRITEMOVEDATA_STANDING_UP` (time `NITE`) | `OBJECTTYPE_SCRIPT` | `MomScript` | `EVENT_PLAYERS_HOUSE_MOM_2` |
Note the object flags are inverted relative to intuition: a *set* event flag hides
the object. Mom1 is the cutscene Mom (visible while `EVENT_PLAYERS_HOUSE_MOM_1`
is clear); `MeetMomScript` ends by setting `EVENT_PLAYERS_HOUSE_MOM_1` and
clearing `EVENT_PLAYERS_HOUSE_MOM_2`, which swaps in the three time-of-day Moms
on the next map load.
**Scripts of interest**
- `MeetMomScript` (the whole Pokegear beat, `maps/PlayersHouse1F.asm:21`):
1. `applymovement PLAYER, PlayersHouseDownstairsMovement` (one `step DOWN`, i.e. off the stair tile).
2. `playmusic MUSIC_MOM`, `turnobject PLAYERSHOUSE1F_MOM1, UP`, `showemote EMOTE_SHOCK, PLAYERSHOUSE1F_MOM1, 15`.
3. `applymovement PLAYERSHOUSE1F_MOM1, MomWalksToPlayerMovement` (`slow_step RIGHT, RIGHT, UP`: (7,3) -> (9,2), beside the player at (9,1)).
4. `writetext ElmsLookingForYouText`, `getstring STRING_BUFFER_4, PokegearName` ("#GEAR"), `scall PlayersHouse1FReceiveItemStd` -> `jumpstd ReceiveItemScript` (`engine/events/std_scripts.asm:656`).
5. `setflag ENGINE_POKEGEAR`, `setflag ENGINE_PHONE_CARD`, `addcellnum PHONE_MOM`.
6. `setscene SCENE_PLAYERSHOUSE1F_NOOP`, `setevent EVENT_PLAYERS_HOUSE_MOM_1`, `clearevent EVENT_PLAYERS_HOUSE_MOM_2`.
7. `special SetDayOfWeek`, then the DST loop: `IsItDSTText` -> `yesorno` -> `special InitialSetDSTFlag` / `special InitialClearDSTFlag`, each confirmed by a second `yesorno` that loops back to `.SetDayOfWeek` on "no".
8. Phone-instructions branch (`.KnowPhone` / `.ExplainPhone`, both fall into `.FinishPhone`), then `applymovement PLAYERSHOUSE1F_MOM1, MomWalksBackMovement` and `special RestartMapMusic`.
Crucially the #GEAR is **not** an inventory item: it is two engine flags. There
is no `giveitem` in this script.
- `MomScript` (talking to Mom afterwards): checks, in order,
`EVENT_FIRST_TIME_BANKING_WITH_MOM` -> `EVENT_TALKED_TO_MOM_AFTER_MYSTERY_EGG_QUEST`
-> `EVENT_GAVE_MYSTERY_EGG_TO_ELM` -> `EVENT_GOT_A_POKEMON_FROM_ELM`; in this
section all four are clear so it prints `HurryUpElmIsWaitingText`. The
`special BankOfMom` path is not reachable yet.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_POKEGEAR` | `constants/engine_flags.asm:8` | set by `MeetMomScript` | Pokegear menu becomes usable |
| `ENGINE_PHONE_CARD` | `constants/engine_flags.asm:6` | set by `MeetMomScript` | Pokegear PHONE card unlocked |
| `EVENT_PLAYERS_HOUSE_MOM_1` | `constants/event_flags.asm:1129` | set by `MeetMomScript` | hides the cutscene Mom |
| `EVENT_PLAYERS_HOUSE_MOM_2` | `constants/event_flags.asm:1130` | cleared by `MeetMomScript` | reveals the time-of-day Moms |
| `SCENE_PLAYERSHOUSE1F_MEET_MOM` (0) / `SCENE_PLAYERSHOUSE1F_NOOP` (1) | `maps/PlayersHouse1F.asm:9-10` | `setscene` at step 6 | scene 0 must be live exactly once |
| `PHONE_MOM` | `constants/phone_constants.asm` (via `addcellnum`) | `MeetMomScript` | Mom's number in the Pokegear |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| POKEGEAR (not an item - `ENGINE_POKEGEAR` + `ENGINE_PHONE_CARD`) | Mom cutscene | `MeetMomScript` | `EVENT_PLAYERS_HOUSE_MOM_1` / `SCENE_PLAYERSHOUSE1F_NOOP` |
**Trainers**: none.
**Wild encounters**: none (indoor).
### MAP_NEW_BARK_TOWN
- Script: `maps/NewBarkTown.asm`
- Blocks: `maps/NewBarkTown.blk`
- Header (`data/maps/maps.asm:475`): `map NewBarkTown, TILESET_JOHTO, TOWN, LANDMARK_NEW_BARK_TOWN, MUSIC_NEW_BARK_TOWN, FALSE, PALETTE_AUTO, FISHGROUP_OCEAN`
- Dimensions (`constants/map_constants.asm:440`): `map_const NEW_BARK_TOWN, 10, 9` (20x18 cells)
- Connections (`data/maps/attributes.asm:119-121`): `map_attributes NewBarkTown, NEW_BARK_TOWN, $05`, `connection west, Route29, ROUTE_29, 0`, `connection east, Route27, ROUTE_27, 0`
- Scene var: `wNewBarkTownSceneID` (`data/maps/scenes.asm:28`).
`SCENE_NEWBARKTOWN_TEACHER_STOPS_YOU` = 0, `SCENE_NEWBARKTOWN_NOOP` = 1.
- Fly point / spawn: `NewBarkTownFlypointCallback` sets `ENGINE_FLYPOINT_NEW_BARK`
(`constants/engine_flags.asm:79`) on every `MAPCALLBACK_NEWMAP`, and also
`clearevent EVENT_FIRST_TIME_BANKING_WITH_MOM`. `SPAWN_NEW_BARK` is
(13,6) in `data/maps/spawn_points.asm:27`.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 6 | 3 | `ELMS_LAB` | 1 |
| 2 | 13 | 5 | `PLAYERS_HOUSE_1F` | 1 |
| 3 | 3 | 11 | `PLAYERS_NEIGHBORS_HOUSE` | 1 |
| 4 | 11 | 13 | `ELMS_HOUSE` | 1 |
**Coord events** (`def_coord_events`)
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_NEWBARKTOWN_TEACHER_STOPS_YOU` (0) | 1 | 8 | `NewBarkTown_TeacherStopsYouScene1` | teacher walks 4x LEFT to (2,8), `follow`s the player and drags them back 4x RIGHT |
| `SCENE_NEWBARKTOWN_TEACHER_STOPS_YOU` (0) | 1 | 9 | `NewBarkTown_TeacherStopsYouScene2` | same, 5 steps, teacher ends at (1,8) facing DOWN |
These two cells are the entire west-exit gate. There is no coord event on the
east side.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 8 | 8 | `BGEVENT_READ` | `NewBarkTownSign` |
| 11 | 5 | `BGEVENT_READ` | `NewBarkTownPlayersHouseSign` |
| 3 | 3 | `BGEVENT_READ` | `NewBarkTownElmsLabSign` |
| 9 | 13 | `BGEVENT_READ` | `NewBarkTownElmsHouseSign` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `NEWBARKTOWN_TEACHER` | `SPRITE_TEACHER` | 6 | 8 | `SPRITEMOVEDATA_SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT` | `NewBarkTownTeacherScript` | `-1` (always visible) |
| `NEWBARKTOWN_FISHER` | `SPRITE_FISHER` | 12 | 9 | `SPRITEMOVEDATA_WALK_UP_DOWN`, `PAL_NPC_GREEN` | `OBJECTTYPE_SCRIPT` | `NewBarkTownFisherScript` | `-1` |
| `NEWBARKTOWN_RIVAL` | `SPRITE_RIVAL` | 3 | 2 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `NewBarkTownRivalScript` | `EVENT_RIVAL_NEW_BARK_TOWN` |
**Scripts of interest**
- `NewBarkTownRivalScript` (the "he'll push you out of the way" beat): no
`faceplayer`. Prints `NewBarkTownRivalText1` ("So this is the famous ELM #MON
LAB…"), `turnobject NEWBARKTOWN_RIVAL, LEFT`, prints `NewBarkTownRivalText2`,
then `follow PLAYER, NEWBARKTOWN_RIVAL` +
`applymovement PLAYER, NewBarkTown_RivalPushesYouAwayMovement` (`turn_head UP`,
`step DOWN`), `stopfollow`, `turnobject NEWBARKTOWN_RIVAL, DOWN`,
`playsound SFX_TACKLE`, `applymovement PLAYER, NewBarkTown_RivalShovesYouOutMovement`
(`fix_facing`, `jump_step DOWN`), and the rival steps RIGHT back to (3,2).
**It sets no flag** - it is repeatable and skippable. `EVENT_RIVAL_NEW_BARK_TOWN`
is only set much later, at `maps/MrPokemonsHouse.asm:122`.
- `NewBarkTown_TeacherStopsYouScene1/2`: the "It's dangerous to go out without a
#MON!" block. Ends with `special RestartMapMusic` and does **not** change the
scene, so it re-fires every time you step on (1,8)/(1,9) until the scene id is
changed from Elm's Lab.
- `NewBarkTownTeacherScript`: four-way text branch on
`EVENT_TALKED_TO_MOM_AFTER_MYSTERY_EGG_QUEST` / `EVENT_GAVE_MYSTERY_EGG_TO_ELM` /
`EVENT_GOT_A_POKEMON_FROM_ELM`; flavour only.
- `NewBarkTownFlypointCallback`: `setflag ENGINE_FLYPOINT_NEW_BARK` on first entry.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `SCENE_NEWBARKTOWN_TEACHER_STOPS_YOU` (0) | `maps/NewBarkTown.asm:8` | cleared to `SCENE_NEWBARKTOWN_NOOP` by `ElmDirectionsScript`'s `setmapscene NEW_BARK_TOWN, SCENE_NEWBARKTOWN_NOOP` | the west-exit gate |
| `EVENT_RIVAL_NEW_BARK_TOWN` | `constants/event_flags.asm:1119` | set at `maps/MrPokemonsHouse.asm:122` | rival object hidden once set; clear (visible) all through this section |
| `ENGINE_FLYPOINT_NEW_BARK` | `constants/engine_flags.asm:79` | `NewBarkTownFlypointCallback` | Fly destination registered |
| `EVENT_FIRST_TIME_BANKING_WITH_MOM` | `constants/event_flags.asm` | cleared by `NewBarkTownFlypointCallback` | not reachable this section |
**Items**: none. See "Unresolved" for the Everstone.
**Trainers**: none. The rival encounter here is a conversation, not a battle.
**Wild encounters**
- No `def_grass_wildmons NEW_BARK_TOWN` entry exists in `data/wild/johto_grass.asm`.
- Water (`data/wild/johto_water.asm:211`): `def_water_wildmons NEW_BARK_TOWN`,
`db 6 percent`, `db 20, TENTACOOL` / `db 15, TENTACOOL` / `db 20, TENTACRUEL`.
Needs Surf, unreachable this section.
- Fishing: the header's `FISHGROUP_OCEAN` selects
`data/wild/fish.asm` `.Ocean_Old` (Magikarp 10 / Magikarp 10 / Tentacool 10),
`.Ocean_Good`, `.Ocean_Super`. No rod yet.
- Headbutt: `data/wild/treemon_maps.asm:28` `treemon_map NEW_BARK_TOWN, TREEMON_SET_CITY`
-> `data/wild/treemons.asm:18` `TreeMonSet_City` (Venonat/Abra/Venomoth common,
Venonat/Magnemite/Abra/Venomoth rare, all L15). Needs TM02, far later.
### MAP_ELMS_LAB
- Script: `maps/ElmsLab.asm`
- Blocks: `maps/ElmsLab.blk`
- Header (`data/maps/maps.asm:476`): `map ElmsLab, TILESET_LAB, INDOOR, LANDMARK_NEW_BARK_TOWN, MUSIC_PROF_ELM, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions (`constants/map_constants.asm:441`): `map_const ELMS_LAB, 5, 6` (10x12 cells)
- Connections: none (indoor)
- Scene var: `wElmsLabSceneID` (`data/maps/scenes.asm:29`). Declaration order
gives `SCENE_ELMSLAB_MEET_ELM` = 0, `SCENE_ELMSLAB_CANT_LEAVE` = 1,
`SCENE_ELMSLAB_NOOP` = 2, `SCENE_ELMSLAB_MEET_OFFICER` = 3,
`SCENE_ELMSLAB_UNUSED` = 4, `SCENE_ELMSLAB_AIDE_GIVES_POTION` = 5,
`SCENE_ELMSLAB_AIDE_GIVES_POKE_BALLS` = 6 (the last one is a bare
`scene_const`, no script row).
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 11 | `NEW_BARK_TOWN` | 1 |
| 2 | 5 | 11 | `NEW_BARK_TOWN` | 1 |
**Coord events** (`def_coord_events`)
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_ELMSLAB_CANT_LEAVE` (1) | 4 | 6 | `LabTryToLeaveScript` | Elm turns DOWN, `LabWhereGoingText`, `applymovement PLAYER, ElmsLab_CantLeaveMovement` (one `step UP`) |
| `SCENE_ELMSLAB_CANT_LEAVE` (1) | 5 | 6 | `LabTryToLeaveScript` | same |
| `SCENE_ELMSLAB_MEET_OFFICER` (3) | 4 | 5 | `MeetCopScript` | post-theft, next section |
| `SCENE_ELMSLAB_MEET_OFFICER` (3) | 5 | 5 | `MeetCopScript2` | post-theft, next section |
| `SCENE_ELMSLAB_AIDE_GIVES_POTION` (5) | 4 | 8 | `AideScript_WalkPotion1` | aide walks 2x RIGHT, gives POTION, walks back |
| `SCENE_ELMSLAB_AIDE_GIVES_POTION` (5) | 5 | 8 | `AideScript_WalkPotion2` | aide walks 3x RIGHT, gives POTION, walks back |
| `SCENE_ELMSLAB_AIDE_GIVES_POKE_BALLS` (6) | 4 | 8 | `AideScript_WalkBalls1` | 5x POKE_BALL, next section |
| `SCENE_ELMSLAB_AIDE_GIVES_POKE_BALLS` (6) | 5 | 8 | `AideScript_WalkBalls2` | 5x POKE_BALL, next section |
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 2 | 1 | `BGEVENT_READ` | `ElmsLabHealingMachine` |
| 6 | 1 | `BGEVENT_READ` | `ElmsLabBookshelf` |
| 7 | 1 | `BGEVENT_READ` | `ElmsLabBookshelf` |
| 8 | 1 | `BGEVENT_READ` | `ElmsLabBookshelf` |
| 9 | 1 | `BGEVENT_READ` | `ElmsLabBookshelf` |
| 0 | 7 | `BGEVENT_READ` | `ElmsLabTravelTip1` |
| 1 | 7 | `BGEVENT_READ` | `ElmsLabTravelTip2` |
| 2 | 7 | `BGEVENT_READ` | `ElmsLabTravelTip3` |
| 3 | 7 | `BGEVENT_READ` | `ElmsLabTravelTip4` |
| 6 | 7 | `BGEVENT_READ` | `ElmsLabBookshelf` |
| 7 | 7 | `BGEVENT_READ` | `ElmsLabBookshelf` |
| 8 | 7 | `BGEVENT_READ` | `ElmsLabBookshelf` |
| 9 | 7 | `BGEVENT_READ` | `ElmsLabBookshelf` |
| 9 | 3 | `BGEVENT_READ` | `ElmsLabTrashcan` |
| 5 | 0 | `BGEVENT_READ` | `ElmsLabWindow` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ELMSLAB_ELM` | `SPRITE_ELM` | 5 | 2 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `ProfElmScript` | `-1` |
| `ELMSLAB_ELMS_AIDE` | `SPRITE_SCIENTIST` | 2 | 9 | `SPRITEMOVEDATA_SPINRANDOM_SLOW`, `PAL_NPC_BLUE` | `OBJECTTYPE_SCRIPT` | `ElmsAideScript` | `EVENT_ELMS_AIDE_IN_LAB` |
| `ELMSLAB_POKE_BALL1` | `SPRITE_POKE_BALL` | 6 | 3 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_SCRIPT` | `CyndaquilPokeBallScript` | `EVENT_CYNDAQUIL_POKEBALL_IN_ELMS_LAB` |
| `ELMSLAB_POKE_BALL2` | `SPRITE_POKE_BALL` | 7 | 3 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_SCRIPT` | `TotodilePokeBallScript` | `EVENT_TOTODILE_POKEBALL_IN_ELMS_LAB` |
| `ELMSLAB_POKE_BALL3` | `SPRITE_POKE_BALL` | 8 | 3 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_SCRIPT` | `ChikoritaPokeBallScript` | `EVENT_CHIKORITA_POKEBALL_IN_ELMS_LAB` |
| `ELMSLAB_OFFICER` | `SPRITE_OFFICER` | 5 | 3 | `SPRITEMOVEDATA_STANDING_UP`, `PAL_NPC_BLUE` | `OBJECTTYPE_SCRIPT` | `CopScript` | `EVENT_COP_IN_ELMS_LAB` |
The ball order on screen is left-to-right **Cyndaquil (6,3), Totodile (7,3),
Chikorita (8,3)** - not the Pokedex order the walkthrough's title uses. The
officer is hidden all through this section because `InitializeEventsScript`
*sets* `EVENT_COP_IN_ELMS_LAB` (`engine/events/std_scripts.asm:469`).
**Scripts of interest**
- `ElmsLabMeetElmScene` (scene 0) -> `sdefer ElmsLabWalkUpToElmScript`:
`applymovement PLAYER, ElmsLab_WalkUpToElmMovement` (nine `step UP` then
`turn_head RIGHT`, i.e. from the door at (4,11) up to (4,2) beside Elm at
(5,2)), `turnobject ELMSLAB_ELM, LEFT`, `writetext ElmText_Intro` (the Mr.
Pokemon errand speech), then `setscene SCENE_ELMSLAB_CANT_LEAVE`.
- `CyndaquilPokeBallScript` / `TotodilePokeBallScript` / `ChikoritaPokeBallScript`:
identical shape. `checkevent EVENT_GOT_A_POKEMON_FROM_ELM` -> if set, just
`LookAtElmPokeBallScript`. Otherwise `turnobject ELMSLAB_ELM, DOWN`,
`reanchormap`, `pokepic <SPECIES>`, `cry <SPECIES>`, `closepokepic`,
`writetext Take<Species>Text`, `yesorno`; `iffalse DidntChooseStarterScript`
(fully cancellable, nothing is written). On yes:
`disappear ELMSLAB_POKE_BALL<n>`, `setevent EVENT_GOT_<SPECIES>_FROM_ELM`,
`ChoseStarterText`, `getmonname STRING_BUFFER_3`, `ReceivedStarterText`,
`playsound SFX_CAUGHT_MON`, then **`givepoke <SPECIES>, 5, BERRY`** - level 5,
holding a BERRY. Finally an `applymovement` that walks the player to (5,3),
directly below Elm, and `sjump ElmDirectionsScript`:
- Cyndaquil: `readvar VAR_FACING` / `ifequal RIGHT, ElmDirectionsScript` (skips the walk if you took it from the left side), else `AfterCyndaquilMovement` = LEFT, UP.
- Totodile: `AfterTotodileMovement` = LEFT, LEFT, UP.
- Chikorita: `AfterChikoritaMovement` = LEFT, LEFT, LEFT, UP.
- `ElmDirectionsScript` - the one script that opens the section up:
`turnobject PLAYER, UP`, three Elm texts, `addcellnum PHONE_ELM`,
`playsound SFX_REGISTER_PHONE_NUMBER`, then
`setevent EVENT_GOT_A_POKEMON_FROM_ELM`,
`setevent EVENT_RIVAL_CHERRYGROVE_CITY`,
`setscene SCENE_ELMSLAB_AIDE_GIVES_POTION`,
`setmapscene NEW_BARK_TOWN, SCENE_NEWBARKTOWN_NOOP`.
That last opcode is what removes the teacher gate.
- `AideScript_GivePotion` (reached by the (4,8)/(5,8) coord events once the scene
is 5): `writetext AideText_GiveYouPotion`, `verbosegiveitem POTION`,
`AideText_AlwaysBusy`, `setscene SCENE_ELMSLAB_NOOP`. One-shot, gated by the
scene id rather than an event flag.
- `LabTryToLeaveScript`: the "you cannot walk out yet" push-back described above.
- `ElmsLabHealingMachine` (bg_event at (2,1)): before
`EVENT_GOT_A_POKEMON_FROM_ELM` it just prints text; after, it offers a
`yesorno` -> `special HealParty`, `setval HEALMACHINE_ELMS_LAB`,
`special HealMachineAnim`. Free healing point for a bot from this section on.
- `ProfElmScript`: long check chain. In this section every branch falls through
to `writetext ElmText_LetYourMonBattleIt` (after the starter) or
`ElmDescribesMrPokemonScript`. The `ElmGiveEverstoneScript` arm needs
`EVENT_SHOWED_TOGEPI_TO_ELM`; the Master Ball arm needs `ENGINE_RISINGBADGE`;
the S.S. Ticket arm needs `EVENT_BEAT_ELITE_FOUR`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_GOT_A_POKEMON_FROM_ELM` | `constants/event_flags.asm:35` | set by `ElmDirectionsScript`; read by the ball scripts, `MomScript`, `NewBarkTownTeacherScript`, `PlayersHouseRadioScript`, `ElmsLabHealingMachine` | the section's terminal flag |
| `EVENT_GOT_CYNDAQUIL_FROM_ELM` | `constants/event_flags.asm:36` | `CyndaquilPokeBallScript` | which starter (Totodile/Chikorita variants are the next two consts) |
| `EVENT_CYNDAQUIL_POKEBALL_IN_ELMS_LAB` | `constants/event_flags.asm:993` | `disappear ELMSLAB_POKE_BALL1` | ball object removed |
| `EVENT_TOTODILE_POKEBALL_IN_ELMS_LAB` | `constants/event_flags.asm:994` | `disappear ELMSLAB_POKE_BALL2` | ball object removed |
| `EVENT_CHIKORITA_POKEBALL_IN_ELMS_LAB` | `constants/event_flags.asm:995` | `disappear ELMSLAB_POKE_BALL3` | ball object removed |
| `EVENT_COP_IN_ELMS_LAB` | `constants/event_flags.asm:1188` | set by `InitializeEventsScript`, cleared at `maps/MrPokemonsHouse.asm:127` | officer hidden this section |
| `EVENT_ELMS_AIDE_IN_LAB` | `constants/event_flags.asm:1187` | cleared at `maps/VioletPokecenter1F.asm:31`, set at `engine/phone/scripts/elm.asm:85` | aide visible (flag clear) this section |
| `EVENT_RIVAL_CHERRYGROVE_CITY` | `constants/event_flags.asm` | set by `InitializeEventsScript` and again by `ElmDirectionsScript` | Cherrygrove rival still hidden |
| `EVENT_GOT_EVERSTONE_FROM_ELM` | `constants/event_flags.asm:95` | `ElmGiveEverstoneScript` | **not reachable in this section** |
| `SCENE_ELMSLAB_*` | `maps/ElmsLab.asm:11-17` | `setscene` / `setmapscene` | drives every coord event on the map |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `POTION` | walk onto (4,8) or (5,8) on the way out after picking a starter | `AideScript_GivePotion` (`verbosegiveitem POTION`) | `SCENE_ELMSLAB_AIDE_GIVES_POTION` -> `SCENE_ELMSLAB_NOOP` |
| starter, L5, holding `BERRY` | `givepoke` in the chosen ball script | `CyndaquilPokeBallScript` / `TotodilePokeBallScript` / `ChikoritaPokeBallScript` | `EVENT_GOT_<SPECIES>_FROM_ELM` + `EVENT_GOT_A_POKEMON_FROM_ELM` |
| `EVERSTONE` | Elm, **after** showing him a hatched Togepi | `ElmGiveEverstoneScript` (`maps/ElmsLab.asm:341`) | `EVENT_GOT_EVERSTONE_FROM_ELM` |
| `POKE_BALL` x5 | aide, after the Mystery Egg hand-off | `AideScript_GiveYouBalls` | `SCENE_ELMSLAB_AIDE_GIVES_POKE_BALLS` -> `SCENE_ELMSLAB_NOOP` |
**Trainers**: none. No `loadtrainer` / `startbattle` anywhere in
`maps/ElmsLab.asm`, `maps/NewBarkTown.asm`, `maps/PlayersHouse1F.asm` or
`maps/PlayersHouse2F.asm`.
**Wild encounters**: none (indoor).
**Starter data** (`data/pokemon/evos_attacks.asm`, checked against the
walkthrough's claims - all three match)
| species | evolves | L5 known moves | later moves the FAQ names |
|---|---|---|---|
| `CHIKORITA` | `EVOLVE_LEVEL, 16, BAYLEEF`; Bayleef `EVOLVE_LEVEL, 32, MEGANIUM` | TACKLE (1), GROWL (1) | RAZOR_LEAF 8, REFLECT 12, POISONPOWDER 15, SYNTHESIS 22, BODY_SLAM 29, LIGHT_SCREEN 36, SAFEGUARD 43, SOLARBEAM 50 |
| `CYNDAQUIL` | `EVOLVE_LEVEL, 14, QUILAVA`; Quilava `EVOLVE_LEVEL, 36, TYPHLOSION` | TACKLE (1), LEER (1) | SMOKESCREEN 6, EMBER 12, QUICK_ATTACK 19, FLAME_WHEEL 27, SWIFT 36, FLAMETHROWER 46 |
| `TOTODILE` | `EVOLVE_LEVEL, 18, CROCONAW`; Croconaw `EVOLVE_LEVEL, 30, FERALIGATR` | SCRATCH (1), LEER (1) | RAGE 7, WATER_GUN 13, BITE 20, SCARY_FACE 27, SLASH 35, SCREECH 43, HYDRO_PUMP 52 |
Note the FAQ's move lists are aspirational (they mix level-up moves with HM03 /
HM04). At the moment you receive it the starter knows exactly its two level-1
moves; Cyndaquil does **not** have Ember yet at L5.
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Clock must be set before the world exists | `engine/menus/intro_menu.asm:494` `OakSpeech` -> `farcall InitClock` (`engine/rtc/timeset.asm:4`); default hour is 10 (`ld a, 10 ; default hour = 10 AM`) | hour then minute then a `YesNoBox` confirm each | answering "yes" to both confirmations |
| Player must be named | `engine/menus/intro_menu.asm` `NamePlayer` -> `ShowPlayerNamingChoices` / `StorePlayerName` | pick a preset or type a name | any accepted name |
| Cannot walk out of Elm's Lab before the errand speech ends | `maps/ElmsLab.asm` coord events (4,6) and (5,6) under `SCENE_ELMSLAB_CANT_LEAVE`, script `LabTryToLeaveScript` | scene id must leave 1 | `ElmDirectionsScript`'s `setscene SCENE_ELMSLAB_AIDE_GIVES_POTION` - i.e. take a starter |
| Cannot leave New Bark Town west toward Route 29 | `maps/NewBarkTown.asm` coord events (1,8) and (1,9) under `SCENE_NEWBARKTOWN_TEACHER_STOPS_YOU`, scripts `NewBarkTown_TeacherStopsYouScene1/2` | scene id must leave 0 | `ElmDirectionsScript`'s `setmapscene NEW_BARK_TOWN, SCENE_NEWBARKTOWN_NOOP` |
| Aide's Potion is scene-gated, not flag-gated | `maps/ElmsLab.asm` coord events (4,8)/(5,8), `AideScript_GivePotion` | be on scene 5 and step on x=4 or 5, y=8 | script itself does `setscene SCENE_ELMSLAB_NOOP` |
| Everstone (listed by the FAQ under New Bark Town) | `maps/ElmsLab.asm:63,338-347` `ElmCheckEverstone` / `ElmGiveEverstoneScript` | `EVENT_SHOWED_TOGEPI_TO_ELM`, which needs a hatched Togepi in the party (`special FindPartyMonThatSpeciesYourTrainerID`) | many sections later |
No HM field-move gate applies in this section: nothing under
`engine/overworld/` (cut/surf/strength/whirlpool/waterfall/flash/fly) is
consulted between the bedroom and the starter.
## 4. Bot checklist
| # | Map | Target | Intent | Precondition | Postcondition |
|---|---|---|---|---|---|
| 1 | - (main menu) | NEW GAME | menu select | fresh save | `NewGame` runs `ResetWRAM` |
| 2 | - (`InitClock`) | hour, then minute, then yes/no per field | menu | in `OakSpeech` | RTC base written; default hour 10 |
| 3 | - (`NamePlayer`) | any name | naming screen | after `OakText6` | `wPlayerName` set |
| 4 | `PLAYERS_HOUSE_2F` | spawn (3,3) | - | `SPAWN_HOME` | `MAPCALLBACK_NEWMAP` runs `InitializeEventsScript` once; `EVENT_INITIALIZED_EVENTS` set |
| 5 | `PLAYERS_HOUSE_2F` | warp 1 at (7,0) | walk onto | - | now on `PLAYERS_HOUSE_1F` warp 3 (9,0) |
| 6 | `PLAYERS_HOUSE_1F` | (scene 0 auto-fires) | wait / mash A through `MeetMomScript` | `wPlayersHouse1FSceneID` == 0 | `ENGINE_POKEGEAR`, `ENGINE_PHONE_CARD`, `PHONE_MOM`, `EVENT_PLAYERS_HOUSE_MOM_1`, scene -> 1 |
| 6a | `PLAYERS_HOUSE_1F` | DST `yesorno` pair | answer, then confirm | inside `MeetMomScript` | answering "no" to the confirm loops back to `.SetDayOfWeek` - a bot must confirm, not just answer |
| 7 | `PLAYERS_HOUSE_1F` | warp 1 (6,7) or warp 2 (7,7) | walk onto | - | `NEW_BARK_TOWN` at warp 2 (13,5) |
| 8 | `NEW_BARK_TOWN` | object `NEWBARKTOWN_RIVAL` at (3,2) | talk (optional) | rival visible while `EVENT_RIVAL_NEW_BARK_TOWN` clear | pushed one cell DOWN + a `jump_step DOWN`; **no flag set**, skippable |
| 9 | `NEW_BARK_TOWN` | warp 1 at (6,3) | walk onto | avoid (1,8)/(1,9) until step 12 | `ELMS_LAB` at warp 1 (4,11) |
| 10 | `ELMS_LAB` | (scene 0 auto-fires) | wait / mash A | `wElmsLabSceneID` == 0 | player auto-walked to (4,2); scene -> 1 (`CANT_LEAVE`) |
| 11 | `ELMS_LAB` | ball object: (6,3) Cyndaquil / (7,3) Totodile / (8,3) Chikorita | face + A, answer **yes** to `yesorno` | `EVENT_GOT_A_POKEMON_FROM_ELM` clear | starter L5 w/ BERRY in party; `EVENT_GOT_<SP>_FROM_ELM`; ball disappears; auto-walk to (5,3); `ElmDirectionsScript` runs -> `EVENT_GOT_A_POKEMON_FROM_ELM`, `PHONE_ELM`, `ELMS_LAB` scene -> 5, `NEW_BARK_TOWN` scene -> 1 |
| 12 | `ELMS_LAB` | (4,8) or (5,8) | walk onto on the way out | scene == 5 | `POTION` in bag; scene -> 2 (`NOOP`) |
| 13 | `ELMS_LAB` | bg_event (2,1) | face UP + A -> yes (optional) | `EVENT_GOT_A_POKEMON_FROM_ELM` set | party healed (`special HealParty`) |
| 14 | `ELMS_LAB` | warp 1 (4,11) or warp 2 (5,11) | walk onto | - | `NEW_BARK_TOWN` (6,3) |
| 15 | `NEW_BARK_TOWN` | west edge past (1,8)/(1,9) | walk west | `NEW_BARK_TOWN` scene == 1 | crosses the `connection west, Route29` into `ROUTE_29` - section 01 |
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Title -> NEW GAME -> Oak speech -> name pick -> naming screen -> bedroom | `src/ui/gen2/TitleState.lua`, `src/ui/gen2/MainMenu.lua`, `src/ui/gen2/OakSpeech.lua`, `src/ui/gen2/NamePick.lua`, `src/ui/gen2/NamingScreen.lua`; asserted end-to-end by `tests/drivers/gold_boot_smoke.lua` | implemented |
| `InitClock` (the "what time is it?" / minute screens) | none - `src/ui/gen2/OakSpeech.lua:3` says outright "Gender select / InitClock are Crystal-later" | **missing** (bot step 2 has no screen to drive) |
| `SPAWN_HOME` start at `PLAYERS_HOUSE_2F` (3,3) | `src/world/gen2/World.lua:228-232` (`START_MAP = "PLAYERS_HOUSE_2F"`, cites `data/maps/spawn_points.asm`) | implemented |
| Map load, warps, collision, connections | `src/world/gen2/Map.lua`, `src/world/gen2/World.lua`; `tests/drivers/gold_walk_smoke.lua`, `tests/drivers/gold_warp_scene.lua` (asserts the New Bark door lands on `ELMS_LAB`) | implemented |
| Scene scripts + coord events (the mechanism every gate in this section uses) | `src/world/gen2/World.lua:5011-5035` (`coordEvents` scan filtered by `self:scene()`, `sceneScripts` lookup), extracted by `src/import/RomExtractorGen2.lua:804-977` | implemented |
| `MAPCALLBACK_NEWMAP` / `MAPCALLBACK_TILES`, `ToggleDecorationsVisibility`, `ToggleMaptileDecorations` | `src/script/gen2/Specials.lua:489-505`, `tests/drivers/gold_map_callbacks.lua`, `tests/gen2_map_callbacks_test.lua` | implemented |
| `MeetMomScript` (emote, follow-free walk-in, `yesorno` under a prompt, mid-scene object swap) | driven by the VM; regression driver `tests/drivers/gold_mom_scene.lua` exists specifically for this scene; the Mom object-swap timing is commented at `src/world/gen2/World.lua:558-568,730` | implemented |
| `special SetDayOfWeek` / `InitialSetDSTFlag` / `InitialClearDSTFlag` | `src/script/gen2/Specials.lua:836-860` | partial - the flags/RTC fields are written from the host clock, but no day-of-week picker UI is drawn, so the player never chooses |
| Teacher west-exit gate | `tests/drivers/gold_teacher_scene.lua` (asserts `follow` drags the player back and she stands beside them) | implemented |
| Rival push-out (`follow` + `jump_step` + `SFX_TACKLE`) | VM ops `follow`/`stopfollow` (`src/script/gen2/Opcodes.lua:117-118`), movement in `src/script/gen2/Movement.lua` | partial - opcodes present, no driver covering `NewBarkTownRivalScript` specifically |
| Starter pick: `pokepic`, `cry`, `yesorno`, `disappear`, `givepoke SP, 5, BERRY` | `src/script/gen2/Opcodes.lua:91-92,51`; `src/script/gen2/Vm.lua:386-394` (`pokepic` -> `showPicFn`), `:439-445` (`givepoke` -> `givePokeFn` with species/level/held item) | partial - all opcodes implemented, but no driver or test in `tests/drivers/gold_*.lua` walks the Elm's Lab starter choice end to end |
| `verbosegiveitem POTION` (aide) | `src/script/gen2/Vm.lua:490-510` (received text, SFX, "put in the ITEM POCKET") | implemented |
| `addcellnum PHONE_MOM` / `PHONE_ELM`, Pokegear phone card | `src/script/gen2/Vm.lua:611`, `src/core/gen2/Phone.lua:137,146`, `src/ui/gen2/Pokegear.lua` | implemented |
| `special HealParty` / `HealMachineAnim` (lab healing machine) | `src/script/gen2/Specials.lua:456` | implemented |
| `special PlayersHousePC` + `warp NONE, 0, 0` | `src/script/gen2/Specials.lua:467-487`, `src/ui/gen2/PcMenu.lua` | implemented |
| `special NameRival` (fires next section, from `CopScript`) | `src/script/gen2/Specials.lua:825-828` -> `World:nameRival` (`src/world/gen2/World.lua:4931`) | implemented |
| Event-flag bitfield semantics (set flag = object hidden) | `src/world/gen2/Events.lua` (header comment cites `CheckObjectFlag` in `map_objects_2.asm`) | implemented |
## 6. Unresolved / verify by hand
- **Everstone.** The FAQ lists "Everstone" under "Items found in New Bark Town".
There is no Everstone item ball, hidden item or bg_event on `MAP_NEW_BARK_TOWN`,
`MAP_ELMS_LAB`, `MAP_PLAYERS_HOUSE_1F` or `MAP_PLAYERS_HOUSE_2F`. The only New
Bark Town Everstone is `ElmGiveEverstoneScript` (`maps/ElmsLab.asm:338-347`),
which requires `EVENT_SHOWED_TOGEPI_TO_ELM` - a hatched Togepi shown to Elm,
many sections later. Treat the FAQ line as forward-looking, not as an item to
collect here.
- **"Pokegear" as an item.** The FAQ says "You get the Pokegear". In the asm it is
`setflag ENGINE_POKEGEAR` + `setflag ENGINE_PHONE_CARD`, with the item name
faked through `getstring STRING_BUFFER_4, PokegearName` ("#GEAR") into the
standard `ReceiveItemScript` text. Nothing enters the bag - a bot must not
check inventory for it.
- **"Professor Oak will ask you the time. First answer what hour it is and what
minute it is."** `InitClock` (`engine/rtc/timeset.asm`) is reached from
`OakSpeech` *before* any Oak text is printed (`farcall InitClock` is the first
instruction of `OakSpeech`), so the clock screen actually comes first and Oak's
greeting second. The FAQ's ordering is the reverse.
- **"even if you don't have the GBA on, the time will keep running"** - an RTC
hardware claim, nothing in the disassembly to cite; ignore.
- **East exit toward Route 27.** `data/maps/attributes.asm:121` gives New Bark Town
a `connection east, Route27, ROUTE_27, 0`, and unlike the west side there is
*no* coord event guarding it. Whether a bot can actually walk east before
getting Surf depends on the shoreline tiles in `maps/NewBarkTown.blk`, which I
did not decode. Verify by hand before relying on the west gate as the only exit
block.
- **Teacher gate coverage.** Only (1,8) and (1,9) carry coord events. If the map's
west connection strip is reachable at any other y, the gate has a hole. I read
the coord table but not the collision data, so this is unverified.
- **Rival battle.** The FAQ says only that he pushes you; confirmed - there is no
`loadtrainer`/`startbattle` in `NewBarkTownRivalScript`. The first rival battle
is not in this section.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,777 @@
# Section 02 - Sprout Tower and Violet City Gym
Source: `../section-02-sprout-tower-and-violet-city-gym.txt`
Maps covered: `MAP_SPROUT_TOWER_1F`, `MAP_SPROUT_TOWER_2F`, `MAP_SPROUT_TOWER_3F`,
`MAP_VIOLET_CITY`, `MAP_VIOLET_POKECENTER_1F`, `MAP_VIOLET_GYM`
Badges / key milestones in this section: HM05 Flash (`EVENT_GOT_HM05_FLASH`),
the Sprout Tower rival cutscene (`EVENT_RIVAL_SPROUT_TOWER`), ZEPHYRBADGE
(`ENGINE_ZEPHYRBADGE`, `EVENT_BEAT_FALKNER`), TM31 Mud-Slap
(`EVENT_GOT_TM31_MUD_SLAP`).
All map constants below use the `MAP_*` spelling that both
`constants/map_constants.asm` and this port's map ids use; the port's
`World:setMap` takes exactly the bare constant name (`"SPROUT_TOWER_1F"`), which
is what `tools/rom_manifest_gold.json` -> `constants.mapGroups` stores.
---
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 1 | `MAP_SPROUT_TOWER_1F` | `maps/SproutTower1F.asm` | `MAP_VIOLET_CITY` warp 7 at (23,5) -> 1F warp 1 (9,15) | 1F warp 3 (6,4) -> 2F warp 1 | "Head up the stairs to Floor 2." |
| 2 | `MAP_SPROUT_TOWER_2F` | `maps/SproutTower2F.asm` | 2F warp 1 (6,4) | 2F warp 3 (17,3) -> 1F warp 5 | Sage Nico at (14,4), then "down the stairs" |
| 3 | `MAP_SPROUT_TOWER_1F` | `maps/SproutTower1F.asm` | 1F warp 5 (17,3) | 1F warp 4 (2,6) -> 2F warp 2 | Parlyz Heal ball at (16,7), then Sage Chow at (2,1) |
| 4 | `MAP_SPROUT_TOWER_2F` | `maps/SproutTower2F.asm` | 2F warp 2 (2,6) | 2F warp 4 (10,14) -> 3F warp 1 | X Defend ball at (3,1), Sage Edmond at (3,15) |
| 5 | `MAP_SPROUT_TOWER_3F` | `maps/SproutTower3F.asm` | 3F warp 1 (10,14) | 3F warp 1 (10,14) -> 2F warp 4 | Potion, Jin/Neal/Troy, rival cutscene, Sage Li, HM05, Escape Rope |
| 6 | `MAP_SPROUT_TOWER_2F` -> `MAP_SPROUT_TOWER_1F` | as above | 2F warp 4 / 1F warps 3-5 | 1F warp 1 (9,15) or warp 2 (10,15) -> `MAP_VIOLET_CITY` warp 7 | "Get out of the tower" (Escape Rope also legal: the tower is `DUNGEON`) |
| 7 | `MAP_VIOLET_CITY` | `maps/VioletCity.asm` | city warp 7 (23,5) | city warp 5 (31,25) | "go back to the Pokemon Center of Violet City" |
| 8 | `MAP_VIOLET_POKECENTER_1F` | `maps/VioletPokecenter1F.asm` | PC warp 1 (3,7) | PC warp 1/2 (3,7)/(4,7) | Heal at the nurse before the gym |
| 9 | `MAP_VIOLET_CITY` | `maps/VioletCity.asm` | city warp 5 (31,25) | city warp 2 (18,17) | "head northwest in the city to the Violet City Gym" |
| 10 | `MAP_VIOLET_GYM` | `maps/VioletGym.asm` | gym warp 1 (4,15) | gym warp 1/2 (4,15)/(5,15) -> `MAP_VIOLET_CITY` warp 2 | Abe, Rod, Falkner, ZEPHYRBADGE + TM31 |
| 11 | `MAP_VIOLET_CITY` | `maps/VioletCity.asm` | city warp 2 (18,17) | (section ends) | Section closes with the badge in hand |
Spills into the next section: beating Falkner runs `specialphonecall
SPECIALCALL_ASSISTANT` (`maps/VioletGym.asm`), whose Elm phone script
(`engine/phone/scripts/elm.asm`, `.assistant`) clears
`EVENT_ELMS_AIDE_IN_VIOLET_POKEMON_CENTER` so the aide appears in
`MAP_VIOLET_POKECENTER_1F` with the Togepi egg, and sets
`SCENE_ROUTE32_OFFER_SLOWPOKETAIL` when the egg is taken. That whole beat
belongs to the next section; it is listed here only because the trigger lives
in this section's gym script.
## 2. Maps
### MAP_SPROUT_TOWER_1F
- Script: `maps/SproutTower1F.asm`
- Blocks: `maps/SproutTower1F.blk` (`data/maps/blocks.asm` -> `SproutTower1F_Blocks`)
- Header: `data/maps/maps.asm:79` -> `TILESET_TOWER`, `DUNGEON`,
`LANDMARK_SPROUT_TOWER`, `MUSIC_SPROUT_TOWER`, phone `FALSE`, `PALETTE_DAY`,
`FISHGROUP_SHORE`
- Dimensions / attributes: `constants/map_constants.asm:66`
(`map_const SPROUT_TOWER_1F, 10, 8`) = 20x16 cells;
`data/maps/attributes.asm:398` (`map_attributes SproutTower1F, SPROUT_TOWER_1F, $00`)
- Connections: none (indoor/dungeon)
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 15 | `VIOLET_CITY` | 7 |
| 2 | 10 | 15 | `VIOLET_CITY` | 7 |
| 3 | 6 | 4 | `SPROUT_TOWER_2F` | 1 |
| 4 | 2 | 6 | `SPROUT_TOWER_2F` | 2 |
| 5 | 17 | 3 | `SPROUT_TOWER_2F` | 3 |
**Coord events** (`def_coord_events`)
None (`def_coord_events` is empty).
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 7 | 15 | `BGEVENT_READ` | `SproutTower1FStatue` |
| 12 | 15 | `BGEVENT_READ` | `SproutTower1FStatue` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `SPROUTTOWER1F_SAGE1` | `SPRITE_SAGE` | 7 | 4 | `SPRITEMOVEDATA_STANDING_DOWN`, radius 0/0, pal `PAL_NPC_BLUE` | `OBJECTTYPE_SCRIPT`, sight 0 | `SproutTower1FSage1Script` | -1 |
| `SPROUTTOWER1F_SAGE2` | `SPRITE_SAGE` | 6 | 7 | `SPRITEMOVEDATA_WANDER`, radius 1/1, pal `PAL_NPC_BLUE` | `OBJECTTYPE_SCRIPT`, sight 0 | `SproutTower1FSage2Script` | -1 |
| `SPROUTTOWER1F_GRANNY` | `SPRITE_GRANNY` | 11 | 12 | `SPRITEMOVEDATA_STANDING_LEFT`, radius 0/0 | `OBJECTTYPE_SCRIPT`, sight 0 | `SproutTower1FGrannyScript` | -1 |
| `SPROUTTOWER1F_TEACHER` | `SPRITE_TEACHER` | 9 | 9 | `SPRITEMOVEDATA_STANDING_UP`, radius 0/0 | `OBJECTTYPE_SCRIPT`, sight 0 | `SproutTower1FTeacherScript` | -1 |
| `SPROUTTOWER1F_SAGE3` | `SPRITE_SAGE` | 2 | 1 | `SPRITEMOVEDATA_STANDING_RIGHT`, radius 0/0, pal `PAL_NPC_BLUE` | `OBJECTTYPE_TRAINER`, sight 1 | `TrainerSageChow` | -1 |
| `SPROUTTOWER1F_POKE_BALL` | `SPRITE_POKE_BALL` | 16 | 7 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL`, sight 0 | `SproutTower1FParlyzHeal` | `EVENT_SPROUT_TOWER_1F_PARLYZ_HEAL` |
**Scripts of interest**
- `TrainerSageChow` (`42:400e`): `trainer SAGE, CHOW, EVENT_BEAT_SAGE_CHOW,
SageChowSeenText, SageChowBeatenText, 0, .Script`. Loss text is `0`. The
after-battle arm `.Script` (`42:401a`) is `endifjustbattled` then a single
`writetext SageChowAfterBattleText`.
- `SproutTower1FParlyzHeal` (`42:4022`): a bare `itemball PARLYZ_HEAL` - two raw
bytes, **not** bytecode. Pickup is engine-side (`FindItemInBallScript`), gated
on `EVENT_SPROUT_TOWER_1F_PARLYZ_HEAL`.
- Sage1/Sage2/Granny/Teacher are all `jumptextfaceplayer` one-liners; nothing to
gate on.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_SAGE_CHOW` (1041) | `constants/event_flags.asm:518` | `TrainerSageChow` header word | Set once beaten; suppresses re-battle and the eyesight trigger |
| `EVENT_SPROUT_TOWER_1F_PARLYZ_HEAL` (1607) | `constants/event_flags.asm:1000` | object-event flag on `SPROUTTOWER1F_POKE_BALL` | Set = ball is gone (`CheckObjectFlag`, `engine/overworld/map_objects_2.asm`) |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `PARLYZ_HEAL` | Poke Ball object at (16,7) | `SproutTower1FParlyzHeal` (`itemball PARLYZ_HEAL`) | `EVENT_SPROUT_TOWER_1F_PARLYZ_HEAL` |
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `SAGE`, `CHOW` | `SAGE` (38) | `CHOW` (1) | `SageGroup` "CHOW" (`0e:7324` group base): L3 Bellsprout x3, `TRAINERTYPE_NORMAL` | `TrainerSageChow` | none |
Prize money: `data/trainers/attributes.asm` "Sage" base reward `8`;
`ComputeTrainerReward` x last party level (3) x4 in `WinTrainerBattle` = 96.
**Wild encounters**
None. `data/wild/johto_grass.asm` has **no** `def_grass_wildmons SPROUT_TOWER_1F`
entry; only 2F and 3F have tables. 1F has no grass/water/fish/headbutt data.
---
### MAP_SPROUT_TOWER_2F
- Script: `maps/SproutTower2F.asm`
- Blocks: `maps/SproutTower2F.blk`
- Header: `data/maps/maps.asm:80` -> `TILESET_TOWER`, `DUNGEON`,
`LANDMARK_SPROUT_TOWER`, `MUSIC_SPROUT_TOWER`, `FALSE`, `PALETTE_DAY`,
`FISHGROUP_SHORE`
- Dimensions / attributes: `constants/map_constants.asm:67`
(`map_const SPROUT_TOWER_2F, 10, 8`) = 20x16 cells;
`data/maps/attributes.asm:399`
- Connections: none
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 6 | 4 | `SPROUT_TOWER_1F` | 3 |
| 2 | 2 | 6 | `SPROUT_TOWER_1F` | 4 |
| 3 | 17 | 3 | `SPROUT_TOWER_1F` | 5 |
| 4 | 10 | 14 | `SPROUT_TOWER_3F` | 1 |
**Coord events** (`def_coord_events`)
None.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 12 | 15 | `BGEVENT_READ` | `SproutTower2FStatue` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `SPROUTTOWER2F_SAGE1` | `SPRITE_SAGE` | 14 | 4 | `SPRITEMOVEDATA_SPINRANDOM_FAST`, radius 0/0, pal `PAL_NPC_BLUE` | `OBJECTTYPE_TRAINER`, sight 2 | `TrainerSageNico` | -1 |
| `SPROUTTOWER2F_SAGE2` | `SPRITE_SAGE` | 3 | 15 | `SPRITEMOVEDATA_STANDING_UP`, radius 0/0, pal `PAL_NPC_BLUE` | `OBJECTTYPE_TRAINER`, sight 4 | `TrainerSageEdmond` | -1 |
| `SPROUTTOWER2F_POKE_BALL` | `SPRITE_POKE_BALL` | 3 | 1 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL`, sight 0 | `SproutTower2FXDefend` | `EVENT_SPROUT_TOWER_2F_X_DEFEND` |
**Scripts of interest**
- `TrainerSageNico` (`42:4280`) and `TrainerSageEdmond` (`42:4294`): identical
shape to Chow - `trainer` header, `.Script` = `endifjustbattled` + one
`writetext`.
- `SproutTower2FXDefend` (`42:42ab`): `itemball X_DEFEND`. Note the item is
**X Defend**, not X Accuracy.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_SAGE_NICO` (1042) | `constants/event_flags.asm:519` | `TrainerSageNico` | Beaten once |
| `EVENT_BEAT_SAGE_EDMOND` (1047) | `constants/event_flags.asm:524` | `TrainerSageEdmond` | Beaten once |
| `EVENT_SPROUT_TOWER_2F_X_DEFEND` (1608) | `constants/event_flags.asm:1001` | object-event flag | Ball taken |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `X_DEFEND` | Poke Ball object at (3,1) | `SproutTower2FXDefend` | `EVENT_SPROUT_TOWER_2F_X_DEFEND` |
**Trainers**
| const | class | id | party | script label | rematch/phone |
|---|---|---|---|---|---|
| `SAGE`, `NICO` | `SAGE` | `NICO` (2) | `SageGroup` "NICO": L3 Bellsprout x3 | `TrainerSageNico` | none |
| `SAGE`, `EDMOND` | `SAGE` | `EDMOND` (7) | `SageGroup` "EDMOND": L3 Bellsprout x3 | `TrainerSageEdmond` | none |
Both pay 8 x 3 x 4 = 96.
**Wild encounters**
`data/wild/johto_grass.asm:5` `def_grass_wildmons SPROUT_TOWER_2F`, rates
`2 percent / 2 percent / 2 percent` (morn/day/nite):
- morn: L3 Rattata, L4 Rattata, L5 Rattata, L3 Rattata, L6 Rattata, L5 Rattata, L5 Rattata
- day: identical to morn
- nite: L3 Gastly, L4 Gastly, L5 Gastly, L3 Rattata, L6 Gastly, L5 Rattata, L5 Rattata
No water/fish/headbutt entry for this map.
---
### MAP_SPROUT_TOWER_3F
- Script: `maps/SproutTower3F.asm`
- Blocks: `maps/SproutTower3F.blk`
- Header: `data/maps/maps.asm:81` -> `TILESET_TOWER`, `DUNGEON`,
`LANDMARK_SPROUT_TOWER`, `MUSIC_SPROUT_TOWER`, `FALSE`, `PALETTE_DAY`,
`FISHGROUP_SHORE`
- Dimensions / attributes: `constants/map_constants.asm:68`
(`map_const SPROUT_TOWER_3F, 10, 8`) = 20x16 cells;
`data/maps/attributes.asm:400`
- Connections: none
- Scene variable: `data/maps/scenes.asm:47` -> `wSproutTower3FSceneID`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 10 | 14 | `SPROUT_TOWER_2F` | 4 |
**Coord events** (`def_coord_events`)
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_SPROUTTOWER3F_RIVAL_ENCOUNTER` (0) | 11 | 9 | `SproutTower3FRivalScene` | The elder/rival cutscene; ends with `setscene SCENE_SPROUTTOWER3F_NOOP` |
The two `SCENE_SPROUTTOWER3F_*` constants are generated inline by the
`scene_script` macro (`macros/scripts/maps.asm`, `scene_const`) in
`maps/SproutTower3F.asm:12-13`: index 0 = `SCENE_SPROUTTOWER3F_RIVAL_ENCOUNTER`,
index 1 = `SCENE_SPROUTTOWER3F_NOOP`. Both scene *scripts* are bare `end`
stubs; the scene id exists only to arm/disarm the coord event. The scene starts
at 0 on a new game, so the trip-wire is live on first entry.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 8 | 1 | `BGEVENT_READ` | `SproutTower3FStatue` |
| 11 | 1 | `BGEVENT_READ` | `SproutTower3FStatue` |
| 9 | 0 | `BGEVENT_READ` | `SproutTower3FPainting` |
| 10 | 0 | `BGEVENT_READ` | `SproutTower3FPainting` |
| 5 | 15 | `BGEVENT_READ` | `SproutTower3FStatue` |
| 14 | 15 | `BGEVENT_READ` | `SproutTower3FStatue` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `SPROUTTOWER3F_SAGE1` | `SPRITE_SAGE` | 8 | 13 | `SPRITEMOVEDATA_STANDING_RIGHT`, pal `PAL_NPC_BLUE` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerSageJin` | -1 |
| `SPROUTTOWER3F_SAGE2` | `SPRITE_SAGE` | 8 | 8 | `SPRITEMOVEDATA_STANDING_DOWN`, pal `PAL_NPC_BLUE` | `OBJECTTYPE_TRAINER`, sight 2 | `TrainerSageTroy` | -1 |
| `SPROUTTOWER3F_SAGE3` | `SPRITE_SAGE` | 10 | 2 | `SPRITEMOVEDATA_STANDING_DOWN`, pal `PAL_NPC_BLUE` | `OBJECTTYPE_SCRIPT`, sight 0 | `SageLiScript` | -1 |
| `SPROUTTOWER3F_SAGE4` | `SPRITE_SAGE` | 11 | 11 | `SPRITEMOVEDATA_STANDING_LEFT`, pal `PAL_NPC_BLUE` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerSageNeal` | -1 |
| `SPROUTTOWER3F_POKE_BALL1` | `SPRITE_POKE_BALL` | 6 | 14 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `SproutTower3FPotion` | `EVENT_SPROUT_TOWER_3F_POTION` |
| `SPROUTTOWER3F_POKE_BALL2` | `SPRITE_POKE_BALL` | 14 | 1 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `SproutTower3FEscapeRope` | `EVENT_SPROUT_TOWER_3F_ESCAPE_ROPE` |
| `SPROUTTOWER3F_RIVAL` | `SPRITE_RIVAL` | 10 | 4 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_SCRIPT`, sight 0 | `ObjectEvent` (the shared ROM0 stub) | `EVENT_RIVAL_SPROUT_TOWER` |
Object-flag polarity, confirmed in `engine/overworld/map_objects_2.asm`
(`CheckObjectFlag`): the object is **masked when its event flag is SET**.
`EVENT_RIVAL_SPROUT_TOWER` therefore starts *clear* (rival visible) and the
cutscene's `disappear SPROUTTOWER3F_RIVAL` sets it
(`Script_disappear` -> `ApplyEventActionAppearDisappear`, `b = 1`,
`engine/overworld/scripting.asm:887`).
**Scripts of interest**
- `SproutTower3FRivalScene` (`42:444b`), fired by the coord event at (11,9):
`turnobject PLAYER, UP`; `showemote EMOTE_SHOCK, PLAYER, 15`;
`special FadeOutMusic`; two rounds of `playsound SFX_TACKLE` +
`playsound SFX_ELEVATOR` + `earthquake 79` + `pause 15`;
`applymovement PLAYER, SproutTower3FPlayerApproachesRivalMovement` (four
`step UP`, so the player ends at (11,5));
`applymovement SPROUTTOWER3F_RIVAL, SproutTower3FRivalApproachesElderMovement`
(one `step UP`, rival (10,4) -> (10,3));
`writetext SproutTowerElderLecturesRivalText`;
`showemote EMOTE_SHOCK, SPROUTTOWER3F_RIVAL, 15`;
`turnobject SPROUTTOWER3F_RIVAL, DOWN`;
`applymovement SPROUTTOWER3F_RIVAL, SproutTower3FRivalLeavesElderMovement`
(`step RIGHT`, `step DOWN`); `playmusic MUSIC_RIVAL_ENCOUNTER`;
two more `writetext`s; `playsound SFX_WARP_TO`; `special FadeOutToBlack`;
`special ReloadSpritesNoPalettes`; `disappear SPROUTTOWER3F_RIVAL`;
`waitsfx`; `special FadeInFromBlack`; `setscene SCENE_SPROUTTOWER3F_NOOP`;
`special RestartMapMusic`; `end`.
**No battle.** This is a pure cutscene - the rival is not fought here.
- `SageLiScript` (`42:44aa`): `faceplayer`, `opentext`,
`checkevent EVENT_GOT_HM05_FLASH` / `iftrue .GotFlash`. First-time arm:
`writetext SageLiSeenText`, `winlosstext SageLiBeatenText, 0`,
`loadtrainer SAGE, LI`, `startbattle`, `reloadmapafterbattle`,
`writetext SageLiTakeThisFlashText`, `promptbutton`,
`verbosegiveitem HM_FLASH`, `setevent EVENT_GOT_HM05_FLASH`,
`setevent EVENT_BEAT_SAGE_LI`, `writetext SageLiFlashExplanationText`, `end`.
`.GotFlash` (`42:44d5`) is a single `writetext SageLiAfterBattleText`.
Note the flag order: the HM is given *before* both events are set, and
`verbosegiveitem`'s failure return is **not** checked here (unlike Falkner's
TM), so a full bag at this point loses the HM.
- `TrainerSageJin` (`42:44db`), `TrainerSageTroy` (`42:44ef`),
`TrainerSageNeal` (`42:4503`): standard `trainer` headers + `endifjustbattled`
after-battle text.
- `SproutTower3FPotion` (`42:451d`) = `itemball POTION`;
`SproutTower3FEscapeRope` (`42:451f`) = `itemball ESCAPE_ROPE`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_SAGE_JIN` (1043) | `constants/event_flags.asm:520` | `TrainerSageJin` | Beaten |
| `EVENT_BEAT_SAGE_TROY` (1044) | `constants/event_flags.asm:521` | `TrainerSageTroy` | Beaten |
| `EVENT_BEAT_SAGE_NEAL` (1048) | `constants/event_flags.asm:525` | `TrainerSageNeal` | Beaten |
| `EVENT_BEAT_SAGE_LI` (1049) | `constants/event_flags.asm:526` | set by `SageLiScript` after the battle | Set only on the HM-giving pass; Li is an `OBJECTTYPE_SCRIPT`, not a trainer object, so this flag does not gate re-battle - `EVENT_GOT_HM05_FLASH` does |
| `EVENT_GOT_HM05_FLASH` (20) | `constants/event_flags.asm:27` | read+written by `SageLiScript` | The real "tower cleared" flag |
| `EVENT_RIVAL_SPROUT_TOWER` (1732) | `constants/event_flags.asm:1126` | object-event flag; set by `disappear` in the cutscene; also `setevent` in `maps/HallOfFame.asm:35` | Clear = rival still standing at (10,4) |
| `EVENT_SPROUT_TOWER_3F_POTION` (1609) | `constants/event_flags.asm:1002` | object-event flag | Ball taken |
| `EVENT_SPROUT_TOWER_3F_ESCAPE_ROPE` (1610) | `constants/event_flags.asm:1003` | object-event flag | Ball taken |
| `SCENE_SPROUTTOWER3F_RIVAL_ENCOUNTER` = 0 | `maps/SproutTower3F.asm:12` (macro-generated) | `wSproutTower3FSceneID` (`data/maps/scenes.asm:47`) | Coord event live |
| `SCENE_SPROUTTOWER3F_NOOP` = 1 | `maps/SproutTower3F.asm:13` | `setscene` at the end of the cutscene; `setmapscene` in `maps/HallOfFame.asm:39` | Coord event dead |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `POTION` | Poke Ball at (6,14) | `SproutTower3FPotion` | `EVENT_SPROUT_TOWER_3F_POTION` |
| `ESCAPE_ROPE` | Poke Ball at (14,1) | `SproutTower3FEscapeRope` | `EVENT_SPROUT_TOWER_3F_ESCAPE_ROPE` |
| `HM_FLASH` (HM05) | `verbosegiveitem` from Sage Li after his battle | `SageLiScript` | `EVENT_GOT_HM05_FLASH` |
`HM_FLASH` is the fifth `add_hm` in `constants/item_constants.asm`
(CUT, FLY, SURF, STRENGTH, **FLASH**, WHIRLPOOL, WATERFALL), i.e. HM05.
**Trainers**
| const | class | id | party | script label | rematch/phone |
|---|---|---|---|---|---|
| `SAGE`, `JIN` | `SAGE` | `JIN` (3) | `SageGroup` "JIN": L6 Bellsprout | `TrainerSageJin` | none |
| `SAGE`, `TROY` | `SAGE` | `TROY` (4) | `SageGroup` "TROY": L7 Bellsprout, L7 Hoothoot | `TrainerSageTroy` | none |
| `SAGE`, `NEAL` | `SAGE` | `NEAL` (8) | `SageGroup` "NEAL": L6 Bellsprout | `TrainerSageNeal` | none |
| `SAGE`, `LI` | `SAGE` | `LI` (9) | `SageGroup` "LI": L7 Bellsprout, L7 Bellsprout, L10 Hoothoot | `SageLiScript` (`loadtrainer`, not an `OBJECTTYPE_TRAINER`) | none |
Prize money (base 8 x last party level x 4): Jin 192, Neal 192, Troy 224,
Li 320. All `TRAINERTYPE_NORMAL` (no explicit moves; movesets come from level-up
data). Class DVs `dn 9, 8, 8, 8` (`data/trainers/dvs.asm:61`).
**Wild encounters**
`data/wild/johto_grass.asm:33` `def_grass_wildmons SPROUT_TOWER_3F` - byte for
byte identical to the 2F table: rates 2/2/2 percent, morn+day all Rattata L3-L6,
nite Gastly L3-L6 mixed with Rattata L3/L5/L5. No water/fish/headbutt entry.
---
### MAP_VIOLET_CITY
- Script: `maps/VioletCity.asm`
- Blocks: `maps/VioletCity.blk` (`data/maps/blocks.asm:532`)
- Header: `data/maps/maps.asm:251` -> `TILESET_JOHTO`, `TOWN`,
`LANDMARK_VIOLET_CITY`, `MUSIC_VIOLET_CITY`, phone `FALSE`, `PALETTE_AUTO`,
`FISHGROUP_POND`
- Dimensions / attributes: `constants/map_constants.asm:230`
(`map_const VIOLET_CITY, 20, 18`) = 40x36 cells;
`data/maps/attributes.asm:127` (`map_attributes VioletCity, VIOLET_CITY, $05`)
- Connections (`data/maps/attributes.asm:127-130`):
south `Route32`/`ROUTE_32` offset 0, west `Route36`/`ROUTE_36` offset 0,
east `Route31`/`ROUTE_31` offset 9. No north connection.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 17 | `VIOLET_MART` | 2 |
| 2 | 18 | 17 | `VIOLET_GYM` | 1 |
| 3 | 30 | 17 | `EARLS_POKEMON_ACADEMY` | 1 |
| 4 | 3 | 15 | `VIOLET_NICKNAME_SPEECH_HOUSE` | 1 |
| 5 | 31 | 25 | `VIOLET_POKECENTER_1F` | 1 |
| 6 | 21 | 29 | `VIOLET_KYLES_HOUSE` | 1 |
| 7 | 23 | 5 | `SPROUT_TOWER_1F` | 1 |
| 8 | 39 | 24 | `ROUTE_31_VIOLET_GATE` | 1 |
| 9 | 39 | 25 | `ROUTE_31_VIOLET_GATE` | 2 |
**Coord events** (`def_coord_events`)
None.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 24 | 20 | `BGEVENT_READ` | `VioletCitySign` |
| 15 | 17 | `BGEVENT_READ` | `VioletGymSign` |
| 24 | 8 | `BGEVENT_READ` | `SproutTowerSign` |
| 27 | 17 | `BGEVENT_READ` | `EarlsPokemonAcademySign` |
| 32 | 25 | `BGEVENT_READ` | `VioletCityPokecenterSign` (`jumpstd PokecenterSignScript`) |
| 10 | 17 | `BGEVENT_READ` | `VioletCityMartSign` (`jumpstd MartSignScript`) |
| 37 | 14 | `BGEVENT_ITEM` | `VioletCityHiddenHyperPotion` = `hiddenitem HYPER_POTION, EVENT_VIOLET_CITY_HIDDEN_HYPER_POTION` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `VIOLETCITY_EARL` | `SPRITE_FISHER` | 13 | 16 | `SPRITEMOVEDATA_SPINRANDOM_SLOW`, pal `PAL_NPC_GREEN` | `OBJECTTYPE_SCRIPT` | `VioletCityEarlScript` | `EVENT_VIOLET_CITY_EARL` |
| `VIOLETCITY_LASS` | `SPRITE_LASS` | 28 | 28 | `SPRITEMOVEDATA_WANDER`, radius 2/2, pal `PAL_NPC_GREEN` | `OBJECTTYPE_SCRIPT` | `VioletCityLassScript` | -1 |
| `VIOLETCITY_SUPER_NERD` | `SPRITE_SUPER_NERD` | 24 | 14 | `SPRITEMOVEDATA_WANDER`, radius 1/2, pal `PAL_NPC_RED` | `OBJECTTYPE_SCRIPT` | `VioletCitySuperNerdScript` | -1 |
| `VIOLETCITY_GRAMPS` | `SPRITE_GRAMPS` | 17 | 20 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT`, radius 1/0 | `OBJECTTYPE_SCRIPT` | `VioletCityGrampsScript` | -1 |
| `VIOLETCITY_YOUNGSTER` | `SPRITE_YOUNGSTER` | 5 | 18 | `SPRITEMOVEDATA_SPINRANDOM_SLOW`, pal `PAL_NPC_GREEN` | `OBJECTTYPE_SCRIPT` | `VioletCityYoungsterScript` | -1 |
| `VIOLETCITY_FRUIT_TREE` | `SPRITE_FRUIT_TREE` | 14 | 29 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_SCRIPT` | `VioletCityFruitTree` = `fruittree FRUITTREE_VIOLET_CITY` | -1 |
| `VIOLETCITY_POKE_BALL1` | `SPRITE_POKE_BALL` | 4 | 1 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `VioletCityPPUp` | `EVENT_VIOLET_CITY_PP_UP` |
| `VIOLETCITY_POKE_BALL2` | `SPRITE_POKE_BALL` | 35 | 5 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `VioletCityRareCandy` | `EVENT_VIOLET_CITY_RARE_CANDY` |
**Scripts of interest**
- `VioletCityFlypointCallback` (`48:4c6d`), registered as
`callback MAPCALLBACK_NEWMAP`: a single `setflag ENGINE_FLYPOINT_VIOLET`
then `endcallback`. Entering the map at all unlocks Fly to Violet.
- `VioletCityEarlScript`: spins, `faceplayer`,
`writetext Text_EarlAsksIfYouBeatFalkner`, `yesorno`. Answering **yes** is
the `.PointlessJump` arm (one line of text, no state change). Answering **no**
is `.FollowEarl`: `playmusic MUSIC_SHOW_ME_AROUND`,
`follow VIOLETCITY_EARL, PLAYER`, a long
`VioletCityFollowEarl_MovementData` walk to the academy door,
`stopfollow`, `disappear VIOLETCITY_EARL`,
`clearevent EVENT_EARLS_ACADEMY_EARL`. A bot that does not want to be
dragged across the map should answer **yes**, or simply not talk to him.
- The four remaining townsfolk are `jumptextfaceplayer` one-liners.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_FLYPOINT_VIOLET` (66) | `constants/engine_flags.asm:81` | `VioletCityFlypointCallback` | Fly destination unlocked on first map load |
| `EVENT_VIOLET_CITY_EARL` (1738) | `constants/event_flags.asm:1132` | object-event flag; set by `disappear` in `.FollowEarl` | Clear = Earl still outside |
| `EVENT_VIOLET_CITY_PP_UP` (1603) | `constants/event_flags.asm:996` | object-event flag | Ball taken |
| `EVENT_VIOLET_CITY_RARE_CANDY` (1604) | `constants/event_flags.asm:997` | object-event flag | Ball taken |
| `EVENT_VIOLET_CITY_HIDDEN_HYPER_POTION` (176) | `constants/event_flags.asm:186` | `hiddenitem` operand at bg_event (37,14) | Hidden item taken |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `PP_UP` | Poke Ball at (4,1) | `VioletCityPPUp` | `EVENT_VIOLET_CITY_PP_UP` |
| `RARE_CANDY` | Poke Ball at (35,5) | `VioletCityRareCandy` | `EVENT_VIOLET_CITY_RARE_CANDY` |
| `HYPER_POTION` | Hidden, A-press or Itemfinder on (37,14) | `VioletCityHiddenHyperPotion` | `EVENT_VIOLET_CITY_HIDDEN_HYPER_POTION` |
| berry | `fruittree FRUITTREE_VIOLET_CITY` at (14,29) | `VioletCityFruitTree` | daily reset, not an `EVENT_*` |
None of these four are mentioned by the walkthrough section; they are listed
because a bot walking this map will pass them.
**Trainers**
None on the overworld map.
**Wild encounters**
- Grass: no `def_grass_wildmons VIOLET_CITY` entry in `data/wild/johto_grass.asm`.
- Water (`data/wild/johto_water.asm:225`): rate `2 percent`, L20 Poliwag,
L15 Poliwag, L20 Poliwhirl - Surf only, i.e. not reachable in this section.
- Fishing group `FISHGROUP_POND` (`data/maps/maps.asm:251`), table in
`data/wild/fish.asm`.
---
### MAP_VIOLET_POKECENTER_1F
- Script: `maps/VioletPokecenter1F.asm`
- Blocks: shared `VioletPokecenter1F_Blocks` label in `data/maps/blocks.asm:351`
(same blockset as the other Pokecenter 1Fs; no dedicated `.blk`)
- Header: `data/maps/maps.asm:256` -> `TILESET_POKECENTER`, `INDOOR`,
`LANDMARK_VIOLET_CITY`, `MUSIC_POKEMON_CENTER`, `FALSE`, `PALETTE_DAY`,
`FISHGROUP_SHORE`
- Dimensions / attributes: `constants/map_constants.asm:235`
(`map_const VIOLET_POKECENTER_1F, 5, 4`) = 10x8 cells;
`data/maps/attributes.asm:538`
- Connections: none
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `VIOLET_CITY` | 5 |
| 2 | 4 | 7 | `VIOLET_CITY` | 5 |
| 3 | 0 | 7 | `POKECENTER_2F` | 1 |
**Coord events** / **BG events**: both empty.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `VIOLETPOKECENTER1F_NURSE` | `SPRITE_NURSE` | 3 | 1 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `VioletPokecenterNurse` (`jumpstd PokecenterNurseScript`) | -1 |
| `VIOLETPOKECENTER1F_SUPER_NERD` | `SPRITE_SUPER_NERD` | 7 | 6 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT`, radius 1/0, pal `PAL_NPC_GREEN` | `OBJECTTYPE_SCRIPT` | `VioletPokecenter1FSuperNerdScript` | -1 |
| `VIOLETPOKECENTER1F_GENTLEMAN` | `SPRITE_GENTLEMAN` | 1 | 4 | `SPRITEMOVEDATA_SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT` | `VioletPokecenter1FGentlemanScript` | -1 |
| `VIOLETPOKECENTER1F_YOUNGSTER` | `SPRITE_YOUNGSTER` | 8 | 1 | `SPRITEMOVEDATA_STANDING_DOWN`, pal `PAL_NPC_RED` | `OBJECTTYPE_SCRIPT` | `VioletPokecenter1FYoungsterScript` | -1 |
| `VIOLETPOKECENTER1F_ELMS_AIDE` | `SPRITE_SCIENTIST` | 4 | 3 | `SPRITEMOVEDATA_STANDING_DOWN`, pal `PAL_NPC_BLUE` | `OBJECTTYPE_SCRIPT` | `VioletPokecenter1F_ElmsAideScript` | `EVENT_ELMS_AIDE_IN_VIOLET_POKEMON_CENTER` |
**Scripts of interest**
- `VioletPokecenterNurse` -> `jumpstd PokecenterNurseScript`: the only beat the
walkthrough uses here ("go back to the Pokemon Center to heal").
- `VioletPokecenter1F_ElmsAideScript` is **not** reachable during this section:
`EVENT_ELMS_AIDE_IN_VIOLET_POKEMON_CENTER` is set at new game
(`engine/events/std_scripts.asm:468`), which masks the object, and is only
cleared by Elm's `.assistant` phone call
(`engine/phone/scripts/elm.asm:84`) queued by beating Falkner. It gives
`giveegg TOGEPI, EGG_LEVEL` and belongs to the next section.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_ELMS_AIDE_IN_VIOLET_POKEMON_CENTER` (1792) | `constants/event_flags.asm:1186` | set in `engine/events/std_scripts.asm:468`, cleared in `engine/phone/scripts/elm.asm:84` | Set = aide hidden. Stays set for the whole of this section |
**Items / Trainers / Wild encounters**: none.
---
### MAP_VIOLET_GYM
- Script: `maps/VioletGym.asm`
- Blocks: `maps/VioletGym.blk` (`data/maps/blocks.asm:734`)
- Header: `data/maps/maps.asm:253` -> `TILESET_ELITE_FOUR_ROOM`, `INDOOR`,
`LANDMARK_VIOLET_CITY`, `MUSIC_GYM`, phone `TRUE`, `PALETTE_DAY`,
`FISHGROUP_SHORE`
- Dimensions / attributes: `constants/map_constants.asm:232`
(`map_const VIOLET_GYM, 5, 8`) = 10x16 cells;
`data/maps/attributes.asm:535`
- Connections: none
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 15 | `VIOLET_CITY` | 2 |
| 2 | 5 | 15 | `VIOLET_CITY` | 2 |
**Coord events** (`def_coord_events`)
None. There is no scripted trip-wire in the gym; Abe and Rod fire off eyesight
alone.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 3 | 13 | `BGEVENT_READ` | `VioletGymStatue` |
| 6 | 13 | `BGEVENT_READ` | `VioletGymStatue` |
`VioletGymStatue` branches on `checkflag ENGINE_ZEPHYRBADGE`: unbeaten ->
`jumpstd GymStatue1Script`; beaten -> `gettrainername STRING_BUFFER_4, FALKNER,
FALKNER1` + `jumpstd GymStatue2Script`. Reading it is a cheap way for a bot to
confirm the badge landed.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `VIOLETGYM_FALKNER` | `SPRITE_FALKNER` | 5 | 1 | `SPRITEMOVEDATA_STANDING_DOWN`, radius 0/0, pal `PAL_NPC_BLUE` | `OBJECTTYPE_SCRIPT`, sight 0 | `VioletGymFalknerScript` | -1 |
| `VIOLETGYM_YOUNGSTER1` | `SPRITE_YOUNGSTER` | 7 | 6 | `SPRITEMOVEDATA_STANDING_LEFT`, radius 2/0, pal `PAL_NPC_BLUE` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerBirdKeeperRod` | -1 |
| `VIOLETGYM_YOUNGSTER2` | `SPRITE_YOUNGSTER` | 2 | 10 | `SPRITEMOVEDATA_STANDING_RIGHT`, radius 2/0, pal `PAL_NPC_BLUE` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerBirdKeeperAbe` | -1 |
| `VIOLETGYM_GYM_GUIDE` | `SPRITE_GYM_GUIDE` | 7 | 13 | `SPRITEMOVEDATA_STANDING_DOWN`, radius 0/0, pal `PAL_NPC_RED` | `OBJECTTYPE_SCRIPT`, sight 0 | `VioletGymGuideScript` | -1 |
Walking in at (4,15)/(5,15) and heading north, **Abe** (facing right at y=10) is
met before **Rod** (facing left at y=6), which is the order the walkthrough
gives even though it names Abe first and Rod second.
**Scripts of interest**
- `VioletGymFalknerScript` (`56:412f`):
`faceplayer`, `opentext`, `checkevent EVENT_BEAT_FALKNER` /
`iftrue .FightDone`. First pass: `writetext FalknerIntroText`,
`winlosstext FalknerWinLossText, 0`, `loadtrainer FALKNER, FALKNER1`,
`startbattle`, `reloadmapafterbattle`, `setevent EVENT_BEAT_FALKNER`,
`writetext ReceivedZephyrBadgeText`, `playsound SFX_GET_BADGE`, `waitsfx`,
**`setflag ENGINE_ZEPHYRBADGE`**, `readvar VAR_BADGES`,
`scall VioletGymActivateRockets`.
Then `.FightDone` (`56:4159`): `checkevent EVENT_GOT_TM31_MUD_SLAP` /
`iftrue .SpeechAfterTM`; otherwise
`setevent EVENT_BEAT_BIRD_KEEPER_ROD`, `setevent EVENT_BEAT_BIRD_KEEPER_ABE`
(the two gym trainers are force-flagged so they never re-trigger),
`setmapscene ELMS_LAB, SCENE_ELMSLAB_NOOP`,
`specialphonecall SPECIALCALL_ASSISTANT`,
`writetext FalknerZephyrBadgeText`, `promptbutton`,
`verbosegiveitem TM_MUD_SLAP`, `iffalse .NoRoomForMudSlap`,
`setevent EVENT_GOT_TM31_MUD_SLAP`, `writetext FalknerTMMudSlapText`.
Unlike Sage Li, this one **does** check `verbosegiveitem`'s return, so a full
bag leaves `EVENT_GOT_TM31_MUD_SLAP` clear and the TM is offered again on the
next talk.
- `VioletGymActivateRockets` (`56:4185`): `ifequal 7, .RadioTowerRockets`,
`ifequal 6, .GoldenrodRockets` against the `VAR_BADGES` count. With one badge
this is a no-op; it exists so that a late Violet clear still arms the Rocket
arcs.
- `TrainerBirdKeeperRod` (`56:4194`), `TrainerBirdKeeperAbe` (`56:41a8`):
standard `trainer` headers + `endifjustbattled` after-battle text.
- `VioletGymGuideScript`: branches on `EVENT_BEAT_FALKNER`; pure text.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_FALKNER` (1213) | `constants/event_flags.asm:706` | read+written by `VioletGymFalknerScript`; read by `VioletGymGuideScript` | The gym-cleared flag |
| `ENGINE_ZEPHYRBADGE` (26) | `constants/engine_flags.asm:38` | `setflag` in `VioletGymFalknerScript`; `checkflag` in `VioletGymStatue`; `CheckBadge` in `engine/events/overworld.asm` | The badge bit. Gates out-of-battle Flash and the Gen 2 attack boost (`engine/battle/core.asm:6566`) |
| `EVENT_GOT_TM31_MUD_SLAP` (8) | `constants/event_flags.asm:14` | `VioletGymFalknerScript` | TM31 collected |
| `EVENT_BEAT_BIRD_KEEPER_ROD` (1019) | `constants/event_flags.asm:494` | `TrainerBirdKeeperRod`; also force-set by `VioletGymFalknerScript.FightDone` | Beaten |
| `EVENT_BEAT_BIRD_KEEPER_ABE` (1020) | `constants/event_flags.asm:495` | `TrainerBirdKeeperAbe`; also force-set by `.FightDone` | Beaten |
| `SCENE_ELMSLAB_NOOP` | `maps/ElmsLab.asm` (macro-generated) | `setmapscene ELMS_LAB, ...` in `.FightDone` | Disarms the lab's remaining scene script |
| `SPECIALCALL_ASSISTANT` | `constants/phone_constants.asm:47` | `specialphonecall` in `.FightDone`; consumed by `ElmPhoneCallerScript` (`engine/phone/scripts/elm.asm:67`) | Queues the Togepi-egg phone call |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| ZEPHYRBADGE | Automatic on beating Falkner | `VioletGymFalknerScript` (`setflag ENGINE_ZEPHYRBADGE`) | `ENGINE_ZEPHYRBADGE` |
| `TM_MUD_SLAP` (TM31) | `verbosegiveitem` after the badge text | `VioletGymFalknerScript.FightDone` | `EVENT_GOT_TM31_MUD_SLAP` |
**Trainers**
| const | class | id | party (`data/trainers/parties.asm`) | script label | rematch/phone |
|---|---|---|---|---|---|
| `BIRD_KEEPER`, `ROD` | `BIRD_KEEPER` (18) | `ROD` (1) | `BirdKeeperGroup` "ROD" (`0e:5f05` group base): L7 Pidgey, L7 Pidgey, `TRAINERTYPE_NORMAL` | `TrainerBirdKeeperRod` | none |
| `BIRD_KEEPER`, `ABE` | `BIRD_KEEPER` | `ABE` (2) | `BirdKeeperGroup` "ABE": L9 Spearow, `TRAINERTYPE_NORMAL` | `TrainerBirdKeeperAbe` | none |
| `FALKNER`, `FALKNER1` | `FALKNER` (1) | `FALKNER1` (1) | `FalknerGroup` (`0e:59c2`), `TRAINERTYPE_MOVES`: L7 Pidgey (TACKLE, MUD_SLAP), L9 Pidgeotto (TACKLE, MUD_SLAP, GUST) | `VioletGymFalknerScript` (`loadtrainer`) | listed in `data/trainers/leaders.asm:8` |
Prize money: Bird Keeper base 6 (`data/trainers/attributes.asm`) -> Rod
6 x 7 x 4 = 168, Abe 6 x 9 x 4 = 216. Falkner base 25 -> 25 x 9 x 4 = 900.
Falkner DVs `dn 9, 10, 7, 7` (`data/trainers/dvs.asm:5`); Bird Keeper
`dn 9, 8, 8, 8` (line 28).
**Wild encounters**
None (indoor gym).
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Sprout Tower 3F rival cutscene fires before you can reach Sage Li | `maps/SproutTower3F.asm` `coord_event 11, 9, SCENE_SPROUTTOWER3F_RIVAL_ENCOUNTER, SproutTower3FRivalScene` | Standing on (11,9) while `wSproutTower3FSceneID` == 0 | The scene's own `setscene SCENE_SPROUTTOWER3F_NOOP`. Not a hard block - the tile is on the only path north |
| HM05 Flash | `maps/SproutTower3F.asm:SageLiScript` | Beat `SAGE, LI` (`loadtrainer` + `startbattle`) | `verbosegiveitem HM_FLASH` + `setevent EVENT_GOT_HM05_FLASH` |
| Using Flash outside battle | `engine/events/overworld.asm` `FlashFunction.CheckUseFlash` (`03:48f1`): `ld de, ENGINE_ZEPHYRBADGE` / `farcall CheckBadge` / `jr c, .nozephyrbadge` | `ENGINE_ZEPHYRBADGE` set **and** `wTimeOfDayPalset == DARKNESS_PALSET` | Beat Falkner. Note the second half: even with the badge, Flash is refused on any map that is not a dark cave (`.notadarkcave` -> `FieldMoveFailed`) |
| ZEPHYRBADGE attack boost / obedience | `engine/battle/core.asm:6566` (the `and` mask that starts `(1 << ZEPHYRBADGE)`) | badge bit | Beat Falkner |
| Gym trainers Abe and Rod | `maps/VioletGym.asm` object events, `OBJECTTYPE_TRAINER` sight 3 on shared rows | eyesight only; they do **not** physically block the aisle | Beating them, or `EVENT_BEAT_BIRD_KEEPER_*` being force-set by `VioletGymFalknerScript.FightDone` |
| Falkner re-battle | `maps/VioletGym.asm:VioletGymFalknerScript` `checkevent EVENT_BEAT_FALKNER` / `iftrue .FightDone` | none | one-shot |
| Sage Li re-battle | `maps/SproutTower3F.asm:SageLiScript` `checkevent EVENT_GOT_HM05_FLASH` / `iftrue .GotFlash` | none | one-shot, keyed on the HM flag rather than on `EVENT_BEAT_SAGE_LI` |
| Sprout Tower is a `DUNGEON` | `data/maps/maps.asm:79-81` | - | `ESCAPE_ROPE` and Dig work here, which is what the walkthrough's "you may use that to get out of the tower" relies on |
Nothing in this section is gated on a field move, a key item, or an NPC standing
on a tile. The only hard requirement is winning battles.
## 4. Bot checklist
Coordinates are asm map cells. Port driver form:
`world:setMap("<MAP_CONST>", x, y, facing)`, as in
`tests/drivers/gold_walk_smoke.lua:58`.
| # | Map | Target | Intent | Precondition | Postcondition |
|---|---|---|---|---|---|
| 1 | `SPROUT_TOWER_1F` | warp 1 (9,15) | enter from `VIOLET_CITY` (23,5) | - | on 1F |
| 2 | `SPROUT_TOWER_1F` | (6,4) | walk onto warp 3 | - | on `SPROUT_TOWER_2F` at (6,4) |
| 3 | `SPROUT_TOWER_2F` | `SPROUTTOWER2F_SAGE1` at (14,4), sight 3 wide facing spin | step into his line, battle | `EVENT_BEAT_SAGE_NICO` clear | `EVENT_BEAT_SAGE_NICO` set |
| 4 | `SPROUT_TOWER_2F` | (17,3) | walk onto warp 3 | - | on `SPROUT_TOWER_1F` at (17,3) |
| 5 | `SPROUT_TOWER_1F` | ball at (16,7) | face + A | `EVENT_SPROUT_TOWER_1F_PARLYZ_HEAL` clear | `PARLYZ_HEAL` in bag, flag set |
| 6 | `SPROUT_TOWER_1F` | `SPROUTTOWER1F_SAGE3` at (2,1), facing RIGHT, sight 1 | step to (3,1), battle | `EVENT_BEAT_SAGE_CHOW` clear | `EVENT_BEAT_SAGE_CHOW` set |
| 7 | `SPROUT_TOWER_1F` | (2,6) | walk onto warp 4 | - | on `SPROUT_TOWER_2F` at (2,6) |
| 8 | `SPROUT_TOWER_2F` | ball at (3,1) | face + A | `EVENT_SPROUT_TOWER_2F_X_DEFEND` clear | `X_DEFEND` in bag, flag set |
| 9 | `SPROUT_TOWER_2F` | `SPROUTTOWER2F_SAGE2` at (3,15), facing UP, sight 4 | approach from above, battle | `EVENT_BEAT_SAGE_EDMOND` clear | `EVENT_BEAT_SAGE_EDMOND` set |
| 10 | `SPROUT_TOWER_2F` | (10,14) | walk onto warp 4 | - | on `SPROUT_TOWER_3F` at (10,14) |
| 11 | `SPROUT_TOWER_3F` | ball at (6,14) | face + A | `EVENT_SPROUT_TOWER_3F_POTION` clear | `POTION` in bag, flag set |
| 12 | `SPROUT_TOWER_3F` | `SPROUTTOWER3F_SAGE1` at (8,13), facing RIGHT, sight 3 | battle | `EVENT_BEAT_SAGE_JIN` clear | `EVENT_BEAT_SAGE_JIN` set |
| 13 | `SPROUT_TOWER_3F` | `SPROUTTOWER3F_SAGE4` at (11,11), facing LEFT, sight 3 | battle | `EVENT_BEAT_SAGE_NEAL` clear | `EVENT_BEAT_SAGE_NEAL` set |
| 14 | `SPROUT_TOWER_3F` | `SPROUTTOWER3F_SAGE2` at (8,8), facing DOWN, sight 2 | battle | `EVENT_BEAT_SAGE_TROY` clear | `EVENT_BEAT_SAGE_TROY` set |
| 15 | `SPROUT_TOWER_3F` | (11,9) | walk onto the coord event | scene id == `SCENE_SPROUTTOWER3F_RIVAL_ENCOUNTER` (0) | cutscene runs; player ends at (11,5); `EVENT_RIVAL_SPROUT_TOWER` set; scene id -> 1 |
| 16 | `SPROUT_TOWER_3F` | `SPROUTTOWER3F_SAGE3` at (10,2) | face + A, battle `SAGE, LI` | `EVENT_GOT_HM05_FLASH` clear; **bag must have room for an HM** | `HM_FLASH`, `EVENT_GOT_HM05_FLASH`, `EVENT_BEAT_SAGE_LI` |
| 17 | `SPROUT_TOWER_3F` | ball at (14,1) | face + A | `EVENT_SPROUT_TOWER_3F_ESCAPE_ROPE` clear | `ESCAPE_ROPE` in bag, flag set |
| 18 | `SPROUT_TOWER_3F` | (10,14) | warp 1, then 2F warp 1/2/3 down to 1F, then 1F warp 1 (9,15) | - | back on `VIOLET_CITY` at (23,5) |
| 19 | `VIOLET_CITY` | (31,25) | walk onto warp 5 | - | on `VIOLET_POKECENTER_1F` at (3,7) |
| 20 | `VIOLET_POKECENTER_1F` | `VIOLETPOKECENTER1F_NURSE` at (3,1) | face + A, accept heal | - | party healed |
| 21 | `VIOLET_POKECENTER_1F` | (3,7) | warp 1 | - | back on `VIOLET_CITY` at (31,25) |
| 22 | `VIOLET_CITY` | (18,17) | walk onto warp 2 | - | on `VIOLET_GYM` at (4,15) |
| 23 | `VIOLET_GYM` | `VIOLETGYM_YOUNGSTER2` at (2,10), facing RIGHT, sight 3 | enter his row, battle Abe | `EVENT_BEAT_BIRD_KEEPER_ABE` clear | flag set |
| 24 | `VIOLET_GYM` | `VIOLETGYM_YOUNGSTER1` at (7,6), facing LEFT, sight 3 | enter his row, battle Rod | `EVENT_BEAT_BIRD_KEEPER_ROD` clear | flag set |
| 25 | `VIOLET_GYM` | (4,15) -> `VIOLET_CITY` -> (31,25) PC | optional re-heal before the leader | - | party healed |
| 26 | `VIOLET_GYM` | `VIOLETGYM_FALKNER` at (5,1) | face + A, battle | `EVENT_BEAT_FALKNER` clear | `EVENT_BEAT_FALKNER`, `ENGINE_ZEPHYRBADGE`, Rod/Abe flags force-set, `SCENE_ELMSLAB_NOOP`, `SPECIALCALL_ASSISTANT` queued |
| 27 | `VIOLET_GYM` | same NPC, second talk if bag was full | face + A | `EVENT_GOT_TM31_MUD_SLAP` clear | `TM_MUD_SLAP`, `EVENT_GOT_TM31_MUD_SLAP` |
| 28 | `VIOLET_GYM` | bg_event (3,13) or (6,13) | face + A (verification only) | - | `GymStatue2Script` text = badge confirmed |
Optional pickups on the way through, none of which the walkthrough mentions:
`VIOLET_CITY` PP Up (4,1), Rare Candy (35,5), hidden Hyper Potion (37,14),
berry tree (14,29).
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Map load, block grid, warps for all six maps (data-driven from the ROM) | `src/world/gen2/Map.lua`, `src/import/RomExtractorGen2.lua` (`readMapGroupEntry`, `mapNameByIds`) | implemented - ids are the same `MAP_*` names, see `tools/rom_manifest_gold.json` -> `constants.mapGroups` |
| `MAPCALLBACK_NEWMAP` -> `setflag ENGINE_FLYPOINT_VIOLET` | `src/world/gen2/World.lua` (callback dispatch), driver `tests/drivers/gold_map_callbacks.lua` | implemented |
| Fly point 66 = Violet | `src/world/gen2/FieldMoves.lua:346` (`{ landmark = "LANDMARK_VIOLET_CITY", spawn = "SPAWN_VIOLET", flag = 66 }`) | implemented |
| Script VM for every `maps/*.asm` script in this section | `src/script/gen2/Vm.lua`, `src/script/gen2/Opcodes.lua` | implemented - scripts are disassembled from the ROM, not hand-ported, so `SageLiScript` / `VioletGymFalknerScript` run as-is |
| `coord_event` + scene ids (the 3F rival trip-wire) | `src/world/gen2/World.lua:5013` (coord event scan), `:1183` / `:5026` (scene scripts) | implemented |
| Cutscene opcodes used by `SproutTower3FRivalScene` (`showemote`, `earthquake`, `applymovement`, `turnobject`, `disappear`, `setscene`, `playmusic`) | `src/script/gen2/Opcodes.lua:122/125`, `src/world/gen2/World.lua:1006/1528/647`, `src/script/gen2/Movement.lua` | implemented |
| `special FadeOutMusic` / `FadeOutToBlack` / `ReloadSpritesNoPalettes` / `FadeInFromBlack` / `RestartMapMusic` | `src/script/gen2/Specials.lua` | implemented |
| Object masking by event flag (rival, item balls) | `src/world/gen2/Events.lua` (`objectVisible`), documented against `CheckObjectFlag` | implemented |
| Trainer objects: eyesight, approach walk, seen text, battle, beat flag | `src/world/gen2/Trainers.lua`, `src/world/gen2/World.lua:5205-5253`, driver `tests/drivers/gold_trainer_smoke.lua` | implemented |
| `loadtrainer` / `startbattle` for Sage Li and Falkner | `src/script/gen2/Vm.lua`, `src/battle/gen2/Battle.lua` | implemented |
| Gym leader battle music | `src/battle/gen2/BattleMusic.lua:24` (`FALKNER = true`) | implemented |
| Prize money (Falkner 900 = 25 x 9 x 4) | `src/battle/gen2/Prize.lua`, `tests/gen2_prize_test.lua` | implemented |
| `setflag ENGINE_ZEPHYRBADGE` (id 26) and `readvar VAR_BADGES` | `src/script/gen2/Vm.lua:670`, `src/world/gen2/World.lua:117/1240`, `tests/gen2_world_test.lua:1783` | implemented |
| Flash badge gate (`FlashFunction.CheckUseFlash`) | `src/world/gen2/FieldMoves.lua:105` (`FLASH = "ZEPHYR"`), `tests/gen2_world_test.lua:755-781` | implemented, including the "refused before the darkness check" ordering |
| `verbosegiveitem` (HM05, TM31) incl. the `iffalse` no-room arm | `src/script/gen2/Vm.lua:490-500` | implemented |
| Hidden item at Violet City (37,14) | `src/world/gen2/HiddenItems.lua`, `src/world/gen2/World.lua:5285` | implemented |
| Berry tree `fruittree FRUITTREE_VIOLET_CITY` | `src/script/gen2/CallAsm.lua:75-77`, `src/core/gen2/Apricorns.lua:363` | implemented |
| `specialphonecall SPECIALCALL_ASSISTANT` | `src/script/gen2/Opcodes.lua:161`, `src/core/gen2/Phone.lua:390` | implemented |
| `giveegg TOGEPI` (Elm's aide, next section's payoff) | `src/script/gen2/Vm.lua:453`, driver `tests/drivers/gold_egg_hatch.lua` | implemented |
| Earl's `follow` / `stopfollow` escort | `src/script/gen2/Vm.lua:982` | implemented |
| **Poke Ball item pickup (`OBJECTTYPE_ITEMBALL`)** - Parlyz Heal, X Defend, Potion, Escape Rope, PP Up, Rare Candy | `src/import/RomExtractorGen2.lua:2969` writes `obj.itemball`; **nothing in `src/` reads it**, and `World:interact()` (`src/world/gen2/World.lua:5257`) has no itemball branch (trainer / strength boulder / `scriptKey` / bg sign / hidden item only). `src/script/gen2/CallAsm.lua:550` stubs `TryReceiveItem` out on purpose | **missing** - the balls render and are masked correctly, but pressing A on one does nothing. Every item ball in this section is unobtainable in the port today |
| Hand-ported per-map Lua for Sprout Tower / Violet Gym | none - by design | n/a (the port runs the ROM's own bytecode) |
## 6. Unresolved / verify by hand
1. **"X Accuracy" in the Sprout Tower item list.** The walkthrough's header
lists Escape Rope, HM05, Parlyz Heal, Potion, **X Accuracy**, and the prose
says "grab that X Defense above you". The asm has exactly one such ball,
`SproutTower2FXDefend` = `itemball X_DEFEND` at 2F (3,1). There is no
`X_ACCURACY` anywhere in `maps/SproutTower*.asm`. The header list is wrong;
the prose is right.
2. **"Go up to the item to the right of the monk. In reality, it is a...
Level 3 Rattata"** and the follow-up "Go back to the Rattata you faced
before, and now it's an Escape Rope." Nothing in `maps/SproutTower3F.asm`
supports this. The object at (14,1) is a plain
`object_event ... OBJECTTYPE_ITEMBALL, 0, SproutTower3FEscapeRope,
EVENT_SPROUT_TOWER_3F_ESCAPE_ROPE` whose script label is a bare
`itemball ESCAPE_ROPE`. There is no `wildbattle`, no `loadwildmon`, no
second object at that cell, and no branch on `EVENT_GOT_HM05_FLASH` anywhere
near it. This looks like the walkthrough importing the Gen 1 Rocket Hideout
Voltorb trick. **Treat the ball at (14,1) as an ordinary Escape Rope.**
3. **"Pokemon found in Sprout Tower: Rattata, Bellsprout, Gastly."**
`data/wild/johto_grass.asm` gives Rattata (morn/day) and Gastly + Rattata
(nite) for `SPROUT_TOWER_2F` and `SPROUT_TOWER_3F`, and **no table at all**
for `SPROUT_TOWER_1F`. Bellsprout is not a Sprout Tower wild encounter; the
Bellsprouts are the sages' party members.
4. **Sage Li's party order.** The walkthrough lists L7 Bellsprout, L10 Hoothoot,
L7 Bellsprout. `data/trainers/parties.asm` `SageGroup` "LI" is
`db 7, BELLSPROUT / db 7, BELLSPROUT / db 10, HOOTHOOT` - the Hoothoot is
last, not second. The 320G figure the walkthrough quotes only works with the
asm order, since `ComputeTrainerReward` uses the level of the **last** party
row (8 x 10 x 4 = 320).
5. **Walkthrough trainer order in the gym.** The text says "Head up to fight
your trainer battles first. Bird Keeper Abe ... Head up the path to the
second trainer. Bird Keeper Rod." Geometrically that is correct (Abe at
y=10 is south of Rod at y=6), but be aware the *object const* order in the
file is Rod first (`VIOLETGYM_YOUNGSTER1`), Abe second
(`VIOLETGYM_YOUNGSTER2`), which is the opposite of the walk order. Do not
index trainers by object const when following the prose.
6. **"You'll need the 1st badge to use it [Flash]".** True but incomplete:
`FlashFunction.CheckUseFlash` (`engine/events/overworld.asm`, `03:48f1`)
requires `ENGINE_ZEPHYRBADGE` **and** `wTimeOfDayPalset == DARKNESS_PALSET`.
With the badge, on any non-dark map, the move still fails through
`.notadarkcave` -> `FieldMoveFailed`. A bot should not treat "have badge" as
"Flash will work here".
7. **EXP values.** The walkthrough quotes per-Pokemon EXP figures (54, 108, 126,
217, ...). Those are computed from base experience and level at runtime
(`engine/battle/experience.asm` / `data/pokemon/base_stats/`), not stored
anywhere as a per-trainer constant, so they were not verified against a
single asm line here. The **money** figures were: all seven quoted values
match `base reward x last party level x 4`.
8. **"you should be around an hour into the game now"** and the video link -
prose, nothing to resolve.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,907 @@
# Section 04 - Slowpoke Well & Azalea Town Gym
Source: `../section-04-slowpoke-well-azalea-town-gym.txt`
Maps covered: `MAP_AZALEA_TOWN`, `MAP_KURTS_HOUSE`, `MAP_AZALEA_POKECENTER_1F`,
`MAP_SLOWPOKE_WELL_B1F`, `MAP_SLOWPOKE_WELL_B2F` (off-route, Surf-only),
`MAP_AZALEA_GYM`. `MAP_AZALEA_MART` is referenced for its stock only
(`data/items/marts.asm` `MartAzalea`); its map file was not transcribed.
Badges / key milestones in this section: **HIVEBADGE** (`ENGINE_HIVEBADGE`),
`EVENT_CLEARED_SLOWPOKE_WELL`, `EVENT_BEAT_BUGSY`, `TM_FURY_CUTTER` (TM49),
`LURE_BALL`, `SUPER_POTION`, `WHT_APRICORN`, hidden `FULL_HEAL`. Clearing the
well is what unlocks the Gym door and arms the Azalea rival battle for the next
section.
Coordinate convention throughout: the raw asm x/y from
`warp_event` / `coord_event` / `bg_event` / `object_event`, i.e. walk cells
(two 8x8 tiles per side, two cells per map block). `macros/scripts/maps.asm`
adds the +4 border internally; the numbers below are the pre-border values as
written in the map file.
---
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 1 | `MAP_AZALEA_TOWN` | `maps/AzaleaTown.asm` | west edge of Route 33 (`connection east, Route33, ROUTE_33, 0` in `data/maps/attributes.asm`) | warp 4, `warp_event 9, 5, KURTS_HOUSE, 1` | Rocket guard is standing on the well; head west then north to Kurt's house |
| 2 | `MAP_KURTS_HOUSE` | `maps/KurtsHouse.asm` | warp 1 (`3, 7`) | warps 1/2 (`3, 7` / `4, 7`) back to `AZALEA_TOWN` warp 4 | `Kurt1` first-visit arm: Kurt storms off to the well and sets `EVENT_AZALEA_TOWN_SLOWPOKETAIL_ROCKET`, which hides the guard |
| 3 | `MAP_AZALEA_POKECENTER_1F` | `maps/AzaleaPokecenter1F.asm` | `AZALEA_TOWN` warp 1 (`15, 9`) | warps 1/2 (`3, 7` / `4, 7`) | "heal at the Pokemon Center" |
| 4 | `MAP_AZALEA_TOWN` | `maps/AzaleaTown.asm` | Pokecenter warp | warp 6, `warp_event 31, 7, SLOWPOKE_WELL_B1F, 1` | walk east to the now-unguarded well |
| 5 | `MAP_SLOWPOKE_WELL_B1F` | `maps/SlowpokeWellB1F.asm` | warp 1 (`17, 15`) | **scripted** `warp KURTS_HOUSE, 3, 3` at the end of `TrainerGruntM1.Script` | four Rocket grunts, Super Potion, tailless Slowpoke |
| 6 | `MAP_KURTS_HOUSE` | `maps/KurtsHouse.asm` | script warp to `3, 3` | warps 1/2 | `Kurt1.ClearedSlowpokeWell` -> free `LURE_BALL` |
| 7 | `MAP_AZALEA_TOWN` | `maps/AzaleaTown.asm` | Kurt's House warp | fruit tree object at `8, 2`, then warp 1 (Pokecenter) | `WHT_APRICORN` from `WhiteApricornTree` / `FRUITTREE_AZALEA_TOWN`, then heal |
| 8 | `MAP_AZALEA_GYM` | `maps/AzaleaGym.asm` | `AZALEA_TOWN` warp 5, `warp_event 10, 15, AZALEA_GYM, 1` | warps 1/2 (`4, 15` / `5, 15`) | five gym trainers + Bugsy -> HIVEBADGE + TM49 |
Off-route in this section: `MAP_SLOWPOKE_WELL_B2F` (`maps/SlowpokeWellB2F.asm`)
hangs off `SLOWPOKE_WELL_B1F` warp 2 at `7, 11`, but the only land approach to
that ladder is across water (see the derived grid under B1F) - it is a Surf
return trip, not part of this visit, and holds the King's Rock and TM Rain Dance.
Spills into the next section: `AzaleaTown_MapEvents` has
`coord_event 5, 10` / `coord_event 5, 11` for `SCENE_AZALEATOWN_RIVAL_BATTLE`.
`TrainerGruntM1.Script` arms that scene with
`setmapscene AZALEA_TOWN, SCENE_AZALEATOWN_RIVAL_BATTLE`, so the rival ambush
fires the first time you walk west toward the Ilex Forest gate. That battle and
Ilex Forest belong to section 05.
## 2. Maps
### MAP_AZALEA_TOWN
- Script: `maps/AzaleaTown.asm`
- Blocks: `maps/AzaleaTown.blk`
- Header: `data/maps/maps.asm:232` -> `TILESET_JOHTO_MODERN`, `TOWN`,
`LANDMARK_AZALEA_TOWN`, `MUSIC_AZALEA_TOWN`, phone `FALSE`, `PALETTE_AUTO`,
`FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:213` -> `map_const AZALEA_TOWN, 20, 9`
(20x9 blocks = 40x18 walk cells)
- Connections (`data/maps/attributes.asm:132`): `map_attributes AzaleaTown, AZALEA_TOWN, $05`,
`connection west, Route34, ROUTE_34, -18`, `connection east, Route33, ROUTE_33, 0`
- Scene var: `data/maps/scenes.asm:37` -> `scene_var AZALEA_TOWN, wAzaleaTownSceneID`
- Spawn / fly landing: `data/maps/spawn_points.asm:31` -> `spawn AZALEA_TOWN, 15, 10`;
flypoint row `data/maps/flypoints.asm:8` -> `db LANDMARK_AZALEA_TOWN, SPAWN_AZALEA`
- Map scripts: `def_scene_scripts` = `AzaleaTownNoop1Scene` (`SCENE_AZALEATOWN_NOOP` = 0),
`AzaleaTownNoop2Scene` (`SCENE_AZALEATOWN_RIVAL_BATTLE` = 1);
`def_callbacks` = `callback MAPCALLBACK_NEWMAP, AzaleaTownFlypointCallback`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 15 | 9 | `AZALEA_POKECENTER_1F` | 1 |
| 2 | 21 | 13 | `CHARCOAL_KILN` | 1 |
| 3 | 21 | 5 | `AZALEA_MART` | 2 |
| 4 | 9 | 5 | `KURTS_HOUSE` | 1 |
| 5 | 10 | 15 | `AZALEA_GYM` | 1 |
| 6 | 31 | 7 | `SLOWPOKE_WELL_B1F` | 1 |
| 7 | 2 | 10 | `ILEX_FOREST_AZALEA_GATE` | 3 |
| 8 | 2 | 11 | `ILEX_FOREST_AZALEA_GATE` | 4 |
**Coord events** (`def_coord_events`)
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_AZALEATOWN_RIVAL_BATTLE` | 5 | 10 | `AzaleaTownRivalBattleScene1` | rival ambush, approach from the east; next section |
| `SCENE_AZALEATOWN_RIVAL_BATTLE` | 5 | 11 | `AzaleaTownRivalBattleScene2` | same battle, second entry row; next section |
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 19 | 9 | `BGEVENT_READ` | `AzaleaTownSign` |
| 10 | 9 | `BGEVENT_READ` | `KurtsHouseSign` |
| 14 | 15 | `BGEVENT_READ` | `AzaleaGymSign` |
| 29 | 7 | `BGEVENT_READ` | `SlowpokeWellSign` |
| 19 | 13 | `BGEVENT_READ` | `CharcoalKilnSign` |
| 16 | 9 | `BGEVENT_READ` | `AzaleaTownPokecenterSign` (`jumpstd PokecenterSignScript`) |
| 22 | 5 | `BGEVENT_READ` | `AzaleaTownMartSign` (`jumpstd MartSignScript`) |
| 3 | 9 | `BGEVENT_READ` | `AzaleaTownIlextForestSign` |
| 31 | 6 | `BGEVENT_ITEM` | `AzaleaTownHiddenFullHeal` -> `hiddenitem FULL_HEAL, EVENT_AZALEA_TOWN_HIDDEN_FULL_HEAL` |
**Object events** (`def_object_events`) - `object_const_def` is `const_def 2`, so
the first const is 2 and row N carries const N+1.
| const | sprite | x | y | movement (radius x,y / hours / pal) | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| 2 `AZALEATOWN_AZALEA_ROCKET1` | `SPRITE_AZALEA_ROCKET` | 31 | 9 | `STANDING_DOWN` 0,0 / -1,-1 / 0 | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaTownRocket1Script` | `EVENT_AZALEA_TOWN_SLOWPOKETAIL_ROCKET` |
| 3 `AZALEATOWN_GRAMPS` | `SPRITE_GRAMPS` | 21 | 9 | `WANDER` 1,2 / -1,-1 / 0 | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaTownGrampsScript` | -1 |
| 4 `AZALEATOWN_TEACHER` | `SPRITE_TEACHER` | 15 | 13 | `WALK_UP_DOWN` 0,2 / -1,-1 / `PAL_NPC_BLUE` | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaTownTeacherScript` | -1 |
| 5 `AZALEATOWN_YOUNGSTER` | `SPRITE_YOUNGSTER` | 7 | 9 | `WALK_LEFT_RIGHT` 1,0 / -1,-1 / `PAL_NPC_GREEN` | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaTownYoungsterScript` | -1 |
| 6 `AZALEATOWN_SLOWPOKE1` | `SPRITE_SLOWPOKE` | 8 | 17 | `STILL` | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaTownSlowpokeScript` | `EVENT_AZALEA_TOWN_SLOWPOKES` |
| 7 `AZALEATOWN_SLOWPOKE2` | `SPRITE_SLOWPOKE` | 18 | 9 | `STILL` | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaTownSlowpokeScript` | `EVENT_AZALEA_TOWN_SLOWPOKES` |
| 8 `AZALEATOWN_SLOWPOKE3` | `SPRITE_SLOWPOKE` | 29 | 9 | `STILL` | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaTownSlowpokeScript` | `EVENT_AZALEA_TOWN_SLOWPOKES` |
| 9 `AZALEATOWN_SLOWPOKE4` | `SPRITE_SLOWPOKE` | 15 | 15 | `STILL` | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaTownSlowpokeScript` | `EVENT_AZALEA_TOWN_SLOWPOKES` |
| 10 `AZALEATOWN_FRUIT_TREE` | `SPRITE_FRUIT_TREE` | 8 | 2 | `STILL` | `OBJECTTYPE_SCRIPT`, 0 | `WhiteApricornTree` | -1 |
| 11 `AZALEATOWN_RIVAL` | `SPRITE_AZALEA_ROCKET` | 11 | 10 | `STANDING_LEFT` | `OBJECTTYPE_SCRIPT`, 0 | `ObjectEvent` | `EVENT_RIVAL_AZALEA_TOWN` |
| 12 `AZALEATOWN_AZALEA_ROCKET3` | `SPRITE_AZALEA_ROCKET` | 10 | 16 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaTownRocket2Script` | `EVENT_SLOWPOKE_WELL_ROCKETS` |
`SPRITE_AZALEA_ROCKET` (`constants/sprite_constants.asm:156`, id `$f6`) is a
variable sprite. `engine/events/std_scripts.asm:539` seeds it as
`variablesprite SPRITE_AZALEA_ROCKET, SPRITE_ROCKET`; `TrainerGruntM1.Script`
in `maps/SlowpokeWellB1F.asm` re-points it with
`variablesprite SPRITE_AZALEA_ROCKET, SPRITE_RIVAL`, which is why const 11 draws
as the rival even though the row names the Rocket slot.
**Derived walkability** (from `maps/AzaleaTown.blk` + `data/tilesets/johto_modern_collision.asm`
+ `data/collision/collision_permissions.asm`; `.` land, `#` wall, `h` ledge hop,
`W` warp tile, columns are x, rows are y)
```
0123456789012345678901234567890123456789
0 ###############################.........
1 ######....#####################.........
2 ######....#####################.........
3 ######....##############################
4 ######..################################
5 ######..#W###########W######......######
6 ######hhh.hh########....####..###.######
7 #########.##########....####.##W#.######
8 ##...#......######............#.#.######
9 ######....#.###W##.#..............######
10 ##W.........................hhh.hh######
11 ##W.....................#######.########
12 ######..######......########......######
13 ######..######.....##W######............
14 ######..######..........####............
15 ######..##W####.........####............
16 ######..............####################
17 ######..............####################
```
Two chokepoints matter: `31, 9` is the only walkable cell that reaches `31, 8`
and then the well ladder at `31, 7`, and `10, 16` is the only walkable cell
adjacent to the Gym door at `10, 15` (`9, 15`, `11, 15` and `10, 14` are all
wall). The two Rocket objects sit exactly on those cells.
**Scripts of interest**
- `AzaleaTownFlypointCallback` - `MAPCALLBACK_NEWMAP`; `setflag ENGINE_FLYPOINT_AZALEA`,
`endcallback`. Entering the map once is enough to register the fly point.
- `AzaleaTownRocket1Script` / `AzaleaTownRocket2Script` -
`jumptextfaceplayer AzaleaTownRocket1Text` / `...Rocket2Text`. Pure flavour;
neither one gates anything by script. They block by standing on tiles.
- `AzaleaTownGrampsScript` - `checkevent EVENT_CLEARED_SLOWPOKE_WELL` /
`iftrue .ClearedWell`, then one of `AzaleaTownGrampsTextBefore` /
`AzaleaTownGrampsTextAfter`. Cheap read-only probe of well state.
- `WhiteApricornTree` - `fruittree FRUITTREE_AZALEA_TOWN`.
`constants/script_constants.asm:226` gives `FRUITTREE_AZALEA_TOWN ; 14` (hex),
and `data/items/fruit_trees.asm:23` is `db WHT_APRICORN ; AZALEA_TOWN`.
Once-per-day, tracked by the fruit-tree daily bit, not by an `EVENT_*`.
- `AzaleaTownHiddenFullHeal` - `hiddenitem FULL_HEAL, EVENT_AZALEA_TOWN_HIDDEN_FULL_HEAL`;
reached by facing the wall cell `31, 6` from `31, 7`/`31, 5` neighbours.
- `AzaleaTownRivalBattleScene1` / `...Scene2` / `AzaleaTownRivalBattleScript` -
next section. For completeness they `setevent EVENT_RIVAL_AZALEA_TOWN`,
branch on `EVENT_GOT_TOTODILE_FROM_ELM` / `EVENT_GOT_CHIKORITA_FROM_ELM`,
`loadtrainer RIVAL1, RIVAL1_2_TOTODILE|RIVAL1_2_CHIKORITA|RIVAL1_2_CYNDAQUIL`,
and end with `setscene SCENE_AZALEATOWN_NOOP`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_FLYPOINT_AZALEA` | `constants/engine_flags.asm:82` | set by `AzaleaTownFlypointCallback` | Fly destination unlocked on first entry |
| `EVENT_AZALEA_TOWN_SLOWPOKETAIL_ROCKET` | `constants/event_flags.asm:1180` | set by `Kurt1` (first-visit arm) and read by `KurtsGranddaughter.Lonely` | **set = the well guard at `31, 9` is hidden**; this is the well gate |
| `EVENT_SLOWPOKE_WELL_ROCKETS` | `constants/event_flags.asm:1182` | set by the `disappear` calls in `TrainerGruntM1.Script` | **set = the Rocket at `10, 16` is hidden**; this is the Gym gate |
| `EVENT_AZALEA_TOWN_SLOWPOKES` | `constants/event_flags.asm:1179` | `clearevent` in `TrainerGruntM1.Script` | cleared = the four town Slowpoke appear |
| `EVENT_CLEARED_SLOWPOKE_WELL` | `constants/event_flags.asm:52` | set in `TrainerGruntM1.Script`; read by `AzaleaTownGrampsScript`, `Kurt1`, `KurtsGranddaughter`, `KurtsHouseKurtCallback` | the section-4 milestone flag |
| `EVENT_RIVAL_AZALEA_TOWN` | `constants/event_flags.asm:1121` | set by `AzaleaTownRivalBattleScript` | next section |
| `EVENT_AZALEA_TOWN_HIDDEN_FULL_HEAL` | `constants/event_flags.asm:187` | `hiddenitem` | one-shot hidden item |
| `SCENE_AZALEATOWN_NOOP` = 0, `SCENE_AZALEATOWN_RIVAL_BATTLE` = 1 | generated by `scene_script` in `macros/scripts/maps.asm:25` (`scene_const` + `EXPORT`) | `setscene` / `setmapscene` | scene id in `wAzaleaTownSceneID` |
Object visibility semantics, since everything above depends on it:
`engine/overworld/scripting.asm:879` `Script_appear` calls
`ApplyEventActionAppearDisappear` with `b = 0 ; clear`, and `:887`
`Script_disappear` with `b = 1 ; set`. **An object's `EVENT_*` flag SET means the
object is hidden.**
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `WHT_APRICORN` | talk to the tree object at `8, 2` | `WhiteApricornTree` -> `fruittree FRUITTREE_AZALEA_TOWN`, `data/items/fruit_trees.asm:23` | none (daily respawn) |
| `FULL_HEAL` | hidden, face `31, 6` | `AzaleaTownHiddenFullHeal` bg_event | `EVENT_AZALEA_TOWN_HIDDEN_FULL_HEAL` |
Azalea Mart stock (`data/items/marts.asm:71`, `MartAzalea`, 9 items):
`CHARCOAL`, `POKE_BALL`, `POTION`, `SUPER_POTION`, `ESCAPE_ROPE`, `REPEL`,
`ANTIDOTE`, `PARLYZ_HEAL`, `FLOWER_MAIL`.
**Trainers**
None on this map in this section (the rival at const 11 is section 05).
**Wild encounters**
`AZALEA_TOWN` has no entry in `data/wild/johto_grass.asm` or
`data/wild/johto_water.asm`. Fishing group is `FISHGROUP_SHORE`
(`data/maps/maps.asm:232`); headbutt set is
`data/wild/treemon_maps.asm:31` -> `treemon_map AZALEA_TOWN, TREEMON_SET_FOREST`.
---
### MAP_KURTS_HOUSE
- Script: `maps/KurtsHouse.asm`
- Blocks: `maps/KurtsHouse.blk`
- Header: `data/maps/maps.asm:229` -> `TILESET_TRADITIONAL_HOUSE`, `INDOOR`,
`LANDMARK_AZALEA_TOWN`, `MUSIC_AZALEA_TOWN`, phone `FALSE`, `PALETTE_DAY`,
`FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:210` -> `map_const KURTS_HOUSE, 8, 4`
- Connections: none (indoor)
- Map scripts: no scene scripts; `callback MAPCALLBACK_OBJECTS, KurtsHouseKurtCallback`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `AZALEA_TOWN` | 4 |
| 2 | 4 | 7 | `AZALEA_TOWN` | 4 |
**Coord events** (`def_coord_events`) - none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 6 | 1 | `BGEVENT_READ` | `KurtsHouseRadio` (`jumpstd Radio2Script`) |
| 8 | 0 | `BGEVENT_READ` | `KurtsHouseOakPhoto` |
| 9 | 0 | `BGEVENT_READ` | `KurtsHouseOakPhoto` |
| 5 | 1 | `BGEVENT_READ` | `KurtsHouseBookshelf` (`jumpstd DifficultBookshelfScript`) |
| 2 | 1 | `BGEVENT_READ` | `KurtsHouseBookshelf` |
| 3 | 1 | `BGEVENT_READ` | `KurtsHouseBookshelf` |
| 4 | 1 | `BGEVENT_READ` | `KurtsHouseCelebiStatue` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| 2 `KURTSHOUSE_KURT1` | `SPRITE_KURT` | 3 | 2 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT`, 0 | `Kurt1` | `EVENT_KURTS_HOUSE_KURT_1` |
| 3 `KURTSHOUSE_TWIN` | `SPRITE_TWIN` | 5 | 3 | `SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT`, 0 | `KurtsGranddaughter` | -1 |
| 4 `KURTSHOUSE_SLOWPOKE` | `SPRITE_SLOWPOKE` | 6 | 3 | `STILL` | `OBJECTTYPE_SCRIPT`, 0 | `KurtsHouseSlowpoke` | `EVENT_KURTS_HOUSE_SLOWPOKE` |
| 5 `KURTSHOUSE_KURT2` | `SPRITE_KURT` | 14 | 3 | `STANDING_UP` | `OBJECTTYPE_SCRIPT`, 0 | `Kurt1` | `EVENT_KURTS_HOUSE_KURT_2` |
Both Kurt objects run the same `Kurt1` script; the callback picks which one is
visible.
**Derived walkability** (`maps/KurtsHouse.blk` + `data/tilesets/traditional_house_collision.asm`)
```
0123456789012345
0 ################
1 #######...######
2 #...........####
3 .......##...#...
4 .......##...#...
5 ................
6 ................
7 ...WW...........
```
The post-well script warp lands you on `3, 3`, one cell south of `KURTSHOUSE_KURT1`
at `3, 2`, who faces down. Talk immediately, no walking required.
**Scripts of interest**
- `KurtsHouseKurtCallback` (`MAPCALLBACK_OBJECTS`) -
`checkevent EVENT_CLEARED_SLOWPOKE_WELL` / `iffalse .Done`;
then `checkflag ENGINE_KURT_MAKING_BALLS` / `iftrue .MakingBalls`.
Not making balls: `disappear KURTSHOUSE_KURT2`, `appear KURTSHOUSE_KURT1`.
Making balls: `disappear KURTSHOUSE_KURT1`, `appear KURTSHOUSE_KURT2`
(he moves to the workshop at `14, 3`).
- `Kurt1` - three arms, tested in this order:
1. `checkevent EVENT_KURT_GAVE_YOU_LURE_BALL` / `iftrue .GotLureBall`
2. `checkevent EVENT_CLEARED_SLOWPOKE_WELL` / `iftrue .ClearedSlowpokeWell`
3. fallthrough = **first visit**: `writetext KurtsHouseKurtMakingBallsMustWaitText`,
`special FadeOutMusic`, `setevent EVENT_AZALEA_TOWN_SLOWPOKETAIL_ROCKET`,
`readvar VAR_FACING` / `ifequal UP, .RunAround`, `turnobject PLAYER, DOWN`,
`playsound SFX_FLY`,
`applymovement KURTSHOUSE_KURT1, KurtsHouseKurtExitHouseMovement`
(5x `big_step DOWN`; the `.RunAround` variant is
`KurtsHouseKurtGoAroundPlayerThenExitHouseMovement`, `big_step RIGHT` then
5x `big_step DOWN`), `disappear KURTSHOUSE_KURT1`, `special RestartMapMusic`.
**This single `setevent` is what removes the well guard.**
- `.ClearedSlowpokeWell`: `writetext KurtsHouseKurtHonoredToMakeBallsText`,
`promptbutton`, `verbosegiveitem LURE_BALL`, `iffalse .NoRoomForBall`,
`setevent EVENT_KURT_GAVE_YOU_LURE_BALL`, then falls into `.GotLureBall`.
- `.GotLureBall`: the apricorn loop. Checks `EVENT_TEMPORARY_UNTIL_MAP_RELOAD_2`,
`EVENT_TEMPORARY_UNTIL_MAP_RELOAD_1`, then the seven
`EVENT_GAVE_KURT_<colour>_APRICORN` in order RED, BLU, YLW, GRN, WHT, BLK, PNK;
if none, `checkitem` the seven apricorns, `special SelectApricornForKurt`,
set the matching event, `setevent EVENT_TEMPORARY_UNTIL_MAP_RELOAD_1`,
`setflag ENGINE_KURT_MAKING_BALLS`. WHT -> `verbosegiveitem FAST_BALL`
(`.GiveFastBall`) the next day. Only one apricorn is held at a time, which
is exactly the reader comment quoted in the walkthrough.
- `KurtsGranddaughter` - branch order `EVENT_FAST_SHIP_FIRST_TIME`,
`EVENT_CLEARED_SLOWPOKE_WELL`, `EVENT_AZALEA_TOWN_SLOWPOKETAIL_ROCKET`. Read-only.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_KURTS_HOUSE_KURT_1` | `constants/event_flags.asm:1248` | `disappear`/`appear` in `Kurt1` + callback | set = house Kurt hidden (he is at the well) |
| `EVENT_KURTS_HOUSE_KURT_2` | `constants/event_flags.asm:1249` | callback; also `setevent` in `engine/events/std_scripts.asm:531` | set = workshop Kurt hidden |
| `EVENT_KURTS_HOUSE_SLOWPOKE` | `constants/event_flags.asm:1183` | `clearevent` in `TrainerGruntM1.Script` | cleared = the family Slowpoke is back |
| `EVENT_KURT_GAVE_YOU_LURE_BALL` | `constants/event_flags.asm:62` | set in `Kurt1.ClearedSlowpokeWell` | free ball already taken |
| `EVENT_GAVE_KURT_WHT_APRICORN` | `constants/event_flags.asm:263` | `Kurt1.Wht` / cleared in `.GiveFastBall` | apricorn currently in Kurt's hands |
| `ENGINE_KURT_MAKING_BALLS` | `constants/engine_flags.asm:97` (`wDailyFlags1`) | `setflag` in `.GaveKurtApricorns`, read by the callback and every `.Give*Ball` arm | the day-long wait; cleared by the daily reset, not by a script |
| `EVENT_TEMPORARY_UNTIL_MAP_RELOAD_1` / `_2` | `constants/event_flags.asm:5` / `:6` | `Kurt1` | per-visit conversation state, wiped on map reload |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `LURE_BALL` | talk to Kurt after clearing the well | `Kurt1.ClearedSlowpokeWell` -> `verbosegiveitem LURE_BALL` | `EVENT_KURT_GAVE_YOU_LURE_BALL` |
| `FAST_BALL` | give Kurt the `WHT_APRICORN`, return a day later | `Kurt1.GiveFastBall` -> `verbosegiveitem FAST_BALL` | clears `EVENT_GAVE_KURT_WHT_APRICORN` |
**Trainers** - none. **Wild encounters** - none (indoor).
---
### MAP_AZALEA_POKECENTER_1F
- Script: `maps/AzaleaPokecenter1F.asm`
- Blocks: not present in `maps/` (shared Pokecenter layout)
- Header: `data/maps/maps.asm:226` -> `TILESET_POKECENTER`, `INDOOR`,
`LANDMARK_AZALEA_TOWN`, `MUSIC_POKEMON_CENTER`, phone `FALSE`, `PALETTE_DAY`,
`FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:207` -> `map_const AZALEA_POKECENTER_1F, 5, 4`
- Map scripts: `def_scene_scripts` = `AzaleaPokecenter1FNoopScene` (marked `; unusable`)
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `AZALEA_TOWN` | 1 |
| 2 | 4 | 7 | `AZALEA_TOWN` | 1 |
| 3 | 0 | 7 | `POKECENTER_2F` | 1 |
**Coord events** / **BG events** - none.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| 2 `AZALEAPOKECENTER1F_NURSE` | `SPRITE_NURSE` | 3 | 1 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaPokecenter1FNurseScript` (`jumpstd PokecenterNurseScript`) | -1 |
| 3 `AZALEAPOKECENTER1F_GENTLEMAN` | `SPRITE_GENTLEMAN` | 9 | 6 | `WALK_UP_DOWN` 0,1 | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaPokecenter1FGentlemanScript` | -1 |
| 4 `AZALEAPOKECENTER1F_FISHING_GURU` | `SPRITE_FISHING_GURU` | 6 | 1 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaPokecenter1FFishingGuruScript` | -1 |
| 5 `AZALEAPOKECENTER1F_POKEFAN_F` | `SPRITE_POKEFAN_F` | 1 | 4 | `WANDER` 1,2 | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaPokecenter1FPokefanFScript` | -1 |
Healing is `jumpstd PokecenterNurseScript`; no flags are touched.
---
### MAP_SLOWPOKE_WELL_B1F
- Script: `maps/SlowpokeWellB1F.asm`
- Blocks: `maps/SlowpokeWellB1F.blk`
- Header: `data/maps/maps.asm:110` -> `TILESET_CAVE`, `CAVE`,
`LANDMARK_SLOWPOKE_WELL`, `MUSIC_DARK_CAVE`, phone `TRUE`, `PALETTE_NITE`,
`FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:97` -> `map_const SLOWPOKE_WELL_B1F, 10, 9`
(20x18 walk cells)
- Connections: none
- Map scripts: `def_scene_scripts` and `def_callbacks` are both empty
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 17 | 15 | `AZALEA_TOWN` | 6 |
| 2 | 7 | 11 | `SLOWPOKE_WELL_B2F` | 1 |
**Coord events** / **BG events** - none. Nothing trips automatically on this map;
every beat is a trainer sight-line or an A press.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement / pal | type, sight | script label | event flag |
|---|---|---|---|---|---|---|---|
| 2 `SLOWPOKEWELLB1F_ROCKET1` | `SPRITE_ROCKET` | 14 | 8 | `STANDING_RIGHT` | `OBJECTTYPE_TRAINER`, 1 | `TrainerGruntM29` | `EVENT_SLOWPOKE_WELL_ROCKETS` |
| 3 `SLOWPOKEWELLB1F_ROCKET2` | `SPRITE_ROCKET` | 5 | 2 | `STANDING_DOWN` | `OBJECTTYPE_TRAINER`, 1 | `TrainerGruntM1` | `EVENT_SLOWPOKE_WELL_ROCKETS` |
| 4 `SLOWPOKEWELLB1F_ROCKET3` | `SPRITE_ROCKET` | 5 | 6 | `STANDING_RIGHT` | `OBJECTTYPE_TRAINER`, 2 | `TrainerGruntM2` | `EVENT_SLOWPOKE_WELL_ROCKETS` |
| 5 `SLOWPOKEWELLB1F_ROCKET_GIRL` | `SPRITE_ROCKET_GIRL` | 13 | 2 | `STANDING_DOWN` / `PAL_NPC_RED` | `OBJECTTYPE_TRAINER`, 2 | `TrainerGruntF1` | `EVENT_SLOWPOKE_WELL_ROCKETS` |
| 6 `SLOWPOKEWELLB1F_SLOWPOKE1` | `SPRITE_SLOWPOKE` | 7 | 4 | `STILL` / `PAL_NPC_RED` | `OBJECTTYPE_SCRIPT`, 0 | `SlowpokeWellB1FSlowpokeWithMailScript` | `EVENT_SLOWPOKE_WELL_SLOWPOKES` |
| 7 `SLOWPOKEWELLB1F_SLOWPOKE2` | `SPRITE_SLOWPOKE` | 6 | 2 | `STILL` / `PAL_NPC_RED` | `OBJECTTYPE_SCRIPT`, 0 | `SlowpokeWellB1FTaillessSlowpokeScript` | `EVENT_SLOWPOKE_WELL_SLOWPOKES` |
| 8 `SLOWPOKEWELLB1F_KURT` | `SPRITE_KURT` | 16 | 14 | `STANDING_UP` | `OBJECTTYPE_SCRIPT`, 0 | `SlowpokeWellB1FKurtScript` | `EVENT_SLOWPOKE_WELL_KURT` |
| 9 `SLOWPOKEWELLB1F_BOULDER` | `SPRITE_BOULDER` | 3 | 2 | `STRENGTH_BOULDER` | `OBJECTTYPE_SCRIPT`, 0 | `SlowpokeWellB1FBoulder` (`jumpstd StrengthBoulderScript`) | -1 |
| 10 `SLOWPOKEWELLB1F_POKE_BALL` | `SPRITE_POKE_BALL` | 10 | 3 | `STILL` | `OBJECTTYPE_ITEMBALL`, 0 | `SlowpokeWellB1FSuperPotion` (`itemball SUPER_POTION`) | `EVENT_SLOWPOKE_WELL_B1F_SUPER_POTION` |
**Derived walkability** (`maps/SlowpokeWellB1F.blk` + `data/tilesets/cave_collision.asm`
+ `data/collision/collision_permissions.asm`; `~` = water, `L` = ladder/warp)
```
01234567890123456789
1 .################...
2 .#......#......##...
3 .#..#...#......##...
4 .#.#....#......##...
5 .#.#....###.##.##...
6 .#..#.......#...#...
7 .##.##########..#...
8 .#..#####....#..#...
9 .#.######....#..#...
10 .#.##........#..#...
11 .#.##..L.#####..###.
12 .#~~#....#~~~~....#.
13 .#~~###.##~~~~....#.
14 .#~~~~~~##~~~~....#.
15 .#~~~~~~##~~~~...L#.
```
Route through the well, matching the walkthrough beat for beat:
`17, 15` (entry ladder) -> Kurt at `16, 14` -> north up the `14-15` column to
`14, 8`/`15, 8` where `TrainerGruntM29` sees you -> continue north to the top
room `9..14, 2..4`, where `TrainerGruntF1` at `13, 2` faces down with sight 2 ->
Super Potion ball at `10, 3` -> back east/down through the gap at `11, 5` into
the long row-6 corridor -> west to `TrainerGruntM2` at `5, 6` (faces right,
sight 2) -> Slowpoke with mail at `7, 4`, tailless Slowpoke at `6, 2` ->
`TrainerGruntM1` at `5, 2` (faces down, sight 1).
The B2F ladder at `7, 11` sits in a chamber walled off along row 7 and reachable
only from `7, 13`, whose only non-wall neighbour outside the chamber is the water
at `7, 14`. **B2F is Surf-only**; the water is also the only route down from the
land at `2, 11`.
**Scripts of interest**
- `SlowpokeWellB1FKurtScript` - `jumptextfaceplayer SlowpokeWellB1FKurtText`.
Flavour only; Kurt does not gate anything here.
- `TrainerGruntM29` - `trainer GRUNTM, GRUNTM_29, EVENT_BEAT_ROCKET_GRUNTM_29, GruntM29SeenText, GruntM29BeatenText, 0, .Script`;
`.Script` is `endifjustbattled` + `GruntM29AfterBattleText`. Talking again is
the "they're chopping off Slowpoke tails" line the walkthrough mentions.
- `TrainerGruntF1` - `trainer GRUNTF, GRUNTF_1, EVENT_BEAT_ROCKET_GRUNTF_1, ...`,
same `endifjustbattled` shape.
- `TrainerGruntM2` - `trainer GRUNTM, GRUNTM_2, EVENT_BEAT_ROCKET_GRUNTM_2, ...`,
same shape.
- `TrainerGruntM1` - **the section's payload.**
`trainer GRUNTM, GRUNTM_1, EVENT_BEAT_ROCKET_GRUNTM_1, GruntM1SeenText, GruntM1BeatenText, 0, .Script`.
`.Script` has no `endifjustbattled`, so it runs immediately after the win:
`writetext TrainerGruntM1WhenTalkText`, `special FadeOutToBlack`,
`special ReloadSpritesNoPalettes`,
`disappear` on consts 2/3/4/5 (all four share `EVENT_SLOWPOKE_WELL_ROCKETS`,
so this is the flag that clears the Gym door in town), `pause 15`,
`special FadeInFromBlack`, `disappear SLOWPOKEWELLB1F_KURT`,
`moveobject SLOWPOKEWELLB1F_KURT, 11, 6`, `appear SLOWPOKEWELLB1F_KURT`,
`applymovement SLOWPOKEWELLB1F_KURT, KurtSlowpokeWellVictoryMovementData`
(4x `step LEFT`, `step UP`, 3x `step_sleep 8`, `step LEFT`, 2x `step UP`,
3x `step_sleep 8`, `turn_head LEFT`), `turnobject PLAYER, RIGHT`,
`writetext KurtLeaveSlowpokeWellText`, then the flag block:
```
setevent EVENT_CLEARED_SLOWPOKE_WELL
variablesprite SPRITE_AZALEA_ROCKET, SPRITE_RIVAL
setmapscene AZALEA_TOWN, SCENE_AZALEATOWN_RIVAL_BATTLE
clearevent EVENT_ILEX_FOREST_APPRENTICE
clearevent EVENT_ILEX_FOREST_FARFETCHD_1
setevent EVENT_CHARCOAL_KILN_FARFETCH_D
setevent EVENT_CHARCOAL_KILN_APPRENTICE
setevent EVENT_SLOWPOKE_WELL_SLOWPOKES
setevent EVENT_SLOWPOKE_WELL_KURT
clearevent EVENT_AZALEA_TOWN_SLOWPOKES
clearevent EVENT_KURTS_HOUSE_SLOWPOKE
clearevent EVENT_KURTS_HOUSE_KURT_1
```
then `special FadeOutToWhite`, `special HealParty`, `pause 15`,
`warp KURTS_HOUSE, 3, 3`, `end`. Note the free party heal, and note that the
Ilex Forest Farfetch'd quest is armed here, not in Ilex Forest.
- `SlowpokeWellB1FSlowpokeWithMailScript` - `faceplayer`, `cry SLOWPOKE`,
`SlowpokeWellB1FSlowpokeWithMailText`, `yesorno` / `iftrue .ReadMail` ->
`SlowpokeWellB1FSlowpokeMailText`. No flag, no item, purely optional.
- `SlowpokeWellB1FSuperPotion` - `itemball SUPER_POTION`, guarded by
`EVENT_SLOWPOKE_WELL_B1F_SUPER_POTION`.
- `SlowpokeWellB1FBoulder` - `jumpstd StrengthBoulderScript`; needs Strength,
which is not obtainable in this section.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_ROCKET_GRUNTM_29` | `constants/event_flags.asm:792` | `TrainerGruntM29` | grunt 1 beaten |
| `EVENT_BEAT_ROCKET_GRUNTF_1` | `constants/event_flags.asm:796` | `TrainerGruntF1` | grunt 2 beaten |
| `EVENT_BEAT_ROCKET_GRUNTM_2` | `constants/event_flags.asm:765` | `TrainerGruntM2` | grunt 3 beaten |
| `EVENT_BEAT_ROCKET_GRUNTM_1` | `constants/event_flags.asm:764` | `TrainerGruntM1` | grunt 4 (leader) beaten |
| `EVENT_SLOWPOKE_WELL_ROCKETS` | `constants/event_flags.asm:1182` | set via the four `disappear` calls | hides the well grunts **and** the Azalea Gym blocker |
| `EVENT_SLOWPOKE_WELL_SLOWPOKES` | `constants/event_flags.asm:1181` | `setevent` in `TrainerGruntM1.Script` | hides the two well Slowpoke |
| `EVENT_SLOWPOKE_WELL_KURT` | `constants/event_flags.asm:1250` | `setevent` in `TrainerGruntM1.Script` | hides well Kurt |
| `EVENT_CLEARED_SLOWPOKE_WELL` | `constants/event_flags.asm:52` | set here | section milestone |
| `EVENT_SLOWPOKE_WELL_B1F_SUPER_POTION` | `constants/event_flags.asm:1027` | itemball | Super Potion taken |
| `EVENT_ILEX_FOREST_APPRENTICE` / `EVENT_ILEX_FOREST_FARFETCHD_1` | `constants/event_flags.asm:1173` / `:1163` | `clearevent` here | Farfetch'd quest NPCs become visible in Ilex Forest (section 05) |
| `EVENT_CHARCOAL_KILN_FARFETCH_D` / `EVENT_CHARCOAL_KILN_APPRENTICE` | `constants/event_flags.asm:1175` / `:1176` | `setevent` here | they leave the Charcoal Kiln |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `SUPER_POTION` | ball at `10, 3` | `SlowpokeWellB1FSuperPotion` | `EVENT_SLOWPOKE_WELL_B1F_SUPER_POTION` |
**Trainers**
Money = `base reward` x level of the last enemy mon x 4. The x4 is
`engine/battle/read_trainer_party.asm:300` `ComputeTrainerReward`
(`base * wCurPartyLevel`) followed by the `ld c, 4` add loop and two
unconditional `call .DoubleReward` at `engine/battle/core.asm:2340-2361`.
`GRUNTM` / `GRUNTF` base reward is 10 (`data/trainers/attributes.asm:185`, `:395`).
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `GRUNTM_29` | `GRUNTM` | `constants/trainer_constants.asm:289` | `GruntMGroup` "GRUNT@", `TRAINERTYPE_NORMAL`: L9 `RATTATA`, L9 `RATTATA` | `TrainerGruntM29` | no (360 = 10x9x4) |
| `GRUNTF_1` | `GRUNTF` | `constants/trainer_constants.asm:648` | `GruntFGroup` "GRUNT@", `TRAINERTYPE_NORMAL`: L9 `ZUBAT`, L11 `EKANS` | `TrainerGruntF1` | no (440 = 10x11x4) |
| `GRUNTM_2` | `GRUNTM` | `constants/trainer_constants.asm:262` | `GruntMGroup` "GRUNT@", `TRAINERTYPE_NORMAL`: L7 `RATTATA`, L9 `ZUBAT`, L9 `ZUBAT` | `TrainerGruntM2` | no (360 = 10x9x4) |
| `GRUNTM_1` | `GRUNTM` | `constants/trainer_constants.asm:261` | `GruntMGroup` "GRUNT@", `TRAINERTYPE_NORMAL`: L14 `KOFFING` | `TrainerGruntM1` | no (560 = 10x14x4) |
**Wild encounters**
- Grass/cave, `data/wild/johto_grass.asm:582` `def_grass_wildmons SLOWPOKE_WELL_B1F`,
`db 2 percent, 2 percent, 2 percent` - **identical morn/day/nite lists**:
L5 `ZUBAT`, L6 `ZUBAT`, L7 `ZUBAT`, L6 `SLOWPOKE`, L8 `ZUBAT`, L8 `SLOWPOKE`,
L8 `SLOWPOKE`.
- Water, `data/wild/johto_water.asm:33` `def_water_wildmons SLOWPOKE_WELL_B1F`,
`db 2 percent`: L15 `SLOWPOKE`, L20 `SLOWPOKE`, L10 `SLOWPOKE`.
- Fishing: `FISHGROUP_LAKE` (`data/wild/fish.asm:14`) ->
`.Lake_Old` L10 `MAGIKARP`/`MAGIKARP`/`GOLDEEN`, `.Lake_Good` L20
`MAGIKARP`/`GOLDEEN`/`GOLDEEN` + `time_group 4`, `.Lake_Super` L40
`GOLDEEN`/`time_group 5`/`MAGIKARP`/`SEAKING`.
- Headbutt: `data/wild/treemon_maps.asm:47` -> `treemon_map SLOWPOKE_WELL_B1F, TREEMON_SET_ROCK`
(rock smash set; no headbutt trees underground).
---
### MAP_SLOWPOKE_WELL_B2F (off-route, Surf-gated)
- Script: `maps/SlowpokeWellB2F.asm`
- Blocks: `maps/SlowpokeWellB2F.blk`
- Header: `data/maps/maps.asm:111` -> `TILESET_CAVE`, `CAVE`,
`LANDMARK_SLOWPOKE_WELL`, `MUSIC_DARK_CAVE`, phone `TRUE`, `PALETTE_NITE`,
`FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:98` -> `map_const SLOWPOKE_WELL_B2F, 10, 9`
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 11 | `SLOWPOKE_WELL_B1F` | 2 |
**Coord events** / **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| 2 `SLOWPOKEWELLB2F_GYM_GUIDE` | `SPRITE_GYM_GUIDE` | 5 | 4 | `WANDER` 1,2 | `OBJECTTYPE_SCRIPT`, 1 | `SlowpokeWellB2FGymGuideScript` | -1 |
| 3 `SLOWPOKEWELLB2F_POKE_BALL` | `SPRITE_POKE_BALL` | 15 | 5 | `STILL` | `OBJECTTYPE_ITEMBALL`, 0 | `SlowpokeWellB2FTMRainDance` (`itemball TM_RAIN_DANCE`) | `EVENT_SLOWPOKE_WELL_B2F_TM_RAIN_DANCE` |
`SlowpokeWellB2FGymGuideScript`: `checkevent EVENT_GOT_KINGS_ROCK_IN_SLOWPOKE_WELL`
/ `iftrue .GotKingsRock`, else `verbosegiveitem KINGS_ROCK`, `iffalse .NoRoom`,
`setevent EVENT_GOT_KINGS_ROCK_IN_SLOWPOKE_WELL`
(`constants/event_flags.asm:124`).
Wild: `data/wild/johto_grass.asm:610` L19-23 `ZUBAT`/`SLOWPOKE`/`GOLBAT`
(same three time slots); `data/wild/johto_water.asm:40` is `4 percent` in Gold
(`IF DEF(_GOLD)`) / `2 percent` in Silver, L15 `SLOWPOKE`, L20 `SLOWPOKE`,
L20 `SLOWBRO`.
---
### MAP_AZALEA_GYM
- Script: `maps/AzaleaGym.asm`
- Blocks: `maps/AzaleaGym.blk`
- Header: `data/maps/maps.asm:230` -> `TILESET_ELITE_FOUR_ROOM`, `INDOOR`,
`LANDMARK_AZALEA_TOWN`, `MUSIC_GYM`, phone `TRUE`, `PALETTE_DAY`,
`FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:211` -> `map_const AZALEA_GYM, 5, 8`
(10x16 walk cells)
- Map scripts: `def_scene_scripts` and `def_callbacks` are both empty
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 15 | `AZALEA_TOWN` | 5 |
| 2 | 5 | 15 | `AZALEA_TOWN` | 5 |
**Coord events** - none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 3 | 13 | `BGEVENT_READ` | `AzaleaGymStatue` |
| 6 | 13 | `BGEVENT_READ` | `AzaleaGymStatue` |
`AzaleaGymStatue`: `checkflag ENGINE_HIVEBADGE` / `iftrue .Beaten`;
unbeaten -> `jumpstd GymStatue1Script`, beaten ->
`gettrainername STRING_BUFFER_4, BUGSY, BUGSY1` + `jumpstd GymStatue2Script`.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement / pal | type, sight | script label | event flag |
|---|---|---|---|---|---|---|---|
| 2 `AZALEAGYM_BUGSY` | `SPRITE_BUGSY` | 5 | 7 | `SPINRANDOM_SLOW` / `PAL_NPC_GREEN` | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaGymBugsyScript` | -1 |
| 3 `AZALEAGYM_BUG_CATCHER1` | `SPRITE_BUG_CATCHER` | 5 | 3 | `SPINRANDOM_FAST` / `PAL_NPC_BROWN` | `OBJECTTYPE_TRAINER`, 2 | `TrainerBugCatcherBenny` | -1 |
| 4 `AZALEAGYM_BUG_CATCHER2` | `SPRITE_BUG_CATCHER` | 8 | 8 | `STANDING_DOWN` / `PAL_NPC_BROWN` | `OBJECTTYPE_TRAINER`, 3 | `TrainerBugCatcherAl` | -1 |
| 5 `AZALEAGYM_BUG_CATCHER3` | `SPRITE_BUG_CATCHER` | 0 | 2 | `STANDING_DOWN` / `PAL_NPC_BROWN` | `OBJECTTYPE_TRAINER`, 3 | `TrainerBugCatcherJosh` | -1 |
| 6 `AZALEAGYM_TWIN1` | `SPRITE_TWIN` | 4 | 10 | `STANDING_DOWN` / `PAL_NPC_RED` | `OBJECTTYPE_TRAINER`, 1 | `TrainerTwinsAmyandmay1` | -1 |
| 7 `AZALEAGYM_TWIN2` | `SPRITE_TWIN` | 5 | 10 | `STANDING_DOWN` / `PAL_NPC_RED` | `OBJECTTYPE_TRAINER`, 1 | `TrainerTwinsAmyandmay2` | -1 |
| 8 `AZALEAGYM_GYM_GUIDE` | `SPRITE_GYM_GUIDE` | 7 | 13 | `STANDING_DOWN` / `PAL_NPC_RED` | `OBJECTTYPE_SCRIPT`, 0 | `AzaleaGymGuideScript` | -1 |
None of the gym objects carry an event flag, so they are always drawn; being
"beaten" is entirely the `trainer` macro's own `EVENT_BEAT_*` check.
**Derived walkability** (`maps/AzaleaGym.blk` + `data/tilesets/elite_four_room_collision.asm`)
```
0123456789
0 ##########
1 ##########
2 ..........
3 ..........
4 ..##..##..
5 ..#....#..
6 .#..##..#.
7 .#......#.
8 ..#....#..
9 ..##..##..
10 ..........
11 ..........
12 ##.#..#.##
13 ##.#..#.##
14 ###.....##
15 ###.WW..##
```
It is a spiral. Entry `4, 15` / `5, 15` -> `4, 14`..`7, 14` -> the two
entry stalks at columns 4-5 and 7 -> the row 10/11 ring -> up either side
column (0-1 or 8-9) -> row 2/3 -> in at `4, 4` / `5, 4` -> `4, 5`..`6, 5` ->
`3, 6` -> `3, 7` -> Bugsy at `5, 7`.
Derived from the grid plus the sight fields: only the **twins** sit on the
straight-line path (`4, 11` and `5, 11` are their sight cells), and even they
are dodgeable by taking the column-7 stalk from `7, 14` up to `7, 11`. Josh's
sight covers `0, 3`-`0, 5` so the left column is his; Al's covers `8, 9`-`8, 11`
so the column-8 approach is his; column 9 is clear all the way from row 11 to
row 3. Benny spins (`SPINRANDOM_FAST`, sight 2) at `5, 3`, so crossing row 2/3
near column 5 is a coin flip rather than a guarantee.
**Scripts of interest**
- `AzaleaGymBugsyScript` -
`faceplayer`, `opentext`, `checkevent EVENT_BEAT_BUGSY` / `iftrue .FightDone`;
otherwise `BugsyText_INeverLose`,
`winlosstext BugsyText_ResearchIncomplete, 0` (no loss text - losing is a
whiteout), `loadtrainer BUGSY, BUGSY1`, `startbattle`, `reloadmapafterbattle`,
`setevent EVENT_BEAT_BUGSY`, `Text_ReceivedHiveBadge`,
`playsound SFX_GET_BADGE`, `waitsfx`, **`setflag ENGINE_HIVEBADGE`**,
`readvar VAR_BADGES`, `scall AzaleaGymActivateRockets`.
`.FightDone`: `checkevent EVENT_GOT_TM49_FURY_CUTTER` / `iftrue .GotFuryCutter`,
then - and this is the bot-relevant part -
```
setevent EVENT_BEAT_TWINS_AMY_AND_MAY
setevent EVENT_BEAT_BUG_CATCHER_BENNY
setevent EVENT_BEAT_BUG_CATCHER_AL
setevent EVENT_BEAT_BUG_CATCHER_JOSH
```
so beating Bugsy retroactively marks the five gym trainers beaten. Then
`BugsyText_HiveBadgeSpeech`, `promptbutton`,
`verbosegiveitem TM_FURY_CUTTER`, `iffalse .NoRoomForFuryCutter`,
`setevent EVENT_GOT_TM49_FURY_CUTTER`, `BugsyText_FuryCutterSpeech`.
- `AzaleaGymActivateRockets` - `ifequal 7, .RadioTowerRockets`,
`ifequal 6, .GoldenrodRockets`, `end`. `VAR_BADGES`
(`constants/script_constants.asm:55`, id 7) is the badge count, which is 2
right after HIVEBADGE, so **this scall is a no-op at Bugsy**; it is the shared
gym-clear hook that only fires at the 6th and 7th badge.
- `TrainerTwinsAmyandmay1` / `TrainerTwinsAmyandmay2` - both use
`EVENT_BEAT_TWINS_AMY_AND_MAY`, so you fight **one** twin, whichever spots you
first, and the other drops to her after-battle line.
- `TrainerBugCatcherBenny` / `...Al` / `...Josh` - plain
`trainer BUG_CATCHER, <id>, EVENT_BEAT_BUG_CATCHER_<id>, ...` with
`endifjustbattled` after-scripts.
- `AzaleaGymGuideScript` - `checkevent EVENT_BEAT_BUGSY`; the pre-battle text is
the "bug Pokemon don't like fire, flying-type moves are super-effective" hint.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_BUGSY` | `constants/event_flags.asm:707` | `AzaleaGymBugsyScript` | leader beaten |
| `ENGINE_HIVEBADGE` | `constants/engine_flags.asm:39` (`wJohtoBadges`, `data/events/engine_flags.asm:47`) | `setflag` here; read by `CutFunction.CheckAble`, `TryCutOW`, `AzaleaGymStatue`, `engine/battle/core.asm:6566` (obedience) | **the badge that enables field Cut and L30 obedience** |
| `EVENT_GOT_TM49_FURY_CUTTER` | `constants/event_flags.asm:15` | `AzaleaGymBugsyScript` | TM49 already handed over |
| `EVENT_BEAT_TWINS_AMY_AND_MAY` | `constants/event_flags.asm:608` | both twin objects; force-set by `.FightDone` | one flag, two objects |
| `EVENT_BEAT_BUG_CATCHER_BENNY` | `constants/event_flags.asm:843` | `TrainerBugCatcherBenny`; force-set by `.FightDone` | |
| `EVENT_BEAT_BUG_CATCHER_AL` | `constants/event_flags.asm:844` | `TrainerBugCatcherAl`; force-set by `.FightDone` | |
| `EVENT_BEAT_BUG_CATCHER_JOSH` | `constants/event_flags.asm:845` | `TrainerBugCatcherJosh`; force-set by `.FightDone` | |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `TM_FURY_CUTTER` (TM49) | Bugsy hands it over after the badge speech | `AzaleaGymBugsyScript.FightDone` -> `verbosegiveitem TM_FURY_CUTTER` | `EVENT_GOT_TM49_FURY_CUTTER` |
**Trainers**
Base rewards: `BUG_CATCHER` 4 (`data/trainers/attributes.asm:215`),
`TWINS` 5 (`:365`), `BUGSY` 25 (`:17`). Money = base x last-mon level x 4.
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `AMYANDMAY1` | `TWINS` | `constants/trainer_constants.asm:621` | `TwinsGroup` TWINS(1) "AMY & MAY@", `TRAINERTYPE_NORMAL`: L10 `SPINARAK`, L10 `LEDYBA` | `TrainerTwinsAmyandmay1` | no (200 = 5x10x4) |
| `AMYANDMAY2` | `TWINS` | `constants/trainer_constants.asm:624` | `TwinsGroup` TWINS(4) "AMY & MAY@", `TRAINERTYPE_NORMAL`: L10 `LEDYBA`, L10 `SPINARAK` | `TrainerTwinsAmyandmay2` | no (200) |
| `JOSH` | `BUG_CATCHER` | `constants/trainer_constants.asm:319` | `BugCatcherGroup` BUG_CATCHER(7) "JOSH@", `TRAINERTYPE_NORMAL`: L13 `PARAS` | `TrainerBugCatcherJosh` | no (208 = 4x13x4) |
| `BENNY` | `BUG_CATCHER` | `constants/trainer_constants.asm:317` | `BugCatcherGroup` BUG_CATCHER(5) "BENNY@", `TRAINERTYPE_NORMAL`: L7 `WEEDLE`, L9 `KAKUNA`, L12 `BEEDRILL` | `TrainerBugCatcherBenny` | no (192 = 4x12x4) |
| `AL` | `BUG_CATCHER` | `constants/trainer_constants.asm:318` | `BugCatcherGroup` BUG_CATCHER(6) "AL@", `TRAINERTYPE_NORMAL`: L12 `CATERPIE`, L12 `WEEDLE` | `TrainerBugCatcherAl` | no (192 = 4x12x4) |
| `BUGSY1` | `BUGSY` (class 3, `constants/trainer_constants.asm:33`) | `:34` | `BugsyGroup` "BUGSY@", **`TRAINERTYPE_MOVES`**: L14 `METAPOD` (`TACKLE`, `STRING_SHOT`, `HARDEN`), L14 `KAKUNA` (`POISON_STING`, `STRING_SHOT`, `HARDEN`), L16 `SCYTHER` (`QUICK_ATTACK`, `LEER`, `FURY_CUTTER`) | `AzaleaGymBugsyScript` | no (1600 = 25x16x4) |
Bugsy's AI/items row (`data/trainers/attributes.asm:17`): `NO_ITEM, NO_ITEM`,
`AI_BASIC | AI_SETUP | AI_SMART | AI_AGGRESSIVE | AI_CAUTIOUS | AI_STATUS | AI_RISKY`,
`CONTEXT_USE | SWITCH_SOMETIMES`. Bug Catchers are `AI_BASIC | AI_SETUP | AI_STATUS`;
Twins are `NO_AI` with `SWITCH_OFTEN`.
**Wild encounters** - none (indoor).
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Rocket standing on the Slowpoke Well approach | `maps/AzaleaTown.asm` object const 2 at `31, 9`, flag `EVENT_AZALEA_TOWN_SLOWPOKETAIL_ROCKET`. `31, 9` is the only walkable neighbour of `31, 8`, which is the only walkable neighbour of the ladder at `31, 7` (derived from `maps/AzaleaTown.blk` + `data/tilesets/johto_modern_collision.asm`) | talk to Kurt once | `Kurt1` first-visit arm runs `setevent EVENT_AZALEA_TOWN_SLOWPOKETAIL_ROCKET`, which hides him (`Script_disappear` semantics, `engine/overworld/scripting.asm:887`) |
| Rocket standing on the Azalea Gym door | `maps/AzaleaTown.asm` object const 12 at `10, 16`, flag `EVENT_SLOWPOKE_WELL_ROCKETS`. `10, 16` is the only walkable neighbour of the gym warp at `10, 15` | clear Slowpoke Well | the four `disappear SLOWPOKEWELLB1F_ROCKET*` calls in `maps/SlowpokeWellB1F.asm` `TrainerGruntM1.Script` set that shared flag |
| Slowpoke Well B2F (King's Rock, TM Rain Dance) | `maps/SlowpokeWellB1F.asm` warp 2 at `7, 11`; the chamber's only outside neighbour is water at `7, 14` (derived from `maps/SlowpokeWellB1F.blk` + `data/tilesets/cave_collision.asm` + `data/collision/collision_permissions.asm`) | Surf (`ENGINE_FOGBADGE` + HM03) | out of section - Fog Badge is Morty |
| Boulder at Slowpoke Well `3, 2` | `SlowpokeWellB1FBoulder` -> `jumpstd StrengthBoulderScript` | Strength | out of section; nothing behind it is required |
| Field Cut (leaving Azalea west for Ilex Forest) | `engine/events/overworld.asm` `CutFunction.CheckAble` (`ld de, ENGINE_HIVEBADGE` / `call CheckBadge` / `jr c, .nohivebadge`, sym `03:47e1`) and `TryCutOW` (`CheckPartyMove` with `CUT`, then `ld de, ENGINE_HIVEBADGE` / `CheckEngineFlag`, sym `03:5193`) | HIVEBADGE **and** a party member that knows CUT | HIVEBADGE from `AzaleaGymBugsyScript`; HM01 itself comes from the Charcoal Kiln / Ilex Forest chain in section 05 |
| Pokemon obedience above L30 | `engine/battle/core.asm:6566` masks `(1 << ZEPHYRBADGE) | (1 << HIVEBADGE) | ...` | HIVEBADGE | same |
| Bugsy himself | `AzaleaGymBugsyScript` has no `checkevent` guard before the battle | nothing - the five gym trainers are optional | `.FightDone` force-sets all four of their beat events afterwards |
## 4. Bot checklist
Coordinates are `x, y` as written in the asm. "warp N" means the Nth
`warp_event` row of the source map.
1. `MAP_AZALEA_TOWN`, walk in from the Route 33 east connection. Post: engine
flag `ENGINE_FLYPOINT_AZALEA` set by `AzaleaTownFlypointCallback`
(`MAPCALLBACK_NEWMAP`). No input needed.
2. `MAP_AZALEA_TOWN`, walk to `9, 5` and step on it -> warp 4 ->
`MAP_KURTS_HOUSE` `3, 7`. Pre: none.
3. `MAP_KURTS_HOUSE`, walk to `3, 3`, face UP, press A on object const 2
(`KURTSHOUSE_KURT1`, `3, 2`). Pre: `EVENT_CLEARED_SLOWPOKE_WELL` clear and
`EVENT_KURT_GAVE_YOU_LURE_BALL` clear. Post:
`EVENT_AZALEA_TOWN_SLOWPOKETAIL_ROCKET` set, `EVENT_KURTS_HOUSE_KURT_1` set
(Kurt walks out). If the player is facing UP when the script starts, Kurt
takes the `.RunAround` movement instead - same result, one extra step.
4. `MAP_KURTS_HOUSE`, step on `3, 7` or `4, 7` -> `MAP_AZALEA_TOWN` warp 4 (`9, 5`).
5. Optional heal: `MAP_AZALEA_TOWN` `15, 9` -> `MAP_AZALEA_POKECENTER_1F`;
talk to object const 2 at `3, 1` (`jumpstd PokecenterNurseScript`); leave via
`3, 7`.
6. `MAP_AZALEA_TOWN`, walk east along row 10 to `31, 10`, north to `31, 9`
(now empty), `31, 8`, then step on `31, 7` -> warp 6 -> `MAP_SLOWPOKE_WELL_B1F`
`17, 15`. Optional: from `31, 7` face UP and press A for the hidden
`FULL_HEAL` at `31, 6` (post: `EVENT_AZALEA_TOWN_HIDDEN_FULL_HEAL`).
7. `MAP_SLOWPOKE_WELL_B1F`, optional A on const 8 (`16, 14`, Kurt).
8. Walk north along column 15 to `15, 8`. Trigger: const 2 `TrainerGruntM29`
(sight 1, faces right). Battle `GRUNTM` / `GRUNTM_29` (L9 Rattata x2).
Post: `EVENT_BEAT_ROCKET_GRUNTM_29`. Optional A again for the after-battle text.
9. Continue north column 14 to `14, 4`, then west into the top room. Trigger:
const 5 `TrainerGruntF1` at `13, 2` (sight 2, faces down) from `13, 3` or
`13, 4`. Battle `GRUNTF` / `GRUNTF_1` (L9 Zubat, L11 Ekans). Post:
`EVENT_BEAT_ROCKET_GRUNTF_1`.
10. Walk to `11, 3` -> `10, 3`, press A on const 10 -> `SUPER_POTION`. Post:
`EVENT_SLOWPOKE_WELL_B1F_SUPER_POTION`.
11. Go to `11, 4` -> `11, 5` -> `11, 6`, then west along row 6. Trigger: const 4
`TrainerGruntM2` at `5, 6` (sight 2, faces right) from `7, 6` or `6, 6`.
Battle `GRUNTM` / `GRUNTM_2` (L7 Rattata, L9 Zubat, L9 Zubat). Post:
`EVENT_BEAT_ROCKET_GRUNTM_2`.
12. Optional: `7, 5` face UP, A on const 6 at `7, 4` (Slowpoke with mail,
`yesorno` -> read). No flag.
13. Walk to `5, 3`. Trigger: const 3 `TrainerGruntM1` at `5, 2` (sight 1, faces
down). Battle `GRUNTM` / `GRUNTM_1` (L14 Koffing). Post:
`EVENT_BEAT_ROCKET_GRUNTM_1`, then `.Script` runs with no `endifjustbattled`,
so the whole cutscene plays automatically and ends with
`special HealParty` + `warp KURTS_HOUSE, 3, 3`. Post-flags to assert:
`EVENT_CLEARED_SLOWPOKE_WELL`, `EVENT_SLOWPOKE_WELL_ROCKETS`,
`EVENT_SLOWPOKE_WELL_SLOWPOKES`, `EVENT_SLOWPOKE_WELL_KURT`,
`EVENT_CHARCOAL_KILN_FARFETCH_D`, `EVENT_CHARCOAL_KILN_APPRENTICE`;
cleared: `EVENT_AZALEA_TOWN_SLOWPOKES`, `EVENT_KURTS_HOUSE_SLOWPOKE`,
`EVENT_KURTS_HOUSE_KURT_1`, `EVENT_ILEX_FOREST_APPRENTICE`,
`EVENT_ILEX_FOREST_FARFETCHD_1`; scene: `AZALEA_TOWN` -> `SCENE_AZALEATOWN_RIVAL_BATTLE`.
14. `MAP_KURTS_HOUSE` at `3, 3`, face UP, A on const 2. Pre:
`EVENT_CLEARED_SLOWPOKE_WELL` set, `EVENT_KURT_GAVE_YOU_LURE_BALL` clear.
Post: `LURE_BALL` in bag, `EVENT_KURT_GAVE_YOU_LURE_BALL` set. The script
then falls straight into the apricorn prompt - answer no unless you want to
hand over the White Apricorn you have not picked yet.
15. Leave via `3, 7`; in `MAP_AZALEA_TOWN` walk to `8, 3`, face UP, A on const 10
at `8, 2` -> `WHT_APRICORN` (daily). Optional: return to Kurt and give it
(`special SelectApricornForKurt`, post: `EVENT_GAVE_KURT_WHT_APRICORN` +
`ENGINE_KURT_MAKING_BALLS`), then come back a real in-game day later for
`FAST_BALL`.
16. Optional heal at the Pokecenter (`15, 9`).
17. `MAP_AZALEA_TOWN`, walk to `10, 17` -> `10, 16` (now empty) -> step on
`10, 15` -> warp 5 -> `MAP_AZALEA_GYM` `4, 15`.
18. `MAP_AZALEA_GYM`: optional A on const 8 at `7, 13` for the type hint.
Walk `4, 14` -> `4, 13` -> `4, 12` -> `4, 11`. Trigger: const 6
`TrainerTwinsAmyandmay1` at `4, 10` (sight 1). Battle `TWINS` / `AMYANDMAY1`
(L10 Spinarak, L10 Ledyba). Post: `EVENT_BEAT_TWINS_AMY_AND_MAY`, which also
retires const 7.
19. West along row 11 to `0, 11`, north up column 0 to `0, 5`. Trigger: const 5
`TrainerBugCatcherJosh` at `0, 2` (sight 3, faces down). Battle
`BUG_CATCHER` / `JOSH` (L13 Paras). Post: `EVENT_BEAT_BUG_CATCHER_JOSH`.
20. North to `0, 3`, east along row 3 toward `4, 3`. Trigger: const 3
`TrainerBugCatcherBenny` at `5, 3` (sight 2, spins). Battle
`BUG_CATCHER` / `BENNY` (L7 Weedle, L9 Kakuna, L12 Beedrill). Post:
`EVENT_BEAT_BUG_CATCHER_BENNY`.
21. East along row 2/3 to column 8-9, south to `8, 11`. Trigger: const 4
`TrainerBugCatcherAl` at `8, 8` (sight 3, faces down). Battle
`BUG_CATCHER` / `AL` (L12 Caterpie, L12 Weedle). Post:
`EVENT_BEAT_BUG_CATCHER_AL`.
22. Route into the middle: `4, 3` or `5, 3` row -> `4, 4` -> `4, 5` -> `3, 5` ->
`3, 6` -> `3, 7` -> `4, 7`, face RIGHT, A on const 2 at `5, 7`. Pre:
`EVENT_BEAT_BUGSY` clear. Battle `BUGSY` / `BUGSY1` (L14 Metapod, L14 Kakuna,
L16 Scyther). Post: `EVENT_BEAT_BUGSY`, `ENGINE_HIVEBADGE`, then the same
conversation continues and yields `TM_FURY_CUTTER` +
`EVENT_GOT_TM49_FURY_CUTTER` and force-sets the four gym-trainer events.
A bot that wants the badge fast can skip steps 19-21 entirely.
23. Leave via `4, 15` / `5, 15`. Next section starts when you walk west and hit
`coord_event 5, 10` / `5, 11`.
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Map data for all six maps (blocks, connections, warps, coord/bg/object events, `scriptKey`) | `src/import/RomExtractorGen2.lua` (`self:write("maps", out)`, line 987), consumed by `src/world/gen2/Map.lua` | implemented - `docs/gold-phase1.md` states all 368 maps extract |
| Collision / walkability (`COLL_*` quads, block -> cell) | `src/world/gen2/Map.lua` `Map:cellCollision`, `src/world/gen2/Permissions.lua` | implemented (quad order `ly*2 + lx` matches `gfx/tilesets.asm` `tilecoll`) |
| Warps between these maps | `src/world/gen2/World.lua` + `Map:warpAt`; driver `tests/drivers/gold_warp_scene.lua` | implemented |
| Map callbacks (`MAPCALLBACK_NEWMAP` flypoint, `MAPCALLBACK_OBJECTS` Kurt swap) | `src/world/gen2/World.lua`, driver `tests/drivers/gold_map_callbacks.lua` | implemented |
| Script VM opcodes used here (`checkevent`/`setevent`/`clearevent`, `checkflag`/`setflag`, `readvar`, `scall`, `jumpstd`, `special`, `warp`, `setscene`/`setmapscene`, `loadtrainer`/`startbattle`/`winlosstext`/`endifjustbattled`, `applymovement`, `moveobject`, `showemote`, `variablesprite`, `verbosegiveitem`, `yesorno`, `checkitem`, `promptbutton`, `fruittree`) | `src/script/gen2/Opcodes.lua`, `src/script/gen2/Vm.lua` | implemented - every opcode in this section's scripts has a row and a VM arm |
| Specials used here (`FadeOutToBlack`, `FadeInFromBlack`, `FadeOutToWhite`, `ReloadSpritesNoPalettes`, `HealParty`, `SelectApricornForKurt`, `FadeOutMusic`/`RestartMapMusic`) | `src/script/gen2/Specials.lua` | implemented (spot-checked `FadeOutToBlack`, `ReloadSpritesNoPalettes`, `HealParty`, `SelectApricornForKurt`) |
| Trainer sight lines / approach walk (the four grunts, the five gym trainers) | `src/world/gen2/Trainers.lua` (`Trainers.sees`, `Trainers.approach`), wired at `src/world/gen2/World.lua:5212-5264`; driver `tests/drivers/gold_trainer_smoke.lua` | implemented |
| Trainer parties + class attributes | `src/import/RomExtractorGen2.lua` (trainers), `src/world/gen2/Trainers.lua`, `src/battle/gen2/Battle.lua` (`BUGSY` in the gym-leader set, line 85) | implemented |
| Prize money (base x level x 4, Mom's split) | `src/battle/gen2/Prize.lua` | implemented, and it ports the `ld c, 4` loop and `.DoubleReward` faithfully |
| Badge storage + `VAR_BADGES` count | `src/core/gen2/Save.lua` (`player.badges`, `player.kantoBadges`), `src/world/gen2/World.lua:1241-1244`, `World:engineFlags` (`:1304`) | implemented |
| Cut gate on `ENGINE_HIVEBADGE` | `src/world/gen2/FieldMoves.lua` (`FieldMoves.BADGE.CUT = "HIVE"`, `cutFromMenu`, the overworld ask path) | implemented |
| Fly point `ENGINE_FLYPOINT_AZALEA` | `src/world/gen2/FieldMoves.lua:347` (`LANDMARK_AZALEA_TOWN` / `SPAWN_AZALEA` / flag 67) | implemented |
| Hidden item at `31, 6` (`BGEVENT_ITEM`) | `src/world/gen2/HiddenItems.lua` | implemented (module exists and the extractor carries `hiddenItem = { item, event }`); not exercised by any Azalea driver |
| Kurt / apricorn conversation, `ENGINE_KURT_MAKING_BALLS` day wait, Lure Ball, Fast Ball | `src/core/gen2/Apricorns.lua` | implemented at the model level; the file is written against `maps/KurtsHouse.asm` directly and lists `FRUITTREE_AZALEA_TOWN` -> `WHT_APRICORN` at index `0x14` |
| Item balls (`OBJECTTYPE_ITEMBALL`: Super Potion at B1F `10, 3`, TM Rain Dance at B2F `15, 5`) | extracted by `src/import/RomExtractorGen2.lua:2968`; **no runtime handler found in `src/world/gen2/World.lua`** | **missing** - the object data is there, nothing picks it up |
| `fruittree` at runtime (White Apricorn tree object) | opcode row exists (`src/script/gen2/Opcodes.lua`, `[0x9a]`) and `Vm.lua` references it; the daily-tree store is described in `src/core/gen2/Apricorns.lua` (`save.fruitTrees`) | partial - modelled, not verified end to end in a driver |
| Wild encounters for Slowpoke Well (grass 3-slot time split, water, fishing) | `src/import/RomExtractorGen2.lua` (Encounters), `src/battle/gen2/Encounter.lua` | implemented per `docs/gold-phase1.md`; not verified for this map specifically |
| A driver that walks any part of this section | none - `tests/drivers/gold_*.lua` has nothing matching azalea / slowpoke / kurt / bugsy | **missing** |
## 6. Unresolved / verify by hand
- **Bug Catcher Al's prize.** The walkthrough says 200G. The asm gives
4 (`data/trainers/attributes.asm:215`) x 12 (last mon, L12 Weedle) x 4 = **192G**.
Every other figure in the section matches the formula exactly
(360 / 440 / 360 / 560 / 200 / 208 / 192 / 1600), so this looks like a
walkthrough typo, not a special case.
- **Bugsy's gender.** The walkthrough says "Bugsy and her apprentices" and
"she may have the Bug Pokemon". `AzaleaGymGuideText` in `maps/AzaleaGym.asm`
says "BUGSY's young, but **his** knowledge of bug #MON is for real". The asm
wins.
- **"Liz the Picnicker calls about Moo Moo Milk."** `LizPhoneCallerScript`
(`engine/phone/scripts/trainers.asm:234-254`) only does
`PhoneScript_GreetPhone_Female`, a `checkflag ENGINE_FLYPOINT_GOLDENROD`
rematch gate, `PhoneScript_Random2` and `Phone_GenericCall_Female` /
`Phone_CheckIfUnseenRare_Female`. There is no Moo Moo Milk text in her script,
and no `MOOMOO` string turned up anywhere under `engine/phone/`. The generic
call pool may contain such a line, but it is not attributable to Liz from the
map/phone scripts - unresolved.
- **"TM49" listed under *Items found in Azalea Town*.** TM49 is
`verbosegiveitem TM_FURY_CUTTER` inside `AzaleaGymBugsyScript`, i.e. found in
`MAP_AZALEA_GYM`, not on the town map. Cosmetic, but a bot indexing by map
should not look for it outdoors.
- **Battle order inside Slowpoke Well.** The walkthrough's "move on up the
stairs", "head down the stairs" prose does not correspond to any `LADDER` or
`STAIRCASE` collision inside B1F - the only two ladder cells are the two warps
(`17, 15` and `7, 11`). The trainer order it describes (M29 -> F1 -> M2 -> M1)
does match the geometry, so the "stairs" are just prose for the corridors.
- **`SLOWPOKE_WELL_B2F` reachability.** I derived "Surf only" from
`maps/SlowpokeWellB1F.blk` decoded against `data/tilesets/cave_collision.asm`
and `data/collision/collision_permissions.asm`; no script or engine check
states it. If a future reader wants a stronger citation, it would have to come
from the block data, not from an `EVENT_*`.
- **Gym-trainer skippability.** The claim that Josh, Al and (via the column-7
stalk) the twins can be walked past is derived from `maps/AzaleaGym.blk` plus
each object's sight field, not asserted anywhere in the asm. The retroactive
`setevent` block in `AzaleaGymBugsyScript.FightDone` is direct asm evidence
that skipping them is an anticipated state, but the exact dodge path should be
confirmed in-game before a driver depends on it.
- **`variablesprite SPRITE_AZALEA_ROCKET, SPRITE_RIVAL`.** This is why
`AZALEATOWN_RIVAL` is declared with `SPRITE_AZALEA_ROCKET` in
`maps/AzaleaTown.asm`. It also means consts 2 and 12 on that map would draw as
the rival if they were still visible after the well is cleared - they are not,
because both are hidden by then. Worth a look if a port ever renders them.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,987 @@
# Section 06 - Bug Catching Contest and Sudowoodo
Source: `../section-06-bug-catching-contest-and-sudowoodo.txt`
Maps covered: `MAP_GOLDENROD_FLOWER_SHOP`, `MAP_ROUTE_35_GOLDENROD_GATE`, `MAP_ROUTE_35`,
`MAP_ROUTE_35_NATIONAL_PARK_GATE`, `MAP_NATIONAL_PARK`, `MAP_NATIONAL_PARK_BUG_CONTEST`,
`MAP_ROUTE_36_NATIONAL_PARK_GATE`, `MAP_ROUTE_36`, `MAP_ROUTE_37` (listed by the
walkthrough header, never entered by its route)
Badges / key milestones in this section:
- SQUIRTBOTTLE (key item) - gated behind `ENGINE_PLAINBADGE`, i.e. Whitney must already
be beaten before this section can be started at all.
- Bug-Catching Contest run (Tue/Thu/Sat only) - SUN STONE / EVERSTONE / GOLD BERRY / BERRY.
- `EVENT_FOUGHT_SUDOWOODO` - the single event that unblocks Route 36 west<->east, which
is the only land route between Goldenrod and Violet through Route 36.
- TM08 ROCK SMASH (`EVENT_GOT_TM08_ROCK_SMASH`), TM04 ROLLOUT, TM28 DIG, QUICK CLAW.
- No badge is earned in this section.
Everything in this file was transcribed from files that were opened; nothing is
reconstructed from memory. Symbol addresses are `bank:addr` from
`pokegold-symbols/pokegold.sym`.
---
## 1. Route order
The walkthrough's own prose is out of order in one place: it opens by collecting the
SQUIRTBOTTLE in Goldenrod, then much later describes Floria running off to fetch it.
In pokegold the SQUIRTBOTTLE has no Route 36 precondition at all (see
"Unresolved / verify by hand"), so the opening order is the one a bot should follow.
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 1 | `MAP_GOLDENROD_FLOWER_SHOP` | `maps/GoldenrodFlowerShop.asm` | `GOLDENROD_CITY` warp 6 @ (33, 5) | warp 1/2 @ (2,7)/(3,7) -> `GOLDENROD_CITY` 6 | Talk to the teacher for the SQUIRTBOTTLE |
| 2 | `MAP_ROUTE_35_GOLDENROD_GATE` | `maps/Route35GoldenrodGate.asm` | `GOLDENROD_CITY` warp 13 @ (19, 1) -> gate warp 3 | warp 1/2 @ (4,0)/(5,0) -> `ROUTE_35` 1/2 | Randy hands over Kenya the SPEAROW with FLOWER MAIL |
| 3 | `MAP_ROUTE_35` | `maps/Route35.asm` | gate warp @ (9,33)/(10,33) | warp 3 @ (3, 5) -> `ROUTE_35_NATIONAL_PARK_GATE` 3 | Six trainers + Officer Dirk, walk north |
| 4 | `MAP_ROUTE_35_NATIONAL_PARK_GATE` | `maps/Route35NationalParkGate.asm` | warp 3/4 @ (3,7)/(4,7) | warp 1/2 @ (3,0)/(4,0) -> `NATIONAL_PARK` 3/4 | Contest officer; optional entry to the Contest |
| 5 | `MAP_NATIONAL_PARK_BUG_CONTEST` | `maps/NationalParkBugContest.asm` | script `warp NATIONAL_PARK_BUG_CONTEST, 10, 47` from the south gate (or `33, 18` from the north gate) | contest timer / balls exhausted, or either gate | Optional Tue/Thu/Sat Contest |
| 6 | `MAP_NATIONAL_PARK` | `maps/NationalPark.asm` | warp 3/4 @ (10,47)/(11,47) | warp 1/2 @ (33,18)/(33,19) -> `ROUTE_36_NATIONAL_PARK_GATE` 1/2 | QUICK CLAW, Beverly, Jack, Krise, William, TM28 DIG |
| 7 | `MAP_ROUTE_36_NATIONAL_PARK_GATE` | `maps/Route36NationalParkGate.asm` | warp 1/2 @ (0,4)/(0,5) | warp 3/4 @ (9,4)/(9,5) -> `ROUTE_36` 1/2 | Contest results / prize holding |
| 8 | `MAP_ROUTE_36` | `maps/Route36.asm` | warp 1/2 @ (18,8)/(18,9) | south connection @ y=17 -> `ROUTE_35`; later east connection -> `VIOLET_CITY` | Ice Berry, Psychic Mark, Schoolboy Alan, Sudowoodo, TM08 |
| 9 | `MAP_ROUTE_35` (again) | `maps/Route35.asm` | north connection from `ROUTE_36` (offset 0) | back north to `ROUTE_36` | Cut the tree, Bug Catcher Arnie, Bird Keeper Bryan, TM04 ROLLOUT |
| 10 | `MAP_ROUTE_36` (again) | `maps/Route36.asm` | north connection from `ROUTE_35` | **east connection -> `VIOLET_CITY`** | Sudowoodo cleared, then east into Violet City |
Spills into the next section: step 10's east connection
(`connection east, VioletCity, VIOLET_CITY, 0` in `data/maps/attributes.asm`) drops the
player into `MAP_VIOLET_CITY`, which belongs to a neighbouring section. Route 37
(`MAP_ROUTE_37`, north connection off Route 36 with offset 10) is named in this section's
header but the walkthrough never routes into it; its data is recorded below for
completeness because the item list ("Blk/Blu/Red Apricorn", "Ice Berry") mixes 36 and 37.
---
## 2. Maps
### MAP_GOLDENROD_FLOWER_SHOP
- Script: `maps/GoldenrodFlowerShop.asm`
- Blocks: `maps/GoldenrodFlowerShop.blk`
- Header (`data/maps/maps.asm:275`): `TILESET_HOUSE, INDOOR, LANDMARK_GOLDENROD_CITY, MUSIC_GOLDENROD_CITY, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:253` -> `map_const GOLDENROD_FLOWER_SHOP, 4, 4`
- Connections: none (indoor)
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 2 | 7 | `GOLDENROD_CITY` | 6 |
| 2 | 3 | 7 | `GOLDENROD_CITY` | 6 |
**Coord events** (`def_coord_events`) - none.
**BG events** (`def_bg_events`) - none. (`FlowerShopShelf1`, `FlowerShopShelf2`,
`FlowerShopRadio` exist in the file but are marked `; unreferenced`.)
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `GOLDENRODFLOWERSHOP_TEACHER` | `SPRITE_TEACHER` | 2 | 4 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `FlowerShopTeacherScript` | -1 |
| `GOLDENRODFLOWERSHOP_FLORIA` | `SPRITE_LASS` | 5 | 6 | `SPRITEMOVEDATA_WANDER` (1,1) | `OBJECTTYPE_SCRIPT` | `FlowerShopFloriaScript` | -1 |
**Scripts of interest**
- `FlowerShopTeacherScript` (`57:52d1`) - `checkevent EVENT_GOT_SQUIRTBOTTLE` /
`iftrue .Lalala`; then `checkflag ENGINE_PLAINBADGE` / `iffalse .Lalala`. Only with the
badge and without the event does it `faceplayer` / `opentext` /
`writetext GoldenrodFlowerShopTeacherBetterThanWhitneyText` / `promptbutton` /
`verbosegiveitem SQUIRTBOTTLE` / `setevent EVENT_GOT_SQUIRTBOTTLE`. **Note the bug in
the cart**: there is no `iffalse` after `verbosegiveitem`, so a full PACK still sets
`EVENT_GOT_SQUIRTBOTTLE` and the item is lost. A bot must have a free KEY ITEM slot.
- `FlowerShopFloriaScript` (`57:52f4`) - pure flavour, branches on `ENGINE_PLAINBADGE`.
Gives nothing.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_GOT_SQUIRTBOTTLE` | `constants/event_flags.asm:101` | read+written by `FlowerShopTeacherScript` | one-shot; set even on a full bag |
| `ENGINE_PLAINBADGE` | `constants/engine_flags.asm:40` | read by `FlowerShopTeacherScript`, `FlowerShopFloriaScript` | Whitney's badge is the hard precondition for the whole section |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `SQUIRTBOTTLE` | talk to teacher at (2,4) | `FlowerShopTeacherScript` `verbosegiveitem` | `EVENT_GOT_SQUIRTBOTTLE` |
**Trainers** - none.
**Wild encounters** - none (indoor).
---
### MAP_ROUTE_35_GOLDENROD_GATE
- Script: `maps/Route35GoldenrodGate.asm`
- Blocks: no `.blk` in `maps/` for this map (gate maps of this group share generated
attributes; `data/maps/attributes.asm:542` carries `map_attributes Route35GoldenrodGate, ROUTE_35_GOLDENROD_GATE, $00`)
- Header (`data/maps/maps.asm:260`): `TILESET_GATE, GATE, LANDMARK_ROUTE_35, MUSIC_ROUTE_36, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:239` -> `map_const ROUTE_35_GOLDENROD_GATE, 5, 4`
- Connections: none. `GATE` environment is why the walkthrough's "ride your bike through"
works.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 0 | `ROUTE_35` | 1 |
| 2 | 5 | 0 | `ROUTE_35` | 2 |
| 3 | 4 | 7 | `GOLDENROD_CITY` | 13 |
| 4 | 5 | 7 | `GOLDENROD_CITY` | 13 |
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE35GOLDENRODGATE_RANDY` | `SPRITE_OFFICER` | 0 | 4 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `RandyScript` | -1 |
| `ROUTE35GOLDENRODGATE_POKEFAN_F` | `SPRITE_POKEFAN_F` | 6 | 4 | `SPRITEMOVEDATA_WALK_UP_DOWN` (0,1) | `OBJECTTYPE_SCRIPT` | `Route35GoldenrodGatePokefanFScript` | -1 |
**Scripts of interest**
- `RandyScript` (`56:59ce`) - four-way branch, checked in this order:
`EVENT_GOT_HP_UP_FROM_RANDY` -> `.gothpup`; `EVENT_GAVE_KENYA` -> `.questcomplete`
(`verbosegiveitem HP_UP`, `setevent EVENT_GOT_HP_UP_FROM_RANDY`);
`EVENT_GOT_KENYA` -> `.alreadyhavekenya`; otherwise the offer. On YES it prints thanks,
then `readvar VAR_PARTYCOUNT` / `ifequal PARTY_LENGTH, .partyfull`. Only with a free
slot does it run `givepoke SPEAROW, 10, NO_ITEM, GiftSpearowName, GiftSpearowOTName`,
`givepokemail GiftSpearowMail`, `setevent EVENT_GOT_KENYA`.
- `GiftSpearowMail` = `FLOWER_MAIL`, text `"DARK CAVE leads / to another road"`.
- `GiftSpearowName` = `KENYA`, `GiftSpearowOTName` = `RANDY`.
- The delivery target (`EVENT_GAVE_KENYA`) is on Route 31, i.e. a later section.
- `Route35GoldenrodGatePokefanFScript` - flavour, branches on `EVENT_FOUGHT_SUDOWOODO`;
its pre-Sudowoodo text is the in-game hint that the SQUIRTBOTTLE is what wakes the tree.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_GOT_KENYA` | `constants/event_flags.asm:89` | `RandyScript` | Spearow accepted |
| `EVENT_GAVE_KENYA` | `constants/event_flags.asm:90` | read here, set on Route 31 | enables the HP UP reward |
| `EVENT_GOT_HP_UP_FROM_RANDY` | `constants/event_flags.asm:91` | `RandyScript` | reward taken |
| `EVENT_FOUGHT_SUDOWOODO` | `constants/event_flags.asm:51` | read here | flavour only on this map |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| SPEAROW "KENYA" (lv 10) + `FLOWER_MAIL` | say YES to Randy with a party slot free | `RandyScript` `givepoke` / `givepokemail` | `EVENT_GOT_KENYA` |
| `HP_UP` | return after delivering to Route 31 | `RandyScript` `.questcomplete` | `EVENT_GOT_HP_UP_FROM_RANDY` |
**Trainers** - none. **Wild encounters** - none.
---
### MAP_ROUTE_35
- Script: `maps/Route35.asm`
- Blocks: `maps/Route35.blk` (180 bytes = 10 x 18 blocks)
- Header (`data/maps/maps.asm:248`): `TILESET_JOHTO, ROUTE, LANDMARK_ROUTE_35, MUSIC_ROUTE_36, FALSE, PALETTE_AUTO, FISHGROUP_POND`
- Dimensions: `constants/map_constants.asm:227` -> `map_const ROUTE_35, 10, 18` (so cell
coordinates run 0..19 in x and 0..35 in y)
- Connections (`data/maps/attributes.asm:202`): north `ROUTE_36` offset 0, south
`GOLDENROD_CITY` offset -5
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 33 | `ROUTE_35_GOLDENROD_GATE` | 1 |
| 2 | 10 | 33 | `ROUTE_35_GOLDENROD_GATE` | 2 |
| 3 | 3 | 5 | `ROUTE_35_NATIONAL_PARK_GATE` | 3 |
**Coord events** - none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 1 | 7 | `BGEVENT_READ` | `Route35Sign` |
| 11 | 31 | `BGEVENT_READ` | `Route35Sign` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE35_YOUNGSTER1` | `SPRITE_YOUNGSTER` | 3 | 19 | `STANDING_RIGHT` | `OBJECTTYPE_TRAINER` sight 4 | `TrainerCamperIvan` | -1 |
| `ROUTE35_YOUNGSTER2` | `SPRITE_YOUNGSTER` | 8 | 20 | `STANDING_DOWN` | `OBJECTTYPE_TRAINER` sight 3 | `TrainerCamperElliot` | -1 |
| `ROUTE35_LASS1` | `SPRITE_LASS` | 7 | 20 | `STANDING_DOWN` | `OBJECTTYPE_TRAINER` sight 3 | `TrainerPicnickerBrooke` | -1 |
| `ROUTE35_LASS2` | `SPRITE_LASS` | 11 | 24 | `STANDING_LEFT` | `OBJECTTYPE_TRAINER` sight 3 | `TrainerPicnickerKim` | -1 |
| `ROUTE35_YOUNGSTER3` | `SPRITE_YOUNGSTER` | 14 | 28 | `STANDING_DOWN` | `OBJECTTYPE_TRAINER` sight 0 | `TrainerBirdKeeperBryan` | -1 |
| `ROUTE35_FISHER` | `SPRITE_FISHER` | 2 | 10 | `SPINCOUNTERCLOCKWISE` | `OBJECTTYPE_TRAINER` sight 2 | `TrainerFirebreatherWalt` | -1 |
| `ROUTE35_BUG_CATCHER` | `SPRITE_BUG_CATCHER` | 16 | 7 | `STANDING_DOWN` (radius 2,0) | `OBJECTTYPE_TRAINER` sight 3 | `TrainerBugCatcherArnie` | -1 |
| `ROUTE35_SUPER_NERD` | `SPRITE_SUPER_NERD` | 5 | 10 | `SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` sight 2 | `TrainerJugglerIrwin` | -1 |
| `ROUTE35_OFFICER` | `SPRITE_OFFICER` | 5 | 6 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `TrainerOfficerDirk` | -1 |
| `ROUTE35_FRUIT_TREE` | `SPRITE_FRUIT_TREE` | 2 | 25 | `STILL` | `OBJECTTYPE_SCRIPT` | `Route35FruitTree` (`4b:5c07`) | -1 |
| `ROUTE35_POKE_BALL` | `SPRITE_POKE_BALL` | 13 | 16 | `STILL` | `OBJECTTYPE_ITEMBALL` | `Route35TMRollout` (`4b:5c05`) | `EVENT_ROUTE_35_TM_ROLLOUT` |
**Scripts of interest**
- `TrainerOfficerDirk` - **not** an `OBJECTTYPE_TRAINER` object. It is a plain script that
runs `checktime NITE` / `iffalse .NotNight` first, so Dirk only battles at night. Then
`checkevent EVENT_BEAT_OFFICER_DIRK`, `playmusic MUSIC_OFFICER_ENCOUNTER`,
`winlosstext OfficerDirkBeatenText, 0`, `loadtrainer OFFICER, DIRK`, `startbattle`,
`reloadmapafterbattle`, `setevent EVENT_BEAT_OFFICER_DIRK`. A daytime bot will get
`OfficerDirkPrettyToughText` and no battle.
- `TrainerJugglerIrwin` / `TrainerBugCatcherArnie` - standard phone trainers. After the
win, `checkcellnum PHONE_JUGGLER_IRWIN` / `PHONE_BUG_CATCHER_ARNIE`, then
`setevent EVENT_IRWIN_ASKED_FOR_PHONE_NUMBER` / `EVENT_ARNIE_ASKED_FOR_PHONE_NUMBER`
and `askforphonenumber`. The walkthrough's "head back to Arnie and talk to her for her
phone number" is this second talk.
Rematch tiers: Irwin uses `EVENT_CLEARED_RADIO_TOWER` -> `IRWIN3`,
`ENGINE_FLYPOINT_CIANWOOD` -> `IRWIN2`, else `IRWIN1`. Arnie uses
`ENGINE_FLYPOINT_BLACKTHORN` -> `ARNIE3`, `ENGINE_FLYPOINT_LAKE_OF_RAGE` -> `ARNIE2`,
else `ARNIE1`.
- `Route35TMRollout` -> `itemball TM_ROLLOUT`. `Route35FruitTree` -> `fruittree FRUITTREE_ROUTE_35`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_PICNICKER_KIM` / `_CAMPER_ELLIOT` / `_PICNICKER_BROOKE` / `_CAMPER_IVAN` / `_JUGGLER_IRWIN` / `_FIREBREATHER_WALT` / `_BUG_CATCHER_ARNIE` / `_BIRD_KEEPER_BRYAN` | `constants/event_flags.asm` | `trainer` macro rows | per-trainer defeated bits |
| `EVENT_BEAT_OFFICER_DIRK` | `constants/event_flags.asm` | `TrainerOfficerDirk` | set by hand, not by the `trainer` macro |
| `EVENT_ROUTE_35_TM_ROLLOUT` | `constants/event_flags.asm:1107` | item ball object | TM04 taken |
| `EVENT_IRWIN_ASKED_FOR_PHONE_NUMBER`, `EVENT_IRWIN_READY_FOR_REMATCH` | `constants/event_flags.asm` | Irwin script | phone state machine |
| `EVENT_ARNIE_ASKED_FOR_PHONE_NUMBER`, `EVENT_ARNIE_READY_FOR_REMATCH` | `constants/event_flags.asm` | Arnie script | phone state machine |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `TM_ROLLOUT` (TM04, `constants/item_constants.asm:223`) | item ball at cell (13, 16) | `Route35TMRollout` | `EVENT_ROUTE_35_TM_ROLLOUT` |
| `MYSTERYBERRY` | fruit tree object at (2, 25) | `FRUITTREE_ROUTE_35` -> `data/items/fruit_trees.asm:14` | daily reset (`Apricorns`/`TryResetFruitTrees` equivalent) |
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `KIM` | `PICNICKER` (0x35) | 4 | `parties.asm:2601` `KIM` - lv15 VULPIX | `TrainerPicnickerKim` | no |
| `ELLIOT` | `CAMPER` (0x36) | 4 | `parties.asm:2722` `ELLIOT` - lv13 SANDSHREW, lv15 MARILL | `TrainerCamperElliot` | no |
| `BROOKE` | `PICNICKER` (0x35) | 3 | `parties.asm:2596` `BROOKE` `TRAINERTYPE_MOVES` - lv16 PIKACHU (THUNDERSHOCK, GROWL, QUICK_ATTACK, DOUBLE_TEAM) | `TrainerPicnickerBrooke` | no |
| `IVAN` | `CAMPER` (0x36) | 3 | `parties.asm:2715` `IVAN` - lv10 DIGLETT, lv10 ZUBAT, lv14 DIGLETT | `TrainerCamperIvan` | no |
| `IRWIN1` | `JUGGLER` (0x31) | 1 | `parties.asm:2384` `IRWIN` - lv2/6/10/14 VOLTORB | `TrainerJugglerIrwin` | yes (`PHONE_JUGGLER_IRWIN`); IRWIN2 `parties.asm:2407`, IRWIN3 `parties.asm:2415` |
| `WALT` | `FIREBREATHER` (0x30) | 6 | `parties.asm:2365` `WALT` - lv11 MAGMAR, lv13 MAGMAR | `TrainerFirebreatherWalt` | no |
| `DIRK` | `OFFICER` (0x41) | 2 | `parties.asm:3172` `DIRK` - lv14 GROWLITHE, lv14 GROWLITHE | `TrainerOfficerDirk` | night only |
| `ARNIE1` | `BUG_CATCHER` (0x24) | 8 | `parties.asm:1455` `ARNIE` - lv15 VENONAT | `TrainerBugCatcherArnie` | yes (`PHONE_BUG_CATCHER_ARNIE`); ARNIE2 `parties.asm:1487`, ARNIE3 `parties.asm:1492` (lv28 VENOMOTH w/ moves) |
| `BRYAN` | `BIRD_KEEPER` (0x18) | 3 | `parties.asm:534` `BRYAN` - lv12 PIDGEY, lv14 PIDGEOTTO | `TrainerBirdKeeperBryan` | no |
**Wild encounters** - `data/wild/johto_grass.asm`, `def_grass_wildmons ROUTE_35`,
rates 10/10/10 percent (morn/day/nite). Gold table:
| slot | morn | day | nite |
|---|---|---|---|
| 1 | 12 NIDORAN_M | 12 NIDORAN_M | 12 NIDORAN_M |
| 2 | 12 NIDORAN_F | 12 NIDORAN_F | 12 NIDORAN_F |
| 3 | 14 DROWZEE | 14 DROWZEE | 14 DROWZEE |
| 4 | 10 ABRA | 10 ABRA | 10 ABRA |
| 5 | 14 PIDGEY | 14 PIDGEY | 14 HOOTHOOT |
| 6 | 10 DITTO | 10 DITTO | 10 DITTO |
| 7 | 12 YANMA | 12 YANMA | 12 YANMA |
(Silver swaps slots 1/2 to NIDORAN_F/NIDORAN_M.)
Water (`data/wild/johto_water.asm:156` `def_water_wildmons ROUTE_35`): 4 percent -
20 PSYDUCK, 15 PSYDUCK, 20 GOLDUCK. Fishing group is `FISHGROUP_POND`
(`data/wild/fish.asm:15`). Headbutt: `data/wild/treemon_maps.asm:16` ->
`TREEMON_SET_FOREST` (`data/wild/treemons.asm:34`; Gold common CATERPIE/METAPOD/
EXEGGCUTE/BUTTERFREE, rare adds PINECO). Swarm: `data/wild/swarm_grass.asm:6` is the
YANMA swarm table for `ROUTE_35`.
**Cut tree** - `maps/Route35.blk` has exactly one cuttable block: block id `$5b` at block
(8, 3), i.e. walk cells x 16-17, y 6-7. `data/collision/field_move_blocks.asm:14` gives
`db $5b, $3c, 0` for `TILESET_JOHTO`. That is the tree the walkthrough tells the player to
cut, and Bug Catcher Arnie at (16, 7) sits immediately behind it. Route 36, Route 37 and
National Park have **no** cuttable blocks (National Park's `.park` entry in
`field_move_blocks.asm` is only the two grass ids `$13` and `$03`).
---
### MAP_ROUTE_35_NATIONAL_PARK_GATE
- Script: `maps/Route35NationalParkGate.asm`
- Blocks: `maps/Route35NationalParkGate.blk`
- Header (`data/maps/maps.asm:261`): `TILESET_GATE, INDOOR, LANDMARK_ROUTE_35, MUSIC_GOLDENROD_CITY, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:240` -> `map_const ROUTE_35_NATIONAL_PARK_GATE, 4, 4`
- Connections: none
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 0 | `NATIONAL_PARK` | 3 |
| 2 | 4 | 0 | `NATIONAL_PARK` | 4 |
| 3 | 3 | 7 | `ROUTE_35` | 3 |
| 4 | 4 | 7 | `ROUTE_35` | 3 |
**Coord events** - none. The map instead uses scene scripts:
| scene id | value | script | effect |
|---|---|---|---|
| `SCENE_ROUTE35NATIONALPARKGATE_NOOP` | 0 | `Route35NationalParkGateNoop1Scene` | `end` |
| `SCENE_ROUTE35NATIONALPARKGATE_UNUSED` | 1 | `Route35NationalParkGateNoop2Scene` | `end` |
| `SCENE_ROUTE35NATIONALPARKGATE_LEAVE_CONTEST_EARLY` | 2 | `Route35NationalParkGateLeaveContestEarlyScene` | `sdefer Route35NationalParkGateLeavingContestEarlyScript` |
(The `SCENE_*` names are generated by the `scene_script` macro,
`macros/scripts/maps.asm:25`, in the order they appear in this file - there is no
separate constants file for them.)
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 5 | 0 | `BGEVENT_READ` | `BugCatchingContestExplanationSign` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE35NATIONALPARKGATE_OFFICER1` | `SPRITE_OFFICER` | 2 | 1 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `Route35OfficerScriptContest` (`56:5e0c`) | `EVENT_ROUTE_35_NATIONAL_PARK_GATE_OFFICER_CONTEST_DAY` |
| `ROUTE35NATIONALPARKGATE_YOUNGSTER` | `SPRITE_YOUNGSTER` | 6 | 5 | `WANDER` (1,1) | `OBJECTTYPE_SCRIPT` | `Route35NationalParkGateYoungsterScript` | `EVENT_ROUTE_35_NATIONAL_PARK_GATE_YOUNGSTER` |
| `ROUTE35NATIONALPARKGATE_OFFICER2` | `SPRITE_OFFICER` | 0 | 3 | `STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `Route35NationalParkGateOfficerScript` | `EVENT_ROUTE_35_NATIONAL_PARK_GATE_OFFICER_NOT_CONTEST_DAY` |
**Callbacks**
- `MAPCALLBACK_NEWMAP` -> `Route35NationalParkGateCheckIfContestRunningCallback`:
`checkflag ENGINE_BUG_CONTEST_TIMER` -> `setscene SCENE_..._LEAVE_CONTEST_EARLY`,
else `setscene SCENE_..._NOOP`.
- `MAPCALLBACK_OBJECTS` -> `Route35NationalParkGateCheckIfContestAvailableCallback`
(`56:5db8`): `readvar VAR_WEEKDAY`; `TUESDAY`/`THURSDAY`/`SATURDAY` ->
`appear OFFICER1`, `disappear YOUNGSTER`, `disappear OFFICER2`. Otherwise
`checkflag ENGINE_BUG_CONTEST_TIMER` (mid-contest keeps the contest officer), else the
non-contest pair appears.
**Scripts of interest**
- `Route35OfficerScriptContest` (`56:5e0c`) - the entry point. Order of checks:
1. `readvar VAR_WEEKDAY`; SUNDAY/MONDAY/WEDNESDAY/FRIDAY -> `Route35NationalParkGate_NoContestToday`.
2. `checkflag ENGINE_DAILY_BUG_CONTEST` -> `Route35NationalParkGate_ContestIsOver`.
3. `yesorno`. NO -> `Route35NationalParkGate_DeclinedToParticipate`.
4. `readvar VAR_PARTYCOUNT` / `ifgreater 1, Route35NationalParkGate_LeaveTheRestBehind`.
With more than one mon it needs box space (`readvar VAR_BOXSPACE`, `ifequal 0` ->
`_NoRoomInBox`), rejects an EGG lead (`special CheckFirstMonIsEgg`) and rejects a
fainted lead (`special ContestDropOffMons` -> `iftrue _FirstMonIsFainted`), then
`setevent EVENT_LEFT_MONS_WITH_CONTEST_OFFICER`.
5. `Route35NationalParkGate_OkayToProceed`: `setflag ENGINE_BUG_CONTEST_TIMER`,
`special PlayMapMusic`, park ball text, `special GiveParkBalls`,
`special FadeOutToWhite`, `special SelectRandomBugContestContestants`,
`warp NATIONAL_PARK_BUG_CONTEST, 10, 47`.
- `Route35NationalParkGateLeavingContestEarlyScript` - runs when re-entering this gate
while `ENGINE_BUG_CONTEST_TIMER` is set. `readvar VAR_CONTESTMINUTES` /
`addval 1` / `getnum STRING_BUFFER_3`, `yesorno`. YES ->
`jumpstd BugContestResultsWarpScript`. NO -> `warp NATIONAL_PARK_BUG_CONTEST, 10, 47`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_BUG_CONTEST_TIMER` | `constants/engine_flags.asm:26` | set by the officer, cleared by `BugContestResultsScript` | "contest in progress" |
| `ENGINE_DAILY_BUG_CONTEST` | `constants/engine_flags.asm:98` | set by `BugContestResultsScript` | "already ran today" |
| `EVENT_LEFT_MONS_WITH_CONTEST_OFFICER` | `constants/event_flags.asm:439` | officer / results script | party is being held |
| `EVENT_ROUTE_35_NATIONAL_PARK_GATE_OFFICER_CONTEST_DAY` / `_NOT_CONTEST_DAY` / `_YOUNGSTER` | `constants/event_flags.asm:1255-1256` (+ youngster) | `MAPCALLBACK_OBJECTS` | which NPC is visible |
| `VAR_WEEKDAY` | `constants/script_constants.asm` (`; 0b`) | callback + officer | 0=Sunday .. 6=Saturday |
| `VAR_CONTESTMINUTES` | `constants/script_constants.asm:65` (`; 11`) | early-exit script | minutes left |
**Items** - park balls only (see the Contest map). **Trainers** - none.
---
### MAP_NATIONAL_PARK
- Script: `maps/NationalPark.asm`
- Blocks: `maps/NationalPark.blk` (540 bytes = 20 x 27 blocks)
- Header (`data/maps/maps.asm:93`): `TILESET_PARK, ROUTE, LANDMARK_NATIONAL_PARK, MUSIC_NATIONAL_PARK, FALSE, PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:80` -> `map_const NATIONAL_PARK, 20, 27` (cells
0..39 x, 0..53 y)
- Connections: none (`data/maps/attributes.asm:412` has no `connection` rows). Both exits
are warps.
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 33 | 18 | `ROUTE_36_NATIONAL_PARK_GATE` | 1 |
| 2 | 33 | 19 | `ROUTE_36_NATIONAL_PARK_GATE` | 2 |
| 3 | 10 | 47 | `ROUTE_35_NATIONAL_PARK_GATE` | 1 |
| 4 | 11 | 47 | `ROUTE_35_NATIONAL_PARK_GATE` | 2 |
**Coord events** - none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 14 | 44 | `BGEVENT_READ` | `NationalParkRelaxationSquareSign` |
| 27 | 31 | `BGEVENT_READ` | `NationalParkBattleNoticeSign` |
| 6 | 47 | `BGEVENT_ITEM` | `NationalParkHiddenFullHeal` -> `hiddenitem FULL_HEAL, EVENT_NATIONAL_PARK_HIDDEN_FULL_HEAL` |
| 12 | 4 | `BGEVENT_READ` | `NationalParkTrainerTipsSign` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `NATIONALPARK_LASS1` | `SPRITE_LASS` | 15 | 24 | `WALK_LEFT_RIGHT` (1,0) | `OBJECTTYPE_SCRIPT` | `NationalParkLassScript` | -1 |
| `NATIONALPARK_POKEFAN_F1` | `SPRITE_POKEFAN_F` | 14 | 4 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `NationalParkPokefanFScript` | -1 |
| `NATIONALPARK_TEACHER1` | `SPRITE_TEACHER` | 27 | 40 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `NationalParkTeacher1Script` (`43:4008`) | -1 |
| `NATIONALPARK_YOUNGSTER1` | `SPRITE_YOUNGSTER` | 11 | 41 | `STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `NationalParkYoungster1Script` | -1 |
| `NATIONALPARK_YOUNGSTER2` | `SPRITE_YOUNGSTER` | 10 | 41 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `NationalParkYoungster2Script` | -1 |
| `NATIONALPARK_TEACHER2` | `SPRITE_TEACHER` | 17 | 41 | `WANDER` (1,2) | `OBJECTTYPE_SCRIPT` | `NationalParkTeacher2Script` | -1 |
| `NATIONALPARK_PERSIAN` | `SPRITE_GROWLITHE` | 26 | 40 | `POKEMON` | `OBJECTTYPE_SCRIPT` | `NationalParkPersian` | -1 |
| `NATIONALPARK_YOUNGSTER3` | `SPRITE_YOUNGSTER` | 27 | 23 | `SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` sight 3 | `TrainerSchoolboyJack1` | -1 |
| `NATIONALPARK_POKEFAN_F2` | `SPRITE_POKEFAN_F` | 18 | 29 | `SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` sight 2 | `TrainerPokefanfBeverly1` | -1 |
| `NATIONALPARK_POKEFAN_M` | `SPRITE_POKEFAN_M` | 16 | 9 | `SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` sight 2 | `TrainerPokefanmWilliam` | -1 |
| `NATIONALPARK_LASS2` | `SPRITE_LASS` | 8 | 14 | `SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` sight 3 | `TrainerLassKrise` | -1 |
| `NATIONALPARK_POKE_BALL1` | `SPRITE_POKE_BALL` | 35 | 12 | `STILL` | `OBJECTTYPE_ITEMBALL` | `NationalParkParlyzHeal` (`43:418f`) | `EVENT_NATIONAL_PARK_PARLYZ_HEAL` |
| `NATIONALPARK_GAMEBOY_KID` | `SPRITE_GAMEBOY_KID` | 26 | 6 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `NationalParkGameboyKidScript` | -1 |
| `NATIONALPARK_POKE_BALL2` | `SPRITE_POKE_BALL` | 1 | 43 | `STILL` | `OBJECTTYPE_ITEMBALL` | `NationalParkTMDig` (`43:4191`) | `EVENT_NATIONAL_PARK_TM_DIG` |
**Scripts of interest**
- `NationalParkTeacher1Script` (`43:4008`) - the walkthrough's "lady on the bench".
`checkevent EVENT_GOT_QUICK_CLAW` / `iftrue .GotQuickClaw`; else text,
`verbosegiveitem QUICK_CLAW`, `iffalse .NoRoom`, `setevent EVENT_GOT_QUICK_CLAW`.
Unlike the flower shop teacher this one **does** guard the `setevent` with `iffalse`.
- `TrainerSchoolboyJack1` - phone trainer (`PHONE_SCHOOLBOY_JACK`). Rematch tiers:
`EVENT_CLEARED_RADIO_TOWER` -> `JACK3`, `ENGINE_FLYPOINT_OLIVINE` -> `JACK2`, else `JACK1`.
- `TrainerPokefanfBeverly1` - phone trainer (`PHONE_POKEFAN_BEVERLY`), female std scripts
(`AskNumber1FScript` etc). Rematch tiers: `EVENT_CLEARED_RADIO_TOWER` -> `BEVERLY3`,
`ENGINE_FLYPOINT_MAHOGANY` -> `BEVERLY2`, else `BEVERLY1`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_GOT_QUICK_CLAW` | `constants/event_flags.asm:96` | `NationalParkTeacher1Script` | one-shot gift |
| `EVENT_NATIONAL_PARK_PARLYZ_HEAL` | `constants/event_flags.asm:1017` | item ball | ball taken |
| `EVENT_NATIONAL_PARK_TM_DIG` | `constants/event_flags.asm:1018` | item ball | ball taken |
| `EVENT_NATIONAL_PARK_HIDDEN_FULL_HEAL` | `constants/event_flags.asm:142` | `hiddenitem` | hidden item taken |
| `EVENT_BEAT_SCHOOLBOY_JACK`, `EVENT_BEAT_POKEFANF_BEVERLY`, `EVENT_BEAT_POKEFANM_WILLIAM`, `EVENT_BEAT_LASS_KRISE` | `constants/event_flags.asm` | `trainer` rows | defeated bits |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `QUICK_CLAW` | talk to teacher at (27, 40) | `NationalParkTeacher1Script` | `EVENT_GOT_QUICK_CLAW` |
| `PARLYZ_HEAL` | item ball at (35, 12) | `NationalParkParlyzHeal` | `EVENT_NATIONAL_PARK_PARLYZ_HEAL` |
| `TM_DIG` (TM28) | item ball at (1, 43) | `NationalParkTMDig` | `EVENT_NATIONAL_PARK_TM_DIG` |
| `FULL_HEAL` | hidden, bg_event at (6, 47) | `NationalParkHiddenFullHeal` | `EVENT_NATIONAL_PARK_HIDDEN_FULL_HEAL` |
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `BEVERLY1` | `POKEFANF` (0x3e) | 1 | `parties.asm:3115` `BEVERLY` `TRAINERTYPE_ITEM` - lv14 SNUBBULL @ BERRY | `TrainerPokefanfBeverly1` | yes (`PHONE_POKEFAN_BEVERLY`); BEVERLY2 `parties.asm:3125`, BEVERLY3 `parties.asm:3130` |
| `JACK1` | `SCHOOLBOY` (0x17) | 1 | `parties.asm:409` `JACK` - lv12 ODDISH, lv15 VOLTORB | `TrainerSchoolboyJack1` | yes (`PHONE_SCHOOLBOY_JACK`); JACK2 `parties.asm:484`, JACK3 `parties.asm:490` |
| `KRISE` | `LASS` (0x19) | 4 | `parties.asm:658` `KRISE` - lv12 ODDISH, lv15 CUBONE | `TrainerLassKrise` | no |
| `WILLIAM` | `POKEFANM` (0x3b) | 1 | `parties.asm:2961` `WILLIAM` `TRAINERTYPE_ITEM` - lv14 RAICHU @ BERRY | `TrainerPokefanmWilliam` | no |
**Wild encounters** - `data/wild/johto_grass.asm`, `def_grass_wildmons NATIONAL_PARK`,
rates 10/10/10 percent. Gold table:
| slot | morn | day | nite |
|---|---|---|---|
| 1 | 10 CATERPIE | 10 CATERPIE | 10 HOOTHOOT |
| 2 | 10 METAPOD | 10 METAPOD | 10 HOOTHOOT |
| 3 | 12 CATERPIE | 11 SUNKERN | 12 HOOTHOOT |
| 4 | 12 PIDGEY | 12 PIDGEY | 12 HOOTHOOT |
| 5 | 10 PIDGEY | 13 SUNKERN | 10 HOOTHOOT |
| 6 | 14 PIDGEY | 14 PIDGEY | 14 HOOTHOOT |
| 7 | 14 PIDGEY | 14 PIDGEY | 14 HOOTHOOT |
(Silver uses WEEDLE/KAKUNA in slots 1-3 morn. The nite block is shared by both versions.)
No `NATIONAL_PARK` entry exists in `data/wild/johto_water.asm`, `data/wild/treemon_maps.asm`
or `data/wild/swarm_grass.asm`. The rare bugs the walkthrough lists (Scyther, Pinsir,
Paras, Venonat, Butterfree, Beedrill) are **Contest-only**; see below.
---
### MAP_NATIONAL_PARK_BUG_CONTEST
- Script: `maps/NationalParkBugContest.asm`
- Blocks: none of its own; `data/maps/attributes.asm:413` gives
`map_attributes NationalParkBugContest, NATIONAL_PARK_BUG_CONTEST, $00` and the map is a
same-geometry twin of National Park.
- Header (`data/maps/maps.asm:94`): `TILESET_PARK, ROUTE, LANDMARK_NATIONAL_PARK, MUSIC_BUG_CATCHING_CONTEST, FALSE, PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:81` -> `map_const NATIONAL_PARK_BUG_CONTEST, 20, 27`
- Connections: none
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 33 | 18 | `ROUTE_36_NATIONAL_PARK_GATE` | 1 |
| 2 | 33 | 19 | `ROUTE_36_NATIONAL_PARK_GATE` | 1 |
| 3 | 10 | 47 | `ROUTE_35_NATIONAL_PARK_GATE` | 1 |
| 4 | 11 | 47 | `ROUTE_35_NATIONAL_PARK_GATE` | 1 |
(Warps 2 and 4 point at dest warp 1, not 2 - that is what the file says.)
**Coord events** - none.
**BG events** - same four rows as National Park, aimed at the `...BugContest...` copies:
(14,44) Relaxation Square sign, (27,31) Battle Notice sign, (6,47) `BGEVENT_ITEM`
`hiddenitem FULL_HEAL, EVENT_NATIONAL_PARK_HIDDEN_FULL_HEAL` (the same flag as the normal
map), (12,4) Trainer Tips sign.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `..._YOUNGSTER1` | `SPRITE_YOUNGSTER` | 19 | 29 | `WANDER` (2,2) | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant1AScript` | `EVENT_BUG_CATCHING_CONTESTANT_1A` |
| `..._YOUNGSTER2` | `SPRITE_YOUNGSTER` | 28 | 22 | `WANDER` (2,2) | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant2AScript` | `..._2A` |
| `..._ROCKER` | `SPRITE_ROCKER` | 9 | 18 | `STANDING_UP` | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant3AScript` | `..._3A` |
| `..._POKEFAN_M` | `SPRITE_POKEFAN_M` | 7 | 13 | `WALK_UP_DOWN` (1,0) | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant4AScript` | `..._4A` |
| `..._YOUNGSTER3` | `SPRITE_YOUNGSTER` | 23 | 9 | `SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant5AScript` | `..._5A` |
| `..._YOUNGSTER4` | `SPRITE_YOUNGSTER` | 27 | 13 | `WANDER` (3,3) | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant6AScript` | `..._6A` |
| `..._LASS` | `SPRITE_LASS` | 7 | 23 | `WALK_LEFT_RIGHT` (2,0) | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant7AScript` | `..._7A` |
| `..._YOUNGSTER5` | `SPRITE_YOUNGSTER` | 11 | 27 | `WANDER` (1,1) | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant8AScript` | `..._8A` |
| `..._YOUNGSTER6` | `SPRITE_YOUNGSTER` | 16 | 8 | `WANDER` (1,1) | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant9AScript` | `..._9A` |
| `..._YOUNGSTER7` | `SPRITE_YOUNGSTER` | 17 | 34 | `WANDER` (3,3) | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant10AScript` | `..._10A` |
| `..._POKE_BALL1` | `SPRITE_POKE_BALL` | 35 | 12 | `STILL` | `OBJECTTYPE_ITEMBALL` | `NationalParkBugContestParlyzHeal` | `EVENT_NATIONAL_PARK_PARLYZ_HEAL` |
| `..._POKE_BALL2` | `SPRITE_POKE_BALL` | 1 | 43 | `STILL` | `OBJECTTYPE_ITEMBALL` | `NationalParkBugContestTMDig` | `EVENT_NATIONAL_PARK_TM_DIG` |
**Scripts of interest**
- Every `BugCatchingContestantNAScript` is `faceplayer / opentext / writetext / waitbutton
/ closetext / end` - flavour only, no branching. The `B` variants in the north gate are
the ones that branch on `EVENT_TEMPORARY_UNTIL_MAP_RELOAD_1` (set by
`BugContestResults_FirstPlace`) to say "you won".
- The battle path is `BugCatchingContestBattleScript::`
(`engine/events/bug_contest/contest.asm:9`): `loadvar VAR_BATTLETYPE, BATTLETYPE_CONTEST`,
`randomwildmon`, `startbattle`, `reloadmapafterbattle`, `readmem wParkBallsRemaining`,
`iffalse BugCatchingContestOutOfBallsScript`.
- `GiveParkBalls` (`engine/events/bug_contest/contest.asm:1`) clears `wContestMon`, writes
`BUG_CONTEST_BALLS` into `wParkBallsRemaining` and `farcall StartBugContestTimer`.
**Contest constants** (`constants/script_constants.asm`)
| constant | line | value |
|---|---|---|
| `BUG_CONTEST_BALLS` | 283 | 20 |
| `BUG_CONTEST_MINUTES` | 284 | 20 |
| `BUG_CONTEST_PLAYER` | 286 | 1 |
| `NUM_BUG_CONTESTANTS` | 287 | 10 (not counting the player) |
| `VAR_CONTESTMINUTES` | 65 | var id 0x11 |
**Contest wild table** - `data/wild/bug_contest_mons.asm`, label `ContestMons`
(`25:7bb8`). This table replaces National Park's grass entirely for the duration:
| % | species | min lv | max lv |
|---|---|---|---|
| 20 | CATERPIE | 7 | 18 |
| 20 | WEEDLE | 7 | 18 |
| 10 | METAPOD | 9 | 18 |
| 10 | KAKUNA | 9 | 18 |
| 5 | BUTTERFREE | 12 | 15 |
| 5 | BEEDRILL | 12 | 15 |
| 10 | VENONAT | 10 | 16 |
| 10 | PARAS | 10 | 17 |
| 5 | SCYTHER | 13 | 14 |
| 5 | PINSIR | 13 | 14 |
| -1 (terminator row) | VENOMOTH | 30 | 40 |
**Scoring** - `ContestScore` (`04:7cbc`, `engine/events/bug_contest/judging.asm`) sums:
`MaxHP * 4` (high byte only), then Attack, Defense, Speed, SpclAtk, SpclDef, then a DV
bonus assembled from bit 1 of each DV nibble, then `remaining HP / 8`, then `+1` if the
mon is holding an item. Five of the ten AI contestants are rolled by
`ComputeAIContestantScores`, each picking one of three canned mon/score pairs from
`BugContestantPointers` (`data/events/bug_contest_winners.asm`) with a 0..7 random
perturbation. `DetermineContestWinners` keeps a 3-deep podium.
**Prizes** - `BugContestResultsScript` (`40:420e`, `engine/events/std_scripts.asm:275`):
| place | item | no-room fallback flag |
|---|---|---|
| 1st | `SUN_STONE` | `EVENT_CONTEST_OFFICER_HAS_SUN_STONE` |
| 2nd | `EVERSTONE` | `EVENT_CONTEST_OFFICER_HAS_EVERSTONE` |
| 3rd | `GOLD_BERRY` | `EVENT_CONTEST_OFFICER_HAS_GOLD_BERRY` |
| consolation | `BERRY` | `EVENT_CONTEST_OFFICER_HAS_BERRY` |
First place also sets `EVENT_TEMPORARY_UNTIL_MAP_RELOAD_1`. The tail of the script clears
`ENGINE_BUG_CONTEST_TIMER`, sets all twenty `EVENT_BUG_CATCHING_CONTESTANT_*` flags, sets
`ENGINE_DAILY_BUG_CONTEST`, and resets both gate scenes to their NOOP.
---
### MAP_ROUTE_36_NATIONAL_PARK_GATE
- Script: `maps/Route36NationalParkGate.asm`
- Blocks: `maps/Route36NationalParkGate.blk`
- Header (`data/maps/maps.asm:263`): `TILESET_GATE, INDOOR, LANDMARK_ROUTE_36, MUSIC_GOLDENROD_CITY, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:242` -> `map_const ROUTE_36_NATIONAL_PARK_GATE, 5, 4`
- Connections: none
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 0 | 4 | `NATIONAL_PARK` | 1 |
| 2 | 0 | 5 | `NATIONAL_PARK` | 2 |
| 3 | 9 | 4 | `ROUTE_36` | 1 |
| 4 | 9 | 5 | `ROUTE_36` | 2 |
**Coord events** - none. Scene scripts (same three-slot shape as the south gate):
`SCENE_ROUTE36NATIONALPARKGATE_NOOP` (0), `..._UNUSED` (1),
`..._LEAVE_CONTEST_EARLY` (2) -> `sdefer Route36NationalParkGateLeavingContestEarlyScript`.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 6 | 0 | `BGEVENT_READ` | `BugCatchingContestExplanationSign` (the label defined in `maps/Route35NationalParkGate.asm`) |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE36NATIONALPARKGATE_OFFICER1` | `SPRITE_OFFICER` | 0 | 3 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `Route36OfficerScriptContest` (`56:67cc`) | `EVENT_ROUTE_36_NATIONAL_PARK_GATE_OFFICER_CONTEST_DAY` |
| `..._YOUNGSTER1` | `SPRITE_YOUNGSTER` | 2 | 5 | `STANDING_UP` | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant1BScript` | `EVENT_BUG_CATCHING_CONTESTANT_1B` |
| `..._YOUNGSTER2` | `SPRITE_YOUNGSTER` | 4 | 5 | `STANDING_UP` | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant2BScript` | `..._2B` |
| `..._ROCKER` | `SPRITE_ROCKER` | 2 | 6 | `STANDING_UP` | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant3BScript` | `..._3B` |
| `..._POKEFAN_M` | `SPRITE_POKEFAN_M` | 6 | 5 | `STANDING_UP` | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant4BScript` | `..._4B` |
| `..._YOUNGSTER3` | `SPRITE_YOUNGSTER` | 2 | 7 | `STANDING_UP` | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant5BScript` | `..._5B` |
| `..._YOUNGSTER4` | `SPRITE_YOUNGSTER` | 5 | 6 | `STANDING_UP` | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant6BScript` | `..._6B` |
| `..._LASS` | `SPRITE_LASS` | 3 | 6 | `STANDING_UP` | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant7BScript` | `..._7B` |
| `..._YOUNGSTER5` | `SPRITE_YOUNGSTER` | 4 | 7 | `STANDING_UP` | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant8BScript` | `..._8B` |
| `..._YOUNGSTER6` | `SPRITE_YOUNGSTER` | 6 | 7 | `STANDING_UP` | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant9BScript` | `..._9B` |
| `..._YOUNGSTER7` | `SPRITE_YOUNGSTER` | 6 | 6 | `STANDING_UP` | `OBJECTTYPE_SCRIPT` | `BugCatchingContestant10BScript` | `..._10B` |
| `ROUTE36NATIONALPARKGATE_OFFICER2` | `SPRITE_OFFICER` | 3 | 2 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `Route36NationalParkGateOfficerScript` | `EVENT_ROUTE_36_NATIONAL_PARK_GATE_OFFICER_NOT_CONTEST_DAY` |
**Callbacks**
- `MAPCALLBACK_NEWMAP` -> `Route36NationalParkGateCheckIfContestRunningCallback`
(same shape as the south gate).
- `MAPCALLBACK_OBJECTS` -> `Route36NationalParkGateCheckIfContestAvailableCallback`
(`56:6710`): first `checkevent EVENT_WARPED_FROM_ROUTE_35_NATIONAL_PARK_GATE` -> return
unchanged (this is how the awards ceremony keeps the contest officer standing there when
you finished from the *south* gate). Then weekday / `ENGINE_BUG_CONTEST_TIMER`, same as
the south gate.
**Scripts of interest**
- `Route36OfficerScriptContest` (`56:67cc`) - same structure as the south gate's officer
but warps to `NATIONAL_PARK_BUG_CONTEST, 33, 18` and, if `ENGINE_DAILY_BUG_CONTEST` is
already set, falls into `Route36Officer_ContestHasConcluded`, which is the **prize
holding desk**: it checks `EVENT_CONTEST_OFFICER_HAS_SUN_STONE`, then `_EVERSTONE`, then
`_GOLD_BERRY`, then `_BERRY`, `verbosegiveitem`s the first one it finds and clears that
event. A bot that won with a full PACK collects here.
- `Route36NationalParkGateLeavingContestEarlyScript` - the finish-now flow.
`special FadeOutToBlack`, `.CopyContestants` (turns each unset `..._NA` flag into an
`appear` of the matching `B` object), `disappear OFFICER1` / `appear OFFICER2`,
`applymovement PLAYER, Route36NationalParkGatePlayerWaitWithContestantsMovement`
(`big_step DOWN`, `big_step RIGHT`, `turn_head UP`), then
`jumpstd BugContestResultsScript`.
---
### MAP_ROUTE_36
- Script: `maps/Route36.asm`
- Blocks: `maps/Route36.blk` (270 bytes = 30 x 9 blocks)
- Header (`data/maps/maps.asm:249`): `TILESET_JOHTO, ROUTE, LANDMARK_ROUTE_36, MUSIC_ROUTE_36, FALSE, PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:228` -> `map_const ROUTE_36, 30, 9` (cells
0..59 x, 0..17 y)
- Connections (`data/maps/attributes.asm:206`): north `ROUTE_37` offset 10, south
`ROUTE_35` offset 0, east `VIOLET_CITY` offset 0
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 18 | 8 | `ROUTE_36_NATIONAL_PARK_GATE` | 3 |
| 2 | 18 | 9 | `ROUTE_36_NATIONAL_PARK_GATE` | 4 |
| 3 | 47 | 13 | `ROUTE_36_RUINS_OF_ALPH_GATE` | 1 |
| 4 | 48 | 13 | `ROUTE_36_RUINS_OF_ALPH_GATE` | 2 |
**Coord events** - none. (Sudowoodo is an object, not a trip-wire.)
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 29 | 1 | `BGEVENT_READ` | `Route36TrainerTips2` |
| 45 | 11 | `BGEVENT_READ` | `RuinsOfAlphNorthSign` |
| 55 | 7 | `BGEVENT_READ` | `Route36Sign` |
| 21 | 7 | `BGEVENT_READ` | `Route36TrainerTips1` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE36_YOUNGSTER1` | `SPRITE_YOUNGSTER` | 20 | 12 | `STANDING_RIGHT` | `OBJECTTYPE_TRAINER` sight 2 | `TrainerPsychicMark` | -1 |
| `ROUTE36_YOUNGSTER2` | `SPRITE_YOUNGSTER` | 31 | 14 | `STANDING_LEFT` | `OBJECTTYPE_TRAINER` sight 5 | `TrainerSchoolboyAlan1` | -1 |
| `ROUTE36_WEIRD_TREE` | `SPRITE_WEIRD_TREE` | 35 | 9 | `SPRITEMOVEDATA_SUDOWOODO` | `OBJECTTYPE_SCRIPT` | `SudowoodoScript` (`4b:61aa`) | `EVENT_ROUTE_36_SUDOWOODO` |
| `ROUTE36_LASS1` | `SPRITE_LASS` | 51 | 8 | `WALK_LEFT_RIGHT` (2,0) | `OBJECTTYPE_SCRIPT` | `Route36LassScript` (`4b:621e`) | -1 |
| `ROUTE36_FISHER` | `SPRITE_FISHER` | 44 | 9 | `STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `Route36RockSmashGuyScript` (`4b:61f7`) | -1 |
| `ROUTE36_FRUIT_TREE` | `SPRITE_FRUIT_TREE` | 21 | 4 | `STILL` | `OBJECTTYPE_SCRIPT` | `Route36FruitTree` (`4b:631a`) | -1 |
| `ROUTE36_ARTHUR` | `SPRITE_YOUNGSTER` | 46 | 6 | `WANDER` (1,1) | `OBJECTTYPE_SCRIPT` | `ArthurScript` (`4b:62d4`) | `EVENT_ROUTE_36_ARTHUR_OF_THURSDAY` |
**Callbacks**
- `MAPCALLBACK_OBJECTS` -> `Route36ArthurCallback` (`4b:619e`): `readvar VAR_WEEKDAY`,
`ifequal THURSDAY, .ArthurAppears` (-> `appear ROUTE36_ARTHUR`), otherwise
`disappear ROUTE36_ARTHUR`.
**Scripts of interest**
- `SudowoodoScript` (`4b:61aa`) - the gate of this section.
`checkitem SQUIRTBOTTLE` / `iftrue .Fight`. Without it: `waitsfx`,
`playsound SFX_SANDSTORM`, `applymovement ROUTE36_WEIRD_TREE, SudowoodoShakeMovement`
(`tree_shake`), `end` - the tree just wobbles and stays solid.
With it: `writetext UseSquirtbottleText`, `yesorno`, `iffalse DidntUseSquirtbottleScript`.
- `WateredWeirdTreeScript::` (`4b:61c1`) - exported so that using the SQUIRTBOTTLE from the
PACK reaches the same body (`engine/events/squirtbottle.asm` does
`farsjump WateredWeirdTreeScript`). Body: text, shake, `SudowoodoAttackedText`,
**`loadwildmon SUDOWOODO, 20`**, `startbattle`, `setevent EVENT_FOUGHT_SUDOWOODO`,
`ifequal DRAW, DidntCatchSudowoodo`, `disappear ROUTE36_WEIRD_TREE`,
`variablesprite SPRITE_WEIRD_TREE, SPRITE_TWIN`, `reloadmapafterbattle`.
Note the ordering: `EVENT_FOUGHT_SUDOWOODO` is set **before** the DRAW check, so a run
or a KO still clears the road and still unlocks TM08.
- `DidntCatchSudowoodo` (`4b:61e9`) - `reloadmapafterbattle`,
`applymovement ROUTE36_WEIRD_TREE, WeirdTreeMovement_Flee` (two `fast_jump_step UP`),
`disappear`, `variablesprite`, `special LoadUsedSpritesGFX`.
- `_Squirtbottle` (`14:4763`, `engine/events/squirtbottle.asm`) - the PACK path.
`.CheckCanUseSquirtbottle` requires `wMapGroup == GROUP_ROUTE_36`,
`wMapNumber == MAP_ROUTE_36`, `GetFacingObject` to succeed, and that object's movement
data to be `SPRITEMOVEDATA_SUDOWOODO`. Anything else prints
`_SquirtbottleNothingText`.
- `Route36RockSmashGuyScript` (`4b:61f7`) - `checkevent EVENT_GOT_TM08_ROCK_SMASH` ->
`.AlreadyGotRockSmash`; `checkevent EVENT_FOUGHT_SUDOWOODO` -> `.ClearedSudowoodo`
(`verbosegiveitem TM_ROCK_SMASH`, `iffalse .NoRoomForTM`,
`setevent EVENT_GOT_TM08_ROCK_SMASH`). Before Sudowoodo it just prints
`RockSmashGuyText1`.
- `ArthurScript` (`4b:62d4`) - `checkevent EVENT_GOT_HARD_STONE_FROM_ARTHUR` ->
`.AlreadyGotStone`; `readvar VAR_WEEKDAY` / `ifnotequal THURSDAY, ArthurNotThursdayScript`;
else `setevent EVENT_MET_ARTHUR_OF_THURSDAY`, `verbosegiveitem HARD_STONE`,
`iffalse .BagFull`, `setevent EVENT_GOT_HARD_STONE_FROM_ARTHUR`.
- `Route36LassScript` (`4b:621e`) - flavour, branches on `EVENT_FOUGHT_SUDOWOODO`. This is
the girl at (51, 8). She is **not** Floria and gives nothing.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_ROUTE_36_SUDOWOODO` | `constants/event_flags.asm:1178` | object visibility flag; cleared by `disappear` in `WateredWeirdTreeScript` | tree present on the map |
| `EVENT_FOUGHT_SUDOWOODO` | `constants/event_flags.asm:51` | set by `WateredWeirdTreeScript`; read by the Rock Smash guy, the Route 36 lass, the Route 35 gate pokefan | **the** progress flag of this section |
| `EVENT_GOT_TM08_ROCK_SMASH` | `constants/event_flags.asm:84` | `Route36RockSmashGuyScript` | TM08 taken |
| `EVENT_ROUTE_36_ARTHUR_OF_THURSDAY` | `constants/event_flags.asm:1276` | `Route36ArthurCallback` | Arthur visible |
| `EVENT_MET_ARTHUR_OF_THURSDAY` | `constants/event_flags.asm:111` | `ArthurScript` | first-meeting text |
| `EVENT_GOT_HARD_STONE_FROM_ARTHUR` | `constants/event_flags.asm:112` | `ArthurScript` | HARD STONE taken |
| `EVENT_BEAT_PSYCHIC_MARK`, `EVENT_BEAT_SCHOOLBOY_ALAN` | `constants/event_flags.asm` | `trainer` rows | defeated bits |
| `EVENT_ALAN_ASKED_FOR_PHONE_NUMBER`, `EVENT_ALAN_READY_FOR_REMATCH` | `constants/event_flags.asm` | Alan script | phone state machine |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `ICE_BERRY` | fruit tree object at (21, 4) | `FRUITTREE_ROUTE_36` -> `data/items/fruit_trees.asm:16` | daily |
| `TM_ROCK_SMASH` (TM08) | talk to the fisher at (44, 9) after Sudowoodo | `Route36RockSmashGuyScript` | `EVENT_GOT_TM08_ROCK_SMASH` |
| `HARD_STONE` | talk to Arthur at (46, 6) on a Thursday | `ArthurScript` | `EVENT_GOT_HARD_STONE_FROM_ARTHUR` |
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `MARK` | `PSYCHIC_T` (0x34) | 7 | `parties.asm:2545` `MARK` `TRAINERTYPE_MOVES` - lv13 ABRA (TELEPORT, FLASH), lv13 ABRA (TELEPORT, FLASH), lv15 KADABRA (TELEPORT, KINESIS, CONFUSION) | `TrainerPsychicMark` | no |
| `ALAN1` | `SCHOOLBOY` (0x17) | 3 | `parties.asm:423` `ALAN` - lv16 TANGELA | `TrainerSchoolboyAlan1` | yes (`PHONE_SCHOOLBOY_ALAN`); ALAN2 `parties.asm:496`, ALAN3 `parties.asm:502` |
**Wild** - `data/wild/johto_grass.asm`, `def_grass_wildmons ROUTE_36`, 10/10/10 percent.
Gold:
| slot | morn | day | nite |
|---|---|---|---|
| 1 | 12 NIDORAN_M | 12 NIDORAN_M | 12 NIDORAN_M |
| 2 | 12 NIDORAN_F | 12 NIDORAN_F | 12 NIDORAN_F |
| 3 | 13 PIDGEY | 14 PIDGEY | 13 HOOTHOOT |
| 4 | 13 GROWLITHE | 13 GROWLITHE | 13 GROWLITHE |
| 5 | 13 STANTLER | 13 STANTLER | 13 STANTLER |
| 6 | 15 PIDGEY | 15 GROWLITHE | 15 HOOTHOOT |
| 7 | 15 PIDGEY | 15 GROWLITHE | 15 HOOTHOOT |
(Silver substitutes VULPIX for GROWLITHE and swaps the NIDORAN order.) No `ROUTE_36`
entry in `johto_water.asm`. Headbutt: `data/wild/treemon_maps.asm:17` -> `TREEMON_SET_FOREST`.
`ROUTE_36` is a roam node in `data/wild/roammon_maps.asm:26` with four exits
(`ROUTE_35, ROUTE_31, ROUTE_32, ROUTE_37`).
---
### MAP_ROUTE_37 (header-listed only; not entered by this walkthrough section)
- Script: `maps/Route37.asm`, Blocks: `maps/Route37.blk`
- Header (`data/maps/maps.asm:250`): `TILESET_JOHTO, ROUTE, LANDMARK_ROUTE_37, MUSIC_ROUTE_36, FALSE, PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:229` -> `map_const ROUTE_37, 10, 9`
- Connections (`data/maps/attributes.asm:211`): north `ECRUTEAK_CITY` offset -5,
south `ROUTE_36` offset -10
- **Warps: `def_warp_events` is empty.** Route 37 is reached only by connection.
- BG events: (5,3) `BGEVENT_READ` `Route37Sign`; (4,2) `BGEVENT_ITEM`
`Route37HiddenEther` -> `hiddenitem ETHER, EVENT_ROUTE_37_HIDDEN_ETHER`.
- Objects: Twins Ann & Anne at (6,12) and (7,12) (`ANNANDANNE1` / `ANNANDANNE2`, both
sharing `EVENT_BEAT_TWINS_ANN_AND_ANNE`), Psychic Greg at (9,6), three fruit trees at
(13,5) `FRUITTREE_ROUTE_37_1` = `RED_APRICORN`, (16,5) `_2` = `BLU_APRICORN`,
(15,7) `_3` = `BLK_APRICORN` (`data/items/fruit_trees.asm:20-22`), and Sunny of Sunday
at (16,8) behind `EVENT_ROUTE_37_SUNNY_OF_SUNDAY` (gift `MAGNET`,
`EVENT_GOT_MAGNET_FROM_SUNNY`).
- Callback: `MAPCALLBACK_OBJECTS` -> `Route37SunnyCallback` (weekday == SUNDAY).
- Wild: `data/wild/johto_grass.asm` `def_grass_wildmons ROUTE_37`, 10/10/10; Gold nite
slot 1/5/6/7 are SPINARAK 13/15/15/15, Silver morn is LEDYBA - this is where the
walkthrough's "Ledyba (Silver only, morning only) / Spinarak (Gold only, night only)"
actually lives.
---
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| SQUIRTBOTTLE not obtainable | `maps/GoldenrodFlowerShop.asm:FlowerShopTeacherScript` (`57:52d1`) - `checkflag ENGINE_PLAINBADGE` / `iffalse .Lalala` | Plain Badge (Whitney) | beat Whitney; talk to the teacher at (2,4) |
| Sudowoodo blocks Route 36 | `maps/Route36.asm:SudowoodoScript` (`4b:61aa`) - the object at cell (35,9) with `SPRITEMOVEDATA_SUDOWOODO` is a solid NPC; the script's `checkitem SQUIRTBOTTLE` / `iftrue .Fight` is the only way past | SQUIRTBOTTLE in the PACK | `EVENT_FOUGHT_SUDOWOODO` set by `WateredWeirdTreeScript` (`4b:61c1`); the `disappear` runs on any outcome except a draw-flee, and the flee path disappears it too |
| SQUIRTBOTTLE from the PACK does nothing elsewhere | `engine/events/squirtbottle.asm:_Squirtbottle` (`14:4763`) `.CheckCanUseSquirtbottle` | map must be `ROUTE_36` and the faced object must carry `SPRITEMOVEDATA_SUDOWOODO` | face the tree first |
| TM08 ROCK SMASH withheld | `maps/Route36.asm:Route36RockSmashGuyScript` (`4b:61f7`) - `checkevent EVENT_FOUGHT_SUDOWOODO` | Sudowoodo dealt with | talk to the fisher at (44, 9) |
| East side of Route 35 (Arnie, Bryan, TM04) | `maps/Route35.blk` block `$5b` at block (8,3) = cells x16-17 y6-7; `data/collision/field_move_blocks.asm:14` | CUT, which needs `ENGINE_HIVEBADGE` (`engine/events/overworld.asm:133` `CutFunction.CheckAble`) and a party mon that knows CUT | Azalea's badge + HM01; or enter from Route 36 via the south connection, which is what the walkthrough does |
| Bug Contest closed | `maps/Route35NationalParkGate.asm:Route35OfficerScriptContest` (`56:5e0c`) and `maps/Route36NationalParkGate.asm:Route36OfficerScriptContest` (`56:67cc`) - `readvar VAR_WEEKDAY`, four `ifequal` bail-outs, plus `checkflag ENGINE_DAILY_BUG_CONTEST` | Tuesday, Thursday or Saturday, and not already run today | set the RTC weekday; `ENGINE_DAILY_BUG_CONTEST` clears on the daily reset |
| Contest entry refused | same scripts - `readvar VAR_PARTYCOUNT` / `VAR_BOXSPACE` / `special CheckFirstMonIsEgg` / `special ContestDropOffMons` | lead mon is not an EGG, is not fainted, and there is party-or-box room for a caught bug | reorder the party, heal, free a box slot |
| Kenya not given | `maps/Route35GoldenrodGate.asm:RandyScript` (`56:59ce`) - `readvar VAR_PARTYCOUNT` / `ifequal PARTY_LENGTH, .partyfull` | fewer than 6 party mons | deposit one |
| Arthur / Sunny absent | `Route36ArthurCallback` (`4b:619e`) / `Route37SunnyCallback` - `MAPCALLBACK_OBJECTS` + `VAR_WEEKDAY` | Thursday / Sunday | RTC weekday |
| Officer Dirk will not battle | `maps/Route35.asm:TrainerOfficerDirk` - `checktime NITE` / `iffalse .NotNight` | night time of day | wait, or set the RTC |
---
## 4. Bot checklist
Coordinates are `(x, y)` walk cells as written in the asm. "talk" means stand adjacent
facing the target and press A.
1. `MAP_GOLDENROD_CITY` -> warp 6 at (33, 5). Pre: `ENGINE_PLAINBADGE` set. Post: on
`MAP_GOLDENROD_FLOWER_SHOP`.
2. `MAP_GOLDENROD_FLOWER_SHOP`: talk to `GOLDENRODFLOWERSHOP_TEACHER` at (2, 4).
Pre: `!EVENT_GOT_SQUIRTBOTTLE` and a free KEY ITEM slot. Post:
`EVENT_GOT_SQUIRTBOTTLE`, SQUIRTBOTTLE in the PACK.
3. Exit via warp 1 (2, 7) -> `MAP_GOLDENROD_CITY` warp 6. Walk to (19, 1), warp 13 ->
`MAP_ROUTE_35_GOLDENROD_GATE` warp 3.
4. Optional: talk to `ROUTE35GOLDENRODGATE_RANDY` at (0, 4), answer YES.
Pre: `VAR_PARTYCOUNT < 6`. Post: `EVENT_GOT_KENYA`, lv10 SPEAROW "KENYA" with
`FLOWER_MAIL` in the last party slot.
5. Warp 1 at (4, 0) -> `MAP_ROUTE_35` warp 1 (lands at (9, 33)).
6. `MAP_ROUTE_35`, walk north. Battle, in the walkthrough's order:
Kim (11, 24), Elliot (8, 20), Brooke (7, 20), Ivan (3, 19), Irwin (5, 10),
Walt (2, 10). Post: `EVENT_BEAT_*` for each. Sight ranges are 3/3/3/4/2/2.
7. Optional, night only: talk to `ROUTE35_OFFICER` at (5, 6). Pre: `checktime NITE` true
and `!EVENT_BEAT_OFFICER_DIRK`. Post: `EVENT_BEAT_OFFICER_DIRK`.
8. Walk to (3, 5), warp 3 -> `MAP_ROUTE_35_NATIONAL_PARK_GATE` warp 3.
9. Optional Contest: on a Tue/Thu/Sat with `!ENGINE_DAILY_BUG_CONTEST`, talk to
`ROUTE35NATIONALPARKGATE_OFFICER1` at (2, 1) and answer YES. Post:
`ENGINE_BUG_CONTEST_TIMER` set, 20 PARK BALLs, warp to
`MAP_NATIONAL_PARK_BUG_CONTEST` (10, 47). Finish by re-entering either gate and
answering YES, or by running the timer/balls out. Post: `ENGINE_DAILY_BUG_CONTEST`,
prize per placing, `ENGINE_BUG_CONTEST_TIMER` cleared.
10. Warp 1 at (3, 0) -> `MAP_NATIONAL_PARK` warp 3 (lands at (10, 47)).
11. `MAP_NATIONAL_PARK`: talk to `NATIONALPARK_TEACHER1` at (27, 40).
Pre: `!EVENT_GOT_QUICK_CLAW`, PACK room. Post: `EVENT_GOT_QUICK_CLAW`.
12. Optional: item ball at (1, 43) -> `TM_DIG`, flag `EVENT_NATIONAL_PARK_TM_DIG`.
Hidden `FULL_HEAL` at bg (6, 47), flag `EVENT_NATIONAL_PARK_HIDDEN_FULL_HEAL`.
Item ball at (35, 12) -> `PARLYZ_HEAL`, flag `EVENT_NATIONAL_PARK_PARLYZ_HEAL`.
13. Battle Beverly (18, 29), Jack (27, 23), Krise (8, 14), William (16, 9).
14. Walk to (33, 18), warp 1 -> `MAP_ROUTE_36_NATIONAL_PARK_GATE` warp 1.
If a prize was held: talk to `ROUTE36NATIONALPARKGATE_OFFICER1` at (0, 3) with
`ENGINE_DAILY_BUG_CONTEST` set to collect it.
15. Warp 3 at (9, 4) -> `MAP_ROUTE_36` warp 1 (lands at (18, 8)).
16. `MAP_ROUTE_36`: fruit tree at (21, 4) -> `ICE_BERRY`. Battle Psychic Mark at (20, 12).
17. Leave south (`ROUTE_36` south connection, offset 0) into `MAP_ROUTE_35`'s north edge.
18. `MAP_ROUTE_35` east side: CUT the tree at block (8, 3) (cells x16-17, y6-7).
Pre: `ENGINE_HIVEBADGE` + a mon knowing CUT. Then battle Arnie at (16, 7) and
Bryan at (14, 28), and take the item ball at (13, 16) -> `TM_ROLLOUT`
(`EVENT_ROUTE_35_TM_ROLLOUT`). Talk to Arnie a second time for
`PHONE_BUG_CATCHER_ARNIE`.
19. Return north to `MAP_ROUTE_36`. Battle Schoolboy Alan at (31, 14); talk again for
`PHONE_SCHOOLBOY_ALAN`.
20. Walk to face `ROUTE36_WEIRD_TREE` at (35, 9) and press A. Pre: SQUIRTBOTTLE in the
PACK (`checkitem`). Answer YES. A lv20 SUDOWOODO wild battle starts
(`loadwildmon SUDOWOODO, 20`). Post: `EVENT_FOUGHT_SUDOWOODO` set regardless of
outcome; `EVENT_ROUTE_36_SUDOWOODO` cleared, sprite swapped to `SPRITE_TWIN`.
21. Talk to `ROUTE36_FISHER` at (44, 9). Pre: `EVENT_FOUGHT_SUDOWOODO`, PACK room.
Post: `EVENT_GOT_TM08_ROCK_SMASH`, TM08 ROCK SMASH.
22. Optional, Thursday only: talk to `ROUTE36_ARTHUR` at (46, 6). Post:
`EVENT_MET_ARTHUR_OF_THURSDAY`, `EVENT_GOT_HARD_STONE_FROM_ARTHUR`, HARD STONE.
23. Continue east past x=59 on the `ROUTE_36` east connection into `MAP_VIOLET_CITY`
(next section).
---
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Map geometry, warps, bg/object events for all nine maps | `src/import/RomExtractorGen2.lua` (extracted into the cache), consumed by `src/world/gen2/World.lua` / `Map.lua` | implemented (data-driven; nothing map-specific is hand written) |
| Script bytecode for every label above (`FlowerShopTeacherScript`, `RandyScript`, `SudowoodoScript`, `Route36RockSmashGuyScript`, `ArthurScript`, both contest officers, `BugContestResultsScript`) | `src/script/gen2/Vm.lua` + extracted bytecode | implemented - the opcodes these scripts use (`checkevent`, `checkflag`, `checkitem`, `verbosegiveitem`, `givepoke`, `givepokemail`, `loadwildmon`, `variablesprite`, `appear`/`disappear`, `setscene`, `sdefer`, `askforphonenumber`, `checkcellnum`, `checktime`, `fruittree`) all have branches in `Vm.lua` / `Opcodes.lua` |
| `VAR_WEEKDAY` (Arthur, Sunny, contest days) | `src/world/gen2/World.lua:1201` `World:weekday`, `World.lua:101` `VAR_WEEKDAY = 0x0b` | implemented |
| `MAPCALLBACK_OBJECTS` day-of-week NPC swaps | `src/world/gen2/World.lua:5991` `World:runMapCallback`, called from `World.lua:5700` | implemented; assertion driver `tests/drivers/gold_map_callbacks.lua` exists but does **not** cover Route 36 / Route 37 / either park gate |
| Bug Catching Contest rules, timer, park balls, scoring, AI contestants, prizes | `src/core/gen2/BugContest.lua` (815 lines; transcribed from `bug_contest/judging.asm`, `contest.asm`, `contest_2.asm`, `caught_mon.asm`, `bug_contest_mons.asm`) | implemented - `BugContest.score`, `rollContestant`, `judge`, `prizeFor`, `pickContestants`, `dropOffMons`, `returnMons`, `start/stop/tickTimer`, `chooseWild` |
| Contest comparison / debug UI | `src/ui/gen2/ContestMenu.lua` | implemented (a comparison screen, not the cart's HUD) |
| Contest super-tall-grass doubled encounter rate | `src/world/gen2/Permissions.lua:85` `SUPER_TALL_GRASS`, `BugContest.encounterRate` | implemented |
| Squirtbottle field check | `src/script/gen2/CallAsm.lua:411` `H.CheckCanUseSquirtbottle` (registered as `"14:4786"`), tested in `tests/gen2_callasm_test.lua:224-244` | implemented |
| **Using the SQUIRTBOTTLE from the PACK** | `src/world/gen2/World.lua:3294` `World:useFieldItem` | **missing** - the dispatch has arms only for `ITEMFINDER`, `BICYCLE`, `SACRED_ASH`, repels, trophy boxes and rods. There is no `SQUIRTBOTTLE` arm, so `_Squirtbottle` is unreachable from the PACK. The talk-to-the-tree path (`SudowoodoScript`'s `checkitem`) still works, which is the route the walkthrough takes |
| **Item ball pickup** (TM04 on Route 35, TM28 DIG and PARLYZ HEAL in National Park) | extractor reads them (`src/import/RomExtractorGen2.lua:2969` `obj.itemball = readItemBall(...)`) but `src/world/gen2/World.lua:5257` `World:interact` only dispatches on `npc.def.trainer`, strength boulders, `npc.def.scriptKey`, bg events and hidden items | **missing** - `OBJECTTYPE_ITEMBALL` objects have no `scriptKey` by design, and nothing consumes `obj.itemball`. A bot cannot pick up any of this section's three item balls |
| Hidden item (National Park FULL HEAL, Route 37 ETHER) | `src/world/gen2/HiddenItems.lua`, wired into `World:interact` | implemented |
| Fruit trees (MYSTERYBERRY, ICE BERRY, the Route 37 apricorns) | `src/core/gen2/Apricorns.lua` + the VM's `fruittree` branch | implemented |
| Overworld trainers, sight ranges, `loadtrainer` / `startbattle` | `src/world/gen2/Trainers.lua`, `src/battle/gen2/Battle.lua` | implemented |
| Phone numbers and rematch tiers (Irwin, Arnie, Jack, Beverly, Alan) | `src/core/gen2/Phone.lua` (Jack, Beverly, Irwin, Arnie, Alan all have callee/caller script keys and `..._READY_FOR_REMATCH` event ids) | implemented |
| Kenya the SPEAROW + FLOWER MAIL | `src/core/gen2/Mail.lua` + `Vm.lua:439` `givepoke` / `Vm.lua:462` `givepokemail` | implemented (extractor resolves `cmd.mail`) |
| CUT field move + `ENGINE_HIVEBADGE` gate + the Route 35 tree block | `src/world/gen2/FieldMoves.lua:104` (`CUT = "HIVE"`), `FieldMoves.somethingToCut`, `World:tryCutOW` | implemented |
| Sudowoodo wild battle + DRAW-flee branch | `src/script/gen2/Vm.lua:44` (DRAW), `Vm.lua:836` `loadwildmon`, `World.lua:5067` (`variablesprite` with a raw sprite id) | implemented |
| Driver / regression coverage for this specific stretch | `tests/drivers/gold_*.lua` | **missing** - there is no driver that walks Route 35 -> National Park -> Route 36, runs a contest, or fights Sudowoodo. `tests/gen2_contest_test.lua` covers the contest rules headlessly; `tests/gen2_callasm_test.lua` covers only the Squirtbottle facing check |
---
## 6. Unresolved / verify by hand
1. **"Talk to Floria... while she goes away to get the Squirtbottle."** No such script
exists in pokegold. `maps/GoldenrodFlowerShop.asm` has a `GOLDENRODFLOWERSHOP_FLORIA`
object at (5, 6) whose `FlowerShopFloriaScript` only prints text, and the girl on
Route 36 at (51, 8) is an anonymous `ROUTE36_LASS1` running `Route36LassScript`, which
also only prints text. The SQUIRTBOTTLE comes from `FlowerShopTeacherScript` gated on
`ENGINE_PLAINBADGE` alone, with no Route 36 precondition. The described sequence looks
like Crystal behaviour that leaked into a Gold/Silver walkthrough.
2. **Contest Pokemon list.** The walkthrough lists "Caterpie/Weedle, Nincada, Scyther,
Pinsir, Volbeat, Wurmple" at "Levels 28-31". `data/wild/bug_contest_mons.asm` has
CATERPIE, WEEDLE, METAPOD, KAKUNA, BUTTERFREE, BEEDRILL, VENONAT, PARAS, SCYTHER,
PINSIR at levels 7-18 (plus VENOMOTH 30-40 on the `-1` terminator row, which is never
selected). NINCADA, VOLBEAT and WURMPLE do not exist in Gen 2 at all. Treat the
walkthrough's list and levels as wrong for this game.
3. **"20 Sport Balls."** The item is `PARK_BALL` (`constants/item_constants.asm:185`),
given by `special GiveParkBalls`, count `BUG_CONTEST_BALLS = 20`. "Sport Ball" is the
Gen 3+ name.
4. **"If you win the contest, you can win a Shiny Stone!"** First place gives `SUN_STONE`
(`BugContestResults_FirstPlace`, `engine/events/std_scripts.asm:346`). The
walkthrough's own item list ("Sun Stone (1st Prize)") agrees; the prose line does not.
There is no Shiny Stone in Gen 2.
5. **"If you catch a Scyther at full HP, it may be worth around 342 points."** Not
verifiable from the asm without a concrete mon: `ContestScore` (`04:7cbc`) is
`MaxHP*4 + Atk + Def + Spe + SpA + SpD + DVbonus + HP/8 + heldItem`, all taken from the
high byte of each stat word. The walkthrough's qualitative claim ("score takes HP into
consideration") is confirmed; the specific number is not checked here.
6. **National Park species list.** The walkthrough lists Butterfree, Weedle, Kakuna,
Beedrill, Paras, Venonat, Scyther and Pinsir as "found in National Park". None of them
appear in `def_grass_wildmons NATIONAL_PARK`; they are Contest-only
(`data/wild/bug_contest_mons.asm`). Sunkern, Caterpie, Metapod, Pidgey and Hoothoot are
the only real National Park grass encounters in Gold.
7. **"Routes 36 & 37" item and species lists are merged.** Everything the walkthrough
attributes to "Routes 36 & 37" except the ICE BERRY, HARD STONE and TM08 actually lives
on Route 37 (`RED_APRICORN`, `BLU_APRICORN`, `BLK_APRICORN`; Ledyba/Spinarak/Pidgeotto),
which this section never enters. Vulpix/Growlithe and Stantler appear on both routes.
8. **"Route 35 & National Park" item list omits the hidden FULL HEAL** at National Park
bg (6, 47) and lists no hidden items at all. Not a contradiction, just incomplete.
9. **"Head back to Arnie and talk to *her*."** `BUG_CATCHER, ARNIE1` uses the male phone
std scripts (`AskNumber1MScript`, `RegisteredNumberMScript`). Cosmetic only.
10. **"Then, we're ready to head into Route 35 after Liz's offer for a rematch on Route
32."** Liz's rematch is a phone event outside this section's maps; no code on any map
in this section references it, so it was not resolved here.
11. **`EVENT_FOUGHT_SUDOWOODO` is set before the DRAW check** in
`WateredWeirdTreeScript`, and both the win branch and `DidntCatchSudowoodo` run
`disappear ROUTE36_WEIRD_TREE`. So the walkthrough's "catch it or else" is a
preference, not a gate - fainting or fleeing still opens the road and still unlocks
TM08. Worth confirming in-game that a *black-out* mid-battle does not leave a
half-applied state, which the asm alone cannot answer.
12. **`MAP_NATIONAL_PARK_BUG_CONTEST` warps 2 and 4 point at destination warp 1**, not 2,
in `maps/NationalParkBugContest.asm:228,230`. Transcribed verbatim; a bot pathing off
those rows should expect the asymmetry rather than assume a typo.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,681 @@
# Section 09 - Olivine City Gym
Source: `../section-09-olivine-city-gym.txt`
Maps covered: `MAP_GOLDENROD_CITY` (fly stop, optional), `MAP_GOLDENROD_UNDERGROUND_SWITCH_ROOM_ENTRANCES` (transit), `MAP_GOLDENROD_UNDERGROUND`, `MAP_OLIVINE_CITY`, `MAP_OLIVINE_LIGHTHOUSE_1F` .. `MAP_OLIVINE_LIGHTHOUSE_6F`, `MAP_OLIVINE_GYM`
Badges / key milestones in this section: **MINERALBADGE** (`ENGINE_MINERALBADGE`), `EVENT_BEAT_JASMINE`, `TM_IRON_TAIL` (TM23), `EVENT_JASMINE_RETURNED_TO_GYM` (set on Lighthouse 6F, the gate for the gym even existing), the 6th-badge Rocket trigger (`GoldenrodRocketsScript`).
This section assumes the player already has the SECRETPOTION from Cianwood (section 8) and
`ENGINE_STORMBADGE` (Chuck), which is what makes `FLY` usable at all.
---
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 1 | `GOLDENROD_CITY` (optional) | `maps/GoldenrodCity.asm` | Fly (`SPAWN_GOLDENROD`, `ENGINE_FLYPOINT_GOLDENROD`) | warp 15 at (11, 29) -> `GOLDENROD_UNDERGROUND_SWITCH_ROOM_ENTRANCES` warp 5 | Only if raising an Eevee: buy the 500 haircut for happiness |
| 2 | `GOLDENROD_UNDERGROUND_SWITCH_ROOM_ENTRANCES` | `maps/GoldenrodUndergroundSwitchRoomEntrances.asm` | warp 5 at (4, 29) / warp 6 at (5, 29) | warp 4 at (5, 25) -> `GOLDENROD_UNDERGROUND` warp 2 | Pure transit corridor to the salon |
| 3 | `GOLDENROD_UNDERGROUND` | `maps/GoldenrodUnderground.asm` | warp 2 at (3, 34) | back out the way in, then Fly | Talk to the *older* Haircut Brother at (7, 14), pay 500, happiness bump |
| 4 | `OLIVINE_CITY` | `maps/OlivineCity.asm` | Fly (`SPAWN_OLIVINE`, `ENGINE_FLYPOINT_OLIVINE`) | warp 9 at (29, 27) -> `OLIVINE_LIGHTHOUSE_1F` warp 1 | Head for the lighthouse |
| 5 | `OLIVINE_LIGHTHOUSE_1F` -> `2F` -> `3F` -> `4F` -> `3F` -> `4F` -> `5F` -> `6F` | `maps/OlivineLighthouse[1-6]F.asm` | ladders + one floor pit ("fall to the left of that one lass") | same ladders/pits back down | Hand Jasmine the SECRETPOTION |
| 6 | `OLIVINE_LIGHTHOUSE_6F` | `maps/OlivineLighthouse6F.asm` | warp 1 at (9, 15) from 5F warp 1 | warp 1 at (9, 15) back to 5F | `OlivineLighthouseJasmine` -> `EVENT_JASMINE_RETURNED_TO_GYM`, `clearevent EVENT_OLIVINE_GYM_JASMINE` |
| 7 | `OLIVINE_CITY` | `maps/OlivineCity.asm` | `OLIVINE_LIGHTHOUSE_1F` warps 1/2 at (10, 17)/(11, 17) | warp 2 at (10, 11) -> `OLIVINE_GYM` warp 1 | Walk to the gym door |
| 8 | `OLIVINE_GYM` | `maps/OlivineGym.asm` | warps 1/2 at (4, 15)/(5, 15) | same warps back to `OLIVINE_CITY` warp 2 | Beat Jasmine, take MINERALBADGE + TM23 |
Spill into the next section: the walkthrough offers "fly to Ecruteak and go through the Lake of
Rage" as an alternative to fighting Jasmine now. `MAP_LAKE_OF_RAGE` and the Route 42/43 chain
belong to the next section; nothing in this section's asm depends on them.
---
## 2. Maps
### MAP_GOLDENROD_UNDERGROUND
- Script: `maps/GoldenrodUnderground.asm` (`46:4900 GoldenrodUnderground_MapEvents`, `46:4129 OlderHaircutBrotherScript`)
- Blocks: `maps/GoldenrodUnderground.blk`
- Header: `data/maps/maps.asm:123` -> `map GoldenrodUnderground, TILESET_GATE, DUNGEON, LANDMARK_GOLDENROD_CITY, MUSIC_UNION_CAVE, TRUE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:110` -> `map_const GOLDENROD_UNDERGROUND, 15, 18` (map group 1, the `newgroup OLIVINE` group, map id 45 - it is *not* in the Goldenrod group)
- Attributes: `data/maps/attributes.asm:442` -> `map_attributes GoldenrodUnderground, GOLDENROD_UNDERGROUND, $00` (no connections)
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 2 | `GOLDENROD_UNDERGROUND_SWITCH_ROOM_ENTRANCES` | 7 |
| 2 | 3 | 34 | `GOLDENROD_UNDERGROUND_SWITCH_ROOM_ENTRANCES` | 4 |
| 3 | 18 | 6 | `GOLDENROD_UNDERGROUND` | 4 |
| 4 | 21 | 31 | `GOLDENROD_UNDERGROUND` | 3 |
| 5 | 22 | 31 | `GOLDENROD_UNDERGROUND` | 3 |
| 6 | 22 | 27 | `GOLDENROD_UNDERGROUND_SWITCH_ROOM_ENTRANCES` | 1 |
**Coord events** (`def_coord_events`) - none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 18 | 6 | `BGEVENT_READ` | `BasementDoorScript` |
| 19 | 6 | `BGEVENT_READ` | `GoldenrodUndergroundNoEntrySign` |
| 6 | 13 | `BGEVENT_ITEM` | `GoldenrodUndergroundHiddenParlyzHeal` (`hiddenitem PARLYZ_HEAL, EVENT_GOLDENROD_UNDERGROUND_HIDDEN_PARLYZ_HEAL`) |
| 4 | 18 | `BGEVENT_ITEM` | `GoldenrodUndergroundHiddenSuperPotion` (`hiddenitem SUPER_POTION, EVENT_GOLDENROD_UNDERGROUND_HIDDEN_SUPER_POTION`) |
| 17 | 8 | `BGEVENT_ITEM` | `GoldenrodUndergroundHiddenAntidote` (`hiddenitem ANTIDOTE, EVENT_GOLDENROD_UNDERGROUND_HIDDEN_ANTIDOTE`) |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `GOLDENRODUNDERGROUND_SUPER_NERD1` | `SPRITE_SUPER_NERD` | 5 | 31 | `STANDING_LEFT` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerSupernerdEric` | -1 |
| `GOLDENRODUNDERGROUND_SUPER_NERD2` | `SPRITE_SUPER_NERD` | 6 | 9 | `STANDING_UP` | `OBJECTTYPE_TRAINER`, sight 2 | `TrainerSupernerdTeru` | -1 |
| `GOLDENRODUNDERGROUND_SUPER_NERD3` | `SPRITE_SUPER_NERD` | 3 | 27 | `SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER`, sight 2 | `TrainerPokemaniacIssac` | -1 |
| `GOLDENRODUNDERGROUND_SUPER_NERD4` | `SPRITE_SUPER_NERD` | 2 | 6 | `STANDING_RIGHT` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerPokemaniacDonald` | -1 |
| `GOLDENRODUNDERGROUND_POKE_BALL` | `SPRITE_POKE_BALL` | 7 | 25 | `STILL` | `OBJECTTYPE_ITEMBALL` | `GoldenrodUndergroundCoinCase` (`itemball COIN_CASE`) | `EVENT_GOLDENROD_UNDERGROUND_COIN_CASE` |
| `GOLDENRODUNDERGROUND_GRAMPS` | `SPRITE_GRAMPS` | 7 | 11 | `STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `BargainMerchantScript` | `EVENT_GOLDENROD_UNDERGROUND_GRAMPS` |
| `GOLDENRODUNDERGROUND_OLDER_HAIRCUT_BROTHER` | `SPRITE_SUPER_NERD` | 7 | 14 | `STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `OlderHaircutBrotherScript` | `EVENT_GOLDENROD_UNDERGROUND_OLDER_HAIRCUT_BROTHER` |
| `GOLDENRODUNDERGROUND_YOUNGER_HAIRCUT_BROTHER` | `SPRITE_SUPER_NERD` | 7 | 15 | `STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `YoungerHaircutBrotherScript` | `EVENT_GOLDENROD_UNDERGROUND_YOUNGER_HAIRCUT_BROTHER` |
| `GOLDENRODUNDERGROUND_GRANNY` | `SPRITE_GRANNY` | 7 | 21 | `STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `BitterMerchantScript` | `EVENT_GOLDENROD_UNDERGROUND_GRANNY` |
Object-event visibility convention (verified in `engine/overworld/scripting.asm:879-898`): `appear`
*clears* the flag and `disappear` *sets* it, so **an object is on the map only while its event flag
is clear.**
**Scripts of interest**
- `GoldenrodUndergroundCheckDayOfWeekCallback` (`callback MAPCALLBACK_OBJECTS`): `readvar VAR_WEEKDAY`
and appear/disappear the four merchants. Who is standing there:
- Sunday: younger brother + granny
- Monday: gramps only, and only during `MORN` (`checktime MORN`)
- Tuesday / Thursday: **older brother**
- Wednesday / Friday: younger brother
- Saturday: older brother + granny
The walkthrough's "500 haircut" therefore only exists on **Tuesday, Thursday or Saturday**.
- `OlderHaircutBrotherScript` (`46:4129`): `readvar VAR_WEEKDAY`, bails to
`GoldenrodUndergroundScript_ShopClosed` unless TUE/THU/SAT. Then
`checkflag ENGINE_GOLDENROD_UNDERGROUND_GOT_HAIRCUT` (one cut per day, cleared by the daily
reset), `yesorno`, `checkmoney YOUR_MONEY, 500`, `special OlderHaircutBrother`, then
`takemoney YOUR_MONEY, 500`. `DEF GOLDENRODUNDERGROUND_OLDER_HAIRCUT_PRICE EQU 500` is at the top
of the file. The special returns 0/1 (cancel or Egg) or 2/3/4, which the script mirrors into
`EVENT_TEMPORARY_UNTIL_MAP_RELOAD_1/2/3` just to pick which of three "looks happier" lines to
print.
- `special OlderHaircutBrother` -> `engine/events/haircut.asm:15` -> `HaircutOrGrooming`, weighted
roll over `HappinessData_OlderHaircutBrother` (`data/events/happiness_probabilities.asm:1`):
30% -> `HAPPINESS_OLDERCUT1`, 50%+1 -> `HAPPINESS_OLDERCUT2`, remainder -> `HAPPINESS_OLDERCUT3`.
The actual deltas are `data/events/happiness_changes.asm:13-15`:
`+1/+1/+1`, `+3/+3/+1`, `+5/+5/+2` (columns are happiness <100 / <200 / otherwise).
The younger brother (300, SUN/WED/FRI) rolls 60%+1 / 30% / rest for `+1/+1/+1`, `+3/+3/+1`,
`+10/+10/+4` - **its jackpot row is strictly better than the older brother's**, which the
walkthrough does not mention.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_GOLDENROD_UNDERGROUND_GOT_HAIRCUT` (id 89) | `constants/engine_flags.asm` | checked+set by both `*HaircutBrotherScript` | one haircut per day, either brother |
| `EVENT_GOLDENROD_UNDERGROUND_OLDER_HAIRCUT_BROTHER` (0x754) | `constants/event_flags.asm:1270` | day-of-week callback | clear = older brother is standing at (7, 14) |
| `EVENT_GOLDENROD_UNDERGROUND_YOUNGER_HAIRCUT_BROTHER` (0x755) | `constants/event_flags.asm` | day-of-week callback | clear = younger brother at (7, 15) |
| `EVENT_TEMPORARY_UNTIL_MAP_RELOAD_1/2/3` (0x000-0x002) | `constants/event_flags.asm:5-7` | haircut scripts | scratch, do not persist |
**Items** - nothing the walkthrough takes here; the Coin Case itemball and the three hidden items
belong to earlier sections.
**Trainers** - the four in the table above are optional and already beaten by this point in a
linear run.
**Wild encounters** - none (indoor `DUNGEON`, no entry in `data/wild/johto_grass.asm`).
---
### MAP_OLIVINE_CITY
- Script: `maps/OlivineCity.asm` (`49:4463 OlivineCity_MapEvents`)
- Blocks: `maps/OlivineCity.blk`
- Header: `data/maps/maps.asm:63` -> `map OlivineCity, TILESET_JOHTO, TOWN, LANDMARK_OLIVINE_CITY, MUSIC_VIOLET_CITY, FALSE, PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:52` -> `map_const OLIVINE_CITY, 20, 18` (group 1, id 14)
- Connections: `data/maps/attributes.asm:143-145` -> north `Route39` (`ROUTE_39`, offset 5), west `Route40` (`ROUTE_40`, offset 9). No south/east.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 13 | 21 | `OLIVINE_POKECENTER_1F` | 1 |
| 2 | 10 | 11 | `OLIVINE_GYM` | 1 |
| 3 | 25 | 11 | `OLIVINE_TIMS_HOUSE` | 1 |
| 4 | 0 | 0 | `OLIVINE_HOUSE_BETA` | 1 (marked `; inaccessible` in the asm) |
| 5 | 29 | 11 | `OLIVINE_PUNISHMENT_SPEECH_HOUSE` | 1 |
| 6 | 13 | 15 | `OLIVINE_GOOD_ROD_HOUSE` | 1 |
| 7 | 7 | 21 | `OLIVINE_CAFE` | 1 |
| 8 | 19 | 17 | `OLIVINE_MART` | 2 |
| 9 | 29 | 27 | `OLIVINE_LIGHTHOUSE_1F` | 1 |
| 10 | 19 | 27 | `OLIVINE_PORT_PASSAGE` | 1 |
| 11 | 20 | 27 | `OLIVINE_PORT_PASSAGE` | 2 |
**Coord events** (`def_coord_events`)
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_OLIVINECITY_RIVAL_ENCOUNTER` (= 0) | 13 | 12 | `OlivineCityRivalSceneTop` | rival cutscene, ends with `setscene SCENE_OLIVINECITY_NOOP` |
| `SCENE_OLIVINECITY_RIVAL_ENCOUNTER` (= 0) | 13 | 13 | `OlivineCityRivalSceneBottom` | same, mirrored movement |
Scene ids come from the `scene_script` macro's own `const_def` (`macros/scripts/maps.asm:12-36`):
`SCENE_OLIVINECITY_RIVAL_ENCOUNTER` = 0, `SCENE_OLIVINECITY_NOOP` = 1. By this section the scene is
already 1 (the rival scene fires on the *first* Olivine visit, section 8), so both trip-wires are
inert and a bot may walk x=13 freely.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 17 | 11 | `BGEVENT_READ` | `OlivineCitySign` |
| 20 | 24 | `BGEVENT_READ` | `OlivineCityPortSign` |
| 7 | 11 | `BGEVENT_READ` | `OlivineGymSign` |
| 30 | 28 | `BGEVENT_READ` | `OlivineLighthouseSign` |
| 14 | 21 | `BGEVENT_READ` | `OlivineCityPokecenterSign` (`jumpstd PokecenterSignScript`) |
| 20 | 17 | `BGEVENT_READ` | `OlivineCityMartSign` (`jumpstd MartSignScript`) |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `OLIVINECITY_SAILOR1` | `SPRITE_SAILOR` | 26 | 27 | `WALK_UP_DOWN` (0,1) | `OBJECTTYPE_SCRIPT` | `OlivineCitySailor1Script` | -1 |
| `OLIVINECITY_STANDING_YOUNGSTER` | `SPRITE_YOUNGSTER` | 20 | 13 | `WALK_LEFT_RIGHT` (1,0) | `OBJECTTYPE_SCRIPT` | `OlivineCityStandingYoungsterScript` | -1 |
| `OLIVINECITY_SAILOR2` | `SPRITE_SAILOR` | 17 | 21 | `WANDER` (1,1) | `OBJECTTYPE_SCRIPT` | `OlivineCitySailor2Script` | -1 |
| `OLIVINECITY_OLIVINE_RIVAL` | `SPRITE_OLIVINE_RIVAL` | 10 | 11 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `ObjectEvent` | `EVENT_RIVAL_OLIVINE_CITY` |
Note the rival object sits **on the gym door tile** (10, 11). `InitializeEventsScript`
(`engine/events/std_scripts.asm:520`) sets `EVENT_RIVAL_OLIVINE_CITY` at new game, so he is hidden
except during the cutscene, and the door is walkable.
**Scripts of interest**
- `OlivineCityFlypointCallback` (`callback MAPCALLBACK_NEWMAP`): `setflag ENGINE_FLYPOINT_OLIVINE`
(engine flag id 70). This is what makes "fly straight back to Olivine City" legal; it was already
set in section 8.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_FLYPOINT_OLIVINE` (id 70) | `constants/engine_flags.asm:85` | set by `OlivineCityFlypointCallback` | Olivine selectable on the Fly map |
| `ENGINE_FLYPOINT_GOLDENROD` (id 69) | `constants/engine_flags.asm:84` | set by `maps/GoldenrodCity.asm:24` | Goldenrod selectable on the Fly map |
| `EVENT_RIVAL_OLIVINE_CITY` | `constants/event_flags.asm` | set by `InitializeEventsScript`, `appear`/`disappear` in the scene scripts | clear = rival standing on the gym door |
**Items** - none taken in this section.
**Trainers** - none on the overworld map.
**Wild encounters** - `data/wild/johto_water.asm:239`:
```
def_water_wildmons OLIVINE_CITY
db 6 percent ; encounter rate
db 20, TENTACOOL
db 15, TENTACOOL
db 20, TENTACRUEL
```
No grass table (`data/wild/johto_grass.asm` has no `OLIVINE` entry). Fish group is
`FISHGROUP_SHORE` per the header row.
---
### MAP_OLIVINE_LIGHTHOUSE_1F .. 6F
- Scripts: `maps/OlivineLighthouse1F.asm` ... `maps/OlivineLighthouse6F.asm`
- Blocks: `maps/OlivineLighthouse[1-6]F.blk`
- Header: `data/maps/maps.asm:112-117`. 1F-5F are
`TILESET_LIGHTHOUSE, DUNGEON, LANDMARK_LIGHTHOUSE, MUSIC_LIGHTHOUSE, FALSE, PALETTE_DAY, FISHGROUP_SHORE`;
**6F swaps the music to `MUSIC_VIOLET_CITY`** (line 117), which is a cheap way for a bot to
confirm it reached the top.
- Dimensions: `constants/map_constants.asm:99-104` -> every floor is `10, 9` blocks = 20 x 18 tiles
(group 1, ids 34-39).
- Attributes: `data/maps/attributes.asm:436` (6F) and neighbours - `$00`, no connections.
**Warps** - transcribed verbatim, all six floors.
`OLIVINE_LIGHTHOUSE_1F`:
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 10 | 17 | `OLIVINE_CITY` | 9 |
| 2 | 11 | 17 | `OLIVINE_CITY` | 9 |
| 3 | 3 | 11 | `OLIVINE_LIGHTHOUSE_2F` | 1 |
| 4 | 16 | 13 | `OLIVINE_LIGHTHOUSE_2F` | 3 |
| 5 | 17 | 13 | `OLIVINE_LIGHTHOUSE_2F` | 4 |
`OLIVINE_LIGHTHOUSE_2F`:
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 11 | `OLIVINE_LIGHTHOUSE_1F` | 3 |
| 2 | 5 | 3 | `OLIVINE_LIGHTHOUSE_3F` | 2 |
| 3 | 16 | 13 | `OLIVINE_LIGHTHOUSE_1F` | 4 |
| 4 | 17 | 13 | `OLIVINE_LIGHTHOUSE_1F` | 5 |
| 5 | 16 | 11 | `OLIVINE_LIGHTHOUSE_3F` | 4 |
| 6 | 17 | 11 | `OLIVINE_LIGHTHOUSE_3F` | 5 |
`OLIVINE_LIGHTHOUSE_3F`:
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 13 | 3 | `OLIVINE_LIGHTHOUSE_4F` | 1 |
| 2 | 5 | 3 | `OLIVINE_LIGHTHOUSE_2F` | 2 |
| 3 | 9 | 5 | `OLIVINE_LIGHTHOUSE_4F` | 4 |
| 4 | 16 | 11 | `OLIVINE_LIGHTHOUSE_2F` | 5 |
| 5 | 17 | 11 | `OLIVINE_LIGHTHOUSE_2F` | 6 |
| 6 | 16 | 9 | `OLIVINE_LIGHTHOUSE_4F` | 5 |
| 7 | 17 | 9 | `OLIVINE_LIGHTHOUSE_4F` | 6 |
| 8 | 8 | 3 | `OLIVINE_LIGHTHOUSE_4F` | 7 |
| 9 | 9 | 3 | `OLIVINE_LIGHTHOUSE_4F` | 8 |
`OLIVINE_LIGHTHOUSE_4F`:
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 13 | 3 | `OLIVINE_LIGHTHOUSE_3F` | 1 |
| 2 | 3 | 5 | `OLIVINE_LIGHTHOUSE_5F` | 2 |
| 3 | 9 | 7 | `OLIVINE_LIGHTHOUSE_5F` | 3 |
| 4 | 9 | 5 | `OLIVINE_LIGHTHOUSE_3F` | 3 |
| 5 | 16 | 9 | `OLIVINE_LIGHTHOUSE_3F` | 6 |
| 6 | 17 | 9 | `OLIVINE_LIGHTHOUSE_3F` | 7 |
| 7 | 8 | 3 | `OLIVINE_LIGHTHOUSE_3F` | 8 |
| 8 | 9 | 3 | `OLIVINE_LIGHTHOUSE_3F` | 9 |
| 9 | 16 | 7 | `OLIVINE_LIGHTHOUSE_5F` | 4 |
| 10 | 17 | 7 | `OLIVINE_LIGHTHOUSE_5F` | 5 |
`OLIVINE_LIGHTHOUSE_5F`:
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 15 | `OLIVINE_LIGHTHOUSE_6F` | 1 |
| 2 | 3 | 5 | `OLIVINE_LIGHTHOUSE_4F` | 2 |
| 3 | 9 | 7 | `OLIVINE_LIGHTHOUSE_4F` | 3 |
| 4 | 16 | 7 | `OLIVINE_LIGHTHOUSE_4F` | 9 |
| 5 | 17 | 7 | `OLIVINE_LIGHTHOUSE_4F` | 10 |
| 6 | 16 | 5 | `OLIVINE_LIGHTHOUSE_6F` | 2 |
| 7 | 17 | 5 | `OLIVINE_LIGHTHOUSE_6F` | 3 |
`OLIVINE_LIGHTHOUSE_6F`:
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 15 | `OLIVINE_LIGHTHOUSE_5F` | 1 |
| 2 | 16 | 5 | `OLIVINE_LIGHTHOUSE_5F` | 6 |
| 3 | 17 | 5 | `OLIVINE_LIGHTHOUSE_5F` | 7 |
**Which warps are ladders and which are pits.** Derived by decoding each floor's `.blk` against
`data/tilesets/lighthouse_collision.asm`. Block `$3a` and `$31` carry `LADDER` in their bottom-right
tile; block `$28` is `tilecoll FLOOR, FLOOR, PIT, PIT`, i.e. its two bottom tiles drop you a floor.
| floor | tile | block | kind | goes to |
|---|---|---|---|---|
| 1F | (10, 17), (11, 17) | `$2e` bottom tiles `WARP_CARPET_DOWN` | exit door | `OLIVINE_CITY` |
| 1F | (3, 11) | `$3a` | ladder UP | 2F (3, 11) |
| 2F | (3, 11) | `$31` | ladder DOWN | 1F (3, 11) |
| 2F | (5, 3) | `$3a` | ladder UP | 3F (5, 3) |
| 2F | (16, 13), (17, 13) | `$28` | **PIT** | 1F (16, 13) / (17, 13) |
| 3F | (5, 3) | `$31` | ladder DOWN | 2F (5, 3) |
| 3F | (13, 3) | `$3a` | ladder UP | 4F (13, 3) |
| 3F | (9, 5) | `$3a` | ladder UP | 4F (9, 5) |
| 3F | (16, 11), (17, 11) | `$28` | **PIT** | 2F (16, 11) / (17, 11) |
| 4F | (13, 3) | `$31` | ladder DOWN | 3F (13, 3) |
| 4F | (9, 5) | `$31` | ladder DOWN | 3F (9, 5) |
| 4F | (3, 5) | `$3a` | ladder UP | 5F (3, 5) |
| 4F | (9, 7) | `$3a` | ladder UP | 5F (9, 7) |
| 4F | (8, 3), (9, 3) | `$28` | **PIT** | 3F (8, 3) / (9, 3) |
| 4F | (16, 9), (17, 9) | `$28` | **PIT** | 3F (16, 9) / (17, 9) |
| 5F | (3, 5) | `$31` | ladder DOWN | 4F (3, 5) |
| 5F | (9, 7) | `$31` | ladder DOWN | 4F (9, 7) |
| 5F | (9, 15) | `$3a` | ladder UP | 6F (9, 15) |
| 5F | (16, 7), (17, 7) | `$28` | **PIT** | 4F (16, 7) / (17, 7) |
| 6F | (9, 15) | `$31` | ladder DOWN | 5F (9, 15) |
| 6F | (16, 5), (17, 5) | `$28` | **PIT** | 5F (16, 5) / (17, 5) |
The pit destination coordinates always match the pit's own coordinates on the floor below - the
warp row on the lower floor exists purely as a landing slot and its own destination is never
triggered (the landing tile is plain `FLOOR`).
**Coord events** - none on any floor.
**BG events** - only 5F: `bg_event 3, 13, BGEVENT_ITEM, OlivineLighthouse5FHiddenHyperPotion`.
**Object events** (trainers a bot will be forced into on the climb)
| floor | const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|---|
| 1F | - | `SPRITE_SAILOR` | 8 | 2 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `OlivineLighthouse1FSailorScript` | -1 |
| 1F | - | `SPRITE_POKEFAN_F` | 16 | 9 | `WALK_UP_DOWN` (0,2) | `OBJECTTYPE_SCRIPT` | `OlivineLighthouse1FPokefanFScript` | -1 |
| 2F | - | `SPRITE_SAILOR` | 9 | 3 | `STANDING_RIGHT` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerSailorHuey` | -1 |
| 2F | - | `SPRITE_GENTLEMAN` | 17 | 8 | `STANDING_LEFT` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerGentlemanAlfred` | -1 |
| 3F | - | `SPRITE_SAILOR` | 9 | 2 | `STANDING_DOWN` | `OBJECTTYPE_TRAINER`, sight 1 | `TrainerSailorTerrell` | -1 |
| 3F | - | `SPRITE_GENTLEMAN` | 13 | 5 | `STANDING_RIGHT` | `OBJECTTYPE_TRAINER`, sight 4 | `TrainerGentlemanPreston` | -1 |
| 3F | - | `SPRITE_YOUNGSTER` | 3 | 9 | `STANDING_UP` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerBirdKeeperTheo` | -1 |
| 3F | - | `SPRITE_POKE_BALL` | 8 | 2 | `STILL` | `OBJECTTYPE_ITEMBALL` | `OlivineLighthouse3FEther` | `EVENT_OLIVINE_LIGHTHOUSE_3F_ETHER` |
| 4F | - | `SPRITE_SAILOR` | 7 | 14 | `STANDING_LEFT` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerSailorKent` | -1 |
| 4F | - | `SPRITE_LASS` | 11 | 2 | `STANDING_DOWN` | `OBJECTTYPE_TRAINER`, sight 1 | `TrainerLassConnie` | -1 |
| 5F | - | `SPRITE_SAILOR` | 8 | 11 | `SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerSailorErnest` | -1 |
| 5F | - | `SPRITE_YOUNGSTER` | 8 | 3 | `STANDING_LEFT` | `OBJECTTYPE_TRAINER`, sight 4 | `TrainerBirdKeeperDenis` | -1 |
| 5F | - | `SPRITE_POKE_BALL` | 15 | 12 | `STILL` | `OBJECTTYPE_ITEMBALL` | `OlivineLighthouse5FRareCandy` | `EVENT_OLIVINE_LIGHTHOUSE_5F_RARE_CANDY` |
| 5F | - | `SPRITE_POKE_BALL` | 6 | 15 | `STILL` | `OBJECTTYPE_ITEMBALL` | `OlivineLighthouse5FGreatBall` | `EVENT_OLIVINE_LIGHTHOUSE_5F_GREAT_BALL` |
| 5F | - | `SPRITE_POKE_BALL` | 2 | 13 | `STILL` | `OBJECTTYPE_ITEMBALL` | `OlivineLighthouse5FTMSwagger` | `EVENT_OLIVINE_LIGHTHOUSE_5F_TM_SWAGGER` |
`TrainerLassConnie` at 4F (11, 2) is "that one lass". The pit at 4F (8, 3) / (9, 3) is the tile
pair immediately left-and-below her, which matches the walkthrough's "falling to the left of that
one lass".
**6F object events** (`def_object_events`, `maps/OlivineLighthouse6F.asm:270-273`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `OLIVINELIGHTHOUSE6F_JASMINE` | `SPRITE_JASMINE` | 8 | 8 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `OlivineLighthouseJasmine` | `EVENT_OLIVINE_LIGHTHOUSE_JASMINE` (0x6d2) |
| `OLIVINELIGHTHOUSE6F_MONSTER` | `SPRITE_MONSTER` | 9 | 8 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `OlivineLighthouseAmphy` | -1 |
| `OLIVINELIGHTHOUSE6F_POKE_BALL` | `SPRITE_POKE_BALL` | 3 | 4 | `STILL` | `OBJECTTYPE_ITEMBALL` | `OlivineLighthouse6FSuperPotion` (`itemball SUPER_POTION`) | `EVENT_OLIVINE_LIGHTHOUSE_6F_SUPER_POTION` (0x668) |
**Scripts of interest**
- `OlivineLighthouseJasmine` (`44:6ccd`, `maps/OlivineLighthouse6F.asm:11`):
1. `faceplayer` / `opentext`
2. `checkitem SECRETPOTION` -> `.BroughtSecretpotion` (`44:6ce7`). Without the potion it falls
into the `EVENT_JASMINE_EXPLAINED_AMPHYS_SICKNESS` (0x37) branch and just repeats the request.
3. `.BroughtSecretpotion`: `writetext JasmineCureAmphyText`, `yesorno`. **`iffalse .Refused`** -
answering No leaves everything unset and the gym stays empty, so a bot must answer Yes.
4. `takeitem SECRETPOTION`, a long cutscene (`turnobject`, `playmusic MUSIC_HEAL`,
`special RestartMapMusic`, `cry AMPHAROS`, `special FadeOutToWhite` / `FadeInFromWhite`).
5. **`setevent EVENT_JASMINE_RETURNED_TO_GYM`** (0x20) and
**`clearevent EVENT_OLIVINE_GYM_JASMINE`** (0x6d3) - this pair is the entire gate on the gym.
6. `readvar VAR_FACING` picks one of three exit movements
(`OlivineLighthouseJasmineLeaves{Up,Down,Right}Movement`) then `disappear OLIVINELIGHTHOUSE6F_JASMINE`.
- `OlivineLighthouseAmphy`: cosmetic; branches on `EVENT_JASMINE_RETURNED_TO_GYM` for the healthy
cry.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_JASMINE_RETURNED_TO_GYM` (0x20) | `constants/event_flags.asm:41` | set by `OlivineLighthouseJasmine`; read by `OlivineGymGuideScript` and `maps/EcruteakCity.asm:47` | the section's real progress bit |
| `EVENT_OLIVINE_GYM_JASMINE` (0x6d3) | `constants/event_flags.asm:1141` | set at new game by `InitializeEventsScript` (`engine/events/std_scripts.asm:511`), cleared by `OlivineLighthouseJasmine` | clear = Jasmine is standing in her gym |
| `EVENT_OLIVINE_LIGHTHOUSE_JASMINE` (0x6d2) | `constants/event_flags.asm:1140` | `disappear` at the end of the cure | set = she has left the lighthouse |
| `EVENT_JASMINE_EXPLAINED_AMPHYS_SICKNESS` (0x37) | `constants/event_flags.asm:64` | `OlivineLighthouseJasmine` | first-visit text latch only |
**Items** - `SECRETPOTION` is consumed (`takeitem SECRETPOTION`). Nothing is given.
**Wild encounters** - none on any lighthouse floor (indoor `DUNGEON`, no table).
---
### MAP_OLIVINE_GYM
- Script: `maps/OlivineGym.asm` (`51:410e OlivineGym_MapScripts`, `51:4507 OlivineGym_MapEvents`)
- Blocks: `maps/OlivineGym.blk` (`2b:7c7b OlivineGym_Blocks`)
- Header: `data/maps/maps.asm:51` -> `map OlivineGym, TILESET_CHAMPIONS_ROOM, INDOOR, LANDMARK_OLIVINE_CITY, MUSIC_GYM, TRUE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:40` -> `map_const OLIVINE_GYM, 5, 8` = 10 x 16 tiles (group 1, id 2)
- Attributes: `data/maps/attributes.asm:473` -> `map_attributes OlivineGym, OLIVINE_GYM, $00`, no connections
- `def_scene_scripts` and `def_callbacks` are both **empty** - no scene variable, no map callbacks.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 15 | `OLIVINE_CITY` | 2 |
| 2 | 5 | 15 | `OLIVINE_CITY` | 2 |
**Coord events** (`def_coord_events`) - **none**. There is no trip-wire in this gym; every
interaction is a `faceplayer` talk.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 3 | 13 | `BGEVENT_READ` | `OlivineGymStatue` |
| 6 | 13 | `BGEVENT_READ` | `OlivineGymStatue` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `OLIVINEGYM_JASMINE` | `SPRITE_JASMINE` | 5 | 3 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT`, `PAL_NPC_RED` | `OlivineGymJasmineScript` | `EVENT_OLIVINE_GYM_JASMINE` |
| `OLIVINEGYM_GYM_GUIDE` | `SPRITE_GYM_GUIDE` | 7 | 13 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT`, `PAL_NPC_RED` | `OlivineGymGuideScript` | -1 |
The walkthrough's "there are no apprentices of any kind" is literally true in the asm: the object
table has exactly two rows and neither is `OBJECTTYPE_TRAINER`.
**Walkable shape** (decoded from `maps/OlivineGym.blk` against
`data/tilesets/champions_room_collision.asm`, `.` = FLOOR, `#` = WALL, `W` = `WARP_CARPET_DOWN`,
`U` = `UP_WALL`; x across 0..9, y down 0..15):
```
y0 # # # # # # # # # #
y1 # # # # # # # # # #
y2 # # # U U U U # # #
y3 # # # . . . . # # #
y4 # # # . . . . # # #
y5 # # # # . . # # # #
y6 # # # # . . # # # #
y7 # # # . . . . # # #
y8 # # . . . . . . # #
y9 # # # # . . # # # #
y10 # # # . . . . # # #
y11 # # # . . . . # # #
y12 # # . # . . # . # #
y13 # # . # . . # . # #
y14 # . . . . . . . . #
y15 # . . . W W . . . #
```
Columns x=4 and x=5 are floor from y=14 straight up to y=3, so a bot can enter at (4, 15) or
(5, 15) and hold UP to reach Jasmine at (5, 3) with no turns. The two statues are the wall tiles at
(3, 13) and (6, 13); the gym guide stands at (7, 13), reachable from (7, 14).
**Scripts of interest**
- `OlivineGymJasmineScript` (`51:4110`, `maps/OlivineGym.asm:10`). Opcode-by-opcode:
```
faceplayer / opentext
checkevent EVENT_BEAT_JASMINE -> iftrue .FightDone
writetext Jasmine_SteelTypeIntro / waitbutton / closetext
winlosstext Jasmine_BetterTrainer, 0
loadtrainer JASMINE, JASMINE1
startbattle
reloadmapafterbattle
setevent EVENT_BEAT_JASMINE
opentext / writetext Text_ReceivedMineralBadge
playsound SFX_GET_BADGE / waitsfx
setflag ENGINE_MINERALBADGE
readvar VAR_BADGES
scall OlivineGymActivateRockets
.FightDone:
checkevent EVENT_GOT_TM23_IRON_TAIL -> iftrue .GotIronTail
writetext Jasmine_BadgeSpeech / promptbutton
verbosegiveitem TM_IRON_TAIL
iffalse .NoRoomForIronTail
setevent EVENT_GOT_TM23_IRON_TAIL
writetext Jasmine_IronTailSpeech / waitbutton / closetext / end
```
Two bot-relevant consequences: (a) `winlosstext ..., 0` means there is **no loss-warp script**,
losing just blacks you out normally; (b) if the bag has no room in the TM pocket,
`verbosegiveitem` returns false and `EVENT_GOT_TM23_IRON_TAIL` is **not** set - re-talking to
Jasmine retries the give from `.FightDone`. Keep TM room free.
- `OlivineGymActivateRockets` (`51:4159`): the `scall`ed tail, dispatching on the `readvar
VAR_BADGES` value that was just pushed:
```
ifequal 7, .RadioTowerRockets -> jumpstd RadioTowerRocketsScript
ifequal 6, .GoldenrodRockets -> jumpstd GoldenrodRocketsScript
end
```
`VAR_BADGES` is `CountSetBits` over the two `wBadges` bytes
(`engine/overworld/variables.asm:80-86`), so it counts Johto **and** Kanto badges. Since
`setflag ENGINE_MINERALBADGE` runs first, MINERALBADGE is included in the count. On the
walkthrough's route (Zephyr, Hive, Plain, Fog, Storm, Mineral) the count is **6** and
`GoldenrodRocketsScript` fires: `clearevent EVENT_GOLDENROD_CITY_ROCKET_TAKEOVER`
(`engine/events/std_scripts.asm:251-253`), which makes the two Rocket grunts appear in Goldenrod
City at (28, 20) and (8, 15) (`maps/GoldenrodCity.asm:376-377`). Every gym carries the same
`scall` (`maps/VioletGym.asm:31`, `maps/MahoganyGym.asm:34`, ...), so whichever gym hands you
badge 6 is the one that arms the Rockets - beating Jasmine *before* Chuck simply moves the
trigger to Chuck.
- `OlivineGymGuideScript` (`51:4168`): three-way on `EVENT_BEAT_JASMINE`, then
`EVENT_JASMINE_RETURNED_TO_GYM`. Text only, no flags written.
- `OlivineGymStatue` (`51:418a`): `checkflag ENGINE_MINERALBADGE`, `jumpstd GymStatue1Script`
before, `gettrainername STRING_BUFFER_4, JASMINE, JASMINE1` + `jumpstd GymStatue2Script` after.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_OLIVINE_GYM_JASMINE` (0x6d3) | `constants/event_flags.asm:1141` | set by `InitializeEventsScript`; cleared by `OlivineLighthouseJasmine` | **precondition**: while set, the gym is empty and unbeatable |
| `EVENT_BEAT_JASMINE` (0x4c1) | `constants/event_flags.asm:710` | set by `OlivineGymJasmineScript` | postcondition of the battle |
| `ENGINE_MINERALBADGE` (engine flag id 30) | `constants/engine_flags.asm:42`, `data/events/engine_flags.asm:50` (`engine_flag wJohtoBadges, MINERALBADGE`) | `setflag` here, `checkflag` in `OlivineGymStatue` | the badge bit; `MINERALBADGE` bit index in `wJohtoBadges` is `constants/ram_constants.asm:256` |
| `EVENT_GOT_TM23_IRON_TAIL` (0x0d) | `constants/event_flags.asm:19` | `OlivineGymJasmineScript` | one-time TM guard |
| `EVENT_JASMINE_RETURNED_TO_GYM` (0x20) | `constants/event_flags.asm:41` | read by `OlivineGymGuideScript` | flavour only inside the gym |
| `EVENT_GOLDENROD_CITY_ROCKET_TAKEOVER` (0x6cd) | `constants/event_flags.asm` | cleared by `GoldenrodRocketsScript` | side effect of reaching 6 badges |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `TM_IRON_TAIL` (TM23, item id `$d6`, `constants/item_constants.asm:243`) | `verbosegiveitem` after the battle | `OlivineGymJasmineScript` | `EVENT_GOT_TM23_IRON_TAIL` |
| 3500 prize money | battle reward | see below | - |
Prize money check: `TrainerClassAttributes` for Jasmine (`data/trainers/attributes.asm`, the sixth
gym-leader block) is `db HYPER_POTION, NO_ITEM ; items` and `db 25 ; base reward`.
`ComputeTrainerReward` (`engine/battle/read_trainer_party.asm:300-317`) sets
`wBattleReward = 25 * wCurPartyLevel` = `25 * 35` = **875**, and the payout loop in
`engine/battle/core.asm:2340-2360` adds `wBattleReward` **four times** (`ld c, 4`). 875 x 4 =
**3500**, which matches the walkthrough exactly. (Note Jasmine's AI holds a `HYPER_POTION`.)
Badge effect check: `BadgeStatBoosts` (`engine/battle/core.asm:6533-6588`) - the comment block
states "MineralBadge: Defense", and the code deliberately swaps the PlainBadge and MineralBadge
bits before the every-other-badge walk, boosting the stat at `wBattleMonDefense` by 1/8
(`BoostStat`). Separately `data/types/badge_type_boosts.asm:7` maps MINERALBADGE to `STEEL` for the
type-matching trainer-card/boost table. So the walkthrough's "raises your Pokemon's defense
slightly" is correct.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `JASMINE` / `JASMINE1` | `JASMINE` (trainer class 6, `constants/trainer_constants.asm:42-43`) | 1 | `JasmineGroup` (`0e:5a42`, `data/trainers/parties.asm:53`) | `OlivineGymJasmineScript` | none (gym leaders have no phone row) |
`JasmineGroup`, verbatim (`TRAINERTYPE_MOVES`):
```
db "JASMINE@", TRAINERTYPE_MOVES
db 30, MAGNEMITE, THUNDERBOLT, SUPERSONIC, SONICBOOM, THUNDER_WAVE
db 30, MAGNEMITE, THUNDERBOLT, SUPERSONIC, SONICBOOM, THUNDER_WAVE
db 35, STEELIX, SCREECH, SUNNY_DAY, ROCK_THROW, IRON_TAIL
db -1 ; end
```
EXP check against the walkthrough's numbers: trainer EXP is `base * level / 7 * 3 / 2`.
Magnemite base exp 89 (`data/pokemon/base_stats/magnemite.asm:8`) -> `89*30/7 = 381`, `*3/2 = 571`.
Steelix base exp 196 (`data/pokemon/base_stats/steelix.asm:8`) -> `196*35/7 = 980`, `*3/2 = 1470`.
Both match.
**Wild encounters** - none (`INDOOR`).
---
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Jasmine is not in her gym | `maps/OlivineGym.asm:212` object row, event flag `EVENT_OLIVINE_GYM_JASMINE`; visibility semantics in `engine/overworld/scripting.asm:879-898` | flag must be **clear** | `clearevent EVENT_OLIVINE_GYM_JASMINE` at `maps/OlivineLighthouse6F.asm:69` |
| Jasmine will not leave the lighthouse | `maps/OlivineLighthouse6F.asm:14` `checkitem SECRETPOTION` | `SECRETPOTION` in the bag (from the Cianwood Pharmacy, section 8) | hand it over and answer **Yes** to `JasmineCureAmphyText` (`yesorno` at line 29; `iffalse .Refused` aborts the whole scene) |
| Cannot Fly to Olivine / Goldenrod | `engine/events/overworld.asm:544-547` (`FlyFunction.TryFly` -> `ld de, ENGINE_STORMBADGE` / `call CheckBadge`), mirrored in the port at `src/world/gen2/FieldMoves.lua:107` (`FLY = "STORM"`), plus `ENGINE_FLYPOINT_OLIVINE` (id 70) / `ENGINE_FLYPOINT_GOLDENROD` (id 69) set by each town's `MAPCALLBACK_NEWMAP` (`maps/OlivineCity.asm:22`, `maps/GoldenrodCity.asm:24`) | `ENGINE_STORMBADGE` + having visited the town on foot | both already true entering this section |
| Haircut unavailable | `OlderHaircutBrotherScript` `readvar VAR_WEEKDAY` + `GoldenrodUndergroundCheckDayOfWeekCallback` | in-game weekday must be TUE/THU/SAT for the 500 brother | wait for the day, or use the younger brother SUN/WED/FRI for 300 |
| Second haircut same day | `checkflag ENGINE_GOLDENROD_UNDERGROUND_GOT_HAIRCUT` (engine flag 89) | flag clear | daily reset |
| 4F of the lighthouse is a dead end from the (13, 3) ladder | geometry: 4F pit at (8, 3)/(9, 3), block `$28` in `maps/OlivineLighthouse4F.blk`, warps 7/8 | must step into the pit and re-climb via 3F warp 3 at (9, 5) | no flag, purely navigational |
| No TM pocket room | `verbosegiveitem TM_IRON_TAIL` -> `iffalse .NoRoomForIronTail` (`maps/OlivineGym.asm:35-36`) | free TM slot | drop a TM and re-talk to Jasmine |
Nothing in this section requires `CUT`, `SURF`, `STRENGTH`, `WHIRLPOOL`, `WATERFALL` or `FLASH`.
---
## 4. Bot checklist
Optional Eevee-happiness detour (steps 1-5) can be skipped entirely.
1. **Fly** to `SPAWN_GOLDENROD`. Precondition: `ENGINE_STORMBADGE` set and `ENGINE_FLYPOINT_GOLDENROD` (id 69) set. Postcondition: on `GOLDENROD_CITY`.
2. `GOLDENROD_CITY`: walk to (11, 29), step onto warp 15. Postcondition: on `GOLDENROD_UNDERGROUND_SWITCH_ROOM_ENTRANCES` at warp 5 (4, 29).
3. `GOLDENROD_UNDERGROUND_SWITCH_ROOM_ENTRANCES`: walk to (5, 25), step onto warp 4. Postcondition: on `GOLDENROD_UNDERGROUND` at warp 2 (3, 34).
4. `GOLDENROD_UNDERGROUND`: precondition `VAR_WEEKDAY in {TUESDAY, THURSDAY, SATURDAY}` and `ENGINE_GOLDENROD_UNDERGROUND_GOT_HAIRCUT` clear and money >= 500. Walk to (6, 14) and face RIGHT (the brother stands at (7, 14) facing left); talk. Answer **Yes**, pick the Eevee. Postcondition: `ENGINE_GOLDENROD_UNDERGROUND_GOT_HAIRCUT` set, money -500, Eevee happiness +1/+3/+5.
5. Retrace warps out (warp 2 at (3, 34) -> switch-room warp 5/6 at (4, 29)/(5, 29) -> `GOLDENROD_CITY`).
6. **Fly** to `SPAWN_OLIVINE`. Precondition: `ENGINE_FLYPOINT_OLIVINE` (id 70). Postcondition: on `OLIVINE_CITY`.
7. `OLIVINE_CITY`: walk to (29, 27), step onto warp 9. Postcondition: on `OLIVINE_LIGHTHOUSE_1F` at warp 1 (10, 17).
8. `1F`: walk to (3, 11), step on ladder-up. -> `2F` (3, 11).
9. `2F`: walk to (5, 3), ladder-up. -> `3F` (5, 3). (Sailor Huey (9, 3) and Gentleman Alfred (17, 8) are already beaten; their `EVENT_BEAT_*` flags guard re-battle.)
10. `3F`: walk to (13, 3), ladder-up. -> `4F` (13, 3).
11. `4F`: walk to the pit at (8, 3) or (9, 3) - "left of Lass Connie at (11, 2)". Step in. -> `3F` (8, 3)/(9, 3).
12. `3F`: walk to (9, 5), ladder-up. -> `4F` (9, 5).
13. `4F`: walk to (9, 7), ladder-up. -> `5F` (9, 7). (Alternative: 4F (3, 5) -> 5F (3, 5).)
14. `5F`: walk to (9, 15), ladder-up. -> `6F` (9, 15). Confirm arrival by `MUSIC_VIOLET_CITY` playing (6F is the only lighthouse floor with it).
15. `6F`: precondition `SECRETPOTION` in bag and `EVENT_OLIVINE_LIGHTHOUSE_JASMINE` clear. Walk to (8, 9) and face UP (Jasmine at (8, 8) faces down) and talk. Answer **Yes** to `JasmineCureAmphyText`. Sit through ~120 frames of cutscene (`pause 60`, `pause 15`, several `pause 10`). Postcondition: `SECRETPOTION` removed, `EVENT_JASMINE_RETURNED_TO_GYM` set, `EVENT_OLIVINE_GYM_JASMINE` **cleared**, `EVENT_OLIVINE_LIGHTHOUSE_JASMINE` set.
16. Descend: `6F` pit at (16, 5)/(17, 5) -> `5F`; `5F` pit at (16, 7)/(17, 7) -> `4F`; `4F` pit at (16, 9)/(17, 9) -> `3F`; `3F` pit at (16, 11)/(17, 11) -> `2F`; `2F` pit at (16, 13)/(17, 13) -> `1F`. Then `1F` (10, 17)/(11, 17) facing DOWN -> `OLIVINE_CITY` warp 9 at (29, 27).
17. `OLIVINE_CITY`: (optional) heal at the Pokecenter, warp 1 at (13, 21). Save before the gym.
18. `OLIVINE_CITY`: walk to (10, 11), step onto warp 2. Postcondition: on `OLIVINE_GYM` at warp 1 (4, 15).
19. `OLIVINE_GYM`: (optional) talk to the guide by walking to (7, 14) and facing UP.
20. `OLIVINE_GYM`: hold UP along x=5 from (5, 14) to (5, 4), face UP, talk to Jasmine at (5, 3). Precondition: `EVENT_OLIVINE_GYM_JASMINE` clear, `EVENT_BEAT_JASMINE` clear, a free TM slot. Battle `loadtrainer JASMINE, JASMINE1`.
21. Win. Postconditions in order: `EVENT_BEAT_JASMINE` set -> `ENGINE_MINERALBADGE` set -> money +3500 -> `VAR_BADGES` re-read -> if it equals 6, `EVENT_GOLDENROD_CITY_ROCKET_TAKEOVER` cleared (Rockets appear in Goldenrod) -> `verbosegiveitem TM_IRON_TAIL` -> `EVENT_GOT_TM23_IRON_TAIL` set.
22. Leave via (4, 15)/(5, 15) facing DOWN -> `OLIVINE_CITY` warp 2 (10, 11).
Battle notes for the driver: lead a Fire or Fighting attacker; both Magnemite carry
`THUNDER_WAVE` (paralysis risk) and `SONICBOOM` (fixed 20 damage, ignores type - it will connect on
a Ground type); Steelix has `IRON_TAIL` and `SUNNY_DAY` (which boosts your own Fire moves too).
---
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Map headers, blocks, warps, coord/bg/object events for all of these maps | `src/import/RomExtractorGen2.lua` (`readItemBall` ~line 2875, object/trainer struct decode ~line 2961) | implemented (data-driven from ROM, no hand-ported Olivine map) |
| Script opcodes used by `OlivineGymJasmineScript` (`faceplayer`, `checkevent`, `winlosstext`, `loadtrainer`, `startbattle`, `reloadmapafterbattle`, `setflag`, `readvar`, `scall`, `verbosegiveitem`, `jumpstd`, `gettrainername`) | `src/script/gen2/Vm.lua` (`faceplayer` L164, `gettrainername` L407, `verbosegiveitem` L490, `jumpstd` L742, `loadtrainer` L806, `winlosstext` L918); opcode table `src/script/gen2/Opcodes.lua` | implemented |
| `VAR_BADGES` (`CountSetBits` over Johto+Kanto) | `src/world/gen2/World.lua:1240-1245` | implemented |
| Fly gate + flypoint engine flags | `src/world/gen2/FieldMoves.lua:103-130`, `FLYPOINTS` L340-368 (`OLIVINE` flag 70, `GOLDENROD` flag 69) | implemented |
| Pit / warp-carpet collision (the lighthouse climb) | `src/world/gen2/Permissions.lua:162-180` (`COLL_PIT` 0x60 / 0x68 and the `$7x` warp nybble; carpet direction table) | implemented |
| Haircut brothers (weighted roll + happiness deltas) | `src/script/gen2/Specials.lua:1577-1615` (`HAIRCUT_TABLES`), `src/core/gen2/Happiness.lua:79-84` | implemented |
| Trainer prize money incl. the x4 payout loop | `src/battle/gen2/Prize.lua` (header comment cites `ld c, 4`) | implemented |
| Gym-leader battle music for `JASMINE` | `src/battle/gen2/BattleMusic.lua:25`, `src/battle/gen2/Battle.lua:86` | implemented |
| Eevee -> Espeon/Umbreon (`EVOLVE_HAPPINESS`, `HAPPINESS_TO_EVOLVE` 220, `TR_MORNDAY`/`TR_NITE`) | `src/core/gen2/Evolution.lua:40-52,143` | implemented |
| Trainer card badge display | `src/ui/gen2/TrainerCard.lua`; leader palette list at `src/import/RomExtractorGen2.lua:4982` | implemented |
| **`BadgeStatBoosts`** - the MINERALBADGE +1/8 Defense (and the PlainBadge/MineralBadge bit swap) | nothing found. `grep -i badge src/battle/gen2/` returns no hits; the only badge-boost code in the repo is Gen 1's (`src/battle/Damage.lua:24`, `src/battle/BattleState.lua:439`) | **missing** |
| **`BadgeTypeBoosts`** (`data/types/badge_type_boosts.asm`, MINERALBADGE -> STEEL) | no hits for `BadgeTypeBoosts` anywhere in `src/` | **missing** |
| A driver that walks the lighthouse or fights a gym leader | `tests/drivers/gold_*.lua` - `gold_trainer_smoke.lua` and `gold_battle_smoke.lua` exist but neither names Olivine or Jasmine | **missing** (no section-9 driver) |
---
## 6. Unresolved / verify by hand
- **"Climb up the Olivine Lighthouse in the EXACT same way you came up."** The ladder/pit graph in
section 2 is fully decoded from the `.blk` files and `data/tilesets/lighthouse_collision.asm`, but
I did **not** flood-fill each floor to prove the specific walking path between a ladder and the
next pit. Steps 8-14 of the checklist are the geometrically obvious route (and the pit at 4F
(8, 3)/(9, 3) really is left of Lass Connie at (11, 2)), but the intermediate tile-by-tile route
on 3F, 4F and 5F should be confirmed in-game before a driver hard-codes it.
- **"Talk to the man who is running the second/middle shop."** The asm has four merchant objects in
one column at x=7: gramps y=11, older brother y=14, younger brother y=15, granny y=21, and
`GoldenrodUndergroundCheckDayOfWeekCallback` hides most of them on any given day. "Middle" is
therefore day-dependent; the unambiguous identifier is the **500 price**
(`GOLDENRODUNDERGROUND_OLDER_HAIRCUT_PRICE`), i.e. the older brother at (7, 14), available
TUE/THU/SAT only. The walkthrough does not mention the weekday restriction at all.
- **"Eevee's happiness meter should get a nice boost."** The older brother's roll is +1 (30%),
+3 (50%) or +5 (20%) at happiness < 200. The younger brother's top roll is +10. The walkthrough's
implied "500 is the better option" is not supported by
`data/events/happiness_probabilities.asm` / `data/events/happiness_changes.asm`.
- **"Say Yes to the fact that Ampharos will be cured by the medicine."** Confirmed as a hard
requirement (`yesorno` / `iffalse .Refused` at `maps/OlivineLighthouse6F.asm:29-30`), but note the
refusal path is *recoverable*: it sets nothing, so the player can simply talk again.
- **EXP values (571 / 1470)** are reproduced by the standard trainer formula from the base-exp bytes
I read, but I did not open the EXP-award routine itself to confirm the `*3/2` trainer multiplier
in this disassembly; treat those two numbers as arithmetic agreement rather than a code citation.
- **`data/trainers/attributes.asm` Jasmine row**: the file has no per-class labels, only ordered
comment headers. I identified Jasmine's row by the `; Jasmine` comment (sixth block, `db 25 ;
base reward`, `db HYPER_POTION, NO_ITEM`). If that comment ever drifts from the class order in
`constants/trainer_constants.asm`, the 3500 derivation drifts with it.
- The walkthrough's "you can go to the Lake of Rage first" branch is out of scope here; nothing in
the Olivine Gym asm checks for it.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,851 @@
# Section 11 - Team Rocket Hideout and Mahogany Town Gym
Source: `../section-11-team-rocket-hideout-and-mahogany-town-gym.txt`
(the FAQ's own heading is "17 > Team Rocket Hideout and Mahogany Town Gym")
Maps covered: `MAP_MAHOGANY_TOWN`, `MAP_MAHOGANY_POKECENTER_1F`, `MAP_MAHOGANY_MART_1F`,
`MAP_TEAM_ROCKET_BASE_B1F`, `MAP_TEAM_ROCKET_BASE_B2F`, `MAP_TEAM_ROCKET_BASE_B3F`,
`MAP_MAHOGANY_GYM`
Badges / key milestones in this section:
- `EVENT_UNCOVERED_STAIRCASE_IN_MAHOGANY_MART` - the hideout entrance exists
- `EVENT_LEARNED_SLOWPOKETAIL`, `EVENT_LEARNED_RATICATE_TAIL` - Giovanni's-office door
- `EVENT_LEARNED_HAIL_GIOVANNI` - transmitter-room door
- `EVENT_GOT_HM06_WHIRLPOOL` (HM06 = `HM_WHIRLPOOL`)
- `EVENT_CLEARED_ROCKET_HIDEOUT` + `clearflag ENGINE_ROCKET_SIGNAL_ON_CH20`
- `ENGINE_GLACIERBADGE` (GLACIERBADGE, badge 7) + `EVENT_GOT_TM16_ICY_WIND` (TM16 = `TM_ICY_WIND`)
Entry precondition, set in the previous section: `maps/LakeOfRage.asm`
`LakeOfRageLanceScript.AgreedToHelp` runs
`clearevent EVENT_MAHOGANY_MART_LANCE_AND_DRAGONITE`,
`setevent EVENT_DECIDED_TO_HELP_LANCE`,
`setmapscene MAHOGANY_MART_1F, SCENE_MAHOGANYMART1F_LANCE_UNCOVERS_STAIRS`.
Without that scene set, the mart is an ordinary shop and there is no staircase.
---
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 1 | `MAP_MAHOGANY_TOWN` | `maps/MahoganyTown.asm` | from Route 43 gate (warp 5, `9,1`) or fly point `ENGINE_FLYPOINT_MAHOGANY` | warp 4 `15,13` -> Pokecenter, warp 1 `11,7` -> Mart | "Heal at the Pokemon Center and withdraw your Gyarados" |
| 2 | `MAP_MAHOGANY_POKECENTER_1F` | `maps/MahoganyPokecenter1F.asm` | warp 1/2 `3,7` / `4,7` | same | heal / PC withdraw |
| 3 | `MAP_MAHOGANY_MART_1F` | `maps/MahoganyMart1F.asm` | warp 1/2 `3,7` / `4,7` | warp 3 `7,3` | Lance's Dragonite Hyper Beams the shop open, uncovers the staircase |
| 4 | `MAP_TEAM_ROCKET_BASE_B1F` | `maps/TeamRocketBaseB1F.asm` | warp 1 `27,2` | warp 2 `3,14` | security cameras, GruntM16, Scientist Jed, secret switch, 3 item balls |
| 5 | `MAP_TEAM_ROCKET_BASE_B2F` (bottom region) | `maps/TeamRocketBaseB2F.asm` | warp 1 `3,14` | warp 5 `27,14` | Lance heals you; GruntM19 then GruntM17 |
| 6 | `MAP_TEAM_ROCKET_BASE_B3F` (right region) | `maps/TeamRocketBaseB3F.asm` | warp 4 `27,14` | warp 2 `27,2` | Lance's two-password speech, 4 item balls, Ross / GruntF5 / Mitch / GruntM28, both passwords |
| 7 | `MAP_TEAM_ROCKET_BASE_B2F` (top region) | `maps/TeamRocketBaseB2F.asm` | warp 3 `27,2` | warp 2 `3,2` | walk left past GruntM18 |
| 8 | `MAP_TEAM_ROCKET_BASE_B3F` (left region) | `maps/TeamRocketBaseB3F.asm` | warp 1 `3,2` | warp 3 `3,6` | rival cutscene, password door, ExecutiveM 4, Murkrow gives HAIL GIOVANNI |
| 9 | `MAP_TEAM_ROCKET_BASE_B2F` (TM room) | `maps/TeamRocketBaseB2F.asm` | warp 4 `3,6` | warp 4 `3,6` | "the second stairs that you did NOT come out of" - TM46 Thief at `3,10` |
| 10 | back through 8 -> 7 -> 6 -> 5 | | | | "Head back the way you came, past the spinning Team Rocket member" |
| 11 | `MAP_TEAM_ROCKET_BASE_B2F` (transmitter room) | `maps/TeamRocketBaseB2F.asm` | password door at `14,12` / `15,12` | warp 1 `3,14` (then B1F warp panel `5,15`) | ExecutiveF 2 ambush, 3 Electrode, HM06 Whirlpool from Lance |
| 12 | `MAP_MAHOGANY_TOWN` | `maps/MahoganyTown.asm` | Mart warps 1/2 | warp 3 `6,13` | heal, then the gym |
| 13 | `MAP_MAHOGANY_GYM` | `maps/MahoganyGym.asm` | warp 1/2 `4,17` / `5,17` | same | five ice-puzzle trainers, then Pryce -> GLACIERBADGE + TM16 |
Spill into the next section: the last paragraph ("You can now use Whirlpool, once you
find that in a dark cavern") points at `MAP_ICE_PATH_1F` / `MAP_ICE_PATH_B2F_MAHOGANY_SIDE`.
Not covered here.
---
## 2. Maps
### MAP_MAHOGANY_TOWN
- Script: `maps/MahoganyTown.asm`
- Blocks: `maps/MahoganyTown.blk`
- Header: `data/maps/maps.asm:74` -> `map MahoganyTown, TILESET_JOHTO, TOWN, LANDMARK_MAHOGANY_TOWN, MUSIC_CHERRYGROVE_CITY, FALSE, PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm` -> `map_const MAHOGANY_TOWN, 10, 9` (group `MAHOGANY` = 2, map id 7); 10x9 blocks = 20x18 map cells
- Connections: `data/maps/attributes.asm:152-155` -> north `Route43`, west `Route42`, east `Route44`
- Scene ids (declared inline by `scene_script`): `SCENE_MAHOGANYTOWN_TRY_RAGECANDYBAR` = 0, `SCENE_MAHOGANYTOWN_NOOP` = 1
- Callback: `MAPCALLBACK_NEWMAP, MahoganyTownFlypointCallback` -> `setflag ENGINE_FLYPOINT_MAHOGANY`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 11 | 7 | `MAHOGANY_MART_1F` | 1 |
| 2 | 17 | 7 | `MAHOGANY_RED_GYARADOS_SPEECH_HOUSE` | 1 |
| 3 | 6 | 13 | `MAHOGANY_GYM` | 1 |
| 4 | 15 | 13 | `MAHOGANY_POKECENTER_1F` | 1 |
| 5 | 9 | 1 | `ROUTE_43_MAHOGANY_GATE` | 3 |
**Coord events** (`def_coord_events`)
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_MAHOGANYTOWN_TRY_RAGECANDYBAR` (0) | 19 | 8 | `MahoganyTownTryARageCandyBarScript` | merchant steps in front of you and pitches a RAGECANDYBAR |
| `SCENE_MAHOGANYTOWN_TRY_RAGECANDYBAR` (0) | 19 | 9 | `MahoganyTownTryARageCandyBarScript` | same |
The script never calls `setscene`, so the pitch re-fires every time you cross x=19
until `RadioTowerRocketsScript` (`engine/events/std_scripts.asm:263`) runs
`setmapscene MAHOGANY_TOWN, SCENE_MAHOGANYTOWN_NOOP`.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 1 | 5 | `BGEVENT_READ` | `MahoganyTownSign` |
| 9 | 7 | `BGEVENT_READ` | `MahoganyTownRagecandybarSign` |
| 3 | 13 | `BGEVENT_READ` | `MahoganyGymSign` |
| 16 | 13 | `BGEVENT_READ` | `MahoganyTownPokecenterSign` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `MAHOGANYTOWN_POKEFAN_M` | `SPRITE_POKEFAN_M` | 19 | 8 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `MahoganyTownPokefanMScript` | `EVENT_MAHOGANY_TOWN_POKEFAN_M_BLOCKS_EAST` |
| `MAHOGANYTOWN_GRAMPS` | `SPRITE_GRAMPS` | 6 | 9 | `WALK_LEFT_RIGHT` (radius 1,0) | `OBJECTTYPE_SCRIPT` | `MahoganyTownGrampsScript` | -1 |
| `MAHOGANYTOWN_FISHER` | `SPRITE_FISHER` | 6 | 14 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `MahoganyTownFisherScript` | `EVENT_MAHOGANY_TOWN_POKEFAN_M_BLOCKS_GYM` |
| `MAHOGANYTOWN_LASS` | `SPRITE_LASS` | 12 | 8 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `MahoganyTownLassScript` | `EVENT_MAHOGANY_MART_OWNERS` |
Object-event flag polarity (verified in `engine/overworld/map_objects_2.asm`
`CheckObjectFlag`): the flag being **set** masks (hides) the object; `-1` means
always visible. So `EVENT_MAHOGANY_TOWN_POKEFAN_M_BLOCKS_GYM` clear = the fisher
stands on `6,14`, one cell south of the gym warp at `6,13`, blocking it. That flag
is set by `RocketBaseElectrodeScript` at the end of the hideout, which is the real
gate on the Mahogany gym (see section 3).
### MAP_MAHOGANY_POKECENTER_1F
- Script: `maps/MahoganyPokecenter1F.asm`
- Header: `data/maps/maps.asm:70` -> `TILESET_POKECENTER, INDOOR, LANDMARK_MAHOGANY_TOWN, MUSIC_POKEMON_CENTER, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `map_const MAHOGANY_POKECENTER_1F, 5, 4` (group 2, map id 3)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `MAHOGANY_TOWN` | 4 |
| 2 | 4 | 7 | `MAHOGANY_TOWN` | 4 |
| 3 | 0 | 7 | `POKECENTER_2F` | 1 |
No coord events, no bg events. Nurse is `MAHOGANYPOKECENTER1F_NURSE`
`SPRITE_NURSE` at `3,1`, script `MahoganyPokecenter1FNurseScript` ->
`jumpstd PokecenterNurseScript`.
### MAP_MAHOGANY_MART_1F
- Script: `maps/MahoganyMart1F.asm` (`45:4000 MahoganyMart1F_MapScripts`)
- Blocks: shares `maps/GiftShop.blk` (`data/maps/blocks.asm:700` -> `MahoganyMart1F_Blocks: MountMoonGiftShop_Blocks: INCBIN "maps/GiftShop.blk"`)
- Header: `data/maps/maps.asm:118` -> `TILESET_TRADITIONAL_HOUSE, INDOOR, LANDMARK_MAHOGANY_TOWN, MUSIC_MAHOGANY_MART, TRUE (no phone), PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `map_const MAHOGANY_MART_1F, 4, 4` (group `DUNGEONS` = 3, map id 40)
- Scene ids: `SCENE_MAHOGANYMART1F_NOOP` = 0, `SCENE_MAHOGANYMART1F_LANCE_UNCOVERS_STAIRS` = 1
- Callback: `MAPCALLBACK_TILES, MahoganyMart1FStaircaseCallback` - if `EVENT_UNCOVERED_STAIRCASE_IN_MAHOGANY_MART` is set, `changeblock 6, 2, $1e ; stairs`
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `MAHOGANY_TOWN` | 1 |
| 2 | 4 | 7 | `MAHOGANY_TOWN` | 1 |
| 3 | 7 | 3 | `TEAM_ROCKET_BASE_B1F` | 1 |
`def_coord_events` and `def_bg_events` are both empty.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `MAHOGANYMART1F_PHARMACIST` | `SPRITE_PHARMACIST` | 4 | 3 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `MahoganyMart1FPharmacistScript` | `EVENT_TEAM_ROCKET_BASE_POPULATION` |
| `MAHOGANYMART1F_BLACK_BELT` | `SPRITE_BLACK_BELT` | 1 | 6 | `SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT` | `MahoganyMart1FBlackBeltScript` | `EVENT_TEAM_ROCKET_BASE_POPULATION` |
| `MAHOGANYMART1F_LANCE` | `SPRITE_LANCE` | 4 | 6 | `STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `ObjectEvent` | `EVENT_MAHOGANY_MART_LANCE_AND_DRAGONITE` |
| `MAHOGANYMART1F_DRAGONITE` | `SPRITE_DRAGON` | 3 | 6 | `STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `ObjectEvent` | `EVENT_MAHOGANY_MART_LANCE_AND_DRAGONITE` |
| `MAHOGANYMART1F_GRANNY` | `SPRITE_GRANNY` | 1 | 3 | `STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `MahoganyMart1FGrannyScript` | `EVENT_MAHOGANY_MART_OWNERS` |
**Scripts of interest**
`MahoganyMart1FLanceUncoversStaircaseScript` (`45:4046`), reached as
`scene_script ... SCENE_MAHOGANYMART1F_LANCE_UNCOVERS_STAIRS` via `sdefer`, so it
fires on map entry without any player input:
1. Dragonite `big_step LEFT / big_step RIGHT` into the Black Belt, `disappear MAHOGANYMART1F_DRAGONITE`
2. Lance walks over (`MahoganyMart1FLanceApproachPlayerMovement`), `follow MAHOGANYMART1F_LANCE, PLAYER`, shoves the pharmacist aside
3. `changeblock 6, 2, $1e` + `refreshmap` + `setevent EVENT_UNCOVERED_STAIRCASE_IN_MAHOGANY_MART`
4. `disappear MAHOGANYMART1F_LANCE`, `setscene SCENE_MAHOGANYMART1F_NOOP`
`changeblock`'s operands are **map cell coordinates**, not block coordinates
(`Script_changeblock` at `engine/overworld/scripting.asm:2031` adds 4 to each and
calls `GetBlockLocation`, which halves them - `home/map.asm:2099`). `6,2` is
therefore block (3,1), covering cells (6..7, 2..3); block `$1e` in
`data/tilesets/traditional_house_collision.asm` is `FLOOR, FLOOR, FLOOR, LADDER`,
putting the LADDER on cell `7,3` - exactly the warp row above.
Marts: `MahoganyMart1FPharmacistScript` -> `pokemart MARTTYPE_STANDARD, MART_MAHOGANY_1`
(closed once `EVENT_DECIDED_TO_HELP_LANCE` is set),
`MahoganyMart1FGrannyScript` -> `pokemart MARTTYPE_STANDARD, MART_MAHOGANY_2`.
### MAP_TEAM_ROCKET_BASE_B1F
- Script: `maps/TeamRocketBaseB1F.asm` (`45:430c TeamRocketBaseB1F_MapScripts`)
- Blocks: `maps/TeamRocketBaseB1F.blk`
- Header: `data/maps/maps.asm:119` -> `TILESET_UNDERGROUND, DUNGEON, LANDMARK_MAHOGANY_TOWN, MUSIC_ROCKET_HIDEOUT, TRUE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `map_const TEAM_ROCKET_BASE_B1F, 15, 9` (group 3, map id 41) = 30x18 cells
- Scene ids: `SCENE_TEAMROCKETBASEB1F_TRAPS` = 0 (the only one)
- Callback: `MAPCALLBACK_OBJECTS, TeamRocketBaseB1FHideSecurityGruntCallback` -> `disappear TEAMROCKETBASEB1F_ROCKET1` (the camera grunt is parked off-screen at `0,0` and teleported in by `moveobject`)
The whole floor is one connected walkable region (flood-filled from
`maps/TeamRocketBaseB1F.blk` against `data/tilesets/underground_collision.asm`).
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 27 | 2 | `MAHOGANY_MART_1F` | 3 |
| 2 | 3 | 14 | `TEAM_ROCKET_BASE_B2F` | 1 |
| 3 | 5 | 15 | `TEAM_ROCKET_BASE_B1F` | 4 |
| 4 | 25 | 2 | `TEAM_ROCKET_BASE_B1F` | 3 |
Warps 3 and 4 are the warp panel pair Scientist Jed describes: cell `5,15` has
collision `WARP_PANEL` and dumps you at `25,2`, two cells from the exit ladder.
**Coord events** (`def_coord_events`) - all on scene `SCENE_TEAMROCKETBASEB1F_TRAPS` (0)
Security cameras (each triggers `GRUNTM_20` then `GRUNTM_21`):
| x | y | script label | guarding statue (bg_event) |
|---|---|---|---|
| 24 | 2 | `SecurityCamera1a` | `24,1` |
| 24 | 3 | `SecurityCamera1b` | `24,1` |
| 6 | 2 | `SecurityCamera2a` | `6,1` |
| 6 | 3 | `SecurityCamera2b` | `6,1` |
| 24 | 6 | `SecurityCamera3a` | `24,5` |
| 24 | 7 | `SecurityCamera3b` | `24,5` |
| 22 | 16 | `SecurityCamera4` | `22,15` |
| 8 | 16 | `SecurityCamera5` | `8,15` |
Exploding-trap tiles (each is a one-shot wild battle with
`loadvar VAR_BATTLETYPE, BATTLETYPE_TRAP`):
| x | y | script | mon |
|---|---|---|---|
| 2 | 7 | `ExplodingTrap1` | `KoffingExplodingTrap` - KOFFING L21 |
| 3 | 7 | `ExplodingTrap2` | `VoltorbExplodingTrap` - VOLTORB L23 |
| 4 | 7 | `ExplodingTrap3` | `GeodudeExplodingTrap` - GEODUDE L21 |
| 1 | 8 | `ExplodingTrap4` | VOLTORB L23 |
| 3 | 8 | `ExplodingTrap5` | GEODUDE L21 |
| 5 | 8 | `ExplodingTrap6` | KOFFING L21 |
| 3 | 9 | `ExplodingTrap7` | VOLTORB L23 |
| 4 | 9 | `ExplodingTrap8` | KOFFING L21 |
| 1 | 10 | `ExplodingTrap9` | KOFFING L21 |
| 2 | 10 | `ExplodingTrap10` | VOLTORB L23 |
| 3 | 10 | `ExplodingTrap11` | GEODUDE L21 |
| 5 | 10 | `ExplodingTrap12` | GEODUDE L21 |
| 2 | 11 | `ExplodingTrap13` | GEODUDE L21 |
| 4 | 11 | `ExplodingTrap14` | KOFFING L21 |
| 1 | 12 | `ExplodingTrap15` | VOLTORB L23 |
| 2 | 12 | `ExplodingTrap16` | KOFFING L21 |
| 4 | 12 | `ExplodingTrap17` | VOLTORB L23 |
| 5 | 12 | `ExplodingTrap18` | GEODUDE L21 |
| 1 | 13 | `ExplodingTrap19` | GEODUDE L21 |
| 3 | 13 | `ExplodingTrap20` | VOLTORB L23 |
| 4 | 13 | `ExplodingTrap21` | KOFFING L21 |
| 5 | 13 | `ExplodingTrap22` | VOLTORB L23 |
Safe cells inside the 5x7 trap field (x 1..5, y 7..13) are exactly those with no
row above: `(1,7)`, `(5,7)`, `(2,8)`, `(4,8)`, `(1,9)`, `(2,9)`, `(5,9)`, `(4,10)`,
`(1,11)`, `(3,11)`, `(5,11)`, `(3,12)`, `(2,13)`. `(3,11)` is also the hidden REVIVE.
That is the "zig-zag" the walkthrough describes.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 19 | 11 | `BGEVENT_READ` | `TeamRocketBaseB1FSecretSwitch` |
| 24 | 1 | `BGEVENT_UP` | `TeamRocketBaseB1FSecurityCamera` |
| 6 | 1 | `BGEVENT_UP` | `TeamRocketBaseB1FSecurityCamera` |
| 8 | 15 | `BGEVENT_UP` | `TeamRocketBaseB1FSecurityCamera` |
| 22 | 15 | `BGEVENT_UP` | `TeamRocketBaseB1FSecurityCamera` |
| 24 | 5 | `BGEVENT_UP` | `TeamRocketBaseB1FSecurityCamera` |
| 20 | 11 | `BGEVENT_READ` | `TeamRocketBaseB1FBookshelf` (`jumpstd TeamRocketOathScript`) |
| 21 | 11 | `BGEVENT_READ` | `TeamRocketBaseB1FBookshelf` |
| 3 | 11 | `BGEVENT_ITEM` | `TeamRocketBaseB1FHiddenRevive` -> `hiddenitem REVIVE, EVENT_TEAM_ROCKET_BASE_B1F_HIDDEN_REVIVE` |
**Object events**
| const | sprite | x | y | movement | type | sight | script label | event flag |
|---|---|---|---|---|---|---|---|---|
| `TEAMROCKETBASEB1F_ROCKET1` | `SPRITE_ROCKET` | 0 | 0 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | 0 | `ObjectEvent` | `EVENT_TEAM_ROCKET_BASE_SECURITY_GRUNTS` |
| - | `SPRITE_ROCKET` | 2 | 4 | `STANDING_RIGHT` | `OBJECTTYPE_TRAINER` | 3 | `TrainerGruntM16` | `EVENT_TEAM_ROCKET_BASE_POPULATION` |
| - | `SPRITE_SCIENTIST` | 18 | 12 | `STANDING_LEFT` | `OBJECTTYPE_TRAINER` | 3 | `TrainerScientistJed` | `EVENT_TEAM_ROCKET_BASE_POPULATION` |
| `TEAMROCKETBASEB1F_POKE_BALL1` | `SPRITE_POKE_BALL` | 27 | 6 | `STILL` | `OBJECTTYPE_ITEMBALL` | 0 | `TeamRocketBaseB1FHyperPotion` | `EVENT_TEAM_ROCKET_BASE_B1F_HYPER_POTION` |
| `TEAMROCKETBASEB1F_POKE_BALL2` | `SPRITE_POKE_BALL` | 14 | 15 | `STILL` | `OBJECTTYPE_ITEMBALL` | 0 | `TeamRocketBaseB1FNugget` | `EVENT_TEAM_ROCKET_BASE_B1F_NUGGET` |
| `TEAMROCKETBASEB1F_POKE_BALL3` | `SPRITE_POKE_BALL` | 21 | 12 | `STILL` | `OBJECTTYPE_ITEMBALL` | 0 | `TeamRocketBaseB1FXAccuracy` | `EVENT_TEAM_ROCKET_BASE_B1F_X_ACCURACY` |
**Scripts of interest**
- `SecurityCamera1a` .. `SecurityCamera5`: `checkevent EVENT_SECURITY_CAMERA_n` -> bail;
`PlaySecurityCameraSounds` (6x `SFX_LICK`); `checkevent EVENT_TEAM_ROCKET_BASE_POPULATION`
-> bail (never true, see below); `showemote EMOTE_SHOCK, PLAYER`;
`playmusic MUSIC_ROCKET_ENCOUNTER`; `moveobject TEAMROCKETBASEB1F_ROCKET1, <x>, <y>`;
`appear`; `applymovement SecurityCameraMovement*`; `scall TrainerCameraGrunt1`;
repeat for `TrainerCameraGrunt2`; `setevent EVENT_SECURITY_CAMERA_n`.
Both grunt scripts use `loadtrainer` + `startbattle` directly (no `trainer` macro,
so no per-trainer beat flag) - the same two teams are re-fought at every camera.
- `TeamRocketBaseB1FSecretSwitch` (`45:4757`): if `EVENT_TURNED_OFF_SECURITY_CAMERAS`
is already set, prints "the switch is turned off"; otherwise
`playsound SFX_TALLY` and sets
`EVENT_TURNED_OFF_SECURITY_CAMERAS` + `EVENT_SECURITY_CAMERA_1..5` in one go.
Cell `19,11` is a wall, read from `19,12` facing up.
- `TrainerScientistJed` -> `.Script` after-battle text tells you about the warp panel.
- `TrainerGruntM16` -> after-battle text about the traps.
**Flags and events** (B1F)
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_SECURITY_CAMERA_1` .. `_5` | `constants/event_flags.asm:403-407` | set by each camera script and by the switch | camera n is spent |
| `EVENT_TURNED_OFF_SECURITY_CAMERAS` | `event_flags.asm:402` | `TeamRocketBaseB1FSecretSwitch`, `RocketBaseElectrodeScript` | switch state only |
| `EVENT_TEAM_ROCKET_BASE_SECURITY_GRUNTS` | `event_flags.asm:1147` | `disappear`/`appear` of `TEAMROCKETBASEB1F_ROCKET1` | ambush grunt visibility |
| `EVENT_TEAM_ROCKET_BASE_POPULATION` | `event_flags.asm:1148` | read by 8 camera scripts, shared visibility flag of 11 base NPCs | **never set anywhere in the ROM** - grep confirms no `setevent`/`disappear` on it, so those `checkevent` arms are dead and the base NPCs never vanish |
| `EVENT_TEAM_ROCKET_BASE_B1F_HYPER_POTION` / `_NUGGET` / `_X_ACCURACY` | `event_flags.asm:1034-1036` | `itemball` | item ball taken |
| `EVENT_TEAM_ROCKET_BASE_B1F_HIDDEN_REVIVE` | `event_flags.asm:144` | `hiddenitem` | hidden REVIVE at `3,11` taken |
| `EVENT_EXPLODING_TRAP_1..22` | `constants/event_flags.asm` | each `ExplodingTrapN` | trap spent |
**Items** (B1F)
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `HYPER_POTION` | item ball at `27,6` | `TeamRocketBaseB1FHyperPotion` | `EVENT_TEAM_ROCKET_BASE_B1F_HYPER_POTION` |
| `NUGGET` | item ball at `14,15` | `TeamRocketBaseB1FNugget` | `EVENT_TEAM_ROCKET_BASE_B1F_NUGGET` |
| `X_ACCURACY` | item ball at `21,12` | `TeamRocketBaseB1FXAccuracy` | `EVENT_TEAM_ROCKET_BASE_B1F_X_ACCURACY` |
| `REVIVE` | hidden, face cell `3,11` | `bg_event 3, 11, BGEVENT_ITEM` | `EVENT_TEAM_ROCKET_BASE_B1F_HIDDEN_REVIVE` |
**Trainers** (B1F)
| const | class | id | party (`data/trainers/parties.asm`) | script label | notes |
|---|---|---|---|---|---|
| `GRUNTM`, `GRUNTM_16` | `GRUNTM` (`$1f`) | 16 | RATTATA 16, RATTATA 16, RATTATA 16, RATTATA 16 | `TrainerGruntM16` | `EVENT_BEAT_ROCKET_GRUNTM_16`, sight 3 |
| `SCIENTIST`, `JED` | `SCIENTIST` (`$0e`) | 3 | MAGNEMITE 20 x3 | `TrainerScientistJed` | `EVENT_BEAT_SCIENTIST_JED`, sight 3 |
| `GRUNTM`, `GRUNTM_20` | `GRUNTM` | 20 | DROWZEE 17, ZUBAT 19 | `TrainerCameraGrunt1` | no beat flag, repeats per camera |
| `GRUNTM`, `GRUNTM_21` | `GRUNTM` | 21 | ZUBAT 16, GRIMER 17, RATTATA 18 | `TrainerCameraGrunt2` | no beat flag, repeats per camera |
**Wild encounters**: none. `TEAM_ROCKET_BASE_B1F` has no entry in
`data/wild/johto_grass.asm`, `johto_water.asm`, `fish.asm` or `treemons.asm`. The
only wild battles are the `loadwildmon` traps above and the three Electrode on B2F.
### MAP_TEAM_ROCKET_BASE_B2F
- Script: `maps/TeamRocketBaseB2F.asm` (`45:4c2b TeamRocketBaseB2F_MapScripts`)
- Blocks: `maps/TeamRocketBaseB2F.blk`
- Header: `data/maps/maps.asm:120` -> `TILESET_FACILITY, DUNGEON, LANDMARK_MAHOGANY_TOWN, MUSIC_ROCKET_HIDEOUT, TRUE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `map_const TEAM_ROCKET_BASE_B2F, 15, 9` (group 3, map id 42) = 30x18 cells
- Scene ids: `SCENE_TEAMROCKETBASEB2F_LANCE_HEALS` = 0, `..._ROCKET_BOSS` = 1, `..._ELECTRODES` = 2, `..._NOOP` = 3
- Callback: `MAPCALLBACK_TILES, TeamRocketBaseB2FTransmitterDoorCallback` - if `EVENT_OPENED_DOOR_TO_ROCKET_HIDEOUT_TRANSMITTER`, `changeblock 14, 12, $07 ; floor`
Walkable regions (flood fill of `maps/TeamRocketBaseB2F.blk` against
`data/tilesets/facility_collision.asm`) - this floor is four disconnected rooms:
| region | contents |
|---|---|
| top corridor | warp 2 `3,2`, warp 3 `27,2`, GruntM18 `2,1` |
| transmitter room | boss coord `14,11`/`15,11`, Electrodes `7,5`/`7,7`/`7,9` and `22,5`/`22,7`/`22,9`; sealed until the password door opens |
| TM room | warp 4 `3,6`, TM_THIEF item ball `3,10` |
| bottom corridor | warp 1 `3,14` (to B1F), warp 5 `27,14`, Lance heal coords, GruntM19 `21,14`, GruntM17 `25,13` |
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 14 | `TEAM_ROCKET_BASE_B1F` | 2 |
| 2 | 3 | 2 | `TEAM_ROCKET_BASE_B3F` | 1 |
| 3 | 27 | 2 | `TEAM_ROCKET_BASE_B3F` | 2 |
| 4 | 3 | 6 | `TEAM_ROCKET_BASE_B3F` | 3 |
| 5 | 27 | 14 | `TEAM_ROCKET_BASE_B3F` | 4 |
**Coord events**
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `LANCE_HEALS` (0) | 5 | 14 | `LanceHealsScript1` | face UP, then `LanceHealsCommon` |
| `LANCE_HEALS` (0) | 4 | 13 | `LanceHealsScript2` | face RIGHT, then `LanceHealsCommon` |
| `ROCKET_BOSS` (1) | 14 | 11 | `RocketBaseBossFLeft` | Executive F ambush (Lance spawns at `9,13`) |
| `ROCKET_BOSS` (1) | 15 | 11 | `RocketBaseBossFRight` | same, actors nudged one cell right |
| `ELECTRODES` (2) | 14 | 12 | `RocketBaseCantLeaveScript` | shoves you back into the room |
| `ELECTRODES` (2) | 15 | 12 | `RocketBaseCantLeaveScript` | same |
| `ELECTRODES` (2) | 12 | 3 | `RocketBaseLancesSideScript` | "Leave this side to me", step LEFT |
| `ELECTRODES` (2) | 12 | 10 | `RocketBaseLancesSideScript` | same |
| `ELECTRODES` (2) | 12 | 11 | `RocketBaseLancesSideScript` | same |
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 14 | 12 | `BGEVENT_IFNOTSET` | `conditional_event EVENT_OPENED_DOOR_TO_ROCKET_HIDEOUT_TRANSMITTER, TeamRocketBaseB2FLockedDoor.Script` |
| 15 | 12 | `BGEVENT_IFNOTSET` | same |
| 12..17, 4 and 9; 12, 5..8; 17, 5..8 (20 cells) | | `BGEVENT_READ` | `TeamRocketBaseB2FTransmitterScript` |
| 26 | 7 | `BGEVENT_ITEM` | `TeamRocketBaseB2FHiddenFullHeal` -> `hiddenitem FULL_HEAL, EVENT_TEAM_ROCKET_BASE_B2F_HIDDEN_FULL_HEAL` |
**Object events**
| const | sprite | x | y | movement | type | sight | script label | event flag |
|---|---|---|---|---|---|---|---|---|
| `TEAMROCKETBASEB2F_ROCKET1` | `SPRITE_ROCKET` | 20 | 16 | `STANDING_UP` | `SCRIPT` | 0 | `ObjectEvent` | `EVENT_TEAM_ROCKET_BASE_B2F_GRUNT_WITH_EXECUTIVE` |
| `TEAMROCKETBASEB2F_ROCKET_GIRL` | `SPRITE_ROCKET_GIRL` | 20 | 16 | `STANDING_UP` | `SCRIPT` | 0 | `ObjectEvent` | `EVENT_TEAM_ROCKET_BASE_B2F_EXECUTIVE` |
| `TEAMROCKETBASEB2F_LANCE` | `SPRITE_LANCE` | 5 | 13 | `STANDING_DOWN` | `SCRIPT` | 0 | `ObjectEvent` | `EVENT_TEAM_ROCKET_BASE_B2F_LANCE` |
| `TEAMROCKETBASEB2F_DRAGON` | `SPRITE_DRAGON` | 9 | 13 | `STANDING_RIGHT` | `SCRIPT` | 0 | `ObjectEvent` | `EVENT_TEAM_ROCKET_BASE_B2F_DRAGONITE` |
| `TEAMROCKETBASEB2F_ELECTRODE1` | `SPRITE_VOLTORB` | 7 | 5 | `POKEMON` | `SCRIPT` | 0 | `RocketElectrode1` | `EVENT_TEAM_ROCKET_BASE_B2F_ELECTRODE_1` |
| `TEAMROCKETBASEB2F_ELECTRODE2` | `SPRITE_VOLTORB` | 7 | 7 | `POKEMON` | `SCRIPT` | 0 | `RocketElectrode2` | `EVENT_TEAM_ROCKET_BASE_B2F_ELECTRODE_2` |
| `TEAMROCKETBASEB2F_ELECTRODE3` | `SPRITE_VOLTORB` | 7 | 9 | `POKEMON` | `SCRIPT` | 0 | `RocketElectrode3` | `EVENT_TEAM_ROCKET_BASE_B2F_ELECTRODE_3` |
| `TEAMROCKETBASEB2F_ELECTRODE4` | `SPRITE_VOLTORB` | 22 | 5 | `POKEMON` | `SCRIPT` | 0 | `ObjectEvent` | `EVENT_TEAM_ROCKET_BASE_B2F_ELECTRODE_1` |
| `TEAMROCKETBASEB2F_ELECTRODE5` | `SPRITE_VOLTORB` | 22 | 7 | `POKEMON` | `SCRIPT` | 0 | `ObjectEvent` | `EVENT_TEAM_ROCKET_BASE_B2F_ELECTRODE_2` |
| `TEAMROCKETBASEB2F_ELECTRODE6` | `SPRITE_VOLTORB` | 22 | 9 | `POKEMON` | `SCRIPT` | 0 | `ObjectEvent` | `EVENT_TEAM_ROCKET_BASE_B2F_ELECTRODE_3` |
| `TEAMROCKETBASEB2F_ROCKET2` | `SPRITE_ROCKET` | 25 | 13 | `STANDING_DOWN` | `TRAINER` | 3 | `TrainerGruntM17` | `EVENT_TEAM_ROCKET_BASE_POPULATION` |
| `TEAMROCKETBASEB2F_ROCKET3` | `SPRITE_ROCKET` | 2 | 1 | `STANDING_RIGHT` | `TRAINER` | 3 | `TrainerGruntM18` | `EVENT_TEAM_ROCKET_BASE_POPULATION` |
| `TEAMROCKETBASEB2F_ROCKET4` | `SPRITE_ROCKET` | 21 | 14 | `STANDING_LEFT` | `TRAINER` | 4 | `TrainerGruntM19` | `EVENT_TEAM_ROCKET_BASE_POPULATION` |
| `TEAMROCKETBASEB2F_POKE_BALL` | `SPRITE_POKE_BALL` | 3 | 10 | `STILL` | `ITEMBALL` | 0 | `TeamRocketBaseB2FTMThief` | `EVENT_TEAM_ROCKET_BASE_B2F_TM_THIEF` |
The Electrodes on the right (`22,5/7/9`) mirror the left three and share their flags;
`RocketElectrodeN` disappears both of a pair, so Lance's three "count" without ever
being battled.
**Scripts of interest**
- `LanceHealsCommon` (`45:4d4c`): `special FadeOutToWhite`, `playsound SFX_FULL_HEAL`,
`special HealParty`, `special FadeInFromWhite`,
`setscene SCENE_TEAMROCKETBASEB2F_ROCKET_BOSS`,
`setevent EVENT_LANCE_HEALED_YOU_IN_TEAM_ROCKET_BASE`, then `readvar VAR_FACING`
picks one of two exit movements and `disappear TEAMROCKETBASEB2F_LANCE`.
**This is a hard ordering dependency**: the boss ambush coord events only exist
on scene 1, so the transmitter room cannot be entered until Lance has healed you.
- `TeamRocketBaseB2FLockedDoor` (`45:4e8f`): `conditional_event` on
`EVENT_OPENED_DOOR_TO_ROCKET_HIDEOUT_TRANSMITTER`; `.Script` checks
`EVENT_LEARNED_HAIL_GIOVANNI`. If known: `playsound SFX_ENTER_DOOR`,
`changeblock 14, 12, $07`, `refreshmap`, `setevent EVENT_OPENED_DOOR_TO_ROCKET_HIDEOUT_TRANSMITTER`.
- `RocketBaseBossFScript` (`45:4c67`): cutscene, then
`loadtrainer EXECUTIVEF, EXECUTIVEF_2` + `startbattle`;
sets `EVENT_TEAM_ROCKET_BASE_B2F_EXECUTIVE`, `..._GRUNT_WITH_EXECUTIVE`,
`..._LANCE`, `EVENT_BEAT_ROCKET_EXECUTIVEF_2`; then
`setscene SCENE_TEAMROCKETBASEB2F_ELECTRODES`, `clearevent EVENT_TEAM_ROCKET_BASE_B2F_LANCE`
(re-showing Lance), the Electrode briefing, `disappear TEAMROCKETBASEB2F_LANCE`.
- `RocketElectrode1/2/3`: `cry ELECTRODE`, `loadwildmon ELECTRODE, 23`, `startbattle`.
`iftrue TeamRocketBaseB2FReloadMap` covers the "you lost/fled" path. After all three
events are set, the player is auto-walked out by
`RocketBasePlayerLeavesElectrodesMovement1/2/3` into `RocketBaseElectrodeScript`.
No `BATTLETYPE_*` is armed here, so these are catchable ordinary wild battles.
- `RocketBaseElectrodeScript` (`45:4e3c`) - the section's payoff:
`verbosegiveitem HM_WHIRLPOOL`, `setevent EVENT_GOT_HM06_WHIRLPOOL`, then
`setevent EVENT_CLEARED_ROCKET_HIDEOUT`,
`clearflag ENGINE_ROCKET_SIGNAL_ON_CH20`,
`setevent EVENT_ROUTE_43_GATE_ROCKETS`,
`setevent EVENT_MAHOGANY_TOWN_POKEFAN_M_BLOCKS_GYM` (hides the fisher -> gym opens),
`setscene SCENE_TEAMROCKETBASEB2F_NOOP`,
`clearevent EVENT_LAKE_OF_RAGE_CIVILIANS`,
`setevent EVENT_TURNED_OFF_SECURITY_CAMERAS` + `EVENT_SECURITY_CAMERA_1..5`.
**Items** (B2F)
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `TM_THIEF` (TM46) | item ball at `3,10` (TM room) | `TeamRocketBaseB2FTMThief` | `EVENT_TEAM_ROCKET_BASE_B2F_TM_THIEF` |
| `FULL_HEAL` | hidden, face cell `26,7` | `bg_event 26, 7, BGEVENT_ITEM` | `EVENT_TEAM_ROCKET_BASE_B2F_HIDDEN_FULL_HEAL` |
| `HM_WHIRLPOOL` (HM06) | `verbosegiveitem` from Lance | `RocketBaseElectrodeScript` | `EVENT_GOT_HM06_WHIRLPOOL` |
TM numbering check: `constants/item_constants.asm` `add_tm THIEF ; ee` is the 46th
`add_tm` (the `const ITEM_C3` / `const ITEM_DC` holes do not consume TM numbers), so
TM46 = Thief, matching the walkthrough. `add_tm ICY_WIND ; cf` is the 16th = TM16.
**Trainers** (B2F)
| const | class | id | party | script label | sight |
|---|---|---|---|---|---|
| `GRUNTM_17` | `GRUNTM` | 17 | GOLBAT 18 | `TrainerGruntM17` | 3 |
| `GRUNTM_18` | `GRUNTM` | 18 | RATTATA 17, ZUBAT 17, RATTATA 17 | `TrainerGruntM18` | 3 |
| `GRUNTM_19` | `GRUNTM` | 19 | VENONAT 18, VENONAT 18 | `TrainerGruntM19` | 4 |
| `EXECUTIVEF_2` | `EXECUTIVEF` (`$37`) | 2 | ARBOK 23 (WRAP/LEER/POISON_STING/BITE), GLOOM 23 (ABSORB/SWEET_SCENT/SLEEP_POWDER/ACID), MURKROW 25 (PECK/PURSUIT/HAZE/-) | `RocketBaseBossFScript` | cutscene |
**Wild encounters**: none in the tables; only `loadwildmon ELECTRODE, 23` x3.
### MAP_TEAM_ROCKET_BASE_B3F
- Script: `maps/TeamRocketBaseB3F.asm` (`45:5c8c TeamRocketBaseB3F_MapScripts`)
- Blocks: `maps/TeamRocketBaseB3F.blk`
- Header: `data/maps/maps.asm:121` -> `TILESET_FACILITY, DUNGEON, LANDMARK_MAHOGANY_TOWN, MUSIC_ROCKET_HIDEOUT, TRUE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `map_const TEAM_ROCKET_BASE_B3F, 15, 9` (group 3, map id 43) = 30x18 cells
- Scene ids: `SCENE_TEAMROCKETBASEB3F_LANCE_GETS_PASSWORD` = 0, `..._RIVAL_ENCOUNTER` = 1, `..._ROCKET_BOSS` = 2, `..._NOOP` = 3
- Callback: `MAPCALLBACK_TILES, TeamRocketBaseB3FCheckGiovanniDoorCallback` - if `EVENT_OPENED_DOOR_TO_GIOVANNIS_OFFICE`, `changeblock 10, 8, $07 ; floor`
Walkable regions (same flood-fill method):
| region | contents |
|---|---|
| left | warp 1 `3,2`, warp 3 `3,6`, rival coord `8,10` |
| Giovanni's office | boss coords `10,8`/`11,8`, ExecutiveM `8,3`, Murkrow `7,2`; sealed until both passwords |
| right / main | warp 2 `27,2`, warp 4 `27,14`, Lance `25,14`, GruntF5 `21,7`, GruntM28 `5,15`, Ross `25,12`, Mitch `14,15`, all four item balls |
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 2 | `TEAM_ROCKET_BASE_B2F` | 2 |
| 2 | 27 | 2 | `TEAM_ROCKET_BASE_B2F` | 3 |
| 3 | 3 | 6 | `TEAM_ROCKET_BASE_B2F` | 4 |
| 4 | 27 | 14 | `TEAM_ROCKET_BASE_B2F` | 5 |
**Coord events**
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `ROCKET_BOSS` (2) | 10 | 8 | `RocketBaseBossLeft` | auto-walk `UP UP UP LEFT LEFT`, then `RocketBaseBoss` |
| `ROCKET_BOSS` (2) | 11 | 8 | `RocketBaseBossRight` | auto-walk `UP UP LEFT UP LEFT LEFT`, then `RocketBaseBoss` |
| `RIVAL_ENCOUNTER` (1) | 8 | 10 | `RocketBaseRival` | rival cutscene, no battle |
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 10 | 9 | `BGEVENT_IFNOTSET` | `conditional_event EVENT_OPENED_DOOR_TO_GIOVANNIS_OFFICE, TeamRocketBaseB3FLockedDoor.Script` |
| 11 | 9 | `BGEVENT_IFNOTSET` | same |
| 10..13, 1 | | `BGEVENT_READ` | `TeamRocketBaseB3FOathScript` (`jumpstd TeamRocketOathScript`) |
| 4..7, 13 | | `BGEVENT_READ` | `TeamRocketBaseB3FOathScript` |
**Object events**
| const | sprite | x | y | movement | type | sight | script label | event flag |
|---|---|---|---|---|---|---|---|---|
| `TEAMROCKETBASEB3F_LANCE` | `SPRITE_LANCE` | 25 | 14 | `STANDING_DOWN` | `SCRIPT` | 0 | `LanceGetPasswordScript` | `EVENT_TEAM_ROCKET_BASE_B3F_LANCE_PASSWORDS` |
| `TEAMROCKETBASEB3F_ROCKET1` | `SPRITE_ROCKET` | 8 | 3 | `STANDING_UP` | `SCRIPT` | 0 | `ObjectEvent` | `EVENT_TEAM_ROCKET_BASE_B3F_EXECUTIVE` |
| `TEAMROCKETBASEB3F_MOLTRES` | `SPRITE_MOLTRES` | 7 | 2 | `POKEMON` | `SCRIPT` | 0 | `RocketBaseMurkrow` | `EVENT_TEAM_ROCKET_BASE_POPULATION` |
| `TEAMROCKETBASEB3F_ROCKET_GIRL` | `SPRITE_ROCKET_GIRL` | 21 | 7 | `STANDING_UP` | `TRAINER` | **0** | `SlowpokeTailGrunt` | `EVENT_TEAM_ROCKET_BASE_POPULATION` |
| `TEAMROCKETBASEB3F_ROCKET2` | `SPRITE_ROCKET` | 5 | 15 | `SPINRANDOM_FAST` | `TRAINER` | 3 | `RaticateTailGrunt` | `EVENT_TEAM_ROCKET_BASE_POPULATION` |
| `TEAMROCKETBASEB3F_SCIENTIST1` | `SPRITE_SCIENTIST` | 25 | 12 | `STANDING_LEFT` | `TRAINER` | 4 | `TrainerScientistRoss` | `EVENT_TEAM_ROCKET_BASE_POPULATION` |
| `TEAMROCKETBASEB3F_SCIENTIST2` | `SPRITE_SCIENTIST` | 14 | 15 | `STANDING_UP` | `TRAINER` | 3 | `TrainerScientistMitch` | `EVENT_TEAM_ROCKET_BASE_POPULATION` |
| `TEAMROCKETBASEB3F_ROCKET3` | `SPRITE_ROCKET` | 24 | 14 | `STANDING_DOWN` | `SCRIPT` | 0 | `TeamRocketBaseB3FRocketScript` | `EVENT_TEAM_ROCKET_BASE_POPULATION` |
| `TEAMROCKETBASEB3F_RIVAL` | `SPRITE_RIVAL` | 4 | 5 | `STANDING_DOWN` | `SCRIPT` | 0 | `ObjectEvent` | `EVENT_RIVAL_TEAM_ROCKET_BASE` |
| `TEAMROCKETBASEB3F_POKE_BALL1` | `SPRITE_POKE_BALL` | 1 | 12 | `STILL` | `ITEMBALL` | 0 | `TeamRocketBaseB3FFullHeal` | `EVENT_TEAM_ROCKET_BASE_B3F_FULL_HEAL` |
| `TEAMROCKETBASEB3F_POKE_BALL2` | `SPRITE_POKE_BALL` | 3 | 12 | `STILL` | `ITEMBALL` | 0 | `TeamRocketBaseB3FDireHit` | `EVENT_TEAM_ROCKET_BASE_B3F_DIRE_HIT` |
| `TEAMROCKETBASEB3F_POKE_BALL3` | `SPRITE_POKE_BALL` | 28 | 9 | `STILL` | `ITEMBALL` | 0 | `TeamRocketBaseB3FProtein` | `EVENT_TEAM_ROCKET_BASE_B3F_PROTEIN` |
| `TEAMROCKETBASEB3F_POKE_BALL4` | `SPRITE_POKE_BALL` | 17 | 2 | `STILL` | `ITEMBALL` | 0 | `TeamRocketBaseB3FIceHeal` | `EVENT_TEAM_ROCKET_BASE_B3F_ICE_HEAL` |
The Murkrow the walkthrough talks to is the object using `SPRITE_MOLTRES` at `7,2`;
its script is `RocketBaseMurkrow` and its text says MURKROW. The sprite constant is
the generic large-bird overworld sprite, not a Moltres encounter.
**Scripts of interest**
- `LanceGetPasswordScript` (`45:5cb4`): both the `sdefer` scene script for scene 0
and the Lance object's own script. Runs on entry to the right region, prints
`LanceGetPasswordText`, `disappear TEAMROCKETBASEB3F_LANCE` (which sets
`EVENT_TEAM_ROCKET_BASE_B3F_LANCE_PASSWORDS`), `setscene SCENE_TEAMROCKETBASEB3F_RIVAL_ENCOUNTER`.
- `SlowpokeTailGrunt` -> `trainer GRUNTF, GRUNTF_5, EVENT_BEAT_ROCKET_GRUNTF_5, ...`;
`GruntF5Script` (the after-battle arm) does `setevent EVENT_LEARNED_SLOWPOKETAIL`.
**Sight range 0** - she never initiates; the bot must walk up and talk, then talk
a second time for the password.
- `RaticateTailGrunt` -> `trainer GRUNTM, GRUNTM_28, EVENT_BEAT_ROCKET_GRUNTM_28, ...`;
`GruntM28Script` does `setevent EVENT_LEARNED_RATICATE_TAIL`. Sight 3,
`SPINRANDOM_FAST` (the "spinning Team Rocket member").
- `TeamRocketBaseB3FLockedDoor` (`45:5da9`): `.Script` requires
`EVENT_LEARNED_SLOWPOKETAIL` **and** `EVENT_LEARNED_RATICATE_TAIL`; on success
`changeblock 10, 8, $07`, `refreshmap`, `setevent EVENT_OPENED_DOOR_TO_GIOVANNIS_OFFICE`.
- `RocketBaseRival` (`45:5cd1`): `appear TEAMROCKETBASEB3F_RIVAL`, walk-in, dialogue,
`applymovement PLAYER, RocketBaseRivalShovesPlayerMovement` (a `big_step RIGHT`
with `fix_facing`), `disappear`, `setscene SCENE_TEAMROCKETBASEB3F_ROCKET_BOSS`.
No battle - matches the walkthrough ("He can't battle you though").
- `RocketBaseBoss` (`45:5d0e`): `loadtrainer EXECUTIVEM, EXECUTIVEM_4`, `startbattle`,
`setevent EVENT_BEAT_ROCKET_EXECUTIVEM_4`, long exit movement,
`disappear TEAMROCKETBASEB3F_ROCKET1`, `setscene SCENE_TEAMROCKETBASEB3F_NOOP`.
- `RocketBaseMurkrow` (`45:5d49`): text, then `setevent EVENT_LEARNED_HAIL_GIOVANNI`.
**Items** (B3F)
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `FULL_HEAL` | item ball `1,12` | `TeamRocketBaseB3FFullHeal` | `EVENT_TEAM_ROCKET_BASE_B3F_FULL_HEAL` |
| `DIRE_HIT` | item ball `3,12` | `TeamRocketBaseB3FDireHit` | `EVENT_TEAM_ROCKET_BASE_B3F_DIRE_HIT` |
| `PROTEIN` | item ball `28,9` | `TeamRocketBaseB3FProtein` | `EVENT_TEAM_ROCKET_BASE_B3F_PROTEIN` |
| `ICE_HEAL` | item ball `17,2` | `TeamRocketBaseB3FIceHeal` | `EVENT_TEAM_ROCKET_BASE_B3F_ICE_HEAL` |
**Trainers** (B3F)
| const | class | id | party | script label | sight |
|---|---|---|---|---|---|
| `SCIENTIST`, `ROSS` | `SCIENTIST` (`$0e`) | 1 | KOFFING 22, KOFFING 22 | `TrainerScientistRoss` | 4 |
| `SCIENTIST`, `MITCH` | `SCIENTIST` | 2 | DITTO 24 | `TrainerScientistMitch` | 3 |
| `GRUNTF`, `GRUNTF_5` | `GRUNTF` (`$42`) | 5 | EKANS 18 (WRAP/LEER/POISON_STING/BITE), GLOOM 18 (ABSORB/SWEET_SCENT/STUN_SPORE/SLEEP_POWDER) | `SlowpokeTailGrunt` | 0 |
| `GRUNTM`, `GRUNTM_28` | `GRUNTM` (`$1f`) | 28 | RATICATE 19 | `RaticateTailGrunt` | 3 |
| `EXECUTIVEM`, `EXECUTIVEM_4` | `EXECUTIVEM` (`$33`) | 4 | ZUBAT 22, RATICATE 24, KOFFING 22 | `RocketBaseBoss` | cutscene |
**Wild encounters**: none.
### MAP_MAHOGANY_GYM
- Script: `maps/MahoganyGym.asm` (`51:536c MahoganyGym_MapScripts`)
- Blocks: `maps/MahoganyGym.blk`
- Header: `data/maps/maps.asm:69` -> `map MahoganyGym, TILESET_ELITE_FOUR_ROOM, INDOOR, LANDMARK_MAHOGANY_TOWN, MUSIC_GYM, TRUE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `map_const MAHOGANY_GYM, 5, 9` (group 2, map id 2) = 10x18 cells
- `def_scene_scripts` and `def_callbacks` are both **empty**
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 17 | `MAHOGANY_TOWN` | 3 |
| 2 | 5 | 17 | `MAHOGANY_TOWN` | 3 |
`def_coord_events` is empty.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 3 | 15 | `BGEVENT_READ` | `MahoganyGymStatue` |
| 6 | 15 | `BGEVENT_READ` | `MahoganyGymStatue` |
**Object events**
| const | sprite | x | y | movement | type | sight | script label | event flag |
|---|---|---|---|---|---|---|---|---|
| `MAHOGANYGYM_PRYCE` | `SPRITE_PRYCE` | 5 | 3 | `STANDING_DOWN` | `SCRIPT` | 0 | `MahoganyGymPryceScript` | -1 |
| `MAHOGANYGYM_BEAUTY1` | `SPRITE_BEAUTY` | 4 | 6 | `STANDING_DOWN` | `TRAINER` | 1 | `TrainerSkierRoxanne` | -1 |
| `MAHOGANYGYM_ROCKER1` | `SPRITE_ROCKER` | 0 | 17 | `STANDING_UP` | `TRAINER` | 1 | `TrainerBoarderRonald` | -1 |
| `MAHOGANYGYM_BEAUTY2` | `SPRITE_BEAUTY` | 9 | 17 | `STANDING_UP` | `TRAINER` | 1 | `TrainerSkierClarissa` | -1 |
| `MAHOGANYGYM_ROCKER2` | `SPRITE_ROCKER` | 5 | 9 | `STANDING_DOWN` | `TRAINER` | 1 | `TrainerBoarderBrad` | -1 |
| `MAHOGANYGYM_ROCKER3` | `SPRITE_ROCKER` | 2 | 4 | `SPINRANDOM_FAST` | `TRAINER` | 1 | `TrainerBoarderDouglas` | -1 |
| `MAHOGANYGYM_GYM_GUIDE` | `SPRITE_GYM_GUIDE` | 7 | 15 | `STANDING_DOWN` | `SCRIPT` | 0 | `MahoganyGymGuideScript` | -1 |
**Ice-floor map**, decoded from `maps/MahoganyGym.blk` against
`data/tilesets/elite_four_room_collision.asm`. `I` = `COLL_ICE` (`$23`),
`.` = walkable floor, `#` = wall, `v` = warp carpet:
```
x0123456789
y0 ##########
y1 ##########
y2 .IIIII.I#I
y3 IIIIIIII#I
y4 I#I.I.IIII
y5 I#IIII.III
y6 I#IIIIIIII
y7 I#II.IIII.
y8 .IIIIIII#I
y9 IIIIIIII#I
y10 I#.II.III.
y11 I#IIIII.II
y12 I#IIIIII#I
y13 I#I.II.I#I
y14 I#.#..#.#I
y15 I#.#..#.#I
y16 .I......I.
y17 II..vv..II
```
Sliding rule (`engine/overworld/player_movement.asm`): `CheckIceTile` on the tile
you land on -> `STEP_ICE`; `.CheckForced` / `CheckStandingOnIce` re-injects the same
d-pad direction, so a single press slides until you land on a non-ice tile, hit a
wall, or hit an NPC.
Simulating that rule against the grid above (NPCs treated as blockers) reproduces
the walkthrough's directions exactly:
| from | press | lands on | who |
|---|---|---|---|
| `4,17` (warp) | `UP` | `4,16` | - |
| `4,16` | `LEFT`, `LEFT`, `LEFT` | `3,16` -> `2,16` -> `0,16` | faces Boarder Ronald `0,17` |
| `0,16` | `UP`,`UP`,`RIGHT`,`DOWN`,`LEFT` | `0,8` -> `0,2` -> `6,2` -> `6,5` -> `2,5` | faces Boarder Douglas `2,4` |
| `2,5` | `DOWN`,`RIGHT` | `2,10` -> `5,10` | faces Boarder Brad `5,9` |
| `5,10` | `RIGHT`,`DOWN` | `9,10` -> `9,16` | faces Skier Clarissa `9,17` |
| `9,16` | `UP`,`UP`,`LEFT` | `9,10` -> `9,7` -> `4,7` | faces Skier Roxanne `4,6` |
| `4,7` | `DOWN` | `4,14` | back toward the exit |
| `2,14` ("just left of the left pillar", pillar block at `3,14`/`3,15`) | `UP`,`UP`,`RIGHT`,`DOWN`,`LEFT`,`UP`,`RIGHT` | `2,10` -> `2,5` -> `6,5` -> `6,13` -> `3,13` -> `3,4` -> `5,4` | standing directly below Pryce `5,3` |
Reaching `2,14` from the entrance: `UP` (to `4,16`), `LEFT` (`3,16`), `LEFT`
(`2,16`), `UP` (`2,15`), `UP` (`2,14`) - `2,15`/`2,16`/`2,14` are ordinary floor,
not ice, so those are single steps.
**Scripts of interest**
`MahoganyGymPryceScript` (`51:536e`):
1. `checkevent EVENT_BEAT_PRYCE` -> `.FightDone`
2. `writetext PryceText_Intro`; `winlosstext PryceText_Impressed, 0`;
`loadtrainer PRYCE, PRYCE1`; `startbattle`; `reloadmapafterbattle`
3. `setevent EVENT_BEAT_PRYCE`; `Text_ReceivedGlacierBadge`;
`playsound SFX_GET_BADGE`; `setflag ENGINE_GLACIERBADGE`
4. `readvar VAR_BADGES`; `scall MahoganyGymActivateRockets` -
`ifequal 7, .RadioTowerRockets` / `ifequal 6, .GoldenrodRockets`.
Badge count is read **after** GLACIERBADGE is set, so the normal route
(Glacier as the 7th badge) takes `jumpstd RadioTowerRocketsScript`
(`engine/events/std_scripts.asm:255`), which sets
`ENGINE_ROCKETS_IN_RADIO_TOWER`, `EVENT_GOLDENROD_CITY_CIVILIANS`,
`EVENT_RADIO_TOWER_BLACKBELT_BLOCKS_STAIRS`,
`EVENT_MAHOGANY_TOWN_POKEFAN_M_BLOCKS_EAST`,
`specialphonecall SPECIALCALL_WEIRDBROADCAST`, and
`setmapscene MAHOGANY_TOWN, SCENE_MAHOGANYTOWN_NOOP`.
Skipping Jasmine first (Glacier as the 6th) takes
`GoldenrodRocketsScript` instead, which only clears
`EVENT_GOLDENROD_CITY_ROCKET_TAKEOVER`.
5. `.FightDone`: if `EVENT_GOT_TM16_ICY_WIND` is set -> farewell text. Otherwise
`setevent EVENT_BEAT_SKIER_ROXANNE / _CLARISSA / _BOARDER_RONALD / _BRAD / _DOUGLAS`
(so the five gym trainers never battle again), `PryceText_GlacierBadgeSpeech`,
`verbosegiveitem TM_ICY_WIND`, `setevent EVENT_GOT_TM16_ICY_WIND`.
`MahoganyGymStatue`: `checkflag ENGINE_GLACIERBADGE`; before the badge
`jumpstd GymStatue1Script`, after it `gettrainername STRING_BUFFER_4, PRYCE, PRYCE1`
+ `jumpstd GymStatue2Script`.
**Flags and events** (gym)
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_PRYCE` | `constants/event_flags.asm:712` | `MahoganyGymPryceScript` | leader beaten |
| `EVENT_GOT_TM16_ICY_WIND` | `event_flags.asm:20` | `MahoganyGymPryceScript` | TM handed over |
| `ENGINE_GLACIERBADGE` | `constants/engine_flags.asm:44` | set here; read by `WhirlpoolFunction` and `MahoganyGymStatue` | badge 7 |
| `EVENT_BEAT_SKIER_ROXANNE`, `_CLARISSA`, `EVENT_BEAT_BOARDER_RONALD`, `_BRAD`, `_DOUGLAS` | `constants/event_flags.asm` | the five `trainer` macros, force-set by Pryce | gym trainer beaten |
**Trainers** (gym)
| const | class | id | party (`data/trainers/parties.asm`) | script label |
|---|---|---|---|---|
| `BOARDER`, `RONALD` | `BOARDER` (`$3a`) | 1 | SEEL 24, DEWGONG 25, SEEL 24 | `TrainerBoarderRonald` |
| `BOARDER`, `BRAD` | `BOARDER` | 2 | SWINUB 26, SWINUB 26 | `TrainerBoarderBrad` |
| `BOARDER`, `DOUGLAS` | `BOARDER` | 3 | SHELLDER 24, CLOYSTER 25, SHELLDER 24 | `TrainerBoarderDouglas` |
| `SKIER`, `ROXANNE` | `SKIER` (`$21`) | 1 | JYNX 28 | `TrainerSkierRoxanne` |
| `SKIER`, `CLARISSA` | `SKIER` | 2 | DEWGONG 28 | `TrainerSkierClarissa` |
| `PRYCE`, `PRYCE1` | `PRYCE` (`$05`) | 1 | SEEL 27 (HEADBUTT/ICY_WIND/AURORA_BEAM/REST), DEWGONG 29 (HEADBUTT/ICY_WIND/AURORA_BEAM/REST), PILOSWINE 31 (ICY_WIND/FURY_ATTACK/MIST/BLIZZARD) | `MahoganyGymPryceScript` |
**Wild encounters**: none.
---
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Hideout entrance does not exist | `maps/MahoganyMart1F.asm:MahoganyMart1FStaircaseCallback` + `MahoganyMart1FLanceUncoversStaircaseScript` (`45:4046`) | `EVENT_UNCOVERED_STAIRCASE_IN_MAHOGANY_MART` for the block; the cutscene needs `SCENE_MAHOGANYMART1F_LANCE_UNCOVERS_STAIRS` | `maps/LakeOfRage.asm:LakeOfRageLanceScript.AgreedToHelp` (previous section) |
| Camera grunt ambushes | `maps/TeamRocketBaseB1F.asm:SecurityCamera1a..5` | `EVENT_SECURITY_CAMERA_n` clear | beat both grunts at that camera, or press the switch at `19,11` (`TeamRocketBaseB1FSecretSwitch`, `45:4757`) |
| Exploding floor traps | `maps/TeamRocketBaseB1F.asm:ExplodingTrap1..22` | `EVENT_EXPLODING_TRAP_n` clear | step on the tile once (or route around; safe cells listed above) |
| B2F transmitter room sealed (block) | `maps/TeamRocketBaseB2F.asm:TeamRocketBaseB2FLockedDoor` (`45:4e8f`) + `TeamRocketBaseB2FTransmitterDoorCallback` | `EVENT_LEARNED_HAIL_GIOVANNI` | talk to the Murkrow object at B3F `7,2` (`RocketBaseMurkrow`, `45:5d49`) after beating ExecutiveM 4 |
| B2F boss ambush will not fire | `maps/TeamRocketBaseB2F.asm` coord events on `SCENE_TEAMROCKETBASEB2F_ROCKET_BOSS` | map scene must be 1 | trip `LanceHealsScript1/2` at `5,14` or `4,13` first |
| B3F Giovanni's office sealed | `maps/TeamRocketBaseB3F.asm:TeamRocketBaseB3FLockedDoor` (`45:5da9`) + `TeamRocketBaseB3FCheckGiovanniDoorCallback` | `EVENT_LEARNED_SLOWPOKETAIL` **and** `EVENT_LEARNED_RATICATE_TAIL` | beat `GRUNTF_5` at `21,7` then talk again; beat `GRUNTM_28` at `5,15` then talk again |
| Cannot leave the Electrode room | `maps/TeamRocketBaseB2F.asm:RocketBaseCantLeaveScript` / `RocketBaseLancesSideScript` | scene `SCENE_TEAMROCKETBASEB2F_ELECTRODES` | faint all three left-hand Electrodes |
| Mahogany Gym door blocked | `maps/MahoganyTown.asm` object `SPRITE_FISHER` at `6,14` gated on `EVENT_MAHOGANY_TOWN_POKEFAN_M_BLOCKS_GYM`; visibility rule in `engine/overworld/map_objects_2.asm:CheckObjectFlag` | flag set = NPC hidden | `RocketBaseElectrodeScript` (`45:4e3c`) sets it when the hideout is cleared |
| Whirlpool unusable in the field | `engine/events/overworld.asm:WhirlpoolFunction.TryWhirlpool` (`03:4da0`, `ld de, ENGINE_GLACIERBADGE / call CheckBadge`) and `TryWhirlpoolOW` (`03:4e41`, `CheckPartyMove WHIRLPOOL` then `CheckEngineFlag ENGINE_GLACIERBADGE`) | GLACIERBADGE + a party member that knows WHIRLPOOL | beat Pryce |
| Rage Candy Bar merchant trip-wire | `maps/MahoganyTown.asm` coord events `19,8`/`19,9` on scene 0 | scene `SCENE_MAHOGANYTOWN_TRY_RAGECANDYBAR` | only cleared later, by `RadioTowerRocketsScript` `setmapscene MAHOGANY_TOWN, SCENE_MAHOGANYTOWN_NOOP` |
---
## 4. Bot checklist
Coordinates are `(x, y)` map cells as written in the asm. "talk" = face the target
and press A.
1. `MAHOGANY_TOWN`: warp 4 at `(15,13)` -> `MAHOGANY_POKECENTER_1F`; talk to nurse at `(3,1)`. Pre: none. Post: party healed.
2. `MAHOGANY_TOWN`: warp 1 at `(11,7)` -> `MAHOGANY_MART_1F`. Pre: `EVENT_DECIDED_TO_HELP_LANCE` set and map scene = 1. Post: `MahoganyMart1FLanceUncoversStaircaseScript` runs unattended; wait for `EVENT_UNCOVERED_STAIRCASE_IN_MAHOGANY_MART`.
3. `MAHOGANY_MART_1F`: walk to `(7,3)` -> `TEAM_ROCKET_BASE_B1F` warp 1, arriving at `(27,2)`.
4. B1F: walk west along y=2. Crossing `(24,2)` or `(24,3)` fires `SecurityCamera1a/1b` -> battle `GRUNTM_20` then `GRUNTM_21`. Post: `EVENT_SECURITY_CAMERA_1`.
5. B1F: item ball at `(27,6)` = HYPER_POTION. Post: `EVENT_TEAM_ROCKET_BASE_B1F_HYPER_POTION`.
6. B1F: continue west; `(6,2)`/`(6,3)` fires camera 2, `(24,6)`/`(24,7)` camera 3, `(22,16)` camera 4, `(8,16)` camera 5. Each is the same two grunts.
7. B1F: talk to `GRUNTM_16` at `(2,4)` (sight 3, faces RIGHT - approaching from the east triggers him). Post: `EVENT_BEAT_ROCKET_GRUNTM_16`.
8. B1F: cross the trap field x1..5 / y7..13 using only the safe cells listed in section 2, or accept the one-shot `BATTLETYPE_TRAP` battles. Optional: hidden REVIVE by facing `(3,11)`.
9. B1F: battle `SCIENTIST JED` at `(18,12)` (sight 3, faces LEFT). Post: `EVENT_BEAT_SCIENTIST_JED`. Talk again for the warp-panel hint.
10. B1F: stand at `(19,12)`, face UP, press A on the bg event at `(19,11)`. Post: `EVENT_TURNED_OFF_SECURITY_CAMERAS` + `EVENT_SECURITY_CAMERA_1..5`.
11. B1F: item balls `(21,12)` = X_ACCURACY, `(14,15)` = NUGGET.
12. B1F: walk to `(3,14)` -> `TEAM_ROCKET_BASE_B2F` arriving at `(3,14)`.
13. B2F: step on `(5,14)` or `(4,13)`. Pre: map scene 0. Post: party healed, `EVENT_LANCE_HEALED_YOU_IN_TEAM_ROCKET_BASE`, scene -> 1.
14. B2F: battle `GRUNTM_19` at `(21,14)` (sight 4, faces LEFT), then `GRUNTM_17` at `(25,13)` (sight 3, faces DOWN).
15. B2F: walk to `(27,14)` -> `TEAM_ROCKET_BASE_B3F` arriving at `(27,14)`. Pre: none. Post: `LanceGetPasswordScript` auto-runs, scene -> 1, `EVENT_TEAM_ROCKET_BASE_B3F_LANCE_PASSWORDS`.
16. B3F: item balls `(28,9)` = PROTEIN, `(17,2)` = ICE_HEAL.
17. B3F: battle `SCIENTIST ROSS` at `(25,12)` (sight 4, faces LEFT).
18. B3F: walk to `(21,8)` and talk UP to `GRUNTF_5` at `(21,7)` - **sight 0, she will not initiate**. Post: `EVENT_BEAT_ROCKET_GRUNTF_5`; talk again -> `EVENT_LEARNED_SLOWPOKETAIL`.
19. B3F: battle `SCIENTIST MITCH` at `(14,15)` (sight 3, faces UP).
20. B3F: battle `GRUNTM_28` at `(5,15)` (sight 3, spinning). Post: `EVENT_BEAT_ROCKET_GRUNTM_28`; talk again -> `EVENT_LEARNED_RATICATE_TAIL`.
21. B3F: item balls `(3,12)` = DIRE_HIT, `(1,12)` = FULL_HEAL.
22. B3F: walk to `(27,2)` -> `TEAM_ROCKET_BASE_B2F` arriving at `(27,2)`.
23. B2F top corridor: walk west along y=1..2; battle `GRUNTM_18` at `(2,1)` (sight 3, faces RIGHT).
24. B2F: walk to `(3,2)` -> `TEAM_ROCKET_BASE_B3F` arriving at `(3,2)`.
25. B3F left region: walk to `(8,10)`. Pre: scene 1. Post: rival cutscene, no battle, scene -> 2, `EVENT_RIVAL_TEAM_ROCKET_BASE` set by `disappear`.
26. B3F: stand at `(10,10)` or `(11,10)`, face UP, press A on the bg event at `(10,9)`/`(11,9)`. Pre: both password events. Post: `EVENT_OPENED_DOOR_TO_GIOVANNIS_OFFICE`, block at `(10,8)` becomes floor.
27. B3F: step onto `(10,8)` or `(11,8)`. Pre: scene 2. Post: auto-walk, battle `EXECUTIVEM_4`, `EVENT_BEAT_ROCKET_EXECUTIVEM_4`, scene -> 3.
28. B3F: talk to the bird object at `(7,2)`. Post: `EVENT_LEARNED_HAIL_GIOVANNI`.
29. B3F: walk to `(3,6)` -> `TEAM_ROCKET_BASE_B2F` arriving at `(3,6)`; item ball `(3,10)` = TM_THIEF (TM46). Return via `(3,6)`.
30. Back-track: B3F `(3,2)` -> B2F `(3,2)`; B2F top corridor east to `(27,2)` -> B3F `(27,2)`; B3F right region south to `(27,14)` -> B2F `(27,14)`; walk west to the door.
31. B2F: stand at `(14,13)`/`(15,13)`, face UP, press A on `(14,12)`/`(15,12)`. Pre: `EVENT_LEARNED_HAIL_GIOVANNI`. Post: `EVENT_OPENED_DOOR_TO_ROCKET_HIDEOUT_TRANSMITTER`, block becomes floor.
32. B2F: step onto `(14,11)` or `(15,11)`. Pre: scene 1. Post: `EXECUTIVEF_2` battle, `EVENT_BEAT_ROCKET_EXECUTIVEF_2`, scene -> 2, Lance briefing.
33. B2F: talk to the three left Electrodes at `(7,5)`, `(7,7)`, `(7,9)`. Each is a wild ELECTRODE L23 that may Selfdestruct. Do **not** walk east past x=12 (`RocketBaseLancesSideScript` bounces you back). Post: three `EVENT_TEAM_ROCKET_BASE_B2F_ELECTRODE_n`.
34. After the third, `RocketBaseElectrodeScript` auto-runs: accept `HM_WHIRLPOOL`. Post: `EVENT_GOT_HM06_WHIRLPOOL`, `EVENT_CLEARED_ROCKET_HIDEOUT`, `EVENT_MAHOGANY_TOWN_POKEFAN_M_BLOCKS_GYM`, `clearflag ENGINE_ROCKET_SIGNAL_ON_CH20`, scene -> 3.
35. Exit: B2F `(3,14)` -> B1F `(3,14)`; B1F warp panel `(5,15)` -> `(25,2)`; `(27,2)` -> mart; mart `(3,7)`/`(4,7)` -> town.
36. `MAHOGANY_TOWN`: heal at the Pokecenter, then warp 3 at `(6,13)` -> `MAHOGANY_GYM`, arriving at `(4,17)`/`(5,17)`. Pre: `EVENT_MAHOGANY_TOWN_POKEFAN_M_BLOCKS_GYM` set (otherwise the fisher stands on `(6,14)`).
37. Gym: `UP`, `LEFT`, `LEFT`, `LEFT` -> `(0,16)`, Boarder Ronald spots you from `(0,17)`.
38. Gym: `UP`,`UP`,`RIGHT`,`DOWN`,`LEFT` -> `(2,5)`, Boarder Douglas at `(2,4)`. Talk again for the Pryce-waterfall line.
39. Gym: `DOWN`,`RIGHT` -> `(5,10)`, Boarder Brad at `(5,9)`.
40. Gym: `RIGHT`,`DOWN` -> `(9,16)`, Skier Clarissa at `(9,17)`.
41. Gym: `UP`,`UP`,`LEFT` -> `(4,7)`, Skier Roxanne at `(4,6)`.
42. Gym: `DOWN` -> `(4,14)`, then walk to `(2,14)`.
43. Gym: from `(2,14)`: `UP`,`UP`,`RIGHT`,`DOWN`,`LEFT`,`UP`,`RIGHT` -> `(5,4)`. Talk UP to Pryce at `(5,3)`.
44. Battle `PRYCE1`. Post: `EVENT_BEAT_PRYCE`, `ENGINE_GLACIERBADGE`, the five gym-trainer beat flags, `EVENT_GOT_TM16_ICY_WIND`, and (at 7 badges) `RadioTowerRocketsScript`.
45. Gym: leave via `(4,17)`/`(5,17)`.
---
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Map headers / warps / coord events / bg events / objects for all six maps | `src/import/RomExtractorGen2.lua`, `src/world/gen2/Map.lua` | implemented (generic extraction, nothing map-specific needed) |
| Map scenes + `sdefer` scene scripts | `src/world/gen2/World.lua` (`World:scene`, `mapScenes`), `src/script/gen2/Vm.lua` (`sdefer`, `setscene`, `setmapscene`) | implemented |
| Coord-event trip-wires | `src/world/gen2/World.lua` (coord event dispatch) | implemented |
| `changeblock` (mart staircase, both password doors) | `src/script/gen2/Vm.lua:1006` - the comment cites `MahoganyMart1F`'s `changeblock 6, 2, $1e` and cell `(7,3)` by name | implemented |
| `BGEVENT_IFNOTSET` / `conditional_event` (both password doors) | `src/world/gen2/World.lua:5147 World:bgEventAt` only matches `(ev.kind or 0) == 0`; `src/world/gen2/HiddenItems.lua` handles kind 7 | **missing** - pressing A on the transmitter door or Giovanni's door does nothing in the port |
| Ice sliding (`STEP_ICE`, `CheckStandingOnIce` forced d-pad) | `src/world/gen2/Permissions.lua:128 ICE` exists but is only consumed by `FieldMoves.canEncounterWildMon`; `World:movePlayer` picks step frames from `Bike.stepFrames` only | **missing** - Mahogany Gym floor behaves as ordinary floor, so the puzzle and the trainer sight lines are wrong |
| Trainer sight range / approach | `src/world/gen2/Trainers.lua` (`Trainers.sees`, `Trainers.approach`) | implemented (`sight 0` on GruntF5 falls out naturally) |
| `itemball` / `hiddenitem` | `src/script/gen2/CallAsm.lua`, `src/world/gen2/HiddenItems.lua` | implemented |
| `verbosegiveitem HM_WHIRLPOOL` | `src/script/gen2/Vm.lua`, `src/script/gen2/Opcodes.lua` | implemented |
| Whirlpool field move + GLACIERBADGE gate | `src/world/gen2/FieldMoves.lua:109` (`WHIRLPOOL = "GLACIER"`), `.somethingToWhirlpool`, `.whirlpoolFromMenu`, `.tryWhirlpoolOW`, `WHIRLPOOL_BLOCKS` | implemented |
| `loadwildmon` + `BATTLETYPE_TRAP` | `src/world/gen2/World.lua:4587` (names "TRAP 9 (the Rocket base)"), `src/script/gen2/Vm.lua:699` | implemented |
| `special HealParty` / `FadeOutToWhite` / `FadeInFromWhite` / `PlayMapMusic` / `ReloadSpritesNoPalettes` / `FadeOutToBlack` / `FadeInFromBlack` / `RestartMapMusic` / `FadeOutMusic` | `src/script/gen2/Specials.lua:450, 992-1066` | implemented |
| `moveobject` / `appear` / `disappear` / `applymovement` / `follow` / `showemote` | `src/script/gen2/Vm.lua`, `src/script/gen2/Movement.lua`, `src/world/gen2/World.lua` | implemented |
| `jumpstd` (`TeamRocketOathScript`, `GymStatue1/2Script`, `PokecenterSignScript`, `RadioTowerRocketsScript`) | `src/script/gen2/Vm.lua:742`, `data/generated/std_scripts.lua` via `World.lua:635` | implemented (generic) |
| `readvar VAR_BADGES` for `MahoganyGymActivateRockets` | `src/world/gen2/World.lua:117,1240` | implemented |
| `specialphonecall SPECIALCALL_WEIRDBROADCAST` | `src/script/gen2/Vm.lua:1364` stores the id; `src/core/gen2/Phone.lua` delivers | implemented |
| `ENGINE_ROCKET_SIGNAL_ON_CH20` radio behaviour | `src/ui/gen2/Pokegear.lua:816-821, 1097` | implemented |
| Driver coverage for this stretch | `tests/drivers/gold_*.lua` - none touches Mahogany, the hideout or Pryce (`gold_icepath_boulder.lua` is the only Mahogany-adjacent one, and it is Ice Path boulders) | **missing** |
The two "missing" rows are the blocking ones: without `BGEVENT_IFNOTSET` the
hideout cannot be completed, and without ice sliding the gym cannot be crossed.
---
## 6. Unresolved / verify by hand
- **Floor labels in the walkthrough are wrong past the first password.** It says
"go up the stairs at the upper-right part of the floor, then head left on Negative
Floor 2 ... Head up the stairs to Negative Floor 1. Head right and then before you
reach the locked door, your rival will come." The rival coord event
(`8,10`) and Giovanni's office are both on **B3F**, not B1F. The route is
B3F(right) -> B2F(top) -> B3F(left). Verified by flood-filling
`maps/TeamRocketBaseB2F.blk` / `maps/TeamRocketBaseB3F.blk`: the four B2F warps
and four B3F warps sit in disconnected rooms, and B2F warp 2 `(3,2)` reaches
B3F `(3,2)` which is the region containing `8,10`.
- **Boarder Ronald's party order.** Walkthrough lists Seel 24 / Seel 24 / Dewgong 25;
`data/trainers/parties.asm` `BOARDER (1)` is SEEL 24, DEWGONG 25, SEEL 24.
Same three mon, different lead order. Same class of discrepancy for
`GRUNTM_18` (walkthrough Rattata/Rattata/Zubat, asm Rattata/Zubat/Rattata).
- **Item list is incomplete.** The walkthrough's "Items in the Team Rocket Hideout"
block lists six; the asm has ten: the six named plus `FULL_HEAL` and `DIRE_HIT`
item balls on B3F (mentioned later in the prose) and two hidden items the
walkthrough never mentions - `REVIVE` at B1F `(3,11)` and `FULL_HEAL` at B2F
`(26,7)`.
- **"Every time you pass one of those statues, two Team Rocket grunts will fight
you"** is right, but the walkthrough implies only two statues on the first floor.
There are five (`bg_event` at `24,1`, `6,1`, `24,5`, `8,15`, `22,15`) with eight
trigger cells, i.e. up to ten grunt battles if none are skipped.
- **`EVENT_TEAM_ROCKET_BASE_POPULATION` is dead.** It is the shared visibility flag
of eleven base NPCs and is read by all eight `SecurityCamera*` scripts, but a
full-tree grep finds no `setevent`, `clearevent` or `disappear` that writes it.
Those `checkevent ... iftrue NoSecurityCamera` arms therefore never take, and the
Rocket NPCs stay on the maps forever - including after the hideout is cleared.
Flagged in case a bot author expects the base to empty out.
- **Lance's three right-hand Electrodes are never battled.** `RocketElectrodeN`
disappears both members of a mirrored pair, and the objects at `22,5`/`22,7`/`22,9`
point at the no-op `ObjectEvent` script. The walkthrough's "Lance runs to the right
to take the Electrodes on the right" is flavour; the game just hides them.
- **Exact reachability of `19,11` (the secret switch)** was checked by collision
(`19,11` is a WALL, `19,12` is FLOOR and in the single B1F region), but the
in-game facing direction for the `BGEVENT_READ` was not observed running - the
bg event type is `BGEVENT_READ`, not `BGEVENT_UP`, so any adjacent facing should
work; worth confirming on hardware if a driver misses it.
- The gym slide table in section 2 was produced by simulating
`engine/overworld/player_movement.asm`'s ice rule against the decoded
`maps/MahoganyGym.blk`, treating NPCs as blockers. It reproduces every direction
string in the walkthrough, but it has not been executed in an emulator.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,762 @@
# Section 15 - Mount Mortar and Dark Cave
Source: `../section-15-mount-mortar-and-dark-cave.txt`
Maps covered: `MAP_MOUNT_MORTAR_1F_OUTSIDE`, `MAP_MOUNT_MORTAR_1F_INSIDE`,
`MAP_MOUNT_MORTAR_2F_INSIDE`, `MAP_MOUNT_MORTAR_B1F`,
`MAP_DARK_CAVE_BLACKTHORN_ENTRANCE`, `MAP_DARK_CAVE_VIOLET_ENTRANCE`.
Touched in passing (owned by neighbouring sections): `MAP_MAHOGANY_TOWN`,
`MAP_ROUTE_42`, `MAP_BLACKTHORN_CITY`, `MAP_ROUTE_45`, `MAP_ROUTE_31`,
`MAP_ROUTE_46`.
Badges / key milestones in this section: **no badge**. The milestones are
`EVENT_BEAT_BLACKBELT_KIYO` + `EVENT_GOT_TYROGUE_FROM_KIYO` (a free level 10
`TYROGUE`, the only one in the game) and `EVENT_GOT_BLACKGLASSES_IN_DARK_CAVE`.
Everything else here is optional loot. Nothing in this section gates any later
section.
A note on coordinates: every table below is transcribed verbatim from the map's
`_MapEvents` block in the named `maps/*.asm` file. `warp_event`, `coord_event`,
`bg_event` and `object_event` all share one coordinate space (the
`object_event` macro in `macros/scripts/maps.asm` applies the +4 border offset
itself), so the numbers are directly comparable. A map declared
`map_const NAME, W, H` spans x in `0 .. 2W-1` and y in `0 .. 2H-1`.
A note on the `event flag` column of `object_event`:
`engine/overworld/map_objects_2.asm` `CheckObjectFlag` **hides** the object when
the flag is set, and `-1` means always visible. So for a Poke Ball, "flag set =
already taken = gone".
A note on darkness: all six maps in this section are `PALETTE_DARK` in
`data/maps/maps.asm`. `engine/tilesets/timeofday_pals.asm`
`ReplaceTimeOfDayPals` (`23:43e9`) sends `PALETTE_DARK` down `.NeedsFlash`,
which writes `DARKNESS_PALSET` unless `STATUSFLAGS_FLASH_F` is set in
`wStatusFlags`. That bit is set by `BlindingFlash` (`engine/events/field_moves.asm`)
and cleared by `ResetFlashIfOutOfCave` (`home/flag.asm`, `00:2f1d`) on any map
whose `wEnvironment` is `ROUTE` or `TOWN`. **A bot must re-cast Flash every time
it re-enters, including after every Fly.**
---
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 1 | `MAP_MAHOGANY_TOWN` | `data/maps/maps.asm:74`, `data/maps/spawn_points.asm` `spawn MAHOGANY_TOWN, 15, 14` | Fly (`ENGINE_FLYPOINT_MAHOGANY`) | west map connection -> `ROUTE_42` | heal, buy Max Repels / Escape Ropes, drop to 5 party members so Tyrogue fits |
| 2 | `MAP_ROUTE_42` | `maps/Route42.asm` | east connection from Mahogany, or warps 1/2 at (0,8)/(0,9) from the Ecruteak gate | warp 3 (10,5) west entrance, warp 4 (28,9) middle entrance, warp 5 (46,7) east entrance -> `MOUNT_MORTAR_1F_OUTSIDE` warps 1/2/3 | "three cave entrances to Mt. Mortar" |
| 3 | `MAP_MOUNT_MORTAR_1F_OUTSIDE` | `maps/MountMortar1FOutside.asm` | warps 1/2/3 at (3,33)/(17,33)/(37,33) | warp 4 (17,5) -> 2F after Waterfall; warp 7 (17,29) -> B1F | all three Route 42 doors land on this one map; the Surf lake and the waterfall live here |
| 4 | `MAP_MOUNT_MORTAR_1F_INSIDE` | `maps/MountMortar1FInside.asm` | 1F Outside warps 5/6 (11,21)/(29,21) or 8/9 (7,13)/(33,13) | warp 5 (3,19) -> B1F; warp 6 (9,9) -> 2F | the "giant room" with the Strength boulder, Escape Rope and Hyper Potion |
| 5 | `MAP_MOUNT_MORTAR_2F_INSIDE` | `maps/MountMortar2FInside.asm` | warp 1 (17,33) from 1F Outside warp 4 (top of the waterfall) | warp 2 (3,5) -> `MOUNT_MORTAR_1F_INSIDE` warp 6 | Rare Candy, Max Potion, TM40, Dragon Scale, Elixer, Escape Rope, hidden Full Restore |
| 6 | `MAP_MOUNT_MORTAR_B1F` | `maps/MountMortarB1F.asm` | warp 1 (3,3) from 1F Inside warp 5 | Escape Rope, or Strength the boulder at (9,10) and take warp 2 (19,29) -> 1F Outside warp 7 | **Blackbelt Kiyo, then the free Tyrogue** |
| 7 | `MAP_BLACKTHORN_CITY` | `data/maps/maps.asm:187`, `data/maps/spawn_points.asm` `spawn BLACKTHORN_CITY, 21, 30` | Fly (`ENGINE_FLYPOINT_BLACKTHORN`) | south map connection (`data/maps/attributes.asm:161` `connection south, Route45, ROUTE_45, 0`) | heal, then head south |
| 8 | `MAP_ROUTE_45` | `maps/Route45.asm` | north connection from Blackthorn | warp 1 (2,5) -> `DARK_CAVE_BLACKTHORN_ENTRANCE` warp 1 | "enter the Dark Cave to the left before there are even any jumps" |
| 9 | `MAP_DARK_CAVE_BLACKTHORN_ENTRANCE` | `maps/DarkCaveBlackthornEntrance.asm` | warp 1 (23,3) | warp 2 (3,25) -> `DARK_CAVE_VIOLET_ENTRANCE` warp 2 | Blackglasses NPC, Revive, TM13 Snore, Wobbuffet |
| 10 | `MAP_DARK_CAVE_VIOLET_ENTRANCE` | `maps/DarkCaveVioletEntrance.asm` | warp 2 (17,1) | warp 1 (3,15) -> `ROUTE_31` warp 3 | "surf down, rock smash the rock, grab the Potion, head out onto Route 31" |
| 11 | `MAP_ROUTE_31` | `maps/Route31.asm` | warp 3 (34,5) | Fly back to Blackthorn | walkthrough exit |
Spill into the next section: the walkthrough's video links name Blackthorn Gym,
Route 45/46, Tohjo Falls, Routes 26/27, Victory Road and the Elite Four, but its
**prose** stops at "head back south to actually take Route 45". `MAP_ROUTE_45`
proper, `MAP_ROUTE_46`, `MAP_BLACKTHORN_GYM` and everything past them belong to
the next section; only the Route 45 warp that reaches Dark Cave is documented
here.
`MAP_DARK_CAVE_VIOLET_ENTRANCE` is also reachable from `ROUTE_46` warp 3 at
(14,5) (-> Dark Cave warp 3 at (35,33)) and from `ROUTE_31` warp 3; the earlier
sections that visit Route 31 already touch the north end of this map. This
section only walks it top-to-bottom-left, from the Blackthorn side out to
Route 31.
---
## 2. Maps
### MAP_MOUNT_MORTAR_1F_OUTSIDE
- Script: `maps/MountMortar1FOutside.asm`
- Blocks: `maps/MountMortar1FOutside.blk`
- Header: `data/maps/maps.asm:127` -> `TILESET_DARK_CAVE`, `CAVE`,
`LANDMARK_MT_MORTAR`, `MUSIC_UNION_CAVE`, phone `TRUE`, `PALETTE_DARK`,
`FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:114` `map_const MOUNT_MORTAR_1F_OUTSIDE, 20, 18`
(x 0..39, y 0..35), map id 49, group 3
- Attributes: `data/maps/attributes.asm:446`, border block `$09`, **no connections**
- Map events block: `MountMortar1FOutside_MapEvents` = `46:5db9`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 33 | `ROUTE_42` | 3 |
| 2 | 17 | 33 | `ROUTE_42` | 4 |
| 3 | 37 | 33 | `ROUTE_42` | 5 |
| 4 | 17 | 5 | `MOUNT_MORTAR_2F_INSIDE` | 1 |
| 5 | 11 | 21 | `MOUNT_MORTAR_1F_INSIDE` | 1 |
| 6 | 29 | 21 | `MOUNT_MORTAR_1F_INSIDE` | 2 |
| 7 | 17 | 29 | `MOUNT_MORTAR_B1F` | 2 |
| 8 | 7 | 13 | `MOUNT_MORTAR_1F_INSIDE` | 3 |
| 9 | 33 | 13 | `MOUNT_MORTAR_1F_INSIDE` | 4 |
**Coord events** (`def_coord_events`)
`def_coord_events` is **empty**. No scripted trip-wires anywhere in Mt. Mortar.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 25 | 22 | `BGEVENT_ITEM` | `MountMortar1FOutsideHiddenHyperPotion` -> `hiddenitem HYPER_POTION, EVENT_MOUNT_MORTAR_1F_OUTSIDE_HIDDEN_HYPER_POTION` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `MOUNTMORTAR1FOUTSIDE_POKE_BALL1` | `SPRITE_POKE_BALL` | 13 | 15 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `MountMortar1FOutsideEther` (`itemball ETHER`) | `EVENT_MOUNT_MORTAR_1F_OUTSIDE_ETHER` |
| `MOUNTMORTAR1FOUTSIDE_POKE_BALL2` | `SPRITE_POKE_BALL` | 31 | 18 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `MountMortar1FOutsideGuardSpec` (`itemball GUARD_SPEC`) | `EVENT_MOUNT_MORTAR_1F_OUTSIDE_REVIVE` |
Note the flag/item mismatch on the second ball: the item is `GUARD_SPEC` but the
flag is named `..._REVIVE`. That is verbatim from the disassembly - the label
name is cosmetic, the flag bit is what matters.
**Scripts of interest**
- `MountMortar1FOutsideEther` / `MountMortar1FOutsideGuardSpec` - bare
`itemball ITEM` rows (two bytes, no bytecode). Picked up by the shared
`FindItemInBallScript` path, not by a per-map script.
- `MountMortar1FOutsideHiddenHyperPotion` - `hiddenitem` data behind a
`BGEVENT_ITEM` row; A-pressed on (25,22) or found by the ITEMFINDER.
**Wild encounters**
`data/wild/johto_grass.asm:686` `def_grass_wildmons MOUNT_MORTAR_1F_OUTSIDE`,
encounter rate `6 percent` for all three of morn/day/nite, and **all three
slot lists are identical**:
| slot | lvl | species |
|---|---|---|
| 1 | 13 | ZUBAT |
| 2 | 15 | ZUBAT |
| 3 | 14 | MACHOP |
| 4 | 14 | RATTATA |
| 5 | 14 | GEODUDE |
| 6 | 16 | RATTATA |
| 7 | 15 | MARILL |
`data/wild/johto_water.asm:58` `def_water_wildmons MOUNT_MORTAR_1F_OUTSIDE`,
rate `4 percent`: 20 GOLDEEN / 15 GOLDEEN / 20 SEAKING.
Fishing: `FISHGROUP_LAKE`, `data/wild/fish.asm:57` - Old rod MAGIKARP 10 /
GOLDEEN 10; Good rod MAGIKARP 20 / GOLDEEN 20 / `time_group 4`; Super rod
GOLDEEN 40 / `time_group 5` / MAGIKARP 40 / SEAKING 40.
No headbutt or rock-smash table (`data/wild/treemon_maps.asm` does not list this
map).
---
### MAP_MOUNT_MORTAR_1F_INSIDE
- Script: `maps/MountMortar1FInside.asm`
- Blocks: `maps/MountMortar1FInside.blk`
- Header: `data/maps/maps.asm:128` -> `TILESET_DARK_CAVE`, `CAVE`,
`LANDMARK_MT_MORTAR`, `MUSIC_UNION_CAVE`, phone `TRUE`, `PALETTE_DARK`,
`FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:115` `map_const MOUNT_MORTAR_1F_INSIDE, 20, 27`
(x 0..39, y 0..53), map id 50, group 3
- Attributes: `data/maps/attributes.asm:447`, border block `$09`, **no connections**
- Map events block: `MountMortar1FInside_MapEvents` = `46:5e19`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 11 | 47 | `MOUNT_MORTAR_1F_OUTSIDE` | 5 |
| 2 | 29 | 47 | `MOUNT_MORTAR_1F_OUTSIDE` | 6 |
| 3 | 5 | 39 | `MOUNT_MORTAR_1F_OUTSIDE` | 8 |
| 4 | 33 | 41 | `MOUNT_MORTAR_1F_OUTSIDE` | 9 |
| 5 | 3 | 19 | `MOUNT_MORTAR_B1F` | 1 |
| 6 | 9 | 9 | `MOUNT_MORTAR_2F_INSIDE` | 2 |
**Coord events** - empty.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 31 | 9 | `BGEVENT_ITEM` | `MountMortar1FInsideHiddenMaxRepel` -> `hiddenitem MAX_REPEL, EVENT_MOUNT_MORTAR_1F_INSIDE_HIDDEN_MAX_REPEL` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `MOUNTMORTAR1FINSIDE_BOULDER` | `SPRITE_BOULDER` | 21 | 43 | `SPRITEMOVEDATA_STRENGTH_BOULDER` | `OBJECTTYPE_SCRIPT` | `MountMortar1FBoulder` (`jumpstd StrengthBoulderScript`) | `-1` |
| `MOUNTMORTAR1FINSIDE_POKE_BALL1` | `SPRITE_POKE_BALL` | 33 | 22 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `MountMortar1FInsideEscapeRope` (`itemball ESCAPE_ROPE`) | `EVENT_MOUNT_MORTAR_1F_INSIDE_ESCAPE_ROPE` |
| `MOUNTMORTAR1FINSIDE_POKE_BALL2` | `SPRITE_POKE_BALL` | 16 | 10 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `MountMortar1FInsideMaxRevive` (`itemball MAX_REVIVE`) | `EVENT_MOUNT_MORTAR_1F_INSIDE_MAX_REVIVE` |
| `MOUNTMORTAR1FINSIDE_POKE_BALL3` | `SPRITE_POKE_BALL` | 12 | 21 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `MountMortar1FInsideHyperPotion` (`itemball HYPER_POTION`) | `EVENT_MOUNT_MORTAR_1F_INSIDE_HYPER_POTION` |
**Scripts of interest**
- `MountMortar1FBoulder` - one line: `jumpstd StrengthBoulderScript`.
`engine/events/std_scripts.asm:196` `StrengthBoulderScript: farsjump AskStrengthScript`
(`03:4d4e`). See section 3 for the gate.
**Wild encounters**
`data/wild/johto_grass.asm:714` `def_grass_wildmons MOUNT_MORTAR_1F_INSIDE`,
rate `6 percent` morn/day/nite, all three lists identical:
| slot | lvl | species |
|---|---|---|
| 1 | 13 | GEODUDE |
| 2 | 13 | MACHOP |
| 3 | 15 | GEODUDE |
| 4 | 14 | RATTATA |
| 5 | 15 | MACHOP |
| 6 | 14 | ZUBAT |
| 7 | 14 | ZUBAT |
**There is no `def_water_wildmons MOUNT_MORTAR_1F_INSIDE`** in
`data/wild/johto_water.asm` - only 1F Outside, 2F Inside and B1F have water
tables. Fishing group is still `FISHGROUP_LAKE`.
---
### MAP_MOUNT_MORTAR_2F_INSIDE
- Script: `maps/MountMortar2FInside.asm`
- Blocks: `maps/MountMortar2FInside.blk`
- Header: `data/maps/maps.asm:129` -> `TILESET_DARK_CAVE`, `CAVE`,
`LANDMARK_MT_MORTAR`, `MUSIC_UNION_CAVE`, phone `TRUE`, `PALETTE_DARK`,
`FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:116` `map_const MOUNT_MORTAR_2F_INSIDE, 20, 18`
(x 0..39, y 0..35), map id 51, group 3
- Attributes: `data/maps/attributes.asm:448`, border block `$09`, **no connections**
- Map events block: `MountMortar2FInside_MapEvents` = `46:5e87`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 17 | 33 | `MOUNT_MORTAR_1F_OUTSIDE` | 4 |
| 2 | 3 | 5 | `MOUNT_MORTAR_1F_INSIDE` | 6 |
**Coord events** - empty.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 19 | 6 | `BGEVENT_ITEM` | `MountMortar2FInsideHiddenFullRestore` -> `hiddenitem FULL_RESTORE, EVENT_MOUNT_MORTAR_2F_INSIDE_HIDDEN_FULL_RESTORE` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `MOUNTMORTAR2FINSIDE_POKE_BALL1` | `SPRITE_POKE_BALL` | 31 | 23 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `MountMortar2FInsideMaxPotion` (`itemball MAX_POTION`) | `EVENT_MOUNT_MORTAR_2F_INSIDE_MAX_POTION` |
| `MOUNTMORTAR2FINSIDE_POKE_BALL2` | `SPRITE_POKE_BALL` | 2 | 24 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `MountMortar2FInsideRareCandy` (`itemball RARE_CANDY`) | `EVENT_MOUNT_MORTAR_2F_INSIDE_RARE_CANDY` |
| `MOUNTMORTAR2FINSIDE_POKE_BALL3` | `SPRITE_POKE_BALL` | 19 | 17 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `MountMortar2FInsideTMDefenseCurl` (`itemball TM_DEFENSE_CURL`) | `EVENT_MOUNT_MORTAR_2F_INSIDE_TM_DEFENSE_CURL` |
| `MOUNTMORTAR2FINSIDE_POKE_BALL4` | `SPRITE_POKE_BALL` | 14 | 5 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `MountMortar2FInsideDragonScale` (`itemball DRAGON_SCALE`) | `EVENT_MOUNT_MORTAR_2F_INSIDE_DRAGON_SCALE` |
| `MOUNTMORTAR2FINSIDE_POKE_BALL5` | `SPRITE_POKE_BALL` | 8 | 9 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `MountMortar2FInsideElixer` (`itemball ELIXER`) | `EVENT_MOUNT_MORTAR_2F_INSIDE_ELIXER` |
| `MOUNTMORTAR2FINSIDE_POKE_BALL6` | `SPRITE_POKE_BALL` | 28 | 5 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `MountMortar2FInsideEscapeRope` (`itemball ESCAPE_ROPE`) | `EVENT_MOUNT_MORTAR_2F_INSIDE_ESCAPE_ROPE` |
`TM_DEFENSE_CURL` is **TM40** by the `add_tm` ordering in
`constants/item_constants.asm:261`. The walkthrough calls it "TM40 Aerial Ace",
which is the HGSS remake's TM40 - see section 6.
**Wild encounters**
`data/wild/johto_grass.asm:742` `def_grass_wildmons MOUNT_MORTAR_2F_INSIDE`,
rate `6 percent` morn/day/nite, all three lists identical - this is the floor
the walkthrough's "Raticate / Machoke / Graveler" list is drawn from:
| slot | lvl | species |
|---|---|---|
| 1 | 31 | GRAVELER |
| 2 | 32 | MACHOKE |
| 3 | 31 | GEODUDE |
| 4 | 30 | RATICATE |
| 5 | 28 | MACHOP |
| 6 | 30 | GOLBAT |
| 7 | 30 | GOLBAT |
`data/wild/johto_water.asm:65` `def_water_wildmons MOUNT_MORTAR_2F_INSIDE`,
rate `2 percent`: 20 GOLDEEN / 25 GOLDEEN / 25 SEAKING.
---
### MAP_MOUNT_MORTAR_B1F
- Script: `maps/MountMortarB1F.asm`
- Blocks: `maps/MountMortarB1F.blk`
- Header: `data/maps/maps.asm:130` -> `TILESET_DARK_CAVE`, `CAVE`,
`LANDMARK_MT_MORTAR`, `MUSIC_UNION_CAVE`, phone `TRUE`, `PALETTE_DARK`,
`FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:117` `map_const MOUNT_MORTAR_B1F, 20, 18`
(x 0..39, y 0..35), map id 52, group 3
- Attributes: `data/maps/attributes.asm:449`, border block `$09`, **no connections**
- Map events block: `MountMortarB1F_MapEvents` = `46:60f0`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 3 | `MOUNT_MORTAR_1F_INSIDE` | 5 |
| 2 | 19 | 29 | `MOUNT_MORTAR_1F_OUTSIDE` | 7 |
**Coord events** - empty. Kiyo is a talk-to trigger, not a trip-wire.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 4 | 6 | `BGEVENT_ITEM` | `MountMortarB1FHiddenMaxRevive` -> `hiddenitem MAX_REVIVE, EVENT_MOUNT_MORTAR_B1F_HIDDEN_MAX_REVIVE` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `MOUNTMORTARB1F_POKE_BALL1` | `SPRITE_POKE_BALL` | 31 | 17 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `MountMortarB1FHyperPotion` (`itemball HYPER_POTION`) | `EVENT_MOUNT_MORTAR_B1F_HYPER_POTION` |
| `MOUNTMORTARB1F_POKE_BALL2` | `SPRITE_POKE_BALL` | 4 | 16 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `MountMortarB1FFullHeal` (`itemball FULL_HEAL`) | `EVENT_MOUNT_MORTAR_B1F_FULL_HEAL` |
| `MOUNTMORTARB1F_BOULDER` | `SPRITE_BOULDER` | 9 | 10 | `SPRITEMOVEDATA_STRENGTH_BOULDER` | `OBJECTTYPE_SCRIPT` | `MountMortarB1FBoulder` (`jumpstd StrengthBoulderScript`) | `-1` |
| `MOUNTMORTARB1F_KIYO` | `SPRITE_BLACK_BELT` | 13 | 4 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT`, palette `PAL_NPC_BROWN` | `MountMortarB1FKiyoScript` | `-1` |
**Scripts of interest**
`MountMortarB1FKiyoScript` (`46:5eec`) - the whole point of this section. Note
it is `OBJECTTYPE_SCRIPT`, **not** `OBJECTTYPE_TRAINER`: Kiyo has no `trainer`
header, no sight range, and never walks up to you. He must be talked to.
```
faceplayer
opentext
checkevent EVENT_GOT_TYROGUE_FROM_KIYO ; iftrue -> .GotTyrogue (46:5f27)
checkevent EVENT_BEAT_BLACKBELT_KIYO ; iftrue -> .BeatKiyo (46:5f0d)
writetext MountMortarB1FKiyoIntroText
waitbutton
closetext
winlosstext MountMortarB1FKiyoWinText, 0
loadtrainer BLACKBELT_T, KIYO
startbattle
reloadmapafterbattle
setevent EVENT_BEAT_BLACKBELT_KIYO
opentext
.BeatKiyo:
writetext MountMortarB1FTyrogueRewardText
promptbutton
waitsfx
readvar VAR_PARTYCOUNT
ifequal PARTY_LENGTH, .NoRoom ; 46:5f2d
writetext MountMortarB1FReceiveMonText
playsound SFX_CAUGHT_MON
waitsfx
givepoke TYROGUE, 10
setevent EVENT_GOT_TYROGUE_FROM_KIYO
.GotTyrogue:
writetext MountMortarB1FKiyoGotTyrogueText
waitbutton
closetext
end
```
Bot-relevant control flow:
- The battle is unconditional on first talk (no `checkevent` guard before
`loadtrainer`), and the win-flag write is **after** `startbattle`, so a
blackout means the whole script re-runs from the top.
- `readvar VAR_PARTYCOUNT` / `ifequal PARTY_LENGTH` is the "you have no room"
branch. `PARTY_LENGTH` is 6. **If the party is full, `EVENT_BEAT_BLACKBELT_KIYO`
is already set but `EVENT_GOT_TYROGUE_FROM_KIYO` is not**, so re-talking
re-enters at `.BeatKiyo` and offers the Tyrogue again. This is why the
walkthrough tells you to free a slot in advance - but it is recoverable, not
missable.
- `givepoke TYROGUE, 10` - level 10, no held item, no nickname prompt in this
form of the opcode.
`MountMortarB1FBoulder` - `jumpstd StrengthBoulderScript`, same as 1F Inside.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_BLACKBELT_KIYO` | `constants/event_flags.asm:684` | set by `MountMortarB1FKiyoScript` after `startbattle` | Kiyo will not fight again |
| `EVENT_GOT_TYROGUE_FROM_KIYO` | `constants/event_flags.asm:106` | set by `MountMortarB1FKiyoScript` after `givepoke` | the Tyrogue is banked; the script becomes pure flavour |
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `KIYO` (`constants/trainer_constants.asm:500`) | `BLACKBELT_T` | 6th entry of `BlackbeltGroup` | `data/trainers/parties.asm:2455` `; BLACKBELT_T (6)` `db "KIYO@", TRAINERTYPE_NORMAL` / `db 34, HITMONLEE` / `db 34, HITMONCHAN` | `MountMortarB1FKiyoScript` | none - no `trainer` header, so no phone number, no rematch |
`TRAINERTYPE_NORMAL` means **no custom moves and no held items**: both mons use
their level-34 learnset defaults. The walkthrough's "816G" prize and the
1012/1020 EXP figures were not verified against `data/trainers/attributes.asm` -
see section 6.
**Wild encounters**
`data/wild/johto_grass.asm:770` `def_grass_wildmons MOUNT_MORTAR_B1F`, rate
`6 percent` morn/day/nite, all three lists identical:
| slot | lvl | species |
|---|---|---|
| 1 | 15 | ZUBAT |
| 2 | 17 | ZUBAT |
| 3 | 16 | RATTATA |
| 4 | 16 | MACHOP |
| 5 | 16 | GEODUDE |
| 6 | 16 | RATICATE |
| 7 | 16 | RATICATE |
`data/wild/johto_water.asm:72` `def_water_wildmons MOUNT_MORTAR_B1F`, rate
`2 percent`: 20 GOLDEEN / 15 GOLDEEN / 20 SEAKING.
---
### MAP_DARK_CAVE_BLACKTHORN_ENTRANCE
- Script: `maps/DarkCaveBlackthornEntrance.asm`
- Blocks: `maps/DarkCaveBlackthornEntrance.blk`
- Header: `data/maps/maps.asm:149` -> `TILESET_DARK_CAVE`, `CAVE`,
`LANDMARK_DARK_CAVE`, `MUSIC_DARK_CAVE`, phone `TRUE`, `PALETTE_DARK`,
`FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:136` `map_const DARK_CAVE_BLACKTHORN_ENTRANCE, 15, 18`
(x 0..29, y 0..35), map id 71, group 3
- Attributes: `data/maps/attributes.asm:468`, border block `$09`, **no connections**
- Map events block: `DarkCaveBlackthornEntrance_MapEvents` = `47:4489`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 23 | 3 | `ROUTE_45` | 1 |
| 2 | 3 | 25 | `DARK_CAVE_VIOLET_ENTRANCE` | 2 |
**Coord events** - empty.
**BG events** - `def_bg_events` is **empty**. No signs, no hidden items.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `DARKCAVEBLACKTHORNENTRANCE_PHARMACIST` | `SPRITE_PHARMACIST` | 7 | 3 | `SPRITEMOVEDATA_SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT` | `DarkCaveBlackthornEntrancePharmacistScript` | `-1` |
| `DARKCAVEBLACKTHORNENTRANCE_POKE_BALL1` | `SPRITE_POKE_BALL` | 21 | 24 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `DarkCaveBlackthornEntranceRevive` (`itemball REVIVE`) | `EVENT_DARK_CAVE_BLACKTHORN_ENTRANCE_REVIVE` |
| `DARKCAVEBLACKTHORNENTRANCE_POKE_BALL2` | `SPRITE_POKE_BALL` | 7 | 22 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `DarkCaveBlackthornEntranceTMSnore` (`itemball TM_SNORE`) | `EVENT_DARK_CAVE_BLACKTHORN_ENTRANCE_TM_SNORE` |
`TM_SNORE` is **TM13** by the `add_tm` ordering in
`constants/item_constants.asm:233`, which matches the walkthrough's "TM 13".
**Scripts of interest**
`DarkCaveBlackthornEntrancePharmacistScript` (`47:436c`):
```
faceplayer
opentext
checkevent EVENT_GOT_BLACKGLASSES_IN_DARK_CAVE ; iftrue -> .GotBlackglasses (47:4381)
writetext DarkCaveBlackthornEntrancePharmacistText1
promptbutton
verbosegiveitem BLACKGLASSES
iffalse .PackFull ; 47:4385
setevent EVENT_GOT_BLACKGLASSES_IN_DARK_CAVE
.GotBlackglasses:
writetext DarkCaveBlackthornEntrancePharmacistText2
waitbutton
.PackFull:
closetext
end
```
Bot-relevant: `verbosegiveitem` returns 0 when the pack is full, and the
`setevent` is **after** the give, so a full item pocket leaves the flag clear
and the NPC re-offers. Not missable. The NPC spins
(`SPRITEMOVEDATA_SPINRANDOM_SLOW`), so the bot must stand adjacent and press A;
`faceplayer` handles the turn.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_GOT_BLACKGLASSES_IN_DARK_CAVE` | `constants/event_flags.asm:123` | `DarkCaveBlackthornEntrancePharmacistScript` | BLACKGLASSES banked; NPC becomes flavour text |
| `EVENT_DARK_CAVE_BLACKTHORN_ENTRANCE_REVIVE` | `constants/event_flags.asm:1090` | object visibility mask | set = ball already taken |
| `EVENT_DARK_CAVE_BLACKTHORN_ENTRANCE_TM_SNORE` | `constants/event_flags.asm:1091` | object visibility mask | set = TM13 already taken |
**Wild encounters**
`data/wild/johto_grass.asm:1545` `def_grass_wildmons DARK_CAVE_BLACKTHORN_ENTRANCE`,
rate `4 percent` morn/day/nite, all three lists identical - this is the
walkthrough's Wobbuffet spot:
| slot | lvl | species |
|---|---|---|
| 1 | 23 | GEODUDE |
| 2 | 23 | ZUBAT |
| 3 | 25 | GRAVELER |
| 4 | 20 | WOBBUFFET |
| 5 | 25 | WOBBUFFET |
| 6 | 23 | GOLBAT |
| 7 | 23 | GOLBAT |
`data/wild/johto_water.asm:114` `def_water_wildmons DARK_CAVE_BLACKTHORN_ENTRANCE`,
rate `2 percent`: 15 MAGIKARP / 10 MAGIKARP / 5 MAGIKARP.
No `treemon_map` row for this map - **rock smash here yields nothing**, and
there are no smashable rock objects on it either.
---
### MAP_DARK_CAVE_VIOLET_ENTRANCE
- Script: `maps/DarkCaveVioletEntrance.asm`
- Blocks: `maps/DarkCaveVioletEntrance.blk`
- Header: `data/maps/maps.asm:148` -> `TILESET_DARK_CAVE`, `CAVE`,
`LANDMARK_DARK_CAVE`, `MUSIC_DARK_CAVE`, phone `TRUE`, `PALETTE_DARK`,
`FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:135` `map_const DARK_CAVE_VIOLET_ENTRANCE, 20, 18`
(x 0..39, y 0..35), map id 70, group 3
- Attributes: `data/maps/attributes.asm:467`, border block `$09`, **no connections**
- Map events block: `DarkCaveVioletEntrance_MapEvents` = `47:42f5`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 15 | `ROUTE_31` | 3 |
| 2 | 17 | 1 | `DARK_CAVE_BLACKTHORN_ENTRANCE` | 2 |
| 3 | 35 | 33 | `ROUTE_46` | 3 |
**Coord events** - empty.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 26 | 3 | `BGEVENT_ITEM` | `DarkCaveVioletEntranceHiddenElixer` -> `hiddenitem ELIXER, EVENT_DARK_CAVE_VIOLET_ENTRANCE_HIDDEN_ELIXER` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `DARKCAVEVIOLETENTRANCE_POKE_BALL1` | `SPRITE_POKE_BALL` | 6 | 8 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `DarkCaveVioletEntrancePotion` (`itemball POTION`) | `EVENT_DARK_CAVE_VIOLET_ENTRANCE_POTION` |
| `DARKCAVEVIOLETENTRANCE_ROCK1` | `SPRITE_ROCK` | 16 | 14 | `SPRITEMOVEDATA_SMASHABLE_ROCK` | `OBJECTTYPE_SCRIPT` | `DarkCaveVioletEntranceRock` (`jumpstd SmashRockScript`) | `-1` |
| `DARKCAVEVIOLETENTRANCE_ROCK2` | `SPRITE_ROCK` | 27 | 6 | `SPRITEMOVEDATA_SMASHABLE_ROCK` | `OBJECTTYPE_SCRIPT` | `DarkCaveVioletEntranceRock` | `-1` |
| `DARKCAVEVIOLETENTRANCE_ROCK3` | `SPRITE_ROCK` | 7 | 14 | `SPRITEMOVEDATA_SMASHABLE_ROCK` | `OBJECTTYPE_SCRIPT` | `DarkCaveVioletEntranceRock` | `-1` |
| `DARKCAVEVIOLETENTRANCE_ROCK4` | `SPRITE_ROCK` | 36 | 31 | `SPRITEMOVEDATA_SMASHABLE_ROCK` | `OBJECTTYPE_SCRIPT` | `DarkCaveVioletEntranceRock` | `-1` |
| `DARKCAVEVIOLETENTRANCE_POKE_BALL2` | `SPRITE_POKE_BALL` | 36 | 22 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `DarkCaveVioletEntranceFullHeal` (`itemball FULL_HEAL`) | `EVENT_DARK_CAVE_VIOLET_ENTRANCE_FULL_HEAL` |
| `DARKCAVEVIOLETENTRANCE_POKE_BALL3` | `SPRITE_POKE_BALL` | 35 | 9 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `DarkCaveVioletEntranceHyperPotion` (`itemball HYPER_POTION`) | `EVENT_DARK_CAVE_VIOLET_ENTRANCE_HYPER_POTION` |
Note the four rocks all share one script label; the smash `disappear LAST_TALKED`
in `RockSmashScript` acts on `hLastTalked`, so they are independent objects
despite the shared pointer. Rocks have **no event flag** (`-1`) - they come back
on every map reload.
**Scripts of interest**
`DarkCaveVioletEntranceRock` - `jumpstd SmashRockScript`
(`engine/events/std_scripts.asm:199` -> `farsjump AskRockSmashScript`, `03:4f60`).
`AskRockSmashScript` calls `HasRockSmash` (`03:4f7f`), which is **inverted**
(`wScriptVar = 1` when the party does NOT know ROCK SMASH); on 0 it prompts
yes/no and falls into `RockSmashScript` (`03:4f35`):
`playsound SFX_STRENGTH` / `earthquake 84` / `applymovementlasttalked MovementData_RockSmash`
/ `disappear LAST_TALKED` / `callasm RockMonEncounter` / `randomwildmon` /
`startbattle`.
**Wild encounters**
`data/wild/johto_grass.asm:1517` `def_grass_wildmons DARK_CAVE_VIOLET_ENTRANCE`,
rate `4 percent` morn/day/nite, all three lists identical:
| slot | lvl | species |
|---|---|---|
| 1 | 3 | GEODUDE |
| 2 | 2 | ZUBAT |
| 3 | 2 | GEODUDE |
| 4 | 4 | GEODUDE |
| 5 | 3 | ZUBAT |
| 6 | 4 | ZUBAT |
| 7 | 4 | DUNSPARCE |
`data/wild/johto_water.asm:107` `def_water_wildmons DARK_CAVE_VIOLET_ENTRANCE`,
rate `2 percent`: 15 MAGIKARP / 10 MAGIKARP / 5 MAGIKARP.
**Rock smash encounters** - this map IS in `data/wild/treemon_maps.asm:46`
`RockMonMaps`: `treemon_map DARK_CAVE_VIOLET_ENTRANCE, TREEMON_SET_ROCK`.
`RockMonEncounter` (`2e:63a1`) rolls `RandomRange 10 < 4` (a 40% chance) and then
`SelectTreeMon` over `data/wild/treemons.asm:91` `TreeMonSet_Rock`:
90% KRABBY lvl 15, 10% SHUCKLE lvl 15. **This is the only Shuckle source in
Johto reachable in this section**, and the walkthrough does not mention it.
---
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Darkness on all six maps | `engine/tilesets/timeofday_pals.asm` `ReplaceTimeOfDayPals` (`23:43e9`) `.NeedsFlash` -> `DARKNESS_PALSET`, because `data/maps/maps.asm` lines 127-130 / 148-149 all say `PALETTE_DARK` | FLASH cast, i.e. `STATUSFLAGS_FLASH_F` set in `wStatusFlags` | `engine/events/overworld.asm` `FlashFunction.CheckUseFlash` (`03:48f1`): `ENGINE_ZEPHYRBADGE` **and** `wTimeOfDayPalset == DARKNESS_PALSET`. The bit is cleared by `home/flag.asm` `ResetFlashIfOutOfCave` (`00:2f1d`) on any `ROUTE`/`TOWN` map, so **re-cast after every exit and every Fly** |
| Mt. Mortar middle entrance (`ROUTE_42` warp 4 at (28,9)) | terrain: the tile is across water on Route 42 | SURF | `engine/events/overworld.asm` `SurfFunction` (`03:493b`), `.TrySurf` arm / `TrySurfOW` (`03:4a06`): `ENGINE_FOGBADGE` + a party member knowing SURF |
| The lake and the waterfall between 1F Outside and 2F | terrain on `MountMortar1FOutside.blk`; the only route to warp 4 at (17,5) | SURF, then WATERFALL | `WaterfallFunction.TryWaterfall` (`03:4af6`) / `TryWaterfallOW` (`03:4b5f`): `ENGINE_RISINGBADGE`, then `CheckMapCanWaterfall` - **player must be facing UP and the tile ABOVE (`wTileUp`) must be a waterfall tile** |
| `MOUNTMORTAR1FINSIDE_BOULDER` at (21,43) | `MountMortar1FBoulder` -> `jumpstd StrengthBoulderScript` -> `AskStrengthScript` (`03:4d4e`) | STRENGTH toggled on | `TryStrengthOW` (`03:4d7b`): party knows STRENGTH **and** `ENGINE_PLAINBADGE` **and** `BIKEFLAGS_STRENGTH_ACTIVE_F` set. `StrengthFunction.TryStrength` (`03:4cf1`) only checks the badge. Note `ResetBikeFlags` clears the active bit on **every map load** - Strength must be re-activated per floor |
| `MOUNTMORTARB1F_BOULDER` at (9,10) | `MountMortarB1FBoulder`, same std script | STRENGTH | as above. This is the walkthrough's "move the rock with HM Strength to get out"; the alternative is an Escape Rope |
| Kiyo's Tyrogue | `MountMortarB1FKiyoScript` `readvar VAR_PARTYCOUNT` / `ifequal PARTY_LENGTH, .NoRoom` | 5 or fewer party members | free a slot and re-talk; `EVENT_BEAT_BLACKBELT_KIYO` stays set so the battle is not repeated |
| Dark Cave water crossings (both entrances) | terrain | SURF | `ENGINE_FOGBADGE` as above |
| `DARKCAVEVIOLETENTRANCE_ROCK1..4` | `DarkCaveVioletEntranceRock` -> `jumpstd SmashRockScript` -> `AskRockSmashScript` (`03:4f60`) | a party member knowing ROCK SMASH (TM08) | `HasRockSmash` (`03:4f7f`) - **no badge check at all** for Rock Smash |
| Fly between Mahogany and Blackthorn | `engine/events/overworld.asm:545` (the `.TryFly` arm of the Fly jumptable) `ld de, ENGINE_STORMBADGE` | `ENGINE_STORMBADGE` + the destination's `ENGINE_FLYPOINT_*` (`constants/engine_flags.asm:87`, `:89`) | both flypoints are set by visiting the towns in earlier sections |
Badge constants used above: `ENGINE_ZEPHYRBADGE` (`constants/engine_flags.asm:38`),
`ENGINE_PLAINBADGE` (`:40`), `ENGINE_FOGBADGE` (`:41`), `ENGINE_STORMBADGE`
(`:43`), `ENGINE_RISINGBADGE` (`:45`). All are checked through
`engine/events/overworld.asm` `CheckBadge` (line 50), which prints
"Badge required" and returns carry.
**Nothing in this section gates anything else.** Both Mt. Mortar and Dark Cave
are entirely optional; a bot that skips them loses only items, the Tyrogue and
the Blackglasses.
---
## 4. Bot checklist
Preconditions for the whole section: `ENGINE_ZEPHYRBADGE`, `ENGINE_FOGBADGE`,
`ENGINE_PLAINBADGE`, `ENGINE_RISINGBADGE`, `ENGINE_STORMBADGE`; party members
knowing FLASH, SURF, STRENGTH, WATERFALL (and ROCK SMASH for step 26); party
count <= 5 before step 20.
| # | Map | Target | Intent | Precondition | Postcondition |
|---|---|---|---|---|---|
| 1 | `MAHOGANY_TOWN` | spawn (15,14) | Fly in, heal, buy MAX_REPEL / ESCAPE_ROPE | `ENGINE_FLYPOINT_MAHOGANY` | party healthy, bag stocked |
| 2 | `MAHOGANY_TOWN` | west map connection | walk west | - | on `ROUTE_42` |
| 3 | `ROUTE_42` | warp 3 at (10,5) | step onto warp (west entrance) | - | on `MOUNT_MORTAR_1F_OUTSIDE` at (3,33) |
| 4 | `MOUNT_MORTAR_1F_OUTSIDE` | - | use FLASH from the PACK/party menu | `ENGINE_ZEPHYRBADGE`, map palset is `DARKNESS_PALSET` | `STATUSFLAGS_FLASH_F` set |
| 5 | `MOUNT_MORTAR_1F_OUTSIDE` | - | use MAX_REPEL | bag has one | encounters suppressed |
| 6 | `MOUNT_MORTAR_1F_OUTSIDE` | object at (13,15) | walk onto / A-press the Poke Ball | `EVENT_MOUNT_MORTAR_1F_OUTSIDE_ETHER` clear | ETHER in bag, flag set |
| 7 | `MOUNT_MORTAR_1F_OUTSIDE` | object at (31,18) | pick up Poke Ball | `EVENT_MOUNT_MORTAR_1F_OUTSIDE_REVIVE` clear | GUARD_SPEC in bag, flag set |
| 8 | `MOUNT_MORTAR_1F_OUTSIDE` | bg event (25,22) | face the tile, press A | `EVENT_MOUNT_MORTAR_1F_OUTSIDE_HIDDEN_HYPER_POTION` clear | HYPER_POTION in bag, flag set |
| 9 | `MOUNT_MORTAR_1F_OUTSIDE` | warp 5 at (11,21) | step on warp | - | on `MOUNT_MORTAR_1F_INSIDE` at (11,47) |
| 10 | `MOUNT_MORTAR_1F_INSIDE` | - | re-cast FLASH only if it was lost (it is not - cave to cave keeps the bit) | - | - |
| 11 | `MOUNT_MORTAR_1F_INSIDE` | object at (33,22) | pick up Poke Ball | `EVENT_MOUNT_MORTAR_1F_INSIDE_ESCAPE_ROPE` clear | ESCAPE_ROPE in bag |
| 12 | `MOUNT_MORTAR_1F_INSIDE` | object at (12,21) | pick up Poke Ball | `EVENT_MOUNT_MORTAR_1F_INSIDE_HYPER_POTION` clear | HYPER_POTION in bag |
| 13 | `MOUNT_MORTAR_1F_INSIDE` | boulder at (21,43) | activate STRENGTH from the party menu, then push | `ENGINE_PLAINBADGE`, party knows STRENGTH | `BIKEFLAGS_STRENGTH_ACTIVE_F` set, boulder movable |
| 14 | `MOUNT_MORTAR_1F_INSIDE` | warp 1 at (11,47) | back out | - | on `MOUNT_MORTAR_1F_OUTSIDE` at (11,21) |
| 15 | `MOUNT_MORTAR_1F_OUTSIDE` | the lake | walk into water / A-press facing water -> SURF | `ENGINE_FOGBADGE` | `wPlayerState = PLAYER_SURF` |
| 16 | `MOUNT_MORTAR_1F_OUTSIDE` | the waterfall tile | face **UP** at the waterfall, press A -> WATERFALL | `ENGINE_RISINGBADGE`, `CheckMapCanWaterfall` passes | climbed |
| 17 | `MOUNT_MORTAR_1F_OUTSIDE` | warp 4 at (17,5) | step on warp | - | on `MOUNT_MORTAR_2F_INSIDE` at (17,33) |
| 18 | `MOUNT_MORTAR_2F_INSIDE` | objects at (31,23), (2,24), (19,17), (14,5), (8,9), (28,5) | pick up all six Poke Balls | matching `EVENT_MOUNT_MORTAR_2F_INSIDE_*` clear | MAX_POTION, RARE_CANDY, TM_DEFENSE_CURL (TM40), DRAGON_SCALE, ELIXER, ESCAPE_ROPE |
| 19 | `MOUNT_MORTAR_2F_INSIDE` | bg event (19,6) | face, press A | `EVENT_MOUNT_MORTAR_2F_INSIDE_HIDDEN_FULL_RESTORE` clear | FULL_RESTORE in bag |
| 20 | `MOUNT_MORTAR_2F_INSIDE` | warp 2 at (3,5) | step on warp | - | on `MOUNT_MORTAR_1F_INSIDE` at (9,9) |
| 21 | `MOUNT_MORTAR_1F_INSIDE` | object at (16,10) | pick up Poke Ball | `EVENT_MOUNT_MORTAR_1F_INSIDE_MAX_REVIVE` clear | MAX_REVIVE in bag |
| 22 | `MOUNT_MORTAR_1F_INSIDE` | bg event (31,9) | face, press A | `EVENT_MOUNT_MORTAR_1F_INSIDE_HIDDEN_MAX_REPEL` clear | MAX_REPEL in bag |
| 23 | `MOUNT_MORTAR_1F_INSIDE` | warp 5 at (3,19) | step on warp | - | on `MOUNT_MORTAR_B1F` at (3,3) |
| 24 | `MOUNT_MORTAR_B1F` | bg (4,6), objects (4,16) and (31,17) | pick up hidden MAX_REVIVE, FULL_HEAL, HYPER_POTION | matching `EVENT_MOUNT_MORTAR_B1F_*` clear | three items |
| 25 | `MOUNT_MORTAR_B1F` | `MOUNTMORTARB1F_KIYO` at (13,4) | stand adjacent, press A | party count <= 5, `EVENT_GOT_TYROGUE_FROM_KIYO` clear | battle `BLACKBELT_T`/`KIYO` (L34 HITMONLEE, L34 HITMONCHAN) -> `EVENT_BEAT_BLACKBELT_KIYO` |
| 26 | `MOUNT_MORTAR_B1F` | Kiyo (same object, script continues) | press A through the reward text | party count <= 5 | `givepoke TYROGUE, 10`, `EVENT_GOT_TYROGUE_FROM_KIYO` set |
| 27 | `MOUNT_MORTAR_B1F` | - | use ESCAPE_ROPE (or STRENGTH the boulder at (9,10) then warp 2 at (19,29)) | ESCAPE_ROPE in bag | out of the cave |
| 28 | anywhere outdoors | `SPAWN_BLACKTHORN` | Fly to Blackthorn, heal | `ENGINE_STORMBADGE`, `ENGINE_FLYPOINT_BLACKTHORN` | at Blackthorn spawn (21,30); **`STATUSFLAGS_FLASH_F` is now cleared** |
| 29 | `BLACKTHORN_CITY` | south map connection | walk south | - | on `ROUTE_45` |
| 30 | `ROUTE_45` | warp 1 at (2,5) | step on warp | - | on `DARK_CAVE_BLACKTHORN_ENTRANCE` at (23,3) |
| 31 | `DARK_CAVE_BLACKTHORN_ENTRANCE` | - | use FLASH again | `ENGINE_ZEPHYRBADGE` | `STATUSFLAGS_FLASH_F` set |
| 32 | `DARK_CAVE_BLACKTHORN_ENTRANCE` | water in the middle of the map | SURF | `ENGINE_FOGBADGE` | crossing possible |
| 33 | `DARK_CAVE_BLACKTHORN_ENTRANCE` | `DARKCAVEBLACKTHORNENTRANCE_PHARMACIST` at (7,3) | stand adjacent, press A | `EVENT_GOT_BLACKGLASSES_IN_DARK_CAVE` clear, item pocket has room | BLACKGLASSES, flag set |
| 34 | `DARK_CAVE_BLACKTHORN_ENTRANCE` | objects at (7,22) and (21,24) | pick up both Poke Balls | matching events clear | TM_SNORE (TM13), REVIVE |
| 35 | `DARK_CAVE_BLACKTHORN_ENTRANCE` | warp 2 at (3,25) | step on warp | - | on `DARK_CAVE_VIOLET_ENTRANCE` at (17,1) |
| 36 | `DARK_CAVE_VIOLET_ENTRANCE` | water below (17,1) | SURF down | `ENGINE_FOGBADGE` | - |
| 37 | `DARK_CAVE_VIOLET_ENTRANCE` | rock at (16,14) or (7,14) | face rock, press A, answer yes | party knows ROCK_SMASH | rock gone; 40% chance of a KRABBY/SHUCKLE L15 battle |
| 38 | `DARK_CAVE_VIOLET_ENTRANCE` | object at (6,8) | pick up Poke Ball | `EVENT_DARK_CAVE_VIOLET_ENTRANCE_POTION` clear | POTION in bag |
| 39 | `DARK_CAVE_VIOLET_ENTRANCE` | warp 1 at (3,15) | step on warp | - | on `ROUTE_31` at (34,5); **FLASH bit cleared** |
| 40 | `ROUTE_31` | - | Fly back to Blackthorn, heal | - | section complete |
Optional extras this route skips, with their coordinates if a completionist bot
wants them: `DARK_CAVE_VIOLET_ENTRANCE` objects at (36,22) FULL_HEAL and (35,9)
HYPER_POTION plus the hidden ELIXER bg at (26,3) - all three sit on the
north-east lobe of the map that the earlier Route 31 section already reaches.
---
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Map headers / dimensions for all six maps | `tools/rom_manifest_gold.json` (e.g. `MOUNT_MORTAR_1F_OUTSIDE` 20x18 group 3 map 49, `DARK_CAVE_BLACKTHORN_ENTRANCE` 15x18 group 3 map 71), read by `src/import/RomExtractorGen2.lua` | implemented (extracted from ROM, no hand-ported map file needed) |
| Warp / coord / bg / object event tables | `src/import/RomExtractorGen2.lua` (`warps`, `coordEvents`, `bgEvents`, `objects` around lines 787-862) -> `src/world/gen2/Map.lua` | implemented |
| `PALETTE_DARK` -> `DARKNESS_PALSET` and the FLASH override | `src/world/gen2/Palettes.lua`, asserted by `tests/gen2_palettes_test.lua:118-129` (`dark cave` / `dark cave with flash`) | implemented |
| `ResetFlashIfOutOfCave` (FLASH dies on ROUTE/TOWN) | `src/world/gen2/World.lua:5622-5625` | implemented |
| FLASH / SURF / WATERFALL / STRENGTH badge gates | `src/world/gen2/FieldMoves.lua:105-110` (`FLASH=ZEPHYR`, `SURF=FOG`, `STRENGTH=PLAIN`, `WATERFALL=RISING`), `FieldMoves.flashFromMenu` / `surfFromMenu` / `waterfallFromMenu` / `strengthFromMenu` | implemented |
| `CheckMapCanWaterfall` (facing UP, `wTileUp`) and the climb loop | `src/world/gen2/FieldMoves.lua:253-266`, `src/world/gen2/World.lua:4030-4046` `runWaterfall` / `waterfallStep`, `World:tryWaterfallOW` (4217) | implemented |
| Strength boulder push (`StrengthBoulderScript` -> `AskStrengthScript`) | `src/world/gen2/World.lua:5268-5274` (the boulder arm of `World:interact`), `World:tryStrengthOW`, driver `tests/drivers/gold_icepath_boulder.lua` | implemented |
| `BIKEFLAGS_STRENGTH_ACTIVE_F` cleared on map load | `src/world/gen2/World.lua:5611-5613` | implemented |
| Hidden items (`BGEVENT_ITEM` / `hiddenitem`) | `src/world/gen2/HiddenItems.lua`, wired at `src/world/gen2/World.lua:5290-5296` | implemented |
| `verbosegiveitem BLACKGLASSES` (the Dark Cave pharmacist) | `src/script/gen2/Vm.lua:490-510`, hook `World.lua:831` `giveItem` | implemented |
| `givepoke TYROGUE, 10` (Kiyo's reward) | `src/script/gen2/Vm.lua:439-445`, hook `World.lua:812` `givePoke` | implemented |
| `readvar VAR_PARTYCOUNT` (Kiyo's "no room" branch) | `src/world/gen2/World.lua:112` `VAR_PARTYCOUNT = 0x01`, read at `World.lua:1225` | implemented |
| `loadtrainer` / `startbattle` / `winlosstext` / `reloadmapafterbattle` | `src/script/gen2/Vm.lua:806`, `:817`, `:886`, `:918`; `src/world/gen2/Trainers.lua`; driver `tests/drivers/gold_trainer_smoke.lua` | implemented |
| Tyrogue's three-way stat evolution (what the reward mon becomes) | `src/core/gen2/Evolution.lua:71`, asserted by `tests/gen2_evolution_test.lua:347-357` | implemented |
| Repel / Max Repel step counter | `src/world/gen2/World.lua:3382` `World:useRepel`, `src/ui/gen2/PackMenu.lua:199-206` | implemented |
| **Picking up a Poke Ball (`OBJECTTYPE_ITEMBALL`)** | extracted into `obj.itemball` by `src/import/RomExtractorGen2.lua:2968-2969`, but `World:interact` (`src/world/gen2/World.lua:5256-5310`) has arms for trainer, boulder, `scriptKey`, `BGEVENT_READ` and `BGEVENT_ITEM` **and no itemball arm**; `CallAsm.lua:551` stubs `TryReceiveItem` out | **missing** - all 15 Poke Balls in this section are unobtainable in the port today |
| **ROCK SMASH as a field move** | `src/script/gen2/CallAsm.lua:346-350` implements `HasRockSmash`, but line 526 stubs `RockMonEncounter` with the note "ROCK SMASH has no field-move path yet; RockMonMaps is unported"; `World:interact`'s tile-event chain (`World.lua:5301-5308`) lists cut / whirlpool / waterfall / headbutt / surf and **not** rock smash, and nothing in `src/` references `SPRITEMOVEDATA_SMASHABLE_ROCK` | **missing** - the four Dark Cave Violet rocks cannot be broken, and the Krabby/Shuckle table is unreachable |
| **ESCAPE_ROPE / DIG as a field item** | `src/world/gen2/World.lua:3293-3302` `World:useFieldItem` handles ITEMFINDER, BICYCLE, SACRED_ASH, repels, trophy boxes and rods only. (Gen 1's path exists at `src/ui/BagMenu.lua:231-240`, but that is the Red/Blue engine, not the Gen 2 one.) | **missing** - the walkthrough's "use one of the Escape Ropes to get out" has no port path; the Strength-boulder exit at B1F (9,10) does work |
| Fly to Mahogany / Blackthorn | `src/world/gen2/FieldMoves.lua:394-440` `hasVisitedSpawn` / `flyPoints`, `flyFromMenu` (504) | implemented |
| Wild encounter tables (grass + water + fishing) for these maps | extracted; `src/world/gen2/FieldMoves.lua:162-177` `canEncounterWildMon` / `encounterTable` | implemented (not separately verified against these six maps) |
No driver in `tests/drivers/gold_*.lua` visits Mt. Mortar or Dark Cave.
---
## 6. Unresolved / verify by hand
1. **The walkthrough's Mt. Mortar item list is the HGSS list, not the GS list.**
It names Carbos, Escape Rope x2, Full Restore, Hyper Potion x2, Max Ether,
Max Potion, Max Revive, PP Up, Rare Candy, Dragon Scale and "TM40 Aerial
Ace". What is actually in `maps/MountMortar*.asm` is: ETHER, GUARD_SPEC,
hidden HYPER_POTION (1F Outside); ESCAPE_ROPE, MAX_REVIVE, HYPER_POTION,
hidden MAX_REPEL (1F Inside); MAX_POTION, RARE_CANDY, TM_DEFENSE_CURL,
DRAGON_SCALE, ELIXER, ESCAPE_ROPE, hidden FULL_RESTORE (2F Inside);
HYPER_POTION, FULL_HEAL, hidden MAX_REVIVE (B1F). **No CARBOS, no PP_UP, no
MAX_ETHER, no IRON anywhere in Mt. Mortar.** TM40 is the right *number* but
the move is DEFENSE_CURL (`constants/item_constants.asm:261`); Aerial Ace is
a Gen 3 move that does not exist in this disassembly.
2. **"grab the free Iron on the cliff" (1F Inside, after coming down from 2F).**
There is no IRON object on any Mt. Mortar map. The nearest items to warp 6 at
(9,9) are the MAX_REVIVE ball at (16,10) and the hidden MAX_REPEL bg event at
(31,9). Treat "Iron" as an HGSS-only pickup.
3. **"surf clockwise around and reach a Hyper Potion and a Max Ether."** There is
no MAX_ETHER in Mt. Mortar. If the intended floor is B1F (the ladder from the
central entrance) the pair is HYPER_POTION (31,17) + FULL_HEAL (4,16); if 2F,
the closest analogue is ELIXER (8,9). Which one the author meant could not be
determined from the asm.
4. **Kiyo's prize money and EXP yields.** The walkthrough gives 816G and
1012/1020 EXP. `data/trainers/parties.asm` only carries the class, name, type
byte and party; the money formula lives in the battle engine's
`data/trainers/attributes.asm` base-money table times level, which was not
opened for this document. The party itself is verified: L34 HITMONLEE, L34
HITMONCHAN, `TRAINERTYPE_NORMAL` (default moves, no held items).
5. **"a giant room where you can zig zag around ... and get two items, to include
a Hyper Potion and Escape Rope."** `MOUNT_MORTAR_1F_INSIDE` actually holds
*three* balls (ESCAPE_ROPE (33,22), MAX_REVIVE (16,10), HYPER_POTION (12,21))
plus a hidden MAX_REPEL - but MAX_REVIVE and the hidden item sit on the
section of the map only reachable from 2F, which is presumably why the
walkthrough counts two.
6. **"The west entrance and east entrance lead to each other."** All three Route
42 doors land on the same map, `MOUNT_MORTAR_1F_OUTSIDE`; the west/east link
runs through `MOUNT_MORTAR_1F_INSIDE` via warp pairs 5/6 (at y=21) and 8/9
(at y=13). The blk files were not decoded, so the exact walkable path between
any two of these warps is not verified here - only the warp graph is.
7. **Dark Cave Pokemon list.** The walkthrough lists Zubat, Golbat, Geodude,
Graveler, Magikarp and Wobbuffet - that is the `DARK_CAVE_BLACKTHORN_ENTRANCE`
table exactly. It omits DUNSPARCE, which is slot 7 of
`DARK_CAVE_VIOLET_ENTRANCE` and is on the map the route exits through.
8. **Dark Cave item list.** The walkthrough lists Blackglasses, Full Heal, Hyper
Potion, Revive and TM13. The Full Heal (36,22) and Hyper Potion (35,9) are on
`DARK_CAVE_VIOLET_ENTRANCE`'s north-east lobe, which this route does not pass
through; the hidden ELIXER at (26,3) is not mentioned at all. The POTION at
(6,8) is named in the prose but missing from the list.
9. **The Shuckle/Krabby rock-smash table** (`data/wild/treemon_maps.asm:46`,
`data/wild/treemons.asm:91`) is real and reachable at
`DARK_CAVE_VIOLET_ENTRANCE`, but the walkthrough never mentions it. Worth
flagging to a bot author as a cheap Shuckle source.
10. **`MOUNT_MORTAR_1F_OUTSIDE` object 2 flag naming.** The item is
`GUARD_SPEC` but the event flag is `EVENT_MOUNT_MORTAR_1F_OUTSIDE_REVIVE`
(`constants/event_flags.asm:1053`). Verbatim from the disassembly; not a
transcription error here, but it will look like one to anyone grepping by
item name.
11. **Mahogany Town and Blackthorn City interiors** (Poke Center, Mart, gym) were
not opened for this document - only their `data/maps/maps.asm` header rows
and `data/maps/spawn_points.asm` entries. Those maps belong to neighbouring
sections.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,599 @@
# Section 17 - Victory Road
Source: `../section-17-victory-road.txt` (the FAQ numbers this chapter "23 > Victory Road")
Maps covered: `MAP_VICTORY_ROAD`, `MAP_VICTORY_ROAD_GATE` (entry gate, badge check)
Badges / key milestones in this section: no badge. Milestones are the fifth and
final `RIVAL1` battle (`EVENT_RIVAL_VICTORY_ROAD`), TM26 Earthquake, and the
north exit onto Route 23 / Indigo Plateau.
Important structural fact the walkthrough hides: **Gold/Silver Victory Road is a
single 10x36-block map**, not three floors. What the FAQ calls "first / second /
third floor" are three vertically stacked regions of the same map joined by
`warp_event` rows whose destination map is `VICTORY_ROAD` itself. Every "ladder"
and the "hole" are self-warps inside `maps/VictoryRoad.asm`.
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 0 | `MAP_VICTORY_ROAD_GATE` | `maps/VictoryRoadGate.asm` | gate warps 3/4 at (9,17)/(10,17), arriving from Route 26 (`maps/Route26.asm:397` warps to `VICTORY_ROAD_GATE, 3`) | gate warps 5/6 at (9,0)/(10,0) -> `VICTORY_ROAD` warp 1 | 8-badge check at coord event (10,11); belongs to the previous section but is the only door in |
| 1 | `MAP_VICTORY_ROAD` (entrance region, y 48..71) | `maps/VictoryRoad.asm` | warp 1 at (9,67) | warp 2, the ladder at (1,49) | "Head up the stairway and up the cliff"; Full Heal (15,48) and Max Revive (12,48) sit here, plus the hidden Full Heal at (3,65) |
| 2 | `MAP_VICTORY_ROAD` (mid region, y 32..43) | same | warp 3 at (1,35) | warp 4, the ladder at (13,31) | "second floor" - X Special at (7,38), then "up along the path ... another ladder" |
| 3 | `MAP_VICTORY_ROAD` (top region, y 5..21) | same | warp 5 at (13,17) | warp 7, the ladder at (17,19) | "Save when you get to the third floor. Go up the cliff to your left, go down the ladder" |
| 4 | `MAP_VICTORY_ROAD` (Full Restore shelf, y 28..34) | same | warp 6 at (17,33) | one-way `HOP_DOWN` ledge row y=34, x=16..19, landing on y=36 | "grab the Full Restore, and jump down" |
| 5 | `MAP_VICTORY_ROAD` (mid region again) | same | ledge landing (x,36) | warp 4 at (13,31) | "Head back up the ladder to the third floor" |
| 6 | `MAP_VICTORY_ROAD` (top region) | same | warp 5 at (13,17) | coord events at (12,8) / (13,8) | rival ambush, `VictoryRoadRivalLeft` / `VictoryRoadRivalRight` |
| 7 | `MAP_VICTORY_ROAD` (TM pocket, y 26..30) | same | warp 8, the `COLL_PIT` at (0,11) | warp 8's landing is warp 9 at (0,27); leave by `HOP_RIGHT` at (8,28)->(10,28) or `HOP_DOWN` at (2,30)/(3,30)->y=32 | "go left instead of out the door, and fall down the hole to get TM26 Earthquake" |
| 8 | `MAP_VICTORY_ROAD` exit | same | corridor x=12..13 at y=7..8, x=11..14 at y=6 | warp 10 at (13,5) -> `ROUTE_23` warp 3 | leave the dungeon |
| 9 | `MAP_ROUTE_23` | `maps/Route23.asm` | warp 3 at (9,13) | - | **next section** (Indigo Plateau / Pokemon League). `Route23FlypointCallback` sets `ENGINE_FLYPOINT_INDIGO_PLATEAU` on arrival. Stop here. |
## 2. Maps
### MAP_VICTORY_ROAD
- Script: `maps/VictoryRoad.asm` (included from `data/maps/scripts.asm:24`)
- Blocks: `maps/VictoryRoad.blk` (360 bytes; `data/maps/blocks.asm:962`)
- Header: `data/maps/maps.asm:160`
`map VictoryRoad, TILESET_CAVE, CAVE, LANDMARK_VICTORY_ROAD, MUSIC_VICTORY_ROAD, TRUE, PALETTE_NITE, FISHGROUP_SHORE`
(per the `map` macro at the head of `data/maps/maps.asm`: tileset, environment,
location, music, phone-service flag TRUE = phone calls suppressed, time-of-day
palette, fishing group.)
- Attributes: `data/maps/attributes.asm:494` `map_attributes VictoryRoad, VICTORY_ROAD, $1d` - border block `$1d`, and **no `connection` rows follow**, so the map has no overworld connections.
- Dimensions: `constants/map_constants.asm:147` `map_const VICTORY_ROAD, 10, 36` -> 10x36 blocks = **20x72 walk cells**. Group `DUNGEONS` (group 3), map id 82.
- Scene variable: `data/maps/scenes.asm:60` `scene_var VICTORY_ROAD, wVictoryRoadSceneID` (sym `01:d6eb`)
- Callbacks: `def_callbacks` is empty - no `MAPCALLBACK_*` for this map.
- Connections: none.
**Warps** (`def_warp_events`, transcribed from `maps/VictoryRoad.asm:242-252`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 67 | `VICTORY_ROAD_GATE` | 5 |
| 2 | 1 | 49 | `VICTORY_ROAD` | 3 |
| 3 | 1 | 35 | `VICTORY_ROAD` | 2 |
| 4 | 13 | 31 | `VICTORY_ROAD` | 5 |
| 5 | 13 | 17 | `VICTORY_ROAD` | 4 |
| 6 | 17 | 33 | `VICTORY_ROAD` | 7 |
| 7 | 17 | 19 | `VICTORY_ROAD` | 6 |
| 8 | 0 | 11 | `VICTORY_ROAD` | 9 |
| 9 | 0 | 27 | `VICTORY_ROAD` | 8 |
| 10 | 13 | 5 | `ROUTE_23` | 3 |
Self-warp pairs, as a bot should read them: 2<->3, 4<->5, 6<->7, 8->9.
Warp 8's tile is `COLL_PIT` and warp 9's tile is plain `COLL_FLOOR`, so **warp 9
never fires**: the hole at (0,11) is one-way down to (0,27). (Tile kinds verified
below.)
**Coord events** (`def_coord_events`, `maps/VictoryRoad.asm:254-256`)
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_VICTORYROAD_RIVAL_BATTLE` (0) | 12 | 8 | `VictoryRoadRivalLeft` (sym `42:59ba`) | rival is teleported to (18,11), walks LEFTx6 + UPx2 to (12,9), battle, then leaves DOWNx2 + RIGHTx6 |
| `SCENE_VICTORYROAD_RIVAL_BATTLE` (0) | 13 | 8 | `VictoryRoadRivalRight` (sym `42:59dd`) | rival walks from (18,13) UPx2 + LEFTx5 + UPx2 to (13,9), battle, then leaves DOWNx2 + RIGHTx5 + DOWNx2 |
These two tiles are the entire width of the corridor that leads to the exit
(y=8 is walkable only at x=6,7 and x=12,13), so the ambush is unavoidable.
**BG events** (`def_bg_events`, `maps/VictoryRoad.asm:258-260`)
| x | y | type | script/item |
|---|---|---|---|
| 3 | 29 | `BGEVENT_ITEM` | `VictoryRoadHiddenMaxPotion` = `hiddenitem MAX_POTION, EVENT_VICTORY_ROAD_HIDDEN_MAX_POTION` |
| 3 | 65 | `BGEVENT_ITEM` | `VictoryRoadHiddenFullHeal` = `hiddenitem FULL_HEAL, EVENT_VICTORY_ROAD_HIDDEN_FULL_HEAL` |
Both target tiles are `COLL_WALL`, i.e. the player stands next to them and
presses A into the wall. (3,29) is faced from (2,29), (4,29) or (3,28);
(3,65) is faced from (2,65), (4,65), (3,64) or (3,66).
**Object events** (`def_object_events`, `maps/VictoryRoad.asm:262-268`)
Column order follows the `object_event` macro in `macros/scripts/maps.asm:113`:
x, y, sprite, movement, radius x, radius y, hour1, hour2, palette, type, sight,
script, event flag.
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `VICTORYROAD_RIVAL` | `SPRITE_RIVAL` | 18 | 13 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_SCRIPT` | `ObjectEvent` (shared ROM0 stub, sym `00:2812`) | `EVENT_RIVAL_VICTORY_ROAD` |
| `VICTORYROAD_POKE_BALL1` | `SPRITE_POKE_BALL` | 3 | 28 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `VictoryRoadTMEarthquake` = `itemball TM_EARTHQUAKE` | `EVENT_VICTORY_ROAD_TM_EARTHQUAKE` |
| `VICTORYROAD_POKE_BALL2` | `SPRITE_POKE_BALL` | 12 | 48 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `VictoryRoadMaxRevive` = `itemball MAX_REVIVE` | `EVENT_VICTORY_ROAD_MAX_REVIVE` |
| `VICTORYROAD_POKE_BALL3` | `SPRITE_POKE_BALL` | 18 | 29 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `VictoryRoadFullRestore` = `itemball FULL_RESTORE` | `EVENT_VICTORY_ROAD_FULL_RESTORE` |
| `VICTORYROAD_POKE_BALL4` | `SPRITE_POKE_BALL` | 15 | 48 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `VictoryRoadFullHeal` = `itemball FULL_HEAL` | `EVENT_VICTORY_ROAD_FULL_HEAL` |
| `VICTORYROAD_POKE_BALL5` | `SPRITE_POKE_BALL` | 7 | 38 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `VictoryRoadXSpecial` = `itemball X_SPECIAL` | `EVENT_VICTORY_ROAD_X_SPECIAL` |
All rows use radius 0,0, hours -1,-1 (always present), palette 0, sight 0.
Object-flag polarity (this bites bot authors): `CheckObjectFlag` in
`engine/overworld/map_objects_2.asm:32` masks an object when its event flag is
**set**, and shows it when the flag is clear or is `-1`. `Script_appear` clears
the flag, `Script_disappear` sets it (`engine/overworld/scripting.asm:879-897`).
`InitializeEventsScript` in `engine/events/std_scripts.asm:521` sets
`EVENT_RIVAL_VICTORY_ROAD` at new game, so the rival object starts hidden and is
only revealed by the `appear` inside the ambush script.
**Derived collision map** (not verbatim asm - built by decoding
`maps/VictoryRoad.blk` through `data/tilesets/cave_collision.asm` and
`data/collision/collision_permissions.asm`, with the quadrant index
`(x & 1) + 2 * (y & 1)` taken from `GetCoordTileCollision` in
`home/map.asm:2065`). It is included because Victory Road is the one map in this
section where "which tile can I stand on" is the whole problem.
Verification: all ten `warp_event` tiles land on the expected collision
(`WARP_CARPET_DOWN`, six `LADDER`, `PIT`, `FLOOR`, `CAVE`) and all six
`object_event` tiles land on walkable tiles, so the decode is aligned.
Legend: `.` floor, `#` wall (impassable), `^` `COLL_UP_WALL` (walkable, but you
cannot step *down* into it and cannot step *up* off it), `L` ladder, `P` pit,
`C` cave exit, `W` warp carpet (needs a DOWN press), `<` `HOP_LEFT`,
`>` `HOP_RIGHT`, `v` `HOP_DOWN` (all three are walkable land; pressing the
matching direction while standing on one jumps two tiles - see `.TryJump` and
`.ledge_table` in `engine/overworld/player_movement.asm:354-391`, which reads the
tile the player is **standing on**, not the tile ahead).
```
01234567890123456789
0 ....................
1 ....................
2 ....................
3 ....................
4 ....................
5 #############C######
6 ###########....#####
7 ############..######
8 ######..####..######
9 #####....##....#####
10 ......##...........#
11 P....####...........
12 ...######^^^^^^^^#..
13 #########........#..
14 #^^^^^^##........#..
15 #......##..#####.#..
16 #......##..#........
17 ###.#..##..#.L..####
18 #...#......#####..##
19 #...##########...L##
20 ##......>#.......###
21 ###.....>#.....#####
22 ####################
23 ####################
24 ####################
25 ####################
26 #...################
27 .......#############
28 ........>#...#######
29 ...#....>#....####.#
30 ##vv..#####...####..
31 ###########..L###...
32 ##..########..##....
33 ....########..##.L..
34 ..#^^^^^^^^#...#vvvv
35 .L#........#...#####
36 ..#........#........
37 ..###.###..##.......
38 ......#<#...^^^^^#..
39 #..#..#<#........#..
40 ##....#<#........#..
41 ####..#<#####.####..
42 #######.............
43 ##########........##
44 ####################
45 ####################
46 ####################
47 ####################
48 ....########.##..###
49 .L..########.###..##
50 #.....####........##
51 #......#####..#....#
52 #...#^^^^^^^^^^^^#..
53 #...#............#..
54 ##..#............#..
55 ##..###.###..##.##..
56 ##........#..##.....
57 ##........#..##....#
58 ##.##^^####..#######
59 ##..#..####..#######
60 ##vv#...^^...#######
61 #####........#######
62 ##..#........######^
63 ##..###.###########.
64 #....#..##.....####.
65 #..#....#......####.
66 ##............###^..
67 ######...W##.####...
68 #######^^#^^^^^^....
69 #######.............
70 ^^^^^^..............
71 ....................
```
One-way transitions a bot must respect (all derived from the grid above):
- (0,11) `PIT` -> (0,27). No route back up from (0,27); the return is a ledge.
- (8,28) or (8,29) `HOP_RIGHT` + RIGHT -> (10,28) / (10,29), which reaches the (13,31) ladder. This is the short way out of the TM pocket.
- (2,30) or (3,30) `HOP_DOWN` + DOWN -> (2,32) / (3,32), which drops into the pocket that holds the (1,35) ladder. This is the long way out of the TM pocket.
- (16..19, 34) `HOP_DOWN` + DOWN -> (16..19, 36). This is the only exit from the Full Restore shelf, and it is why the FAQ says "jump down".
- (8,20) or (8,21) `HOP_RIGHT` + RIGHT -> (10,20) / (10,21) in the top region.
- Rows of `^` (`COLL_UP_WALL`) are walkable dead strips: you may enter them from the side or from below and leave sideways or downward, never upward, and you can never step down onto one.
**Scripts of interest**
- `VictoryRoadRivalLeft` (`maps/VictoryRoad.asm:22`, sym `42:59ba`)
`moveobject VICTORYROAD_RIVAL, 18, 11` / `turnobject PLAYER, DOWN` /
`showemote EMOTE_SHOCK, PLAYER, 15` / `special FadeOutMusic` / `pause 15` /
`appear VICTORYROAD_RIVAL` / `applymovement` `VictoryRoadRivalBattleApproachMovement1`
(LEFT x6, UP x2) / `scall VictoryRoadRivalNext` /
`applymovement VictoryRoadRivalBattleExitMovement1` (DOWN x2, RIGHT x6) /
`disappear VICTORYROAD_RIVAL` / `setscene SCENE_VICTORYROAD_NOOP` /
`playmapmusic` / `end`.
- `VictoryRoadRivalRight` (`maps/VictoryRoad.asm:37`, sym `42:59dd`) - identical
except there is no `moveobject` (he starts at his object_event position 18,13),
the approach is `VictoryRoadRivalBattleApproachMovement2` (UP x2, LEFT x5,
UP x2) and the exit is `VictoryRoadRivalBattleExitMovement2` (DOWN x2,
RIGHT x5, DOWN x2).
- `VictoryRoadRivalNext` (`maps/VictoryRoad.asm:51`, sym `42:59fc`) - the shared
body. `turnobject PLAYER, DOWN`, `playmusic MUSIC_RIVAL_ENCOUNTER`, text
`VictoryRoadRivalBeforeText`, then `setevent EVENT_RIVAL_VICTORY_ROAD`
(**before** the battle, so a reset or blackout mid-battle still leaves the
object hidden), then a three-way starter branch:
- `checkevent EVENT_GOT_TOTODILE_FROM_ELM` -> `.GotTotodile` -> `loadtrainer RIVAL1, RIVAL1_5_CHIKORITA`
- `checkevent EVENT_GOT_CHIKORITA_FROM_ELM` -> `.GotChikorita` -> `loadtrainer RIVAL1, RIVAL1_5_CYNDAQUIL`
- fall-through (player picked Cyndaquil) -> `loadtrainer RIVAL1, RIVAL1_5_TOTODILE`
Each arm runs `winlosstext VictoryRoadRivalDefeatText, VictoryRoadRivalVictoryText`,
`setlasttalked VICTORYROAD_RIVAL`, `startbattle`, `dontrestartmapmusic`,
`reloadmapafterbattle`, then `.AfterBattle`: `playmusic MUSIC_RIVAL_AFTER` and
`VictoryRoadRivalAfterText`.
- `VictoryRoadTMEarthquake` / `MaxRevive` / `FullRestore` / `FullHeal` /
`XSpecial` (`maps/VictoryRoad.asm:97-110`) - each is a bare
`itemball <ITEM>` (two bytes: item, quantity 1; `macros/scripts/maps.asm:155`),
not bytecode. Do not try to disassemble them.
- `VictoryRoadHiddenMaxPotion` / `VictoryRoadHiddenFullHeal`
(`maps/VictoryRoad.asm:112-116`) - `hiddenitem item, flag`, three bytes laid
down as `dwb flag, item` (`macros/scripts/maps.asm:165`).
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_RIVAL_VICTORY_ROAD` | `constants/event_flags.asm:1124` | set by `InitializeEventsScript` (`engine/events/std_scripts.asm:521`) and again by `VictoryRoadRivalNext`; cleared by the `appear` in the ambush scripts | set = rival object masked. Postcondition of the ambush. |
| `EVENT_VICTORY_ROAD_TM_EARTHQUAKE` | `constants/event_flags.asm:1092` | itemball object flag | set once TM26 is taken |
| `EVENT_VICTORY_ROAD_MAX_REVIVE` | `constants/event_flags.asm:1093` | itemball object flag | set once Max Revive is taken |
| `EVENT_VICTORY_ROAD_FULL_RESTORE` | `constants/event_flags.asm:1094` | itemball object flag | set once Full Restore is taken |
| `EVENT_VICTORY_ROAD_FULL_HEAL` | `constants/event_flags.asm:1095` | itemball object flag | set once Full Heal is taken |
| `EVENT_VICTORY_ROAD_X_SPECIAL` | `constants/event_flags.asm:1096` | itemball object flag | set once X Special is taken |
| `EVENT_VICTORY_ROAD_HIDDEN_MAX_POTION` | `constants/event_flags.asm:168` | `hiddenitem` operand at bg (3,29) | set once dug up |
| `EVENT_VICTORY_ROAD_HIDDEN_FULL_HEAL` | `constants/event_flags.asm:169` | `hiddenitem` operand at bg (3,65) | set once dug up |
| `EVENT_GOT_TOTODILE_FROM_ELM` (`:37`), `EVENT_GOT_CHIKORITA_FROM_ELM` (`:38`) | `constants/event_flags.asm` | read by `VictoryRoadRivalNext` | selects which of the three rival parties loads. `EVENT_GOT_CYNDAQUIL_FROM_ELM` (`:36`) is never read here; Cyndaquil is the fall-through case |
| `SCENE_VICTORYROAD_RIVAL_BATTLE` = 0 | defined inline by the `scene_script` macro (`macros/scripts/maps.asm:25`, `const_def` starting at 0) via `maps/VictoryRoad.asm:11` | compared by both coord events | the scene the map starts a new game on |
| `SCENE_VICTORYROAD_NOOP` = 1 | same, `maps/VictoryRoad.asm:12` | written by `setscene` at the end of both ambush scripts | rival ambush disarmed |
Both scene scripts (`VictoryRoadNoop1Scene`, `VictoryRoadNoop2Scene`) are a bare
`end`; the scene id only gates the coord events.
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `TM_EARTHQUAKE` (TM26 - counted off the `add_tm` list in `constants/item_constants.asm:218-246`) | Poke Ball at (3,28), only reachable through the pit at (0,11) | `VictoryRoadTMEarthquake` | `EVENT_VICTORY_ROAD_TM_EARTHQUAKE` |
| `MAX_REVIVE` | Poke Ball at (12,48) | `VictoryRoadMaxRevive` | `EVENT_VICTORY_ROAD_MAX_REVIVE` |
| `FULL_RESTORE` | Poke Ball at (18,29), reached by ladder warp 7 -> (17,33) then north | `VictoryRoadFullRestore` | `EVENT_VICTORY_ROAD_FULL_RESTORE` |
| `FULL_HEAL` | Poke Ball at (15,48) | `VictoryRoadFullHeal` | `EVENT_VICTORY_ROAD_FULL_HEAL` |
| `X_SPECIAL` | Poke Ball at (7,38), standing on the `HOP_LEFT` column; walk north from (7,42) | `VictoryRoadXSpecial` | `EVENT_VICTORY_ROAD_X_SPECIAL` |
| `MAX_POTION` | hidden, face the wall at (3,29) | `bg_event 3, 29, BGEVENT_ITEM` | `EVENT_VICTORY_ROAD_HIDDEN_MAX_POTION` |
| `FULL_HEAL` (second one) | hidden, face the wall at (3,65) | `bg_event 3, 65, BGEVENT_ITEM` | `EVENT_VICTORY_ROAD_HIDDEN_FULL_HEAL` |
**Trainers**
There are no `OBJECTTYPE_TRAINER` objects on this map. The single battle is a
scripted `loadtrainer` with no `EVENT_BEAT_*` flag of its own.
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `RIVAL1_5_CHIKORITA` | `RIVAL1` (trainer class 9, `constants/trainer_constants.asm:51`) | 13th member of the class | `Rival1Group` "RIVAL1 (13)" (`data/trainers/parties.asm:164`, sym `0e:5a92` for the group) | `VictoryRoadRivalNext.GotTotodile` (sym `42:5a27`) | none |
| `RIVAL1_5_CYNDAQUIL` | `RIVAL1` | 14th | `Rival1Group` "RIVAL1 (14)" (`data/trainers/parties.asm:174`) | `VictoryRoadRivalNext.GotChikorita` (sym `42:5a37`) | none |
| `RIVAL1_5_TOTODILE` | `RIVAL1` | 15th | `Rival1Group` "RIVAL1 (15)" (`data/trainers/parties.asm:184`) | fall-through arm of `VictoryRoadRivalNext` | none |
Parties, verbatim (`TRAINERTYPE_MOVES`, name string `"?@"`):
RIVAL1 (13) - loaded when the player chose Totodile:
```
db 34, SNEASEL, QUICK_ATTACK, SCREECH, FAINT_ATTACK, FURY_CUTTER
db 36, GOLBAT, LEECH_LIFE, BITE, CONFUSE_RAY, WING_ATTACK
db 35, MAGNETON, THUNDERSHOCK, SONICBOOM, THUNDER_WAVE, SWIFT
db 35, HAUNTER, MEAN_LOOK, CURSE, SHADOW_BALL, CONFUSE_RAY
db 35, KADABRA, DISABLE, PSYBEAM, RECOVER, FUTURE_SIGHT
db 38, MEGANIUM, REFLECT, RAZOR_LEAF, POISONPOWDER, BODY_SLAM
```
RIVAL1 (14) - loaded when the player chose Chikorita:
```
db 34, SNEASEL, QUICK_ATTACK, SCREECH, FAINT_ATTACK, FURY_CUTTER
db 36, GOLBAT, LEECH_LIFE, BITE, CONFUSE_RAY, WING_ATTACK
db 35, MAGNETON, THUNDERSHOCK, SONICBOOM, THUNDER_WAVE, SWIFT
db 35, HAUNTER, MEAN_LOOK, CURSE, SHADOW_BALL, CONFUSE_RAY
db 35, KADABRA, DISABLE, PSYBEAM, RECOVER, FUTURE_SIGHT
db 38, TYPHLOSION, SMOKESCREEN, EMBER, QUICK_ATTACK, FLAME_WHEEL
```
RIVAL1 (15) - loaded when the player chose Cyndaquil:
```
db 34, SNEASEL, QUICK_ATTACK, SCREECH, FAINT_ATTACK, FURY_CUTTER
db 36, GOLBAT, LEECH_LIFE, BITE, CONFUSE_RAY, WING_ATTACK
db 34, MAGNETON, THUNDERSHOCK, SONICBOOM, THUNDER_WAVE, SWIFT
db 35, HAUNTER, MEAN_LOOK, CURSE, SHADOW_BALL, CONFUSE_RAY
db 35, KADABRA, DISABLE, PSYBEAM, RECOVER, FUTURE_SIGHT
db 38, FERALIGATR, RAGE, WATER_GUN, SCARY_FACE, SLASH
```
Note the send-out order: Sneasel, Golbat, Magneton, Haunter, Kadabra, starter.
The FAQ lists the starter fourth; the asm puts it last.
**Wild encounters**
`data/wild/kanto_grass.asm:144` `def_grass_wildmons VICTORY_ROAD` (Victory Road
lives in the Kanto grass table even though it is Johto-side).
Encounter rates: `db 6 percent, 6 percent, 6 percent` (morn / day / nite - all
equal). Slot weights come from `GrassMonProbTable`
(`data/wild/probabilities.asm:6`): 30 / 30 / 20 / 10 / 5 / 4 / 1 percent.
Gold (`IF DEF(_GOLD)`), identical for morn, day and nite:
| slot | % | level | species |
|---|---|---|---|
| 1 | 30 | 32 | `GRAVELER` |
| 2 | 30 | 32 | `GOLBAT` |
| 3 | 20 | 33 | `URSARING` |
| 4 | 10 | 34 | `ONIX` |
| 5 | 5 | 36 | `ONIX` |
| 6 | 4 | 35 | `RHYHORN` |
| 7 | 1 | 35 | `RHYHORN` |
Silver (`ELIF DEF(_SILVER)`) is the same table with slot 3 replaced by
`db 33, DONPHAN`.
No water, fishing, headbutt or rock-smash data: there is no `VICTORY_ROAD` row in
`data/wild/kanto_water.asm`, `data/wild/johto_water.asm`, `data/wild/fish.asm` or
`data/wild/treemons.asm`, and the decoded collision map contains no water tiles.
The `FISHGROUP_SHORE` in the header is therefore unreachable. No roamer either
(`VICTORY_ROAD` is absent from `data/wild/roammon_maps.asm`).
### MAP_VICTORY_ROAD_GATE
Included here because it is the only entrance and it carries the badge gate. The
NPC beats themselves belong to the previous / later sections.
- Script: `maps/VictoryRoadGate.asm` (`data/maps/scripts.asm:452`)
- Blocks: `maps/VictoryRoadGate.blk` (`data/maps/blocks.asm:943`)
- Header: `data/maps/maps.asm:467`
`map VictoryRoadGate, TILESET_GATE, GATE, LANDMARK_ROUTE_26, MUSIC_INDIGO_PLATEAU, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Attributes: `data/maps/attributes.asm:666`, border block `$00`, no connections.
- Dimensions: `constants/map_constants.asm:433` `map_const VICTORY_ROAD_GATE, 10, 9` -> 20x18 cells. Group `VIRIDIAN` (group 23), map id 13.
- Scene variable: `data/maps/scenes.asm:16` `scene_var VICTORY_ROAD_GATE, wVictoryRoadGateSceneID` (sym `01:d6bf`)
**Warps** (`maps/VictoryRoadGate.asm:101-109`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 17 | 7 | `ROUTE_22` | 1 |
| 2 | 18 | 7 | `ROUTE_22` | 1 |
| 3 | 9 | 17 | `ROUTE_26` | 1 |
| 4 | 10 | 17 | `ROUTE_26` | 1 |
| 5 | 9 | 0 | `VICTORY_ROAD` | 1 |
| 6 | 10 | 0 | `VICTORY_ROAD` | 1 |
| 7 | 1 | 7 | `ROUTE_28` | 2 |
| 8 | 2 | 7 | `ROUTE_28` | 2 |
Cross-checked from the other side: `maps/Route22.asm:20` warps to
`VICTORY_ROAD_GATE, 1`, `maps/Route26.asm:397` warps to `VICTORY_ROAD_GATE, 3`,
`maps/Route28.asm:21` warps to `VICTORY_ROAD_GATE, 7`.
**Coord events**
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_VICTORYROADGATE_BADGE_CHECK` (0) | 10 | 11 | `VictoryRoadGateBadgeCheckScript` (sym `5f:4fdc`) | turns the player LEFT and falls into `_VictoryRoadGateBadgeCheckScript` |
**BG events**: none.
**Object events** (`maps/VictoryRoadGate.asm:116-119`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `VICTORYROADGATE_OFFICER` | `SPRITE_OFFICER` | 8 | 11 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `VictoryRoadGateOfficerScript` | `-1` (always present) |
| `VICTORYROADGATE_BLACK_BELT1` | `SPRITE_BLACK_BELT` | 7 | 5 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `VictoryRoadGateLeftBlackBeltScript` | `EVENT_OPENED_MT_SILVER` |
| `VICTORYROADGATE_BLACK_BELT2` | `SPRITE_BLACK_BELT` | 12 | 5 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `VictoryRoadGateRightBlackBeltScript` | `EVENT_FOUGHT_SNORLAX` |
**Derived collision map** (same method, `data/tilesets/gate_collision.asm`;
`c` = `COLL_COUNTER`, a wall):
```
01234567890123456789
0 #########DD#########
1 ########....########
2 ########....########
3 ########....########
4 ########....########
5 ....................
6 ....####....####....
7 .WW.####....####.WW.
8 ########....########
9 ########....########
10 ########cc..########
11 ########.c.c########
12 ########.c..########
13 ########cc..########
14 ########....########
15 ########....########
16 ########....########
17 ########.WW.########
```
Row 11 is walkable only at x=8 (where the officer stands) and x=10, so
**(10,11) is a mandatory chokepoint**: the badge check cannot be walked around.
**Scripts of interest**
- `_VictoryRoadGateBadgeCheckScript` (`maps/VictoryRoadGate.asm:25`, sym `5f:4fe3`):
`writetext VictoryRoadGateOfficerText`, `promptbutton`, `readvar VAR_BADGES`,
`ifgreater NUM_JOHTO_BADGES - 1, .AllEightBadges`. On failure it prints
`VictoryRoadGateNotEnoughBadgesText` and `applymovement PLAYER,
VictoryRoadGateStepDownMovement` (one `step DOWN`, pushing the player back
south). On success it prints `VictoryRoadGateEightBadgesText` and
`setscene SCENE_VICTORYROADGATE_NOOP`, permanently disarming the coord event.
- `VictoryRoadGateOfficerScript` (`maps/VictoryRoadGate.asm:23`) - `faceplayer`
then the same body, so talking to the officer also opens the gate.
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Entry to Victory Road | `maps/VictoryRoadGate.asm:19` `VictoryRoadGateBadgeCheckScript` / `_VictoryRoadGateBadgeCheckScript` | `readvar VAR_BADGES` then `ifgreater NUM_JOHTO_BADGES - 1` (that is, badge count must be >= 8). `NUM_JOHTO_BADGES` = 8 (`constants/ram_constants.asm:260`). `VAR_BADGES` resolves to `.CountBadges` in `engine/overworld/variables.asm:80`, which is `CountSetBits` over **two** bytes (`wBadges`, b = 2), i.e. Johto **and** Kanto badges together | own all eight Johto badges. The script then sets `SCENE_VICTORYROADGATE_NOOP`, so the check runs at most once |
| Physical block west to Route 28 | `maps/VictoryRoadGate.asm:118` object `VICTORYROADGATE_BLACK_BELT1` at (7,5), masked when `EVENT_OPENED_MT_SILVER` is set | row 5 is the only east-west corridor in the gate and he stands in it | out of scope here (Mt. Silver, later section) |
| Physical block east to Route 22 | `maps/VictoryRoadGate.asm:119` object `VICTORYROADGATE_BLACK_BELT2` at (12,5), masked when `EVENT_FOUGHT_SNORLAX` is set | same corridor | out of scope here (Kanto, later section) |
| Rival ambush before the exit | `maps/VictoryRoad.asm:255-256` coord events at (12,8) and (13,8), gated on `SCENE_VICTORYROAD_RIVAL_BATTLE` | those two tiles are the whole corridor to the exit at (13,5) | win the battle; the script then runs `setscene SCENE_VICTORYROAD_NOOP` |
| TM26 Earthquake | geometry only, no flag: (3,28) is inside the y=26..30 pocket whose only entrance is the `COLL_PIT` at (0,11) | reach the top region first (so: beat the ladders, not the rival) | none |
| Full Restore | geometry only: (18,29) is on a shelf whose only entrance is warp 7 (17,19) -> (17,33) | reach the top region | none |
Field moves: **none required**. There is no `COLL_CUT_TREE`, no water, no
strength boulder and no whirlpool anywhere in the decoded map, and the header
palette is `PALETTE_NITE`, not `PALETTE_DARK`, so **Flash is not needed** either.
The FAQ's "no puzzles to solve now" is accurate.
## 4. Bot checklist
Coordinates are map cells `(x, y)` on the named map. "step" means walk onto the
tile; ladders and the pit fire on arrival (`Permissions.isImmediateWarp`
equivalent: `COLL_LADDER` = `$72` and `COLL_PIT` = `$60` are warp collisions),
while the entrance carpet at (9,67) fires on a DOWN press.
1. `MAP_VICTORY_ROAD_GATE`: from the Route 26 door at (9,17)/(10,17) walk north along x=10. Precondition: 8 Johto badges. Step (10,11) -> `VictoryRoadGateBadgeCheckScript` fires. Postcondition: `wVictoryRoadGateSceneID` = `SCENE_VICTORYROADGATE_NOOP`. If badges < 8 the script pushes you one step DOWN and you cannot pass.
2. `MAP_VICTORY_ROAD_GATE`: step (9,0) or (10,0) -> warp to `MAP_VICTORY_ROAD` (9,67).
3. `MAP_VICTORY_ROAD` entrance region. Waypoints that were checked against the
collision map: (9,66) -> west along y=66 to (7,66) -> north x=7 through
y=65,64 to (7,63) -> (7,62) -> east along y=62 to (11,62)/(12,62) -> north
x=11/12 through y=61..55 -> (11,54) -> the long y=53/54 corridor (x=5..16).
4. Max Revive and Full Heal: from the y=53/54 corridor go east to (15,54) ->
(15,55) -> (15,56) -> east along y=56 to (18,56) -> north along x=18 through
y=55,54,53,52 to (18,51) -> west along y=51 to (15,51) -> (15,50) -> (16,50)
-> (16,49) -> (16,48), press A facing LEFT
into (15,48) -> Full Heal (`EVENT_VICTORY_ROAD_FULL_HEAL`). Then west along
y=50 to (12,50) -> (12,49) -> press A facing UP into (12,48) -> Max Revive
(`EVENT_VICTORY_ROAD_MAX_REVIVE`).
5. Optional hidden Full Heal: face the wall at (3,65) from (2,65), (4,65),
(3,64) or (3,66) and press A. Postcondition
`EVENT_VICTORY_ROAD_HIDDEN_FULL_HEAL`.
6. Back along the y=53/54 corridor to its west end, (3,53) -> (2,52) -> (2,51)
-> (2,50) -> (1,49); the ladder fires on arrival -> warp 2 -> (1,35).
7. X Special. Verified chain from the ladder landing: (1,35) -> (1,36) -> (1,37)
-> (1,38) -> east along y=38 to (5,38) -> (5,37) -> (5,36) -> east along y=36
to (9,36)/(10,36) -> (9,37)/(10,37) -> (9,38)/(10,38) -> south through
y=39,40 (x=9..16 is open) -> (13,41) -> (13,42) -> west along y=42 to (7,42)
-> north along x=7 (the `HOP_LEFT` column is walkable) through y=41,40,39 to
(7,38). Press A -> X Special. Postcondition `EVENT_VICTORY_ROAD_X_SPECIAL`.
Do **not** press LEFT while on that column (it triggers a two-tile jump into
a wall column).
8. To the (13,31) ladder: back to (7,42) -> east along y=42 to (18,42) -> north
along x=18 through y=41,40,39,38 to (18,37) -> west along y=37 then y=36 to
(13,36) -> north (13,35), (13,34), (13,33), (13,32) -> step (13,31) -> warp 4
-> arrive (13,17). Note you cannot climb x=13 straight up from y=41: (13,38)
is `COLL_UP_WALL` and blocks the step up out of it, and the x=11 column walls
off the y=34..36 strip on its west side.
9. Optional Full Restore detour. The verified chain from the ladder to (17,19) is:
(13,17) -> (13,16) -> east along y=16 to (16,16) -> (16,15) -> (16,14) ->
west along y=14 to (9,14)/(10,14) -> south x=9/10 through y=15,16,17 to
(9,18)/(10,18) -> west along y=18 to (5,18) -> (5,17) -> (5,16) -> west along
y=16 to (3,16) -> (3,17) -> (3,18) -> (3,19) -> (3,20) -> east along y=20 to
(8,20) -> **press RIGHT to hop the ledge**, landing (10,20) -> east/north to
(16,19) -> (17,19). On the cart that `HOP_RIGHT` is the only way into the
(17,19) pocket. Step (17,19) -> warp 7 -> arrive (17,33). Walk north along
x=17..19 to (18,29), press A -> Full Restore. Postcondition
`EVENT_VICTORY_ROAD_FULL_RESTORE`. Return to y=34 (x=16..19) and press DOWN to
hop the ledge, landing on y=36 - which is already the y=36 corridor, so go
west to (13,36) and north to (13,31), warp 4, back to (13,17).
10. From (13,17): (13,16) -> east along y=16 to (18,16) -> north along x=18
through (18,15), (18,14), (18,13) (the rival's masked object tile), (18,12)
to (18,11) -> west along y=11 to (13,11) -> north (13,10), (13,9) to (13,8).
11. Step (12,8) or (13,8) -> rival ambush. Precondition `wVictoryRoadSceneID` = 0. Battle `RIVAL1` member 13/14/15 depending on the starter events. Postconditions: `EVENT_RIVAL_VICTORY_ROAD` set, `wVictoryRoadSceneID` = `SCENE_VICTORYROAD_NOOP`.
12. TM26 detour. The x=6,7 wall pair on y=10 and the x=5..8 wall run on y=11 mean
you cannot walk straight west; the verified chain is (9,11) -> (9,10) ->
(8,10) -> (8,9) -> west along y=9 to (5,9) -> (5,10) -> west along y=10 to
(0,10) -> (0,11), the `COLL_PIT`, which fires on arrival -> land at (0,27).
Then (0,28) -> east along y=28 to (2,28), press A facing RIGHT into the ball
object at (3,28) -> TM26 Earthquake (`EVENT_VICTORY_ROAD_TM_EARTHQUAKE`).
Optionally step to (2,29) and press A facing RIGHT into the wall at (3,29)
-> hidden Max Potion (`EVENT_VICTORY_ROAD_HIDDEN_MAX_POTION`).
13. Leave the pocket: east along y=28 to (7,28) -> step onto (8,28), the
`HOP_RIGHT` tile -> press RIGHT to hop to (10,28) -> (10,29) -> east to
(12,29) -> (12,30) -> (12,31) -> step (13,31) -> warp 4 -> (13,17).
(Alternative, longer: stand on (2,30) or (3,30) and press DOWN, landing at
y=32, which drops you back in the (1,35) ladder pocket and forces a full
re-cross of the middle region via steps 7 and 8.)
14. Repeat step 10 to return to the exit corridor. The coord events no longer fire.
15. Walk (13,7) -> (13,6) -> step (13,5) (`COLL_CAVE`) -> warp 10 -> `MAP_ROUTE_23` (9,13). End of section.
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Map header, dimensions, warps, coord events, bg events, object events for any Gold map | `src/import/RomExtractorGen2.lua` (map event reader at lines 785-865, `extractMaps` from 866) | implemented - data driven off the ROM, so `VICTORY_ROAD` needs no hand port |
| `OBJECTTYPE_ITEMBALL` item data extraction | `src/import/RomExtractorGen2.lua:2968` (`obj.itemball = readItemBall(...)`) | implemented (extraction only) |
| **Picking up a Poke Ball object** (all five Victory Road items) | `src/world/gen2/World.lua:5257` `World:interact` | **missing** - the A-press path handles trainers, strength boulders, `scriptKey` NPCs, `BGEVENT_READ` signs, `BGEVENT_ITEM` hidden items and the tile-collision field moves. Nothing consumes `obj.itemball`, and an itemball object has no `scriptKey`, so the press falls through |
| Hidden items (`BGEVENT_ITEM`, the two here) | `src/world/gen2/HiddenItems.lua`, used at `src/world/gen2/World.lua:5290` | implemented |
| Ladder / cave / pit warps | `src/world/gen2/Permissions.lua:162` `isWarpCollision` (covers `$60` PIT and the `$7x` block) and `isImmediateWarp` | implemented - the (0,11) hole and all six ladders work |
| Warp carpet needing a direction press (the (9,67) entrance) | `src/world/gen2/Permissions.lua:170` `CARPET_DIR` | implemented |
| Walkability from `CollisionPermissionTable` | `src/world/gen2/Permissions.lua:11` (256-entry table), `src/world/gen2/Map.lua:59` | implemented |
| **Ledge hops** (`COLL_HOP_LEFT/RIGHT/DOWN`, five of them in this map) | - | **missing** - no `$a0..$a7` handling anywhere under `src/world/gen2/`. The tiles are LAND in the port's permission table, so a bot can walk over them freely. Net effect: the one-way ledges become two-way, and the Full Restore shelf / TM pocket exits behave differently from the cart |
| **Directional side walls** (`COLL_UP_WALL`, heavily used here) | `src/world/gen2/FieldMoves.lua:313` `BLOCKED_BY` | partial - the `$b0..$b7` table exists but is only consulted by `FieldMoves.directionBlocked` for surf/field-move refusals. There is no port equivalent of `GetMovementPermissions` (`home/map.asm:1868`), so `UP_WALL` does not restrict ordinary walking |
| Coord events gated on the map scene | `src/world/gen2/World.lua:5007` `World:tryCoordScript`, scene storage at `World.lua:740` / `World:scene` at 1161 | implemented |
| Scene scripts / `setscene` persistence across save | `src/world/gen2/World.lua:558-584`, `src/core/gen2/Save.lua` | implemented |
| Rival ambush opcodes (`moveobject`, `turnobject`, `showemote`, `appear`, `disappear`, `applymovement`, `scall`, `setscene`, `playmapmusic`, `playmusic`, `winlosstext`, `setlasttalked`, `loadtrainer`, `startbattle`, `dontrestartmapmusic`, `reloadmapafterbattle`) | `src/script/gen2/Opcodes.lua` (table), `src/script/gen2/Vm.lua` (`moveobject` 337, `showemote` 961, `loadtrainer` 806, `winlosstext` 918, `reloadmapafterbattle` 886) | implemented |
| `special FadeOutMusic` | `src/script/gen2/Specials.lua:1066` | implemented |
| `VAR_BADGES` (the gate check) | `src/world/gen2/World.lua:1240` - counts `player.badges` + `player.kantoBadges`, matching `CountSetBits` over two bytes | implemented |
| Object masking by event flag | `src/import/RomExtractorGen2.lua` object `eventFlag` field; `src/world/gen2/Npc.lua` | partial - the flag is extracted, but confirm the mask polarity (hidden when **set**) matches `CheckObjectFlag`; not verified in this pass |
| Wild encounters for this map | `src/import/RomExtractorGen2.lua:3720` (`JohtoGrassWildMons` / `KantoGrassWildMons`), `src/battle/gen2/Encounter.lua` | implemented |
| Battle music landmark split | `src/battle/gen2/BattleMusic.lua:45-56` explicitly names the Victory Road landmark boundary | implemented |
| A driver that walks Victory Road | - | **missing** - no `tests/drivers/gold_*.lua` mentions Victory Road |
## 6. Unresolved / verify by hand
- **Rival Magneton level.** The FAQ says "Level 34 Magneton". The asm has 35 in
RIVAL1 (13) and RIVAL1 (14) and 34 only in RIVAL1 (15), the Feraligatr set
loaded when the player picked Cyndaquil. The FAQ appears to have been written
from one playthrough.
- **Rival party order.** The FAQ lists the starter evolution fourth. The asm
order is Sneasel, Golbat, Magneton, Haunter, Kadabra, starter (starter last).
- **"You get: 2280G"** and the per-mon EXP figures were not verified; base money
for the `RIVAL1` class lives in the trainer class attribute table, which was
not opened for this pass.
- **Wild list.** The FAQ lists only Golbat, Graveler, Rhyhorn and "Donphan
(Silver only)". The asm table also carries Onix (two slots, L34 and L36) and,
in Gold, Ursaring at L33 in the slot Silver gives to Donphan.
- **Item list.** The FAQ's item list omits the hidden Max Potion at (3,29) and
does not distinguish the itemball Full Heal at (15,48) from the hidden Full
Heal at (3,65).
- **"Head up the stairway"** - there is no `COLL_STAIRCASE` in the decoded
Victory Road map. What the FAQ calls stairways and cliffs are ordinary floor
plus the `COLL_UP_WALL` strips; the only vertical transitions are the six
ladders, the pit and the five ledges.
- **"Head back up the ladder to get back to the third floor"** (after TM26).
There is no ladder inside the TM pocket. The two ways out are the `HOP_RIGHT`
at (8,28)/(8,29) into the x=10..13 corridor that reaches the (13,31) ladder,
and the `HOP_DOWN` at (2,30)/(3,30) into the (1,35) pocket. The checklist above
prefers the first; which one the FAQ author meant is not recoverable from the
text.
- **X Special tile.** The Poke Ball at (7,38) sits on a `COLL_HOP_LEFT` tile with
`COLL_WALL` on both sides and above. Ledge tiles are `LAND_TILE` in
`CollisionPermissionTable`, so the column x=7, y=38..41 is walkable from
(7,42) upward and the ball is reachable; but this should be confirmed on
hardware or in an emulator, because it is the one placement in this map that
looks like a mistake rather than a design.
- **The gate's black belts as physical blockers.** The claim that
`VICTORYROADGATE_BLACK_BELT1` at (7,5) and `BLACK_BELT2` at (12,5) block the
only east-west corridor is inferred from the decoded gate collision (row 5 is
the sole corridor and rows 4/6 are walls beside it) plus the fact that NPCs are
solid. It is not stated in any script, and it belongs to later sections
(Route 28 / Route 22) rather than this one.
- **Object flag polarity in the port** was not tested; see the Port coverage row.
@@ -0,0 +1,883 @@
# Section 18 - Pokémon League
Source: `../section-18-pok-mon-league.txt`
Maps covered: `MAP_ROUTE_23`, `MAP_INDIGO_PLATEAU_POKECENTER_1F`, `MAP_WILLS_ROOM`,
`MAP_KOGAS_ROOM`, `MAP_BRUNOS_ROOM`, `MAP_KARENS_ROOM`, `MAP_LANCES_ROOM`,
`MAP_HALL_OF_FAME`
Badges / key milestones in this section: no badge. `ENGINE_FLYPOINT_INDIGO_PLATEAU`
(fly point), `EVENT_BEAT_ELITE_4_WILL/KOGA/BRUNO/KAREN`,
`EVENT_BEAT_CHAMPION_LANCE`, `EVENT_BEAT_ELITE_FOUR`, `STATUSFLAGS_HALL_OF_FAME_F`
(Hall of Fame induction -> credits -> post-game respawn at `SPAWN_NEW_BARK`).
All eight maps live in map group `INDIGO` (group 16, `constants/map_constants.asm:321`).
---
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 0 | `MAP_VICTORY_ROAD` | `maps/VictoryRoad.asm` | (previous section) | warp 10 at `(13, 5)` -> `ROUTE_23` warp 3 | Victory Road's north exit; the walkthrough opens already standing on the Plateau approach |
| 1 | `MAP_ROUTE_23` | `maps/Route23.asm` | warps 3/4 at `(9, 13)`/`(10, 13)` from Victory Road | warps 1/2 at `(9, 5)`/`(10, 5)` -> `INDIGO_PLATEAU_POKECENTER_1F` warps 1/2 | "Route 32 is very short, and you'll immediately be at the Pokémon League building" (the FAQ means Route **23**). `MAPCALLBACK_NEWMAP` registers the fly point here |
| 2 | `MAP_INDIGO_PLATEAU_POKECENTER_1F` | `maps/IndigoPlateauPokecenter1F.asm` | warps 1/2 at `(5, 13)`/`(6, 13)` | warp 4 at `(14, 3)` -> `WILLS_ROOM` warp 1 | Heal (nurse, left), buy (clerk, right), optional Abra teleport home, optional Mon/Wed rival ambush, then through the north door |
| 3 | `MAP_WILLS_ROOM` | `maps/WillsRoom.asm` | warp 1 at `(5, 17)` | warps 2/3 at `(4, 2)`/`(5, 2)` -> `KOGAS_ROOM` warps 1/2 | Elite Four 1 - Will |
| 4 | `MAP_KOGAS_ROOM` | `maps/KogasRoom.asm` | warps 1/2 at `(4, 17)`/`(5, 17)` | warps 3/4 at `(4, 2)`/`(5, 2)` -> `BRUNOS_ROOM` warps 1/2 | Elite Four 2 - Koga |
| 5 | `MAP_BRUNOS_ROOM` | `maps/BrunosRoom.asm` | warps 1/2 at `(4, 17)`/`(5, 17)` | warps 3/4 at `(4, 2)`/`(5, 2)` -> `KARENS_ROOM` warps 1/2 | Elite Four 3 - Bruno |
| 6 | `MAP_KARENS_ROOM` | `maps/KarensRoom.asm` | warps 1/2 at `(4, 17)`/`(5, 17)` | warps 3/4 at `(4, 2)`/`(5, 2)` -> `LANCES_ROOM` warps 1/2 | Elite Four 4 - Karen |
| 7 | `MAP_LANCES_ROOM` | `maps/LancesRoom.asm` | warps 1/2 at `(4, 23)`/`(5, 23)` | scripted `warp HALL_OF_FAME, 4, 13` at the tail of `LancesRoomLanceScript` (warps 3/4 at `(4, 0)`/`(5, 0)` are the same door) | Champion Lance, then the Mary / Prof. Oak cutscene |
| 8 | `MAP_HALL_OF_FAME` | `maps/HallOfFame.asm` | scripted warp to `(4, 13)` | `halloffame` -> `Credits` -> title screen | Induction, credits, "The End" |
Post-credits: `engine/menus/intro_menu.asm` `.SpawnAfterE4` sends the next CONTINUE
to `SPAWN_NEW_BARK`, not to the saved position. That is the start of the Kanto
half and belongs to the next section.
---
## 2. Maps
### MAP_ROUTE_23
- Script: `maps/Route23.asm`
- Blocks: `maps/Route23.blk`
- Header (`data/maps/maps.asm:349`): `map Route23, TILESET_KANTO, TOWN, LANDMARK_ROUTE_23, MUSIC_INDIGO_PLATEAU, FALSE, PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:322` -> `map_const ROUTE_23, 10, 9` (10 x 9 blocks = 20 x 18 tiles)
- Attributes: `data/maps/attributes.asm:397` -> `map_attributes Route23, ROUTE_23, $0f`, **no connections** (despite `TOWN` environment)
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 5 | `INDIGO_PLATEAU_POKECENTER_1F` | 1 |
| 2 | 10 | 5 | `INDIGO_PLATEAU_POKECENTER_1F` | 2 |
| 3 | 9 | 13 | `VICTORY_ROAD` | 10 |
| 4 | 10 | 13 | `VICTORY_ROAD` | 10 |
**Coord events** - none (`def_coord_events` is empty).
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 11 | 7 | `BGEVENT_READ` | `IndigoPlateauSign` (`IndigoPlateauSignText`) |
**Object events** - none (`def_object_events` is empty).
**Scripts of interest**
- `Route23FlypointCallback` (`50:5888`), registered as `callback MAPCALLBACK_NEWMAP`.
Body is two opcodes: `setflag ENGINE_FLYPOINT_INDIGO_PLATEAU` / `endcallback`.
This is the whole of the walkthrough's "You'll now be able to fly to this
location directly", and it is also the gate on the Kanto half of the Fly map
(see section 5 - Blockers).
- `IndigoPlateauSign` -> `jumptext IndigoPlateauSignText`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_FLYPOINT_INDIGO_PLATEAU` | `constants/engine_flags.asm:78` | set by `Route23FlypointCallback` | after one step onto Route 23, Fly to Indigo Plateau is available; also unlocks the Kanto Fly map |
**Items** - none.
**Trainers** - none.
**Wild encounters** - none. `ROUTE_23` has no entry in `data/wild/johto_grass.asm`,
`data/wild/kanto_grass.asm`, `data/wild/johto_water.asm` or
`data/wild/kanto_water.asm` (grepped; `kanto_grass.asm` jumps
`ROUTE_22` -> `ROUTE_24`). The header's `FISHGROUP_SHORE` is the header default and
there is no water to fish from.
---
### MAP_INDIGO_PLATEAU_POKECENTER_1F
- Script: `maps/IndigoPlateauPokecenter1F.asm`
- Blocks: `maps/IndigoPlateauPokecenter1F.blk`
- Header (`data/maps/maps.asm:350`): `map IndigoPlateauPokecenter1F, TILESET_POKECENTER, INDOOR, LANDMARK_INDIGO_PLATEAU, MUSIC_INDIGO_PLATEAU, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:323` -> `map_const INDIGO_PLATEAU_POKECENTER_1F, 9, 7` (18 x 14 tiles)
- Attributes: `data/maps/attributes.asm:598`, border `$00`, no connections
`object_const_def` order (the ids `moveobject` / `applymovement` / `appear` take):
```
0 INDIGOPLATEAUPOKECENTER1F_NURSE
1 INDIGOPLATEAUPOKECENTER1F_CLERK
2 INDIGOPLATEAUPOKECENTER1F_COOLTRAINER_M
3 INDIGOPLATEAUPOKECENTER1F_RIVAL
4 INDIGOPLATEAUPOKECENTER1F_GRAMPS
5 INDIGOPLATEAUPOKECENTER1F_ABRA
```
(the macro is `const_def 2`, so the emitted numbers start at 2)
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 5 | 13 | `ROUTE_23` | 1 |
| 2 | 6 | 13 | `ROUTE_23` | 2 |
| 3 | 0 | 13 | `POKECENTER_2F` | 1 |
| 4 | 14 | 3 | `WILLS_ROOM` | 1 |
**Coord events** (`def_coord_events`)
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_INDIGOPLATEAUPOKECENTER1F_RIVAL_BATTLE` (= 0) | 16 | 4 | `PlateauRivalBattle1` (`5a:48ff`) | rival walks up from the left column and blocks you |
| `SCENE_INDIGOPLATEAUPOKECENTER1F_RIVAL_BATTLE` (= 0) | 17 | 4 | `PlateauRivalBattle2` (`5a:4940`) | same fight, approached from the right column |
**BG events** - none.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `..._NURSE` | `SPRITE_NURSE` | 3 | 7 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `IndigoPlateauPokecenter1FNurseScript` | -1 |
| `..._CLERK` | `SPRITE_CLERK` | 11 | 7 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `IndigoPlateauPokecenter1FClerkScript` | -1 |
| `..._COOLTRAINER_M` | `SPRITE_COOLTRAINER_M` | 11 | 11 | `SPRITEMOVEDATA_WANDER` (radius 2,2) | `OBJECTTYPE_SCRIPT` | `IndigoPlateauPokecenter1FCooltrainerMScript` | -1 |
| `..._RIVAL` | `SPRITE_RIVAL` | 16 | 9 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_SCRIPT` | `ObjectEvent` | `EVENT_INDIGO_PLATEAU_POKECENTER_RIVAL` |
| `..._GRAMPS` | `SPRITE_GRAMPS` (`PAL_NPC_BLUE`) | 1 | 9 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `TeleportGuyScript` | `EVENT_TELEPORT_GUY` |
| `..._ABRA` | `SPRITE_JYNX` (`PAL_NPC_BROWN`) | 0 | 9 | `SPRITEMOVEDATA_POKEMON` | `OBJECTTYPE_SCRIPT` | `AbraScript` | `EVENT_TELEPORT_GUY` |
Note the last row: the "Abra" the walkthrough names is declared with
`SPRITE_JYNX` (`constants/sprite_constants.asm:122`; `data/sprites/sprite_mons.asm`
maps that slot to `JYNX`). Its script is `AbraScript`, which does
`cry ABRA`. Verbatim from the file, not a typo of mine.
An object's event flag hides it when the flag is **set** (`Script_disappear`
sets, `Script_appear` clears - `engine/overworld/scripting.asm:879-898`).
`EVENT_INDIGO_PLATEAU_POKECENTER_RIVAL` is set on New Game by
`engine/events/std_scripts.asm:556` (the `EVENT_INITIALIZED_EVENTS` block), so
the rival starts hidden. `EVENT_TELEPORT_GUY` is not in that block, so the old
man and his Pokémon are visible from the first visit.
**Scripts of interest**
- `IndigoPlateauPokecenter1FPrepareElite4Callback` (`5a:48b6`), a
`callback MAPCALLBACK_NEWMAP`. **This is the single most important script in
the section for a bot**: every time you set foot in the lobby it resets the
whole Elite Four run.
```
setmapscene WILLS_ROOM, SCENE_WILLSROOM_LOCK_DOOR
setmapscene KOGAS_ROOM, SCENE_KOGASROOM_LOCK_DOOR
setmapscene BRUNOS_ROOM, SCENE_BRUNOSROOM_LOCK_DOOR
setmapscene KARENS_ROOM, SCENE_KARENSROOM_LOCK_DOOR
setmapscene LANCES_ROOM, SCENE_LANCESROOM_LOCK_DOOR
setmapscene HALL_OF_FAME, SCENE_HALLOFFAME_ENTER
clearevent EVENT_{WILLS,KOGAS,BRUNOS,KARENS,LANCES}_ROOM_ENTRANCE_CLOSED
clearevent EVENT_{WILLS,KOGAS,BRUNOS,KARENS,LANCES}_ROOM_EXIT_OPEN
clearevent EVENT_BEAT_ELITE_4_WILL / _KOGA / _BRUNO / _KAREN
clearevent EVENT_BEAT_CHAMPION_LANCE
setevent EVENT_LANCES_ROOM_OAK_AND_MARY ; hide Oak + Mary again
endcallback
```
Consequences: the five `EVENT_BEAT_ELITE_4_*` flags are **not** durable
progress markers - they only survive within one uninterrupted run. The durable
"I beat the League" flag is `EVENT_BEAT_ELITE_FOUR`, set in `HallOfFameEnterScript`.
- `PlateauRivalBattle1` / `PlateauRivalBattle2` -> `PlateauRivalBattleCommon`.
Guards, in order:
1. `checkevent EVENT_BEAT_RIVAL_IN_MT_MOON` / `iffalse` -> done (so this only
ever fires post-game, after the Mt. Moon rematch),
2. `checkflag ENGINE_INDIGO_PLATEAU_RIVAL_FIGHT` / `iftrue` -> done (once a day),
3. `readvar VAR_WEEKDAY` with `ifequal SUNDAY / TUESDAY / THURSDAY / FRIDAY /
SATURDAY` -> done. **Only Monday and Wednesday.**
Then `moveobject`/`appear` the rival, `showemote EMOTE_SHOCK`,
`special FadeOutMusic`, a 5-step `applymovement`, `playmusic MUSIC_RIVAL_ENCOUNTER`,
`setevent EVENT_INDIGO_PLATEAU_POKECENTER_RIVAL`, then a starter-dependent
`loadtrainer RIVAL2, RIVAL2_2_*` + `startbattle`. Afterwards
`PlateauRivalPostBattle` plays `MUSIC_RIVAL_AFTER`, walks him off with
`disappear`, and `setflag ENGINE_INDIGO_PLATEAU_RIVAL_FIGHT`.
- `IndigoPlateauPokecenter1FNurseScript` -> `jumpstd PokecenterNurseScript` (free heal).
- `IndigoPlateauPokecenter1FClerkScript` -> `pokemart MARTTYPE_STANDARD, MART_INDIGO_PLATEAU`.
- `TeleportGuyScript` (`5a:49e5`): `yesorno`; on yes, `playsound SFX_WARP_TO`,
`special FadeOutToWhite`, `warp NEW_BARK_TOWN, 13, 6`. The walkthrough's "Abra
will teleport you back" lands you in **New Bark Town**, specifically, not
"back to Johto" generically.
- `AbraScript`: text + `cry ABRA`, nothing else.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_INDIGO_PLATEAU_POKECENTER_RIVAL` | `constants/event_flags.asm:1309` | set by `std_scripts.asm:556` on New Game and by `PlateauRivalBattleCommon`; cleared by `appear` | hides the rival object |
| `EVENT_TELEPORT_GUY` | `constants/event_flags.asm:1310` | set by `HallOfFameEnterScript` | after the Hall of Fame the old man **and** his Pokémon vanish; the ride home is a pre-champion service only |
| `EVENT_BEAT_RIVAL_IN_MT_MOON` | `constants/event_flags.asm:456` | read by both `PlateauRivalBattle*` | precondition for the lobby ambush |
| `ENGINE_INDIGO_PLATEAU_RIVAL_FIGHT` | `constants/engine_flags.asm:111` (in the `wDailyFlags2` block) | read/set by `PlateauRivalBattle*` | daily one-shot; cleared with the rest of the daily flags at midnight |
| `SCENE_INDIGOPLATEAUPOKECENTER1F_RIVAL_BATTLE` | the map's own `def_scene_scripts` (value 0) | coord events | see the quirk note below |
Scene quirk worth knowing: this map declares exactly one scene script, so
`SCENE_INDIGOPLATEAUPOKECENTER1F_RIVAL_BATTLE == 0`, which is also the default
value of `wMapScenes` for the map. `PlateauRivalPostBattle`'s
`setscene SCENE_INDIGOPLATEAUPOKECENTER1F_RIVAL_BATTLE` therefore writes 0 over
0 and the two coord events stay armed forever. The real one-shot guards are
`ENGINE_INDIGO_PLATEAU_RIVAL_FIGHT` and the weekday check.
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `ULTRA_BALL`, `MAX_REPEL`, `HYPER_POTION`, `MAX_POTION`, `FULL_RESTORE`, `REVIVE`, `FULL_HEAL` | bought | `MartIndigoPlateau` (`data/items/marts.asm:384`, symbol `05:645a`), opened by `IndigoPlateauPokecenter1FClerkScript` | n/a |
This is where the walkthrough's "ten Hyper Potions, ten Revives, ten Full Heals"
shopping list is actually satisfiable - the mart carries all three plus Full
Restore.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `RIVAL2` | `RIVAL2` (`$2a`) | `RIVAL2_2_CHIKORITA` / `RIVAL2_2_CYNDAQUIL` / `RIVAL2_2_TOTODILE` (chosen by which starter the player took: `checkevent EVENT_GOT_TOTODILE_FROM_ELM` -> Chikorita team, `EVENT_GOT_CHIKORITA_FROM_ELM` -> Cyndaquil team, else Totodile team) | `Rival2Group` entries 4/5/6 | `PlateauRivalBattleCommon` | daily, Mon/Wed only, post Mt. Moon |
`Rival2Group` (4)-(6), all `TRAINERTYPE_MOVES`:
`45 SNEASEL`, `48 CROBAT`, `45 MAGNETON`, `46 GENGAR`, `46 ALAKAZAM`, and then
`50 MEGANIUM` / `50 TYPHLOSION` / `50 FERALIGATR` respectively. The walkthrough
does not mention this fight at all - a bot that walks the lobby on a Monday
will hit it whether or not it planned to.
**Wild encounters** - none (indoor).
---
### MAP_WILLS_ROOM
- Script: `maps/WillsRoom.asm`
- Blocks: `maps/WillsRoom.blk`
- Header (`data/maps/maps.asm:351`): `map WillsRoom, TILESET_ELITE_FOUR_ROOM, INDOOR, LANDMARK_INDIGO_PLATEAU, MUSIC_INDIGO_PLATEAU, TRUE, PALETTE_DAY, FISHGROUP_SHORE` (the `TRUE` is the phone-service flag: **no phone calls** in any Elite Four room)
- Dimensions: `constants/map_constants.asm:324` -> `map_const WILLS_ROOM, 5, 9` (10 x 18 tiles)
- Attributes: `data/maps/attributes.asm:599`, border `$00`, no connections
`object_const_def`: `WILLSROOM_WILL` (emitted id 2).
Scene ids from the map's own `def_scene_scripts`:
`SCENE_WILLSROOM_LOCK_DOOR = 0`, `SCENE_WILLSROOM_NOOP = 1`.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 5 | 17 | `INDIGO_PLATEAU_POKECENTER_1F` | 4 |
| 2 | 4 | 2 | `KOGAS_ROOM` | 1 |
| 3 | 5 | 2 | `KOGAS_ROOM` | 2 |
**Coord events** - none. The door-lock cutscene is a **scene script**, not a
coord event: `scene_script WillsRoomLockDoorScene, SCENE_WILLSROOM_LOCK_DOOR`
fires on map load via `sdefer`.
**BG events** - none.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `WILLSROOM_WILL` | `SPRITE_WILL` (`PAL_NPC_RED`) | 5 | 7 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `WillScript_Battle` | -1 |
**Scripts of interest**
- `WillsRoomDoorLocksBehindYouScript` (`5a:4d8c`), deferred by the scene script:
`applymovement PLAYER, WillsRoom_EnterMovement` (4 x `step UP`, so you enter at
`(5, 17)` and end at `(5, 13)`), `reanchormap $85`, `playsound SFX_STRENGTH`,
`earthquake 80`, `changeblock 4, 14, $2a` (wall), `refreshmap`,
`setscene SCENE_WILLSROOM_NOOP`, `setevent EVENT_WILLS_ROOM_ENTRANCE_CLOSED`.
`changeblock` operands are in the same tile space as warps but are halved
internally (`Script_changeblock` adds 4 then `GetBlockLocation` shifts right,
`home/map.asm:2099`), so `4, 14` is the block covering tiles `(4..5, 14..15)` -
the corridor square immediately below where the walk-in leaves you. That is
what makes the walkthrough's "You can't go back to the Pokémon Center to heal
between battles" literally true.
- `WillsRoomDoorsCallback`, `callback MAPCALLBACK_TILES`: re-applies
`changeblock 4, 14, $2a` while `EVENT_WILLS_ROOM_ENTRANCE_CLOSED` is set and
`changeblock 4, 2, $16` (open door) while `EVENT_WILLS_ROOM_EXIT_OPEN` is set,
so the room's geometry survives a battle reload.
- `WillScript_Battle` (`5a:4da4`): `faceplayer`, `opentext`,
`checkevent EVENT_BEAT_ELITE_4_WILL` / `iftrue WillScript_AfterBattle`,
`writetext WillScript_WillBeforeText`, `winlosstext WillScript_WillBeatenText, 0`,
`loadtrainer WILL, WILL1`, `startbattle`, `reloadmapafterbattle`,
`setevent EVENT_BEAT_ELITE_4_WILL`, defeat text, `playsound SFX_ENTER_DOOR`,
`changeblock 4, 2, $16` (open the north door), `refreshmap`,
`setevent EVENT_WILLS_ROOM_EXIT_OPEN`.
Note there is **no `winlosstext` loss branch** (the second operand is `0`) and
no `BATTLETYPE_CANLOSE`: losing to any Elite Four member is an ordinary
blackout, which is why the FAQ says "If you lose, you have to start all over".
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_WILLS_ROOM_ENTRANCE_CLOSED` | `constants/event_flags.asm:440` | set by `WillsRoomDoorLocksBehindYouScript`, read by `WillsRoomDoorsCallback`, cleared by the lobby callback | south door sealed |
| `EVENT_WILLS_ROOM_EXIT_OPEN` | `constants/event_flags.asm:441` | set by `WillScript_Battle`, read by `WillsRoomDoorsCallback`, cleared by the lobby callback | north door open -> `(4, 2)` / `(5, 2)` are walkable warps |
| `EVENT_BEAT_ELITE_4_WILL` | `constants/event_flags.asm:980` | set by `WillScript_Battle`, cleared by the lobby callback | run-local "Will is done" |
| `SCENE_WILLSROOM_LOCK_DOOR` / `SCENE_WILLSROOM_NOOP` | this map's `def_scene_scripts` (0 / 1) | `setmapscene` from the lobby; `setscene` in the lock script | 0 = play the seal-in cutscene on entry |
**Items** - none.
**Trainers**
| const | class | id | party | script label | rematch/phone |
|---|---|---|---|---|---|
| `WILL1` | `WILL` (`$b`) | 1 | `WillGroup` (`data/trainers/parties.asm:197`, symbol `0e:5bf4`) | `WillScript_Battle` | none |
`WillGroup`, `TRAINERTYPE_MOVES`, in **send-out order**:
| # | lvl | species | moves |
|---|---|---|---|
| 1 | 40 | `XATU` | Quick Attack, Future Sight, Confuse Ray, Psychic |
| 2 | 41 | `JYNX` | DoubleSlap, Lovely Kiss, Ice Punch, Psychic |
| 3 | 41 | `EXEGGUTOR` | Reflect, Leech Seed, Egg Bomb, Psychic |
| 4 | 41 | `SLOWBRO` | Curse, Amnesia, Body Slam, Psychic |
| 5 | 42 | `XATU` | Quick Attack, Future Sight, Confuse Ray, Psychic |
Class attributes (`data/trainers/attributes.asm`, `; Will` block):
items `MAX_POTION, NO_ITEM`; base reward 25; DVs `atk 13 / def 12 / spd 13 / spc 13`
(`data/trainers/dvs.asm:15`). Money = base reward x level of the last enemy mon
(`ComputeTrainerReward`, `engine/battle/read_trainer_party.asm:300`) = **25 x 42 = 1050**.
**Wild encounters** - none.
---
### MAP_KOGAS_ROOM
- Script: `maps/KogasRoom.asm`
- Blocks: `maps/KogasRoom.blk`
- Header (`data/maps/maps.asm:352`): `map KogasRoom, TILESET_ELITE_FOUR_ROOM, INDOOR, LANDMARK_INDIGO_PLATEAU, MUSIC_INDIGO_PLATEAU, TRUE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:325` -> `map_const KOGAS_ROOM, 5, 9`
- Attributes: `data/maps/attributes.asm:600`, border `$00`
`object_const_def`: `KOGASROOM_KOGA`.
Scenes: `SCENE_KOGASROOM_LOCK_DOOR = 0`, `SCENE_KOGASROOM_NOOP = 1`.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 17 | `WILLS_ROOM` | 2 |
| 2 | 5 | 17 | `WILLS_ROOM` | 3 |
| 3 | 4 | 2 | `BRUNOS_ROOM` | 1 |
| 4 | 5 | 2 | `BRUNOS_ROOM` | 2 |
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `KOGASROOM_KOGA` | `SPRITE_KOGA` (`PAL_NPC_BLUE`) | 5 | 7 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `KogaScript_Battle` | -1 |
**Scripts of interest**
Structurally identical to Will's room: `KogasRoomDoorLocksBehindYouScript`
(4 x `step UP`, `changeblock 4, 14, $2a`, `setevent EVENT_KOGAS_ROOM_ENTRANCE_CLOSED`,
`setscene SCENE_KOGASROOM_NOOP`), `KogasRoomDoorsCallback` (`MAPCALLBACK_TILES`),
and `KogaScript_Battle` (`5a:5006`) -> `loadtrainer KOGA, KOGA1` /
`setevent EVENT_BEAT_ELITE_4_KOGA` / `changeblock 4, 2, $16` /
`setevent EVENT_KOGAS_ROOM_EXIT_OPEN`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_KOGAS_ROOM_ENTRANCE_CLOSED` | `constants/event_flags.asm:442` | lock script / tiles callback | south door sealed |
| `EVENT_KOGAS_ROOM_EXIT_OPEN` | `constants/event_flags.asm:443` | `KogaScript_Battle` / tiles callback | north door open |
| `EVENT_BEAT_ELITE_4_KOGA` | `constants/event_flags.asm:981` | `KogaScript_Battle` | run-local |
**Trainers**
| const | class | id | party | script label | rematch/phone |
|---|---|---|---|---|---|
| `KOGA1` | `KOGA` (`$f`) | 1 | `KogaGroup` (`data/trainers/parties.asm:249`, symbol `0e:5c89`) | `KogaScript_Battle` | none |
| # | lvl | species | moves |
|---|---|---|---|
| 1 | 40 | `ARIADOS` | Double Team, Spider Web, Baton Pass, Giga Drain |
| 2 | 41 | `VENOMOTH` | Supersonic, Gust, Psychic, Toxic |
| 3 | 43 | `FORRETRESS` | Protect, Swift, Explosion, Spikes |
| 4 | 42 | `MUK` | Minimize, Acid Armor, Sludge Bomb, Toxic |
| 5 | 44 | `CROBAT` | Double Team, Quick Attack, Wing Attack, Toxic |
Class attributes (`; Koga`): items `FULL_HEAL, FULL_RESTORE` (the walkthrough's
"three Full Restores" is HGSS; in GS the AI holds **one** Full Heal and **one**
Full Restore per class-attribute slot); base reward 25; DVs `13/12/13/13`
(`data/trainers/dvs.asm:19`). Money = 25 x 44 = **1100**.
**Wild encounters** - none.
---
### MAP_BRUNOS_ROOM
- Script: `maps/BrunosRoom.asm`
- Blocks: `maps/BrunosRoom.blk`
- Header (`data/maps/maps.asm:353`): `map BrunosRoom, TILESET_ELITE_FOUR_ROOM, INDOOR, LANDMARK_INDIGO_PLATEAU, MUSIC_INDIGO_PLATEAU, TRUE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:326` -> `map_const BRUNOS_ROOM, 5, 9`
- Attributes: `data/maps/attributes.asm:601`, border `$00`
`object_const_def`: `BRUNOSROOM_BRUNO`.
Scenes: `SCENE_BRUNOSROOM_LOCK_DOOR = 0`, `SCENE_BRUNOSROOM_NOOP = 1`.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 17 | `KOGAS_ROOM` | 3 |
| 2 | 5 | 17 | `KOGAS_ROOM` | 4 |
| 3 | 4 | 2 | `KARENS_ROOM` | 1 |
| 4 | 5 | 2 | `KARENS_ROOM` | 2 |
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `BRUNOSROOM_BRUNO` | `SPRITE_BRUNO` (`PAL_NPC_BROWN`) | 5 | 7 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `BrunoScript_Battle` | -1 |
**Scripts of interest** - `BrunosRoomDoorLocksBehindYouScript`,
`BrunosRoomDoorsCallback`, `BrunoScript_Battle` (`5a:5271`); same shape as Will's
room, with `loadtrainer BRUNO, BRUNO1` and
`setevent EVENT_BEAT_ELITE_4_BRUNO` / `EVENT_BRUNOS_ROOM_EXIT_OPEN`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BRUNOS_ROOM_ENTRANCE_CLOSED` | `constants/event_flags.asm:444` | lock script / tiles callback | south door sealed |
| `EVENT_BRUNOS_ROOM_EXIT_OPEN` | `constants/event_flags.asm:445` | `BrunoScript_Battle` / tiles callback | north door open |
| `EVENT_BEAT_ELITE_4_BRUNO` | `constants/event_flags.asm:982` | `BrunoScript_Battle` | run-local |
**Trainers**
| const | class | id | party | script label | rematch/phone |
|---|---|---|---|---|---|
| `BRUNO1` | `BRUNO` (`$d`) | 1 | `BrunoGroup` (`data/trainers/parties.asm:229`, symbol `0e:5c3d`) | `BrunoScript_Battle` | none |
| # | lvl | species | moves |
|---|---|---|---|
| 1 | 42 | `HITMONTOP` | Pursuit, Quick Attack, Dig, Detect |
| 2 | 42 | `HITMONLEE` | Swagger, Double Kick, Hi Jump Kick, Foresight |
| 3 | 42 | `HITMONCHAN` | ThunderPunch, Ice Punch, Fire Punch, Mach Punch |
| 4 | 43 | `ONIX` | Bind, Earthquake, Sandstorm, Rock Slide |
| 5 | 46 | `MACHAMP` | Rock Slide, Foresight, Vital Throw, Cross Chop |
Class attributes (`; Bruno`): items `MAX_POTION, NO_ITEM`; base reward 25; DVs
`13/12/13/13` (`data/trainers/dvs.asm:17`). Money = 25 x 46 = **1150**.
**Wild encounters** - none.
---
### MAP_KARENS_ROOM
- Script: `maps/KarensRoom.asm`
- Blocks: `maps/KarensRoom.blk`
- Header (`data/maps/maps.asm:354`): `map KarensRoom, TILESET_ELITE_FOUR_ROOM, INDOOR, LANDMARK_INDIGO_PLATEAU, MUSIC_INDIGO_PLATEAU, TRUE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:327` -> `map_const KARENS_ROOM, 5, 9`
- Attributes: `data/maps/attributes.asm:602`, border `$00`
`object_const_def`: `KARENSROOM_KAREN`.
Scenes: `SCENE_KARENSROOM_LOCK_DOOR = 0`, `SCENE_KARENSROOM_NOOP = 1`.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 17 | `BRUNOS_ROOM` | 3 |
| 2 | 5 | 17 | `BRUNOS_ROOM` | 4 |
| 3 | 4 | 2 | `LANCES_ROOM` | 1 |
| 4 | 5 | 2 | `LANCES_ROOM` | 2 |
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `KARENSROOM_KAREN` | `SPRITE_KAREN` (`PAL_NPC_RED`) | 5 | 7 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `KarenScript_Battle` | -1 |
**Scripts of interest** - `KarensRoomDoorLocksBehindYouScript`,
`KarensRoomDoorsCallback`, `KarenScript_Battle` (`5a:549a`) with
`loadtrainer KAREN, KAREN1`, `setevent EVENT_BEAT_ELITE_4_KAREN`,
`changeblock 4, 2, $16`, `setevent EVENT_KARENS_ROOM_EXIT_OPEN`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_KARENS_ROOM_ENTRANCE_CLOSED` | `constants/event_flags.asm:446` | lock script / tiles callback | south door sealed |
| `EVENT_KARENS_ROOM_EXIT_OPEN` | `constants/event_flags.asm:447` | `KarenScript_Battle` / tiles callback | north door open |
| `EVENT_BEAT_ELITE_4_KAREN` | `constants/event_flags.asm:983` | `KarenScript_Battle` | run-local |
**Trainers**
| const | class | id | party | script label | rematch/phone |
|---|---|---|---|---|---|
| `KAREN1` | `KAREN` (`$e`) | 1 | `KarenGroup` (`data/trainers/parties.asm:239`, symbol `0e:5c63`) | `KarenScript_Battle` | none |
| # | lvl | species | moves |
|---|---|---|---|
| 1 | 42 | `UMBREON` | Sand-Attack, Confuse Ray, Faint Attack, Mean Look |
| 2 | 42 | `VILEPLUME` | Stun Spore, Acid, Moonlight, Petal Dance |
| 3 | 45 | `GENGAR` | Lick, Spite, Curse, Destiny Bond |
| 4 | 44 | `MURKROW` | Quick Attack, Whirlwind, Pursuit, Faint Attack |
| 5 | 47 | `HOUNDOOM` | Roar, Pursuit, Flamethrower, Crunch |
Class attributes (`; Karen`): items `FULL_HEAL, MAX_POTION` - **no Full Restore**,
contra the walkthrough; base reward 25; DVs `atk 7 / def 15 / spd 13 / spc 15`
(`data/trainers/dvs.asm:18`, the only Elite Four member with a different DV row).
Money = 25 x 47 = **1175**.
**Wild encounters** - none.
---
### MAP_LANCES_ROOM
- Script: `maps/LancesRoom.asm`
- Blocks: `maps/LancesRoom.blk`
- Header (`data/maps/maps.asm:355`): `map LancesRoom, TILESET_CHAMPIONS_ROOM, INDOOR, LANDMARK_INDIGO_PLATEAU, MUSIC_INDIGO_PLATEAU, TRUE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:328` -> `map_const LANCES_ROOM, 5, 12` (10 x 24 tiles - the tallest room in the run)
- Attributes: `data/maps/attributes.asm:603`, border `$00`
`object_const_def`: `LANCESROOM_LANCE`, `LANCESROOM_MARY`, `LANCESROOM_OAK`.
Scenes: `SCENE_LANCESROOM_LOCK_DOOR = 0`, `SCENE_LANCESROOM_APPROACH_LANCE = 1`.
Note this map's second scene is **not** a noop-by-name: it is the state in which
the two coord events are live.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 23 | `KARENS_ROOM` | 3 |
| 2 | 5 | 23 | `KARENS_ROOM` | 4 |
| 3 | 4 | 0 | `HALL_OF_FAME` | 1 |
| 4 | 5 | 0 | `HALL_OF_FAME` | 2 |
**Coord events** (`def_coord_events`)
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_LANCESROOM_APPROACH_LANCE` (= 1) | 4 | 5 | `Script_ApproachLanceFromLeft` (`5a:5716`) | `FadeOutMusic`, walk `UP UP UP` + face right, fall into `LancesRoomLanceScript` |
| `SCENE_LANCESROOM_APPROACH_LANCE` (= 1) | 5 | 5 | `Script_ApproachLanceFromRight` (`5a:5720`) | `FadeOutMusic`, walk `UP LEFT UP UP` + face right, then the same |
**BG events** - none.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `LANCESROOM_LANCE` | `SPRITE_LANCE` | 5 | 2 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `LancesRoomLanceScript` | -1 |
| `LANCESROOM_MARY` | `SPRITE_TEACHER` (`PAL_NPC_GREEN`) | 4 | 7 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_SCRIPT` | `ObjectEvent` | `EVENT_LANCES_ROOM_OAK_AND_MARY` |
| `LANCESROOM_OAK` | `SPRITE_OAK` | 4 | 7 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_SCRIPT` | `ObjectEvent` | `EVENT_LANCES_ROOM_OAK_AND_MARY` |
Mary and Oak share the same spawn tile `(4, 7)` and the same hide flag; both are
`appear`ed one at a time during the cutscene.
**Scripts of interest**
- `LancesRoomDoorLocksBehindYouScript` (`5a:56ff`): 4 x `step UP` from
`(4|5, 23)` to y = 19, `reanchormap $85`, `SFX_STRENGTH`, `earthquake 80`,
`changeblock 4, 22, $34` (wall - the champion's-room tileset uses a different
wall block id than the Elite Four rooms' `$2a`), `refreshmap`,
`setscene SCENE_LANCESROOM_APPROACH_LANCE`,
`setevent EVENT_LANCES_ROOM_ENTRANCE_CLOSED`.
- `LancesRoomLanceScript` (`5a:5727`), the section's climax:
`turnobject LANCESROOM_LANCE, LEFT`, intro text,
`winlosstext LanceBattleWinText, 0`, `setlasttalked LANCESROOM_LANCE`,
`loadtrainer CHAMPION, LANCE`, `startbattle`, `dontrestartmapmusic`,
`reloadmapafterbattle`, `setevent EVENT_BEAT_CHAMPION_LANCE`, after-battle
text, `SFX_ENTER_DOOR`, `changeblock 4, 0, $0b` (open the north door),
`setevent EVENT_LANCES_ROOM_ENTRANCE_CLOSED` (again - the script re-sets the
entrance flag here, it does not clear it), then
`musicfadeout MUSIC_BEAUTY_ENCOUNTER, 16` and the cutscene:
`appear LANCESROOM_MARY` -> `LancesRoomMovementData_MaryRushesIn` ->
`appear LANCESROOM_OAK` -> `LancesRoomMovementData_OakWalksIn` ->
`follow`/`stopfollow` pairs -> Mary's interview attempt -> Lance leads the
player north (`follow LANCESROOM_LANCE, PLAYER`,
`LancesRoomMovementData_LanceLeadsPlayerToHallOfFame`), `disappear` both, then
`special FadeOutToWhite` and `warp HALL_OF_FAME, 4, 13`.
This is the only exit that matters: warps 3/4 exist but the script drives the
transition itself.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_LANCES_ROOM_ENTRANCE_CLOSED` | `constants/event_flags.asm:448` | lock script + `LancesRoomLanceScript`; read by `LancesRoomDoorsCallback` | south door sealed |
| `EVENT_LANCES_ROOM_EXIT_OPEN` | `constants/event_flags.asm:449` | read by `LancesRoomDoorsCallback`; cleared by the lobby callback | **never set by this map** - the north door is opened by a direct `changeblock 4, 0, $0b` in the battle script, and the flag only matters if you re-enter the map |
| `EVENT_BEAT_CHAMPION_LANCE` | `constants/event_flags.asm:984` | set by `LancesRoomLanceScript`, cleared by the lobby callback | run-local |
| `EVENT_LANCES_ROOM_OAK_AND_MARY` | `constants/event_flags.asm:1281` | set on New Game (`std_scripts.asm:523`) and by the lobby callback; cleared by `appear` during the cutscene | hides Mary and Oak until the champion falls |
| `SCENE_LANCESROOM_APPROACH_LANCE` | this map's `def_scene_scripts` (= 1) | `setscene` at the end of the lock script | arms the two coord events at y = 5 |
**Items** - none.
**Trainers**
| const | class | id | party | script label | rematch/phone |
|---|---|---|---|---|---|
| `LANCE` | `CHAMPION` (`$10`) | 1 | `ChampionGroup` (`data/trainers/parties.asm:259`, symbol `0e:5cae`) | `LancesRoomLanceScript` | none |
| # | lvl | species | moves |
|---|---|---|---|
| 1 | 44 | `GYARADOS` | Flail, Rain Dance, Surf, Hyper Beam |
| 2 | 47 | `DRAGONITE` | Thunder Wave, Twister, Thunder, Hyper Beam |
| 3 | 47 | `DRAGONITE` | Thunder Wave, Twister, Blizzard, Hyper Beam |
| 4 | 46 | `AERODACTYL` | Wing Attack, AncientPower, Rock Slide, Hyper Beam |
| 5 | 46 | `CHARIZARD` | Flamethrower, Wing Attack, Slash, Hyper Beam |
| 6 | 50 | `DRAGONITE` | Fire Blast, Safeguard, Outrage, Hyper Beam |
Class attributes (`; Champion`): items `FULL_HEAL, FULL_RESTORE`; base reward 25;
DVs `13/12/13/13` (`data/trainers/dvs.asm:20`). Money = 25 x 50 = **1250**.
`data/trainers/leaders.asm:20` lists `CHAMPION`, but the file's own comment notes
`CHAMPION` and `RED` are unused for the leader battle-music check.
**Wild encounters** - none.
---
### MAP_HALL_OF_FAME
- Script: `maps/HallOfFame.asm`
- Blocks: `maps/HallOfFame.blk`
- Header (`data/maps/maps.asm:356`): `map HallOfFame, TILESET_ICE_PATH, INDOOR, LANDMARK_INDIGO_PLATEAU, MUSIC_NEW_BARK_TOWN, TRUE, PALETTE_DAY, FISHGROUP_SHORE` (yes: `TILESET_ICE_PATH` and the New Bark Town theme)
- Dimensions: `constants/map_constants.asm:329` -> `map_const HALL_OF_FAME, 5, 7` (10 x 14 tiles)
- Attributes: `data/maps/attributes.asm:604`, border `$00`
`object_const_def`: `HALLOFFAME_LANCE`.
Scenes: `SCENE_HALLOFFAME_ENTER = 0`, `SCENE_HALLOFFAME_NOOP = 1`.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 13 | `LANCES_ROOM` | 3 |
| 2 | 5 | 13 | `LANCES_ROOM` | 4 |
**Coord events** - none (the induction is a scene script). **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `HALLOFFAME_LANCE` | `SPRITE_LANCE` | 4 | 12 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_SCRIPT` | `ObjectEvent` | -1 |
**Scripts of interest**
- `HallOfFameEnterScript` (`5a:5cff`), deferred from
`scene_script HallOfFameEnterScene, SCENE_HALLOFFAME_ENTER`. In order:
`follow HALLOFFAME_LANCE, PLAYER`,
`applymovement HALLOFFAME_LANCE, HallOfFame_WalkUpWithLance` (8 x `step UP`,
1 x `step RIGHT`, `turn_head LEFT`), `stopfollow`, Lance's speech,
`applymovement PLAYER, HallOfFame_SlowlyApproachMachine` (one `slow_step UP`),
`setscene SCENE_HALLOFFAME_NOOP`, then the state writes:
```
setval HEALMACHINE_HALL_OF_FAME ; constants/script_constants.asm:295
special HealMachineAnim
setevent EVENT_BEAT_ELITE_FOUR ; constants/event_flags.asm:77
setevent EVENT_TELEPORT_GUY ; removes the Abra ride home
setevent EVENT_RIVAL_SPROUT_TOWER
clearevent EVENT_RED_IN_MT_SILVER ; Red becomes available later
setevent EVENT_OLIVINE_PORT_SPRITES_BEFORE_HALL_OF_FAME
clearevent EVENT_OLIVINE_PORT_SPRITES_AFTER_HALL_OF_FAME
setmapscene SPROUT_TOWER_3F, SCENE_SPROUTTOWER3F_NOOP
special HealParty
checkevent EVENT_GOT_SS_TICKET_FROM_ELM / iftrue .SkipPhoneCall
specialphonecall SPECIALCALL_SSTICKET ; constants/phone_constants.asm:49
halloffame
```
So the S.S. Ticket call from Elm is queued **here**, and the Kanto half of the
game is switched on by this one script.
- `halloffame` ($9f) -> `Script_halloffame` (`engine/overworld/scripting.asm:2207`)
-> `HallOfFame` (`21:640a`, `engine/events/halloffame.asm:3`):
fade out music, `wSpawnAfterChampion = SPAWN_LANCE`,
`set STATUSFLAGS_HALL_OF_FAME_F`, bump `wHallOfFameCount` (capped at
`HOF_MASTER_COUNT = 200`), `farcall SaveGameData`, `GetHallOfFameParty`,
`AddHallOfFameEntry`, `AnimateHallOfFame`, then `jp Credits` with the
**pre-set** copy of `wStatusFlags` (which is why a first-time champion cannot
hold B to skip the roll).
- After the credits, `ReturnFromCredits` (`engine/overworld/scripting.asm:2216`)
ends the script and returns `MAPSTATUS_DONE`; the game goes to the title
screen. On the next CONTINUE, `engine/menus/intro_menu.asm:296` `.SpawnAfterE4`
sets `wDefaultSpawnpoint = SPAWN_NEW_BARK` and enters via `MAPSETUP_WARP` -
the walkthrough's "the game isn't actually over".
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_ELITE_FOUR` | `constants/event_flags.asm:77` | set by `HallOfFameEnterScript` | the durable "League cleared" flag; the five `EVENT_BEAT_ELITE_4_*` flags are wiped by the lobby callback and must not be used for this |
| `EVENT_TELEPORT_GUY` | `constants/event_flags.asm:1310` | set here | Abra ride home disappears |
| `EVENT_RED_IN_MT_SILVER` | `constants/event_flags.asm:1284` | set on New Game (`std_scripts.asm:526`), **cleared** here | Red can now exist at Mt. Silver once the rest of his gate is met |
| `EVENT_RIVAL_SPROUT_TOWER` / `SCENE_SPROUTTOWER3F_NOOP` | `constants/event_flags.asm:1126` / Sprout Tower 3F's scenes | set here | post-game Sprout Tower state |
| `EVENT_OLIVINE_PORT_SPRITES_BEFORE_HALL_OF_FAME` / `_AFTER_HALL_OF_FAME` | `constants/event_flags.asm:1241-1242` | flipped here | Olivine Port repopulates for the Fast Ship |
| `EVENT_GOT_SS_TICKET_FROM_ELM` | `constants/event_flags.asm:45` | read here | if unset, `specialphonecall SPECIALCALL_SSTICKET` queues Elm's call (`engine/phone/scripts/elm.asm:69`) |
| `STATUSFLAGS_HALL_OF_FAME_F` | `constants/ram_constants.asm` | set by `HallOfFame::` | unlocks the Kanto Pokégear map, gates credit skipping |
| `SCENE_HALLOFFAME_ENTER` / `_NOOP` | this map's `def_scene_scripts` (0 / 1) | `setmapscene` from the lobby callback; `setscene` mid-script | 0 = play the induction |
**Items** - none. **Trainers** - none. **Wild encounters** - none.
---
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Entry to Victory Road (and therefore to this whole section) | `maps/VictoryRoadGate.asm:_VictoryRoadGateBadgeCheckScript` - `readvar VAR_BADGES` / `ifgreater NUM_JOHTO_BADGES - 1, .AllEightBadges` | all 8 Johto badges | `.AllEightBadges` does `setscene SCENE_VICTORYROADGATE_NOOP`; otherwise you get `applymovement PLAYER, VictoryRoadGateStepDownMovement` and are pushed back. **This gate belongs to the previous section**, listed here only because it is the precondition for everything below |
| Fly to Indigo Plateau | `maps/Route23.asm:Route23FlypointCallback` | one `MAPCALLBACK_NEWMAP` on Route 23 | `setflag ENGINE_FLYPOINT_INDIGO_PLATEAU` |
| No retreat once inside an Elite Four room | `maps/WillsRoom.asm:WillsRoomDoorLocksBehindYouScript` (and the Koga/Bruno/Karen/Lance twins) - `changeblock 4, 14, $2a` (Lance: `changeblock 4, 22, $34`) plus `EVENT_*_ROOM_ENTRANCE_CLOSED` re-applied by each map's `MAPCALLBACK_TILES` | none - it is unconditional on entry | never, within a run. The lobby's `MAPCALLBACK_NEWMAP` clears the flags, but you can only reach the lobby by finishing or blacking out |
| Each room's north door | `WillScript_Battle` / `KogaScript_Battle` / `BrunoScript_Battle` / `KarenScript_Battle` - `changeblock 4, 2, $16` + `setevent EVENT_*_ROOM_EXIT_OPEN`, replayed by the map's `MAPCALLBACK_TILES` | beat that member | door block becomes `$16` (open) and warps at `(4, 2)`/`(5, 2)` become reachable |
| Lance's north door | `maps/LancesRoom.asm:LancesRoomLanceScript` - `changeblock 4, 0, $0b` | beat Lance | opened, but the script warps you through immediately anyway |
| Approaching Lance at all | `def_coord_events` at `(4, 5)` / `(5, 5)` gated on `SCENE_LANCESROOM_APPROACH_LANCE` | that scene is only set at the end of `LancesRoomDoorLocksBehindYouScript` | walking in through warp 1/2 always sets it; a bot teleported straight to y < 5 would skip the fight |
| Lobby rival ambush | `maps/IndigoPlateauPokecenter1F.asm:PlateauRivalBattle1/2` | `EVENT_BEAT_RIVAL_IN_MT_MOON` set, `ENGINE_INDIGO_PLATEAU_RIVAL_FIGHT` clear, `VAR_WEEKDAY` in {MONDAY, WEDNESDAY} | not a progress gate - a hazard. Avoid tiles `(16, 4)` / `(17, 4)` if the run cannot afford a level-45-50 six-mon fight |
| No HM field move is required anywhere in this section | grep: no `Route23`/Elite Four map references any `engine/overworld/` field-move script; the walkthrough's "you do NOT need to have every HM" is correct | - | - |
---
## 4. Bot checklist
Coordinates are the raw asm map coordinates (tile grid, origin top-left, before
the +4 border offset the object/warp readers apply).
| # | Map | Target | Intent | Precondition | Postcondition |
|---|---|---|---|---|---|
| 1 | `VICTORY_ROAD` | warp at `(13, 5)` | walk onto warp | 8 Johto badges (already spent at `VictoryRoadGate`) | on `ROUTE_23` at warp 3 `(9, 13)` |
| 2 | `ROUTE_23` | any tile (map load) | step | - | `ENGINE_FLYPOINT_INDIGO_PLATEAU` set |
| 3 | `ROUTE_23` | `(9, 5)` or `(10, 5)` | walk onto warp | - | in `INDIGO_PLATEAU_POKECENTER_1F` at `(5, 13)`/`(6, 13)`; the `MAPCALLBACK_NEWMAP` resets all Elite Four scenes/flags |
| 4 | `INDIGO_PLATEAU_POKECENTER_1F` | `INDIGOPLATEAUPOKECENTER1F_NURSE` at `(3, 7)` | talk (A, facing up from `(3, 8)`) | - | party healed (`jumpstd PokecenterNurseScript`) |
| 5 | `INDIGO_PLATEAU_POKECENTER_1F` | `INDIGOPLATEAUPOKECENTER1F_CLERK` at `(11, 7)` | talk, buy | money | stock Hyper Potion / Full Heal / Revive / Full Restore from `MartIndigoPlateau` |
| 6 | `INDIGO_PLATEAU_POKECENTER_1F` | avoid `(16, 4)` and `(17, 4)` | pathing constraint | only matters if `EVENT_BEAT_RIVAL_IN_MT_MOON` is set and today is Mon/Wed | skipping avoids `RIVAL2_2_*` |
| 7 | `INDIGO_PLATEAU_POKECENTER_1F` | warp at `(14, 3)` | walk onto warp | - | in `WILLS_ROOM` at `(5, 17)`; scene 0 fires `WillsRoomDoorLocksBehindYouScript` |
| 8 | `WILLS_ROOM` | (automatic) | wait out `applymovement` | - | player at `(5, 13)`, `EVENT_WILLS_ROOM_ENTRANCE_CLOSED` set, scene = 1 |
| 9 | `WILLS_ROOM` | `WILLSROOM_WILL` at `(5, 7)` | walk to `(5, 8)`, face up, A | - | win -> `EVENT_BEAT_ELITE_4_WILL`, `EVENT_WILLS_ROOM_EXIT_OPEN` |
| 10 | `WILLS_ROOM` | `(4, 2)` or `(5, 2)` | walk onto warp | `EVENT_WILLS_ROOM_EXIT_OPEN` | in `KOGAS_ROOM` at `(4, 17)`/`(5, 17)` |
| 11 | `KOGAS_ROOM` | `KOGASROOM_KOGA` at `(5, 7)` | walk to `(5, 8)`, face up, A | door-lock cutscene done (player at `(4|5, 13)`) | `EVENT_BEAT_ELITE_4_KOGA`, `EVENT_KOGAS_ROOM_EXIT_OPEN` |
| 12 | `KOGAS_ROOM` | `(4, 2)` / `(5, 2)` | walk onto warp | exit open | in `BRUNOS_ROOM` at `(4, 17)`/`(5, 17)` |
| 13 | `BRUNOS_ROOM` | `BRUNOSROOM_BRUNO` at `(5, 7)` | talk | cutscene done | `EVENT_BEAT_ELITE_4_BRUNO`, `EVENT_BRUNOS_ROOM_EXIT_OPEN` |
| 14 | `BRUNOS_ROOM` | `(4, 2)` / `(5, 2)` | walk onto warp | exit open | in `KARENS_ROOM` at `(4, 17)`/`(5, 17)` |
| 15 | `KARENS_ROOM` | `KARENSROOM_KAREN` at `(5, 7)` | talk | cutscene done | `EVENT_BEAT_ELITE_4_KAREN`, `EVENT_KARENS_ROOM_EXIT_OPEN` |
| 16 | `KARENS_ROOM` | `(4, 2)` / `(5, 2)` | walk onto warp | exit open | in `LANCES_ROOM` at `(4, 23)`/`(5, 23)` |
| 17 | `LANCES_ROOM` | (automatic) | wait out `applymovement` | - | player at `(4|5, 19)`, `EVENT_LANCES_ROOM_ENTRANCE_CLOSED` set, scene = `SCENE_LANCESROOM_APPROACH_LANCE` |
| 18 | `LANCES_ROOM` | walk to `(4, 5)` (or `(5, 5)`) | walk onto coord event | scene = 1 | `Script_ApproachLanceFromLeft/Right` -> Champion battle vs `CHAMPION`/`LANCE` |
| 19 | `LANCES_ROOM` | (automatic) | hold A through the Mary / Oak cutscene | won | `EVENT_BEAT_CHAMPION_LANCE`, then `warp HALL_OF_FAME, 4, 13` |
| 20 | `HALL_OF_FAME` | (automatic) | hold A through Lance's speech | scene = `SCENE_HALLOFFAME_ENTER` | `EVENT_BEAT_ELITE_FOUR`, `EVENT_TELEPORT_GUY`, S.S. Ticket call queued if needed, `halloffame` runs |
| 21 | (screens) | Hall of Fame roster -> credits -> "The End" | wait; B skips **only** if `STATUSFLAGS_HALL_OF_FAME_F` was already set before this induction | - | back to title; save has been written by `SaveGameData` inside the ceremony |
| 22 | (title) | CONTINUE | select | `wSpawnAfterChampion == SPAWN_LANCE` | game resumes at `SPAWN_NEW_BARK`, not at Indigo Plateau |
Bot notes:
- Do **not** re-enter `INDIGO_PLATEAU_POKECENTER_1F` mid-run to heal. You cannot
(the doors are sealed), and if some other path got you there, its
`MAPCALLBACK_NEWMAP` wipes `EVENT_BEAT_ELITE_4_*` and re-arms all five
door-lock scenes, restarting the gauntlet from Will.
- Every fight uses `winlosstext <win>, 0` - no loss branch, no
`BATTLETYPE_CANLOSE` - so a loss is a full blackout to the last Pokémon Center.
- After each `startbattle` the scripts run `reloadmapafterbattle`, which re-runs
the map's `MAPCALLBACK_TILES` and therefore restores both the sealed south door
and (once its flag is set) the opened north door.
---
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Map/warp/coord/object tables for all 8 maps | extracted generically by `src/import/RomExtractorGen2.lua` into the Gold cache and read by `src/world/gen2/World.lua` + `src/world/gen2/Map.lua` | implemented (data-driven; nothing map-specific is hand-written) |
| `MAPCALLBACK_NEWMAP` / `MAPCALLBACK_TILES` dispatch (the lobby reset, the door callbacks) | `src/world/gen2/World.lua`, asserted end-to-end by `tests/drivers/gold_map_callbacks.lua` | implemented |
| `changeblock` / `refreshmap` door swaps | `src/world/gen2/World.lua:1454` (`World:changeBlock`), `src/script/gen2/Opcodes.lua` | implemented |
| `reanchormap` | `src/script/gen2/Vm.lua:166` - grouped with `opentext`/`closetext` as a no-op | partial (the camera re-anchor the cart does after the wall drops is not reproduced; cosmetic, but the seal-in frame may differ) |
| `earthquake 80` shake | `src/script/gen2/Vm.lua:1044` -> `earthquakeFn`, `World:earthquake` (`src/world/gen2/World.lua:1537`) | implemented |
| Scene scripts / `sdefer` (all five door locks, the Hall of Fame induction) | `src/script/gen2/Vm.lua:92` - `sdefer` is executed **immediately** rather than deferred until the map settles | partial (ordering deviation, called out in the source comment) |
| `setmapscene` / `setscene` | `src/script/gen2/Opcodes.lua`, `World:mapSceneOf` (`src/world/gen2/World.lua:1179`) | implemented |
| `loadtrainer` / `startbattle` / `winlosstext` / `reloadmapafterbattle` | `src/script/gen2/Vm.lua`, `src/battle/gen2/Battle.lua`, `src/world/gen2/Trainers.lua` | implemented |
| Trainer party data (`WillGroup``ChampionGroup`), class DVs, class items | extracted by `src/import/RomExtractorGen2.lua` (`TrainerGroups`, ~line 3871) | implemented (data-driven) |
| AI item use (Koga/Lance Full Restore, Will/Bruno Max Potion, Karen Full Heal) | `src/battle/gen2/Ai.lua:1369-1375`, `src/battle/gen2/Battle.lua:1747` | implemented |
| Prize money `base x last level` | `src/battle/gen2/Prize.lua` (`Prize.rewardLevel`), used at `src/battle/gen2/Battle.lua:1438` | implemented |
| `pokemart MARTTYPE_STANDARD, MART_INDIGO_PLATEAU` | `src/script/gen2/Vm.lua:597`, `src/ui/gen2/MartMenu.lua`, mart tables extracted at `src/import/RomExtractorGen2.lua:2582` | implemented |
| `jumpstd PokecenterNurseScript` (lobby heal) | `src/script/gen2/Vm.lua` `jumpstd` + std-script table | implemented |
| `TeleportGuyScript` warp to New Bark Town, `AbraScript` `cry ABRA` | generic `warp`/`yesorno`/`cry` opcodes | implemented (data-driven) |
| `halloffame` opcode -> induction bookkeeping | `src/script/gen2/Opcodes.lua:171`, `src/script/gen2/Vm.lua:1382`, `src/world/gen2/World.lua:2374` (`World:hallOfFame`), `src/core/gen2/HallOfFame.lua` (`induct`, `bumpCount`, `buildParty`) | implemented |
| Hall of Fame roster screen + credits, first-time-champion skip lockout | `src/ui/gen2/HallOfFame.lua`, `src/ui/gen2/Credits.lua`; eyeball driver `tests/drivers/gold_halloffame_shots.lua` | implemented |
| `wSpawnAfterChampion` -> post-credits respawn at `SPAWN_NEW_BARK` | `src/core/gen2/HallOfFame.lua:51` (`POST_CREDITS_SPAWN`) and `:254` (`consumePostGameSpawn`) | **partial** - the logic exists and is unit-tested (`tests/gen2_halloffame_test.lua:164`), but grepping `src/` finds **no caller**: nothing on the CONTINUE path consumes it, so a port save resumes where it stood rather than at New Bark Town |
| `specialphonecall SPECIALCALL_SSTICKET` from the induction | `src/script/gen2/Vm.lua` (`specialphonecall`), `src/core/gen2/Phone.lua:394` (`SPECIALCALL_SSTICKET`, id 5) | implemented |
| `ENGINE_FLYPOINT_INDIGO_PLATEAU` -> Fly menu + Kanto map unlock | `src/world/gen2/FieldMoves.lua:368` (`SPAWN_INDIGO`, flag 63), `:414` (Kanto half gate) | implemented |
| `ENGINE_INDIGO_PLATEAU_RIVAL_FIGHT` daily reset | `src/core/gen2/Apricorns.lua:88` (`DAILY_ENGINE_FLAGS`, id 92) | implemented |
| A Gold-side driver that actually walks Route 23 -> Hall of Fame | none found (`tests/drivers/gold_*.lua` has boot/walk/battle/HOF-screen drivers but no League run; `tests/drivers/bot_route.lua` and `tests/parity_lance.lua` are the **Gen 1** Kanto Elite Four) | missing |
---
## 6. Unresolved / verify by hand
1. **"Route 32 is very short, and you'll immediately be at the Pokémon League
building."** The map between Victory Road and the Plateau is `MAP_ROUTE_23`
(`constants/map_constants.asm:322`), not Route 32 (which is south of Violet
City). Walkthrough typo.
2. **The walkthrough is describing HeartGold/SoulSilver parties, not Gold.** Four
concrete divergences, all verified against `data/trainers/parties.asm`:
- Will's send-out order is Xatu, Jynx, **Exeggutor, Slowbro**, Xatu; the FAQ
lists Slowbro third and Exeggutor fifth.
- Koga's order is Ariados, **Venomoth, Forretress**, Muk, Crobat; the FAQ
swaps 2 and 3.
- Bruno's order is Hitmontop, **Hitmonlee, Hitmonchan**, Onix, Machamp; the
FAQ swaps 2 and 3.
- Karen and Lance match in order and level.
3. **Prize money.** The FAQ's 4200 / 4400 / 4600 / 4700 / 5000 G are 100 x the
last mon's level (HGSS). GS pays `base reward x level`
(`ComputeTrainerReward`, `engine/battle/read_trainer_party.asm:300`) with a
base reward of 25 for every one of these classes
(`data/trainers/attributes.asm`), i.e. **1050 / 1100 / 1150 / 1175 / 1250 G**.
The EXP figures quoted per Pokémon were not checked against
`data/pokemon/base_stats/` and should be treated as HGSS numbers too.
4. **Held items and AI items.** "Koga has three Full Restores", "Karen has one
Full Restore", "Lance has three Full Restores", "Houndoom holding a Sitrus
Berry", "One of the Dragonites may be holding a Sitrus Berry". In GS these
parties are `TRAINERTYPE_MOVES`, which carries **no held item field at all**,
and the AI's item pool is the two-byte class row: Will `MAX_POTION, NO_ITEM`;
Bruno `MAX_POTION, NO_ITEM`; Karen `FULL_HEAL, MAX_POTION` (no Full Restore);
Koga `FULL_HEAL, FULL_RESTORE`; Champion `FULL_HEAL, FULL_RESTORE`. Sitrus
Berry does not exist in Gen 2.
5. **Move names.** "Dragon Rush" (Lance's Dragonites) is a Gen 4 move; the GS
Dragonites run Thunder Wave / Twister / Thunder or Blizzard / Hyper Beam, and
the level-50 one runs Fire Blast / Safeguard / Outrage / Hyper Beam. Lugia's
"Aero Blast" and Donphan/Sudowoodo suggestions in the sample lineup are fine
as species, but none of that is asm-checkable.
6. **"Routes 46 and 47 aren't bad, either."** `ROUTE_47` does not exist in
pokegold (`constants/map_constants.asm` has `ROUTE_45` and `ROUTE_46` and then
moves on). Route 47/48 are HGSS additions.
7. **"You can go back to the Move Deleter in Blackthorn City."** There is a
`maps/MoveDeletersHouse.asm` reached from `maps/BlackthornCity.asm`; I did not
read its script, so the claim is plausible but unverified here.
8. **"If you trade a Red Scale to Mr. Pokémon, he will give you EXP Share."**
`maps/MrPokemonsHouse.asm` does `checkitem RED_SCALE` (line 53),
`verbosegiveitem EXP_SHARE` (line 72), `takeitem RED_SCALE` (line 74), so the
claim holds; the surrounding flag guards were not read (out of scope).
9. **The "Abra" sprite.** `maps/IndigoPlateauPokecenter1F.asm:324` declares
`SPRITE_JYNX` for the object whose script is `AbraScript` and whose cry is
`cry ABRA`. There is no `SPRITE_ABRA` in `constants/sprite_constants.asm`.
Whether this renders as a Jynx on hardware, or whether some `variablesprite`
elsewhere rewrites it, was not chased down - no `variablesprite` targeting
`SPRITE_JYNX` appears in `engine/events/std_scripts.asm`'s init block.
10. **`EVENT_LANCES_ROOM_EXIT_OPEN` is never set.** `LancesRoomDoorsCallback`
reads it, the lobby callback clears it, but no script in the tree sets it
(grepped `maps/`). The champion's north door is opened by a bare
`changeblock 4, 0, $0b`, which does not survive a map reload. Harmless in
practice because the script warps you out immediately, but a bot that
somehow re-enters Lance's room post-battle will find the door shut again.
11. **`setevent EVENT_LANCES_ROOM_ENTRANCE_CLOSED` appears twice**: once in
`LancesRoomDoorLocksBehindYouScript` (correct) and again immediately after
the champion's after-battle text in `LancesRoomLanceScript`. The second one
looks like it was meant to be the `EXIT_OPEN` set in item 10; recorded as
observed, not corrected.
12. **The lobby's redundant `setscene`.** `PlateauRivalPostBattle` sets the scene
to `SCENE_INDIGOPLATEAUPOKECENTER1F_RIVAL_BATTLE`, which is 0, which is the
map's only scene and its default. Whether this was intended to disarm the
coord events (it does not) is a judgement call; the effective guards are the
engine flag and the weekday check.
13. **"Fully heal your Pokémon and save after every trainer battle."** There is
no heal source inside the sealed rooms - no nurse object, no
`special HealParty` until `HallOfFameEnterScript`. The advice is about bag
items, and the asm agrees that nothing else is available.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,680 @@
# Section 20 - Saffron City Gym
Source: `../section-20-saffron-city-gym.txt` (the FAQ numbers this chapter "26 > Saffron City Gym")
Maps covered: `MAP_ROUTE_6`, `MAP_ROUTE_6_UNDERGROUND_PATH_ENTRANCE`, `MAP_ROUTE_6_SAFFRON_GATE`, `MAP_SAFFRON_CITY`, `MAP_MR_PSYCHICS_HOUSE`, `MAP_SILPH_CO_1F`, `MAP_SAFFRON_MAGNET_TRAIN_STATION`, `MAP_SAFFRON_MART`, `MAP_FIGHTING_DOJO`, `MAP_SAFFRON_GYM`, `MAP_SAFFRON_POKECENTER_1F`
Badges / key milestones in this section: MARSHBADGE (`ENGINE_MARSHBADGE`), TM29 Psychic, Up-Grade, Focus Band, `ENGINE_FLYPOINT_SAFFRON` unlocked on first entry to Saffron City.
Map group / id pairs a bot needs (from `constants/map_constants.asm`):
| Map constant | group | id |
|---|---|---|
| `MAP_ROUTE_6` | 12 (`VERMILION`) | 1 |
| `MAP_ROUTE_6_SAFFRON_GATE` | 12 | 12 |
| `MAP_ROUTE_6_UNDERGROUND_PATH_ENTRANCE` | 12 | 13 |
| `MAP_SAFFRON_CITY` | 25 (`SAFFRON`) | 2 |
| `MAP_FIGHTING_DOJO` | 25 | 3 |
| `MAP_SAFFRON_GYM` | 25 | 4 |
| `MAP_SAFFRON_MART` | 25 | 5 |
| `MAP_SAFFRON_POKECENTER_1F` | 25 | 6 |
| `MAP_MR_PSYCHICS_HOUSE` | 25 | 8 |
| `MAP_SAFFRON_MAGNET_TRAIN_STATION` | 25 | 9 |
| `MAP_SILPH_CO_1F` | 25 | 10 |
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 0 | `MAP_VERMILION_CITY` | `maps/VermilionCity.asm` (previous section) | - | north map connection | "Heal at the Pokémon Center, then head north onto Route 6" - Vermilion belongs to the previous section |
| 1 | `MAP_ROUTE_6` | `maps/Route6.asm` | south connection from Vermilion City | warp 2 `(6, 1)` -> `ROUTE_6_SAFFRON_GATE` warp 3 | "barren wasteland"; the Underground Path door is blocked |
| 1a | `MAP_ROUTE_6_UNDERGROUND_PATH_ENTRANCE` | `maps/Route6UndergroundPathEntrance.asm` | warp 1 `(17, 3)` on Route 6 | - | Walkthrough only notes it is blocked; do not enter this section |
| 2 | `MAP_ROUTE_6_SAFFRON_GATE` | `maps/Route6SaffronGate.asm` | warp 3/4 `(4, 7)` / `(5, 7)` | warp 1/2 `(4, 0)` / `(5, 0)` -> `SAFFRON_CITY` warps 12 / 13 | "make your way all the way up through the checkpoint house" |
| 3 | `MAP_SAFFRON_CITY` | `maps/SaffronCity.asm` | warp 12 `(16, 33)` / warp 13 `(17, 33)` | see below | Hub for the rest of the section; `MAPCALLBACK_NEWMAP` sets the fly point |
| 4 | `MAP_MR_PSYCHICS_HOUSE` | `maps/MrPsychicsHouse.asm` | Saffron warp 5 `(27, 29)` | warp 1/2 `(2, 7)` / `(3, 7)` -> Saffron warp 5 | "Mr. Psychic ... gets you TM29 Psychic" |
| 5 | `MAP_SILPH_CO_1F` | `maps/SilphCo1F.asm` | Saffron warp 7 `(18, 21)` | warp 1/2 `(2, 7)` / `(3, 7)` -> Saffron warp 7 | "Talk to the man blocking the pathway ... he'll give you Up-Grade" |
| 6 | `MAP_SAFFRON_MAGNET_TRAIN_STATION` | `maps/SaffronMagnetTrainStation.asm` | Saffron warp 6 `(8, 3)` | warp 1/2 `(8, 17)` / `(9, 17)` -> Saffron warp 6 | "On the northwest side of town is the Magnet Train ... currently closed" |
| 7 | `MAP_SAFFRON_MART` | `maps/SaffronMart.asm` | Saffron warp 3 `(25, 11)` | warp 1/2 `(2, 7)` / `(3, 7)` -> Saffron warp 3 | "There's a PokeMart if you want to buy or sell items" |
| 8 | `MAP_FIGHTING_DOJO` | `maps/FightingDojo.asm` | Saffron warp 1 `(26, 3)` | warp 1/2 `(4, 11)` / `(5, 11)` -> Saffron warp 1 | "The former fighting gym has a Focus Band you can pick up" |
| 9 | `MAP_SAFFRON_GYM` | `maps/SaffronGym.asm` | Saffron warp 2 `(34, 3)` | warp 1/2 `(8, 17)` / `(9, 17)` -> Saffron warp 2 | "head to the gym on the right ... Sabrina" -> MARSHBADGE |
| 9a | `MAP_SAFFRON_POKECENTER_1F` | `maps/SaffronPokecenter1F.asm` | Saffron warp 4 `(9, 29)` | warp 1/2 `(3, 7)` / `(4, 7)` -> Saffron warp 4 | Optional mid-gym heal trip described in the walkthrough |
Spillover: the walkthrough's opening line ("Heal at the Pokémon Center") refers to Vermilion City, and its closing text ends inside the Saffron Gym exit - nothing past Saffron is described. `MAP_COPYCATS_HOUSE_1F` (Saffron warp 8, `(9, 11)`) exists on this map but the walkthrough never enters it in this section.
## 2. Maps
### MAP_ROUTE_6
- Script: `maps/Route6.asm`
- Blocks: `maps/Route6.blk` (`data/maps/blocks.asm:562` `Route6_Blocks`)
- Header: `data/maps/maps.asm:294` -> `TILESET_KANTO`, `ROUTE`, `LANDMARK_ROUTE_6`, `MUSIC_ROUTE_3`, phone `FALSE`, `PALETTE_AUTO`, `FISHGROUP_POND`
- Dimensions: `constants/map_constants.asm:271` `map_const ROUTE_6, 10, 9` (10x9 blocks = 20x18 cells)
- Connections: `data/maps/attributes.asm:346-348` - north `SaffronCity` (offset -5), south `VermilionCity` (offset -5). No east/west.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 17 | 3 | `ROUTE_6_UNDERGROUND_PATH_ENTRANCE` | 1 |
| 2 | 6 | 1 | `ROUTE_6_SAFFRON_GATE` | 3 |
**Coord events** (`def_coord_events`) - none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 19 | 5 | `BGEVENT_READ` | `Route6UndergroundPathSign` (`Route6UndergroundPathSignText`) |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE6_POKEFAN_M` | `SPRITE_POKEFAN_M` | 17 | 4 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` (sight field 2, unused) | `Route6PokefanMScript` (`4f:581a`) | `EVENT_ROUTE_5_6_POKEFAN_M_BLOCKS_UNDERGROUND_PATH` |
**Scripts of interest**
- `Route6PokefanMScript` (`maps/Route6.asm:9`, sym `4f:581a`): a bare `jumptextfaceplayer Route6PokefanMText` - "The road is closed until the problem at the POWER PLANT is solved." No flag reads. He stands at `(17, 4)`, directly south of the Underground Path door at `(17, 3)`, so his body is the block, not a script check.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_ROUTE_5_6_POKEFAN_M_BLOCKS_UNDERGROUND_PATH` | `constants/event_flags.asm:1299` | set by `maps/PowerPlant.asm:164`; consumed as the object's event flag on `maps/Route6.asm:43` and `maps/Route5.asm:56` | Flag **set** hides the object (`CheckObjectFlag`, `engine/overworld/map_objects_2.asm:32`). Clear (default) = the man is standing there = Underground Path shut |
**Items** - none on this map.
**Trainers** - none on this map.
**Wild encounters**
`data/wild/kanto_grass.asm:480` `def_grass_wildmons ROUTE_6`, encounter rates 10/10/10 percent (morn/day/nite). Gold block (`IF DEF(_GOLD)`, lines 483-506):
| slot | morn | day | nite |
|---|---|---|---|
| 1 | L13 `PIDGEY` | L13 `PIDGEY` | L13 `ODDISH` |
| 2 | L13 `BELLSPROUT` | L13 `BELLSPROUT` | L14 `ODDISH` |
| 3 | L14 `PIDGEY` | L14 `PIDGEY` | L13 `BELLSPROUT` |
| 4 | L15 `MAGNEMITE` | L15 `MAGNEMITE` | L15 `MAGNEMITE` |
| 5 | L12 `ABRA` | L12 `ABRA` | L12 `ABRA` |
| 6 | L14 `ABRA` | L14 `ABRA` | L14 `ABRA` |
| 7 | L14 `ABRA` | L14 `ABRA` | L14 `ABRA` |
Silver (`ELIF DEF(_SILVER)`, lines 508-531) swaps the third slot for `MEOWTH` (L14) morn/day and slots 2/3 for `MEOWTH`/`BELLSPROUT` at nite.
Water: `data/wild/kanto_water.asm:12` `def_water_wildmons ROUTE_6`, 2 percent - L10 `PSYDUCK`, L5 `PSYDUCK`, L10 `GOLDUCK`.
Fishing group is `FISHGROUP_POND` (`data/maps/maps.asm:294`).
### MAP_ROUTE_6_UNDERGROUND_PATH_ENTRANCE
- Script: `maps/Route6UndergroundPathEntrance.asm`
- Blocks: shared `maps/UndergroundPathEntrance.blk` (`data/maps/blocks.asm:466`)
- Header: `data/maps/maps.asm:306` -> `TILESET_GATE`, `GATE`, `LANDMARK_ROUTE_6`, `MUSIC_ROUTE_3`, phone `FALSE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:283` `map_const ROUTE_6_UNDERGROUND_PATH_ENTRANCE, 4, 4`
- Connections: none (indoor)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `ROUTE_6` | 1 |
| 2 | 4 | 7 | `ROUTE_6` | 1 |
| 3 | 4 | 3 | `UNDERGROUND_PATH` | 2 |
No coord events, no bg events, no object events. Nothing gates warp 3 from the inside - the only block is the NPC body on Route 6.
### MAP_ROUTE_6_SAFFRON_GATE
- Script: `maps/Route6SaffronGate.asm`
- Blocks: shared `maps/NorthSouthGate.blk` (`data/maps/blocks.asm:210,215`)
- Header: `data/maps/maps.asm:305` -> `TILESET_GATE`, `GATE`, `LANDMARK_ROUTE_6`, `MUSIC_ROUTE_3`, phone `FALSE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:282` `map_const ROUTE_6_SAFFRON_GATE, 5, 4`
- Scene scripts: one entry, `scene_script Route6SaffronGateNoopScene` with no scene-id argument (`maps/Route6SaffronGate.asm:6`), so no `SCENE_*` constant is generated and the scene is a bare `end`.
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 0 | `SAFFRON_CITY` | 12 |
| 2 | 5 | 0 | `SAFFRON_CITY` | 13 |
| 3 | 4 | 7 | `ROUTE_6` | 2 |
| 4 | 5 | 7 | `ROUTE_6` | 2 |
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE6SAFFRONGATE_OFFICER` | `SPRITE_OFFICER` | 0 | 4 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `Route6SaffronGuardScript` (`59:523c`) | -1 (always) |
**Scripts of interest**
- `Route6SaffronGuardScript` (`maps/Route6SaffronGate.asm:13`, sym `59:523c`): `faceplayer` / `opentext` / `checkevent EVENT_RETURNED_MACHINE_PART` / `iftrue .ReturnedPart`. Before the Power Plant is fixed he explains the Magnet Train is dead; after, a one-liner. He stands at `(0, 4)`, off the walking lane - he never blocks passage.
### MAP_SAFFRON_CITY
- Script: `maps/SaffronCity.asm`
- Blocks: `maps/SaffronCity.blk` (`data/maps/blocks.asm:24`)
- Header: `data/maps/maps.asm:490` -> `TILESET_KANTO`, `TOWN`, `LANDMARK_SAFFRON_CITY`, `MUSIC_VIRIDIAN_CITY`, phone `FALSE`, `PALETTE_AUTO`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:454` `map_const SAFFRON_CITY, 20, 18` (40x36 cells)
- Connections: `data/maps/attributes.asm:350-354` - north `Route5` (+5), south `Route6` (+5), west `Route7` (+9), east `Route8` (+9)
- Spawn / fly: `data/maps/spawn_points.asm:21` `spawn SAFFRON_CITY, 9, 30`; `data/maps/flypoints.asm:26` `db LANDMARK_SAFFRON_CITY, SPAWN_SAFFRON`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 26 | 3 | `FIGHTING_DOJO` | 1 |
| 2 | 34 | 3 | `SAFFRON_GYM` | 1 |
| 3 | 25 | 11 | `SAFFRON_MART` | 2 |
| 4 | 9 | 29 | `SAFFRON_POKECENTER_1F` | 1 |
| 5 | 27 | 29 | `MR_PSYCHICS_HOUSE` | 1 |
| 6 | 8 | 3 | `SAFFRON_MAGNET_TRAIN_STATION` | 2 |
| 7 | 18 | 21 | `SILPH_CO_1F` | 1 |
| 8 | 9 | 11 | `COPYCATS_HOUSE_1F` | 1 |
| 9 | 18 | 3 | `ROUTE_5_SAFFRON_GATE` | 3 |
| 10 | 0 | 24 | `ROUTE_7_SAFFRON_GATE` | 3 |
| 11 | 0 | 25 | `ROUTE_7_SAFFRON_GATE` | 4 |
| 12 | 16 | 33 | `ROUTE_6_SAFFRON_GATE` | 1 |
| 13 | 17 | 33 | `ROUTE_6_SAFFRON_GATE` | 2 |
| 14 | 39 | 22 | `ROUTE_8_SAFFRON_GATE` | 1 |
| 15 | 39 | 23 | `ROUTE_8_SAFFRON_GATE` | 2 |
**Coord events** - none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 21 | 5 | `BGEVENT_READ` | `SaffronCitySign` |
| 33 | 5 | `BGEVENT_READ` | `SaffronGymSign` |
| 25 | 5 | `BGEVENT_READ` | `FightingDojoSign` |
| 15 | 21 | `BGEVENT_READ` | `SilphCoSign` |
| 25 | 29 | `BGEVENT_READ` | `MrPsychicsHouseSign` |
| 11 | 5 | `BGEVENT_READ` | `SaffronCityMagnetTrainStationSign` |
| 10 | 29 | `BGEVENT_READ` | `SaffronCityPokecenterSign` (`jumpstd PokecenterSignScript`) |
| 26 | 11 | `BGEVENT_READ` | `SaffronCityMartSign` (`jumpstd MartSignScript`) |
No hidden items on this map.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `SAFFRONCITY_LASS1` | `SPRITE_LASS` | 7 | 14 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (rx 2) | `OBJECTTYPE_SCRIPT` | `SaffronCityLass1Script` | -1 |
| `SAFFRONCITY_POKEFAN_M` | `SPRITE_POKEFAN_M` | 19 | 30 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (rx 2) | `OBJECTTYPE_SCRIPT` | `SaffronCityPokefanMScript` | -1 |
| `SAFFRONCITY_COOLTRAINER_M` | `SPRITE_COOLTRAINER_M` | 32 | 7 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (rx 1) | `OBJECTTYPE_SCRIPT` | `SaffronCityCooltrainerMScript` | -1 |
| `SAFFRONCITY_COOLTRAINER_F` | `SPRITE_COOLTRAINER_F` | 20 | 24 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (rx 2) | `OBJECTTYPE_SCRIPT` | `SaffronCityCooltrainerFScript` | -1 |
| `SAFFRONCITY_FISHER` | `SPRITE_FISHER` | 27 | 12 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `SaffronCityFisherScript` | -1 |
| `SAFFRONCITY_YOUNGSTER1` | `SPRITE_YOUNGSTER` | 15 | 19 | `SPRITEMOVEDATA_WALK_UP_DOWN` (ry 1) | `OBJECTTYPE_SCRIPT` | `SaffronCityYoungster1Script` | -1 |
| `SAFFRONCITY_YOUNGSTER2` | `SPRITE_YOUNGSTER` | 35 | 22 | `SPRITEMOVEDATA_WANDER` (rx 1, ry 1) | `OBJECTTYPE_SCRIPT` | `SaffronCityYoungster2Script` | -1 |
| `SAFFRONCITY_LASS2` | `SPRITE_LASS` | 19 | 8 | `SPRITEMOVEDATA_SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT` | `SaffronCityLass2Script` | -1 |
Every Saffron City NPC is `OBJECTTYPE_SCRIPT`; none of them blocks a warp tile.
**Scripts of interest**
- `SaffronCityFlypointCallback` (`maps/SaffronCity.asm:17`), registered as `callback MAPCALLBACK_NEWMAP`: `setflag ENGINE_FLYPOINT_SAFFRON` / `endcallback`. Walking in from the Route 6 gate is what unlocks Fly to Saffron - there is no NPC or item involved.
- `SaffronCityLass1Script`, `SaffronCityPokefanMScript`, `SaffronCityFisherScript` all branch on `checkevent EVENT_RETURNED_MACHINE_PART`. Purely dialogue; useful only as a state probe.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_FLYPOINT_SAFFRON` | `constants/engine_flags.asm:74` | set by `SaffronCityFlypointCallback` | Fly destination unlocked; also the `SPAWN_SAFFRON` blackout target once the player has been here |
| `EVENT_RETURNED_MACHINE_PART` | `constants/event_flags.asm:200` | set in `maps/PowerPlant.asm:161`; read by four Saffron scripts and `Route6SaffronGuardScript` | Not set in this section. Its absence is what keeps the Magnet Train shut |
**Items** - none directly on the city map.
**Trainers** - none.
**Wild encounters** - none. `SAFFRON_CITY` has no entry in `data/wild/kanto_grass.asm`, `kanto_water.asm`, `fish.asm` or `treemons.asm` (grep over `data/wild/` for `SAFFRON` returns nothing).
### MAP_MR_PSYCHICS_HOUSE
- Script: `maps/MrPsychicsHouse.asm`
- Blocks: shared `maps/House1.blk` (`data/maps/blocks.asm:195,201`)
- Header: `data/maps/maps.asm:496` -> `TILESET_HOUSE`, `INDOOR`, `LANDMARK_SAFFRON_CITY`, `MUSIC_VIRIDIAN_CITY`, phone `FALSE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:460` `map_const MR_PSYCHICS_HOUSE, 4, 4`
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 2 | 7 | `SAFFRON_CITY` | 5 |
| 2 | 3 | 7 | `SAFFRON_CITY` | 5 |
**Coord events** - none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 0 | 1 | `BGEVENT_READ` | `MrPsychicsHouseBookshelf` (`jumpstd DifficultBookshelfScript`) |
| 1 | 1 | `BGEVENT_READ` | `MrPsychicsHouseBookshelf` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `MRPSYCHICSHOUSE_FISHING_GURU` | `SPRITE_FISHING_GURU` | 5 | 3 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `MrPsychic` (`61:4b15`) | -1 |
**Scripts of interest**
- `MrPsychic` (`maps/MrPsychicsHouse.asm:9`, sym `61:4b15`):
`faceplayer` / `opentext` / `checkevent EVENT_GOT_TM29_PSYCHIC` / `iftrue .AlreadyGotItem` / `writetext MrPsychicText1` / `promptbutton` / `verbosegiveitem TM_PSYCHIC_M` / `iffalse .Done` / `setevent EVENT_GOT_TM29_PSYCHIC` / `.AlreadyGotItem: writetext MrPsychicText2` / `waitbutton` / `.Done: closetext` / `end`.
One `verbosegiveitem`; a full bag drops through `.Done` **without** setting the flag, so a bot must re-talk after making room.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_GOT_TM29_PSYCHIC` | `constants/event_flags.asm:226` | read+set by `MrPsychic` | One-shot guard on TM29 |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `TM_PSYCHIC_M` (TM29, `constants/item_constants.asm:250` `add_tm PSYCHIC_M ; dd`) | talk to `MRPSYCHICSHOUSE_FISHING_GURU` at `(5, 3)` | `MrPsychic`, `verbosegiveitem` | `EVENT_GOT_TM29_PSYCHIC` |
### MAP_SILPH_CO_1F
- Script: `maps/SilphCo1F.asm`
- Blocks: `maps/SilphCo1F.blk` (`data/maps/blocks.asm:874`)
- Header: `data/maps/maps.asm:498` -> `TILESET_FACILITY`, `INDOOR`, `LANDMARK_SAFFRON_CITY`, `MUSIC_VIRIDIAN_CITY`, phone `FALSE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:462` `map_const SILPH_CO_1F, 8, 4`
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 2 | 7 | `SAFFRON_CITY` | 7 |
| 2 | 3 | 7 | `SAFFRON_CITY` | 7 |
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `SILPHCO1F_RECEPTIONIST` | `SPRITE_RECEPTIONIST` | 4 | 2 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `SilphCoReceptionistScript` | -1 |
| `SILPHCO1F_OFFICER` | `SPRITE_OFFICER` | 13 | 1 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `SilphCoOfficerScript` (`61:4f81`) | -1 |
**Scripts of interest**
- `SilphCoOfficerScript` (`maps/SilphCo1F.asm:13`, sym `61:4f81`): `faceplayer` / `opentext` / `checkevent EVENT_GOT_UP_GRADE` / `iftrue .GotUpGrade` / `writetext SilphCoOfficerText` / `promptbutton` / `verbosegiveitem UP_GRADE` / `iffalse .NoRoom` / `setevent EVENT_GOT_UP_GRADE` / `.GotUpGrade: writetext ...` / `waitbutton` / `.NoRoom: closetext` / `end`. Same full-bag shape as Mr. Psychic. There is no upstairs on this map at all - the "man blocking the stairway" is `(13, 1)`, standing on the only path north; nothing scripted opens it.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_GOT_UP_GRADE` | `constants/event_flags.asm:221` | read+set by `SilphCoOfficerScript` | One-shot guard on Up-Grade |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `UP_GRADE` (`constants/item_constants.asm:180`, `$ac`) | talk to `SILPHCO1F_OFFICER` at `(13, 1)` | `SilphCoOfficerScript`, `verbosegiveitem` | `EVENT_GOT_UP_GRADE` |
### MAP_SAFFRON_MAGNET_TRAIN_STATION
- Script: `maps/SaffronMagnetTrainStation.asm`
- Blocks: `maps/SaffronMagnetTrainStation.blk`
- Header: `data/maps/maps.asm:497` -> `TILESET_TRAIN_STATION`, `INDOOR`, `LANDMARK_SAFFRON_CITY`, `MUSIC_VIRIDIAN_CITY`, phone `FALSE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:461` `map_const SAFFRON_MAGNET_TRAIN_STATION, 10, 9`
- Scene variable: `data/maps/scenes.asm:17` `scene_var SAFFRON_MAGNET_TRAIN_STATION, wSaffronMagnetTrainStationSceneID`
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 8 | 17 | `SAFFRON_CITY` | 6 |
| 2 | 9 | 17 | `SAFFRON_CITY` | 6 |
| 3 | 6 | 5 | `GOLDENROD_MAGNET_TRAIN_STATION` | 4 |
| 4 | 11 | 5 | `GOLDENROD_MAGNET_TRAIN_STATION` | 3 |
**Coord events**
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_SAFFRONMAGNETTRAINSTATION_ARRIVE_FROM_GOLDENROD` (= 0, generated by the `scene_script` macro at `maps/SaffronMagnetTrainStation.asm:9`) | 11 | 6 | `Script_ArriveFromGoldenrod` | Arrival cutscene when riding in from Goldenrod; not reachable in this section |
**BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `SAFFRONMAGNETTRAINSTATION_OFFICER` | `SPRITE_OFFICER` | 9 | 9 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `SaffronMagnetTrainStationOfficerScript` (`61:4bc2`) | -1 |
| `SAFFRONMAGNETTRAINSTATION_GYM_GUIDE` | `SPRITE_GYM_GUIDE` | 10 | 14 | `SPRITEMOVEDATA_WANDER` (1,1) | `OBJECTTYPE_SCRIPT` | `SaffronMagnetTrainStationGymGuideScript` | -1 |
| `SAFFRONMAGNETTRAINSTATION_TEACHER` | `SPRITE_TEACHER` | 6 | 11 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `SaffronMagnetTrainStationTeacherScript` | `EVENT_SAFFRON_TRAIN_STATION_POPULATION` |
| `SAFFRONMAGNETTRAINSTATION_LASS` | `SPRITE_LASS` | 6 | 10 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_SCRIPT` | `SaffronMagnetTrainStationLassScript` | `EVENT_SAFFRON_TRAIN_STATION_POPULATION` |
**Scripts of interest**
- `SaffronMagnetTrainStationOfficerScript` (`maps/SaffronMagnetTrainStation.asm:16`, sym `61:4bc2`): `checkevent EVENT_RESTORED_POWER_TO_KANTO` / `iftrue .MagnetTrainToGoldenrod`, else "the MAGNET TRAIN isn't operating now" and `end`. The ride arm additionally runs `yesorno`, `checkitem PASS`, two `applymovement`s, `setval TRUE` / `special MagnetTrain` / `warpcheck` / `newloadmap MAPSETUP_TRAIN`. In this section both `EVENT_RESTORED_POWER_TO_KANTO` and `PASS` are missing, so the officer is a dead end.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_RESTORED_POWER_TO_KANTO` | `constants/event_flags.asm:204` | set at `maps/PowerPlant.asm:165`; read by the officer script | Gate on the Magnet Train being operable at all |
| `EVENT_SAFFRON_TRAIN_STATION_POPULATION` | `constants/event_flags.asm:1300` | cleared at `maps/PowerPlant.asm:160` | While **set** (the state in this section) the Teacher and Lass are hidden - the station looks empty on purpose |
### MAP_SAFFRON_MART
- Script: `maps/SaffronMart.asm`
- Blocks: shared `maps/Mart.blk` (`data/maps/blocks.asm:333,335`)
- Header: `data/maps/maps.asm:493` -> `TILESET_MART`, `INDOOR`, `LANDMARK_SAFFRON_CITY`, `MUSIC_VIRIDIAN_CITY`, phone `FALSE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:457` `map_const SAFFRON_MART, 6, 4`
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 2 | 7 | `SAFFRON_CITY` | 3 |
| 2 | 3 | 7 | `SAFFRON_CITY` | 3 |
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `SAFFRONMART_CLERK` | `SPRITE_CLERK` | 1 | 3 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `SaffronMartClerkScript` | -1 |
| `SAFFRONMART_COOLTRAINER_M` | `SPRITE_COOLTRAINER_M` | 7 | 2 | `SPRITEMOVEDATA_SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT` | `SaffronMartCooltrainerMScript` | -1 |
| `SAFFRONMART_COOLTRAINER_F` | `SPRITE_COOLTRAINER_F` | 7 | 6 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (rx 1) | `OBJECTTYPE_SCRIPT` | `SaffronMartCooltrainerFScript` | -1 |
**Scripts of interest**
- `SaffronMartClerkScript`: `opentext` / `pokemart MARTTYPE_STANDARD, MART_SAFFRON` / `closetext` / `end`.
**Items** - shop stock, `data/items/marts.asm:362` `MartSaffron`: `GREAT_BALL`, `ULTRA_BALL`, `HYPER_POTION`, `MAX_POTION`, `FULL_HEAL`, `X_ATTACK`, `X_DEFEND`, `FLOWER_MAIL`.
### MAP_FIGHTING_DOJO
- Script: `maps/FightingDojo.asm`
- Blocks: `maps/FightingDojo.blk` (`data/maps/blocks.asm:871`)
- Header: `data/maps/maps.asm:491` -> `TILESET_TRAIN_STATION`, `INDOOR`, `LANDMARK_SAFFRON_CITY`, `MUSIC_VIRIDIAN_CITY`, phone `FALSE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:455` `map_const FIGHTING_DOJO, 5, 6`
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 11 | `SAFFRON_CITY` | 1 |
| 2 | 5 | 11 | `SAFFRON_CITY` | 1 |
**Coord events** - none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 4 | 0 | `BGEVENT_READ` | `FightingDojoSign1` |
| 5 | 0 | `BGEVENT_READ` | `FightingDojoSign2` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `FIGHTINGDOJO_BLACK_BELT` | `SPRITE_BLACK_BELT` | 4 | 4 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `FightingDojoBlackBelt` | -1 |
| `FIGHTINGDOJO_POKE_BALL` | `SPRITE_POKE_BALL` | 3 | 1 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `FightingDojoFocusBand` (`61:400b`) | `EVENT_PICKED_UP_FOCUS_BAND` |
**Scripts of interest**
- `FightingDojoFocusBand` (`maps/FightingDojo.asm:19`, sym `61:400b`): the two-byte `itemball FOCUS_BAND` struct (macro at `macros/scripts/maps.asm:154`, quantity defaults to 1), not a script. The engine's own item-ball path runs it.
- There are **no trainers** in the Fighting Dojo in Gen 2. `FightingDojoBlackBeltText` explains the Karate King is away in a Johto cave (Rock Tunnel-era content elsewhere), so nothing here fights back.
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `FOCUS_BAND` (`constants/item_constants.asm:127`, `$77`) | walk onto / press A at `(3, 1)` | `FightingDojoFocusBand` (`OBJECTTYPE_ITEMBALL`) | `EVENT_PICKED_UP_FOCUS_BAND` (`constants/event_flags.asm:1312`) - once set the ball object is masked |
### MAP_SAFFRON_GYM
- Script: `maps/SaffronGym.asm`
- Blocks: `maps/SaffronGym.blk` (`data/maps/blocks.asm:861`)
- Header: `data/maps/maps.asm:492` -> `TILESET_UNDERGROUND`, `INDOOR`, `LANDMARK_SAFFRON_CITY`, `MUSIC_GYM`, phone `TRUE` (phone calls suppressed), `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:456` `map_const SAFFRON_GYM, 10, 9` (20x18 cells)
- Connections: none
**Warps** (`def_warp_events`, `maps/SaffronGym.asm:294-326`) - warps 1-2 are the street doors; warps 3-32 are the 30 teleport pads. Every pad's destination is `SAFFRON_GYM` and the player lands on the destination warp's own tile.
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 8 | 17 | `SAFFRON_CITY` | 2 |
| 2 | 9 | 17 | `SAFFRON_CITY` | 2 |
| 3 | 11 | 15 | `SAFFRON_GYM` | 18 |
| 4 | 19 | 15 | `SAFFRON_GYM` | 19 |
| 5 | 19 | 11 | `SAFFRON_GYM` | 20 |
| 6 | 1 | 11 | `SAFFRON_GYM` | 21 |
| 7 | 5 | 3 | `SAFFRON_GYM` | 22 |
| 8 | 11 | 5 | `SAFFRON_GYM` | 23 |
| 9 | 1 | 15 | `SAFFRON_GYM` | 24 |
| 10 | 19 | 3 | `SAFFRON_GYM` | 25 |
| 11 | 15 | 17 | `SAFFRON_GYM` | 26 |
| 12 | 5 | 17 | `SAFFRON_GYM` | 27 |
| 13 | 5 | 9 | `SAFFRON_GYM` | 28 |
| 14 | 9 | 3 | `SAFFRON_GYM` | 29 |
| 15 | 15 | 9 | `SAFFRON_GYM` | 30 |
| 16 | 15 | 5 | `SAFFRON_GYM` | 31 |
| 17 | 1 | 5 | `SAFFRON_GYM` | 32 |
| 18 | 19 | 17 | `SAFFRON_GYM` | 3 |
| 19 | 19 | 9 | `SAFFRON_GYM` | 4 |
| 20 | 1 | 9 | `SAFFRON_GYM` | 5 |
| 21 | 5 | 5 | `SAFFRON_GYM` | 6 |
| 22 | 11 | 3 | `SAFFRON_GYM` | 7 |
| 23 | 1 | 17 | `SAFFRON_GYM` | 8 |
| 24 | 19 | 5 | `SAFFRON_GYM` | 9 |
| 25 | 15 | 15 | `SAFFRON_GYM` | 10 |
| 26 | 5 | 15 | `SAFFRON_GYM` | 11 |
| 27 | 5 | 11 | `SAFFRON_GYM` | 12 |
| 28 | 9 | 5 | `SAFFRON_GYM` | 13 |
| 29 | 15 | 11 | `SAFFRON_GYM` | 14 |
| 30 | 15 | 3 | `SAFFRON_GYM` | 15 |
| 31 | 1 | 3 | `SAFFRON_GYM` | 16 |
| 32 | 11 | 9 | `SAFFRON_GYM` | 17 |
**Teleporter graph, resolved to landing coordinates.** Every pair is mutual, so the graph is 15 undirected edges. Room labels are derived from the pad clusters and the object coordinates; they are a reading aid, the coordinates are the ground truth.
| room (occupant) | pads (x, y) |
|---|---|
| ENTRANCE (doors `(8,17)`/`(9,17)`, guide `(9,14)`, statue `(8,15)`) | `(11,15)` |
| BL (Psychic Franklin `(3,16)`) | `(1,15)` `(5,15)` `(1,17)` `(5,17)` |
| BR (Medium Rebecca `(17,16)`) | `(15,15)` `(19,15)` `(15,17)` `(19,17)` |
| ML | `(1,9)` `(5,9)` `(1,11)` `(5,11)` |
| CENTER (Sabrina `(9,8)`) | `(11,9)` |
| MR | `(15,9)` `(19,9)` `(15,11)` `(19,11)` |
| TL (Medium Doris `(3,4)`) | `(1,3)` `(5,3)` `(1,5)` `(5,5)` |
| TC | `(9,3)` `(11,3)` `(9,5)` `(11,5)` |
| TR (Psychic Jared `(17,4)`) | `(15,3)` `(19,3)` `(15,5)` `(19,5)` |
| step on pad | warp idx | lands at | room reached |
|---|---|---|---|
| `(11,15)` ENTRANCE | 3 | `(19,17)` | BR |
| `(19,17)` BR | 18 | `(11,15)` | ENTRANCE |
| `(19,15)` BR | 4 | `(19,9)` | MR |
| `(19,9)` MR | 19 | `(19,15)` | BR |
| `(15,17)` BR | 11 | `(5,15)` | BL |
| `(5,15)` BL | 26 | `(15,17)` | BR |
| `(15,15)` BR | 25 | `(19,3)` | TR |
| `(19,3)` TR | 10 | `(15,15)` | BR |
| `(1,15)` BL | 9 | `(19,5)` | TR |
| `(19,5)` TR | 24 | `(1,15)` | BL |
| `(1,17)` BL | 23 | `(11,5)` | TC |
| `(11,5)` TC | 8 | `(1,17)` | BL |
| `(5,17)` BL | 12 | `(5,11)` | ML |
| `(5,11)` ML | 27 | `(5,17)` | BL |
| `(1,9)` ML | 20 | `(19,11)` | MR |
| `(19,11)` MR | 5 | `(1,9)` | ML |
| `(5,9)` ML | 13 | `(9,5)` | TC |
| `(9,5)` TC | 28 | `(5,9)` | ML |
| `(1,11)` ML | 6 | `(5,5)` | TL |
| `(5,5)` TL | 21 | `(1,11)` | ML |
| `(15,9)` MR | 15 | `(15,3)` | TR |
| `(15,3)` TR | 30 | `(15,9)` | MR |
| `(15,11)` MR | 29 | `(9,3)` | TC |
| `(9,3)` TC | 14 | `(15,11)` | MR |
| `(5,3)` TL | 7 | `(11,3)` | TC |
| `(11,3)` TC | 22 | `(5,3)` | TL |
| `(1,3)` TL | 31 | `(15,5)` | TR |
| `(15,5)` TR | 16 | `(1,3)` | TL |
| **`(1,5)` TL** | **17** | **`(11,9)`** | **CENTER (Sabrina)** |
| **`(11,9)` CENTER** | **32** | **`(1,5)`** | **TL** |
`(1,5)` in the top-left room is the only pad that reaches Sabrina, and `(11,9)` is the only way out of her chamber. This matches the walkthrough's "go to the bottom left teleporter and you'll go straight to the gym leader".
**Coord events** - none. The gym has no trip-wires; every transition is a warp tile.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 8 | 15 | `BGEVENT_READ` | `SaffronGymStatue` (`61:4170`) |
**Object events**
| const | sprite | x | y | movement | type | sight | script label | event flag |
|---|---|---|---|---|---|---|---|---|
| `SAFFRONGYM_SABRINA` | `SPRITE_SABRINA` | 9 | 8 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | 0 | `SaffronGymSabrinaScript` (`61:40cf`) | -1 |
| `SAFFRONGYM_GRANNY1` | `SPRITE_GRANNY` | 17 | 16 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` | 3 | `TrainerMediumRebecca` (`61:410c`) | -1 |
| `SAFFRONGYM_YOUNGSTER1` | `SPRITE_YOUNGSTER` | 3 | 16 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` | 3 | `TrainerPsychicFranklin` (`61:4120`) | -1 |
| `SAFFRONGYM_GRANNY2` | `SPRITE_GRANNY` | 3 | 4 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` | 2 | `TrainerMediumDoris` (`61:4134`) | -1 |
| `SAFFRONGYM_YOUNGSTER2` | `SPRITE_YOUNGSTER` | 17 | 4 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` | 2 | `TrainerPsychicJared` (`61:4148`) | -1 |
| `SAFFRONGYM_GYM_GUIDE` | `SPRITE_GYM_GUIDE` | 9 | 14 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | 0 | `SaffronGymGuideScript` | -1 |
All four trainers spin (`SPINRANDOM_FAST`), so their line of sight rotates - a bot cannot assume a fixed approach lane; walking into the room will trigger them.
**Scripts of interest**
- `SaffronGymSabrinaScript` (`maps/SaffronGym.asm:14`, sym `61:40cf`):
`faceplayer` / `opentext` / `checkflag ENGINE_MARSHBADGE` / `iftrue .FightDone` / `writetext SabrinaIntroText` / `waitbutton` / `closetext` / `winlosstext SabrinaWinLossText, 0` / `loadtrainer SABRINA, SABRINA1` / `startbattle` / `reloadmapafterbattle` / then a burst of five `setevent`s: `EVENT_BEAT_SABRINA`, `EVENT_BEAT_MEDIUM_REBECCA`, `EVENT_BEAT_MEDIUM_DORIS`, `EVENT_BEAT_PSYCHIC_FRANKLIN`, `EVENT_BEAT_PSYCHIC_JARED` / `opentext` / `writetext ReceivedMarshBadgeText` / `playsound SFX_GET_BADGE` / `waitsfx` / `setflag ENGINE_MARSHBADGE` / `writetext SabrinaMarshBadgeText` / `waitbutton` / `closetext` / `end`.
Two consequences a bot should exploit: **the four gym trainers are auto-cleared when Sabrina falls**, so they are entirely skippable; and the badge is a `setflag` on `ENGINE_MARSHBADGE`, which is also the re-entry guard (`.FightDone`).
- `TrainerMediumRebecca` / `TrainerPsychicFranklin` / `TrainerMediumDoris` / `TrainerPsychicJared` (`maps/SaffronGym.asm:47,58,69,80`): each is a `trainer CLASS, ID, EVENT_BEAT_*, SeenText, BeatenText, 0, .Script` struct (`macros/scripts/maps.asm:142`; field order is group, id, flag, seen, win, loss, after) followed by an `endifjustbattled` / `opentext` / `writetext ...AfterBattleText` / `waitbutton` / `closetext` / `end` after-battle script.
- `SaffronGymStatue` (`maps/SaffronGym.asm:107`, sym `61:4170`): `checkflag ENGINE_MARSHBADGE` / `iftrue .Beaten` / `jumpstd GymStatue1Script`; the beaten arm does `gettrainername STRING_BUFFER_4, SABRINA, SABRINA1` then `jumpstd GymStatue2Script`. Handy as a cheap post-badge assertion.
- `SaffronGymGuideScript` (`maps/SaffronGym.asm:91`): branches on `checkevent EVENT_BEAT_SABRINA`; no items, no flags written.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_MARSHBADGE` | `constants/engine_flags.asm:52` (bit 5 of `wKantoBadges`; Kanto order is Boulder, Cascade, Thunder, Rainbow, Soul, **Marsh**, Volcano, Earth) | `checkflag` in `SaffronGymSabrinaScript` and `SaffronGymStatue`; `setflag` after the win | The section's terminal postcondition |
| `EVENT_BEAT_SABRINA` | `constants/event_flags.asm:720` | set by `SaffronGymSabrinaScript`; read by `SaffronGymGuideScript` | Leader defeated |
| `EVENT_BEAT_MEDIUM_REBECCA` | `constants/event_flags.asm:935` | trainer struct flag; also force-set by the Sabrina script | Rebecca cleared |
| `EVENT_BEAT_MEDIUM_DORIS` | `constants/event_flags.asm:936` | as above | Doris cleared |
| `EVENT_BEAT_PSYCHIC_FRANKLIN` | `constants/event_flags.asm:564` | as above | Franklin cleared |
| `EVENT_BEAT_PSYCHIC_JARED` | `constants/event_flags.asm:573` | as above | Jared cleared |
**Items** - none in the gym.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `SABRINA` / `SABRINA1` | `SABRINA` (`constants/trainer_constants.asm:309`, class 23) | 1 | `SabrinaGroup` `; SABRINA (1)` (`data/trainers/parties.asm:1400-1406`), `TRAINERTYPE_MOVES`: L46 `ESPEON` (Sand-Attack, Quick Attack, Swift, Psychic), L46 `MR__MIME` (Barrier, Reflect, Baton Pass, Psychic), L48 `ALAKAZAM` (Recover, Future Sight, Psychic, Reflect) | `SaffronGymSabrinaScript` | none; leader (`data/trainers/leaders.asm:29`) |
| `MEDIUM` / `REBECCA` | `MEDIUM` | 6 (`constants/trainer_constants.asm:590`) | `MediumGroup` `; MEDIUM (6)`: L35 `DROWZEE`, L35 `HYPNO` | `TrainerMediumRebecca` | none |
| `PSYCHIC_T` / `FRANKLIN` | `PSYCHIC_T` (class 34) | 2 (`constants/trainer_constants.asm:513`) | `PsychicGroup` `; PSYCHIC_T (2)` (`data/trainers/parties.asm:2516`): L37 `KADABRA` | `TrainerPsychicFranklin` | none |
| `MEDIUM` / `DORIS` | `MEDIUM` | 7 (`constants/trainer_constants.asm:591`) | `MediumGroup` `; MEDIUM (7)`: L34 `SLOWPOKE`, L36 `SLOWBRO` | `TrainerMediumDoris` | none |
| `PSYCHIC_T` / `JARED` | `PSYCHIC_T` | 11 (`constants/trainer_constants.asm:522`) | `PsychicGroup` `; PSYCHIC_T (11)` (`data/trainers/parties.asm:2569`): L32 `MR__MIME`, L32 `EXEGGCUTE`, **L35** `EXEGGCUTE` | `TrainerPsychicJared` | none |
Prize money: `ComputeTrainerReward` (`engine/battle/read_trainer_party.asm:300`) is `base reward x last mon's level`, and `engine/battle/core.asm:2344-2358` pays that quarter out **four times**, so the visible payout is `4 x base x level`. Base rewards: Sabrina 25 (`data/trainers/attributes.asm:211`), Medium 10 (`:343`), Psychic T 8 (`:313`). That reproduces the walkthrough's 4800G / 1400G / 1440G / 1184G / 1120G exactly. Sabrina also carries a `HYPER_POTION` as an AI item (`data/trainers/attributes.asm:210`).
**Wild encounters** - none (indoor gym).
### MAP_SAFFRON_POKECENTER_1F
- Script: `maps/SaffronPokecenter1F.asm`
- Blocks: shared `maps/Pokecenter1F.blk` (`data/maps/blocks.asm:362,364`)
- Header: `data/maps/maps.asm:494` -> `TILESET_POKECENTER`, `INDOOR`, `LANDMARK_SAFFRON_CITY`, `MUSIC_POKEMON_CENTER`, phone `FALSE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:458` `map_const SAFFRON_POKECENTER_1F, 5, 4`
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `SAFFRON_CITY` | 4 |
| 2 | 4 | 7 | `SAFFRON_CITY` | 4 |
| 3 | 0 | 7 | `POKECENTER_2F` | 1 |
**Object events** (nurse only listed; the rest are flavour)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `SAFFRONPOKECENTER1F_NURSE` | `SPRITE_NURSE` | 3 | 1 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `SaffronPokecenter1FNurseScript` (`jumpstd PokecenterNurseScript`) | -1 |
| `SAFFRONPOKECENTER1F_TEACHER` | `SPRITE_TEACHER` | 7 | 2 | `SPRITEMOVEDATA_WANDER` (1,1) | `OBJECTTYPE_SCRIPT` | `SaffronPokecenter1FTeacherScript` | -1 |
| `SAFFRONPOKECENTER1F_FISHER` | `SPRITE_FISHER` | 8 | 6 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `SaffronPokecenter1FFisherScript` | -1 |
| `SAFFRONPOKECENTER1F_YOUNGSTER` | `SPRITE_YOUNGSTER` | 1 | 4 | `SPRITEMOVEDATA_SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT` | `SaffronPokecenter1FYoungsterScript` | -1 |
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Underground Path (Route 6 warp 1 at `(17, 3)`) | `maps/Route6.asm:43` - `ROUTE6_POKEFAN_M` object standing at `(17, 4)` with event flag `EVENT_ROUTE_5_6_POKEFAN_M_BLOCKS_UNDERGROUND_PATH`; visibility decided by `CheckObjectFlag`, `engine/overworld/map_objects_2.asm:32` | Nothing the player can do in this section | `maps/PowerPlant.asm:164` `setevent EVENT_ROUTE_5_6_POKEFAN_M_BLOCKS_UNDERGROUND_PATH` (flag set = object masked), which is part of returning the `MACHINE_PART` |
| Magnet Train Saffron -> Goldenrod | `maps/SaffronMagnetTrainStation.asm:19` `checkevent EVENT_RESTORED_POWER_TO_KANTO`, then `checkitem PASS` at `:30` | `EVENT_RESTORED_POWER_TO_KANTO` **and** `PASS` in bag | `maps/PowerPlant.asm:165` sets the event; the `PASS` comes from elsewhere. Both false here, so the officer only prints "isn't operating now" |
| Silph Co. upper floors | `maps/SilphCo1F.asm:69` - `SILPHCO1F_OFFICER` object at `(13, 1)` with event flag `-1` (never hides). No stairs warp exists on `SilphCo1F` at all | Permanently blocked in Gen 2 | Never; this is not a gate that opens |
| Saffron Gym maze | The 30 pad warps at `maps/SaffronGym.asm:296-326`. Pads are teleport panels (`COLL_WARP_PANEL`-class tiles), so they fire on landing, not on a directional press | Navigation only - no badge, item or flag check anywhere in `SaffronGym.asm` | n/a. The single route to Sabrina is `(11,15) -> (19,17) -> ... -> (1,5) -> (11,9)`; see the pad table |
| MARSHBADGE re-fight guard | `maps/SaffronGym.asm:17` `checkflag ENGINE_MARSHBADGE` / `iftrue .FightDone` | Badge not yet held | Beating Sabrina once; afterwards she only talks |
| Gym trainers | Each `trainer` struct's `EVENT_BEAT_*` flag; the trainer only challenges while its flag is clear | Walking into their sight cone (sight 3 for Rebecca/Franklin, 2 for Doris/Jared) | Beating them, **or** beating Sabrina, which sets all four flags at `maps/SaffronGym.asm:27-30` |
No HM field move is required anywhere in this section - no `engine/overworld/cut.asm`, `surf.asm`, `strength.asm` or `whirlpool.asm` check is reachable from these maps. The Bicycle the walkthrough recommends is convenience only.
## 4. Bot checklist
Coordinates are asm cell coordinates on the named map. "Warp N" means the `def_warp_events` index in that map's table.
1. `MAP_ROUTE_6` - walk north along the route to warp 2 at `(6, 1)`. Precondition: none. Postcondition: on `MAP_ROUTE_6_SAFFRON_GATE` warp 3 `(4, 7)`.
- Optional: read the sign at `(19, 5)`. Do **not** try warp 1 at `(17, 3)`; `ROUTE6_POKEFAN_M` occupies `(17, 4)` while `EVENT_ROUTE_5_6_POKEFAN_M_BLOCKS_UNDERGROUND_PATH` is clear.
2. `MAP_ROUTE_6_SAFFRON_GATE` - walk from `(4, 7)` to warp 1 at `(4, 0)`. Postcondition: on `MAP_SAFFRON_CITY` warp 12 `(16, 33)`; `MAPCALLBACK_NEWMAP` fires and `ENGINE_FLYPOINT_SAFFRON` becomes set.
3. `MAP_SAFFRON_CITY` - walk to warp 5 at `(27, 29)`. Enter `MAP_MR_PSYCHICS_HOUSE`.
4. `MAP_MR_PSYCHICS_HOUSE` - talk to `MRPSYCHICSHOUSE_FISHING_GURU` at `(5, 3)` (approach from `(4, 3)` facing right, he faces left). Precondition: `EVENT_GOT_TM29_PSYCHIC` clear and TM pocket has room. Postcondition: `TM_PSYCHIC_M` in bag, `EVENT_GOT_TM29_PSYCHIC` set. Exit warp 1 `(2, 7)`.
5. `MAP_SAFFRON_CITY` - walk to warp 7 at `(18, 21)`. Enter `MAP_SILPH_CO_1F`.
6. `MAP_SILPH_CO_1F` - talk to `SILPHCO1F_OFFICER` at `(13, 1)`. Precondition: `EVENT_GOT_UP_GRADE` clear, item pocket has room. Postcondition: `UP_GRADE` in bag, `EVENT_GOT_UP_GRADE` set. Exit warp 1 `(2, 7)`.
7. (Optional, dialogue only) `MAP_SAFFRON_CITY` warp 6 at `(8, 3)` -> `MAP_SAFFRON_MAGNET_TRAIN_STATION`; talk to `SAFFRONMAGNETTRAINSTATION_OFFICER` at `(9, 9)`. Expect the "isn't operating now" arm because `EVENT_RESTORED_POWER_TO_KANTO` is clear. Exit warp 1 `(8, 17)`.
8. (Optional) `MAP_SAFFRON_CITY` warp 3 at `(25, 11)` -> `MAP_SAFFRON_MART`; talk to `SAFFRONMART_CLERK` at `(1, 3)` for `MART_SAFFRON`. Exit warp 1 `(2, 7)`.
9. `MAP_SAFFRON_CITY` - warp 1 at `(26, 3)` -> `MAP_FIGHTING_DOJO`. Walk onto / face `FIGHTINGDOJO_POKE_BALL` at `(3, 1)`. Precondition: `EVENT_PICKED_UP_FOCUS_BAND` clear. Postcondition: `FOCUS_BAND` in bag, `EVENT_PICKED_UP_FOCUS_BAND` set (ball object masked from then on). Exit warp 1 `(4, 11)`.
10. (Optional heal) `MAP_SAFFRON_CITY` warp 4 at `(9, 29)` -> `MAP_SAFFRON_POKECENTER_1F`; talk to nurse at `(3, 1)`. Exit warp 1 `(3, 7)`.
11. `MAP_SAFFRON_CITY` - warp 2 at `(34, 3)` -> `MAP_SAFFRON_GYM`, landing on warp 1 `(8, 17)`.
12. `MAP_SAFFRON_GYM` - walk to the pad at `(11, 15)`. Warps to `(19, 17)`.
13. `MAP_SAFFRON_GYM` BR room - battle `SAFFRONGYM_GRANNY1` (Medium Rebecca) at `(17, 16)`, sight 3. Precondition: `EVENT_BEAT_MEDIUM_REBECCA` clear. Postcondition: that flag set. (Skippable: her flag is force-set by the Sabrina script.)
14. `MAP_SAFFRON_GYM` - step on pad `(15, 17)`. Warps to `(5, 15)`.
15. `MAP_SAFFRON_GYM` BL room - battle `SAFFRONGYM_YOUNGSTER1` (Psychic Franklin) at `(3, 16)`, sight 3. Postcondition: `EVENT_BEAT_PSYCHIC_FRANKLIN` set.
16. `MAP_SAFFRON_GYM` - step on pad `(1, 15)`. Warps to `(19, 5)`.
17. `MAP_SAFFRON_GYM` TR room - battle `SAFFRONGYM_YOUNGSTER2` (Psychic Jared) at `(17, 4)`, sight 2. Postcondition: `EVENT_BEAT_PSYCHIC_JARED` set.
18. `MAP_SAFFRON_GYM` - step on pad `(15, 5)`. Warps to `(1, 3)`.
19. `MAP_SAFFRON_GYM` TL room - battle `SAFFRONGYM_GRANNY2` (Medium Doris) at `(3, 4)`, sight 2. Postcondition: `EVENT_BEAT_MEDIUM_DORIS` set.
20. `MAP_SAFFRON_GYM` - step on pad `(1, 5)`. Warps to `(11, 9)`, Sabrina's chamber.
21. `MAP_SAFFRON_GYM` - talk to `SAFFRONGYM_SABRINA` at `(9, 8)` (stand at `(9, 9)` facing up, or `(10, 8)` facing left). Precondition: `ENGINE_MARSHBADGE` clear. Battle `SABRINA`/`SABRINA1`. Postcondition: `EVENT_BEAT_SABRINA`, the four trainer flags, and `ENGINE_MARSHBADGE` all set; +4800G.
22. `MAP_SAFFRON_GYM` exit - `(11, 9)` -> `(1, 5)`; walk right to `(5, 5)` -> `(1, 11)`; walk right to `(5, 11)` -> `(5, 17)`; walk up to `(5, 15)` -> `(15, 17)`; walk right to `(19, 17)` -> `(11, 15)`; walk down to warp 1/2 at `(8, 17)`/`(9, 17)` -> `MAP_SAFFRON_CITY` warp 2 `(34, 3)`.
23. Mid-gym heal-and-return (if step 21 needs a fresh party): exit via step 22, heal at `MAP_SAFFRON_POKECENTER_1F`, re-enter, then `(11,15) -> (19,17)`, up to `(19,15) -> (19,9)`, left to `(15,9) -> (15,3)`, down to `(15,5) -> (1,3)`, down to `(1,5) -> (11,9)`. This is the walkthrough's "1st teleporter, then up, left, down, down" and it checks out against the pad table.
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Map load, block grid, `COLL_*` collision, connections | `src/world/gen2/Map.lua`, `src/world/gen2/Permissions.lua`, `src/world/gen2/World.lua` | Implemented (generic; Gold maps come from `src/import/RomExtractorGen2.lua`, not hand-ported) |
| Warp panels / teleport pads (the whole gym maze) | `src/world/gen2/Permissions.lua:195` `COLL_WARP_PANEL = 0x7c` in `WARP_FACING_DOWN`, `Permissions.warpFacesDown`; warp taking in `src/world/gen2/World.lua:1566` `World:warpTo` | Implemented (panels warp on landing, which is what all 30 gym pads are) |
| Map-to-self warps (every gym pad targets `SAFFRON_GYM`) | `src/world/gen2/World.lua` warp handling | Implemented, but untested for this map - no driver exercises a same-map warp chain |
| `MAPCALLBACK_NEWMAP` -> `setflag ENGINE_FLYPOINT_SAFFRON` | `src/world/gen2/World.lua:5659` `self:runMapCallback("MAPCALLBACK_NEWMAP")`; fly-point row at `src/world/gen2/FieldMoves.lua:365` (`LANDMARK_SAFFRON_CITY` / `SPAWN_SAFFRON` / flag 59) | Implemented |
| Script opcodes used by every script in this section (`checkevent`, `iftrue`, `iffalse`, `setevent`, `checkflag`, `setflag`, `verbosegiveitem`, `promptbutton`, `writetext`, `waitbutton`, `jumptext`, `jumptextfaceplayer`, `jumpstd`, `gettrainername`, `pokemart`, `yesorno`, `checkitem`, `applymovement`, `special`, `warpcheck`, `newloadmap`, `loadtrainer`, `startbattle`, `reloadmapafterbattle`, `winlosstext`, `endifjustbattled`, `playsound`, `waitsfx`) | `src/script/gen2/Opcodes.lua`, `src/script/gen2/Vm.lua` (`checkflag` :195, `verbosegiveitem` :490, `pokemart` :597, `jumpstd` :742, `loadtrainer` :806, `startbattle` :817, `winlosstext` :918) | Implemented |
| Object visibility from event flags (the Route 6 blocker, the station's hidden Teacher/Lass, the picked-up Focus Band ball) | `src/world/gen2/Events.lua:1-2` documents and implements "flag SET -> object hidden", matching `CheckObjectFlag` | Implemented |
| Overworld trainer sight + battle handoff (Rebecca, Franklin, Doris, Jared) | `src/world/gen2/Trainers.lua` (eyesight from `home/trainers.asm`, party build) | Implemented |
| Gym leader battle, badge grant, prize money x4 | `src/battle/gen2/Battle.lua`; `src/battle/gen2/Prize.lua:82` `Prize.reward(baseMoney, level)` and `:181` `local quarter = ...` (the four-way payout) | Implemented |
| Item ball pickup (`FOCUS_BAND` in the Fighting Dojo) | Extracted at `src/import/RomExtractorGen2.lua:2968-2969` into `obj.itemball`, but **nothing reads `obj.itemball` at runtime** (grep for `.itemball` in `src/` hits only the extractor). `src/script/gen2/CallAsm.lua:551` explicitly stubs `TryReceiveItem` | **Missing** - the ball object will exist and be masked correctly by its flag, but stepping on it grants nothing |
| Magnet Train ride (`special MagnetTrain`) | `src/core/gen2/MagnetTrain.lua`, `src/ui/gen2/MagnetTrainRide.lua` | Implemented (not exercised in this section - the officer's gate is false here) |
| Mart (`pokemart MARTTYPE_STANDARD, MART_SAFFRON`) | `src/ui/gen2/MartMenu.lua`, `Vm.lua:597` | Implemented |
| Pokecenter heal (`jumpstd PokecenterNurseScript`) | std scripts extracted (`src/import/RomExtractorGen2.lua:2901`), heal hook `src/script/gen2/Specials.lua:451` | Implemented |
| Bicycle | `src/world/gen2/Bike.lua` | Implemented |
| Any Saffron-specific hand-ported content | none | **Missing by design** - the Gen 2 port is data-driven off the ROM; there are no `SAFFRON` symbols anywhere under `src/` except Gen 1 save-conversion tables (`src/save_convert/`), which are unrelated |
| Driver coverage for this section | `tests/drivers/gold_*.lua` (25 drivers: boot, walk, warp, trainer, battle, map callbacks, ...) | **Missing** - no driver reaches Kanto or Saffron; `gold_warp_scene.lua` / `gold_walk_smoke.lua` are Johto-early smoke tests |
## 6. Unresolved / verify by hand
- **Psychic Jared's third Pokemon.** The walkthrough lists "Level 32 Exeggcute (672 EXP)" twice. `data/trainers/parties.asm:2569-2574` gives `PSYCHIC_T (11) JARED` as L32 `MR__MIME`, L32 `EXEGGCUTE`, **L35** `EXEGGCUTE`. The asm wins; the walkthrough's second Exeggcute level and its EXP figure are both wrong.
- **EXP values.** None of the per-Pokemon EXP numbers in the walkthrough (765, 1237, 1149, 735, 672, 720, 1264, 1941, 1339, 1912) were checked against `data/pokemon/base_stats/` - they are outside what a bot needs and were not verified.
- **"PokeMart ... upper-right corner of Saffron City".** The mart door is warp 3 at `(25, 11)` on a 40x36-cell map, i.e. north-of-centre and east of the middle, not the corner. The two gyms it says are "further north" are at `(26, 3)` and `(34, 3)`, which is correct.
- **"Silph Co ... the multi-story building above the Pokémon Center".** Silph is warp 7 at `(18, 21)`; the Pokecenter is warp 4 at `(9, 29)`. Silph is north-**east** of it, and `SilphCo1F` has no stairs warp - only the two street doors. There is no upper floor in Gen 2.
- **"You now have 10 badges!"** `ENGINE_MARSHBADGE` is bit 5 of `wKantoBadges` (`constants/engine_flags.asm:52`), so it is the 6th Kanto badge in flag order, not the 10th badge overall. The count of 10 only holds for a player who took Vermilion before Saffron, as this FAQ's chapter order does. Nothing in the asm enforces that order.
- **The walkthrough's mid-maze "left/right (whichever way works) till you find that one trainer again (Franklin)" heal route** is deliberately vague in the source text and was not reduced to a single pad sequence. The precise post-heal route it gives afterwards ("1st teleporter, then up, left, down, down") *does* resolve cleanly and is recorded in bot checklist step 23.
- **The "large man who blocks the entire entrance to the Underground Path"** is described as taunting the player about moving boulders. `Route6PokefanMText` (`maps/Route6.asm:15`) contains only the Power Plant line; no boulder or strength text exists on this map. Treat the boulder remark as FAQ colour.
@@ -0,0 +1,896 @@
# Section 21 - Power Plant and Cerulean City Gym
Source: `../section-21-power-plant-and-cerulean-city-gym.txt`
Maps covered: `MAP_ROUTE_5`, `MAP_ROUTE_5_SAFFRON_GATE`, `MAP_ROUTE_5_CLEANSE_TAG_HOUSE`,
`MAP_ROUTE_5_UNDERGROUND_PATH_ENTRANCE`, `MAP_CERULEAN_CITY`, `MAP_CERULEAN_GYM`,
`MAP_ROUTE_9`, `MAP_ROUTE_10_NORTH`, `MAP_POWER_PLANT`, `MAP_ROUTE_24`,
`MAP_ROUTE_25`, `MAP_BILLS_HOUSE`
Badges / key milestones in this section: CASCADEBADGE (`ENGINE_CASCADEBADGE`) from Misty;
the whole Machine Part chain (`EVENT_MET_MANAGER_AT_POWER_PLANT` ->
`EVENT_MET_ROCKET_GRUNT_AT_CERULEAN_GYM` -> Route 24 grunt battle ->
`EVENT_FOUND_MACHINE_PART_IN_CERULEAN_GYM`); Misty's date on Route 25 clears
`EVENT_TRAINERS_IN_CERULEAN_GYM` and is what actually populates the gym.
Coordinate note: `warp_event` / `bg_event` / `object_event` / `coord_event` x,y are in
*map cells* (walk tiles), while `map_const NAME, W, H` in `constants/map_constants.asm`
is in *blocks*. Cells = 2x blocks, so `map_const ROUTE_9, 30, 9` is a 60x18 cell map and
`warp_event 48, 15` is inside it.
---
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 1 | `MAP_ROUTE_5_SAFFRON_GATE` | `maps/Route5SaffronGate.asm` | from `SAFFRON_CITY` warp 9 into gate warps 3/4 (`4,7` / `5,7`) | gate warps 1/2 (`4,0` / `5,0`) -> `ROUTE_5` warps 2/3 | "head north through the route-changing station" |
| 2 | `MAP_ROUTE_5` | `maps/Route5.asm` | gate warps | north connection -> `CERULEAN_CITY` | walk north; optional detour to the old Day Care building |
| 3 | `MAP_ROUTE_5_CLEANSE_TAG_HOUSE` | `maps/Route5CleanseTagHouse.asm` | `ROUTE_5` warp 4 at `10,11` | warps 1/2 at `2,7` / `3,7` -> `ROUTE_5` warp 4 | granny gives CLEANSE_TAG |
| 4 | `MAP_CERULEAN_CITY` | `maps/CeruleanCity.asm` | south connection from `ROUTE_5` | warp 5 at `30,23` -> `CERULEAN_GYM` | first (empty) gym visit |
| 5 | `MAP_CERULEAN_GYM` | `maps/CeruleanGym.asm` | `CERULEAN_CITY` warp 5 | warps 1/2 at `4,15` / `5,15` | gym is empty: `EVENT_TRAINERS_IN_CERULEAN_GYM` is still set from `InitializeEventsScript` |
| 6 | `MAP_ROUTE_9` | `maps/Route9.asm` | east connection from `CERULEAN_CITY` | south connection -> `ROUTE_10_NORTH` | six trainers; needs CUT at the west entrance |
| 7 | `MAP_ROUTE_10_NORTH` | `maps/Route10North.asm` | north connection from `ROUTE_9` | warp 2 at `3,9` -> `POWER_PLANT` | Pokemon Center at warp 1 (`11,1`); Surf the river to the plant |
| 8 | `MAP_POWER_PLANT` | `maps/PowerPlant.asm` | `ROUTE_10_NORTH` warp 2 | warps 1/2 at `2,17` / `3,17` | talk to the MANAGER, then trip the `5,12` coord event on the way out |
| 9 | `MAP_CERULEAN_CITY` | `maps/CeruleanCity.asm` | FLY (`SPAWN_CERULEAN`, `data/maps/spawn_points.asm:17`) | warp 5 -> `CERULEAN_GYM` | "Fly back to Cerulean City" |
| 10 | `MAP_CERULEAN_GYM` | `maps/CeruleanGym.asm` | `CERULEAN_CITY` warp 5 | warps 1/2 | `SCENE_CERULEANGYM_GRUNT_RUNS_OUT` fires on entry |
| 11 | `MAP_ROUTE_24` | `maps/Route24.asm` | north connection from `CERULEAN_CITY` | north connection -> `ROUTE_25` | Rocket grunt on the old Nugget Bridge |
| 12 | `MAP_ROUTE_25` | `maps/Route25.asm` | south connection from `ROUTE_24` | warp 1 at `47,5` -> `BILLS_HOUSE` | six-pack trainers, Kevin's Nugget, Protein, Misty's date coord events |
| 13 | `MAP_BILLS_HOUSE` | `maps/BillsHouse.asm` | `ROUTE_25` warp 1 | warps 1/2 at `2,7` / `3,7` | Bill's grandpa evolution-stone chain |
| 14 | `MAP_CERULEAN_CITY` -> `MAP_CERULEAN_GYM` | `maps/CeruleanGym.asm` | FLY, then warp 5 | - | three Swimmers, hidden MACHINE_PART at `3,8`, Misty |
Spills into the next section: `maps/Route9.asm` warp 1 at `48,15` leads to `ROCK_TUNNEL_1F`,
and the Machine Part is *returned* to `PowerPlantManager` (which is what unlocks the Magnet
Train and the Underground Path). The walkthrough text stops at Misty; Rock Tunnel / Lavender
and the return trip belong to the neighbouring sections.
## 2. Maps
### MAP_ROUTE_5
- Script: `maps/Route5.asm`
- Blocks: `maps/Route5.blk`
- Header: `data/maps/maps.asm:489` -> `map Route5, TILESET_KANTO, ROUTE, LANDMARK_ROUTE_5, MUSIC_ROUTE_3, FALSE, PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:453` -> `map_const ROUTE_5, 10, 9` (group `SAFFRON` = 25, id 1)
- Connections (`data/maps/attributes.asm:356`): north `CERULEAN_CITY` (offset -5), south `SAFFRON_CITY` (offset -5)
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 17 | 15 | `ROUTE_5_UNDERGROUND_PATH_ENTRANCE` | 1 |
| 2 | 8 | 17 | `ROUTE_5_SAFFRON_GATE` | 1 |
| 3 | 9 | 17 | `ROUTE_5_SAFFRON_GATE` | 2 |
| 4 | 10 | 11 | `ROUTE_5_CLEANSE_TAG_HOUSE` | 1 |
**Coord events** - none (`def_coord_events` is empty).
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 17 | 17 | `BGEVENT_READ` | `Route5UndergroundPathSign` |
| 10 | 11 | `BGEVENT_READ` | `HouseForSaleSign` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE5_POKEFAN_M` | `SPRITE_POKEFAN_M` | 17 | 16 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `Route5PokefanMScript` | `EVENT_ROUTE_5_6_POKEFAN_M_BLOCKS_UNDERGROUND_PATH` |
**Scripts of interest**
- `Route5PokefanMScript` - one `jumptextfaceplayer Route5PokefanMText`: "The road is closed
until the problem at the POWER PLANT is solved." He stands at `17,16`, directly below the
Underground Path door at `17,15`, so the tile is physically blocked. His object row carries
`EVENT_ROUTE_5_6_POKEFAN_M_BLOCKS_UNDERGROUND_PATH`; object events are hidden when their
flag is *set*, and that flag is not in `InitializeEventsScript`, so at this point in the
game it is clear and he is present. `PowerPlantManager` `setevent`s it after the part is
returned (`maps/PowerPlant.asm:164`), removing him.
- `Route5UndergroundPathSign` / `HouseForSaleSign` - plain `jumptext`. Note the "House for
Sale" sign shares the exact tile `10,11` with warp 4 into the Cleanse Tag house.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_ROUTE_5_6_POKEFAN_M_BLOCKS_UNDERGROUND_PATH` | `constants/event_flags.asm:1299` | object row `maps/Route5.asm:56` and `maps/Route6.asm:43`; `setevent` at `maps/PowerPlant.asm:164` | clear = NPC present and Underground Path door unreachable; set = NPC gone |
**Items** - none on the route proper.
**Wild encounters** - `data/wild/kanto_grass.asm:425` `def_grass_wildmons ROUTE_5`,
10/10/10 percent. Gold morn+day: L13 PIDGEY, L13 BELLSPROUT, L14 PIDGEY, L15 PIDGEY,
L12 ABRA, L14 ABRA, L14 ABRA. Gold nite: L13 ODDISH, L14 ODDISH, L13 BELLSPROUT,
L15 GLOOM, L12 ABRA, L14 ABRA, L14 ABRA. No water table. Fishing group `FISHGROUP_SHORE`
(`data/wild/fish.asm` `.Shore_*`).
---
### MAP_ROUTE_5_SAFFRON_GATE
- Script: `maps/Route5SaffronGate.asm`
- Blocks: shared, `data/maps/blocks.asm`
- Header: `data/maps/maps.asm:502` -> `map Route5SaffronGate, TILESET_GATE, GATE, LANDMARK_ROUTE_5, MUSIC_VIRIDIAN_CITY, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:466` -> `map_const ROUTE_5_SAFFRON_GATE, 5, 4` (group 25, id 14)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 0 | `ROUTE_5` | 2 |
| 2 | 5 | 0 | `ROUTE_5` | 3 |
| 3 | 4 | 7 | `SAFFRON_CITY` | 9 |
| 4 | 5 | 7 | `SAFFRON_CITY` | 9 |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE5SAFFRONGATE_OFFICER` | `SPRITE_OFFICER` | 0 | 4 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `Route5SaffronGateOfficerScript` | -1 |
Nothing here gates progress: the officer is a single `jumptextfaceplayer`, and there are no
coord or bg events.
---
### MAP_ROUTE_5_CLEANSE_TAG_HOUSE
- Script: `maps/Route5CleanseTagHouse.asm`
- Blocks: shared, `data/maps/blocks.asm:196` (`Route5CleanseTagHouse_Blocks`)
- Header: `data/maps/maps.asm:503` -> `map Route5CleanseTagHouse, TILESET_HOUSE, INDOOR, LANDMARK_ROUTE_5, MUSIC_VIRIDIAN_CITY, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:467` -> `map_const ROUTE_5_CLEANSE_TAG_HOUSE, 4, 4` (group 25, id 15)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 2 | 7 | `ROUTE_5` | 4 |
| 2 | 3 | 7 | `ROUTE_5` | 4 |
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 0 | 1 | `BGEVENT_READ` | `HouseForSaleBookshelf` (`jumpstd DifficultBookshelfScript`) |
| 1 | 1 | `BGEVENT_READ` | `HouseForSaleBookshelf` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE5CLEANSETAGHOUSE_GRANNY` | `SPRITE_GRANNY` | 2 | 5 | `SPRITEMOVEDATA_SPINCLOCKWISE` | `OBJECTTYPE_SCRIPT` | `Route5CleanseTagHouseGrannyScript` | -1 |
| `ROUTE5CLEANSETAGHOUSE_TEACHER` | `SPRITE_TEACHER` | 5 | 3 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `Route5CleanseTagHouseTeacherScript` | -1 |
**Scripts of interest**
- `Route5CleanseTagHouseGrannyScript` (`pokegold.sym` `61:57de`) -
`faceplayer` / `opentext` / `checkevent EVENT_GOT_CLEANSE_TAG` / `iftrue .GotCleanseTag`;
otherwise `writetext Route5CleanseTagHouseGrannyText1`, `promptbutton`,
`verbosegiveitem CLEANSE_TAG`, `iffalse .NoRoom`, `setevent EVENT_GOT_CLEANSE_TAG`,
then the "you are protected now" line. Bag-full drops straight to `.NoRoom` without
setting the flag, so a bot must have a free pocket slot or repeat the talk.
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `CLEANSE_TAG` | talk to `ROUTE5CLEANSETAGHOUSE_GRANNY` at `2,5` | `Route5CleanseTagHouseGrannyScript`, `verbosegiveitem CLEANSE_TAG` | `EVENT_GOT_CLEANSE_TAG` (`constants/event_flags.asm:218`) |
---
### MAP_ROUTE_5_UNDERGROUND_PATH_ENTRANCE
- Script: `maps/Route5UndergroundPathEntrance.asm`
- Header: `data/maps/maps.asm:501` -> `map Route5UndergroundPathEntrance, TILESET_GATE, GATE, LANDMARK_ROUTE_5, MUSIC_ROUTE_3, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:465` -> `map_const ROUTE_5_UNDERGROUND_PATH_ENTRANCE, 4, 4` (group 25, id 13)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `ROUTE_5` | 1 |
| 2 | 4 | 7 | `ROUTE_5` | 1 |
| 3 | 4 | 3 | `UNDERGROUND_PATH` | 1 |
Only reachable once the Route 5 Pokefan is gone. Listed here because the walkthrough calls
him out; nothing in this section actually enters it.
---
### MAP_CERULEAN_CITY
- Script: `maps/CeruleanCity.asm`
- Blocks: `maps/CeruleanCity.blk`
- Header: `data/maps/maps.asm:221` -> `map CeruleanCity, TILESET_KANTO, TOWN, LANDMARK_CERULEAN_CITY, MUSIC_VIRIDIAN_CITY, FALSE, PALETTE_AUTO, FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:203` -> `map_const CERULEAN_CITY, 20, 18` (group `CERULEAN` = 7, id 17)
- Connections (`data/maps/attributes.asm:360`): north `ROUTE_24` (+6), south `ROUTE_5` (+5), west `ROUTE_4` (+5), east `ROUTE_9` (+9)
- Fly / spawn: `data/maps/spawn_points.asm:17` `spawn CERULEAN_CITY, 19, 22`;
`data/maps/flypoints.asm:21` `db LANDMARK_CERULEAN_CITY, SPAWN_CERULEAN`
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 7 | 15 | `CERULEAN_GYM_BADGE_SPEECH_HOUSE` | 1 |
| 2 | 28 | 17 | `CERULEAN_POLICE_STATION` | 1 |
| 3 | 13 | 19 | `CERULEAN_TRADE_SPEECH_HOUSE` | 1 |
| 4 | 19 | 21 | `CERULEAN_POKECENTER_1F` | 1 |
| 5 | 30 | 23 | `CERULEAN_GYM` | 1 |
| 6 | 25 | 29 | `CERULEAN_MART` | 2 |
**Coord events** - none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 23 | 23 | `BGEVENT_READ` | `CeruleanCitySign` |
| 27 | 25 | `BGEVENT_READ` | `CeruleanGymSign` |
| 11 | 29 | `BGEVENT_READ` | `CeruleanBikeShopSign` ("The BIKE SHOP has moved to GOLDENROD") |
| 25 | 17 | `BGEVENT_READ` | `CeruleanPoliceSign` |
| 23 | 7 | `BGEVENT_READ` | `CeruleanCapeSign` |
| 14 | 29 | `BGEVENT_READ` | `CeruleanLockedDoor` |
| 20 | 21 | `BGEVENT_READ` | `CeruleanCityPokecenterSign` |
| 26 | 29 | `BGEVENT_READ` | `CeruleanCityMartSign` |
| 2 | 12 | `BGEVENT_ITEM` | `CeruleanCityHiddenBerserkGene` -> `hiddenitem BERSERK_GENE, EVENT_FOUND_BERSERK_GENE_IN_CERULEAN_CITY` (`pokegold.sym` `4f:60ad`) |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `CERULEANCITY_COOLTRAINER_M` | `SPRITE_COOLTRAINER_M` | 15 | 23 | `SPRITEMOVEDATA_WANDER` (2,2) | `OBJECTTYPE_SCRIPT` | `CeruleanCityCooltrainerMScript` | -1 |
| `CERULEANCITY_SUPER_NERD` | `SPRITE_SUPER_NERD` | 23 | 15 | `SPRITEMOVEDATA_WANDER` (1,1) | `OBJECTTYPE_SCRIPT` | `CeruleanCitySuperNerdScript` | -1 |
| `CERULEANCITY_SLOWPOKE` | `SPRITE_SLOWPOKE` | 20 | 24 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_SCRIPT` | `CeruleanCitySlowbro` | -1 |
| `CERULEANCITY_COOLTRAINER_F` | `SPRITE_COOLTRAINER_F` | 21 | 24 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `CeruleanCityCooltrainerFScript` | -1 |
| `CERULEANCITY_FISHER` | `SPRITE_FISHER` | 30 | 26 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (1,0) | `OBJECTTYPE_SCRIPT` | `CeruleanCityFisherScript` | -1 |
| `CERULEANCITY_YOUNGSTER` | `SPRITE_YOUNGSTER` | 6 | 12 | `SPRITEMOVEDATA_SPINRANDOM_SLOW` (1,0) | `OBJECTTYPE_SCRIPT` | `CeruleanCityYoungsterScript` | -1 |
**Scripts of interest**
- `CeruleanCityFlypointCallback` - `MAPCALLBACK_NEWMAP`, `setflag ENGINE_FLYPOINT_CERULEAN`
then `endcallback`. Simply entering the city registers the Fly destination the walkthrough
relies on twice.
- `CeruleanCityCooltrainerMScript` - `checkevent EVENT_RETURNED_MACHINE_PART`; before the
part is returned he is the hint "KANTO's POWER PLANT is toward the end of ROUTE 9".
- `CeruleanCityFisherScript` - `checkevent EVENT_RETURNED_MACHINE_PART` (iftrue -> generic
line), else `checkevent EVENT_MET_ROCKET_GRUNT_AT_CERULEAN_GYM` -> "I saw this shady guy
go off toward CERULEAN's CAPE", i.e. the pointer to Route 24/25.
- `CeruleanCityYoungsterScript` - after his first line, if
`EVENT_FOUND_BERSERK_GENE_IN_CERULEAN_CITY` is clear he plays the ITEMFINDER ping loop
(`SFX_SECOND_PART_OF_ITEMFINDER` / `SFX_TRANSACTION` x4) and `showemote EMOTE_SHOCK`.
That is the in-game hint for the hidden BERSERK_GENE at `2,12`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_FLYPOINT_CERULEAN` | `constants/engine_flags.asm:70` | `CeruleanCityFlypointCallback` | Fly target unlocked on first entry |
| `EVENT_MET_ROCKET_GRUNT_AT_CERULEAN_GYM` | `constants/event_flags.asm:202` | set by `CeruleanGymGruntRunsOutScript`; read by `CeruleanCityFisherScript` | grunt cutscene has played |
| `EVENT_RETURNED_MACHINE_PART` | `constants/event_flags.asm:200` | set by `PowerPlantManager`; read here and in ~10 other maps | post-section state |
| `EVENT_FOUND_BERSERK_GENE_IN_CERULEAN_CITY` | `constants/event_flags.asm:250` | `CeruleanCityHiddenBerserkGene`, `CeruleanCityYoungsterScript` | hidden item consumed |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `BERSERK_GENE` | face the tile and press A (hidden) | `bg_event 2, 12, BGEVENT_ITEM, CeruleanCityHiddenBerserkGene` | `EVENT_FOUND_BERSERK_GENE_IN_CERULEAN_CITY` |
**Wild encounters** - no grass table. Water: `data/wild/kanto_water.asm:131`
`def_water_wildmons CERULEAN_CITY`, 4 percent: L10 GOLDEEN, L5 GOLDEEN, L10 SEAKING.
Fishing group `FISHGROUP_LAKE`.
---
### MAP_CERULEAN_GYM
- Script: `maps/CeruleanGym.asm`
- Blocks: `maps/CeruleanGym.blk`
- Header: `data/maps/maps.asm:210` -> `map CeruleanGym, TILESET_PORT, INDOOR, LANDMARK_CERULEAN_CITY, MUSIC_GYM, TRUE, PALETTE_DAY, FISHGROUP_SHORE`
(the `data/maps/maps.asm:209` comment notes the known bug that you can fish in the gym pool)
- Dimensions: `constants/map_constants.asm:192` -> `map_const CERULEAN_GYM, 5, 8` (group 7, id 6)
- Scenes (`def_scene_scripts`, index order from `macros/scripts/maps.asm` `def_scene_scripts`/`scene_const`):
`SCENE_CERULEANGYM_NOOP` = 0, `SCENE_CERULEANGYM_GRUNT_RUNS_OUT` = 1.
Scene var: `data/maps/scenes.asm:13` `scene_var CERULEAN_GYM, wCeruleanGymSceneID`.
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 15 | `CERULEAN_CITY` | 5 |
| 2 | 5 | 15 | `CERULEAN_CITY` | 5 |
**Coord events** - none. The grunt cutscene runs from the *scene script*
(`CeruleanGymGruntRunsOutScene` -> `sdefer CeruleanGymGruntRunsOutScript`), which fires on
map load while `wCeruleanGymSceneID == SCENE_CERULEANGYM_GRUNT_RUNS_OUT`, not from a tile.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 3 | 8 | `BGEVENT_ITEM` | `CeruleanGymHiddenMachinePart` -> `hiddenitem MACHINE_PART, EVENT_FOUND_MACHINE_PART_IN_CERULEAN_GYM` (`pokegold.sym` `54:440e`) |
| 2 | 13 | `BGEVENT_READ` | `CeruleanGymStatue1` |
| 6 | 13 | `BGEVENT_READ` | `CeruleanGymStatue2` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `CERULEANGYM_ROCKET` | `SPRITE_ROCKET` | 4 | 10 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `ObjectEvent` (inert; driven by the scene script) | `EVENT_CERULEAN_GYM_ROCKET` |
| `CERULEANGYM_MISTY` | `SPRITE_MISTY` | 5 | 3 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `CeruleanGymMistyScript` | `EVENT_TRAINERS_IN_CERULEAN_GYM` |
| `CERULEANGYM_SWIMMER_GIRL1` | `SPRITE_SWIMMER_GIRL` | 4 | 6 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_TRAINER` (sight 3) | `TrainerSwimmerfDiana` | `EVENT_TRAINERS_IN_CERULEAN_GYM` |
| `CERULEANGYM_SWIMMER_GIRL2` | `SPRITE_SWIMMER_GIRL` | 1 | 9 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_TRAINER` (sight 1) | `TrainerSwimmerfBriana` | `EVENT_TRAINERS_IN_CERULEAN_GYM` |
| `CERULEANGYM_SWIMMER_GUY` | `SPRITE_SWIMMER_GUY` | 8 | 9 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_TRAINER` (sight 3) | `TrainerSwimmermParker` | `EVENT_TRAINERS_IN_CERULEAN_GYM` |
| `CERULEANGYM_GYM_GUIDE` | `SPRITE_GYM_GUIDE` | 7 | 13 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `CeruleanGymGuideScript` | `EVENT_TRAINERS_IN_CERULEAN_GYM` |
**Scripts of interest**
- `CeruleanGymGruntRunsOutScript` (`pokegold.sym` `54:4336`) - the grunt runs down four
`big_step DOWN`, `playsound SFX_TACKLE`, jumps into the player
(`CeruleanGymGruntRunsIntoYouMovement`: `fix_facing` / `set_sliding` / `jump_step UP`),
`playmusic MUSIC_ROCKET_ENCOUNTER`, three text boxes with a `showemote EMOTE_SHOCK`
between them, then `CeruleanGymGruntRunsOutMovement` (`big_step RIGHT`, `big_step DOWN`),
`playsound SFX_EXIT_BUILDING`, `disappear CERULEANGYM_ROCKET`. State changes, in order:
`setevent EVENT_MET_ROCKET_GRUNT_AT_CERULEAN_GYM`,
`clearevent EVENT_ROUTE_24_ROCKET` (spawns the grunt on Route 24),
`clearevent EVENT_ROUTE_25_MISTY_BOYFRIEND` (spawns Misty + her date on Route 25),
`setscene SCENE_CERULEANGYM_NOOP`,
`setmapscene ROUTE_25, SCENE_ROUTE25_MISTYS_DATE`,
`setmapscene POWER_PLANT, SCENE_POWERPLANT_NOOP`, then
`special RestartMapMusic` and `turnobject PLAYER, DOWN`.
- `CeruleanGymMistyScript` (`pokegold.sym` `54:438a`) - `checkflag ENGINE_CASCADEBADGE`,
`iftrue .FightDone`; else `MistyIntroText`, `winlosstext MistyWinLossText, 0`,
`loadtrainer MISTY, MISTY1`, `startbattle`, `reloadmapafterbattle`, then
`setevent EVENT_BEAT_MISTY` **and** `setevent EVENT_BEAT_SWIMMERF_DIANA`,
`EVENT_BEAT_SWIMMERF_BRIANA`, `EVENT_BEAT_SWIMMERM_PARKER` (beating Misty retroactively
marks the three gym trainers beaten), `ReceivedCascadeBadgeText`, `playsound SFX_GET_BADGE`,
`setflag ENGINE_CASCADEBADGE`.
- `CeruleanGymStatue1` / `CeruleanGymStatue2` - `checkevent EVENT_TRAINERS_IN_CERULEAN_GYM`;
if *false* (trainers present) they fall through to the shared `CeruleanGymStatue`
(`jumpstd GymStatue1Script`, or `GymStatue2Script` with `gettrainername STRING_BUFFER_4, MISTY, MISTY1`
once `ENGINE_CASCADEBADGE` is set). If true (gym empty) they print the "Sorry, I'll be out
for a while - MISTY" / "Since MISTY's out, we'll be away too" notes.
- `CeruleanGymGuideScript` - `checkevent EVENT_BEAT_MISTY` for the post-win line.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_TRAINERS_IN_CERULEAN_GYM` | `constants/event_flags.asm:1297` | set in `engine/events/std_scripts.asm:550` (`InitializeEventsScript`); cleared by `Route25MistyDate1Script` / `Route25MistyDate2Script` | set = Misty, three Swimmers and the guide are all hidden. This is why the first gym visit is empty. |
| `EVENT_CERULEAN_GYM_ROCKET` | `constants/event_flags.asm:1295` | set at `std_scripts.asm:547`; cleared by `PowerPlantManager` (`maps/PowerPlant.asm:146`) | clear = grunt object exists so the scene can run |
| `EVENT_FOUND_MACHINE_PART_IN_CERULEAN_GYM` | `constants/event_flags.asm:251` | set at `std_scripts.asm:546`; cleared by `PowerPlantManager` (`maps/PowerPlant.asm:147`) | set = the hidden item at `3,8` yields nothing. Talking to the MANAGER is what arms it. |
| `EVENT_MET_ROCKET_GRUNT_AT_CERULEAN_GYM` | `constants/event_flags.asm:202` | `CeruleanGymGruntRunsOutScript` | cutscene done |
| `EVENT_BEAT_MISTY` | `constants/event_flags.asm:716` | `CeruleanGymMistyScript` | gym cleared |
| `ENGINE_CASCADEBADGE` | `constants/engine_flags.asm:48` | `CeruleanGymMistyScript` `checkflag` / `setflag` | the badge itself |
| `SCENE_CERULEANGYM_GRUNT_RUNS_OUT` (= 1) | `maps/CeruleanGym.asm:12` via `scene_const` | `setmapscene` at `maps/PowerPlant.asm:148`; cleared to `SCENE_CERULEANGYM_NOOP` at `maps/CeruleanGym.asm:49` | entering the gym with this scene id plays the grunt scene |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `MACHINE_PART` | stand at the pool and press A into `3,8` | `bg_event 3, 8, BGEVENT_ITEM, CeruleanGymHiddenMachinePart` | `EVENT_FOUND_MACHINE_PART_IN_CERULEAN_GYM` (must be cleared first by the MANAGER) |
**Trainers**
| const | class | id | party (`data/trainers/parties.asm`) | script label | rematch/phone |
|---|---|---|---|---|---|
| `SWIMMERF, BRIANA` | `SWIMMERF` | 19 (`parties.asm:1885`) | L35 SEAKING, L35 SEAKING | `TrainerSwimmerfBriana` | none |
| `SWIMMERM, PARKER` | `SWIMMERM` | 21 (`parties.asm:1775`) | L32 HORSEA, L32 HORSEA, L35 SEADRA | `TrainerSwimmermParker` | none |
| `SWIMMERF, DIANA` | `SWIMMERF` | 18 (`parties.asm:1880`) | L37 GOLDUCK | `TrainerSwimmerfDiana` | none |
| `MISTY, MISTY1` | `MISTY` | 1 (`parties.asm:281`, `TRAINERTYPE_MOVES`) | L42 GOLDUCK (SURF, DISABLE, PSYCH_UP, PSYCHIC_M); L42 QUAGSIRE (SURF, AMNESIA, EARTHQUAKE, RAIN_DANCE); L44 LAPRAS (SURF, PERISH_SONG, BLIZZARD, RAIN_DANCE); L47 STARMIE (SURF, CONFUSE_RAY, RECOVER, ICE_BEAM) | `CeruleanGymMistyScript` | leader, no phone |
Base rewards (`data/trainers/attributes.asm`): SWIMMERF 5, SWIMMERM 2, MISTY 25. Payout is
`ComputeTrainerReward` (`engine/battle/read_trainer_party.asm:300`) = base x last mon level,
added four times by the `ld c, 4` loop in `engine/battle/core.asm:2340`, i.e. base x level x 4.
That reproduces every number in the walkthrough (Misty 25 x 47 x 4 = 4700).
---
### MAP_ROUTE_9
- Script: `maps/Route9.asm`
- Blocks: `maps/Route9.blk`
- Header: `data/maps/maps.asm:217` -> `map Route9, TILESET_KANTO, ROUTE, LANDMARK_ROUTE_9, MUSIC_ROUTE_3, FALSE, PALETTE_AUTO, FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:199` -> `map_const ROUTE_9, 30, 9` (group 7, id 13)
- Connections (`data/maps/attributes.asm:366`): south `ROUTE_10_NORTH` (+20), west `CERULEAN_CITY` (-9)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 48 | 15 | `ROCK_TUNNEL_1F` | 1 |
**Coord events** - none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 15 | 7 | `BGEVENT_READ` | `Route9Sign` |
| 10 | 5 | `BGEVENT_ITEM` | `Route9HiddenEther` -> `hiddenitem ETHER, EVENT_ROUTE_9_HIDDEN_ETHER` (`pokegold.sym` `50:407d`) |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE9_YOUNGSTER1` | `SPRITE_YOUNGSTER` | 23 | 11 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_TRAINER` (sight 3) | `TrainerCamperDean` | -1 |
| `ROUTE9_LASS1` | `SPRITE_LASS` | 35 | 8 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_TRAINER` (sight 2) | `TrainerPicnickerHeidi` | -1 |
| `ROUTE9_YOUNGSTER2` | `SPRITE_YOUNGSTER` | 11 | 2 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_TRAINER` (sight 2) | `TrainerCamperSid` | -1 |
| `ROUTE9_LASS2` | `SPRITE_LASS` | 9 | 10 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_TRAINER` (sight 1) | `TrainerPicnickerEdna` | -1 |
| `ROUTE9_POKEFAN_M1` | `SPRITE_POKEFAN_M` | 32 | 3 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_TRAINER` (sight 4) | `TrainerHikerTim` | -1 |
| `ROUTE9_POKEFAN_M2` | `SPRITE_POKEFAN_M` | 33 | 15 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_TRAINER` (sight 4) | `TrainerHikerSidney` | -1 |
**Scripts of interest** - all six are the plain `trainer CLASS, ID, EVENT_BEAT_*, seen, beaten, 0, .Script`
form with an `endifjustbattled` + one after-battle text. No flags beyond `EVENT_BEAT_*`.
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `ETHER` | hidden, face `10,5` | `bg_event 10, 5, BGEVENT_ITEM, Route9HiddenEther` | `EVENT_ROUTE_9_HIDDEN_ETHER` (`constants/event_flags.asm:242`) |
**Trainers**
| const | class | id | party | script label | rematch/phone |
|---|---|---|---|---|---|
| `PICNICKER, EDNA` | `PICNICKER` | 14 (`parties.asm:2659`) | L30 NIDORINA, L34 RAICHU | `TrainerPicnickerEdna` | none |
| `CAMPER, SID` | `CAMPER` | 8 (`parties.asm:2744`) | L32 DUGTRIO, L29 PRIMEAPE, L29 POLIWRATH | `TrainerCamperSid` | none |
| `CAMPER, DEAN` | `CAMPER` | 7 (`parties.asm:2738`) | L33 GOLDUCK, L31 SANDSLASH | `TrainerCamperDean` | none |
| `HIKER, SIDNEY` | `HIKER` | 15 (`parties.asm:2210`) | L34 DUGTRIO, L32 ONIX | `TrainerHikerSidney` | none |
| `HIKER, TIM` | `HIKER` | 13 (`parties.asm:2197`) | L31 GRAVELER x3 | `TrainerHikerTim` | none |
| `PICNICKER, HEIDI` | `PICNICKER` | 13 (`parties.asm:2653`) | L32 SKIPLOOM, L32 SKIPLOOM | `TrainerPicnickerHeidi` | none |
Base rewards: PICNICKER 5, CAMPER 5, HIKER 8 (`data/trainers/attributes.asm`).
**Wild encounters** - `data/wild/kanto_grass.asm:645` `def_grass_wildmons ROUTE_9`, 10/10/10 percent,
version-split with `IF DEF(_GOLD)`. Gold morn/day: L13 MANKEY, L15 RATTATA, L13 SPEAROW,
L15 RATICATE, L15 FEAROW, L15 PRIMEAPE, L15 PRIMEAPE. Gold nite: L13 MANKEY, L15 RATTATA,
L15 RATICATE, L13 RATTATA, L15 RATICATE, L15 PRIMEAPE, L15 PRIMEAPE. (Silver swaps MANKEY /
PRIMEAPE for RATTATA / RATICATE, which is exactly the "Gold only" note in the walkthrough.)
Water: `data/wild/kanto_water.asm:19`, 4 percent: L15 GOLDEEN, L10 GOLDEEN, L15 SEAKING.
Fishing group `FISHGROUP_LAKE`. No `treemon_maps.asm` row, so no headbutt table.
---
### MAP_ROUTE_10_NORTH
- Script: `maps/Route10North.asm`
- Blocks: `maps/Route10North.blk`
- Header: `data/maps/maps.asm:218` -> `map Route10North, TILESET_KANTO, ROUTE, LANDMARK_ROUTE_10, MUSIC_ROUTE_3, FALSE, PALETTE_AUTO, FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:200` -> `map_const ROUTE_10_NORTH, 10, 9` (group 7, id 14)
- Connections (`data/maps/attributes.asm:389`): north `ROUTE_9` (-20), south `ROUTE_10_SOUTH` (0)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 11 | 1 | `ROUTE_10_POKECENTER_1F` | 1 |
| 2 | 3 | 9 | `POWER_PLANT` | 1 |
**Coord events** - none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 5 | 11 | `BGEVENT_READ` | `PowerPlantSign` ("KANTO POWER PLANT") |
| 12 | 1 | `BGEVENT_READ` | `Route10PokecenterSign` (`jumpstd PokecenterSignScript`) |
**Object events** - `def_object_events` is empty. No trainers on this map.
**Wild encounters** - `data/wild/kanto_grass.asm:700` `def_grass_wildmons ROUTE_10_NORTH`,
10/10/10 percent, no version split.
morn: L16 SPEAROW, L17 VOLTORB, L16 RATICATE, L18 FEAROW, L16 QUAGSIRE, L15 ELECTABUZZ, L15 ELECTABUZZ.
day: L16 SPEAROW, L17 VOLTORB, L17 RATICATE, L18 FEAROW, L15 ELECTABUZZ, L17 ELECTABUZZ, L17 ELECTABUZZ.
nite: L16 QUAGSIRE, L17 VOLTORB, L16 RATICATE, L17 QUAGSIRE, L18 RATICATE, L15 ELECTABUZZ, L15 ELECTABUZZ.
Water: `data/wild/kanto_water.asm:26`, 4 percent: L15 GOLDEEN, L10 GOLDEEN, L15 SEAKING.
Fishing group `FISHGROUP_LAKE`.
---
### MAP_POWER_PLANT
- Script: `maps/PowerPlant.asm`
- Blocks: `maps/PowerPlant.blk`
- Header: `data/maps/maps.asm:214` -> `map PowerPlant, TILESET_FACILITY, INDOOR, LANDMARK_POWER_PLANT, MUSIC_VIRIDIAN_CITY, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:196` -> `map_const POWER_PLANT, 10, 9` (group 7, id 10)
- Scenes: `SCENE_POWERPLANT_NOOP` = 0, `SCENE_POWERPLANT_GUARD_GETS_PHONE_CALL` = 1
(`maps/PowerPlant.asm:11-12`); scene var `data/maps/scenes.asm:12`
`scene_var POWER_PLANT, wPowerPlantSceneID`
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 2 | 17 | `ROUTE_10_NORTH` | 2 |
| 2 | 3 | 17 | `ROUTE_10_NORTH` | 2 |
**Coord events**
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_POWERPLANT_GUARD_GETS_PHONE_CALL` (1) | 5 | 12 | `PowerPlantGuardPhoneScript` | the "shady character in CERULEAN" cutscene; ends with `setscene SCENE_POWERPLANT_NOOP` |
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 0 | 1 | `BGEVENT_READ` | `PowerPlantBookshelf` (`jumpstd DifficultBookshelfScript`) |
| 1 | 1 | `BGEVENT_READ` | `PowerPlantBookshelf` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `POWERPLANT_OFFICER1` | `SPRITE_OFFICER` | 4 | 14 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `PowerPlantOfficerScript` | -1 |
| `POWERPLANT_GYM_GUIDE1` | `SPRITE_GYM_GUIDE` | 2 | 9 | `SPRITEMOVEDATA_SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT` | `PowerPlantGymGuide1Script` | -1 |
| `POWERPLANT_GYM_GUIDE2` | `SPRITE_GYM_GUIDE` | 6 | 11 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_SCRIPT` | `PowerPlantGymGuide2Script` | -1 |
| `POWERPLANT_OFFICER2` | `SPRITE_OFFICER` | 9 | 3 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `PowerPlantOfficer2Script` | -1 |
| `POWERPLANT_GYM_GUIDE3` | `SPRITE_GYM_GUIDE` | 7 | 2 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (1,0) | `OBJECTTYPE_SCRIPT` | `PowerPlantGymGuide4Script` | -1 |
| `POWERPLANT_MANAGER` | `SPRITE_FISHER` | 14 | 10 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_SCRIPT` | `PowerPlantManager` | -1 |
(The object const list names the fifth guide `POWERPLANT_GYM_GUIDE3` while its script is
called `PowerPlantGymGuide4Script`; that mismatch is in the source, not a transcription slip.)
**Scripts of interest**
- `PowerPlantManager` (`pokegold.sym` `54:4dbd`) - the spine of the whole section:
1. `checkevent EVENT_RETURNED_MACHINE_PART` -> `.ReturnedMachinePart`
2. `checkitem MACHINE_PART` -> `.FoundMachinePart`
3. `checkevent EVENT_MET_MANAGER_AT_POWER_PLANT` -> `.MetManager`
4. first talk: `PowerPlantManagerWhoWouldRuinMyGeneratorText`, then
`setevent EVENT_MET_MANAGER_AT_POWER_PLANT`,
`clearevent EVENT_CERULEAN_GYM_ROCKET`,
`clearevent EVENT_FOUND_MACHINE_PART_IN_CERULEAN_GYM`,
`setmapscene CERULEAN_GYM, SCENE_CERULEANGYM_GRUNT_RUNS_OUT`,
`setscene SCENE_POWERPLANT_GUARD_GETS_PHONE_CALL`.
5. `.FoundMachinePart` (out of scope for this walkthrough section, listed because it is the
payoff): `takeitem MACHINE_PART`, `setevent EVENT_RETURNED_MACHINE_PART`,
`clearevent EVENT_SAFFRON_TRAIN_STATION_POPULATION`,
`setevent EVENT_ROUTE_5_6_POKEFAN_M_BLOCKS_UNDERGROUND_PATH`,
`setevent EVENT_ROUTE_24_ROCKET`, `setevent EVENT_RESTORED_POWER_TO_KANTO`,
`clearevent EVENT_GOLDENROD_TRAIN_STATION_GENTLEMAN`, then
`verbosegiveitem TM_ZAP_CANNON` guarded by `EVENT_GOT_TM07_ZAP_CANNON`.
- `PowerPlantGuardPhoneScript` - `playsound SFX_CALL`, `showemote EMOTE_SHOCK` on
`POWERPLANT_OFFICER1`, `applymovement POWERPLANT_OFFICER1 PowerPlantOfficer1ApproachGymGuide2Movement`
(RIGHT, RIGHT, UP, UP), two text boxes, then `PowerPlantOfficer1ReturnToPostMovement`
(DOWN, DOWN, LEFT, LEFT, turn_head DOWN) and `setscene SCENE_POWERPLANT_NOOP`.
- The four flavour NPCs all branch only on `EVENT_RETURNED_MACHINE_PART`
(`PowerPlantOfficerScript` additionally on `EVENT_MET_MANAGER_AT_POWER_PLANT`).
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_MET_MANAGER_AT_POWER_PLANT` | `constants/event_flags.asm:201` | `PowerPlantManager` | gate for everything downstream |
| `EVENT_CERULEAN_GYM_ROCKET` | `constants/event_flags.asm:1295` | cleared by `PowerPlantManager` | grunt object now exists in the gym |
| `EVENT_FOUND_MACHINE_PART_IN_CERULEAN_GYM` | `constants/event_flags.asm:251` | cleared by `PowerPlantManager` | the hidden item is now live |
| `SCENE_POWERPLANT_GUARD_GETS_PHONE_CALL` (1) | `maps/PowerPlant.asm:12` | `setscene`, then the `5,12` coord event | forces the exit cutscene once |
| `EVENT_RETURNED_MACHINE_PART` / `EVENT_RESTORED_POWER_TO_KANTO` | `constants/event_flags.asm:200` / `:204` | `.FoundMachinePart` | later-section state (Magnet Train, Underground Path, roaming beasts checks in `maps/Route26.asm` etc.) |
| `EVENT_GOT_TM07_ZAP_CANNON` | `constants/event_flags.asm:222` | `.ReturnedMachinePart` | TM07 reward |
**Items** - `TM_ZAP_CANNON` from the MANAGER, but only on the *return* trip, which the
walkthrough section does not cover.
**Trainers / wild encounters** - none.
---
### MAP_ROUTE_24
- Script: `maps/Route24.asm`
- Blocks: `maps/Route24.blk`
- Header: `data/maps/maps.asm:219` -> `map Route24, TILESET_KANTO, ROUTE, LANDMARK_ROUTE_24, MUSIC_ROUTE_3, FALSE, PALETTE_AUTO, FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:201` -> `map_const ROUTE_24, 10, 9` (group 7, id 15)
- Connections (`data/maps/attributes.asm:370`): north `ROUTE_25` (0), south `CERULEAN_CITY` (-6)
**Warps** - `def_warp_events` is empty.
**Coord events** - empty.
**BG events** - empty.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE24_ROCKET` | `SPRITE_ROCKET` | 8 | 7 | `SPRITEMOVEDATA_SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT` | `Route24RocketScript` | `EVENT_ROUTE_24_ROCKET` |
**Scripts of interest**
- `Route24RocketScript` (`pokegold.sym` `50:4407`) - `faceplayer`,
`playmusic MUSIC_ROCKET_ENCOUNTER`, `Route24RocketSeenText`,
`winlosstext Route24RocketBeatenText, -1`, `loadtrainer GRUNTM, GRUNTM_31`, `startbattle`,
`dontrestartmapmusic`, `reloadmapafterbattle`, then `Route24RocketAfterBattleText`
(the "MACHINE PART ... hide it I did in GYM of the CERULEAN ... Inside water put it I did"
hint), `special FadeOutMusic`, `Route24RocketDisappearsText`, `special FadeOutToBlack`,
`special ReloadSpritesNoPalettes`, `disappear ROUTE24_ROCKET`, `pause 25`,
`special FadeInFromBlack`, `playmapmusic`.
Note: he is *talked to*, not a sight-line trainer, and there is no `EVENT_BEAT_*` -
`disappear` sets `EVENT_ROUTE_24_ROCKET` and that is the only record of the win.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_ROUTE_24_ROCKET` | `constants/event_flags.asm:1294` | set at `engine/events/std_scripts.asm:548`; cleared by `CeruleanGymGruntRunsOutScript`; set again by `disappear` and by `PowerPlantManager:165` | clear = grunt is standing on the bridge at `8,7` |
**Trainers**
| const | class | id | party | script label | rematch/phone |
|---|---|---|---|---|---|
| `GRUNTM, GRUNTM_31` | `GRUNTM` | 31 (`data/trainers/parties.asm:1340`) | L30 GOLBAT | `Route24RocketScript` | none |
**Wild encounters** - `data/wild/kanto_grass.asm:1061` `def_grass_wildmons ROUTE_24`, 10/10/10 percent.
morn: L8 BELLSPROUT, L10 BELLSPROUT, L9 ABRA, L12 WEEPINBELL, L8 VENONAT, L14 WEEPINBELL, L14 WEEPINBELL.
day: L8 BELLSPROUT, L10 SUNKERN, L9 ABRA, L12 WEEPINBELL, L10 BELLSPROUT, L14 WEEPINBELL, L14 WEEPINBELL.
nite: L8 VENONAT, L10 ODDISH, L9 ABRA, L13 WEEPINBELL, L10 BELLSPROUT, L10 VENOMOTH, L10 VENOMOTH.
Water: `data/wild/kanto_water.asm:75`, 4 percent: L10 GOLDEEN, L5 GOLDEEN, L10 SEAKING.
---
### MAP_ROUTE_25
- Script: `maps/Route25.asm`
- Blocks: `maps/Route25.blk`
- Header: `data/maps/maps.asm:220` -> `map Route25, TILESET_KANTO, ROUTE, LANDMARK_ROUTE_25, MUSIC_ROUTE_3, FALSE, PALETTE_AUTO, FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:202` -> `map_const ROUTE_25, 30, 9` (group 7, id 16)
- Connections (`data/maps/attributes.asm:374`): south `ROUTE_24` (0). No other connection - the
route is a dead end at Bill's house.
- Scenes: `SCENE_ROUTE25_NOOP` = 0, `SCENE_ROUTE25_MISTYS_DATE` = 1 (`maps/Route25.asm:16-17`);
scene var `data/maps/scenes.asm:14`
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 47 | 5 | `BILLS_HOUSE` | 1 |
**Coord events**
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_ROUTE25_MISTYS_DATE` (1) | 42 | 6 | `Route25MistyDate1Script` (`pokegold.sym` `50:4781`) | the date cutscene, upper approach |
| `SCENE_ROUTE25_MISTYS_DATE` (1) | 42 | 7 | `Route25MistyDate2Script` | same cutscene, lower approach |
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 45 | 5 | `BGEVENT_READ` | `BillsHouseSign` ("SEA COTTAGE / BILL'S HOUSE") |
| 4 | 5 | `BGEVENT_ITEM` | `Route25HiddenPotion` -> `hiddenitem POTION, EVENT_ROUTE_25_HIDDEN_POTION` (`pokegold.sym` `50:48c6`) |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE25_MISTY` | `SPRITE_MISTY` | 46 | 9 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `ObjectEvent` (inert) | `EVENT_ROUTE_25_MISTY_BOYFRIEND` |
| `ROUTE25_COOLTRAINER_M1` | `SPRITE_COOLTRAINER_M` | 46 | 10 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `ObjectEvent` (inert) | `EVENT_ROUTE_25_MISTY_BOYFRIEND` |
| `ROUTE25_YOUNGSTER1` | `SPRITE_YOUNGSTER` | 12 | 8 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_TRAINER` (sight 3) | `TrainerSchoolboyDudley` | -1 |
| `ROUTE25_LASS1` | `SPRITE_LASS` | 16 | 11 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_TRAINER` (sight 3) | `TrainerLassEllen` | -1 |
| `ROUTE25_YOUNGSTER2` | `SPRITE_YOUNGSTER` | 21 | 8 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_TRAINER` (sight 1) | `TrainerSchoolboyJoe` | -1 |
| `ROUTE25_LASS2` | `SPRITE_LASS` | 22 | 6 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_TRAINER` (sight 3) | `TrainerLassLaura` | -1 |
| `ROUTE25_YOUNGSTER3` | `SPRITE_YOUNGSTER` | 25 | 4 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_TRAINER` (sight 2) | `TrainerCamperLloyd` | -1 |
| `ROUTE25_LASS3` | `SPRITE_LASS` | 28 | 11 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_TRAINER` (sight 1) | `TrainerLassShannon` | -1 |
| `ROUTE25_SUPER_NERD` | `SPRITE_SUPER_NERD` | 31 | 7 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_TRAINER` (sight 1) | `TrainerSupernerdPat` | -1 |
| `ROUTE25_COOLTRAINER_M2` | `SPRITE_COOLTRAINER_M` | 37 | 8 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `TrainerCooltrainermKevin` | -1 |
| `ROUTE25_POKE_BALL` | `SPRITE_POKE_BALL` | 32 | 4 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `Route25Protein` (`itemball PROTEIN`, `pokegold.sym` `50:48c4`) | `EVENT_ROUTE_25_PROTEIN` |
**Scripts of interest**
- `Route25MistyDate1Script` / `Route25MistyDate2Script` - identical except for the approach
movement. `showemote EMOTE_HEART` on Misty, `pause 30`, `showemote EMOTE_SHOCK` on the
boyfriend, `applymovement ROUTE25_COOLTRAINER_M1` (one or two `big_step DOWN`),
`disappear ROUTE25_COOLTRAINER_M1`, `playmusic MUSIC_BEAUTY_ENCOUNTER`, Misty walks over
(`Route25MistyApproachesPlayerMovement1` = UP,UP,UP,LEFT,LEFT,LEFT; variant 2 drops one UP),
`writetext Route25MistyDateText` (the "pest" speech that ends with "Come to CERULEAN GYM"),
Misty leaves via `Route25MistyLeavesMovement` (LEFT x5), `disappear ROUTE25_MISTY`,
**`clearevent EVENT_TRAINERS_IN_CERULEAN_GYM`**, `setscene SCENE_ROUTE25_NOOP`,
`special RestartMapMusic`.
- `TrainerCooltrainermKevin` - not a `trainer` macro; a hand-written script.
`checkevent EVENT_BEAT_COOLTRAINERM_KEVIN` -> after-battle text;
`checkevent EVENT_CLEARED_NUGGET_BRIDGE` -> skip the prize;
otherwise `CooltrainermKevinNuggetText`, `verbosegiveitem NUGGET`, `iffalse .NoRoomForNugget`,
`setevent EVENT_CLEARED_NUGGET_BRIDGE`, then `winlosstext CooltrainermKevinBeatenText, 0`,
`loadtrainer COOLTRAINERM, KEVIN`, `startbattle`, `reloadmapafterbattle`,
`setevent EVENT_BEAT_COOLTRAINERM_KEVIN`.
A full bag at the Nugget step aborts before the battle.
- The six "six-pack" trainers are plain `trainer` macros with `EVENT_BEAT_*` flags only.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_ROUTE_25_MISTY_BOYFRIEND` | `constants/event_flags.asm:1296` | set at `engine/events/std_scripts.asm:549`; cleared by `CeruleanGymGruntRunsOutScript` | clear = the pair are standing at `46,9` / `46,10` |
| `SCENE_ROUTE25_MISTYS_DATE` (1) | `maps/Route25.asm:17` | `setmapscene` from `maps/CeruleanGym.asm:50` | arms the two coord events |
| `EVENT_TRAINERS_IN_CERULEAN_GYM` | `constants/event_flags.asm:1297` | cleared here | the gym becomes populated |
| `EVENT_CLEARED_NUGGET_BRIDGE` | `constants/event_flags.asm:215` | `TrainerCooltrainermKevin` | Nugget already handed over |
| `EVENT_BEAT_COOLTRAINERM_KEVIN` | `constants/event_flags.asm:873` | `TrainerCooltrainermKevin` | Kevin battle done |
| `EVENT_ROUTE_25_HIDDEN_POTION` | `constants/event_flags.asm:248` | `Route25HiddenPotion` | hidden Potion taken |
| `EVENT_ROUTE_25_PROTEIN` | `constants/event_flags.asm:1326` | item ball object row | Protein ball taken |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `POTION` | hidden, face `4,5` | `bg_event 4, 5, BGEVENT_ITEM, Route25HiddenPotion` | `EVENT_ROUTE_25_HIDDEN_POTION` |
| `NUGGET` | talk to Kevin at `37,8` | `TrainerCooltrainermKevin`, `verbosegiveitem NUGGET` | `EVENT_CLEARED_NUGGET_BRIDGE` |
| `PROTEIN` | item ball at `32,4` | `object_event ... OBJECTTYPE_ITEMBALL, 0, Route25Protein` | `EVENT_ROUTE_25_PROTEIN` |
**Trainers**
| const | class | id | party | script label | rematch/phone |
|---|---|---|---|---|---|
| `SCHOOLBOY, DUDLEY` | `SCHOOLBOY` | 7 (`parties.asm:448`) | L35 ODDISH | `TrainerSchoolboyDudley` | none |
| `LASS, ELLEN` | `LASS` | 11 (`parties.asm:703`) | L30 WIGGLYTUFF, L34 GRANBULL | `TrainerLassEllen` | none |
| `SCHOOLBOY, JOE` | `SCHOOLBOY` | 8 (`parties.asm:453`) | L33 TANGELA, L33 VAPOREON | `TrainerSchoolboyJoe` | none |
| `LASS, LAURA` | `LASS` | 7 (`parties.asm:676`) | L28 GLOOM, L31 PIDGEOTTO, L31 BELLOSSOM | `TrainerLassLaura` | none |
| `CAMPER, LLOYD` | `CAMPER` | 6 (`parties.asm:2733`) | L34 NIDOKING | `TrainerCamperLloyd` | none |
| `LASS, SHANNON` | `LASS` | 8 (`parties.asm:683`) | L29 PARAS, L29 PARAS, L32 PARASECT | `TrainerLassShannon` | none |
| `SUPER_NERD, PAT` | `SUPER_NERD` | 8 (`parties.asm:2009`) | L36 PORYGON | `TrainerSupernerdPat` | none |
| `COOLTRAINERM, KEVIN` | `COOLTRAINERM` | 17 (`parties.asm:847`) | L38 RHYHORN, L35 CHARMELEON, L35 WARTORTLE | `TrainerCooltrainermKevin` | none |
Base rewards: SCHOOLBOY 8, LASS 6, CAMPER 5, SUPER_NERD 8, COOLTRAINERM 12.
**Wild encounters** - `data/wild/kanto_grass.asm:1089` `def_grass_wildmons ROUTE_25`, 10/10/10 percent.
morn: L8 PIDGEY, L10 BELLSPROUT, L8 VENONAT, L9 ABRA, L10 PIDGEOTTO, L14 WEEPINBELL, L14 WEEPINBELL.
day: L8 PIDGEY, L10 BELLSPROUT, L10 PIDGEY, L9 ABRA, L12 PIDGEOTTO, L14 WEEPINBELL, L14 WEEPINBELL.
nite: L8 VENONAT, L10 ODDISH, L10 VENOMOTH, L9 ABRA, L14 WEEPINBELL, L10 BELLSPROUT, L10 BELLSPROUT.
Water: `data/wild/kanto_water.asm:82`, 4 percent: L10 GOLDEEN, L5 GOLDEEN, L10 SEAKING.
---
### MAP_BILLS_HOUSE
- Script: `maps/BillsHouse.asm`
- Blocks: shared, `data/maps/blocks.asm:163` (`BillsHouse_Blocks`)
- Header: `data/maps/maps.asm:215` -> `map BillsHouse, TILESET_HOUSE, INDOOR, LANDMARK_ROUTE_25, MUSIC_VIRIDIAN_CITY, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:197` -> `map_const BILLS_HOUSE, 4, 4` (group 7, id 11)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 2 | 7 | `ROUTE_25` | 1 |
| 2 | 3 | 7 | `ROUTE_25` | 1 |
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `BILLSHOUSE_GRAMPS` | `SPRITE_GRAMPS` | 2 | 3 | `SPRITEMOVEDATA_STANDING_UP` (radius 0,2) | `OBJECTTYPE_SCRIPT` | `BillsGrandpa` (`pokegold.sym` `54:547c`) | -1 |
**Scripts of interest**
- `BillsGrandpa` - a five-step chain, each step gated on the previous. Order in the asm:
1. LICKITUNG -> `EVENT_SHOWED_LICKITUNG_TO_BILLS_GRANDPA` -> `verbosegiveitem EVERSTONE`
(`EVENT_GOT_EVERSTONE_FROM_BILLS_GRANDPA`)
2. ODDISH -> `EVENT_SHOWED_ODDISH_TO_BILLS_GRANDPA` -> `verbosegiveitem LEAF_STONE`
(`EVENT_GOT_LEAF_STONE_FROM_BILLS_GRANDPA`)
3. STARYU -> `EVENT_SHOWED_STARYU_TO_BILLS_GRANDPA` -> `verbosegiveitem WATER_STONE`
(`EVENT_GOT_WATER_STONE_FROM_BILLS_GRANDPA`)
4. GROWLITHE (Gold) / VULPIX (Silver, selected by `checkver` / `iftrue .AskVulpix`) ->
`EVENT_SHOWED_GROWLITHE_VULPIX_TO_BILLS_GRANDPA` -> `verbosegiveitem FIRE_STONE`
(`EVENT_GOT_FIRE_STONE_FROM_BILLS_GRANDPA`)
5. PICHU -> `EVENT_SHOWED_PICHU_TO_BILLS_GRANDPA` -> `verbosegiveitem THUNDERSTONE`
(`EVENT_GOT_THUNDERSTONE_FROM_BILLS_GRANDPA`, which also ends the chain)
Mechanics: `writetext ... AskToSeeMonText`, `yesorno`, `scall .ExcitedToSee`,
`special BillsGrandfather` (party pick; `iffalse` = cancelled), `ifnotequal <SPECIES>, .WrongPokemon`.
`EVENT_TEMPORARY_UNTIL_MAP_RELOAD_1` (`constants/event_flags.asm:5`) is set after each
successful hand-over, so he only accepts **one mon per visit** - a bot must leave and
re-enter the house between stones.
- First talk sets `EVENT_MET_BILLS_GRANDPA` (`constants/event_flags.asm:457`).
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `EVERSTONE` | show LICKITUNG | `BillsGrandpa` | `EVENT_GOT_EVERSTONE_FROM_BILLS_GRANDPA` |
| `LEAF_STONE` | show ODDISH | `BillsGrandpa` | `EVENT_GOT_LEAF_STONE_FROM_BILLS_GRANDPA` |
| `WATER_STONE` | show STARYU | `BillsGrandpa` | `EVENT_GOT_WATER_STONE_FROM_BILLS_GRANDPA` |
| `FIRE_STONE` | show GROWLITHE (Gold) / VULPIX (Silver) | `BillsGrandpa` | `EVENT_GOT_FIRE_STONE_FROM_BILLS_GRANDPA` |
| `THUNDERSTONE` | show PICHU | `BillsGrandpa` | `EVENT_GOT_THUNDERSTONE_FROM_BILLS_GRANDPA` (`:467`) |
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Cut tree at the Route 9 west entrance (and the tree above Super Nerd Pat on Route 25) | `engine/events/overworld.asm:117` `CutFunction` -> `.CheckAble` (`ld de, ENGINE_HIVEBADGE` / `CheckBadge`, then `CheckMapForSomethingToCut` -> `engine/overworld/tile_events.asm:76` `CheckCutCollision`). The trees themselves are block data in `maps/Route9.blk` / `maps/Route25.blk`, not object events. | HIVEBADGE + a party mon with CUT | already held (Johto badge 2) |
| Surfing the Route 9 / Route 10 river to reach the Power Plant door at `3,9` | `engine/events/overworld.asm:322` `SurfFunction` `.TrySurf` (`ld de, ENGINE_FOGBADGE` / `CheckBadge`, then `GetTilePermission` == `WATER_TILE`); the overworld A-press path is `engine/events/overworld.asm:469` `TrySurfOW` with the same `ENGINE_FOGBADGE` `CheckEngineFlag` | FOGBADGE + SURF | already held (Johto badge 6) |
| "Fly back to Cerulean City" | `engine/events/overworld.asm:529` `FlyFunction` (`ld de, ENGINE_STORMBADGE` at `:545`), destination row `data/maps/flypoints.asm:21` | STORMBADGE + FLY + `ENGINE_FLYPOINT_CERULEAN` set by `CeruleanCityFlypointCallback` | flypoint flag set on first entry to Cerulean |
| Cerulean Gym is empty on the first visit | `maps/CeruleanGym.asm:379-383` object rows all carry `EVENT_TRAINERS_IN_CERULEAN_GYM`, set at `engine/events/std_scripts.asm:550` | - | `clearevent` in `Route25MistyDate1Script` / `Route25MistyDate2Script` (`maps/Route25.asm:48` / `:74`) |
| Rocket grunt does not exist in the gym | `maps/CeruleanGym.asm:378` object row `EVENT_CERULEAN_GYM_ROCKET`, set at `std_scripts.asm:547`; the scene is `SCENE_CERULEANGYM_NOOP` by default | - | first talk to `PowerPlantManager` (`clearevent` + `setmapscene CERULEAN_GYM, SCENE_CERULEANGYM_GRUNT_RUNS_OUT`, `maps/PowerPlant.asm:146-148`) |
| Rocket grunt does not exist on Route 24 | `maps/Route24.asm:129` object row `EVENT_ROUTE_24_ROCKET`, set at `std_scripts.asm:548` | - | `clearevent` in `CeruleanGymGruntRunsOutScript` (`maps/CeruleanGym.asm:47`) |
| Misty + date do not exist on Route 25, and the two coord events are inert | `maps/Route25.asm:445-446` object rows `EVENT_ROUTE_25_MISTY_BOYFRIEND`; coord rows require `SCENE_ROUTE25_MISTYS_DATE` | - | `clearevent` + `setmapscene ROUTE_25, SCENE_ROUTE25_MISTYS_DATE` in `CeruleanGymGruntRunsOutScript` (`maps/CeruleanGym.asm:48`, `:50`) |
| Hidden MACHINE_PART at Cerulean Gym `3,8` yields nothing | `maps/CeruleanGym.asm:373` -> `CeruleanGymHiddenMachinePart`, flag set at `std_scripts.asm:546` | - | `clearevent EVENT_FOUND_MACHINE_PART_IN_CERULEAN_GYM` in `PowerPlantManager` (`maps/PowerPlant.asm:147`) |
| Route 5 Underground Path door at `17,15` physically blocked | `maps/Route5.asm:56` NPC standing on `17,16` | - | `setevent EVENT_ROUTE_5_6_POKEFAN_M_BLOCKS_UNDERGROUND_PATH` in `PowerPlantManager` `.FoundMachinePart` (`maps/PowerPlant.asm:164`) - i.e. **after** this section |
| Bill's grandpa gives one stone per visit | `maps/BillsHouse.asm` `.JustShowedSomething` on `EVENT_TEMPORARY_UNTIL_MAP_RELOAD_1` | - | leave and re-enter the map |
## 4. Bot checklist
Ordered, machine-actionable. `(g,m)` is `(map group, map id)` from `constants/map_constants.asm`.
1. `ROUTE_5_SAFFRON_GATE (25,14)`: walk onto warp 1/2 (`4,0` / `5,0`) -> `ROUTE_5`. Pre: none. Post: none.
2. `ROUTE_5 (25,1)`: optional - walk to warp 4 (`10,11`), enter `ROUTE_5_CLEANSE_TAG_HOUSE (25,15)`, talk to `ROUTE5CLEANSETAGHOUSE_GRANNY` at `2,5`. Pre: `EVENT_GOT_CLEANSE_TAG` clear + free bag slot. Post: `EVENT_GOT_CLEANSE_TAG` set, CLEANSE_TAG in bag. Exit via warp 1/2 (`2,7`/`3,7`).
3. `ROUTE_5`: walk north across the connection into `CERULEAN_CITY (7,17)`. Post: `ENGINE_FLYPOINT_CERULEAN` set by `MAPCALLBACK_NEWMAP`.
4. `CERULEAN_CITY`: optional - face `2,12` and press A for BERSERK_GENE. Pre: `EVENT_FOUND_BERSERK_GENE_IN_CERULEAN_CITY` clear.
5. `CERULEAN_CITY`: heal at warp 4 (`19,21`, `CERULEAN_POKECENTER_1F`). Optional but the walkthrough does it twice.
6. `CERULEAN_CITY`: warp 5 (`30,23`) -> `CERULEAN_GYM (7,6)`. Expect an empty room (`EVENT_TRAINERS_IN_CERULEAN_GYM` set). Read `2,13` / `6,13` for the two notes. Exit via `4,15`/`5,15`.
7. `CERULEAN_CITY`: exit east into `ROUTE_9 (7,13)`. Pre: HIVEBADGE + CUT for the entrance tree.
8. `ROUTE_9`: battle in walkthrough order - `TrainerPicnickerEdna` (`9,10`), `TrainerCamperSid` (`11,2`), `TrainerCamperDean` (`23,11`), `TrainerHikerSidney` (`33,15`), `TrainerHikerTim` (`32,3`), `TrainerPicnickerHeidi` (`35,8`). Post: `EVENT_BEAT_*` per trainer. Optional hidden ETHER at `10,5`.
9. `ROUTE_9`: cross the south connection into `ROUTE_10_NORTH (7,14)`. Optional heal at warp 1 (`11,1`).
10. `ROUTE_10_NORTH`: Surf (pre: FOGBADGE + SURF) down the river and step onto warp 2 (`3,9`) -> `POWER_PLANT (7,10)`.
11. `POWER_PLANT`: walk to `POWERPLANT_MANAGER` at `14,10` and talk. Pre: `EVENT_MET_MANAGER_AT_POWER_PLANT` clear. Post: `EVENT_MET_MANAGER_AT_POWER_PLANT` set, `EVENT_CERULEAN_GYM_ROCKET` cleared, `EVENT_FOUND_MACHINE_PART_IN_CERULEAN_GYM` cleared, `CERULEAN_GYM` scene = 1, `POWER_PLANT` scene = 1.
12. `POWER_PLANT`: walk over `5,12` on the way to the exit. Post: `PowerPlantGuardPhoneScript` runs, `POWER_PLANT` scene back to 0. Then exit via warp 1/2 (`2,17`/`3,17`).
13. FLY to `CERULEAN_CITY` (`SPAWN_CERULEAN` = `19,22`). Pre: STORMBADGE + FLY + `ENGINE_FLYPOINT_CERULEAN`.
14. `CERULEAN_CITY`: warp 5 -> `CERULEAN_GYM`. On load the scene script fires; hold through the movement/text (four A presses: intro, big-mistake, bye, plus the emote pauses). Post: `EVENT_MET_ROCKET_GRUNT_AT_CERULEAN_GYM` set, `EVENT_ROUTE_24_ROCKET` cleared, `EVENT_ROUTE_25_MISTY_BOYFRIEND` cleared, `ROUTE_25` scene = 1, `CERULEAN_GYM` scene = 0.
15. `CERULEAN_CITY`: exit north into `ROUTE_24 (7,15)`. Talk to `ROUTE24_ROCKET` at `8,7`. Battle `GRUNTM, GRUNTM_31` (L30 GOLBAT). Post: `disappear` sets `EVENT_ROUTE_24_ROCKET`.
16. `ROUTE_24`: north connection into `ROUTE_25 (7,16)`.
17. `ROUTE_25`: optional hidden POTION - face `4,5`, press A. Pre: `EVENT_ROUTE_25_HIDDEN_POTION` clear.
18. `ROUTE_25`: battle `TrainerSchoolboyDudley` (`12,8`), `TrainerLassEllen` (`16,11`), `TrainerSchoolboyJoe` (`21,8`), `TrainerLassLaura` (`22,6`), `TrainerCamperLloyd` (`25,4`), `TrainerLassShannon` (`28,11`), `TrainerSupernerdPat` (`31,7`).
19. `ROUTE_25`: talk to `ROUTE25_COOLTRAINER_M2` at `37,8`. Pre: free bag slot. Post: NUGGET, `EVENT_CLEARED_NUGGET_BRIDGE`, then battle `COOLTRAINERM, KEVIN` and `EVENT_BEAT_COOLTRAINERM_KEVIN`.
20. `ROUTE_25`: CUT the tree, walk onto the item ball at `32,4` for PROTEIN. Post: `EVENT_ROUTE_25_PROTEIN`.
21. `ROUTE_25`: walk east onto `42,6` (or `42,7`). Pre: `ROUTE_25` scene = `SCENE_ROUTE25_MISTYS_DATE`. Post: date cutscene, `EVENT_TRAINERS_IN_CERULEAN_GYM` cleared, `ROUTE_25` scene = 0.
22. `ROUTE_25`: warp 1 (`47,5`) -> `BILLS_HOUSE (7,11)`. Talk to `BILLSHOUSE_GRAMPS` at `2,3`. One stone per map load; loop enter/talk/exit until the wanted stones are collected. Post: `EVENT_MET_BILLS_GRANDPA`, then the `EVENT_GOT_*_FROM_BILLS_GRANDPA` chain.
23. FLY to `CERULEAN_CITY`, heal at warp 4, then warp 5 -> `CERULEAN_GYM`.
24. `CERULEAN_GYM`: battle `TrainerSwimmerfBriana` (`1,9`), then face `3,8` and press A for MACHINE_PART. Pre: `EVENT_FOUND_MACHINE_PART_IN_CERULEAN_GYM` clear + free bag slot. Post: flag set, MACHINE_PART in bag.
25. `CERULEAN_GYM`: battle `TrainerSwimmermParker` (`8,9`), `TrainerSwimmerfDiana` (`4,6`).
26. `CERULEAN_GYM`: talk to `CERULEANGYM_MISTY` at `5,3`. Pre: `ENGINE_CASCADEBADGE` clear. Post: `EVENT_BEAT_MISTY`, `EVENT_BEAT_SWIMMERF_DIANA`, `EVENT_BEAT_SWIMMERF_BRIANA`, `EVENT_BEAT_SWIMMERM_PARKER`, `ENGINE_CASCADEBADGE` set, +4700 money.
Follow-up (next section): carry MACHINE_PART back to `POWERPLANT_MANAGER` at `14,10` to set
`EVENT_RETURNED_MACHINE_PART` / `EVENT_RESTORED_POWER_TO_KANTO` and collect TM07 ZAP CANNON.
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Kanto map headers, warps, coord/bg/object event tables extracted from ROM | `src/import/RomExtractorGen2.lua` (`OBJECTTYPE_ITEMBALL, OBJECTTYPE_TRAINER = 1, 2` at :71; branch at :2960-2968) | implemented - generic, all map groups, nothing Kanto-specific needed |
| Coord-event trip-wires (Power Plant `5,12`, Route 25 `42,6`/`42,7`) with the scene id filter | `src/world/gen2/World.lua:5006` `World:tryCoordScript` | implemented |
| Scene scripts on map load + `sdefer` (`CeruleanGymGruntRunsOutScene`) | `src/world/gen2/World.lua` `World:trySceneScript`; `src/script/gen2/Vm.lua:92` | partial - `sdefer` is run immediately rather than queued until the map settles (comment at `Vm.lua:93` says so). Cosmetic ordering risk on the gym grunt scene. |
| `setscene` / `setmapscene` / `checkmapscene` | `src/script/gen2/Vm.lua:274`, `:279`, `:284` | implemented, including the `0xff` "no scene var" sentinel |
| `checkevent` / `setevent` / `clearevent` and `checkflag` / `setflag` (`ENGINE_CASCADEBADGE`, `ENGINE_FLYPOINT_CERULEAN`) | `src/script/gen2/Vm.lua:198-229`, `engineFlags` store at `:1661` | implemented - generic id-keyed stores, no per-flag work needed |
| Hidden items (`BGEVENT_ITEM`): MACHINE_PART, ETHER, POTION, BERSERK_GENE | `src/world/gen2/HiddenItems.lua` | implemented (the header notes this was previously unreachable because `World:bgEventAt` only answered `BGEVENT_READ`) |
| Item ball (`Route25Protein`) | `src/import/RomExtractorGen2.lua:2968` (`OBJECTTYPE_ITEMBALL`) | implemented |
| Trainer sight lines / `OBJECTTYPE_TRAINER` sight radius | `src/world/gen2/Trainers.lua:98` `Trainers.sees` | implemented |
| `loadtrainer` / `startbattle` / `winlosstext` / `reloadmapafterbattle` / `dontrestartmapmusic` / `endifjustbattled` | `src/script/gen2/Vm.lua` | implemented |
| Prize money = base x level x 4 (Misty 4700G etc.) | `src/battle/gen2/Prize.lua:12` (documents the `ld c, 4` loop), `Prize.reward` at `:82` | implemented, incl. Amulet Coin and Mom's savings split |
| CUT gate on HIVEBADGE + cut-tree collision | `src/world/gen2/FieldMoves.lua:440` `badgeGate` / `:452`; `src/world/gen2/Permissions.lua:130` `CUT_TREE`, `:152` `CheckCutCollision` set | implemented |
| SURF gate on FOGBADGE | `src/world/gen2/FieldMoves.lua:482` | implemented |
| FLY gate on STORMBADGE + flypoint table | `src/world/gen2/FieldMoves.lua:342` `FieldMoves.FLYPOINTS`, `:501`; `src/world/gen2/World.lua:4284` (`_FlyMap`) | implemented |
| `special BillsGrandfather` (party pick for the stone chain) | `src/script/gen2/Specials.lua:1267` | implemented |
| `special FadeOutMusic` / `FadeOutToBlack` / `FadeInFromBlack` / `ReloadSpritesNoPalettes` / `RestartMapMusic` (Route 24 grunt exit, gym grunt scene) | `src/script/gen2/Specials.lua:999`, `:1001`, `:1025`, `:1059`, `:1066` | implemented |
| `showemote`, `applymovement` (incl. `big_step`, `jump_step`, `fix_facing`, `set_sliding`, `slow_step`), `turnobject`, `disappear`/`appear` | `src/script/gen2/Vm.lua`, `src/script/gen2/Movement.lua` | implemented |
| Kanto Gen 2 wild tables (`kanto_grass`/`kanto_water`, Gold/Silver split on Route 9) | extracted by `src/import/RomExtractorGen2.lua`; consumed by `src/battle/gen2/Encounter.lua` | implemented - version split comes from the ROM, so it is correct by construction |
| End-to-end driver coverage for this stretch | `tests/drivers/gold_*.lua` (25 drivers: boot, walk, warp, trainer, battle, roamers, ice path, etc.) | missing - no Kanto / Cerulean / Power Plant driver exists |
## 6. Unresolved / verify by hand
- **Cut tree positions.** The walkthrough says "Cut the tree at the entrance" (Route 9) and
"Cut the tree above Pat" (Route 25). Cut trees are block data in `maps/Route9.blk` /
`maps/Route25.blk`, not events, so no coordinate for them appears anywhere in the asm text.
A bot needs the decoded block map (or the port's `Permissions.isCutTree` over the loaded
collision) to find them. The Protein ball at `32,4` sitting two cells above Pat at `31,7`
is consistent with the walkthrough's claim, but the tree cell itself is unverified here.
- **Kevin's battle is omitted from the walkthrough.** The text says only "that guy will give
you a Nugget". `TrainerCooltrainermKevin` gives the NUGGET and then immediately battles you
with L38 RHYHORN / L35 CHARMELEON / L35 WARTORTLE. Treat the walkthrough as incomplete here,
not the asm.
- **Wild lists are abridged in the walkthrough.** Route 24 also has VENONAT / SUNKERN /
ODDISH / VENOMOTH, and Route 25 also has PIDGEY / PIDGEOTTO / VENONAT / WEEPINBELL /
ODDISH / VENOMOTH, none of which the FAQ lists. Route 5's own table (PIDGEY / BELLSPROUT /
ABRA / ODDISH / GLOOM) is not listed at all.
- **"Surf down the river ... and you'll be at the Power Plant."** The Power Plant door is
`warp_event 3, 9` on `ROUTE_10_NORTH`, and the water table for both `ROUTE_9` and
`ROUTE_10_NORTH` exists, so surfing is clearly the route - but the exact water path across
the Route 9 / Route 10 North connection boundary is block data and was not traced.
- **EXP values.** Every per-mon EXP number in the walkthrough (e.g. "Level 42 Golduck
(1566 EXP)") depends on the player's own level and participation; nothing in
`data/trainers/parties.asm` carries them. Money values were all reproduced from
`data/trainers/attributes.asm` base reward x last-mon level x 4 and check out.
- **"the 11th gym badge".** The asm has no badge counter tied to this; `ENGINE_CASCADEBADGE`
is just one bit in `constants/engine_flags.asm:48`. The ordinal is walkthrough prose.
- **Route 5 "Day Care Center from Red, Blue and Yellow".** The building is
`ROUTE_5_CLEANSE_TAG_HOUSE` and its sign at `10,11` reads "House for Sale... Nobody lives
here." Nothing in Gen 2 calls it a Day Care; that identification is the FAQ's own history
note. Also note the sign and the door share the tile `10,11`, so an A-press there may hit
either depending on facing - worth verifying against the port's bg-event-vs-warp priority.
- **`POWERPLANT_GYM_GUIDE3` / `PowerPlantGymGuide4Script` naming mismatch** in
`maps/PowerPlant.asm` (there is no `PowerPlantGymGuide3Script`). Harmless, but a
label-driven bot should not assume the const and script numbers line up.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,919 @@
# Section 24 - Snorlax and Pewter City Gym
Source: `../section-24-snorlax-and-pewter-city-gym.txt`
(the FAQ numbers this chapter "30 > Snorlax and Pewter City Gym"; the file index
is 24)
Maps covered: `MAP_ROUTE_19`, `MAP_ROUTE_19_FUCHSIA_GATE`, `MAP_VERMILION_CITY`,
`MAP_VERMILION_POKECENTER_1F`, `MAP_DIGLETTS_CAVE`, `MAP_ROUTE_2`,
`MAP_ROUTE_2_NUGGET_HOUSE`, `MAP_ROUTE_2_GATE`, `MAP_PEWTER_CITY`,
`MAP_PEWTER_POKECENTER_1F`, `MAP_PEWTER_GYM`
Badges / key milestones in this section:
- Level 50 **SNORLAX** caught in Vermilion City, holding LEFTOVERS
(`BATTLETYPE_FORCEITEM`); clearing it opens the DIGLETT'S CAVE door
- **SILVER_WING** (Gold) / **RAINBOW_WING** (Silver) from the Pewter City gramps
- **NUGGET** from the Route 2 house
- CARBOS, DIRE_HIT, ELIXER, MAX_POTION item balls on Route 2
- **BOULDERBADGE** from BROCK (`ENGINE_BOULDERBADGE`)
---
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 0 | `FUCHSIA_CITY` | - | previous section | south connection, or `ROUTE_19_FUCHSIA_GATE` warps 1/2 at (4,0)/(5,0) | **Belongs to the previous section**; only the hop south is listed |
| 1 | `ROUTE_19_FUCHSIA_GATE` | `maps/Route19FuchsiaGate.asm` | warps 1/2 at (4,0)/(5,0) from `FUCHSIA_CITY` 10/11 | warps 3/4 at (4,7)/(5,7) -> `ROUTE_19` 1 | Officer explains why Route 19 is shut |
| 2 | `ROUTE_19` | `maps/Route19.asm` | warp 1 at (7,3) from the gate | dead end - back through warp 1 | **Blocked.** `Route19ClearRocksCallback` paints six boulders while `EVENT_CINNABAR_ROCKS_CLEARED` is clear |
| 3 | `VERMILION_CITY` | `maps/VermilionCity.asm` | FLY (`ENGINE_FLYPOINT_VERMILION`, set by `VermilionCityFlypointCallback`) | warp 10 at (34,7) -> `DIGLETTS_CAVE` 1 | Wake and battle the SNORLAX at (34,8) with the POKe FLUTE radio channel |
| 4 | `VERMILION_POKECENTER_1F` | `maps/VermilionPokecenter1F.asm` | city warp 2 at (9,5) | warps 1/2 at (3,7)/(4,7) back | Heal and withdraw the caught SNORLAX |
| 5 | `DIGLETTS_CAVE` | `maps/DiglettsCave.asm` | warp 1 at (3,33) from `VERMILION_CITY` 10 | warp 3 at (15,5) -> `ROUTE_2` 5 | Ladder chain 2->5, long cave, ladder 6->4, then the door |
| 6 | `ROUTE_2` | `maps/Route2.asm` | warp 5 at (12,7) from `DIGLETTS_CAVE` 3 | north connection to `PEWTER_CITY` | Carbos, Nugget house, Elixer, three Bug Catchers, two cut trees |
| 7 | `ROUTE_2_NUGGET_HOUSE` | `maps/Route2NuggetHouse.asm` | Route 2 warp 1 at (15,15) | warps 1/2 at (2,7)/(3,7) back | `Route2NuggetHouseFisherScript` -> `verbosegiveitem NUGGET` |
| 8 | `ROUTE_2_GATE` | `maps/Route2Gate.asm` | Route 2 warps 3/4 at (16,27)/(17,27) | warps 3/4 at (4,7)/(5,7) -> `ROUTE_2` 2 at (15,31) | The "route-changing house" that drops you further south on the same route |
| 9 | `PEWTER_CITY` | `maps/PewterCity.asm` | south connection from `ROUTE_2` (`connection north, PewterCity, PEWTER_CITY, -5`) | five warps, see below | Sets `ENGINE_FLYPOINT_PEWTER`; gramps hands over the wing |
| 10 | `PEWTER_POKECENTER_1F` | `maps/PewterPokecenter1F.asm` | city warp 4 at (13,25) | warps 1/2 at (3,7)/(4,7) back | Heal; `Chris` runs `trade NPC_TRADE_CHRIS` (GLOOM -> RAPIDASH) |
| 11 | `PEWTER_GYM` | `maps/PewterGym.asm` | city warp 2 at (16,17) | warps 1/2 at (4,13)/(5,13) back | Camper Jerry, then **BROCK** for the BOULDERBADGE |
Spillover: the FAQ's video-link header also names Tin Tower, Ho-Oh, Routes 1-4
and Pallet Town. Nothing in the prose of this section enters those maps - the
Routes 3/4 and Pallet beats belong to the following section and are not covered
here.
---
## 2. Maps
### MAP_ROUTE_19
- Script: `maps/Route19.asm`
- Blocks: `maps/Route19.blk`
- Header: `data/maps/maps.asm:196` -> `TILESET_KANTO, ROUTE, LANDMARK_ROUTE_19, MUSIC_ROUTE_3, FALSE (phone allowed), PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:180` `map_const ROUTE_19, 10, 18` (group `CINNABAR`, id 5) = 20 x 36 cells
- Attributes / border: `data/maps/attributes.asm:287` `map_attributes Route19, ROUTE_19, $43`
- Connections: `connection north, FuchsiaCity, FUCHSIA_CITY, 0`; `connection west, Route20, ROUTE_20, 9`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 7 | 3 | `ROUTE_19_FUCHSIA_GATE` | 3 |
**Coord events** (`def_coord_events`)
none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 11 | 13 | `BGEVENT_READ` | `Route19Sign` |
| 11 | 1 | `BGEVENT_READ` | `CarefulSwimmingSign` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE19_SWIMMER_GIRL` | `SPRITE_SWIMMER_GIRL` | 9 | 23 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_TRAINER` (sight 0) | `TrainerSwimmerfDawn` | -1 |
| `ROUTE19_SWIMMER_GUY1` | `SPRITE_SWIMMER_GUY` | 13 | 28 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` (sight 3) | `TrainerSwimmermHarold` | -1 |
| `ROUTE19_SWIMMER_GUY2` | `SPRITE_SWIMMER_GUY` | 11 | 17 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` (sight 3) | `TrainerSwimmermJerome` | -1 |
| `ROUTE19_SWIMMER_GUY3` | `SPRITE_SWIMMER_GUY` | 8 | 23 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_TRAINER` (sight 0) | `TrainerSwimmermTucker` | -1 |
| `ROUTE19_FISHER1` | `SPRITE_FISHER` | 9 | 5 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` (sight 1) | `Route19Fisher1Script` | -1 |
| `ROUTE19_FISHER2` | `SPRITE_FISHER` | 11 | 5 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (radius 1,0) | `OBJECTTYPE_SCRIPT` (sight 1) | `Route19Fisher2Script` | -1 |
**Scripts of interest**
- `Route19ClearRocksCallback` (`callback MAPCALLBACK_TILES`). `checkevent
EVENT_CINNABAR_ROCKS_CLEARED` / `iftrue .Done`; otherwise it runs six
`changeblock` writes that lay a rock block (`$7a`) over block coordinates
`(6,6) (8,6) (10,6) (12,8) (4,8) (10,10)`. This is the wall the FAQ hits.
Block coordinates, so the blocked cells are `x = 2*bx .. 2*bx+1`,
`y = 2*by .. 2*by+1`.
- `Route19Fisher1Script` / `Route19Fisher2Script` both branch on
`EVENT_CINNABAR_ROCKS_CLEARED` for flavour text only. Neither one clears the
rocks - see "Blockers and gates".
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_CINNABAR_ROCKS_CLEARED` | `constants/event_flags.asm:214` | read by `Route19ClearRocksCallback`, `Route19Fisher1Script`, `Route19Fisher2Script`, `Route19FuchsiaGateOfficerScript`; **set** by `Route20ClearRocksCallback` (`maps/Route20.asm:13`, `MAPCALLBACK_NEWMAP`) | Route 19 stays blocked until the player has stood on Route 20, which is reached from the Cinnabar side (Pallet -> Route 21 -> Cinnabar -> Route 20) |
**Items** - none on this map.
**Trainers** - four Swimmers, all reachable only by Surf and all beyond the
boulders in the FAQ's play order; not part of this section's critical path.
---
### MAP_ROUTE_19_FUCHSIA_GATE
- Script: `maps/Route19FuchsiaGate.asm`
- Header: `data/maps/maps.asm:194` -> `TILESET_GATE, GATE, LANDMARK_ROUTE_19, MUSIC_ROUTE_3, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:178` `map_const ROUTE_19_FUCHSIA_GATE, 5, 4` (group `CINNABAR`, id 3)
- Attributes: `data/maps/attributes.asm:512` `map_attributes Route19FuchsiaGate, ROUTE_19_FUCHSIA_GATE, $00`; no connections
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 0 | `FUCHSIA_CITY` | 10 |
| 2 | 5 | 0 | `FUCHSIA_CITY` | 11 |
| 3 | 4 | 7 | `ROUTE_19` | 1 |
| 4 | 5 | 7 | `ROUTE_19` | 1 |
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE19FUCHSIAGATE_OFFICER` | `SPRITE_OFFICER` | 0 | 4 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `Route19FuchsiaGateOfficerScript` | -1 |
**Scripts of interest**
- `Route19FuchsiaGateOfficerScript`: `checkevent EVENT_CINNABAR_ROCKS_CLEARED`,
two text branches. Pure flavour; the officer does not gate the warp.
---
### MAP_VERMILION_CITY
- Script: `maps/VermilionCity.asm`
- Blocks: `maps/VermilionCity.blk`
- Header: `data/maps/maps.asm:296` -> `TILESET_KANTO, TOWN, LANDMARK_VERMILION_CITY, MUSIC_VERMILION_CITY, FALSE, PALETTE_AUTO, FISHGROUP_OCEAN`
- Dimensions: `constants/map_constants.asm:273` `map_const VERMILION_CITY, 20, 18` (group `VERMILION`, id 3) = 40 x 36 cells
- Attributes: `data/maps/attributes.asm:342` `map_attributes VermilionCity, VERMILION_CITY, $43`
- Connections: `connection north, Route6, ROUTE_6, 5`; `connection east, Route11, ROUTE_11, 0`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 5 | 5 | `VERMILION_FISHING_SPEECH_HOUSE` | 1 |
| 2 | 9 | 5 | `VERMILION_POKECENTER_1F` | 1 |
| 3 | 7 | 13 | `POKEMON_FAN_CLUB` | 1 |
| 4 | 13 | 13 | `VERMILION_MAGNET_TRAIN_SPEECH_HOUSE` | 1 |
| 5 | 21 | 13 | `VERMILION_MART` | 2 |
| 6 | 21 | 17 | `VERMILION_DIGLETTS_CAVE_SPEECH_HOUSE` | 1 |
| 7 | 10 | 19 | `VERMILION_GYM` | 1 |
| 8 | 19 | 31 | `VERMILION_PORT_PASSAGE` | 1 |
| 9 | 20 | 31 | `VERMILION_PORT_PASSAGE` | 2 |
| 10 | 34 | 7 | `DIGLETTS_CAVE` | 1 |
**Coord events** (`def_coord_events`) - none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 25 | 3 | `BGEVENT_READ` | `VermilionCitySign` |
| 5 | 19 | `BGEVENT_READ` | `VermilionGymSign` |
| 5 | 13 | `BGEVENT_READ` | `PokemonFanClubSign` |
| 33 | 9 | `BGEVENT_READ` | `VermilionCityDiglettsCaveSign` |
| 27 | 15 | `BGEVENT_READ` | `VermilionCityPortSign` |
| 10 | 5 | `BGEVENT_READ` | `VermilionCityPokecenterSign` (`jumpstd PokecenterSignScript`) |
| 22 | 13 | `BGEVENT_READ` | `VermilionCityMartSign` (`jumpstd MartSignScript`) |
| 12 | 19 | `BGEVENT_ITEM` | `VermilionCityHiddenFullHeal` -> `hiddenitem FULL_HEAL, EVENT_VERMILION_CITY_HIDDEN_FULL_HEAL` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `VERMILIONCITY_TEACHER` | `SPRITE_TEACHER` | 18 | 9 | `SPRITEMOVEDATA_WANDER` (1,1) | `OBJECTTYPE_SCRIPT` | `VermilionCityTeacherScript` | -1 |
| `VERMILIONCITY_GRAMPS` | `SPRITE_GRAMPS` | 23 | 6 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `VermilionMachopOwner` | -1 |
| `VERMILIONCITY_MACHOP` | `SPRITE_MACHOP` | 26 | 7 | `SPRITEMOVEDATA_POKEMON` | `OBJECTTYPE_SCRIPT` | `VermilionMachop` | -1 |
| `VERMILIONCITY_SUPER_NERD` | `SPRITE_SUPER_NERD` | 14 | 16 | `SPRITEMOVEDATA_WANDER` (1,1) | `OBJECTTYPE_SCRIPT` | `VermilionCitySuperNerdScript` | -1 |
| `VERMILIONCITY_BIG_SNORLAX` | `SPRITE_BIG_SNORLAX` | 34 | 8 | `SPRITEMOVEDATA_BIGDOLLSYM` | `OBJECTTYPE_SCRIPT` | `VermilionSnorlax` | `EVENT_VERMILION_CITY_SNORLAX` |
| `VERMILIONCITY_POKEFAN_M` | `SPRITE_POKEFAN_M` | 31 | 12 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `VermilionGymBadgeGuy` | -1 |
Note the object-flag polarity (`CheckObjectFlag`,
`engine/overworld/map_objects_2.asm:32`): an object with an `EVENT_*` flag is
**masked when the flag is set**. So the SNORLAX is present only while
`EVENT_VERMILION_CITY_SNORLAX` is clear, and `disappear` is what sets it.
**Scripts of interest**
- `VermilionSnorlax` (`4f:5291`). `opentext` -> `special SnorlaxAwake` ->
`iftrue .Awake`. Asleep branch writes `VermilionCitySnorlaxSleepingText` and
ends. `.Awake` writes `VermilionCityRadioNearSnorlaxText`, `pause 15`,
`cry SNORLAX`, then
`loadvar VAR_BATTLETYPE, BATTLETYPE_FORCEITEM` /
`loadwildmon SNORLAX, 50` / `startbattle` /
`disappear VERMILIONCITY_BIG_SNORLAX` / `setevent EVENT_FOUGHT_SNORLAX` /
`reloadmapafterbattle`.
The `disappear` and `setevent` run **unconditionally after the battle**, so a
fled-from or fainted Snorlax is still gone: catching it is a one-shot.
- `SnorlaxAwake` (`engine/events/specials.asm:358`, symbol `03:45d6`). Returns
`TRUE` only when *both* hold:
1. `[wMapMusic] == MUSIC_POKE_FLUTE_CHANNEL`;
2. the player stands on one of `.ProximityCoords`
(`engine/events/specials.asm:399`): `(33,8) (34,10) (35,10) (36,8) (36,9)`.
The POKe FLUTE channel is the Pokegear radio at knob 78 / "20.0"
(`engine/pokegear/pokegear.asm:1447`, `.PokeFluteRadio` at 1487): it needs
**not-Johto** and `POKEGEAR_EXPN_CARD_F` in `wPokegearFlags`. Tuning the radio
and closing the Pokegear leaves that song as `wMapMusic`.
- `VermilionGymBadgeGuy`: `checkevent EVENT_GOT_HP_UP_FROM_VERMILION_GUY`, then
`readvar VAR_BADGES` with `ifequal NUM_BADGES` / `ifgreater 13` /
`ifgreater 9`. `verbosegiveitem HP_UP` only at all 16 badges. Not this
section's business, but a bot walking Vermilion will trip the text.
- `VermilionCityFlypointCallback` (`MAPCALLBACK_NEWMAP`):
`setflag ENGINE_FLYPOINT_VERMILION`. This is what makes the FAQ's "fly back to
Vermilion City" legal - it must already have been visited on foot.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_VERMILION_CITY_SNORLAX` | `constants/event_flags.asm:1298` | object flag on `VERMILIONCITY_BIG_SNORLAX`; set by `disappear` in `VermilionSnorlax` | clear = Snorlax on the map; set = gone, cave door walkable |
| `EVENT_FOUGHT_SNORLAX` | `constants/event_flags.asm:1266` | set by `VermilionSnorlax`; read by `VermilionPokecenter1FFishingGuruScript` (`maps/VermilionPokecenter1F.asm:18`) and used as the object flag of the right Black Belt in `maps/VictoryRoadGate.asm:119` | "the Snorlax encounter has happened" |
| `ENGINE_FLYPOINT_VERMILION` | `constants/engine_flags.asm:72` | `VermilionCityFlypointCallback` | Fly destination unlocked |
| `POKEGEAR_EXPN_CARD_F` | `constants/ram_constants.asm:280` | read by `.PokeFluteRadio` | without the EXPN CARD the flute channel is static and `SnorlaxAwake` can never be true |
| `EVENT_VERMILION_CITY_HIDDEN_FULL_HEAL` | `constants/event_flags.asm:252` | `VermilionCityHiddenFullHeal` | hidden FULL_HEAL at (12,19) |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `LEFTOVERS` | attached to the wild SNORLAX | `data/pokemon/base_stats/snorlax.asm:9` `db LEFTOVERS, LEFTOVERS` forced by `BATTLETYPE_FORCEITEM` (`engine/battle/core.asm:5771`) | - |
| `FULL_HEAL` | hidden | `bg_event 12, 19, BGEVENT_ITEM, VermilionCityHiddenFullHeal` | `EVENT_VERMILION_CITY_HIDDEN_FULL_HEAL` |
**Wild encounters** - the scripted SNORLAX is not a table entry; it is
`loadwildmon SNORLAX, 50`. Level 50 learnset from
`data/pokemon/evos_attacks.asm:1932`: the four most recent are SNORE (36),
REST (36), BODY_SLAM (43), ROLLOUT (50) - which is exactly the FAQ's "Rest" and
"Rollout". Catch rate 25, base exp 154 (`data/pokemon/base_stats/snorlax.asm`).
---
### MAP_VERMILION_POKECENTER_1F
- Script: `maps/VermilionPokecenter1F.asm`
- Header: `data/maps/maps.asm` (`VERMILION` group); dimensions
`constants/map_constants.asm:275` `map_const VERMILION_POKECENTER_1F, 5, 4`
Only the one script matters here:
- `VermilionPokecenter1FFishingGuruScript`: `checkevent EVENT_FOUGHT_SNORLAX`,
two text branches ("A sleeping #MON is lying in front of DIGLETT'S CAVE" ->
after-text). It is a read-only confirmation that the Snorlax beat is done; the
PC withdraw the FAQ asks for is the standard `POKECENTER_2F` / PC menu, not a
map script.
---
### MAP_DIGLETTS_CAVE
- Script: `maps/DiglettsCave.asm`
- Blocks: `maps/DiglettsCave.blk` (180 bytes = 10 x 18 blocks)
- Header: `data/maps/maps.asm:153` -> `TILESET_CAVE, CAVE, LANDMARK_DIGLETTS_CAVE, MUSIC_MT_MOON, TRUE (no phone), PALETTE_NITE, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:140` `map_const DIGLETTS_CAVE, 10, 18` (group `DUNGEONS`, id 75) = 20 x 36 cells
- Attributes: `data/maps/attributes.asm:487` `map_attributes DiglettsCave, DIGLETTS_CAVE, $09`; no connections
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 33 | `VERMILION_CITY` | 10 |
| 2 | 5 | 31 | `DIGLETTS_CAVE` | 5 |
| 3 | 15 | 5 | `ROUTE_2` | 5 |
| 4 | 17 | 3 | `DIGLETTS_CAVE` | 6 |
| 5 | 17 | 33 | `DIGLETTS_CAVE` | 2 |
| 6 | 3 | 3 | `DIGLETTS_CAVE` | 4 |
Warps 2<->5 and 4<->6 are the two internal ladders. Reading `DiglettsCave.blk`
against `data/tilesets/kanto_collision.asm` (block `$1f`/`$1b` = ladder, `$24` =
door) the map is three disconnected pockets:
- pocket A (bottom-left, blocks (1,15)-(2,17)): warp 1 (VERMILION door) + warp 2
(ladder);
- pocket B (the long cave): warp 5 at (17,33) up to warp 6 at (3,3);
- pocket C (top-right, blocks (7,1)-(8,3)): warp 4 (ladder) + warp 3 (ROUTE_2
door).
So the traversal is: `(3,33)` -> ladder `(5,31)` -> emerge `(17,33)` -> walk the
long cave to `(3,3)` -> ladder -> emerge `(17,3)` -> door `(15,5)` -> Route 2
`(12,7)`. That is exactly the FAQ's "down the ladder, ride up, up that ladder,
exit the door".
**Coord events** - none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 6 | 11 | `BGEVENT_ITEM` | `DiglettsCaveHiddenMaxRevive` -> `hiddenitem MAX_REVIVE, EVENT_DIGLETTS_CAVE_HIDDEN_MAX_REVIVE` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `DIGLETTSCAVE_POKEFAN_M` | `SPRITE_POKEFAN_M` | 3 | 31 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `DiglettsCavePokefanMScript` | -1 |
**Trainers** - none, matching the FAQ.
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `MAX_REVIVE` | hidden (ITEMFINDER / bump) | `bg_event 6, 11, BGEVENT_ITEM` | `EVENT_DIGLETTS_CAVE_HIDDEN_MAX_REVIVE` (`constants/event_flags.asm:228`) |
**Wild encounters** - `data/wild/kanto_grass.asm:5` `def_grass_wildmons
DIGLETTS_CAVE`, rates `4 / 2 / 8` percent (morn/day/nite). All three time slots
carry the same seven rows:
| slot | level | species |
|---|---|---|
| 1 | 15 | DIGLETT |
| 2 | 17 | DIGLETT |
| 3 | 19 | DIGLETT |
| 4 | 13 | DIGLETT |
| 5 | 19 | DUGTRIO |
| 6 | 24 | DUGTRIO |
| 7 | 29 | DUGTRIO |
The FAQ's "capture both Diglett and Dugtrio here" is correct, and night is the
best time (8% vs 2% by day).
The FAQ's "ride your bike" is legal here: `BikeFunction`'s `.CheckEnvironment`
(`engine/events/overworld.asm:1665`) allows outdoor maps, `CAVE` and `GATE`.
---
### MAP_ROUTE_2
- Script: `maps/Route2.asm`
- Blocks: `maps/Route2.blk` (270 bytes = 10 x 27 blocks)
- Header: `data/maps/maps.asm:455` -> `TILESET_KANTO, ROUTE, LANDMARK_ROUTE_2, MUSIC_ROUTE_2, FALSE, PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:421` `map_const ROUTE_2, 10, 27` (group `VIRIDIAN`, id 1) = 20 x 54 cells
- Attributes: `data/maps/attributes.asm:255` `map_attributes Route2, ROUTE_2, $0f`
- Connections: `connection north, PewterCity, PEWTER_CITY, -5`;
`connection south, ViridianCity, VIRIDIAN_CITY, -5`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 15 | 15 | `ROUTE_2_NUGGET_HOUSE` | 1 |
| 2 | 15 | 31 | `ROUTE_2_GATE` | 3 |
| 3 | 16 | 27 | `ROUTE_2_GATE` | 1 |
| 4 | 17 | 27 | `ROUTE_2_GATE` | 2 |
| 5 | 12 | 7 | `DIGLETTS_CAVE` | 3 |
**Coord events** - none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 7 | 51 | `BGEVENT_READ` | `Route2Sign` |
| 11 | 9 | `BGEVENT_READ` | `Route2DiglettsCaveSign` |
| 7 | 23 | `BGEVENT_ITEM` | `Route2HiddenMaxEther` -> `hiddenitem MAX_ETHER, EVENT_ROUTE_2_HIDDEN_MAX_ETHER` |
| 4 | 14 | `BGEVENT_ITEM` | `Route2HiddenFullHeal` -> `hiddenitem FULL_HEAL, EVENT_ROUTE_2_HIDDEN_FULL_HEAL` |
| 4 | 27 | `BGEVENT_ITEM` | `Route2HiddenFullRestore` -> `hiddenitem FULL_RESTORE, EVENT_ROUTE_2_HIDDEN_FULL_RESTORE` |
| 11 | 30 | `BGEVENT_ITEM` | `Route2HiddenRevive` -> `hiddenitem REVIVE, EVENT_ROUTE_2_HIDDEN_REVIVE` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE2_BUG_CATCHER1` | `SPRITE_BUG_CATCHER` | 10 | 45 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_TRAINER` (sight 5) | `TrainerBugCatcherRob` | -1 |
| `ROUTE2_BUG_CATCHER2` | `SPRITE_BUG_CATCHER` | 5 | 5 | `SPRITEMOVEDATA_STANDING_RIGHT` (radius 1,0) | `OBJECTTYPE_TRAINER` (sight 4) | `TrainerBugCatcherEd` | -1 |
| `ROUTE2_BUG_CATCHER3` | `SPRITE_BUG_CATCHER` | 4 | 43 | `SPRITEMOVEDATA_STANDING_UP` (radius 1,0) | `OBJECTTYPE_TRAINER` (sight 5) | `TrainerBugCatcherDoug` | -1 |
| `ROUTE2_POKE_BALL1` | `SPRITE_POKE_BALL` | 0 | 29 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `Route2DireHit` (`itemball DIRE_HIT`) | `EVENT_ROUTE_2_DIRE_HIT` |
| `ROUTE2_POKE_BALL2` | `SPRITE_POKE_BALL` | 2 | 23 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `Route2MaxPotion` (`itemball MAX_POTION`) | `EVENT_ROUTE_2_MAX_POTION` |
| `ROUTE2_POKE_BALL3` | `SPRITE_POKE_BALL` | 19 | 2 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `Route2Carbos` (`itemball CARBOS`) | `EVENT_ROUTE_2_CARBOS` |
| `ROUTE2_POKE_BALL4` | `SPRITE_POKE_BALL` | 14 | 50 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `Route2Elixer` (`itemball ELIXER`) | `EVENT_ROUTE_2_ELIXER` |
| `ROUTE2_FRUIT_TREE` | `SPRITE_FRUIT_TREE` | 10 | 14 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_SCRIPT` | `Route2FruitTree` (`fruittree FRUITTREE_ROUTE_2`) | -1 |
**Cut trees** (not asm rows - decoded from `maps/Route2.blk` against the
`CUT_TREE` entries of `data/tilesets/kanto_collision.asm`, which are blocks
`$00 $32 $33 $34 $35 $60`)
| block (bx,by) | block id | covered cells (x, y) | which FAQ beat |
|---|---|---|---|
| (2, 4) | `$32` | 4-5, 8-9 | near the Diglett's Cave door, not used by the FAQ |
| (7, 9) | `$32` | 14-15, 18-19 | "cut the tree to the south" (just below the Nugget house at (15,15)) |
| (5, 20) | `$34` | 10-11, 40-41 | not named by the FAQ |
| (6, 23) | `$34` | 12-13, 46-47 | not named by the FAQ |
| (6, 25) | `$34` | 12-13, 50-51 | "cut the tree just left of it" (immediately left of the Elixer at (14,50)) |
**Scripts of interest**
- `TrainerBugCatcherRob` / `TrainerBugCatcherEd` / `TrainerBugCatcherDoug`
(`4d:5bc2`, `4d:5bd6`, `4d:5bea`): each is the plain `trainer CLASS, MEMBER,
EVENT_*, seen, beaten, 0, .Script` header with an `endifjustbattled` +
after-battle text body. No items, no flags beyond the beaten event.
- `Route2FruitTree`: `fruittree FRUITTREE_ROUTE_2` ->
`data/items/fruit_trees.asm:28` `db PSNCUREBERRY`. This is the FAQ's
"PSNCure Berry".
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_ROUTE_2_DIRE_HIT` | `constants/event_flags.asm:1318` | item ball object flag | clear = ball present |
| `EVENT_ROUTE_2_MAX_POTION` | `constants/event_flags.asm:1319` | item ball object flag | clear = ball present |
| `EVENT_ROUTE_2_CARBOS` | `constants/event_flags.asm:1320` | item ball object flag | clear = ball present |
| `EVENT_ROUTE_2_ELIXER` | `constants/event_flags.asm:1321` | item ball object flag | clear = ball present |
| `EVENT_ROUTE_2_HIDDEN_MAX_ETHER` | `constants/event_flags.asm:237` | `Route2HiddenMaxEther` | hidden pickup |
| `EVENT_ROUTE_2_HIDDEN_FULL_HEAL` | `constants/event_flags.asm:238` | `Route2HiddenFullHeal` | hidden pickup |
| `EVENT_ROUTE_2_HIDDEN_FULL_RESTORE` | `constants/event_flags.asm:239` | `Route2HiddenFullRestore` | hidden pickup |
| `EVENT_ROUTE_2_HIDDEN_REVIVE` | `constants/event_flags.asm:240` | `Route2HiddenRevive` | hidden pickup |
| `EVENT_BEAT_BUG_CATCHER_ROB` | `constants/event_flags.asm:840` | `TrainerBugCatcherRob` | trainer beaten |
| `EVENT_BEAT_BUG_CATCHER_ED` | `constants/event_flags.asm:841` | `TrainerBugCatcherEd` | trainer beaten |
| `EVENT_BEAT_BUG_CATCHER_DOUG` | `constants/event_flags.asm:850` | `TrainerBugCatcherDoug` | trainer beaten |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `CARBOS` | item ball at (19,2) | `Route2Carbos` | `EVENT_ROUTE_2_CARBOS` |
| `MAX_POTION` | item ball at (2,23) | `Route2MaxPotion` | `EVENT_ROUTE_2_MAX_POTION` |
| `DIRE_HIT` | item ball at (0,29) | `Route2DireHit` | `EVENT_ROUTE_2_DIRE_HIT` |
| `ELIXER` | item ball at (14,50) | `Route2Elixer` | `EVENT_ROUTE_2_ELIXER` |
| `PSNCUREBERRY` | fruit tree object at (10,14) | `FRUITTREE_ROUTE_2`, `data/items/fruit_trees.asm:28` | daily reset, not an event flag |
| `MAX_ETHER` | hidden at (7,23) | bg_event | `EVENT_ROUTE_2_HIDDEN_MAX_ETHER` |
| `FULL_HEAL` | hidden at (4,14) | bg_event | `EVENT_ROUTE_2_HIDDEN_FULL_HEAL` |
| `FULL_RESTORE` | hidden at (4,27) | bg_event | `EVENT_ROUTE_2_HIDDEN_FULL_RESTORE` |
| `REVIVE` | hidden at (11,30) | bg_event | `EVENT_ROUTE_2_HIDDEN_REVIVE` |
| `NUGGET` | Route 2 Nugget House NPC | `Route2NuggetHouseFisherScript` | `EVENT_GOT_NUGGET_FROM_GUY` |
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `ROB` (`constants/trainer_constants.asm:314`) | `BUG_CATCHER` | 2 | `BugCatcherGroup` "ROB" (`data/trainers/parties.asm:1416`): L32 BEEDRILL, L32 BUTTERFREE, `TRAINERTYPE_NORMAL` | `TrainerBugCatcherRob` | no |
| `DOUG` (`constants/trainer_constants.asm:324`) | `BUG_CATCHER` | 12 | `BugCatcherGroup` "DOUG" (`data/trainers/parties.asm:1482`): L34 ARIADOS | `TrainerBugCatcherDoug` | no |
| `ED` (`constants/trainer_constants.asm:315`) | `BUG_CATCHER` | 3 | `BugCatcherGroup` "ED" (`data/trainers/parties.asm:1422`): L30 BEEDRILL x3 | `TrainerBugCatcherEd` | no |
`BUG_CATCHER` base reward 4 (`data/trainers/attributes.asm:215`); prize =
base x last mon level (`ComputeTrainerReward`,
`engine/battle/read_trainer_party.asm:300`).
**Wild encounters** - `data/wild/kanto_grass.asm:259` `def_grass_wildmons
ROUTE_2`, rates `10 / 10 / 10`. Gold (`IF DEF(_GOLD)`):
| slot | morn | day | nite |
|---|---|---|---|
| 1 | L3 CATERPIE | L3 CATERPIE | L3 HOOTHOOT |
| 2 | L3 PIDGEY | L3 PIDGEY | L3 SPINARAK |
| 3 | L5 METAPOD | L5 METAPOD | L5 HOOTHOOT |
| 4 | L7 BUTTERFREE | L7 PIDGEY | L7 NOCTOWL |
| 5 | L7 PIDGEOTTO | L7 PIDGEOTTO | L7 ARIADOS |
| 6 | L4 PIKACHU | L4 PIKACHU | L4 PIKACHU |
| 7 | L4 PIKACHU | L4 PIKACHU | L4 PIKACHU |
(The Silver table under `ELIF DEF(_SILVER)` swaps the Caterpie line for
Weedle/Kakuna/Beedrill and Spinarak for Ledyba.)
---
### MAP_ROUTE_2_NUGGET_HOUSE
- Script: `maps/Route2NuggetHouse.asm`
- Header: `data/maps/maps.asm:465` -> `TILESET_HOUSE, INDOOR, LANDMARK_ROUTE_2, MUSIC_VIRIDIAN_CITY, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:431` `map_const ROUTE_2_NUGGET_HOUSE, 4, 4`
- Attributes: `data/maps/attributes.asm:664`; no connections
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 2 | 7 | `ROUTE_2` | 1 |
| 2 | 3 | 7 | `ROUTE_2` | 1 |
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE2NUGGETHOUSE_FISHER` | `SPRITE_FISHER` | 2 | 4 | `SPRITEMOVEDATA_WALK_UP_DOWN` (radius 0,2) | `OBJECTTYPE_SCRIPT` | `Route2NuggetHouseFisherScript` | -1 |
**Scripts of interest**
- `Route2NuggetHouseFisherScript` (`5f:4e26`): `faceplayer` / `opentext` /
`checkevent EVENT_GOT_NUGGET_FROM_GUY` -> `.GotNugget`; otherwise text,
`promptbutton`, `verbosegiveitem NUGGET`, `iffalse .NoRoom`,
`setevent EVENT_GOT_NUGGET_FROM_GUY`. A full bag skips the flag, so the gift
is re-offerable.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_GOT_NUGGET_FROM_GUY` | `constants/event_flags.asm:199` | `Route2NuggetHouseFisherScript` | one-time NUGGET |
---
### MAP_ROUTE_2_GATE
- Script: `maps/Route2Gate.asm`
- Header: `data/maps/maps.asm:466` -> `TILESET_GATE, GATE, LANDMARK_ROUTE_2, MUSIC_ROUTE_2, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:432` `map_const ROUTE_2_GATE, 5, 4`
- Attributes: `data/maps/attributes.asm:665`; no connections
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 0 | `ROUTE_2` | 3 |
| 2 | 5 | 0 | `ROUTE_2` | 4 |
| 3 | 4 | 7 | `ROUTE_2` | 2 |
| 4 | 5 | 7 | `ROUTE_2` | 2 |
Both sides land back on Route 2 - this is the FAQ's "route-changing house...
You'll still be in Route 2". North side (y=0) <-> Route 2 (16,27)/(17,27);
south side (y=7) <-> Route 2 (15,31).
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE2GATE_SCIENTIST` | `SPRITE_SCIENTIST` | 6 | 4 | `SPRITEMOVEDATA_WALK_UP_DOWN` (radius 0,2) | `OBJECTTYPE_SCRIPT` | `Route2GateScientistScript` | -1 |
`Route2GateScientistScript` is `jumptextfaceplayer` only (Oak's aide flavour).
---
### MAP_PEWTER_CITY
- Script: `maps/PewterCity.asm`
- Blocks: `maps/PewterCity.blk`
- Header: `data/maps/maps.asm:322` -> `TILESET_KANTO, TOWN, LANDMARK_PEWTER_CITY, MUSIC_VIRIDIAN_CITY, FALSE, PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:297` `map_const PEWTER_CITY, 20, 18` (group `PEWTER`, id 2) = 40 x 36 cells
- Attributes: `data/maps/attributes.asm:251` `map_attributes PewterCity, PEWTER_CITY, $0f`
- Connections: `connection south, Route2, ROUTE_2, 5`; `connection east, Route3, ROUTE_3, 5`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 29 | 13 | `PEWTER_NIDORAN_SPEECH_HOUSE` | 1 |
| 2 | 16 | 17 | `PEWTER_GYM` | 1 |
| 3 | 23 | 17 | `PEWTER_MART` | 2 |
| 4 | 13 | 25 | `PEWTER_POKECENTER_1F` | 1 |
| 5 | 7 | 29 | `PEWTER_SNOOZE_SPEECH_HOUSE` | 1 |
There is **no museum warp**: the FAQ's "they even filled the doorway in" is
literally true in the asm - the museum exists only as a sign.
**Coord events** - none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 25 | 23 | `BGEVENT_READ` | `PewterCitySign` |
| 11 | 17 | `BGEVENT_READ` | `PewterGymSign` |
| 15 | 9 | `BGEVENT_READ` | `PewterMuseumSign` ("closed for renovations") |
| 33 | 19 | `BGEVENT_READ` | `PewterCityMtMoonGiftShopSign` |
| 19 | 29 | `BGEVENT_READ` | `PewterCityWelcomeSign` |
| 14 | 25 | `BGEVENT_READ` | `PewterCityPokecenterSign` (`jumpstd PokecenterSignScript`) |
| 24 | 17 | `BGEVENT_READ` | `PewterCityMartSign` (`jumpstd MartSignScript`) |
No hidden items in Pewter City.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `PEWTERCITY_COOLTRAINER_F` | `SPRITE_COOLTRAINER_F` | 19 | 11 | `SPRITEMOVEDATA_WANDER` (2,2) | `OBJECTTYPE_SCRIPT` | `PewterCityCooltrainerFScript` | -1 |
| `PEWTERCITY_BUG_CATCHER` | `SPRITE_BUG_CATCHER` | 14 | 29 | `SPRITEMOVEDATA_WANDER` (2,2) | `OBJECTTYPE_SCRIPT` | `PewterCityBugCatcherScript` | -1 |
| `PEWTERCITY_GRAMPS` | `SPRITE_GRAMPS` | 29 | 17 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (radius 2,0) | `OBJECTTYPE_SCRIPT` | `PewterCityGrampsScript` | -1 |
| `PEWTERCITY_FRUIT_TREE1` | `SPRITE_FRUIT_TREE` | 32 | 3 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_SCRIPT` | `PewterCityFruitTree1` (`fruittree FRUITTREE_PEWTER_CITY_1`) | -1 |
| `PEWTERCITY_FRUIT_TREE2` | `SPRITE_FRUIT_TREE` | 30 | 3 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_SCRIPT` | `PewterCityFruitTree2` (`fruittree FRUITTREE_PEWTER_CITY_2`) | -1 |
**Scripts of interest**
- `PewterCityGrampsScript` (`4d:583e`). `faceplayer` / `opentext` / `checkver`
/ `iftrue .RainbowWing`. `checkver` leaves `GS_VERSION` in `wScriptVar`
(0 = Gold, 1 = Silver), so:
- **Gold**: `checkevent EVENT_GOT_SILVER_WING` -> `.GotSilverWing`; otherwise
text, `promptbutton`, `verbosegiveitem SILVER_WING`,
`setevent EVENT_GOT_SILVER_WING`. Note there is **no `iffalse` guard** on
this `verbosegiveitem`: the flag is set whether or not the bag had room.
- **Silver**: `checkevent EVENT_GOT_RAINBOW_WING` -> `.GotSilverWing`;
otherwise `verbosegiveitem RAINBOW_WING`, `setevent EVENT_GOT_RAINBOW_WING`,
`setevent EVENT_TEAM_ROCKET_DISBANDED`.
This gramps is the *second* source for the wing pair: the Radio Tower director
(`maps/RadioTower5F.asm:120-140`) gives the opposite one on the same
`checkver` split.
- `PewterCityFlypointCallback` (`MAPCALLBACK_NEWMAP`):
`setflag ENGINE_FLYPOINT_PEWTER`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_GOT_SILVER_WING` | `constants/event_flags.asm:130` | Gold branch of `PewterCityGrampsScript`; also `maps/RadioTower5F.asm:139` (Silver) | one-time |
| `EVENT_GOT_RAINBOW_WING` | `constants/event_flags.asm:129` | Silver branch of `PewterCityGrampsScript`; also `maps/RadioTower5F.asm:129` (Gold); read by `maps/EcruteakTinTowerEntrance.asm:62` | one-time; gates Tin Tower entry |
| `EVENT_TEAM_ROCKET_DISBANDED` | `constants/event_flags.asm:1283` | set by the Silver branch here | side effect of the Rainbow Wing gift |
| `ENGINE_FLYPOINT_PEWTER` | `constants/engine_flags.asm:69` | `PewterCityFlypointCallback` | Fly destination |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `SILVER_WING` (Gold) / `RAINBOW_WING` (Silver) | talk to `PEWTERCITY_GRAMPS` at (29,17) | `PewterCityGrampsScript` | `EVENT_GOT_SILVER_WING` / `EVENT_GOT_RAINBOW_WING` |
| `ICE_BERRY` | fruit tree at (32,3) | `FRUITTREE_PEWTER_CITY_1`, `data/items/fruit_trees.asm:31` | daily |
| `MINT_BERRY` | fruit tree at (30,3) | `FRUITTREE_PEWTER_CITY_2`, `data/items/fruit_trees.asm:32` | daily |
**Wild encounters** - none (TOWN, no grass table).
---
### MAP_PEWTER_POKECENTER_1F
- Script: `maps/PewterPokecenter1F.asm`
- Header: `data/maps/maps.asm:326` -> `TILESET_POKECENTER, INDOOR, LANDMARK_PEWTER_CITY, MUSIC_POKEMON_CENTER, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:301` `map_const PEWTER_POKECENTER_1F, 5, 4`
- Attributes: `data/maps/attributes.asm:583`
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `PEWTER_CITY` | 4 |
| 2 | 4 | 7 | `PEWTER_CITY` | 4 |
| 3 | 0 | 7 | `POKECENTER_2F` | 1 |
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `PEWTERPOKECENTER1F_NURSE` | `SPRITE_NURSE` | 3 | 1 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `PewterPokecenter1FNurseScript` (`jumpstd PokecenterNurseScript`) | -1 |
| `PEWTERPOKECENTER1F_TEACHER` | `SPRITE_TEACHER` | 8 | 6 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `PewterPokecenter1FTeacherScript` | -1 |
| `PEWTERPOKECENTER1F_JIGGLYPUFF` | `SPRITE_JIGGLYPUFF` | 1 | 3 | `SPRITEMOVEDATA_POKEMON` | `OBJECTTYPE_SCRIPT` | `PewterJigglypuff` | -1 |
| `PEWTERPOKECENTER1F_BUG_CATCHER` | `SPRITE_BUG_CATCHER` | 2 | 3 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `PewterPokecenter1FBugCatcherScript` | -1 |
| `PEWTERPOKECENTER1F_CHRIS` | `SPRITE_POKEFAN_M` | 7 | 2 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `Chris` | -1 |
**Scripts of interest**
- `Chris` (`5a:46f5`): `faceplayer` / `opentext` / `trade NPC_TRADE_CHRIS` /
`waitbutton` / `closetext` / `end`. The trade row is
`data/events/npc_trades.asm:18`:
`npctrade TRADE_DIALOGSET_HAPPY, GLOOM, RAPIDASH, "RUNNY", $96, $66,
BURNT_BERRY, 15616, "CHRIS", TRADE_GENDER_EITHER` - the player gives GLOOM and
receives a RAPIDASH nicknamed RUNNY, DVs `$96/$66`, holding a BURNT_BERRY, OT
"CHRIS" ID 15616. The FAQ's "trade a Gloom for a Rapidash" is exact. Note the
NPC sprite is `SPRITE_POKEFAN_M`, not a lady.
---
### MAP_PEWTER_GYM
- Script: `maps/PewterGym.asm`
- Blocks: `maps/PewterGym.blk`
- Header: `data/maps/maps.asm:324` -> `TILESET_TOWER, INDOOR, LANDMARK_PEWTER_CITY, MUSIC_GYM, TRUE (no phone), PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:299` `map_const PEWTER_GYM, 5, 7` (group `PEWTER`, id 4) = 10 x 14 cells
- Attributes: `data/maps/attributes.asm:581` `map_attributes PewterGym, PEWTER_GYM, $00`; no connections
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 13 | `PEWTER_CITY` | 2 |
| 2 | 5 | 13 | `PEWTER_CITY` | 2 |
**Coord events** - none. There is no lock-in trip-wire in this gym.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 2 | 11 | `BGEVENT_READ` | `PewterGymStatue` |
| 7 | 11 | `BGEVENT_READ` | `PewterGymStatue` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `PEWTERGYM_BROCK` | `SPRITE_BROCK` | 5 | 1 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `PewterGymBrockScript` | -1 |
| `PEWTERGYM_YOUNGSTER` | `SPRITE_YOUNGSTER` | 2 | 5 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_TRAINER` (sight 3) | `TrainerCamperJerry` | -1 |
| `PEWTERGYM_GYM_GUIDE` | `SPRITE_GYM_GUIDE` | 6 | 11 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` (sight 1) | `PewterGymGuideScript` | -1 |
**Scripts of interest**
- `PewterGymBrockScript` (`5a:405f`). `faceplayer` / `opentext` /
`checkflag ENGINE_BOULDERBADGE` / `iftrue .FightDone`. Otherwise:
`writetext BrockIntroText`, `waitbutton`, `closetext`,
`winlosstext BrockWinLossText, 0`, `loadtrainer BROCK, BROCK1`,
`startbattle`, `reloadmapafterbattle`, `setevent EVENT_BEAT_BROCK`,
**`setevent EVENT_BEAT_CAMPER_JERRY`**, then `opentext` /
`writetext ReceivedBoulderBadgeText` / `playsound SFX_GET_BADGE` / `waitsfx` /
`setflag ENGINE_BOULDERBADGE` / `writetext BrockBoulderBadgeText` /
`waitbutton` / `closetext` / `end`.
Two consequences for a bot: (1) Camper Jerry is optional - beating Brock marks
him beaten; (2) the badge is a `setflag` on the ENGINE namespace, not an
`EVENT_*`.
- `TrainerCamperJerry` (`5a:4093`): standard `trainer CAMPER, JERRY,
EVENT_BEAT_CAMPER_JERRY, ...` header, sight range 3 facing right from (2,5).
- `PewterGymGuideScript`: `checkevent EVENT_BEAT_BROCK`, two text branches.
- `PewterGymStatue`: `checkflag ENGINE_BOULDERBADGE`; unbeaten -> `jumpstd
GymStatue1Script`, beaten -> `gettrainername STRING_BUFFER_4, BROCK, BROCK1`
then `jumpstd GymStatue2Script`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_BOULDERBADGE` | `constants/engine_flags.asm:47` (row `data/events/engine_flags.asm`, backed by `wKantoBadges`) | checked and set by `PewterGymBrockScript`; checked by `PewterGymStatue` | the gym's own re-entry guard, and the ROCK-type damage boost via `BadgeTypeBoosts` |
| `EVENT_BEAT_BROCK` | `constants/event_flags.asm:715` | set by `PewterGymBrockScript`; read by `PewterGymGuideScript` | gym cleared |
| `EVENT_BEAT_CAMPER_JERRY` | `constants/event_flags.asm:545` | `TrainerCamperJerry`, and also set by `PewterGymBrockScript` | trainer beaten / skipped |
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `JERRY` (`constants/trainer_constants.asm:566`) | `CAMPER` | 18 | `CamperGroup` "JERRY" (`data/trainers/parties.asm:2811`): L37 SANDSLASH, `TRAINERTYPE_NORMAL` | `TrainerCamperJerry` | no |
| `BROCK1` (`constants/trainer_constants.asm:91`, class `BROCK` = 11 at line 90) | `BROCK` | 1 | `BrockGroup` (`data/trainers/parties.asm:270`, symbol `0e:5cda`), `TRAINERTYPE_MOVES` | `PewterGymBrockScript` | no phone |
`BrockGroup` party, in asm order:
| # | level | species | moves |
|---|---|---|---|
| 1 | 41 | GRAVELER | DEFENSE_CURL, ROCK_SLIDE, ROLLOUT, EARTHQUAKE |
| 2 | 41 | RHYHORN | FURY_ATTACK, SCARY_FACE, EARTHQUAKE, HORN_DRILL |
| 3 | 42 | OMASTAR | BITE, SURF, PROTECT, SPIKE_CANNON |
| 4 | 44 | ONIX | BIND, ROCK_SLIDE, BIDE, SANDSTORM |
| 5 | 42 | KABUTOPS | SLASH, SURF, ENDURE, GIGA_DRAIN |
Class attributes (`data/trainers/attributes.asm:101`): item1 `HYPER_POTION`,
base reward 25, AI `AI_BASIC | AI_SETUP | AI_SMART | AI_AGGRESSIVE |
AI_CAUTIOUS | AI_STATUS | AI_RISKY`, `CONTEXT_USE | SWITCH_SOMETIMES`. Camper
base reward 5 (`data/trainers/attributes.asm:323`).
**Wild encounters** - none.
---
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Route 19 boulders | `maps/Route19.asm:Route19ClearRocksCallback` (`4e:4f09`), six `changeblock ... $7a` writes under `callback MAPCALLBACK_TILES` | `EVENT_CINNABAR_ROCKS_CLEARED` | Set only by `maps/Route20.asm:Route20ClearRocksCallback` (`MAPCALLBACK_NEWMAP`) - i.e. by reaching Route 20 from the Cinnabar side. Nothing in this section can clear it; the FAQ is right that you must turn around. |
| SNORLAX blocking the DIGLETT'S CAVE door | `maps/VermilionCity.asm` object at (34,8) with a `SPRITEMOVEDATA_BIGDOLLSYM` 2x2 footprint sitting directly under warp 10 at (34,7) | `EVENT_VERMILION_CITY_SNORLAX` clear = present | `disappear VERMILIONCITY_BIG_SNORLAX` at the end of `VermilionSnorlax`, which only runs after `startbattle` |
| SNORLAX will not wake | `engine/events/specials.asm:358 SnorlaxAwake` | `wMapMusic == MUSIC_POKE_FLUTE_CHANNEL` **and** player on one of (33,8), (34,10), (35,10), (36,8), (36,9) | Pokegear radio tuned to knob 78 / "20.0" |
| POKe FLUTE radio channel is static | `engine/pokegear/pokegear.asm:1487 .PokeFluteRadio` | not in Johto (`.InJohto`, landmark >= `KANTO_LANDMARK`) **and** `POKEGEAR_EXPN_CARD_F` set in `wPokegearFlags` | EXPN CARD, obtained earlier (Lavender radio director) |
| Route 2 cut trees | tileset collision `COLL_CUT_TREE` (`constants/collision_constants.asm:16`), tested by `CheckCutCollision` (`engine/overworld/tile_events.asm:76`) | HM01 CUT + `ENGINE_HIVEBADGE` (`engine/events/overworld.asm:133 CutFunction.CheckAble`) | already held long before this section |
| Diglett's Cave traversal | `maps/DiglettsCave.blk` geometry - the three pockets are joined only by warps 2<->5 and 6<->4 | none beyond walking | - |
| Pewter Gym re-fight | `maps/PewterGym.asm:PewterGymBrockScript` `checkflag ENGINE_BOULDERBADGE / iftrue .FightDone` | badge not yet held | one-shot |
| Pewter museum | no warp exists in `PewterCity_MapEvents`; only `bg_event 15, 9 PewterMuseumSign` | - | permanently closed in GS |
---
## 4. Bot checklist
Coordinates are asm cell coordinates. "clear" / "set" refer to event flags.
1. `ROUTE_19_FUCHSIA_GATE` -> `ROUTE_19`: walk warp 3/4 at (4,7)/(5,7).
Precondition: none. Expect to be stopped; `EVENT_CINNABAR_ROCKS_CLEARED` is
clear, so blocks `(6,6) (8,6) (10,6) (12,8) (4,8) (10,10)` are rock. Turn
around through warp 1 at (7,3).
2. FLY to `VERMILION_CITY`. Precondition: `ENGINE_FLYPOINT_VERMILION` set.
3. Open the Pokegear, RADIO card, tune the knob to 78 ("20.0"). Precondition:
`POKEGEAR_EXPN_CARD_F` set and the landmark is Kanto. Postcondition:
`wMapMusic == MUSIC_POKE_FLUTE_CHANNEL`.
4. Close the Pokegear and walk to one of (33,8) / (34,10) / (35,10) / (36,8) /
(36,9). Face `VERMILIONCITY_BIG_SNORLAX` at (34,8) and press A.
Precondition: `EVENT_VERMILION_CITY_SNORLAX` clear.
Effect: `SnorlaxAwake` returns TRUE -> wild L50 SNORLAX with LEFTOVERS forced.
5. Battle: catch or defeat. Either way, postcondition
`EVENT_VERMILION_CITY_SNORLAX` set (object removed) and
`EVENT_FOUGHT_SNORLAX` set. **Save before step 4** - a failed catch is not
repeatable.
6. Optional: `VERMILION_CITY` warp 2 at (9,5) -> `VERMILION_POKECENTER_1F`, heal
and withdraw SNORLAX; back out warps 1/2 at (3,7)/(4,7).
7. `VERMILION_CITY` warp 10 at (34,7) -> `DIGLETTS_CAVE` warp 1 at (3,33).
8. `DIGLETTS_CAVE`: walk (3,33) -> (5,31) (warp 2, ladder). Land at (17,33).
9. Walk the long cave (17,33) -> (3,3) (warp 6, ladder). Land at (17,3).
Optional en route: hidden `MAX_REVIVE` at (6,11),
flag `EVENT_DIGLETTS_CAVE_HIDDEN_MAX_REVIVE`.
10. Walk (17,3) -> (15,5) (warp 3, door) -> `ROUTE_2` warp 5 at (12,7).
11. `ROUTE_2`: item ball `ROUTE2_POKE_BALL3` at (19,2) = CARBOS.
Precondition `EVENT_ROUTE_2_CARBOS` clear; postcondition set.
12. Warp 1 at (15,15) -> `ROUTE_2_NUGGET_HOUSE`. Talk to
`ROUTE2NUGGETHOUSE_FISHER` at (2,4). Precondition
`EVENT_GOT_NUGGET_FROM_GUY` clear and >=1 bag slot; postcondition set,
NUGGET in bag. Exit warps 1/2 at (2,7)/(3,7).
13. Optional: fruit tree `ROUTE2_FRUIT_TREE` at (10,14) = PSNCUREBERRY (daily).
14. CUT the tree covering cells (14-15, 18-19). Precondition: HM01 in bag and
`ENGINE_HIVEBADGE` set.
15. Warp 3/4 at (16,27)/(17,27) -> `ROUTE_2_GATE` -> warps 3/4 at (4,7)/(5,7) ->
`ROUTE_2` warp 2 at (15,31). Still Route 2.
16. Item ball `ROUTE2_POKE_BALL4` at (14,50) = ELIXER.
Flag `EVENT_ROUTE_2_ELIXER`.
17. CUT the tree covering cells (12-13, 50-51), then head north.
18. Battle `TrainerBugCatcherRob` - object at (10,45), facing left, sight 5.
Party L32 BEEDRILL / L32 BUTTERFREE. Postcondition
`EVENT_BEAT_BUG_CATCHER_ROB`.
19. Battle `TrainerBugCatcherDoug` - object at (4,43), facing up, sight 5.
Party L34 ARIADOS. Postcondition `EVENT_BEAT_BUG_CATCHER_DOUG`.
20. Optional pickups on the way north: item ball at (0,29) = DIRE_HIT
(`EVENT_ROUTE_2_DIRE_HIT`); item ball at (2,23) = MAX_POTION
(`EVENT_ROUTE_2_MAX_POTION`); hidden MAX_ETHER at (7,23), hidden FULL_HEAL
at (4,14), hidden FULL_RESTORE at (4,27), hidden REVIVE at (11,30).
21. Battle `TrainerBugCatcherEd` - object at (5,5), facing right, sight 4.
Party L30 BEEDRILL x3. Postcondition `EVENT_BEAT_BUG_CATCHER_ED`.
22. Walk off the north edge of `ROUTE_2` into `PEWTER_CITY`
(`connection north, PewterCity, PEWTER_CITY, -5`). Postcondition
`ENGINE_FLYPOINT_PEWTER` set by `PewterCityFlypointCallback`.
23. Talk to `PEWTERCITY_GRAMPS` at (29,17) (he paces x 27..31 on y=17).
Precondition `EVENT_GOT_SILVER_WING` clear (Gold).
Postcondition: SILVER_WING in bag, `EVENT_GOT_SILVER_WING` set.
On Silver: RAINBOW_WING, `EVENT_GOT_RAINBOW_WING` and
`EVENT_TEAM_ROCKET_DISBANDED` set.
24. Optional: warp 4 at (13,25) -> `PEWTER_POKECENTER_1F`. Heal at
`PEWTERPOKECENTER1F_NURSE` (3,1). Talk to `Chris` at (7,2) to trade a GLOOM
for RAPIDASH "RUNNY". Optional: fruit trees at (32,3) = ICE_BERRY and
(30,3) = MINT_BERRY.
25. `PEWTER_CITY` warp 2 at (16,17) -> `PEWTER_GYM` (arrive at warp 1, (4,13)).
26. Optional: battle `TrainerCamperJerry`, object at (2,5), facing right,
sight 3. L37 SANDSLASH.
27. Talk to `PEWTERGYM_BROCK` at (5,1). Precondition `ENGINE_BOULDERBADGE`
clear. Battle `loadtrainer BROCK, BROCK1` (5 mon, L41-44).
Postconditions: `EVENT_BEAT_BROCK`, `EVENT_BEAT_CAMPER_JERRY`,
`ENGINE_BOULDERBADGE`.
28. Exit warps 1/2 at (4,13)/(5,13) -> `PEWTER_CITY` warp 2; heal.
---
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| All maps / warps / objects / bg events for this section | `src/import/RomExtractorGen2.lua` (`MAP_GROUP_COUNT = 26`, line 47; the group walk at line 527) | implemented - every Kanto group is extracted, so these maps are data, not hand-ported code |
| `MAPCALLBACK_TILES` + `changeblock` (Route 19 boulders) | `src/script/gen2/Opcodes.lua:127` (`changeblock`), driver `tests/drivers/gold_map_callbacks.lua` | implemented |
| `special SnorlaxAwake` | `src/script/gen2/Specials.lua:1562` `H.SnorlaxAwake`, with `Specials.SNORLAX_PROXIMITY` transcribing `.ProximityCoords` and `Specials.POKE_FLUTE_SONG = "Music_PokeFluteChannel"`; hooks `currentMusic` / `playerCell` at `src/world/gen2/World.lua:2712` and `:2723` | implemented |
| POKe FLUTE radio channel (knob 78 / 20.0) + EXPN CARD gate | `src/ui/gen2/Pokegear.lua:812-815` (the `knob = 78` row, `ctx.inJohto` / `ctx.expnCard`) | implemented |
| `BATTLETYPE_FORCEITEM` -> Snorlax holds LEFTOVERS | `src/world/gen2/World.lua:111` and `:4576` | implemented |
| `loadwildmon` / `startbattle` / `reloadmapafterbattle` / `disappear` | `src/script/gen2/Vm.lua:806-841` | implemented |
| Catching a wild mon (Ultra Balls on Snorlax) | `src/battle/gen2/Catching.lua` | implemented |
| Trainer battles from `OBJECTTYPE_TRAINER` rows (Rob / Doug / Ed / Jerry) | `src/world/gen2/Trainers.lua`, `src/script/gen2/Vm.lua:806` (`loadtrainer`) | implemented |
| Gym leader script shape (`checkflag`/`winlosstext`/`setflag ENGINE_BOULDERBADGE`) | `src/script/gen2/Vm.lua:208` (`setflag`/`clearflag`), `src/world/gen2/World.lua:1296-1340` (`engineFlags`) | implemented |
| `BadgeTypeBoosts` - BOULDERBADGE boosting ROCK moves (`data/types/badge_type_boosts.asm`, `engine/battle/misc.asm:146 DoBadgeTypeBoosts`) | no match anywhere in `src/battle/gen2/` | **missing** (the Gen 1 port has its own badge table in `src/battle/Damage.lua:28`, but nothing in the Gen 2 battle path reads badges) |
| Item ball pickup (Route 2 CARBOS / DIRE_HIT / ELIXER / MAX_POTION) | extracted into `obj.itemball` at `src/import/RomExtractorGen2.lua:2874` and `:2968`, but no consumer exists in `src/world/gen2/`; `src/script/gen2/CallAsm.lua:550` stubs `TryReceiveItem` with "wItemBallItemID / wItemBallQuantity are set by a script path this port does not run" | **missing** |
| Hidden items (Diglett's Cave MAX_REVIVE, the four Route 2 hidden items, Vermilion FULL_HEAL) | `src/world/gen2/HiddenItems.lua`, wired at `src/world/gen2/World.lua:5291` and the ITEMFINDER path at `:3423` | implemented |
| `verbosegiveitem` (NUGGET, SILVER_WING/RAINBOW_WING) | `src/script/gen2/Opcodes.lua:163`, `src/script/gen2/Vm.lua:501` / `:1201` | implemented |
| `fruittree` (PSNCUREBERRY, ICE_BERRY, MINT_BERRY) | `src/script/gen2/Opcodes.lua:160`, `src/world/gen2/World.lua:1058` (`fruitTreeItem`) | implemented |
| `trade NPC_TRADE_CHRIS` (GLOOM -> RAPIDASH) | `src/core/gen2/NpcTrade.lua`, `src/script/gen2/Opcodes.lua:155` | implemented |
| `checkver` Gold/Silver split in `PewterCityGrampsScript` | `src/script/gen2/Vm.lua:774`, `src/world/gen2/World.lua:1287` (`gsVersion`) | implemented |
| `setflag ENGINE_FLYPOINT_PEWTER` / `_VERMILION` and Fly | `src/world/gen2/World.lua:5628` | implemented |
| CUT field move + HIVEBADGE check | `src/world/gen2/FieldMoves.lua` (badge table at lines 100-101) | implemented |
| Bike allowed in `CAVE` (Diglett's Cave) | `src/world/gen2/Bike.lua` | partial - the file exists and comments the `hiddenitem`/BICYCLE interaction, but the `.CheckEnvironment` outdoor/CAVE/GATE test was not located; verify before relying on it |
| `gettrainername` for the beaten-gym statue | `src/script/gen2/Vm.lua:407` | implemented |
---
## 6. Unresolved / verify by hand
- **Brock's party order.** The FAQ lists Graveler, Omastar, Rhyhorn, Onix,
Kabutops. `data/trainers/parties.asm:270` `BrockGroup` is Graveler (41),
**Rhyhorn (41), Omastar (42)**, Onix (44), Kabutops (42) - slots 2 and 3 are
swapped relative to the FAQ. Levels and species set match.
- **All the FAQ's prize money figures are exactly 4x the asm value.**
`ComputeTrainerReward` (`engine/battle/read_trainer_party.asm:300`) is
`base reward x last mon level`, with no multiplier. Brock 25 x 42 = 1050 (FAQ
says 4,200); Rob 4 x 32 = 128 (FAQ 512); Doug 4 x 34 = 136 (FAQ 544); Ed
4 x 30 = 120 (FAQ 480); Jerry 5 x 37 = 185 (FAQ 740). The FAQ's EXP figures
likewise do not come from GS. Treat the money/EXP numbers in this section as
belonging to a different game (most likely HeartGold/SoulSilver, which the
text references) and ignore them for a GS bot.
- **"Viridian Forest".** The FAQ says the Max Potion is "in Viridian Forest".
There is no Viridian Forest map in `constants/map_constants.asm`; the
MAX_POTION is `ROUTE2_POKE_BALL2` at (2,23) on `ROUTE_2`, in the area the
forest used to occupy.
- **"the man just to the right of the Pokemart".** `PEWTERCITY_GRAMPS` spawns at
(29,17) with a `WALK_LEFT_RIGHT` radius of 2 (so x 27..31); the mart door is
warp 3 at (23,17). He is to the right of the mart but not adjacent.
- **"The Pokemon Center has a lady".** The trade NPC is `Chris`, sprite
`SPRITE_POKEFAN_M` at (7,2). Male sprite, name CHRIS.
- **Which cut tree the FAQ means.** The two trees named in step 14 and step 17
above are inferred from the block dump of `maps/Route2.blk` plus the item/warp
positions; they are not labelled anywhere in asm. Three other `CUT_TREE`
blocks exist on Route 2 (see the cut-tree table) and one of those may be the
intended route instead. Verify with a real walk.
- **Diglett's Cave pocket connectivity.** The three-pocket reading is derived by
hand from `maps/DiglettsCave.blk` block ids against
`data/tilesets/kanto_collision.asm`; the per-tile collision of the "long cave"
interior was not fully traced, only that every warp block is reachable within
its pocket. The FAQ's described route is consistent with it.
- **"the route is cleared to Diglett's Cave and through Route 8".** Nothing in
`maps/VermilionCity.asm` or `maps/Route11.asm` gates Route 11 / Route 8 on the
Snorlax; the only thing `EVENT_FOUGHT_SNORLAX` unlocks outside Vermilion is the
right-hand Black Belt object in `maps/VictoryRoadGate.asm:119`. The Snorlax
blocks only the Diglett's Cave door tile at (34,7).
- **"Keep around those four Ultra Balls"** and the Rest/Thunder Wave catch
advice are strategy, not asm; nothing enforces a ball count.
- **`src/world/gen2/Bike.lua` CAVE permission.** Marked partial in section 5 -
the port file was read but the equivalent of `BikeFunction.CheckEnvironment`
(`engine/events/overworld.asm:1665`) was not located, so "you can bike through
Diglett's Cave in the port" is unverified.
@@ -0,0 +1,987 @@
# Section 25 - Lugia (Gold) and Ho-Oh (Silver)
Source: `../section-25-lugia-gold-and-ho-oh-silver.txt`
Maps covered: `ROUTE_41`, `WHIRL_ISLAND_NE`, `WHIRL_ISLAND_NW`, `WHIRL_ISLAND_SW`,
`WHIRL_ISLAND_SE`, `WHIRL_ISLAND_CAVE`, `WHIRL_ISLAND_B1F`, `WHIRL_ISLAND_B2F`,
`WHIRL_ISLAND_LUGIA_CHAMBER`, `ECRUTEAK_CITY`, `ECRUTEAK_TIN_TOWER_ENTRANCE`,
`ECRUTEAK_TIN_TOWER_BACK_ENTRANCE`, `TIN_TOWER_1F` .. `TIN_TOWER_9F`,
`TIN_TOWER_ROOF`
Badges / key milestones in this section: no badges. The milestone is catching the
*other* version mascot post-Elite-Four - `EVENT_FOUGHT_LUGIA` /
`EVENT_FOUGHT_HO_OH`, armed by the wing the Pewter City gramps hands over
(`maps/PewterCity.asm`, `PewterCityGrampsScript`).
This section is two mutually exclusive branches. The walkthrough labels them
"Whirl Islands - Gold Version" and "Tin Tower - Silver Version", and the asm
agrees: `Script_checkver` (`engine/overworld/scripting.asm:1471`) returns
`GS_VERSION` (`constants/misc_constants.asm:21-23`: 0 = Gold, 1 = Silver), so on
each mascot the `checkver / iftrue .Silver` pair picks the level. In Gold, Lugia
is **level 70** and Ho-Oh was already caught at level 40; in Silver it is the
mirror image.
---
## 1. Route order
The walkthrough opens on the Magnet Train back to Johto - that hop belongs to the
neighbouring Kanto section, and the only thing this section needs from it is that
the player already holds the version-opposite wing.
**Gold branch (Lugia, Whirl Islands)**
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 1 | `OLIVINE_CITY` | (fly target) | Fly | west connection to `ROUTE_40` (`data/maps/attributes.asm:145`) | "Fly to Olivine City" |
| 2 | `ROUTE_40` | `data/maps/attributes.asm:223` | east connection from Olivine | south connection to `ROUTE_41` (`attributes.asm:224`) | surf southwest |
| 3 | `ROUTE_41` | `maps/Route41.asm` | north connection from Route 40 | `warp_event 36, 19` (warp 2) | "surf southwest and west until you reach the northeast cave", clearing whirlpools |
| 4 | `WHIRL_ISLAND_NE` | `maps/WhirlIslandNE.asm` | warp 1 `(3, 13)` | warp 2 `(17, 3)` or warp 3 `(13, 11)` | Flash, ledges, `ULTRA_BALL` at `(11, 11)` |
| 5 | `WHIRL_ISLAND_B1F` | `maps/WhirlIslandB1F.asm` | warp 2 `(35, 3)` / warp 3 `(29, 9)` | warp 7 `(25, 21)` or warp 8 `(13, 27)` | ledge chain, `ESCAPE_ROPE` at `(19, 26)`, `CALCIUM` at `(33, 23)` |
| 6 | `WHIRL_ISLAND_CAVE` | `maps/WhirlIslandCave.asm` | B1F warp 9 `(17, 21)` -> Cave warp 1 `(7, 5)` | Cave warp 2 `(3, 13)` -> `WHIRL_ISLAND_NW` | "head out the door to breathe, but that's pointless" |
| 7 | `WHIRL_ISLAND_SW` | `maps/WhirlIslandSW.asm` | B1F warp 4 `(9, 31)` / warp 5 `(23, 31)` | warp 5 `(17, 15)` -> B2F warp 4 | `GUARD_SPEC` at `(15, 2)` |
| 8 | `WHIRL_ISLAND_B2F` | `maps/WhirlIslandB2F.asm` | warp 1 `(11, 5)` from B1F warp 7 | warp 3 `(7, 25)` | `MAX_REVIVE` `(6, 4)`, waterfall descent, "head left and through the door" |
| 9 | `WHIRL_ISLAND_LUGIA_CHAMBER` | `maps/WhirlIslandLugiaChamber.asm` | warp 1 `(9, 13)` | Escape Rope (environment is `CAVE`) | surf north to Lugia at `(9, 5)` |
**Silver branch (Ho-Oh, Tin Tower)**
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 1 | `ECRUTEAK_CITY` | `maps/EcruteakCity.asm` | Fly | warp 3 `(18, 11)` | "Go back to Ecruteak City" |
| 2 | `ECRUTEAK_TIN_TOWER_ENTRANCE` | `maps/EcruteakTinTowerEntrance.asm` | warp 1 `(4, 17)` / warp 2 `(5, 17)` | warp 3 `(5, 3)` -> warp 4 `(17, 15)`, then warp 5 `(17, 3)` | "the Bell Tower entry house. Talk to the man" |
| 3 | `ECRUTEAK_TIN_TOWER_BACK_ENTRANCE` | `maps/EcruteakTinTowerBackEntrance.asm` | warp 3 `(2, 4)` | warp 1 `(7, 4)` / warp 2 `(7, 5)` -> `ECRUTEAK_CITY` warps 4/5 `(20, 2)` `(20, 3)` | "follow up the trail to the tower itself" |
| 4 | `ECRUTEAK_CITY` (north strip) | `maps/EcruteakCity.asm` | warps 4/5 | warp 12 `(37, 7)` | walk east along the fenced strip to Tin Tower |
| 5 | `TIN_TOWER_1F` | `maps/TinTower1F.asm` | warp 1 `(9, 15)` / warp 2 `(10, 15)` | warp 3 `(10, 2)` | "another bald man will let you through" - the sage occupies the stair tile |
| 6 | `TIN_TOWER_2F` | `maps/TinTower2F.asm` | warp 2 `(10, 2)` | warp 1 `(10, 14)` | "wild Rattatas will start converging on you" |
| 7 | `TIN_TOWER_3F` | `maps/TinTower3F.asm` | warp 1 `(10, 14)` | warp 2 `(16, 2)` | `FULL_HEAL` at `(3, 14)`, jump-ledge puzzle |
| 8 | `TIN_TOWER_4F` | `maps/TinTower4F.asm` | warp 2 `(16, 2)` | warps 1 `(2, 4)`, 3 `(2, 14)`, 4 `(17, 15)` | `ULTRA_BALL`, `SUPER_POTION`, `ESCAPE_ROPE` |
| 9 | `TIN_TOWER_5F` | `maps/TinTower5F.asm` | warps 2/3/4 | warp 1 `(11, 15)` | `RARE_CANDY` at `(9, 9)` |
| 10 | `TIN_TOWER_6F` | `maps/TinTower6F.asm` | warp 2 `(11, 15)` | warp 1 `(3, 9)` | "go up and you'll get a Max Potion" (see Unresolved) |
| 11 | `TIN_TOWER_7F` | `maps/TinTower7F.asm` | warp 1 `(3, 9)` | warp 2 `(10, 15)`, plus in-floor warp pair 3/4 and warp 5 to 9F | `MAX_REVIVE` at `(16, 1)`; "go through the next two warps" |
| 12 | `TIN_TOWER_8F` | `maps/TinTower8F.asm` | warp 1 `(2, 5)` | warps 2-6 into 9F | `NUGGET`, `MAX_ELIXER`, `FULL_RESTORE` |
| 13 | `TIN_TOWER_9F` | `maps/TinTower9F.asm` | warps 1/2/3/5/6/7 from 7F+8F | warp 4 `(7, 9)` | "cross the planks down, then go up the ladder" |
| 14 | `TIN_TOWER_ROOF` | `maps/TinTowerRoof.asm` | warp 1 `(9, 13)` | back down warp 1 then Escape Rope | level-70 Ho-Oh at `(9, 5)` |
The walkthrough's closing line ("ride the Magnet Train back to Kanto and fly back
to Pewter City") hands off to the next section; nothing on that hop is resolved
here.
---
## 2. Maps
### ROUTE_41
- Script: `maps/Route41.asm`
- Blocks: `maps/Route41.blk`
- Header: `data/maps/maps.asm:443` -> `TILESET_JOHTO`, `ROUTE`, `LANDMARK_ROUTE_41`, `MUSIC_ROUTE_36`, phone `FALSE`, `PALETTE_AUTO`, `FISHGROUP_OCEAN`
- Dimensions: `constants/map_constants.asm:410` -> `map_const ROUTE_41, 25, 27` (group 22 `CIANWOOD`, map 2)
- Connections (`data/maps/attributes.asm:227-229`): north `Route40`, west `CianwoodCity`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 12 | 17 | `WHIRL_ISLAND_NW` | 1 |
| 2 | 36 | 19 | `WHIRL_ISLAND_NE` | 1 |
| 3 | 12 | 37 | `WHIRL_ISLAND_SW` | 1 |
| 4 | 36 | 45 | `WHIRL_ISLAND_SE` | 1 |
**Coord events**: none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 9 | 35 | `BGEVENT_ITEM` | `Route41HiddenMaxEther` -> `hiddenitem MAX_ETHER, EVENT_ROUTE_41_HIDDEN_MAX_ETHER` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE41_OLIVINE_RIVAL1` | `SPRITE_OLIVINE_RIVAL` | 32 | 6 | `SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` (sight 3) | `TrainerSwimmermCharlie` | -1 |
| `ROUTE41_OLIVINE_RIVAL2` | `SPRITE_OLIVINE_RIVAL` | 46 | 8 | `SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` (3) | `TrainerSwimmermGeorge` | -1 |
| `ROUTE41_OLIVINE_RIVAL3` | `SPRITE_OLIVINE_RIVAL` | 20 | 26 | `SPINCOUNTERCLOCKWISE` | `OBJECTTYPE_TRAINER` (3) | `TrainerSwimmermBerke` | -1 |
| `ROUTE41_OLIVINE_RIVAL4` | `SPRITE_OLIVINE_RIVAL` | 32 | 30 | `SPINCLOCKWISE` | `OBJECTTYPE_TRAINER` (3) | `TrainerSwimmermKirk` | -1 |
| `ROUTE41_OLIVINE_RIVAL5` | `SPRITE_OLIVINE_RIVAL` | 19 | 46 | `SPINCOUNTERCLOCKWISE` | `OBJECTTYPE_TRAINER` (3) | `TrainerSwimmermMathew` | -1 |
| `ROUTE41_SWIMMER_GIRL1` | `SPRITE_SWIMMER_GIRL` | 17 | 4 | `SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` (3) | `TrainerSwimmerfKaylee` | -1 |
| `ROUTE41_SWIMMER_GIRL2` | `SPRITE_SWIMMER_GIRL` | 23 | 19 | `STANDING_UP` | `OBJECTTYPE_TRAINER` (3) | `TrainerSwimmerfSusie` | -1 |
| `ROUTE41_SWIMMER_GIRL3` | `SPRITE_SWIMMER_GIRL` | 27 | 34 | `STANDING_LEFT` | `OBJECTTYPE_TRAINER` (3) | `TrainerSwimmerfDenise` | -1 |
| `ROUTE41_SWIMMER_GIRL4` | `SPRITE_SWIMMER_GIRL` | 44 | 28 | `STANDING_RIGHT` | `OBJECTTYPE_TRAINER` (4) | `TrainerSwimmerfKara` | -1 |
| `ROUTE41_SWIMMER_GIRL5` | `SPRITE_SWIMMER_GIRL` | 9 | 50 | `SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` (2) | `TrainerSwimmerfWendy` | -1 |
Note the sight ranges: Kara is `4`, the widest on the route, which is why the
walkthrough uses her as the landmark for the northeast whirlpool ("the whirlpool
is just northwest of where you fought Swimmer Kara"). Her object sits at
`(44, 28)`; the trainer flag is `EVENT_BEAT_SWIMMERF_KARA`, so once beaten she is
still standing there (`event flag` on the row is `-1`, i.e. never masked) and
still walks into your path - the walkthrough's joke about swimmer stamina is
literally the object table.
**Scripts of interest**
- `TrainerSwimmerfKara` - `trainer SWIMMERF, KARA, EVENT_BEAT_SWIMMERF_KARA, ...`.
Post-battle it only prints `SwimmerfKaraAfterBattleText` ("I heard roars from
deep inside the ISLANDS"). No flag beyond the beat flag, no gate.
**Whirlpools**: they are *map blocks*, not events. `data/collision/field_move_blocks.asm:48-55`
gives `WhirlpoolBlockPointers` -> `TILESET_JOHTO` -> `db $07, $36, 0` (facing
block `$07`, replacement `$36`, animation type 0). A bot that wants the whirlpool
coordinates has to scan `maps/Route41.blk` for block id `$07`; the collision
value under it is `COLL_WHIRLPOOL` `$24` (`constants/collision_constants.asm:27`,
tested by `CheckWhirlpoolTile`, `home/map_objects.asm:176-183`).
**Wild encounters**
- Surfing, `data/wild/johto_water.asm:170-181`, `def_water_wildmons ROUTE_41`,
encounter rate `6 percent`. Gold: `20 TENTACOOL`, `20 TENTACRUEL`, `20 MANTINE`.
Silver: `20 TENTACOOL`, `20 TENTACRUEL`, `15 TENTACOOL`.
- Fishing: header fish group is `FISHGROUP_OCEAN` (`data/wild/fish.asm:13`,
`.Ocean_Old/.Ocean_Good/.Ocean_Super`).
- The walkthrough's Max Repel advice is aimed squarely at this 6% table.
---
### WHIRL_ISLAND_NE
- Script: `maps/WhirlIslandNE.asm`
- Blocks: `maps/WhirlIslandNE.blk`
- Header: `data/maps/maps.asm:137` -> `TILESET_DARK_CAVE`, `CAVE`, `LANDMARK_WHIRL_ISLANDS`, `MUSIC_UNION_CAVE`, phone `TRUE`, `PALETTE_DARK`, `FISHGROUP_WHIRL_ISLANDS`
- Dimensions: `constants/map_constants.asm:124` -> `map_const WHIRL_ISLAND_NE, 10, 9` (group 3 `DUNGEONS`, map 59)
- Connections: none (island interior).
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 13 | `ROUTE_41` | 2 |
| 2 | 17 | 3 | `WHIRL_ISLAND_B1F` | 2 |
| 3 | 13 | 11 | `WHIRL_ISLAND_B1F` | 3 |
**Coord events**: none. **BG events**: none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `WHIRLISLANDNE_POKE_BALL` | `SPRITE_POKE_BALL` | 11 | 11 | `STILL` | `OBJECTTYPE_ITEMBALL` | `WhirlIslandNEUltraBall` (`47:401e`) | `EVENT_WHIRL_ISLAND_NE_ULTRA_BALL` |
`WhirlIslandNEUltraBall` is a bare `itemball ULTRA_BALL`.
---
### WHIRL_ISLAND_NW
- Script: `maps/WhirlIslandNW.asm` - no scripts at all, warps only
- Blocks: `maps/WhirlIslandNW.blk`
- Header: `data/maps/maps.asm:136`, same row shape as NE
- Dimensions: `constants/map_constants.asm:123` -> `5, 9` (group 3, map 58)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 5 | 7 | `ROUTE_41` | 1 |
| 2 | 5 | 3 | `WHIRL_ISLAND_B1F` | 1 |
| 3 | 3 | 15 | `WHIRL_ISLAND_SW` | 4 |
| 4 | 7 | 15 | `WHIRL_ISLAND_CAVE` | 2 |
No coord events, bg events or objects.
---
### WHIRL_ISLAND_SW
- Script: `maps/WhirlIslandSW.asm`
- Blocks: `maps/WhirlIslandSW.blk`
- Header: `data/maps/maps.asm:138`
- Dimensions: `constants/map_constants.asm:125` -> `10, 9` (group 3, map 60)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 5 | 7 | `ROUTE_41` | 3 |
| 2 | 17 | 3 | `WHIRL_ISLAND_B1F` | 5 |
| 3 | 3 | 3 | `WHIRL_ISLAND_B1F` | 4 |
| 4 | 3 | 15 | `WHIRL_ISLAND_NW` | 3 |
| 5 | 17 | 15 | `WHIRL_ISLAND_B2F` | 4 |
**Coord events**: none. **BG events**: none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `WHIRLISLANDSW_POKE_BALL` | `SPRITE_POKE_BALL` | 15 | 2 | `STILL` | `OBJECTTYPE_ITEMBALL` | `WhirlIslandSWGuardSpec` (`47:4044`) | `EVENT_WHIRL_ISLAND_SW_GUARD_SPEC` |
`itemball GUARD_SPEC` - the walkthrough's "Guard Specs".
**Wild encounters**: surfing, `data/wild/johto_water.asm:79-84`,
`def_water_wildmons WHIRL_ISLAND_SW`, rate `4 percent`: `20 TENTACOOL`,
`15 HORSEA`, `20 TENTACRUEL`.
---
### WHIRL_ISLAND_SE
- Script: `maps/WhirlIslandSE.asm` - warps only
- Blocks: `maps/WhirlIslandSE.blk`
- Header: `data/maps/maps.asm:140`
- Dimensions: `constants/map_constants.asm:127` -> `5, 9` (group 3, map 62)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 5 | 13 | `ROUTE_41` | 4 |
| 2 | 5 | 3 | `WHIRL_ISLAND_B1F` | 6 |
No coord events, bg events or objects. No water encounter table.
---
### WHIRL_ISLAND_CAVE
- Script: `maps/WhirlIslandCave.asm` - warps only
- Blocks: `maps/WhirlIslandCave.blk`
- Header: `data/maps/maps.asm:139`
- Dimensions: `constants/map_constants.asm:126` -> `5, 9` (group 3, map 61)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 7 | 5 | `WHIRL_ISLAND_B1F` | 9 |
| 2 | 3 | 13 | `WHIRL_ISLAND_NW` | 4 |
This is the "you can now head out the door to breathe, but that's pointless" room:
it is a pure corridor between B1F and the NW island.
---
### WHIRL_ISLAND_B1F
- Script: `maps/WhirlIslandB1F.asm` (`47:4096` `WhirlIslandB1F_MapScripts`)
- Blocks: `maps/WhirlIslandB1F.blk` (`2b:6f61 WhirlIslandB1F_Blocks`)
- Header: `data/maps/maps.asm:141`
- Dimensions: `constants/map_constants.asm:128` -> `20, 18` (group 3, map 63)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 5 | 5 | `WHIRL_ISLAND_NW` | 2 |
| 2 | 35 | 3 | `WHIRL_ISLAND_NE` | 2 |
| 3 | 29 | 9 | `WHIRL_ISLAND_NE` | 3 |
| 4 | 9 | 31 | `WHIRL_ISLAND_SW` | 3 |
| 5 | 23 | 31 | `WHIRL_ISLAND_SW` | 2 |
| 6 | 31 | 29 | `WHIRL_ISLAND_SE` | 2 |
| 7 | 25 | 21 | `WHIRL_ISLAND_B2F` | 1 |
| 8 | 13 | 27 | `WHIRL_ISLAND_B2F` | 2 |
| 9 | 17 | 21 | `WHIRL_ISLAND_CAVE` | 1 |
**Coord events**: none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 30 | 4 | `BGEVENT_ITEM` | `WhirlIslandB1FHiddenRareCandy` -> `hiddenitem RARE_CANDY, EVENT_WHIRL_ISLAND_B1F_HIDDEN_RARE_CANDY` |
| 36 | 18 | `BGEVENT_ITEM` | `WhirlIslandB1FHiddenUltraBall` -> `hiddenitem ULTRA_BALL, EVENT_WHIRL_ISLAND_B1F_HIDDEN_ULTRA_BALL` |
| 2 | 23 | `BGEVENT_ITEM` | `WhirlIslandB1FHiddenFullRestore` -> `hiddenitem FULL_RESTORE, EVENT_WHIRL_ISLAND_B1F_HIDDEN_FULL_RESTORE` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `WHIRLISLANDB1F_POKE_BALL1` | `SPRITE_POKE_BALL` | 7 | 13 | `STILL` | `OBJECTTYPE_ITEMBALL` | `WhirlIslandB1FFullRestore` | `EVENT_WHIRL_ISLAND_B1F_FULL_RESTORE` |
| `WHIRLISLANDB1F_POKE_BALL2` | `SPRITE_POKE_BALL` | 2 | 18 | `STILL` | `OBJECTTYPE_ITEMBALL` | `WhirlIslandB1FCarbos` | `EVENT_WHIRL_ISLAND_B1F_CARBOS` |
| `WHIRLISLANDB1F_POKE_BALL3` | `SPRITE_POKE_BALL` | 33 | 23 | `STILL` | `OBJECTTYPE_ITEMBALL` | `WhirlIslandB1FCalcium` | `EVENT_WHIRL_ISLAND_B1F_CALCIUM` |
| `WHIRLISLANDB1F_POKE_BALL4` | `SPRITE_POKE_BALL` | 17 | 8 | `STILL` | `OBJECTTYPE_ITEMBALL` | `WhirlIslandB1FNugget` | `EVENT_WHIRL_ISLAND_B1F_NUGGET` |
| `WHIRLISLANDB1F_POKE_BALL5` | `SPRITE_POKE_BALL` | 19 | 26 | `STILL` | `OBJECTTYPE_ITEMBALL` | `WhirlIslandB1FEscapeRope` | `EVENT_WHIRL_ISLAND_B1F_ESCAPE_ROPE` |
| `WHIRLISLANDB1F_BOULDER` | `SPRITE_BOULDER` | 23 | 26 | `SPRITEMOVEDATA_STRENGTH_BOULDER` | `OBJECTTYPE_SCRIPT` | `WhirlIslandB1FBoulder` (`jumpstd StrengthBoulderScript`) | -1 |
The boulder at `(23, 26)` sits two tiles east of the Escape Rope ball and is the
only Strength object in the dungeon. The walkthrough never mentions it.
**Wild encounters**: grass/cave-floor, `data/wild/johto_grass.asm:1213-1239`,
`def_grass_wildmons WHIRL_ISLAND_B1F`, rates `6 percent` for all three times of
day, and the morn/day/nite slot lists are identical:
`23 KRABBY`, `24 ZUBAT`, `25 KRABBY`, `23 SEEL`, `24 GOLBAT`, `25 SEEL`, `25 SEEL`.
No water table for B1F.
---
### WHIRL_ISLAND_B2F
- Script: `maps/WhirlIslandB2F.asm`
- Blocks: `maps/WhirlIslandB2F.blk`
- Header: `data/maps/maps.asm:142`
- Dimensions: `constants/map_constants.asm:129` -> `10, 18` (group 3, map 64)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 11 | 5 | `WHIRL_ISLAND_B1F` | 7 |
| 2 | 7 | 11 | `WHIRL_ISLAND_B1F` | 8 |
| 3 | 7 | 25 | `WHIRL_ISLAND_LUGIA_CHAMBER` | 1 |
| 4 | 13 | 31 | `WHIRL_ISLAND_SW` | 5 |
**Coord events**: none. **BG events**: none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `WHIRLISLANDB2F_POKE_BALL1` | `SPRITE_POKE_BALL` | 10 | 11 | `STILL` | `OBJECTTYPE_ITEMBALL` | `WhirlIslandB2FFullRestore` | `EVENT_WHIRL_ISLAND_B2F_FULL_RESTORE` |
| `WHIRLISLANDB2F_POKE_BALL2` | `SPRITE_POKE_BALL` | 6 | 4 | `STILL` | `OBJECTTYPE_ITEMBALL` | `WhirlIslandB2FMaxRevive` | `EVENT_WHIRL_ISLAND_B2F_MAX_REVIVE` |
| `WHIRLISLANDB2F_POKE_BALL3` | `SPRITE_POKE_BALL` | 5 | 12 | `STILL` | `OBJECTTYPE_ITEMBALL` | `WhirlIslandB2FMaxElixer` | `EVENT_WHIRL_ISLAND_B2F_MAX_ELIXER` |
`(6, 4)` is the single `MAX_REVIVE` in the whole Whirl Islands (see Unresolved
for the walkthrough's "Max Revive x2").
**Wild encounters**
- Floor: `data/wild/johto_grass.asm:1241-1267`, rate `6 percent`, identical to B1F
(`23 KRABBY / 24 ZUBAT / 25 KRABBY / 23 SEEL / 24 GOLBAT / 25 SEEL / 25 SEEL`).
- Water: `data/wild/johto_water.asm:86-91`, rate `4 percent`: `15 HORSEA`,
`20 HORSEA`, `20 TENTACRUEL`.
The "two items on the cliff but you can't reach those" line matches the two balls
on the west shelf (`(5, 12)` Max Elixer and `(10, 11)` Full Restore are reachable
from the other side of the floor; exactly which two the FAQ means is a `.blk`
question, see Unresolved).
---
### WHIRL_ISLAND_LUGIA_CHAMBER
- Script: `maps/WhirlIslandLugiaChamber.asm` (`47:418c` callback, `47:41a0 Lugia`)
- Blocks: `maps/WhirlIslandLugiaChamber.blk` (`2b:717d`)
- Header: `data/maps/maps.asm:143` -> `TILESET_DARK_CAVE`, `CAVE`, `LANDMARK_WHIRL_ISLANDS`, `MUSIC_UNION_CAVE`, phone `TRUE`, `PALETTE_DARK`, `FISHGROUP_WHIRL_ISLANDS`
- Dimensions: `constants/map_constants.asm:130` -> `10, 9` (group 3, map 65)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 13 | `WHIRL_ISLAND_B2F` | 3 |
**Coord events**: none. **BG events**: none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `WHIRLISLANDLUGIACHAMBER_LUGIA` | `SPRITE_LUGIA` | 9 | 5 | `SPRITEMOVEDATA_POKEMON` | `OBJECTTYPE_SCRIPT`, palette `PAL_NPC_BLUE` | `Lugia` | `EVENT_WHIRL_ISLAND_LUGIA_CHAMBER_LUGIA` |
**Scripts of interest**
- `WhirlIslandLugiaChamberLugiaCallback` - `callback MAPCALLBACK_OBJECTS`.
`checkevent EVENT_FOUGHT_LUGIA / iftrue .NoAppear`, then
`checkitem SILVER_WING / iftrue .Appear`, else `.NoAppear`. So Lugia is on the
map **only while the SILVER_WING is in the bag and the fight has not happened**.
The wing is a `checkitem`, not an event - selling or storing it makes Lugia
vanish again.
- `Lugia` (`47:41a0`) - `faceplayer`, `opentext`, `writetext LugiaText`
("Gyaaas!"), `cry LUGIA`, `pause 15`, `closetext`,
**`setevent EVENT_FOUGHT_LUGIA`**, `checkver`, `iftrue .Silver`.
- Gold arm: `loadvar VAR_BATTLETYPE, BATTLETYPE_FORCEITEM`,
`loadwildmon LUGIA, 70`, `startbattle`,
`disappear WHIRLISLANDLUGIACHAMBER_LUGIA`, `reloadmapafterbattle`, `end`.
- `.Silver`: identical but `loadwildmon LUGIA, 40`.
- `EVENT_FOUGHT_LUGIA` is set **before** the battle, so fleeing, fainting or
knocking it out all consume the encounter. This is exactly why the
walkthrough insists on saving first: there is no re-arm path in the asm.
- `BATTLETYPE_FORCEITEM` (`constants/battle_constants.asm:101`) makes
`InitEnemyMon`'s `.WildItem` arm hand over `wBaseItem1` unconditionally
(`engine/battle/core.asm:5765-5773`). Lugia's `data/pokemon/base_stats/lugia.asm`
row is `db NO_ITEM, NO_ITEM`, so it holds nothing - the forced-item type is
inherited boilerplate here.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_FOUGHT_LUGIA` | `constants/event_flags.asm:455` | read by the callback, set by `Lugia` before `startbattle` | one-shot. Once set, Lugia never reappears |
| `EVENT_WHIRL_ISLAND_LUGIA_CHAMBER_LUGIA` | `constants/event_flags.asm:1247` | the object row's mask flag, driven by `appear`/`disappear` | object visibility only |
| `EVENT_GOT_SILVER_WING` | `constants/event_flags.asm:130` | set by `PewterCityGrampsScript` (Gold) / `RadioTower5F` (Silver) | prerequisite for holding `SILVER_WING` |
**Wild encounters**
- Floor: `data/wild/johto_grass.asm:1269-1295`, rate `6 percent`:
`24 KRABBY`, `25 ZUBAT`, `26 KRABBY`, `24 SEEL`, `25 GOLBAT`, `26 SEEL`, `26 SEEL`.
- Water: `data/wild/johto_water.asm:93-98`, rate `4 percent`: `20 HORSEA`,
`20 TENTACRUEL`, `20 SEADRA`. This is the only Seadra slot in the dungeon, and
it is on the water directly in front of Lugia.
**Catch math** (`data/pokemon/base_stats/lugia.asm`): `db 3 ; catch rate`.
`UltraBallMultiplier` doubles it to 6; `HeavyBallMultiplier`
(`engine/items/item_effects.asm:752`) adds a flat offset by dex weight, and
Lugia's dex weight is `4760` tenths of a pound = 216 kg
(`data/pokemon/dex_entries/gold/lugia.asm`), which lands in the `+20` bracket ->
effective 23. Thard_Verad's tip in the FAQ is correct and is worth ~4x the Ultra
Ball rate.
---
### ECRUTEAK_CITY (transit only)
- Script: `maps/EcruteakCity.asm`
- Header: `data/maps/maps.asm:173` -> `TILESET_JOHTO`, `TOWN`, `LANDMARK_ECRUTEAK_CITY`, `MUSIC_ECRUTEAK_CITY`, phone `FALSE`, `PALETTE_AUTO`, `FISHGROUP_POND`
- Dimensions: `constants/map_constants.asm:159` -> `20, 18` (group 4, map 9)
**Warps** (only the rows this section uses)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 3 | 18 | 11 | `ECRUTEAK_TIN_TOWER_ENTRANCE` | 1 |
| 4 | 20 | 2 | `ECRUTEAK_TIN_TOWER_BACK_ENTRANCE` | 1 |
| 5 | 20 | 3 | `ECRUTEAK_TIN_TOWER_BACK_ENTRANCE` | 2 |
| 12 | 37 | 7 | `TIN_TOWER_1F` | 1 |
| 13 | 5 | 5 | `BURNED_TOWER_1F` | 1 |
There is a `bg_event 38, 10, BGEVENT_READ, TinTowerSign` next to the tower door.
---
### ECRUTEAK_TIN_TOWER_ENTRANCE
- Script: `maps/EcruteakTinTowerEntrance.asm`
- Blocks: `maps/EcruteakTinTowerEntrance.blk`
- Header: `data/maps/maps.asm:165` -> `TILESET_TOWER`, `INDOOR`, `LANDMARK_ECRUTEAK_CITY`, `MUSIC_ECRUTEAK_CITY`, phone `FALSE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:151` -> `10, 9` (group 4, map 1)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 17 | `ECRUTEAK_CITY` | 3 |
| 2 | 5 | 17 | `ECRUTEAK_CITY` | 3 |
| 3 | 5 | 3 | `ECRUTEAK_TIN_TOWER_ENTRANCE` | 4 |
| 4 | 17 | 15 | `ECRUTEAK_TIN_TOWER_ENTRANCE` | 3 |
| 5 | 17 | 3 | `ECRUTEAK_TIN_TOWER_BACK_ENTRANCE` | 3 |
Warps 3 and 4 are a self-referential pair: the map holds two disconnected
corridors and the "walk up the stairs" is a same-map teleport from `(5, 3)` to
`(17, 15)`.
**Coord events**
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_ECRUTEAKTINTOWERENTRANCE_SAGE_BLOCKS` (0) | 4 | 7 | `EcruteakTinTowerEntranceSageBlocksLeft` (`52:400c`) | sage 2 side-steps LEFT, sage 1 is `moveobject`d to `(4, 6)` and `appear`ed, sage 2 `disappear`s |
| `SCENE_ECRUTEAKTINTOWERENTRANCE_SAGE_BLOCKS` (0) | 5 | 7 | `EcruteakTinTowerEntranceSageBlocksRight` (`52:4021`) | mirror image, sage 2 ends at `(5, 6)` |
Scene ids come from the `scene_script` macro's inline `const` block
(`macros/scripts/maps.asm:12-33`): `SAGE_BLOCKS` = 0, `NOOP` = 1. Both scene
bodies are `end`; the scene id exists only to arm/disarm the two coord events.
**BG events**: none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ECRUTEAKTINTOWERENTRANCE_SAGE1` | `SPRITE_SAGE` | 4 | 6 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `EcruteakTinTowerEntranceSageScript` (`52:4037`) | `EVENT_ECRUTEAK_TIN_TOWER_ENTRANCE_SAGE_LEFT` |
| `ECRUTEAKTINTOWERENTRANCE_SAGE2` | `SPRITE_SAGE` | 5 | 6 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `EcruteakTinTowerEntranceSageScript` | `EVENT_ECRUTEAK_TIN_TOWER_ENTRANCE_SAGE_RIGHT` |
| `ECRUTEAKTINTOWERENTRANCE_SAGE3` | `SPRITE_SAGE` | 6 | 9 | `WANDER` (1,1) | `OBJECTTYPE_SCRIPT` | `EcruteakTinTowerEntranceWanderingSageScript` (`52:404b`) | -1 |
| `ECRUTEAKTINTOWERENTRANCE_GRAMPS` | `SPRITE_GRAMPS` | 3 | 11 | `WANDER` (1,1) | `OBJECTTYPE_SCRIPT` | `EcruteakTinTowerEntranceGrampsScript` | -1 |
Object rows are masked when their flag is **set** (`CheckObjectFlag`,
`engine/overworld/map_objects_2.asm:32-61`: `EventFlagAction`/`CHECK_FLAG`
non-zero -> `.masked`). `InitializeEventsScript`
(`engine/events/std_scripts.asm:438`, line 533) does
`setevent EVENT_ECRUTEAK_TIN_TOWER_ENTRANCE_SAGE_LEFT` at new game, so on a fresh
save only SAGE2 at `(5, 6)` is standing there.
**Scripts of interest**
- `EcruteakTinTowerEntranceSageScript` - `faceplayer`, `opentext`,
`checkflag ENGINE_FOGBADGE`, `iftrue .BlockPassage_GotFogBadge`. Both arms are
pure text (`EcruteakTinTowerEntranceSageText` vs `..._GotFogBadge`); **the
script never opens the path**.
- The actual unblock is `maps/EcruteakGym.asm:34`:
`setmapscene ECRUTEAK_TIN_TOWER_ENTRANCE, SCENE_ECRUTEAKTINTOWERENTRANCE_NOOP`,
run in the same block as `setflag ENGINE_FOGBADGE` after beating Morty. With
the scene at `NOOP` the two coord events no longer match and the sage never
steps into the doorway. By section 25 the player has all 16 badges, so this is
already open - the walkthrough's "talk to the man and he'll let you through" is
flavour text, not a required action.
- `EcruteakTinTowerEntranceWanderingSageScript` - `checkevent EVENT_GOT_RAINBOW_WING`;
the post-wing line is "The TIN TOWER shook! A #MON must have returned to the
top!". This is the only in-map confirmation that Ho-Oh is now spawnable.
---
### ECRUTEAK_TIN_TOWER_BACK_ENTRANCE
- Script: `maps/EcruteakTinTowerBackEntrance.asm` - warps only, no scripts
- Blocks: `maps/EcruteakTinTowerBackEntrance.blk`
- Header: `data/maps/maps.asm:166` -> `TILESET_TRADITIONAL_HOUSE`, `INDOOR`, `LANDMARK_ECRUTEAK_CITY`, `MUSIC_ECRUTEAK_CITY`, phone `FALSE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:152` -> `4, 4` (group 4, map 2)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 7 | 4 | `ECRUTEAK_CITY` | 4 |
| 2 | 7 | 5 | `ECRUTEAK_CITY` | 5 |
| 3 | 2 | 4 | `ECRUTEAK_TIN_TOWER_ENTRANCE` | 5 |
---
### TIN_TOWER_1F
- Script: `maps/TinTower1F.asm` (`42:4b1d TinTowerSageScript`)
- Blocks: `maps/TinTower1F.blk`
- Header: `data/maps/maps.asm:82` -> `TILESET_TOWER`, `DUNGEON`, `LANDMARK_TIN_TOWER`, `MUSIC_TIN_TOWER`, phone `FALSE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:69` -> `10, 9` (group 3, map 4)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 15 | `ECRUTEAK_CITY` | 12 |
| 2 | 10 | 15 | `ECRUTEAK_CITY` | 12 |
| 3 | 10 | 2 | `TIN_TOWER_2F` | 2 |
**Coord events**: none. **BG events**: none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `TINTOWER1F_SAGE` | `SPRITE_SAGE` | 10 | 2 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `TinTowerSageScript` | `EVENT_TEAM_ROCKET_DISBANDED` |
**This is the real gate for the whole Silver branch.** The sage's coordinates are
`(10, 2)`, byte-for-byte the coordinates of warp 3. While the object is visible
the stair tile is occupied and 2F is unreachable. The object is masked - i.e. the
sage disappears - when `EVENT_TEAM_ROCKET_DISBANDED`
(`constants/event_flags.asm:1283`) is **set**, and that flag is only ever set by
the two Rainbow Wing hand-offs:
- `maps/RadioTower5F.asm:124-130` (Gold): `verbosegiveitem RAINBOW_WING` (124),
`setevent EVENT_GOT_RAINBOW_WING` (129), `setevent EVENT_TEAM_ROCKET_DISBANDED` (130).
- `maps/PewterCity.asm:42-51` `.RainbowWing` (Silver): the gramps gives
`RAINBOW_WING` (49), `setevent EVENT_GOT_RAINBOW_WING` (50),
`setevent EVENT_TEAM_ROCKET_DISBANDED` (51).
So the walkthrough's "another bald man will let you through because you have the
beautiful Rainbow Wing" is literally true, but the mechanism is a masked object
keyed on `EVENT_TEAM_ROCKET_DISBANDED`, not a `checkitem RAINBOW_WING`.
**Wild encounters**: none - `TIN_TOWER_1F` has no `def_grass_wildmons` row
(`data/wild/johto_grass.asm` starts the tower at `TIN_TOWER_2F`, line 61).
---
### TIN_TOWER_2F
- Script: `maps/TinTower2F.asm` - warps only
- Blocks: `maps/TinTower2F.blk`
- Header: `data/maps/maps.asm:83`; Dimensions `constants/map_constants.asm:70` -> `10, 9` (group 3, map 5)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 10 | 14 | `TIN_TOWER_3F` | 1 |
| 2 | 10 | 2 | `TIN_TOWER_1F` | 3 |
**Wild encounters**: `data/wild/johto_grass.asm:61-87`,
`def_grass_wildmons TIN_TOWER_2F`, rates `2 percent` morn/day/nite.
- morn/day: `20 RATTATA`, `21 RATTATA`, `22 RATTATA`, `22 RATTATA`, `23 RATTATA`, `24 RATTATA`, `24 RATTATA`
- nite: `20 GASTLY`, `21 GASTLY`, `22 GASTLY`, `22 RATTATA`, `23 RATTATA`, `24 RATTATA`, `24 RATTATA`
Every floor 2F..9F carries the identical table (checked 2F at line 61 and 9F at
line 257). The walkthrough only lists Rattata; Gastly at night is real.
---
### TIN_TOWER_3F
- Script: `maps/TinTower3F.asm`; Blocks `maps/TinTower3F.blk`
- Header: `data/maps/maps.asm:84`; Dimensions `constants/map_constants.asm:71` -> `10, 9` (group 3, map 6)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 10 | 14 | `TIN_TOWER_2F` | 1 |
| 2 | 16 | 2 | `TIN_TOWER_4F` | 2 |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `TINTOWER3F_POKE_BALL` | `SPRITE_POKE_BALL` | 3 | 14 | `STILL` | `OBJECTTYPE_ITEMBALL` | `TinTower3FFullHeal` | `EVENT_TIN_TOWER_3F_FULL_HEAL` |
Matches the walkthrough's "hop left for a Full Heal".
---
### TIN_TOWER_4F
- Script: `maps/TinTower4F.asm`; Blocks `maps/TinTower4F.blk`
- Header: `data/maps/maps.asm:85`; Dimensions `constants/map_constants.asm:72` -> `10, 9` (group 3, map 7)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 2 | 4 | `TIN_TOWER_5F` | 2 |
| 2 | 16 | 2 | `TIN_TOWER_3F` | 2 |
| 3 | 2 | 14 | `TIN_TOWER_5F` | 3 |
| 4 | 17 | 15 | `TIN_TOWER_5F` | 4 |
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 11 | 6 | `BGEVENT_ITEM` | `TinTower4FHiddenMaxPotion` -> `hiddenitem MAX_POTION, EVENT_TIN_TOWER_4F_HIDDEN_MAX_POTION` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `TINTOWER4F_POKE_BALL1` | `SPRITE_POKE_BALL` | 14 | 10 | `STILL` | `OBJECTTYPE_ITEMBALL` | `TinTower4FUltraBall` (`42:4cc1`) | `EVENT_TIN_TOWER_4F_ULTRA_BALL` |
| `TINTOWER4F_POKE_BALL2` | `SPRITE_POKE_BALL` | 17 | 14 | `STILL` | `OBJECTTYPE_ITEMBALL` | `TinTower4FSuperPotion` | `EVENT_TIN_TOWER_4F_SUPER_POTION` |
| `TINTOWER4F_POKE_BALL3` | `SPRITE_POKE_BALL` | 2 | 12 | `STILL` | `OBJECTTYPE_ITEMBALL` | `TinTower4FEscapeRope` | `EVENT_TIN_TOWER_4F_ESCAPE_ROPE` |
---
### TIN_TOWER_5F
- Script: `maps/TinTower5F.asm`; Blocks `maps/TinTower5F.blk`
- Header: `data/maps/maps.asm:86`; Dimensions `constants/map_constants.asm:73` -> `10, 9` (group 3, map 8)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 11 | 15 | `TIN_TOWER_6F` | 2 |
| 2 | 2 | 4 | `TIN_TOWER_4F` | 1 |
| 3 | 2 | 14 | `TIN_TOWER_4F` | 3 |
| 4 | 17 | 15 | `TIN_TOWER_4F` | 4 |
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 16 | 14 | `BGEVENT_ITEM` | `TinTower5FHiddenFullRestore` -> `hiddenitem FULL_RESTORE, EVENT_TIN_TOWER_5F_HIDDEN_FULL_RESTORE` |
| 3 | 15 | `BGEVENT_ITEM` | `TinTower5FHiddenCarbos` -> `hiddenitem CARBOS, EVENT_TIN_TOWER_5F_HIDDEN_CARBOS` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `TINTOWER5F_POKE_BALL` | `SPRITE_POKE_BALL` | 9 | 9 | `STILL` | `OBJECTTYPE_ITEMBALL` | `TinTower5FRareCandy` (`42:4d12`) | `EVENT_TIN_TOWER_5F_RARE_CANDY` |
---
### TIN_TOWER_6F
- Script: `maps/TinTower6F.asm` - warps only, no items, no objects
- Blocks: `maps/TinTower6F.blk`
- Header: `data/maps/maps.asm:87`; Dimensions `constants/map_constants.asm:74` -> `10, 9` (group 3, map 9)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 9 | `TIN_TOWER_7F` | 1 |
| 2 | 11 | 15 | `TIN_TOWER_5F` | 1 |
The walkthrough's "go up and you'll get a Max Potion ... left across the upper
bridge to get a Full Heal" floor has **no item balls and no bg events at all** in
pokegold. See Unresolved.
---
### TIN_TOWER_7F
- Script: `maps/TinTower7F.asm`; Blocks `maps/TinTower7F.blk`
- Header: `data/maps/maps.asm:88`; Dimensions `constants/map_constants.asm:75` -> `10, 9` (group 3, map 10)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 9 | `TIN_TOWER_6F` | 1 |
| 2 | 10 | 15 | `TIN_TOWER_8F` | 1 |
| 3 | 12 | 7 | `TIN_TOWER_7F` | 4 |
| 4 | 8 | 3 | `TIN_TOWER_7F` | 3 |
| 5 | 6 | 9 | `TIN_TOWER_9F` | 5 |
Warps 3/4 are the in-floor pair the walkthrough calls "go through the next two
warps".
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `TINTOWER7F_POKE_BALL` | `SPRITE_POKE_BALL` | 16 | 1 | `STILL` | `OBJECTTYPE_ITEMBALL` | `TinTower7FMaxRevive` | `EVENT_TIN_TOWER_7F_MAX_REVIVE` |
---
### TIN_TOWER_8F
- Script: `maps/TinTower8F.asm`; Blocks `maps/TinTower8F.blk`
- Header: `data/maps/maps.asm:89`; Dimensions `constants/map_constants.asm:76` -> `10, 9` (group 3, map 11)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 2 | 5 | `TIN_TOWER_7F` | 2 |
| 2 | 2 | 11 | `TIN_TOWER_9F` | 1 |
| 3 | 16 | 7 | `TIN_TOWER_9F` | 2 |
| 4 | 10 | 3 | `TIN_TOWER_9F` | 3 |
| 5 | 14 | 15 | `TIN_TOWER_9F` | 6 |
| 6 | 6 | 9 | `TIN_TOWER_9F` | 7 |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `TINTOWER8F_POKE_BALL1` | `SPRITE_POKE_BALL` | 7 | 13 | `STILL` | `OBJECTTYPE_ITEMBALL` | `TinTower8FNugget` | `EVENT_TIN_TOWER_8F_NUGGET` |
| `TINTOWER8F_POKE_BALL2` | `SPRITE_POKE_BALL` | 11 | 6 | `STILL` | `OBJECTTYPE_ITEMBALL` | `TinTower8FMaxElixer` | `EVENT_TIN_TOWER_8F_MAX_ELIXER` |
| `TINTOWER8F_POKE_BALL3` | `SPRITE_POKE_BALL` | 3 | 1 | `STILL` | `OBJECTTYPE_ITEMBALL` | `TinTower8FFullRestore` | `EVENT_TIN_TOWER_8F_FULL_RESTORE` |
---
### TIN_TOWER_9F
- Script: `maps/TinTower9F.asm` (`42:4de0 TinTower9F_MapScripts`) - no callbacks,
no objects; carries two unreferenced texts `TinTower9FUnusedHoOhText` /
`TinTower9FUnusedLugiaText`
- Blocks: `maps/TinTower9F.blk` (`2b:625f`)
- Header: `data/maps/maps.asm:90`; Dimensions `constants/map_constants.asm:77` -> `10, 9` (group 3, map 12)
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 12 | 3 | `TIN_TOWER_8F` | 2 |
| 2 | 2 | 5 | `TIN_TOWER_8F` | 3 |
| 3 | 12 | 7 | `TIN_TOWER_8F` | 4 |
| 4 | 7 | 9 | `TIN_TOWER_ROOF` | 1 |
| 5 | 16 | 7 | `TIN_TOWER_7F` | 5 |
| 6 | 6 | 13 | `TIN_TOWER_8F` | 5 |
| 7 | 8 | 13 | `TIN_TOWER_8F` | 6 |
Warp 4 at `(7, 9)` is the only exit to the roof.
---
### TIN_TOWER_ROOF
- Script: `maps/TinTowerRoof.asm` (`5b:68ff` callback, `5b:6913 TinTowerHoOh`)
- Blocks: `maps/TinTowerRoof.blk` (`2b:62b9`)
- Header: `data/maps/maps.asm:344` -> `TILESET_TOWER`, **`ROUTE`**, `LANDMARK_TIN_TOWER`, `MUSIC_TIN_TOWER`, phone `FALSE`, `PALETTE_AUTO`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:318` -> `10, 9`, and note the group:
`TIN_TOWER_ROOF` is map 12 of **group 15 (`FAST_SHIP`)**, not group 3 with the
rest of the tower.
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 13 | `TIN_TOWER_9F` | 4 |
**Coord events**: none. **BG events**: none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `TINTOWERROOF_HO_OH` | `SPRITE_HO_OH` | 9 | 5 | `SPRITEMOVEDATA_POKEMON` | `OBJECTTYPE_SCRIPT`, palette `PAL_NPC_RED` | `TinTowerHoOh` | `EVENT_TIN_TOWER_ROOF_HO_OH` |
**Scripts of interest**
- `TinTowerRoofHoOhCallback` - `callback MAPCALLBACK_OBJECTS`:
`checkevent EVENT_FOUGHT_HO_OH / iftrue .NoAppear`, then
`checkitem RAINBOW_WING / iftrue .Appear`. Same shape as Lugia's: the wing must
be **in the bag**, not merely once-received.
- `TinTowerHoOh` (`5b:6913`) - text "Shaoooh!", `cry HO_OH`, `pause 15`,
`setevent EVENT_FOUGHT_HO_OH` **before** the fight, `checkver / iftrue .Silver`.
- Gold arm: `loadwildmon HO_OH, 40`.
- `.Silver`: `loadwildmon HO_OH, 70` - the level the walkthrough quotes.
- Both arms run `loadvar VAR_BATTLETYPE, BATTLETYPE_FORCEITEM` first, and
Ho-Oh's base row is `db SACRED_ASH, SACRED_ASH`
(`data/pokemon/base_stats/ho_oh.asm:9`), so **Ho-Oh always holds a SACRED_ASH**.
That is a guaranteed item a bot should strip before/after the catch.
**Escape Rope caveat**: `EscapeRopeFunction` -> `.CheckCanDig`
(`engine/events/overworld.asm:724-754`) only succeeds when
`GetMapEnvironment` is `CAVE` or `DUNGEON`. `TIN_TOWER_ROOF` is `ROUTE`, so the
walkthrough is right that you must go back down to 9F first. Every Whirl Island
map including `WHIRL_ISLAND_LUGIA_CHAMBER` is `CAVE`, so the rope works on the
spot there.
**Catch math**: Ho-Oh's catch rate is also `3`
(`data/pokemon/base_stats/ho_oh.asm:7`). Its dex weight is `4390`
(`data/pokemon/dex_entries/gold/ho_oh.asm`) = 199 kg, which falls in the
`HeavyBallMultiplier` "add 0" bracket - so unlike Lugia, a Heavy Ball is *worse*
than an Ultra Ball on Ho-Oh.
---
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Reaching Route 41 at all | `SurfFunction.TrySurf`, `engine/events/overworld.asm:322` (badge test at `:340`) | `ENGINE_FOGBADGE` + a party member that knows `SURF` | Fog Badge (Morty) |
| Whirlpools guarding the Whirl Islands | `WhirlpoolFunction` `engine/events/overworld.asm:1061` (badge test at `:1077`) and `TryWhirlpoolOW` `:1167` (badge test at `:1171`) | `ENGINE_GLACIERBADGE` + `WHIRLPOOL` in the party | Glacier Badge (Pryce, badge 7) - **not** eight badges, see Unresolved |
| Darkness inside every Whirl Island map | `FlashFunction.CheckUseFlash`, `engine/events/overworld.asm:271` | `ENGINE_ZEPHYRBADGE` + `FLASH`, and `wTimeOfDayPalset == DARKNESS_PALSET` (the map header's `PALETTE_DARK`) | Zephyr Badge. Flash is *refused* on any non-dark map, so a bot must only try it inside |
| Climbing back up the B2F waterfall | `WaterfallFunction` `engine/events/overworld.asm:611` (badge test at `:618`) and `TryWaterfallOW` `:683` (badge test at `:687`); direction check `CheckMapCanWaterfall` (must face UP, `wTileUp` is a waterfall tile) | `ENGINE_RISINGBADGE` + `WATERFALL` | Rising Badge (Clair). Descending needs nothing - only the UP direction is gated |
| B1F boulder at `(23, 26)` | `WhirlIslandB1FBoulder` -> `jumpstd StrengthBoulderScript` | `STRENGTH` + `ENGINE_PLAINBADGE` | optional; not on the Lugia path |
| Lugia not on the map | `WhirlIslandLugiaChamberLugiaCallback` | `checkitem SILVER_WING` true **and** `EVENT_FOUGHT_LUGIA` clear | `EVENT_GOT_SILVER_WING` via `PewterCityGrampsScript` (Gold) or `RadioTower5F` (Silver) - and keep the wing in the bag |
| Ho-Oh not on the map | `TinTowerRoofHoOhCallback` | `checkitem RAINBOW_WING` true **and** `EVENT_FOUGHT_HO_OH` clear | `EVENT_GOT_RAINBOW_WING` via `RadioTower5F` (Gold) or `PewterCityGrampsScript` `.RainbowWing` (Silver) |
| Tin Tower stairs to 2F | `TinTower1F` object row: sage stands on `(10, 2)`, masked by `EVENT_TEAM_ROCKET_DISBANDED` | flag must be **set** | `setevent EVENT_TEAM_ROCKET_DISBANDED` in `maps/RadioTower5F.asm:130` or `maps/PewterCity.asm:51` |
| Tin Tower entrance corridor | `coord_event 4, 7` / `5, 7` under `SCENE_ECRUTEAKTINTOWERENTRANCE_SAGE_BLOCKS` | scene must be `NOOP` | `maps/EcruteakGym.asm:34` `setmapscene ... SCENE_ECRUTEAKTINTOWERENTRANCE_NOOP` on the Fog Badge |
| One-shot mascot | `Lugia` / `TinTowerHoOh` set `EVENT_FOUGHT_LUGIA` / `EVENT_FOUGHT_HO_OH` **before** `startbattle` | n/a | none. Save-before-fight is the only recovery |
---
## 4. Bot checklist
**Preconditions for either branch**: 8 Johto badges (Zephyr, Fog, Plain, Glacier,
Rising in particular), party moves `SURF`, `WHIRLPOOL`, `FLASH`, `WATERFALL`
(`STRENGTH` optional), the version-opposite wing in the bag, a stack of
`ULTRA_BALL` (`HEAVY_BALL` for Lugia), and a save immediately before the mascot.
**Gold branch - Lugia**
| # | Map | Target | Intent | Precondition | Postcondition |
|---|---|---|---|---|---|
| 1 | any | Olivine City | fly | `ENGINE_FLYPOINT_OLIVINE` | on `OLIVINE_CITY` |
| 2 | `OLIVINE_CITY` | west edge | walk, then surf | `ENGINE_FOGBADGE` + `SURF` | on `ROUTE_40` |
| 3 | `ROUTE_40` | south edge | surf | - | on `ROUTE_41` |
| 4 | `ROUTE_41` | any block id `$07` (`TILESET_JOHTO` whirlpool) | face it, press A -> `TryWhirlpoolOW` | `ENGINE_GLACIERBADGE` + `WHIRLPOOL` | block replaced with `$36` for this map session only (`DisappearWhirlpool` writes the map buffer, not the save) |
| 5 | `ROUTE_41` | `(36, 19)` warp 2 | walk onto | whirlpools cleared | on `WHIRL_ISLAND_NE` at `(3, 13)` |
| 6 | `WHIRL_ISLAND_NE` | menu | use `FLASH` | `ENGINE_ZEPHYRBADGE`, map palette `PALETTE_DARK` | cave lit |
| 7 | `WHIRL_ISLAND_NE` | object `WHIRLISLANDNE_POKE_BALL` `(11, 11)` | walk onto / A | `EVENT_WHIRL_ISLAND_NE_ULTRA_BALL` clear | `ULTRA_BALL` in bag, flag set |
| 8 | `WHIRL_ISLAND_NE` | warp 2 `(17, 3)` (or warp 3 `(13, 11)`) | walk onto | - | `WHIRL_ISLAND_B1F` `(35, 3)` / `(29, 9)` |
| 9 | `WHIRL_ISLAND_B1F` | `(19, 26)` | pick up | `EVENT_WHIRL_ISLAND_B1F_ESCAPE_ROPE` clear | `ESCAPE_ROPE` in bag |
| 10 | `WHIRL_ISLAND_B1F` | `(33, 23)` | pick up | `EVENT_WHIRL_ISLAND_B1F_CALCIUM` clear | `CALCIUM` in bag |
| 11 | `WHIRL_ISLAND_B1F` | optional `(30, 4)`, `(36, 18)`, `(2, 23)` | face + A (hidden) | matching `EVENT_..._HIDDEN_*` clear | Rare Candy / Ultra Ball / Full Restore |
| 12 | `WHIRL_ISLAND_B1F` | warp 4 `(9, 31)` or warp 5 `(23, 31)` | walk onto | - | `WHIRL_ISLAND_SW` |
| 13 | `WHIRL_ISLAND_SW` | `(15, 2)` | pick up | `EVENT_WHIRL_ISLAND_SW_GUARD_SPEC` clear | `GUARD_SPEC` in bag |
| 14 | `WHIRL_ISLAND_B1F` | warp 7 `(25, 21)` | walk onto | - | `WHIRL_ISLAND_B2F` `(11, 5)` |
| 15 | `WHIRL_ISLAND_B2F` | `(6, 4)` | pick up | `EVENT_WHIRL_ISLAND_B2F_MAX_REVIVE` clear | `MAX_REVIVE` in bag |
| 16 | `WHIRL_ISLAND_B2F` | waterfall block, moving DOWN | surf south over it | `SURF` only (no badge for descent) | lower B2F pool |
| 17 | `WHIRL_ISLAND_B2F` | warp 3 `(7, 25)` | walk onto | - | `WHIRL_ISLAND_LUGIA_CHAMBER` `(9, 13)` |
| 18 | `WHIRL_ISLAND_LUGIA_CHAMBER` | - | **SAVE** | - | recoverable |
| 19 | `WHIRL_ISLAND_LUGIA_CHAMBER` | object at `(9, 5)` | surf north, A | `SILVER_WING` in bag, `EVENT_FOUGHT_LUGIA` clear | `EVENT_FOUGHT_LUGIA` set, wild Lugia Lv70 (Gold) / Lv40 (Silver) |
| 20 | battle | - | weaken, status, throw balls | catch rate 3 (Ultra 6, Heavy 23) | Lugia caught, `disappear` + `reloadmapafterbattle` |
| 21 | `WHIRL_ISLAND_LUGIA_CHAMBER` | bag | use `ESCAPE_ROPE` | environment `CAVE` | back at last Pokecenter spawn |
**Silver branch - Ho-Oh**
| # | Map | Target | Intent | Precondition | Postcondition |
|---|---|---|---|---|---|
| 1 | any | Ecruteak City | fly | `ENGINE_FLYPOINT_ECRUTEAK` | on `ECRUTEAK_CITY` |
| 2 | `ECRUTEAK_CITY` | warp 3 `(18, 11)` | walk onto | - | `ECRUTEAK_TIN_TOWER_ENTRANCE` `(4, 17)` |
| 3 | `ECRUTEAK_TIN_TOWER_ENTRANCE` | `(4, 7)` / `(5, 7)` | walk through | map scene must be `NOOP` (Fog Badge) | no coord event fires |
| 4 | `ECRUTEAK_TIN_TOWER_ENTRANCE` | warp 3 `(5, 3)` | walk onto | - | same map, `(17, 15)` |
| 5 | `ECRUTEAK_TIN_TOWER_ENTRANCE` | warp 5 `(17, 3)` | walk onto | - | `ECRUTEAK_TIN_TOWER_BACK_ENTRANCE` `(2, 4)` |
| 6 | `ECRUTEAK_TIN_TOWER_BACK_ENTRANCE` | warp 1 `(7, 4)` | walk onto | - | `ECRUTEAK_CITY` `(20, 2)` |
| 7 | `ECRUTEAK_CITY` | warp 12 `(37, 7)` | walk east then onto | - | `TIN_TOWER_1F` `(9, 15)` |
| 8 | `TIN_TOWER_1F` | warp 3 `(10, 2)` | walk onto | `EVENT_TEAM_ROCKET_DISBANDED` **set** (sage masked) | `TIN_TOWER_2F` `(10, 2)` |
| 9 | `TIN_TOWER_2F` | warp 1 `(10, 14)` | walk onto | - | `TIN_TOWER_3F` |
| 10 | `TIN_TOWER_3F` | `(3, 14)` then warp 2 `(16, 2)` | pick up, walk onto | `EVENT_TIN_TOWER_3F_FULL_HEAL` clear | `FULL_HEAL`; `TIN_TOWER_4F` |
| 11 | `TIN_TOWER_4F` | `(14, 10)`, `(17, 14)`, `(2, 12)`, hidden `(11, 6)` | pick up | matching flags clear | Ultra Ball, Super Potion, Escape Rope, hidden Max Potion |
| 12 | `TIN_TOWER_4F` | warp 1 `(2, 4)` / 3 `(2, 14)` / 4 `(17, 15)` | walk onto | - | `TIN_TOWER_5F` |
| 13 | `TIN_TOWER_5F` | `(9, 9)`, hidden `(16, 14)` and `(3, 15)` | pick up | matching flags clear | Rare Candy, Full Restore, Carbos |
| 14 | `TIN_TOWER_5F` | warp 1 `(11, 15)` | walk onto | - | `TIN_TOWER_6F` `(11, 15)` |
| 15 | `TIN_TOWER_6F` | warp 1 `(3, 9)` | walk onto | - | `TIN_TOWER_7F` `(3, 9)` |
| 16 | `TIN_TOWER_7F` | `(16, 1)`; warps 3 `(12, 7)` / 4 `(8, 3)` | pick up, hop the pair | `EVENT_TIN_TOWER_7F_MAX_REVIVE` clear | `MAX_REVIVE` |
| 17 | `TIN_TOWER_7F` | warp 2 `(10, 15)` | walk onto | - | `TIN_TOWER_8F` `(2, 5)` |
| 18 | `TIN_TOWER_8F` | `(7, 13)`, `(11, 6)`, `(3, 1)` | pick up | matching flags clear | Nugget, Max Elixer, Full Restore |
| 19 | `TIN_TOWER_8F` | warp 2 `(2, 11)` (or 3/4/5/6) | walk onto | - | `TIN_TOWER_9F` |
| 20 | `TIN_TOWER_9F` | warp 4 `(7, 9)` | walk onto | - | `TIN_TOWER_ROOF` `(9, 13)` |
| 21 | `TIN_TOWER_ROOF` | - | **SAVE** | - | recoverable |
| 22 | `TIN_TOWER_ROOF` | object at `(9, 5)` | walk up, A | `RAINBOW_WING` in bag, `EVENT_FOUGHT_HO_OH` clear | `EVENT_FOUGHT_HO_OH` set, wild Ho-Oh Lv70 (Silver) / Lv40 (Gold) holding `SACRED_ASH` |
| 23 | battle | - | weaken, status, Ultra Balls | catch rate 3; Heavy Ball gives no bonus here | Ho-Oh caught |
| 24 | `TIN_TOWER_9F` | warp 1 on the roof, then bag | descend, use `ESCAPE_ROPE` | roof is `ROUTE` environment - rope fails there | back at spawn |
---
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Map headers, warps, coord/bg/object events for all of these maps | `src/import/RomExtractorGen2.lua` (walks all 26 map groups, `MAP_GROUP_COUNT` at line 47), consumed by `src/world/gen2/Map.lua` / `World.lua` | implemented (generic - no map-specific code, so these maps come in with everything else) |
| `MAPCALLBACK_OBJECTS` (the Lugia / Ho-Oh appear-check) | `src/world/gen2/World.lua:5700` `runMapCallback("MAPCALLBACK_OBJECTS")`; regression driver `tests/drivers/gold_map_callbacks.lua` | implemented |
| Object masking by event flag (`CheckObjectFlag`) - the Tin Tower 1F sage, every item ball | `src/world/gen2/World.lua:5106,5122` `events:objectVisible(obj.eventFlag)` | implemented |
| `checkver` version split (Lv70 vs Lv40 mascot) | `src/script/gen2/Vm.lua:774-783`; opcode table `src/script/gen2/Opcodes.lua:29` | implemented (comments even cite `WhirlIslandLugiaChamber` by name) |
| `loadwildmon` / `startbattle` / `reloadmapafterbattle` | `src/script/gen2/Opcodes.lua:98-101`, `src/script/gen2/Vm.lua:817-896` | implemented |
| `BATTLETYPE_FORCEITEM` -> Ho-Oh's guaranteed `SACRED_ASH` | `src/world/gen2/World.lua:107-111` and `:4571-4579` | implemented |
| Whirlpool field move, badge gate and block replacement | `src/world/gen2/FieldMoves.lua:109` (`WHIRLPOOL = "GLACIER"`), `:219` `WHIRLPOOL_BLOCKS`, `:247` `somethingToWhirlpool`, `:542` `whirlpoolFromMenu`, `:626` `tryWhirlpoolOW` | implemented |
| Flash + `PALETTE_DARK` darkness | `src/world/gen2/FieldMoves.lua:105,468`; `src/world/gen2/Palettes.lua:53,75-103` | implemented |
| Waterfall (up-only gate, `RISINGBADGE`) | `src/world/gen2/FieldMoves.lua:110,258,646` | implemented |
| Hidden items (`hiddenitem` bg events) | `src/world/gen2/HiddenItems.lua`, `src/world/gen2/World.lua:1373,5286` | implemented |
| Catch rate formula incl. the two cart bugs | `src/battle/gen2/Catching.lua:1-90` | implemented |
| **Heavy Ball weight bonus** (the FAQ's Lugia tip) | `src/battle/gen2/Catching.lua:36` - `HEAVY_BALL = 1`, a flat multiplier with a comment saying the specialty balls "key off conditions the caller supplies", and no dex-weight lookup anywhere | **missing** - a Heavy Ball currently behaves as a plain Poke Ball on Lugia |
| Magnet Train hop that opens the section | `src/core/gen2/MagnetTrain.lua` | implemented (not re-verified for this section) |
| Escape Rope environment check (`CAVE`/`DUNGEON` only, roof excluded) | not located in `src/world/gen2/` or `src/battle/gen2/` by grep | **unverified** - could not find an `EscapeRopeFunction` equivalent; a bot should not assume the roof refusal is modelled |
| Hand-ported scripts for these specific maps | none - grep for `whirl`/`tintower`/`lugia`/`hooh` across `src/` returns only battle-anim and title-screen hits | not applicable (all script bodies are extracted, run through `src/script/gen2/Vm.lua`) |
---
## 6. Unresolved / verify by hand
1. **"You'll need eight badges in order to use Whirlpool."** The asm gates
Whirlpool on `ENGINE_GLACIERBADGE` alone (`engine/events/overworld.asm:1077`
and `:1171`), i.e. seven badges. Eight is wrong, though harmless in practice
for a post-game section.
2. **Whirl Islands item list.** The FAQ lists Calcium, Escape Rope, Guard Specs,
Max Revive x2, Ultra Ball. The asm has *one* Max Revive in the entire dungeon
(`WhirlIslandB2FMaxRevive` at B2F `(6, 4)`) and additionally a `FULL_RESTORE`,
`CARBOS`, `NUGGET` on B1F, a second `FULL_RESTORE` and a `MAX_ELIXER` on B2F,
plus three hidden items on B1F that the FAQ never mentions. The walkthrough's
two "grab the Max Revive" beats cannot both be the B2F ball; the first one is
unlocatable.
3. **Tin Tower item list.** The FAQ names a PP Up, an HP Up, a Max Potion item
ball, and a second Full Heal. None exist in pokegold: there is no `PP_UP` or
`HP_UP` anywhere in `maps/TinTower*.asm`, the only Max Potion is the *hidden*
`TinTower4FHiddenMaxPotion` at `(11, 6)`, and the only Full Heal is
`TinTower3FFullHeal`. Conversely the FAQ omits `SUPER_POTION` (4F),
`NUGGET`/`FULL_RESTORE` (8F) and the three hidden items on 4F/5F. The list
looks like it was written against a different version of the game.
4. **"On the next level, go up and you'll get a Max Potion ... Full Heal"** places
two items on the 6F-shaped floor; `maps/TinTower6F.asm` has an empty
`def_bg_events` and an empty `def_object_events`. Not locatable.
5. **Ho-Oh's moveset.** The FAQ says "Safeguard and Ancient Power ..., Punishment,
and Sacred Fire". `PUNISHMENT` does not exist in Gen 2 at all
(`data/pokemon/evos_attacks.asm:3324-3336` is the whole learnset). Derived
from that table plus `FillMoves` (`engine/pokemon/evolve.asm:478`, which keeps
the last four moves learnable at or below the level), a level-70 Ho-Oh should
have `RECOVER`, `FIRE_BLAST`, `SUNNY_DAY`, `SWIFT`, and a level-40 one
`SACRED_FIRE`, `SAFEGUARD`, `GUST`, `RECOVER`. Worth confirming in-engine
before a bot plans around Sacred Fire; the `FillMoves` derivation is mine, not
a literal table in the asm.
6. **Lugia's Pokemon list at the Whirl Islands.** The FAQ lists Zubat, Seel,
Krabby, Horsea, Seadra and omits `GOLBAT` (in every grass slot 5),
`TENTACOOL` / `TENTACRUEL` (SW and B2F water). Seadra only appears in
`WHIRL_ISLAND_LUGIA_CHAMBER`'s water table.
7. **"Swimmer Kara" as a whirlpool landmark.** Her object is at `(44, 28)` on
Route 41; the whirlpool "just northwest" of her is a `.blk` block, not an
event, so the exact coordinate could not be pinned from the asm text. A bot
must scan `maps/Route41.blk` for `TILESET_JOHTO` block `$07`.
8. **The Whirl Islands ledge/jump routing** ("leap the ledge to the right on that
bike", "two items on the cliff but you can't reach those") is entirely block
and collision data in `maps/WhirlIsland*.blk`. None of it is expressible from
the event tables; a bot needs the decoded block/collision grid.
9. **The Tin Tower jump-platform routing** (the long "left x5, down, right x2 ..."
sequences) is likewise `.blk` data. Only the warp endpoints above are asm
facts.
10. **"go into the Bell Tower entry house"** - the map, the sign and every text
string in pokegold call it TIN TOWER (`TinTowerSign`,
`EcruteakTinTowerEntranceSageText`). "Bell Tower" is the post-GS rename.
11. **Escape Rope on the port.** Could not find the port's equivalent of
`EscapeRopeFunction`'s `CAVE`/`DUNGEON` environment check, so I cannot say
whether the port correctly refuses the rope on `TIN_TOWER_ROOF`.
@@ -0,0 +1,893 @@
# Section 26 - Mount Moon and Routes 1-4
Source: `../section-26-mount-moon-and-routes-1-4.txt` (walkthrough heading "32 > Mount Moon and Routes 1-4")
Maps covered: `MAP_ROUTE_3`, `MAP_MOUNT_MOON`, `MAP_MOUNT_MOON_SQUARE`, `MAP_MOUNT_MOON_GIFT_SHOP`, `MAP_ROUTE_4`, `MAP_VIRIDIAN_CITY`, `MAP_TRAINER_HOUSE_1F`, `MAP_TRAINER_HOUSE_B1F`, `MAP_ROUTE_1`, `MAP_PALLET_TOWN`, `MAP_OAKS_LAB`
Badges / key milestones in this section: no badge. The milestones are `EVENT_BEAT_RIVAL_IN_MT_MOON` (the last mandatory Silver fight, and the unlock for both rival re-encounters), `ENGINE_FLYPOINT_VIRIDIAN` and `ENGINE_FLYPOINT_PALLET` (Fly targets), `EVENT_TALKED_TO_OAK_IN_KANTO` (Oak's Kanto-badge conversation, which later opens Mt. Silver via `EVENT_OPENED_MT_SILVER`), plus the `HP_UP` item ball on Route 4.
Note on paths: everything under `maps/`, `data/`, `constants/`, `engine/`, `macros/` is relative to the pokegold checkout at `/Users/bryanbassett/Documents/development/pokegold`. Everything under `src/` and `tests/` is relative to this repo root. Addresses are `bank:addr` from `/Users/bryanbassett/Documents/development/pokegold-symbols/pokegold.sym`.
---
## 1. Route order
The section starts with the player already standing on Route 3 (arriving from Pewter City, which belongs to the previous section) and ends by handing off to Route 21 (next section).
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 1 | `MAP_ROUTE_3` | `maps/Route3.asm` | west connection from `PEWTER_CITY` (`data/maps/attributes.asm:377`) | warp 1 at (52, 1) -> `MOUNT_MOON` warp 1 | four trainers east: Warren, Jimmy, Otis, Burt |
| 2 | `MAP_ROUTE_3` -> Pewter and back | - | - | - | walkthrough tells the player to heal at the Pewter Pokemon Center and save before Mt. Moon (no asm gate; pure advice) |
| 3 | `MAP_MOUNT_MOON` | `maps/MountMoon.asm` | warp 1 at (3, 3) | see below | mandatory rival battle, fired by `scene_script MountMoonRivalEncounterScene, SCENE_MOUNTMOON_RIVAL_BATTLE` |
| 4 | `MAP_MOUNT_MOON` (north lane) | `maps/MountMoon.asm` | warp 3 at (13, 3) -> warp 7 at (25, 3) | warp 5 at (25, 5) -> `MOUNT_MOON_SQUARE` warp 1 | "go right from your Rival battle, up the ladder, and then out" |
| 5 | `MAP_MOUNT_MOON_SQUARE` | `maps/MountMoonSquare.asm` | warp 1 at (20, 5) | warp 3 at (13, 7) -> gift shop; warp 2 at (22, 11) -> `MOUNT_MOON` warp 6 | the outdoor shop and the Monday-night Clefairy dance |
| 6 | `MAP_MOUNT_MOON_GIFT_SHOP` | `maps/MountMoonGiftShop.asm` | warp 1 (3, 7) / warp 2 (4, 7) | either warp -> `MOUNT_MOON_SQUARE` warp 3 | "a shop outside that sells Fresh Water, Soda Pop, and Lemonade" (`MART_MT_MOON`) |
| 7 | `MAP_MOUNT_MOON` (south lane) | `maps/MountMoon.asm` | warp 6 at (25, 15) | warp 8 at (25, 13) -> warp 4 at (15, 11), then warp 2 at (15, 15) | "go down into another cave entrance and down the ladder, you'll make your way through" |
| 8 | `MAP_ROUTE_4` | `maps/Route4.asm` | warp 1 at (2, 5) from `MOUNT_MOON` warp 2 | east connection to `CERULEAN_CITY` (not taken here) | three trainers: Hope, Hank, Sharon; `HP_UP` item ball at (26, 3) |
| 9 | `MAP_VIRIDIAN_CITY` | `maps/ViridianCity.asm` | walkthrough routes Fly -> Pewter -> `ROUTE_2` -> `VIRIDIAN_FOREST` -> Viridian (those two maps belong to a neighbouring section) | warp 3 at (23, 15) -> `TRAINER_HOUSE_1F` | Trainer House daily battle |
| 10 | `MAP_TRAINER_HOUSE_1F` | `maps/TrainerHouse1F.asm` | warp 1/2 at (2, 13)/(3, 13) | warp 3 at (8, 2) -> `TRAINER_HOUSE_B1F` warp 1 | "simply go downstairs in that house" |
| 11 | `MAP_TRAINER_HOUSE_B1F` | `maps/TrainerHouseB1F.asm` | warp 1 at (9, 4) | same warp back up | coord_event at (7, 3) runs `TrainerHouseReceptionistScript`, the once-a-day CAL battle |
| 12 | `MAP_ROUTE_1` | `maps/Route1.asm` | north connection from `VIRIDIAN_CITY` (`data/maps/attributes.asm:267`) | south connection to `PALLET_TOWN` | Bitter Berry fruit tree, Schoolboy Danny, Cooltrainerf Quinn |
| 13 | `MAP_PALLET_TOWN` | `maps/PalletTown.asm` | north connection from `ROUTE_1` | warp 3 at (12, 11) -> `OAKS_LAB` warp 1 | Oak's badge conversation; sets `ENGINE_FLYPOINT_PALLET` |
| 14 | `MAP_OAKS_LAB` | `maps/OaksLab.asm` | warp 1 at (4, 11) / warp 2 at (5, 11) | either warp -> `PALLET_TOWN` warp 3 | "Talk with Professor Oak in his lab" |
| 15 | hand-off | - | - | south connection from `PALLET_TOWN` to `ROUTE_21` (`data/maps/attributes.asm:273`) | "Now, surf south onto Route 21" - Route 21 belongs to the next section; stopping here. |
`ROUTE_2` and `VIRIDIAN_FOREST` are named in the walkthrough only as the walking route from Pewter back to Viridian; they are not covered here.
---
## 2. Maps
### MAP_ROUTE_3
- Script: `maps/Route3.asm` (`Route3_MapEvents` = `50:514b`)
- Blocks: `maps/Route3.blk`
- Header: `data/maps/maps.asm:321` -> `map Route3, TILESET_KANTO, ROUTE, LANDMARK_ROUTE_3, MUSIC_ROUTE_3, FALSE, PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:296` -> `map_const ROUTE_3, 30, 9` (30x9 blocks = 60x18 walk cells), group `PEWTER`
- Connections: `data/maps/attributes.asm:377` -> west `PewterCity` (offset -5), east `Route4` (offset 0). No north/south.
- Scene scripts: none (`def_scene_scripts` is empty). Callbacks: none.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 52 | 1 | `MOUNT_MOON` | 1 |
**Coord events** (`def_coord_events`)
None.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 49 | 13 | `BGEVENT_READ` | `Route3MtMoonSquareSign` -> `Route3MtMoonSquareSignText` ("MT.MOON SQUARE / Just go up the stairs.") |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE3_FISHER1` | `SPRITE_FISHER` | 26 | 12 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_TRAINER`, sight 2 | `TrainerFirebreatherOtis` (`50:4f26`) | -1 |
| `ROUTE3_YOUNGSTER1` | `SPRITE_YOUNGSTER` | 11 | 2 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_TRAINER`, sight 2 | `TrainerYoungsterWarren` (`50:4f3a`) | -1 |
| `ROUTE3_YOUNGSTER2` | `SPRITE_YOUNGSTER` | 20 | 4 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerYoungsterJimmy` (`50:4f4e`) | -1 |
| `ROUTE3_FISHER2` | `SPRITE_FISHER` | 49 | 5 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerFirebreatherBurt` (`50:4f62`) | -1 |
Column order comes from `macros/scripts/maps.asm:113` (`object_event x, y, sprite, movement, radius_x, radius_y, hour1, hour2, palette, type, sight_range, script, event_flag`). All four here have radius 0/0 and hour limits `-1, -1` (always present).
**Scripts of interest**
- `TrainerFirebreatherOtis` / `TrainerYoungsterWarren` / `TrainerYoungsterJimmy` / `TrainerFirebreatherBurt`: each is a plain `trainer CLASS, ID, EVENT_BEAT_*, SeenText, BeatenText, 0, .Script` header. `.Script` is `endifjustbattled / opentext / writetext <After>Text / waitbutton / closetext / end`. No items, no flags beyond the trainer's own beaten flag, no warps.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_FIREBREATHER_OTIS` | `constants/event_flags.asm:576` | `trainer` header | set on win; trainer will not re-challenge |
| `EVENT_BEAT_FIREBREATHER_BURT` | `constants/event_flags.asm:579` | `trainer` header | same |
| `EVENT_BEAT_YOUNGSTER_WARREN` | `constants/event_flags.asm:971` | `trainer` header | same |
| `EVENT_BEAT_YOUNGSTER_JIMMY` | `constants/event_flags.asm:972` | `trainer` header | same |
**Items**
None on this map.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm`) | script label | rematch/phone |
|---|---|---|---|---|---|
| `WARREN` | `YOUNGSTER` | YOUNGSTER (9), line 386 | L35 FEAROW (`TRAINERTYPE_NORMAL`) | `TrainerYoungsterWarren` | none |
| `JIMMY` | `YOUNGSTER` | YOUNGSTER (10), line 391 | L33 RATICATE, L33 ARBOK | `TrainerYoungsterJimmy` | none |
| `OTIS` | `FIREBREATHER` | FIREBREATHER (1), line 2334 | L29 MAGMAR, L32 WEEZING, L29 MAGMAR | `TrainerFirebreatherOtis` | none |
| `BURT` | `FIREBREATHER` | FIREBREATHER (4), line 2353 | L32 KOFFING, L32 SLUGMA | `TrainerFirebreatherBurt` | none |
All four are `TRAINERTYPE_NORMAL`, i.e. level+species only, moves come from the learnset.
**Wild encounters**
`data/wild/kanto_grass.asm:314` `def_grass_wildmons ROUTE_3`, rates `10 percent, 10 percent, 10 percent` (morn/day/nite). The table is version-split:
- `IF DEF(_GOLD)` - morn/day: L5 SPEAROW, L5 RATTATA, L8 SPEAROW, L6 JIGGLYPUFF, L10 RATTATA, L8 SPEAROW, L8 SPEAROW; nite: L5 RATTATA, L5 ZUBAT, L8 RATTATA, L6 JIGGLYPUFF, L10 RATTATA, L8 RATTATA, L8 RATTATA.
- `ELIF DEF(_SILVER)` - morn/day: L5 SPEAROW, L5 RATTATA, L8 EKANS, L6 JIGGLYPUFF, L10 ARBOK, L8 SPEAROW, L8 SPEAROW; nite: L5 RATTATA, L5 ZUBAT, L8 EKANS, L6 JIGGLYPUFF, L10 ARBOK, L8 RATTATA, L8 RATTATA.
The walkthrough's "Rattata / Ekans / Arbok" is the **Silver** column. On Gold there is no Ekans or Arbok on Route 3. See section 6.
No water/fishing/headbutt entry for `ROUTE_3` in `data/wild/kanto_water.asm` or `data/wild/treemons.asm`.
---
### MAP_MOUNT_MOON
- Script: `maps/MountMoon.asm` (`MountMoon_MapEvents` = `42:587e`, `MountMoonRivalBattleScript` = `42:55b0`)
- Blocks: `maps/MountMoon.blk`
- Header: `data/maps/maps.asm:154` -> `map MountMoon, TILESET_CAVE, CAVE, LANDMARK_MT_MOON, MUSIC_MT_MOON, TRUE, PALETTE_NITE, FISHGROUP_SHORE` (the `TRUE` column is the phone/"requires flash-style" fixed palette flag position in the `map` macro; palette is `PALETTE_NITE`, so the cave is dark-toned regardless of clock)
- Dimensions: `constants/map_constants.asm:141` -> `map_const MOUNT_MOON, 15, 9` (15x9 blocks = 30x18 walk cells), group `CERULEAN`-adjacent index 76 in its group listing
- Attributes: `data/maps/attributes.asm:488` -> `map_attributes MountMoon, MOUNT_MOON, $09`. **No connections** - the map is entered and left only through warps.
- Scene var: `data/maps/scenes.asm:46` -> `wMountMoonSceneID`
**Scene scripts** (`def_scene_scripts`, ids allocated from 0 by `macros/scripts/maps.asm:12`)
| scene id | constant | script |
|---|---|---|
| 0 | `SCENE_MOUNTMOON_RIVAL_BATTLE` | `MountMoonRivalEncounterScene` -> `sdefer MountMoonRivalBattleScript` |
| 1 | `SCENE_MOUNTMOON_NOOP` | `MountMoonNoopScene` -> `end` |
Nothing in the game sets scene 0; it is the power-on default, so the very first entry to Mt. Moon fires the rival cutscene. `RunSceneScript` is polled in `engine/overworld/events.asm:256`, after `CheckTrainerEvent` and `CheckTileEvent`, so it lands on the first player-event poll after the warp completes.
**Callbacks**: none.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 3 | `ROUTE_3` | 1 |
| 2 | 15 | 15 | `ROUTE_4` | 1 |
| 3 | 13 | 3 | `MOUNT_MOON` | 7 |
| 4 | 15 | 11 | `MOUNT_MOON` | 8 |
| 5 | 25 | 5 | `MOUNT_MOON_SQUARE` | 1 |
| 6 | 25 | 15 | `MOUNT_MOON_SQUARE` | 2 |
| 7 | 25 | 3 | `MOUNT_MOON` | 3 |
| 8 | 25 | 13 | `MOUNT_MOON` | 4 |
Warps 3<->7 and 4<->8 are the two internal ladder pairs. Read as a graph:
`ROUTE_3 -(1)- (3,3) ... (13,3) -(3/7)- (25,3) ... (25,5) -(5)- SQUARE`
and `SQUARE -(2/6)- (25,15) ... (25,13) -(8/4)- (15,11) ... (15,15) -(2)- ROUTE_4`.
**Coord events**: none. **BG events**: none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `MOUNTMOON_RIVAL` | `SPRITE_RIVAL` | 7 | 3 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_SCRIPT`, sight 0 | `ObjectEvent` (the shared ROM0 stub) | `EVENT_MT_MOON_RIVAL` |
Flag semantics (`engine/overworld/scripting.asm:879-898`): `appear` clears the object's event flag, `disappear` sets it. `EVENT_MT_MOON_RIVAL` therefore starts **clear** = rival visible, and `disappear MOUNTMOON_RIVAL` at the end of the cutscene sets it = rival gone forever.
**Scripts of interest**
`MountMoonRivalBattleScript` (`42:55b0`), reached through `sdefer` from scene 0:
1. `turnobject PLAYER, RIGHT`, `showemote EMOTE_SHOCK, PLAYER, 15`, `special FadeOutMusic`, `pause 15`.
2. `applymovement MOUNTMOON_RIVAL, MountMoonRivalMovementBefore` = three `step LEFT` (rival walks from (7,3) toward the player at the (3,3) entrance).
3. `playmusic MUSIC_RIVAL_ENCOUNTER`, `opentext`, `writetext MountMoonRivalTextBefore`, `waitbutton`, `closetext`.
4. Starter branch:
- `checkevent EVENT_GOT_TOTODILE_FROM_ELM` -> `.Totodile` -> `loadtrainer RIVAL2, RIVAL2_1_CHIKORITA`
- `checkevent EVENT_GOT_CHIKORITA_FROM_ELM` -> `.Chikorita` -> `loadtrainer RIVAL2, RIVAL2_1_CYNDAQUIL`
- fallthrough (player took Cyndaquil) -> `loadtrainer RIVAL2, RIVAL2_1_TOTODILE`
Note the label names describe the *player's* starter; the constant names describe the *rival's*. Each arm does `winlosstext MountMoonRivalTextWin, MountMoonRivalTextLoss`, `setlasttalked MOUNTMOON_RIVAL`, `startbattle`, `dontrestartmapmusic`, `reloadmapafterbattle`.
5. `.FinishBattle`: `playmusic MUSIC_RIVAL_AFTER`, after-text, `turnobject PLAYER, UP`, `turnobject PLAYER, RIGHT`, `applymovement MOUNTMOON_RIVAL, MountMoonRivalMovementAfter` (`RIGHT, RIGHT, DOWN x5`), `disappear MOUNTMOON_RIVAL`, `setscene SCENE_MOUNTMOON_NOOP`, `setevent EVENT_BEAT_RIVAL_IN_MT_MOON`, `playmapmusic`, `end`.
The battle is unavoidable: there is no `iftrue` skip and no branch that reaches the rest of the map without running it, because the scene id is 0 until `setscene` runs at the very end of the script.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_MT_MOON_RIVAL` | `constants/event_flags.asm:1308` | object row; `disappear` in `MountMoonRivalBattleScript` | clear = rival object on the map |
| `EVENT_BEAT_RIVAL_IN_MT_MOON` | `constants/event_flags.asm:456` | set here; read by `maps/DragonsDenB1F.asm:13` and `maps/IndigoPlateauPokecenter1F.asm:45,67` | unlocks both post-game rival re-encounters |
| `EVENT_GOT_TOTODILE_FROM_ELM` | `constants/event_flags.asm:37` | read here | selects rival party |
| `EVENT_GOT_CHIKORITA_FROM_ELM` | `constants/event_flags.asm:38` | read here | selects rival party |
| `SCENE_MOUNTMOON_RIVAL_BATTLE` = 0 | `maps/MountMoon.asm:6` (allocated by `def_scene_scripts`) | `wMountMoonSceneID` | 0 = cutscene armed |
| `SCENE_MOUNTMOON_NOOP` = 1 | `maps/MountMoon.asm:7` | `setscene` at end of script | 1 = cutscene spent |
**Items**: none on this map.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm`) | script label | rematch/phone |
|---|---|---|---|---|---|
| `RIVAL2_1_CHIKORITA` | `RIVAL2` (`constants/trainer_constants.asm:424`) | RIVAL2 (1), line 2042 | L41 SNEASEL (Quick Attack, Screech, Faint Attack, Fury Cutter); L42 GOLBAT (Leech Life, Bite, Confuse Ray, Wing Attack); L41 MAGNETON (ThunderShock, SonicBoom, Thunder Wave, Swift); L43 GENGAR (Mean Look, Curse, Shadow Ball, Confuse Ray); L43 ALAKAZAM (Disable, Recover, Future Sight, Psychic); **L45 MEGANIUM** (Razor Leaf, PoisonPowder, Body Slam, Light Screen) | `MountMoonRivalBattleScript` `.Totodile` | rematch at Indigo Plateau / Dragon's Den, see gates |
| `RIVAL2_1_CYNDAQUIL` | `RIVAL2` | RIVAL2 (2), line 2052 | same first five, **L45 TYPHLOSION** (SmokeScreen, Quick Attack, Flame Wheel, Swift) | `.Chikorita` | same |
| `RIVAL2_1_TOTODILE` | `RIVAL2` | RIVAL2 (3), line 2062 | same first five, **L45 FERALIGATR** (Rage, Water Gun, Scary Face, Slash) | fallthrough | same |
`TRAINERTYPE_MOVES`, so the four move slots above are exactly what the rival fields. `RIVAL2_2_*` (rows 4-6, levels 45-50 with Crobat) are the Indigo Plateau rematch parties, not this fight.
**Wild encounters**
`data/wild/kanto_grass.asm:33` `def_grass_wildmons MOUNT_MOON`, rates `6 percent, 6 percent, 6 percent`.
- `IF DEF(_GOLD)` - identical morn/day/nite: L6 ZUBAT, L8 GEODUDE, L8 SANDSHREW, L12 PARAS, L10 SANDSLASH, L8 CLEFAIRY, L8 CLEFAIRY.
- `ELIF DEF(_SILVER)` - L6 ZUBAT, L8 GEODUDE, L8 ZUBAT, L12 PARAS, L10 GEODUDE, L8 CLEFAIRY, L8 CLEFAIRY.
The walkthrough's "Zubat / Parus [sic, Paras] / Geodude" is a subset of both columns. Gold additionally has Sandshrew and Sandslash, and both versions have a 2-slot Clefairy line the walkthrough does not mention.
---
### MAP_MOUNT_MOON_SQUARE
- Script: `maps/MountMoonSquare.asm` (`MountMoonSquare_MapEvents` = `5b:6827`, `ClefairyDance` = `5b:676a`)
- Blocks: `maps/MountMoonSquare.blk`
- Header: `data/maps/maps.asm:342` -> `map MountMoonSquare, TILESET_KANTO, ROUTE, LANDMARK_MT_MOON, MUSIC_MT_MOON_SQUARE, FALSE, PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:316` -> `map_const MOUNT_MOON_SQUARE, 15, 9` (30x18 walk cells), group `FAST_SHIP`
- Attributes: `data/maps/attributes.asm:595` -> `$2d`, no connections
- Scene var: `data/maps/scenes.asm:66` -> `wMountMoonSquareSceneID`
**Scene scripts**
| scene id | constant | script |
|---|---|---|
| 0 | `SCENE_MOUNTMOONSQUARE_CLEFAIRY_DANCE` | `MountMoonSquareNoopScene` -> `end` (the constant only exists to name the coord_event's scene, the scene script itself is a no-op and the id is never changed) |
**Callbacks**
- `MAPCALLBACK_NEWMAP` -> `MountMoonSquareDisappearMoonStoneCallback`: `setevent EVENT_MOUNT_MOON_SQUARE_HIDDEN_MOON_STONE` / `endcallback`. Every fresh entry re-hides the Moon Stone.
- `MAPCALLBACK_OBJECTS` -> `MountMoonSquareDisappearRockCallback`: `disappear MOUNTMOONSQUARE_ROCK` / `endcallback`. The smashable rock is hidden on every object refresh; it exists on-screen only during the dance cutscene, which `appear`s it mid-script.
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 20 | 5 | `MOUNT_MOON` | 5 |
| 2 | 22 | 11 | `MOUNT_MOON` | 6 |
| 3 | 13 | 7 | `MOUNT_MOON_GIFT_SHOP` | 1 |
**Coord events**
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_MOUNTMOONSQUARE_CLEFAIRY_DANCE` (0) | 7 | 11 | `ClefairyDance` (`5b:676a`) | the Monday-night Clefairy cutscene; always armed because the scene id is never advanced |
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 7 | 7 | `BGEVENT_ITEM` | `MountMoonSquareHiddenMoonStone` (`5b:67e8`) = `hiddenitem MOON_STONE, EVENT_MOUNT_MOON_SQUARE_HIDDEN_MOON_STONE` |
| 17 | 7 | `BGEVENT_READ` | `DontLitterSign` -> `DontLitterSignText` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `MOUNTMOONSQUARE_FAIRY1` | `SPRITE_FAIRY` | 6 | 6 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT`, sight 0 | `ObjectEvent` | `EVENT_MT_MOON_SQUARE_CLEFAIRY` |
| `MOUNTMOONSQUARE_FAIRY2` | `SPRITE_FAIRY` | 7 | 6 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT`, sight 0 | `ObjectEvent` | `EVENT_MT_MOON_SQUARE_CLEFAIRY` |
| `MOUNTMOONSQUARE_ROCK` | `SPRITE_ROCK` | 7 | 7 | `SPRITEMOVEDATA_SMASHABLE_ROCK` | `OBJECTTYPE_SCRIPT`, sight 0 | `MtMoonSquareRock` (`5b:67ee`) = `jumpstd SmashRockScript` | `EVENT_MT_MOON_SQUARE_ROCK` |
**Scripts of interest**
`ClefairyDance` - the whole cutscene is guarded by three conditions in a row, all of which must pass:
```
checkflag ENGINE_MT_MOON_SQUARE_CLEFAIRY ; iftrue .NoDancing (once per save)
readvar VAR_WEEKDAY / ifnotequal MONDAY, .NoDancing
checktime NITE / iffalse .NoDancing
```
Then: `appear` both fairies, `applymovement PLAYER, PlayerWalksUpToDancingClefairies` (one `step UP`, so the player ends on (7, 10)), `pause 15`, `appear MOUNTMOONSQUARE_ROCK`, a long `follow`/`applymovement`/`cry CLEFAIRY` dance across `ClefairyDanceStep1..7`, `showemote EMOTE_SHOCK` when the Clefairy notice the player, `applymovement ... ClefairyFleeMovement`, `disappear` both fairies, `stopfollow`, then the payoff:
```
clearevent EVENT_MOUNT_MOON_SQUARE_HIDDEN_MOON_STONE
setflag ENGINE_MT_MOON_SQUARE_CLEFAIRY
```
So the Moon Stone at bg_event (7, 7) becomes takeable, but the rock object was `appear`ed onto that exact cell during the cutscene and is only cleared by `MAPCALLBACK_OBJECTS` on the next map load. That is the asm behind Thard_Verad's "Bring Rock Smash": you either smash the rock at (7, 7) in the same visit, or leave and come back (the `MAPCALLBACK_NEWMAP` callback then re-sets the Moon Stone flag and re-hides the item, so leaving loses it). See section 6.
`MtMoonSquareRock` -> `jumpstd SmashRockScript` (`engine/events/std_scripts.asm:199`) -> `farsjump AskRockSmashScript` (`engine/events/overworld.asm:1365`), which is `callasm HasRockSmash` (`engine/events/overworld.asm:1386`: `ld d, ROCK_SMASH / call CheckPartyMove`) then a yes/no prompt. **No badge check** - Rock Smash in Gen 2 is TM08, not an HM.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_MOUNT_MOON_SQUARE_HIDDEN_MOON_STONE` | `constants/event_flags.asm:236` | set by `MAPCALLBACK_NEWMAP`; cleared at the end of `ClefairyDance`; consumed by `hiddenitem` | set = Moon Stone not present |
| `EVENT_MT_MOON_SQUARE_CLEFAIRY` | `constants/event_flags.asm:1307` | object rows; `appear`/`disappear` in `ClefairyDance` | clear = the two Clefairy are on the map (only during the cutscene) |
| `EVENT_MT_MOON_SQUARE_ROCK` | `constants/event_flags.asm:1306` | object row; `disappear` in the OBJECTS callback, `appear` mid-cutscene | clear = rock is standing on (7, 7) |
| `ENGINE_MT_MOON_SQUARE_CLEFAIRY` | `constants/engine_flags.asm:106` | `checkflag`/`setflag` in `ClefairyDance` | set = the dance has been seen once and will never replay |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `MOON_STONE` | hidden item on (7, 7), only after the Clefairy dance clears its flag | `bg_event 7, 7, BGEVENT_ITEM, MountMoonSquareHiddenMoonStone` | `EVENT_MOUNT_MOON_SQUARE_HIDDEN_MOON_STONE` |
**Trainers**: none. **Wild encounters**: no `MOUNT_MOON_SQUARE` entry in `data/wild/kanto_grass.asm` or `kanto_water.asm`.
---
### MAP_MOUNT_MOON_GIFT_SHOP
- Script: `maps/MountMoonGiftShop.asm` (`MountMoonGiftShop_MapEvents` = `5b:68b6`, `MountMoonGiftShopClerkScript` = `5b:6877`)
- Blocks: `data/maps/blocks.asm:701` -> `INCBIN "maps/GiftShop.blk"` (shared blocks file, no `MountMoonGiftShop.blk`)
- Header: `data/maps/maps.asm:343` -> `map MountMoonGiftShop, TILESET_TRADITIONAL_HOUSE, INDOOR, LANDMARK_MT_MOON, MUSIC_VIRIDIAN_CITY, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:317` -> `map_const MOUNT_MOON_GIFT_SHOP, 4, 4` (8x8 walk cells)
- Attributes: `data/maps/attributes.asm:596` -> `$00`, no connections
- No scene scripts, no callbacks.
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `MOUNT_MOON_SQUARE` | 3 |
| 2 | 4 | 7 | `MOUNT_MOON_SQUARE` | 3 |
**Coord events**: none. **BG events**: none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `MOUNTMOONGIFTSHOP_GRAMPS1` | `SPRITE_GRAMPS` | 4 | 3 | `SPRITEMOVEDATA_STANDING_DOWN`, hours `-1, MORN` | `OBJECTTYPE_SCRIPT` | `MountMoonGiftShopClerkScript` | -1 |
| `MOUNTMOONGIFTSHOP_GRAMPS2` | `SPRITE_GRAMPS` | 1 | 2 | `SPRITEMOVEDATA_STANDING_RIGHT`, hours `-1, DAY` | `OBJECTTYPE_SCRIPT` | `MountMoonGiftShopClerkScript` | -1 |
| `MOUNTMOONGIFTSHOP_LASS1` | `SPRITE_LASS` | 1 | 6 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (radius 1,0), hours `-1, MORN` | `OBJECTTYPE_SCRIPT` | `MountMoonGiftShopLassScript` | -1 |
| `MOUNTMOONGIFTSHOP_LASS2` | `SPRITE_LASS` | 5 | 4 | `SPRITEMOVEDATA_WALK_UP_DOWN` (radius 0,1), hours `-1, DAY` | `OBJECTTYPE_SCRIPT` | `MountMoonGiftShopLassScript` | -1 |
The `hour1 = -1` form means `hour2` is a time-of-day mask (`macros/scripts/maps.asm:123`). **There is no NITE clerk**: at night the shop is unattended, which the walkthrough does not mention.
**Scripts of interest**
`MountMoonGiftShopClerkScript`: `faceplayer / opentext / pokemart MARTTYPE_STANDARD, MART_MT_MOON / closetext / end`.
**Items** (`MartMtMoon`, `data/items/marts.asm:374`, 6 items)
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `POKE_DOLL` | bought | `MartMtMoon` | - |
| `FRESH_WATER` | bought | `MartMtMoon` | - |
| `SODA_POP` | bought | `MartMtMoon` | - |
| `LEMONADE` | bought | `MartMtMoon` | - |
| `REPEL` | bought | `MartMtMoon` | - |
| `PORTRAITMAIL` | bought | `MartMtMoon` | - |
The walkthrough names only Fresh Water / Soda Pop / Lemonade; the other three are real and in the same list.
**Trainers**: none. **Wild encounters**: none.
---
### MAP_ROUTE_4
- Script: `maps/Route4.asm` (`Route4_MapEvents` = `50:5338`)
- Blocks: `maps/Route4.blk`
- Header: `data/maps/maps.asm:216` -> `map Route4, TILESET_KANTO, ROUTE, LANDMARK_ROUTE_4, MUSIC_ROUTE_3, FALSE, PALETTE_AUTO, FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:198` -> `map_const ROUTE_4, 20, 9` (40x18 walk cells), group `CERULEAN`
- Connections: `data/maps/attributes.asm:381` -> west `Route3` (offset 0), east `CeruleanCity` (offset -5)
- No scene scripts, no callbacks.
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 2 | 5 | `MOUNT_MOON` | 2 |
**Coord events**: none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 3 | 7 | `BGEVENT_READ` | `MtMoonSquareSign` -> `MtMoonSquareSignText` |
| 10 | 3 | `BGEVENT_ITEM` | `Route4HiddenUltraBall` (`50:51d2`) = `hiddenitem ULTRA_BALL, EVENT_ROUTE_4_HIDDEN_ULTRA_BALL` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE4_YOUNGSTER` | `SPRITE_YOUNGSTER` | 17 | 9 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerBirdKeeperHank` (`50:5191`) | -1 |
| `ROUTE4_LASS1` | `SPRITE_LASS` | 10 | 8 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_TRAINER`, sight 5 | `TrainerPicnickerHope` (`50:51a5`) | -1 |
| `ROUTE4_LASS2` | `SPRITE_LASS` | 21 | 6 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_TRAINER`, sight 4 | `TrainerPicnickerSharon` (`50:51b9`) | -1 |
| `ROUTE4_POKE_BALL` | `SPRITE_POKE_BALL` | 26 | 3 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `Route4HPUp` (`50:51d0`) = `itemball HP_UP` | `EVENT_ROUTE_4_HP_UP` |
**Scripts of interest**
All three trainer scripts are the standard `endifjustbattled / opentext / writetext <After>Text / waitbutton / closetext / end`. `Route4HPUp` is not bytecode at all - `itemball HP_UP` is two raw bytes (`macros/scripts/maps.asm`, `dwb`-style), read by the engine's item-ball path rather than the script VM.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_BIRD_KEEPER_HANK` | `constants/event_flags.asm:501` | `trainer` header | set on win |
| `EVENT_BEAT_PICNICKER_HOPE` | `constants/event_flags.asm:641` | `trainer` header | set on win |
| `EVENT_BEAT_PICNICKER_SHARON` | `constants/event_flags.asm:642` | `trainer` header | set on win |
| `EVENT_ROUTE_4_HP_UP` | `constants/event_flags.asm:1322` | item-ball object row | set = ball already taken (object hidden) |
| `EVENT_ROUTE_4_HIDDEN_ULTRA_BALL` | `constants/event_flags.asm:241` | `hiddenitem` | set = Ultra Ball already found |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `HP_UP` | walk onto / face and A the Poke Ball object at (26, 3) | `object_event ... OBJECTTYPE_ITEMBALL, 0, Route4HPUp` | `EVENT_ROUTE_4_HP_UP` |
| `ULTRA_BALL` | hidden, face (10, 3) and press A, or Itemfinder | `bg_event 10, 3, BGEVENT_ITEM, Route4HiddenUltraBall` | `EVENT_ROUTE_4_HIDDEN_ULTRA_BALL` |
The walkthrough lists only the HP Up.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm`) | script label | rematch/phone |
|---|---|---|---|---|---|
| `HOPE` | `PICNICKER` | PICNICKER (6), line 2611 | L34 FLAAFFY | `TrainerPicnickerHope` | none |
| `HANK` | `BIRD_KEEPER` | BIRD_KEEPER (8), line 569 | L12 PIDGEY, L34 PIDGEOT | `TrainerBirdKeeperHank` | none |
| `SHARON` | `PICNICKER` | PICNICKER (7), line 2616 | L31 FURRET, L33 RAPIDASH | `TrainerPicnickerSharon` | none |
All `TRAINERTYPE_NORMAL`. The L12 Pidgey in Hank's party is not a typo in the walkthrough; it really is level 12.
**Wild encounters**
- Grass: `data/wild/kanto_grass.asm:369` `def_grass_wildmons ROUTE_4`, rates `10/10/10 percent`.
- `IF DEF(_GOLD)` - morn/day: L5 SPEAROW, L5 RATTATA, L8 SPEAROW, L6 JIGGLYPUFF, L10 RATTATA, L8 SPEAROW, L8 SPEAROW; nite: L5 SPEAROW, L5 ZUBAT, L8 RATTATA, L6 JIGGLYPUFF, L10 RATTATA, L8 RATTATA, L8 RATTATA.
- `ELIF DEF(_SILVER)` - morn/day: L5 SPEAROW, L5 RATTATA, L8 EKANS, L6 JIGGLYPUFF, L10 ARBOK, L8 SPEAROW, L8 SPEAROW; nite: L5 RATTATA, L5 ZUBAT, L8 EKANS, L6 JIGGLYPUFF, L10 ARBOK, L8 RATTATA, L8 RATTATA.
The walkthrough's "Rattata / Spearow / Arbok" is the Silver column again.
- Water: `data/wild/kanto_water.asm:5` `def_water_wildmons ROUTE_4`, 4 percent: L10 GOLDEEN, L5 GOLDEEN, L10 SEAKING.
- Fishing group `FISHGROUP_LAKE` from the map header.
---
### MAP_VIRIDIAN_CITY
- Script: `maps/ViridianCity.asm` (`ViridianCity_MapEvents` = `4e:4486`)
- Blocks: `maps/ViridianCity.blk`
- Header: `data/maps/maps.asm:457` -> `map ViridianCity, TILESET_KANTO, TOWN, LANDMARK_VIRIDIAN_CITY, MUSIC_VIRIDIAN_CITY, FALSE, PALETTE_AUTO, FISHGROUP_POND`
- Dimensions: `constants/map_constants.asm:423` -> `map_const VIRIDIAN_CITY, 20, 18` (40x36 walk cells), group `VIRIDIAN`
- Connections: `data/maps/attributes.asm:259` -> north `Route2` (offset 5), south `Route1` (offset 10), west `Route22` (offset 4)
- Spawn: `data/maps/spawn_points.asm:15` -> `spawn VIRIDIAN_CITY, 23, 26`; flypoint `data/maps/flypoints.asm:19` -> `db LANDMARK_VIRIDIAN_CITY, SPAWN_VIRIDIAN`
**Scene scripts**: none. **Callbacks**: `MAPCALLBACK_NEWMAP` -> `ViridianCityFlypointCallback` = `setflag ENGINE_FLYPOINT_VIRIDIAN / endcallback`.
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 32 | 7 | `VIRIDIAN_GYM` | 1 |
| 2 | 21 | 9 | `VIRIDIAN_NICKNAME_SPEECH_HOUSE` | 1 |
| 3 | 23 | 15 | `TRAINER_HOUSE_1F` | 1 |
| 4 | 29 | 19 | `VIRIDIAN_MART` | 2 |
| 5 | 23 | 25 | `VIRIDIAN_POKECENTER_1F` | 1 |
The Trainer House (23, 15) is exactly 10 cells due north of the Pokemon Center (23, 25), which is the walkthrough's "north from the Pokemon Center".
**Coord events**: none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 17 | 17 | `BGEVENT_READ` | `ViridianCitySign` |
| 27 | 7 | `BGEVENT_READ` | `ViridianGymSign` |
| 19 | 1 | `BGEVENT_READ` | `ViridianCityWelcomeSign` |
| 21 | 15 | `BGEVENT_READ` | `TrainerHouseSign` |
| 24 | 25 | `BGEVENT_READ` | `ViridianCityPokecenterSign` (`jumpstd PokecenterSignScript`) |
| 30 | 19 | `BGEVENT_READ` | `ViridianCityMartSign` (`jumpstd MartSignScript`) |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `VIRIDIANCITY_GRAMPS1` | `SPRITE_GRAMPS` | 18 | 5 | `SPRITEMOVEDATA_WANDER` (radius 2,2) | `OBJECTTYPE_SCRIPT` | `ViridianCityCoffeeGramps` | -1 |
| `VIRIDIANCITY_GRAMPS2` | `SPRITE_GRAMPS` | 30 | 8 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `ViridianCityGrampsNearGym` | -1 |
| `VIRIDIANCITY_FISHER` | `SPRITE_FISHER` | 6 | 23 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `ViridianCityDreamEaterFisher` (`4e:4032`) | -1 |
| `VIRIDIANCITY_YOUNGSTER` | `SPRITE_YOUNGSTER` | 17 | 21 | `SPRITEMOVEDATA_WANDER` (radius 3,3) | `OBJECTTYPE_SCRIPT` | `ViridianCityYoungsterScript` | -1 |
**Scripts of interest**
- `ViridianCityFlypointCallback` - runs on `MAPCALLBACK_NEWMAP`, so simply *entering* Viridian City registers it as a Fly destination.
- `ViridianCityDreamEaterFisher` (`4e:4032`) - `faceplayer / opentext / checkevent EVENT_GOT_TM42_DREAM_EATER / iftrue .GotDreamEater / writetext ... / promptbutton / verbosegiveitem TM_DREAM_EATER / iffalse .NoRoomForDreamEater / setevent EVENT_GOT_TM42_DREAM_EATER`. A free TM42 the walkthrough never mentions.
- `ViridianCityGrampsNearGym` - branches on `EVENT_BLUE_IN_CINNABAR`; flavour only.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_FLYPOINT_VIRIDIAN` | `constants/engine_flags.asm:68` | set by `ViridianCityFlypointCallback` | Fly target unlocked on first visit |
| `EVENT_GOT_TM42_DREAM_EATER` | `constants/event_flags.asm:223` | `ViridianCityDreamEaterFisher` | one-time TM |
| `EVENT_BLUE_IN_CINNABAR` | `constants/event_flags.asm:1303` | read only, here | selects the gym gramps' text |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `TM_DREAM_EATER` (TM42) | talk to `VIRIDIANCITY_FISHER` at (6, 23) | `ViridianCityDreamEaterFisher`, `verbosegiveitem` | `EVENT_GOT_TM42_DREAM_EATER` |
**Trainers**: none on the overworld map.
**Wild encounters**: `data/wild/kanto_water.asm:124` `def_water_wildmons VIRIDIAN_CITY`, 2 percent (surf only). No grass entry for `VIRIDIAN_CITY`.
---
### MAP_TRAINER_HOUSE_1F
- Script: `maps/TrainerHouse1F.asm` (`TrainerHouse1F_MapEvents` = `5f:48f9`)
- Blocks: `maps/TrainerHouse1F.blk`
- Header: `data/maps/maps.asm:460` -> `map TrainerHouse1F, TILESET_HOUSE, INDOOR, LANDMARK_VIRIDIAN_CITY, MUSIC_VIRIDIAN_CITY, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:425` -> `map_const TRAINER_HOUSE_1F, 5, 7` (10x14 walk cells)
- Attributes: `data/maps/attributes.asm:659` -> `$00`, no connections
- No scene scripts, no callbacks.
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 2 | 13 | `VIRIDIAN_CITY` | 3 |
| 2 | 3 | 13 | `VIRIDIAN_CITY` | 3 |
| 3 | 8 | 2 | `TRAINER_HOUSE_B1F` | 1 |
**Coord events**: none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 5 | 0 | `BGEVENT_READ` | `TrainerHouseSign1` |
| 7 | 0 | `BGEVENT_READ` | `TrainerHouseSign2` |
| 7 | 10 | `BGEVENT_READ` | `TrainerHouseIllegibleBook` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `TRAINERHOUSE1F_RECEPTIONIST` | `SPRITE_RECEPTIONIST` | 0 | 11 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `TrainerHouse1FReceptionistScript` | -1 |
| `TRAINERHOUSE1F_COOLTRAINER_M` | `SPRITE_COOLTRAINER_M` | 7 | 11 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_SCRIPT` | `TrainerHouse1FCooltrainerMScript` | -1 |
| `TRAINERHOUSE1F_COOLTRAINER_F` | `SPRITE_COOLTRAINER_F` | 6 | 2 | `SPRITEMOVEDATA_STANDING_DOWN` (radius 2,0) | `OBJECTTYPE_SCRIPT` | `TrainerHouse1FCooltrainerFScript` | -1 |
| `TRAINERHOUSE1F_YOUNGSTER` | `SPRITE_YOUNGSTER` | 4 | 8 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (radius 2,0) | `OBJECTTYPE_SCRIPT` | `TrainerHouse1FYoungsterScript` | -1 |
| `TRAINERHOUSE1F_GENTLEMAN` | `SPRITE_GENTLEMAN` | 2 | 4 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `TrainerHouse1FGentlemanScript` | -1 |
Every 1F NPC is `jumptextfaceplayer` flavour. Nothing here gates the basement.
---
### MAP_TRAINER_HOUSE_B1F
- Script: `maps/TrainerHouseB1F.asm` (`TrainerHouseB1F_MapEvents` = `5f:4b97`, `TrainerHouseReceptionistScript` = `5f:4965`)
- Blocks: `maps/TrainerHouseB1F.blk`
- Header: `data/maps/maps.asm:461` -> `map TrainerHouseB1F, TILESET_FACILITY, INDOOR, LANDMARK_VIRIDIAN_CITY, MUSIC_VIRIDIAN_CITY, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:426` -> `map_const TRAINER_HOUSE_B1F, 5, 8` (10x16 walk cells)
- Attributes: `data/maps/attributes.asm:660` -> `$00`, no connections
- Scene var: `data/maps/scenes.asm:15` -> `wTrainerHouseB1FSceneID`
**Scene scripts**
| scene id | constant | script |
|---|---|---|
| 0 | `SCENE_TRAINERHOUSEB1F_ASK_BATTLE` | `TrainerHouseB1FNoopScene` -> `end` (the id only exists to arm the coord_event; it is never changed) |
**Callbacks**: none.
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 4 | `TRAINER_HOUSE_1F` | 3 |
**Coord events**
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_TRAINERHOUSEB1F_ASK_BATTLE` (0) | 7 | 3 | `TrainerHouseReceptionistScript` (`5f:4965`) | the once-a-day CAL battle |
The bot walks in at (9, 4) and must reach (7, 3) - two cells left and one up. That is the walkthrough's "head left after you go downstairs and the lady will talk to you".
**BG events**: none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `TRAINERHOUSEB1F_RECEPTIONIST` | `SPRITE_RECEPTIONIST` | 7 | 1 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `ObjectEvent` (stub - she is only reachable through the coord_event) | -1 |
| `TRAINERHOUSEB1F_CHRIS` | `SPRITE_CHRIS` | 6 | 11 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `ObjectEvent` | -1 |
`TRAINERHOUSEB1F_CHRIS` is the opponent sprite in the battle room; `setlasttalked TRAINERHOUSEB1F_CHRIS` points the battle at it.
**Scripts of interest**
`TrainerHouseReceptionistScript`:
1. `turnobject PLAYER, UP`, `opentext`.
2. `checkflag ENGINE_FOUGHT_IN_TRAINER_HALL_TODAY` -> `iftrue .FoughtTooManyTimes` (refusal text, `applymovement PLAYER, Movement_TrainerHouseTurnBack`).
3. `special TrainerHouse` (`engine/events/specials.asm:454`: reads `sMysteryGiftTrainerHouseFlag` into `wScriptVar`). True -> name from `gettrainername STRING_BUFFER_3, CAL, CAL2`; false -> `CAL, CAL3`.
4. `yesorno`. No -> `.Declined`. Yes -> `setflag ENGINE_FOUGHT_IN_TRAINER_HALL_TODAY`, text, `applymovement PLAYER, Movement_EnterTrainerHouseBattleRoom` (`LEFT x3, DOWN x8, LEFT, turn_head RIGHT`).
5. `special TrainerHouse` again. True -> `loadtrainer CAL, CAL2`; false -> `.NoSpecialBattle` -> `loadtrainer CAL, CAL3`. Both arms `winlosstext TrainerHouseB1FCalBeatenText, 0`, `setlasttalked TRAINERHOUSEB1F_CHRIS`, `startbattle`, `reloadmapafterbattle`.
6. `.End`: `applymovement PLAYER, Movement_ExitTrainerHouseBattleRoom`.
`CAL, CAL2` is the only trainer in the game whose party is **not** read from `data/trainers/parties.asm`: `engine/battle/read_trainer_party.asm:17-22` branches to `.cal2` and reads `sMysteryGiftTrainer` out of SRAM, and `GetTrainerName` (same file, line 326) reads `sMysteryGiftPartnerName`. With no Mystery Gift ever performed the flag is 0 and the game always fights `CAL3`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_FOUGHT_IN_TRAINER_HALL_TODAY` | `constants/engine_flags.asm:104` | `checkflag`/`setflag` here | set = already battled today; cleared by the daily reset |
| `SCENE_TRAINERHOUSEB1F_ASK_BATTLE` = 0 | `maps/TrainerHouseB1F.asm:7` | `wTrainerHouseB1FSceneID` | always 0, so the coord_event is always armed |
**Trainers**
| const | class | id | party (`data/trainers/parties.asm`) | script label | rematch/phone |
|---|---|---|---|---|---|
| `CAL3` | `CAL` (`constants/trainer_constants.asm:73`) | CAL (3), line 222 | L50 MEGANIUM, L50 TYPHLOSION, L50 FERALIGATR (`TRAINERTYPE_NORMAL`) | `TrainerHouseReceptionistScript` `.NoSpecialBattle` | once per day |
| `CAL2` | `CAL` | CAL (2), line 214 - **dead data** | table row is L30 BAYLEEF/QUILAVA/CROCONAW, but `ReadTrainerParty` never reaches it; the real party comes from `sMysteryGiftTrainer` | `TrainerHouseReceptionistScript` main arm | Mystery Gift only |
The walkthrough's "Level 50 Meganium / Typhlosion / Feraligatr" is `CAL3`, and its Thard_Verad note ("Cal is a placeholder; if you Mystery Gift with anyone, they can show up") is exactly the `CAL2` branch.
---
### MAP_ROUTE_1
- Script: `maps/Route1.asm` (`Route1_MapEvents` = `4e:4675`)
- Blocks: `maps/Route1.blk`
- Header: `data/maps/maps.asm:311` -> `map Route1, TILESET_KANTO, ROUTE, LANDMARK_ROUTE_1, MUSIC_ROUTE_1, FALSE, PALETTE_AUTO, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:287` -> `map_const ROUTE_1, 10, 18` (20x36 walk cells), group `PALLET`
- Connections: `data/maps/attributes.asm:267` -> north `ViridianCity` (offset -10), south `PalletTown` (offset 0)
- No scene scripts, no callbacks, **no warps** (connections only).
**Coord events**: none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 7 | 27 | `BGEVENT_READ` | `Route1Sign` -> `Route1SignText` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE1_YOUNGSTER` | `SPRITE_YOUNGSTER` | 7 | 17 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerSchoolboyDanny` (`4e:4531`) | -1 |
| `ROUTE1_COOLTRAINER_F` | `SPRITE_COOLTRAINER_F` | 3 | 26 | `SPRITEMOVEDATA_STANDING_RIGHT` (radius 1,0) | `OBJECTTYPE_TRAINER`, sight 4 | `TrainerCooltrainerfQuinn` (`4e:4545`) | -1 |
| `ROUTE1_FRUIT_TREE` | `SPRITE_FRUIT_TREE` | 3 | 7 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_SCRIPT` | `Route1FruitTree` (`4e:455c`) = `fruittree FRUITTREE_ROUTE_1` | -1 |
**Scripts of interest**
`Route1FruitTree` is one command: `fruittree FRUITTREE_ROUTE_1` (`constants/script_constants.asm:232`). The item comes from `FruitTreeItems` (`data/items/fruit_trees.asm`), whose `ROUTE_1` row is `db BITTER_BERRY`. This is the walkthrough's "Item on Route 1: Bitter Berry" - it is a **fruit tree**, not a ground item, so it is a daily respawn rather than a one-time pickup (`engine/events/fruit_trees.asm`, `TryResetFruitTrees`).
Both trainer scripts are the standard `endifjustbattled` after-battle text.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_SCHOOLBOY_DANNY` | `constants/event_flags.asm:621` | `trainer` header | set on win |
| `EVENT_BEAT_COOLTRAINERF_QUINN` | `constants/event_flags.asm:890` | `trainer` header | set on win |
The fruit tree's picked state is **not** an `EVENT_*` - it is a per-tree bit in the fruit-tree flag array with a daily reset (see `engine/events/fruit_trees.asm`).
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `BITTER_BERRY` | face the tree object at (3, 7) and press A | `Route1FruitTree` -> `FruitTreeItems` row `ROUTE_1` | daily per-tree flag, not an event flag |
**Trainers**
| const | class | id | party (`data/trainers/parties.asm`) | script label | rematch/phone |
|---|---|---|---|---|---|
| `DANNY` | `SCHOOLBOY` | SCHOOLBOY (5), line 435 | L31 JYNX, L31 ELECTABUZZ, L31 MAGMAR | `TrainerSchoolboyDanny` | none |
| `QUINN` | `COOLTRAINERF` | COOLTRAINERF (14), line 948 | L38 IVYSAUR, L38 STARMIE | `TrainerCooltrainerfQuinn` | none |
**Wild encounters**
`data/wild/kanto_grass.asm:231` `def_grass_wildmons ROUTE_1`, rates `10/10/10 percent`. **No version split.**
- morn/day: L2 PIDGEY, L2 RATTATA, L3 SENTRET, L3 PIDGEY, L6 FURRET, L4 PIDGEY, L4 PIDGEY.
- nite: L2 HOOTHOOT, L2 RATTATA, L3 RATTATA, L3 HOOTHOOT, L6 RATTATA, L4 HOOTHOOT, L4 HOOTHOOT.
The walkthrough's "Pidgey / Furret" is the morn/day column minus Rattata and Sentret; at night there is no Pidgey or Furret at all.
---
### MAP_PALLET_TOWN
- Script: `maps/PalletTown.asm` (`PalletTown_MapEvents` = `4e:47aa`)
- Blocks: `maps/PalletTown.blk`
- Header: `data/maps/maps.asm:312` -> `map PalletTown, TILESET_KANTO, TOWN, LANDMARK_PALLET_TOWN, MUSIC_PALLET_TOWN, FALSE, PALETTE_AUTO, FISHGROUP_OCEAN`
- Dimensions: `constants/map_constants.asm:288` -> `map_const PALLET_TOWN, 10, 9` (20x18 walk cells), group `PALLET`
- Connections: `data/maps/attributes.asm:271` -> north `Route1` (offset 0), south `Route21` (offset 0)
- Spawn: `data/maps/spawn_points.asm:14` -> `spawn PALLET_TOWN, 5, 6`; flypoint `data/maps/flypoints.asm:18` -> `db LANDMARK_PALLET_TOWN, SPAWN_PALLET`
**Scene scripts**: none. **Callbacks**: `MAPCALLBACK_NEWMAP` -> `PalletTownFlypointCallback` = `setflag ENGINE_FLYPOINT_PALLET / endcallback`. This is the asm behind "You can now fly to Pallet Town" - it fires the moment the map loads, before any NPC is talked to.
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 5 | 5 | `REDS_HOUSE_1F` | 1 |
| 2 | 13 | 5 | `BLUES_HOUSE` | 1 |
| 3 | 12 | 11 | `OAKS_LAB` | 1 |
**Coord events**: none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 7 | 9 | `BGEVENT_READ` | `PalletTownSign` |
| 3 | 5 | `BGEVENT_READ` | `RedsHouseSign` |
| 13 | 13 | `BGEVENT_READ` | `OaksLabSign` |
| 11 | 5 | `BGEVENT_READ` | `BluesHouseSign` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `PALLETTOWN_TEACHER` | `SPRITE_TEACHER` | 3 | 8 | `SPRITEMOVEDATA_WANDER` (radius 2,2) | `OBJECTTYPE_SCRIPT` | `PalletTownTeacherScript` | -1 |
| `PALLETTOWN_FISHER` | `SPRITE_FISHER` | 12 | 14 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (radius 2,0) | `OBJECTTYPE_SCRIPT` | `PalletTownFisherScript` | -1 |
Both are `jumptextfaceplayer` flavour.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_FLYPOINT_PALLET` | `constants/engine_flags.asm:67` | set by `PalletTownFlypointCallback` | Fly target unlocked on first visit |
**Items**: none. **Trainers**: none.
**Wild encounters**: `data/wild/kanto_water.asm:117` `def_water_wildmons PALLET_TOWN`, 6 percent: L35 TENTACOOL, L30 TENTACOOL, L35 TENTACRUEL. No grass entry. Fishing group `FISHGROUP_OCEAN`.
---
### MAP_OAKS_LAB
- Script: `maps/OaksLab.asm` (`OaksLab_MapEvents` = `59:5f33`, `Oak` = `59:58c3`)
- Blocks: `maps/OaksLab.blk`
- Header: `data/maps/maps.asm:316` -> `map OaksLab, TILESET_LAB, INDOOR, LANDMARK_PALLET_TOWN, MUSIC_POKEMON_TALK, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:291` -> `map_const OAKS_LAB, 5, 6` (10x12 walk cells)
- No connections. `OaksLabNoopScene` exists but is marked `; unreferenced`; `def_scene_scripts` is empty and there are no callbacks.
**Warps**
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 11 | `PALLET_TOWN` | 3 |
| 2 | 5 | 11 | `PALLET_TOWN` | 3 |
**Coord events**: none.
**BG events**
| x | y | type | script/item |
|---|---|---|---|
| 6,7,8,9 | 1 | `BGEVENT_READ` | `OaksLabBookshelf` (`jumpstd DifficultBookshelfScript`) |
| 0,1,2,3 | 7 | `BGEVENT_READ` | `OaksLabBookshelf` |
| 6,7,8,9 | 7 | `BGEVENT_READ` | `OaksLabBookshelf` |
| 4 | 0 | `BGEVENT_READ` | `OaksLabPoster1` |
| 5 | 0 | `BGEVENT_READ` | `OaksLabPoster2` |
| 9 | 3 | `BGEVENT_READ` | `OaksLabTrashcan` |
| 0 | 1 | `BGEVENT_READ` | `OaksLabPC` |
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `OAKSLAB_OAK` | `SPRITE_OAK` | 4 | 2 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `Oak` (`59:58c3`) | -1 |
| `OAKSLAB_SCIENTIST1` | `SPRITE_SCIENTIST` | 1 | 8 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (radius 1,0) | `OBJECTTYPE_SCRIPT` | `OaksAssistant1Script` | -1 |
| `OAKSLAB_SCIENTIST2` | `SPRITE_SCIENTIST` | 8 | 9 | `SPRITEMOVEDATA_WALK_UP_DOWN` (radius 0,1) | `OBJECTTYPE_SCRIPT` | `OaksAssistant2Script` | -1 |
| `OAKSLAB_SCIENTIST3` | `SPRITE_SCIENTIST` | 1 | 4 | `SPRITEMOVEDATA_WANDER` (radius 1,1) | `OBJECTTYPE_SCRIPT` | `OaksAssistant3Script` | -1 |
**Scripts of interest**
`Oak` (`59:58c3`):
```
faceplayer / opentext
checkevent EVENT_OPENED_MT_SILVER ; iftrue .CheckPokedex
checkevent EVENT_TALKED_TO_OAK_IN_KANTO ; iftrue .CheckBadges
writetext OakWelcomeKantoText / promptbutton / setevent EVENT_TALKED_TO_OAK_IN_KANTO
.CheckBadges:
readvar VAR_BADGES
ifequal NUM_BADGES, .OpenMtSilver ; 16
ifequal NUM_JOHTO_BADGES, .Complain ; 8 (i.e. zero Kanto badges)
sjump .AhGood ; 9..15 badges
.OpenMtSilver: writetext OakOpenMtSilverText / promptbutton / setevent EVENT_OPENED_MT_SILVER / sjump .CheckPokedex
.Complain: writetext OakNoKantoBadgesText / promptbutton / sjump .CheckPokedex
.AhGood: writetext OakYesKantoBadgesText / promptbutton / sjump .CheckPokedex
.CheckPokedex: writetext OakLabDexCheckText / waitbutton / special ProfOaksPCBoot / writetext OakLabGoodbyeText / waitbutton / closetext / end
```
`VAR_BADGES` = `constants/script_constants.asm:55`. `NUM_JOHTO_BADGES`, `NUM_KANTO_BADGES` and `NUM_BADGES = NUM_JOHTO_BADGES + NUM_KANTO_BADGES` are at `constants/ram_constants.asm:260,272,273`. The walkthrough's "He notices your Kanto badges. He tells you that he'll give you a prize if you get all the badges" is the `.AhGood` arm; the actual prize is the Mt. Silver unlock in `.OpenMtSilver`, which needs all 16.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_TALKED_TO_OAK_IN_KANTO` | `constants/event_flags.asm:224` | set on the first `Oak` conversation | skips the welcome text afterwards |
| `EVENT_OPENED_MT_SILVER` | `constants/event_flags.asm:1265` | set when `VAR_BADGES == NUM_BADGES` | Mt. Silver / the Red fight opens |
**Items**: none. **Trainers**: none. **Wild encounters**: none.
---
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Mt. Moon rival battle is unavoidable | `maps/MountMoon.asm` `MountMoonRivalEncounterScene` / `MountMoonRivalBattleScript` (`42:55b0`), armed by `wMountMoonSceneID == SCENE_MOUNTMOON_RIVAL_BATTLE` (0) | be able to win a 6-mon L41-45 fight | `setscene SCENE_MOUNTMOON_NOOP` + `setevent EVENT_BEAT_RIVAL_IN_MT_MOON` at the end of the script. Losing does not set the flag; the scene id is unchanged so the fight re-arms on re-entry. |
| Mt. Moon Square Moon Stone | `maps/MountMoonSquare.asm` `MountMoonSquareDisappearMoonStoneCallback` (sets `EVENT_MOUNT_MOON_SQUARE_HIDDEN_MOON_STONE` on every `MAPCALLBACK_NEWMAP`) | see the Clefairy dance | `ClefairyDance` `clearevent EVENT_MOUNT_MOON_SQUARE_HIDDEN_MOON_STONE`. Leaving the map re-sets it. |
| Clefairy dance itself | `maps/MountMoonSquare.asm` `ClefairyDance` head | `ENGINE_MT_MOON_SQUARE_CLEFAIRY` clear **and** `VAR_WEEKDAY == MONDAY` **and** `checktime NITE` | one-shot: the script `setflag ENGINE_MT_MOON_SQUARE_CLEFAIRY` at the end |
| Rock on the Moon Stone tile | `maps/MountMoonSquare.asm` `MtMoonSquareRock` -> `engine/events/std_scripts.asm:199 SmashRockScript` -> `engine/events/overworld.asm:1365 AskRockSmashScript` / `HasRockSmash` (line 1386) | `ROCK_SMASH` on a party member (`CheckPartyMove`) - **no badge check** | teach TM08 to anything |
| Trainer House CAL, once per day | `maps/TrainerHouseB1F.asm` `TrainerHouseReceptionistScript` `checkflag ENGINE_FOUGHT_IN_TRAINER_HALL_TODAY` | flag clear | the game's daily flag reset clears `ENGINE_FOUGHT_IN_TRAINER_HALL_TODAY` |
| Trainer House opponent identity | `engine/events/specials.asm:454 TrainerHouse` reading `sMysteryGiftTrainerHouseFlag`; party via `engine/battle/read_trainer_party.asm:17` | Mystery Gift performed | not reachable without a second cartridge; falls back to `CAL3` |
| Fly back to Pewter/Pallet/Viridian | `engine/events/overworld.asm:544 FlyFunction .TryFly` -> `ld de, ENGINE_STORMBADGE / call CheckBadge` **plus** the destination's own `ENGINE_FLYPOINT_*` | Storm Badge + having visited the town | `ViridianCityFlypointCallback` / `PalletTownFlypointCallback` set their flags on `MAPCALLBACK_NEWMAP` |
| Surf south from Pallet to Route 21 (next section) | `engine/events/overworld.asm:338 SurfFunction .TrySurf` -> `ld de, ENGINE_FOGBADGE / call CheckBadge` | Fog Badge + a party member that knows Surf | out of scope here |
| Indigo Plateau rival re-encounter | `maps/IndigoPlateauPokecenter1F.asm:45,67` `PlateauRivalBattle1/2` | `EVENT_BEAT_RIVAL_IN_MT_MOON` set, `ENGINE_INDIGO_PLATEAU_RIVAL_FIGHT` clear, `VAR_WEEKDAY` not SUNDAY/TUESDAY/THURSDAY/FRIDAY/SATURDAY (so Monday or Wednesday) | matches the walkthrough exactly |
| Dragon's Den rival sighting | `maps/DragonsDenB1F.asm:12` `DragonsDenB1FCheckRivalCallback` | `EVENT_BEAT_RIVAL_IN_MT_MOON` set and `VAR_WEEKDAY == TUESDAY` or `THURSDAY` | matches the walkthrough exactly |
Nothing in this section requires a badge, an HM or a key item to pass through. The only hard stop is the Mt. Moon rival battle.
---
## 4. Bot checklist
Coordinates are asm walk cells `(x, y)` on the named map.
1. `ROUTE_3` - enter from the west connection from `PEWTER_CITY`. Walk east.
2. `ROUTE_3` - trainer `ROUTE3_YOUNGSTER1` at (11, 2), sight 3 downward-ish (`STANDING_DOWN`, range 2). Precondition: `EVENT_BEAT_YOUNGSTER_WARREN` clear. Intent: battle (L35 Fearow). Postcondition: `EVENT_BEAT_YOUNGSTER_WARREN` set.
3. `ROUTE_3` - trainer `ROUTE3_YOUNGSTER2` at (20, 4), `STANDING_RIGHT`, sight 3. Battle (L33 Raticate, L33 Arbok). Post: `EVENT_BEAT_YOUNGSTER_JIMMY`.
4. `ROUTE_3` - trainer `ROUTE3_FISHER1` at (26, 12), `STANDING_UP`, sight 2. Battle (L29 Magmar, L32 Weezing, L29 Magmar). Post: `EVENT_BEAT_FIREBREATHER_OTIS`.
5. `ROUTE_3` - trainer `ROUTE3_FISHER2` at (49, 5), `SPINRANDOM_FAST`, sight 3. Battle (L32 Koffing, L32 Slugma). Post: `EVENT_BEAT_FIREBREATHER_BURT`.
6. `ROUTE_3` - optional: read the sign at (49, 13). Then heal (walkthrough advice, no flag) and save.
7. `ROUTE_3` - step on warp 1 at (52, 1). Postcondition: player on `MOUNT_MOON` at (3, 3).
8. `MOUNT_MOON` - do nothing; the scene script fires. Precondition: `wMountMoonSceneID == 0`, `EVENT_MT_MOON_RIVAL` clear. Intent: battle `RIVAL2` with the party chosen by `EVENT_GOT_TOTODILE_FROM_ELM` / `EVENT_GOT_CHIKORITA_FROM_ELM`. Postcondition: `EVENT_BEAT_RIVAL_IN_MT_MOON` set, `EVENT_MT_MOON_RIVAL` set, `wMountMoonSceneID = 1`.
9. Optional detour: `MOUNT_MOON` (13, 3) warp 3 -> lands at (25, 3); walk to (25, 5) warp 5 -> `MOUNT_MOON_SQUARE` (20, 5).
10. `MOUNT_MOON_SQUARE` - (13, 7) warp 3 -> `MOUNT_MOON_GIFT_SHOP`. Talk to the clerk (`(4, 3)` in MORN, `(1, 2)` in DAY - **no clerk at NITE**) to open `MART_MT_MOON`. Exit at (3, 7) or (4, 7).
11. `MOUNT_MOON_SQUARE` optional, Monday night only: walk onto (7, 11) to trip `ClefairyDance`. Preconditions: `ENGINE_MT_MOON_SQUARE_CLEFAIRY` clear, `VAR_WEEKDAY == MONDAY`, `checktime NITE` true. Postcondition: `ENGINE_MT_MOON_SQUARE_CLEFAIRY` set, `EVENT_MOUNT_MOON_SQUARE_HIDDEN_MOON_STONE` cleared. Then, **without leaving the map**, face (7, 7); if the rock object is there, use Rock Smash (requires `ROCK_SMASH` in the party), then press A on (7, 7) to take `MOON_STONE`.
12. `MOUNT_MOON_SQUARE` - (22, 11) warp 2 -> `MOUNT_MOON` (25, 15). Walk to (25, 13) warp 8 -> lands at (15, 11). Walk to (15, 15) warp 2 -> `ROUTE_4` (2, 5).
13. `ROUTE_4` - trainer `ROUTE4_LASS1` at (10, 8), `STANDING_LEFT`, sight 5. Battle (L34 Flaaffy). Post: `EVENT_BEAT_PICNICKER_HOPE`.
14. `ROUTE_4` - trainer `ROUTE4_YOUNGSTER` at (17, 9), `STANDING_DOWN`, sight 3. Battle (L12 Pidgey, L34 Pidgeot). Post: `EVENT_BEAT_BIRD_KEEPER_HANK`.
15. `ROUTE_4` - trainer `ROUTE4_LASS2` at (21, 6), `STANDING_RIGHT`, sight 4. Battle (L31 Furret, L33 Rapidash). Post: `EVENT_BEAT_PICNICKER_SHARON`.
16. `ROUTE_4` - item ball `ROUTE4_POKE_BALL` at (26, 3). Intent: take `HP_UP`. Precondition: `EVENT_ROUTE_4_HP_UP` clear. Postcondition: set.
17. `ROUTE_4` - optional hidden item: face (10, 3), press A -> `ULTRA_BALL`. Post: `EVENT_ROUTE_4_HIDDEN_ULTRA_BALL`.
18. Travel to `VIRIDIAN_CITY` (Fly needs `ENGINE_STORMBADGE` and the destination flypoint; the walkthrough's route is Fly to Pewter, then `ROUTE_2` south through `VIRIDIAN_FOREST`). Entering Viridian sets `ENGINE_FLYPOINT_VIRIDIAN`.
19. `VIRIDIAN_CITY` - optional: talk to `VIRIDIANCITY_FISHER` at (6, 23) for `TM_DREAM_EATER`. Precondition: `EVENT_GOT_TM42_DREAM_EATER` clear and pack room. Post: set.
20. `VIRIDIAN_CITY` - warp 3 at (23, 15) -> `TRAINER_HOUSE_1F` (2, 13). Then warp 3 at (8, 2) -> `TRAINER_HOUSE_B1F` (9, 4).
21. `TRAINER_HOUSE_B1F` - walk to (7, 3) to trip the coord_event. Precondition: `ENGINE_FOUGHT_IN_TRAINER_HALL_TODAY` clear. Answer YES. Intent: battle `CAL, CAL3` (L50 Meganium / Typhlosion / Feraligatr). Postcondition: `ENGINE_FOUGHT_IN_TRAINER_HALL_TODAY` set. The script walks the player in and out with `applymovement`; do not fight the movement.
22. Return to `VIRIDIAN_CITY` and cross the south connection into `ROUTE_1`.
23. `ROUTE_1` - fruit tree object at (3, 7). Face it, press A -> `BITTER_BERRY`. Daily respawn.
24. `ROUTE_1` - trainer `ROUTE1_YOUNGSTER` at (7, 17), `SPINRANDOM_FAST`, sight 3. Battle (L31 Jynx, L31 Electabuzz, L31 Magmar). Post: `EVENT_BEAT_SCHOOLBOY_DANNY`.
25. `ROUTE_1` - trainer `ROUTE1_COOLTRAINER_F` at (3, 26), `STANDING_RIGHT`, sight 4. Battle (L38 Ivysaur, L38 Starmie). Post: `EVENT_BEAT_COOLTRAINERF_QUINN`.
26. `ROUTE_1` - south connection into `PALLET_TOWN`. Postcondition on map load: `ENGINE_FLYPOINT_PALLET` set.
27. `PALLET_TOWN` - warp 3 at (12, 11) -> `OAKS_LAB` (4, 11).
28. `OAKS_LAB` - talk to `OAKSLAB_OAK` at (4, 2). Postcondition: `EVENT_TALKED_TO_OAK_IN_KANTO` set; if `VAR_BADGES == 16`, also `EVENT_OPENED_MT_SILVER`.
29. Exit to `PALLET_TOWN` and head south to the `ROUTE_21` connection (Surf, `ENGINE_FOGBADGE`) - next section.
---
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Kanto map geometry, warps, connections, bg/coord/object rows | `src/import/RomExtractorGen2.lua` (extracts all map events), `src/world/gen2/Map.lua`, `src/world/gen2/World.lua` | implemented (generic - every map in the ROM comes through the same path, no per-map Kanto code needed) |
| Scene scripts / `wMapSceneID` (Mt. Moon rival, Trainer House coord_event) | `src/world/gen2/World.lua:1175-1183` (`GetMapSceneID` port), `:5014-5040` (coord event and scene script lookup) | implemented |
| `sdefer` timing for the Mt. Moon rival cutscene | `src/script/gen2/Vm.lua:92-93` | partial - the comment says "sdefer queues after the map settles in retail; run immediately here", so the rival scene starts a frame or two earlier than the cart. Cosmetic for a bot, visible in a screenshot diff. |
| Map callbacks (`MAPCALLBACK_NEWMAP` / `MAPCALLBACK_OBJECTS`) - the flypoint setters and the Mt. Moon Square rock/moon-stone callbacks | `src/world/gen2/World.lua:5969`, driver `tests/drivers/gold_map_callbacks.lua` | implemented, with an assertion driver |
| `appear` / `disappear` object visibility via the event bitfield | `src/world/gen2/Events.lua` (header comment cites `CheckObjectFlag`) | implemented |
| Overworld trainers: `trainer` struct, eyesight, party build | `src/world/gen2/Trainers.lua`, `src/world/gen2/World.lua:5257-5264`, driver `tests/drivers/gold_trainer_smoke.lua` | implemented |
| `loadtrainer` / `startbattle` / `winlosstext` / `setlasttalked` (the rival and CAL fights) | `src/script/gen2/Opcodes.lua:99,100,105,109`, `src/script/gen2/Vm.lua:806` | implemented |
| Trainer House daily CAL battle + Mystery Gift fallback | `src/world/gen2/TrainerHouse.lua` | implemented (deliberately answers `CAL3`; Mystery Gift is out of scope, documented in the file header) |
| Cutscene opcodes used by `ClefairyDance` and the rival scene: `showemote`, `follow`, `stopfollow`, `cry`, `moveobject`, `applymovement` | `src/script/gen2/Opcodes.lua:117-137`, `src/script/gen2/Vm.lua:961`, `src/script/gen2/Movement.lua` | implemented |
| `checktime` / `VAR_WEEKDAY` (the Monday-night Clefairy guard) | `src/script/gen2/Vm.lua:784`, `src/core/gen2/BugContest.lua:112` (`weekday`) | implemented |
| Hidden items (`BGEVENT_ITEM`: Mt. Moon Square Moon Stone, Route 4 Ultra Ball) | `src/world/gen2/HiddenItems.lua`, wired at `src/world/gen2/World.lua:5291` | implemented |
| Item balls (`OBJECTTYPE_ITEMBALL`: the Route 4 HP Up) | extracted at `src/import/RomExtractorGen2.lua:2968-2969` as `obj.itemball`; **nothing reads it** - `World:interact` (`src/world/gen2/World.lua:5257`) only dispatches `npc.def.trainer`, strength boulders, `npc.def.scriptKey`, bg events and hidden items | **missing** - a bot cannot pick up the Route 4 HP Up in this port today |
| Fruit trees (`fruittree FRUITTREE_ROUTE_1` -> Bitter Berry) | `src/core/gen2/Apricorns.lua` (`FruitTreeItems` table, per-tree flag, `tryResetFruitTrees`), `src/script/gen2/Vm.lua:1191` | implemented |
| `pokemart` / `MART_MT_MOON` | `src/script/gen2/Vm.lua:597`, `src/ui/gen2/MartMenu.lua:336-348` | implemented (list comes from the extracted `data/generated/marts.lua`) |
| `verbosegiveitem` (Viridian TM42) | `src/script/gen2/Vm.lua:490-498` | implemented |
| Rock Smash on the Mt. Moon Square rock | `src/world/gen2/FieldMoves.lua` (comment at :588 lists `ROCK_SMASH` among the field moves); the strength-boulder equivalent is special-cased in `World:interact` | partial - `jumpstd SmashRockScript` is a `farsjump` into ASM, so there is no bytecode for the extractor to find. `World:interact` has an explicit arm for strength boulders (`World.isStrengthBoulder`) but **no equivalent arm for `SPRITEMOVEDATA_SMASHABLE_ROCK`**, so pressing A on the Mt. Moon Square rock reaches a nil `scriptKey`. Verify by hand. |
| Fly / flypoints (`ENGINE_FLYPOINT_PALLET` = 52, `ENGINE_FLYPOINT_VIRIDIAN` = 53) | `src/world/gen2/FieldMoves.lua:342-370` (both Kanto rows present) | implemented |
| Kanto wild tables (Gold vs Silver split) | extracted by `src/import/RomExtractorGen2.lua` from the ROM, so whichever cart was imported is what you get | implemented (no version conditional needed - the ROM already picked) |
---
## 6. Unresolved / verify by hand
1. **Rival party order contradicts the walkthrough.** The walkthrough lists Sneasel, Golbat, *Starter Evolution*, Magneton, Gengar, Alakazam. `data/trainers/parties.asm:2042-2069` lists Sneasel(41), Golbat(42), Magneton(41), Gengar(43), Alakazam(43), **starter last at level 45**. The walkthrough's per-mon strategy text is still correct; only the ordering is wrong. Levels and species otherwise match.
2. **Route 3 and Route 4 wild lists in the walkthrough are the Silver tables.** `data/wild/kanto_grass.asm:314` and `:369` both have `IF DEF(_GOLD) / ELIF DEF(_SILVER)` arms. Ekans and Arbok only exist in the `_SILVER` arm. On Gold, Route 3 is Spearow/Rattata/Jigglypuff (+Zubat at nite) and Route 4 is the same. The walkthrough is titled for Gold but quoted the Silver data.
3. **Mt. Moon wild list is incomplete in the walkthrough.** Gold also has L8 SANDSHREW, L10 SANDSLASH and two L8 CLEFAIRY slots (`data/wild/kanto_grass.asm:33`). The Clefairy slots mean Clefairy can be caught in the cave at any time, independent of the Monday-night dance.
4. **Route 1 wild list is incomplete and time-dependent.** The walkthrough lists Pidgey and Furret; the nite column (`data/wild/kanto_grass.asm:250`) has neither - it is Hoothoot and Rattata only.
5. **"Bitter Berry" on Route 1 is a fruit tree, not a ground item.** `Route1FruitTree` = `fruittree FRUITTREE_ROUTE_1`, and `data/items/fruit_trees.asm` row `ROUTE_1` is `BITTER_BERRY`. It respawns daily rather than being a one-time pickup, which the walkthrough's "Item on Route 1" phrasing implies.
6. **The two Mt. Moon routes described in the walkthrough could not be verified against the collision map.** The warp graph in section 2 is exact, but which of warps 2/3/4 are reachable from the (3, 3) entrance without passing through the Square depends on `maps/MountMoon.blk` collision, which was not decoded here. The walkthrough's "either way, you end up in the same spot" is consistent with warps 4<->8 and 6 meeting on the (25, 13)/(25, 15) side, but confirm by walking it.
7. **"Bring Rock Smash" for the Clefairy Moon Stone.** The asm supports the claim indirectly: `MountMoonSquareDisappearRockCallback` hides `MOUNTMOONSQUARE_ROCK` on every `MAPCALLBACK_OBJECTS`, `ClefairyDance` `appear`s it at (7, 7) mid-cutscene, and (7, 7) is exactly the `BGEVENT_ITEM` tile the dance unlocks. Whether the rock actually blocks the tile-facing A press (as opposed to being walkable) was not verified; the item could also be reachable by facing (7, 7) from an adjacent cell. Verify in-game.
8. **`ULTRA_BALL` at Route 4 (10, 3) and `TM_DREAM_EATER` in Viridian are absent from the walkthrough.** Both are real (`maps/Route4.asm` bg_event, `maps/ViridianCity.asm` `ViridianCityDreamEaterFisher`). Not a contradiction, just an omission worth carrying into a completionist route.
9. **Mt. Moon Gift Shop has no NITE clerk.** Both `SPRITE_GRAMPS` rows are gated `-1, MORN` and `-1, DAY`. A bot that arrives at night finds no shop, which the walkthrough does not warn about.
10. **`ROUTE_3` warp position vs. its sign.** The only warp is at (52, 1) but the "MT.MOON SQUARE / Just go up the stairs" sign is at (49, 13), twelve cells south. That is what the file says; it reads oddly and is worth an eyeball on the rendered map.
11. **Port: `OBJECTTYPE_ITEMBALL` pickup is unimplemented** (see section 5). The Route 4 HP Up is the first item ball this section needs, and `World:interact` has no arm for it. Confirmed by reading `src/world/gen2/World.lua:5257-5310` and grepping the whole `src/` tree for `itemball` (only the extractor and one comment in `src/script/gen2/CallAsm.lua:547` mention it).
12. **Port: smashable-rock objects have no interact arm** (see section 5). Same class of gap as the item ball, but lower stakes because the only rock in this section is optional.
@@ -0,0 +1,570 @@
# Section 27 - Cinnabar Island and Seafoam Islands Gym
Source: `../section-27-cinnabar-island-and-seafoam-islands-gym.txt`
(the FAQ numbers this chapter "33 > Cinnabar Island and Seafoam Islands Gym"; the
file index is 27)
Maps covered: `MAP_ROUTE_21`, `MAP_CINNABAR_ISLAND`, `MAP_CINNABAR_POKECENTER_1F`,
`MAP_ROUTE_20`, `MAP_SEAFOAM_GYM`
Badges / key milestones in this section:
- `ENGINE_FLYPOINT_CINNABAR` (set on first entry to Cinnabar Island)
- Blue's one-off speech on Cinnabar Island, which is what unlocks him in Viridian Gym
(`clearevent EVENT_VIRIDIAN_GYM_BLUE`)
- Hidden RARE_CANDY on Cinnabar Island
- `EVENT_CINNABAR_ROCKS_CLEARED` (set silently by walking onto Route 20; this is what
clears the boulders that seal Route 19 to the east)
- **VOLCANOBADGE** from BLAINE in the Seafoam Islands cave (`ENGINE_VOLCANOBADGE`,
`EVENT_BEAT_BLAINE`)
---
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 0 | `PALLET_TOWN` | - | - | south connection to `ROUTE_21` (`data/maps/attributes.asm:270-272`) | **Pallet Town belongs to the previous section; only the hop south is listed here.** |
| 1 | `ROUTE_21` | `maps/Route21.asm` | north connection from `PALLET_TOWN` (`data/maps/attributes.asm:274-275`) | south connection to `CINNABAR_ISLAND` (`attributes.asm:277`) | Surf south. Three trainers: Nikki, Arnold, Seth |
| 2 | `CINNABAR_ISLAND` | `maps/CinnabarIsland.asm` | north connection from `ROUTE_21` (`attributes.asm:279-280`) | warp 1 at (11,11) -> `CINNABAR_POKECENTER_1F` 1; east connection to `ROUTE_20` (`attributes.asm:281`) | Sets `ENGINE_FLYPOINT_CINNABAR`; heal; talk to Blue; hidden RARE_CANDY |
| 3 | `CINNABAR_POKECENTER_1F` | `maps/CinnabarPokecenter1F.asm` | Cinnabar warp 1 | warps 1/2 at (3,7)/(4,7) back to `CINNABAR_ISLAND` 1 | Heal; the two flavour NPCs the walkthrough quotes ("a year since the volcano erupted", "BLAINE lives alone in the SEAFOAM ISLANDS cave") |
| 4 | `ROUTE_20` | `maps/Route20.asm` | west connection from `CINNABAR_ISLAND` (`attributes.asm:283-284`) | warp 1 at (38,7) -> `SEAFOAM_GYM` 1 | `MAPCALLBACK_NEWMAP` sets `EVENT_CINNABAR_ROCKS_CLEARED`; Swimmer Cameron at (12,13) |
| 5 | `SEAFOAM_GYM` | `maps/SeafoamGym.asm` | Route 20 warp 1 | warp 1 at (5,5) back to `ROUTE_20` 1 | BLAINE -> **VOLCANOBADGE** |
Spillover: Route 20 continues east past the gym to Swimmer Lori (45,13) and Swimmer
Nicole (52,8) and then over the `connection east, Route19, ROUTE_19, -9` boundary into
Fuchsia territory; the walkthrough does not visit them here and they belong to the
following section.
---
## 2. Maps
### MAP_ROUTE_21
- Script: `maps/Route21.asm`
- Blocks: `maps/Route21.blk`
- Header: `data/maps/maps.asm:198` -> `TILESET_KANTO, ROUTE, LANDMARK_ROUTE_21, MUSIC_ROUTE_3, FALSE (phone calls allowed), PALETTE_AUTO, FISHGROUP_OCEAN`
- Dimensions: `constants/map_constants.asm:182` `map_const ROUTE_21, 10, 18` (group `CINNABAR` = 6, map id 7). 10x18 blocks = 20x36 tile coordinates.
- Attributes / border: `data/maps/attributes.asm:274` `map_attributes Route21, ROUTE_21, $43`
- Connections: north `PALLET_TOWN` (offset 0), south `CINNABAR_ISLAND` (offset 0)
- Landmark position: `data/maps/landmarks.asm:96` `landmark 52, 120, Route21Name`
**Warps** (`def_warp_events`)
None (`def_warp_events` at `maps/Route21.asm:95` is empty).
**Coord events** (`def_coord_events`)
None (`maps/Route21.asm:97`).
**BG events** (`def_bg_events`)
None (`maps/Route21.asm:99`).
**Object events** (`def_object_events`, `maps/Route21.asm:101-104`)
| const | sprite | x | y | movement | type | sight | script label | event flag |
|---|---|---|---|---|---|---|---|---|
| `ROUTE21_SWIMMER_GIRL` | `SPRITE_SWIMMER_GIRL` | 11 | 16 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` | 3 | `TrainerSwimmerfNikki` | -1 (always) |
| `ROUTE21_SWIMMER_GUY` | `SPRITE_SWIMMER_GUY` | 2 | 30 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_TRAINER` | 4 | `TrainerSwimmermSeth` | -1 (always) |
| `ROUTE21_FISHER` | `SPRITE_FISHER` | 14 | 22 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_TRAINER` | 1 | `TrainerFisherArnold` | -1 (always) |
The walkthrough's order (Nikki, then the fisherman, then "head way left then down") is
exactly the y ordering 16 -> 22 -> 30, with Seth in the far south-west corner at x=2.
**Scripts of interest**
- `TrainerSwimmerfNikki` (`maps/Route21.asm:22`, sym `4e:4803`) -
`trainer SWIMMERF, NIKKI, EVENT_BEAT_SWIMMERF_NIKKI, SwimmerfNikkiSeenText, SwimmerfNikkiBeatenText, 0, .Script`.
The `0` is the loss-text slot (no whiteout text). `.Script` is `endifjustbattled` +
one after-battle line; no items, no flags beyond the beat flag.
- `TrainerFisherArnold` (`maps/Route21.asm:33`, sym `4e:4817`) - same shape,
`EVENT_BEAT_FISHER_ARNOLD`.
- `TrainerSwimmermSeth` (`maps/Route21.asm:11`, sym `4e:47ef`) - same shape,
`EVENT_BEAT_SWIMMERM_SETH`.
There are no scene scripts and no callbacks on this map (`def_scene_scripts` /
`def_callbacks` at `maps/Route21.asm:7-9` are both empty).
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_SWIMMERF_NIKKI` | `constants/event_flags.asm:490` | `trainer` macro row, `maps/Route21.asm:23` | set once Nikki is beaten; suppresses re-battle |
| `EVENT_BEAT_FISHER_ARNOLD` | `constants/event_flags.asm:587` | `maps/Route21.asm:34` | as above |
| `EVENT_BEAT_SWIMMERM_SETH` | `constants/event_flags.asm:958` | `maps/Route21.asm:12` | as above |
**Items**
None on this map.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm`) | script label | rematch/phone |
|---|---|---|---|---|---|
| `SWIMMERF, NIKKI` | SWIMMERF ($27) | 17 | `; SWIMMERF (17)` at `parties.asm:1872`: L28 SEEL, L28 SEEL, L28 SEEL, L28 DEWGONG (`TRAINERTYPE_NORMAL`) | `TrainerSwimmerfNikki` | no |
| `FISHER, ARNOLD` | FISHER ($25) | 3 | `; FISHER (3)` at `parties.asm:1511`: L34 TENTACRUEL | `TrainerFisherArnold` | no |
| `SWIMMERM, SETH` | SWIMMERM ($26) | 18 | `; SWIMMERM (18)` at `parties.asm:1754`: L29 QUAGSIRE, L29 OCTILLERY, L32 QUAGSIRE | `TrainerSwimmermSeth` | no |
Payouts (base reward x last-mon level x 4, see section 3 for the citation):
Nikki 5 x 28 x 4 = **560**, Arnold 10 x 34 x 4 = **1360**, Seth 2 x 32 x 4 = **256**.
All three match the walkthrough exactly.
Base rewards: `data/trainers/attributes.asm:223` (Fisher, 10), `:229` (Swimmerm, 2),
`:235` (Swimmerf, 5).
**Wild encounters**
- Water (`data/wild/kanto_water.asm:61-66`), 6% rate: L35 TENTACOOL / L30 TENTACOOL / L35 TENTACRUEL.
- Grass (`data/wild/kanto_grass.asm:1005-1031`), 6/6/6% morn/day/nite. There *is* a
grass table, on the small land tiles:
- morn: 30 TANGELA, 25 TANGELA, 35 TANGELA, 20 TANGELA, 30 TANGELA, 28 MR__MIME, 28 MR__MIME
- day: 30 TANGELA, 25 TANGELA, 35 TANGELA, 20 TANGELA, 28 MR__MIME, 30 MR__MIME, 30 MR__MIME
- nite: 30 TANGELA, 25 TANGELA, 35 TANGELA, 20 TANGELA, 30 TANGELA, 28 MR__MIME, 28 MR__MIME
- Fishing: `FISHGROUP_OCEAN` (`data/wild/fish.asm:13`). Old rod `.Ocean_Old` (`:42`)
MAGIKARP/MAGIKARP/TENTACOOL L10; Good rod `.Ocean_Good` (`:46`) MAGIKARP L20 /
TENTACOOL L20 / CHINCHOU L20 / `time_group 2` = SHELLDER L20 day and nite
(`fish.asm:214`); Super rod `.Ocean_Super` (`:51`) CHINCHOU L40 / `time_group 3` =
SHELLDER L40 (`fish.asm:215`) / TENTACRUEL L40 / LANTURN L40.
- No headbutt tree rows and no rock smash on this map.
---
### MAP_CINNABAR_ISLAND
- Script: `maps/CinnabarIsland.asm`
- Blocks: `maps/CinnabarIsland.blk`
- Header: `data/maps/maps.asm:199` -> `TILESET_KANTO, TOWN, LANDMARK_CINNABAR_ISLAND, MUSIC_VIRIDIAN_CITY, FALSE, PALETTE_AUTO, FISHGROUP_OCEAN`
- Dimensions: `constants/map_constants.asm:183` `map_const CINNABAR_ISLAND, 10, 9` (group `CINNABAR` = 6, map id 8). 20x18 tile coordinates.
- Attributes / border: `data/maps/attributes.asm:279` `map_attributes CinnabarIsland, CINNABAR_ISLAND, $43`
- Connections: north `ROUTE_21` (offset 0), east `ROUTE_20` (offset 0)
- Landmark: `data/maps/landmarks.asm:95` `landmark 52, 132, CinnabarIslandName`
- Fly / respawn point: `data/maps/flypoints.asm:28` `db LANDMARK_CINNABAR_ISLAND, SPAWN_CINNABAR`;
`data/maps/spawn_points.asm:24` `spawn CINNABAR_ISLAND, 11, 12`
**Warps** (`def_warp_events`, `maps/CinnabarIsland.asm:131-132`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 11 | 11 | `CINNABAR_POKECENTER_1F` | 1 |
**Coord events** (`def_coord_events`)
None (`maps/CinnabarIsland.asm:134`). Blue is a talk-to NPC, not a trip-wire.
**BG events** (`def_bg_events`, `maps/CinnabarIsland.asm:136-140`)
| x | y | type | script/item |
|---|---|---|---|
| 12 | 11 | `BGEVENT_READ` | `CinnabarIslandPokecenterSign` -> `jumpstd PokecenterSignScript` |
| 9 | 11 | `BGEVENT_READ` | `CinnabarIslandGymSign` -> "CINNABAR GYM has relocated to SEAFOAM ISLANDS. BLAINE" |
| 7 | 7 | `BGEVENT_READ` | `CinnabarIslandSign` -> town sign |
| 9 | 1 | `BGEVENT_ITEM` | `CinnabarIslandHiddenRareCandy` -> `hiddenitem RARE_CANDY, EVENT_CINNABAR_ISLAND_HIDDEN_RARE_CANDY` (sym `4e:49a2`) |
**Object events** (`def_object_events`, `maps/CinnabarIsland.asm:142-143`)
| const | sprite | x | y | movement | type | sight | script label | event flag |
|---|---|---|---|---|---|---|---|---|
| `CINNABARISLAND_BLUE` | `SPRITE_BLUE` | 9 | 6 | `SPRITEMOVEDATA_SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT` | 0 | `CinnabarIslandBlue` | `EVENT_BLUE_IN_CINNABAR` |
**Scripts of interest**
- `CinnabarIslandFlypointCallback` (`maps/CinnabarIsland.asm:10`, sym `4e:4981`), wired
as `callback MAPCALLBACK_NEWMAP` at `:8`. Body is exactly
`setflag ENGINE_FLYPOINT_CINNABAR` / `endcallback`. Walking in once is the whole
unlock for flying here.
- `CinnabarIslandBlue` (`maps/CinnabarIsland.asm:14`, sym `4e:4985`). Control flow, in
order: `faceplayer`, `opentext`, `writetext CinnabarIslandBlueText`, `waitbutton`,
`closetext`, `playsound SFX_WARP_TO`,
`applymovement CINNABARISLAND_BLUE, CinnabarIslandBlueTeleport`,
`disappear CINNABARISLAND_BLUE`, `clearevent EVENT_VIRIDIAN_GYM_BLUE`, `end`.
- There is **no guard flag and no `iftrue`** at the top: the script is one-shot only
because `disappear` sets the object's own `EVENT_BLUE_IN_CINNABAR` (see
`Script_disappear` at `engine/overworld/scripting.asm:887`, `ld b, 1 ; set`).
- `CinnabarIslandBlueTeleport` (`:38`) is a two-byte movement stream:
`teleport_from` (`$4c`, `macros/scripts/movement.asm:148`) then `step_end`. That is
the "levitates and flies off" the walkthrough describes.
- `clearevent EVENT_VIRIDIAN_GYM_BLUE` is the load-bearing side effect: that flag is
set at boot by `InitializeEventsScript` (`engine/events/std_scripts.asm:552`) and it
hides both `VIRIDIANGYM_BLUE` and the Viridian gym guide
(`maps/ViridianGym.asm:183-184`). **Talking to Blue here is the only thing that
puts Blue in Viridian Gym.**
- `CinnabarIslandHiddenRareCandy` (`maps/CinnabarIsland.asm:35`) - a `hiddenitem`
operand, not a script body: `dwb EVENT_CINNABAR_ISLAND_HIDDEN_RARE_CANDY, RARE_CANDY`.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_FLYPOINT_CINNABAR` | `constants/engine_flags.asm:77` | set by `CinnabarIslandFlypointCallback` | Fly destination unlocked; also the wVisitedSpawns bit for respawn |
| `EVENT_BLUE_IN_CINNABAR` | `constants/event_flags.asm:1303` | *set* implicitly by `disappear CINNABARISLAND_BLUE`; read by `maps/ViridianCity.asm:37` and `maps/ViridianPokecenter1F.asm:18` for "Blue returned" dialogue | clear = Blue is standing on Cinnabar; set = you have heard the speech |
| `EVENT_VIRIDIAN_GYM_BLUE` | `constants/event_flags.asm:1304` | set at `engine/events/std_scripts.asm:552`; cleared by `CinnabarIslandBlue` | while set, Viridian Gym is empty |
| `EVENT_CINNABAR_ISLAND_HIDDEN_RARE_CANDY` | `constants/event_flags.asm:254` | the `hiddenitem` at `maps/CinnabarIsland.asm:36` | one-shot guard on the hidden candy |
Note: `EVENT_BLUE_IN_CINNABAR` is **not** in the `InitializeEventsScript` list, so it
starts clear and Blue is visible from the first time you reach the island.
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `RARE_CANDY` | face tile (9,1) and press A (or ITEMFINDER) | `bg_event 9, 1, BGEVENT_ITEM, CinnabarIslandHiddenRareCandy` | `EVENT_CINNABAR_ISLAND_HIDDEN_RARE_CANDY` |
The walkthrough never mentions this candy.
**Trainers**
None.
**Wild encounters**
No `def_grass_wildmons`/`def_water_wildmons` row for `CINNABAR_ISLAND` in
`data/wild/kanto_grass.asm` or `data/wild/kanto_water.asm`. The surrounding water belongs
to Route 20 / Route 21. Fishing on the map's own water uses `FISHGROUP_OCEAN`
(`data/maps/maps.asm:199`).
---
### MAP_CINNABAR_POKECENTER_1F
- Script: `maps/CinnabarPokecenter1F.asm`
- Header: `data/maps/maps.asm:192` -> `TILESET_POKECENTER, INDOOR, LANDMARK_CINNABAR_ISLAND, MUSIC_POKEMON_CENTER, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:176` `map_const CINNABAR_POKECENTER_1F, 5, 4` (group `CINNABAR` = 6, map id 1)
- Attributes: `data/maps/attributes.asm:510` `map_attributes CinnabarPokecenter1F, CINNABAR_POKECENTER_1F, $00`; no connections
**Warps** (`def_warp_events`, `maps/CinnabarPokecenter1F.asm:38-41`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `CINNABAR_ISLAND` | 1 |
| 2 | 4 | 7 | `CINNABAR_ISLAND` | 1 |
| 3 | 0 | 7 | `POKECENTER_2F` | 1 |
**Coord events / BG events**
None (`maps/CinnabarPokecenter1F.asm:43`, `:45`).
**Object events** (`maps/CinnabarPokecenter1F.asm:47-50`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `CINNABARPOKECENTER1F_NURSE` | `SPRITE_NURSE` | 3 | 1 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `CinnabarPokecenter1FNurseScript` -> `jumpstd PokecenterNurseScript` | -1 |
| `CINNABARPOKECENTER1F_COOLTRAINER_F` | `SPRITE_COOLTRAINER_F` | 7 | 6 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (radius x 2) | `OBJECTTYPE_SCRIPT` | `CinnabarPokecenter1FCooltrainerFScript` (BLAINE lives in the Seafoam cave) | -1 |
| `CINNABARPOKECENTER1F_FISHER` | `SPRITE_FISHER` | 2 | 4 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `CinnabarPokecenter1FFisherScript` ("It's been a year since the volcano erupted.") | -1 |
Both flavour NPCs are unconditional `jumptextfaceplayer` - no flags, no branches. These
are the two people the walkthrough tells you to talk to.
---
### MAP_ROUTE_20
- Script: `maps/Route20.asm`
- Blocks: `maps/Route20.blk`
- Header: `data/maps/maps.asm:197` -> `TILESET_KANTO, ROUTE, LANDMARK_ROUTE_20, MUSIC_ROUTE_3, FALSE, PALETTE_AUTO, FISHGROUP_OCEAN`
- Dimensions: `constants/map_constants.asm:181` `map_const ROUTE_20, 30, 9` (group `CINNABAR` = 6, map id 6). 60x18 tile coordinates.
- Attributes / border: `data/maps/attributes.asm:283` `map_attributes Route20, ROUTE_20, $43`
- Connections: west `CINNABAR_ISLAND` (offset 0), east `ROUTE_19` (offset -9)
- Landmarks in view: `data/maps/landmarks.asm:93` Route 20, `:94` `SeafoamIslandsName`
**Warps** (`def_warp_events`, `maps/Route20.asm:116-117`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 38 | 7 | `SEAFOAM_GYM` | 1 |
**Coord events**
None (`maps/Route20.asm:119`).
**BG events** (`maps/Route20.asm:121-122`)
| x | y | type | script/item |
|---|---|---|---|
| 37 | 11 | `BGEVENT_READ` | `CinnabarGymSign` -> "CINNABAR GYM / LEADER: BLAINE" |
**Object events** (`maps/Route20.asm:124-127`)
| const | sprite | x | y | movement | type | sight | script label | event flag |
|---|---|---|---|---|---|---|---|---|
| `ROUTE20_SWIMMER_GIRL1` | `SPRITE_SWIMMER_GIRL` | 52 | 8 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` | 3 | `TrainerSwimmerfNicole` | -1 |
| `ROUTE20_SWIMMER_GIRL2` | `SPRITE_SWIMMER_GIRL` | 45 | 13 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` | 3 | `TrainerSwimmerfLori` | -1 |
| `ROUTE20_SWIMMER_GUY` | `SPRITE_SWIMMER_GUY` | 12 | 13 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` | 3 | `TrainerSwimmermCameron` | -1 |
Cameron at x=12 is the only one west of the gym door at x=38, which is why the
walkthrough sees "one trainer" on the way in. Lori (45) and Nicole (52) are east of it.
**Scripts of interest**
- `Route20ClearRocksCallback` (`maps/Route20.asm:12`, sym `4e:4cfa`), wired as
`callback MAPCALLBACK_NEWMAP` at `:10`. Body is `setevent EVENT_CINNABAR_ROCKS_CLEARED`
/ `endcallback`. It fires silently the first time the map loads.
Consumers: `Route19ClearRocksCallback` (`maps/Route19.asm:15`, sym `4e:4f09`, a
`MAPCALLBACK_TILES` callback) draws six `changeblock ... $7a ; rock` blocks at
(6,6), (8,6), (10,6), (12,8), (4,8), (10,10) **while the flag is clear**; the officer
in `maps/Route19FuchsiaGate.asm:12` and the two fishers in `maps/Route19.asm:74`/`:90`
swap dialogue on it.
- `TrainerSwimmermCameron` (`maps/Route20.asm:38`, sym `4e:4d26`) - plain trainer,
`EVENT_BEAT_SWIMMERM_CAMERON`, `endifjustbattled` + one line.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_CINNABAR_ROCKS_CLEARED` | `constants/event_flags.asm:214` | set by `Route20ClearRocksCallback`; read by `maps/Route19.asm:16`, `:74`, `:90` and `maps/Route19FuchsiaGate.asm:12` | while clear, six rock blocks seal the Route 19 water path from the Fuchsia side |
| `EVENT_BEAT_SWIMMERM_CAMERON` | `constants/event_flags.asm:957` | `maps/Route20.asm:39` | beat flag |
| `EVENT_BEAT_SWIMMERF_NICOLE` | `constants/event_flags.asm:487` | `maps/Route20.asm:17` | beat flag (next section) |
| `EVENT_BEAT_SWIMMERF_LORI` | `constants/event_flags.asm:488` | `maps/Route20.asm:28` | beat flag (next section) |
**Items**
None on this map.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm`) | script label | rematch/phone |
|---|---|---|---|---|---|
| `SWIMMERM, CAMERON` | SWIMMERM ($26) | 17 | `; SWIMMERM (17)` at `parties.asm:1749`: L34 MARILL | `TrainerSwimmermCameron` | no |
| `SWIMMERF, NICOLE` | SWIMMERF ($27) | 14 | `; SWIMMERF (14)` at `parties.asm:1854`: L29 MARILL, L29 MARILL, L32 LAPRAS | `TrainerSwimmerfNicole` | no |
| `SWIMMERF, LORI` | SWIMMERF ($27) | 15 | `; SWIMMERF (15)` at `parties.asm:1861`: L32 STARMIE, L32 STARMIE | `TrainerSwimmerfLori` | no |
Cameron pays 2 x 34 x 4 = **272**, matching the walkthrough.
**Wild encounters**
- Water (`data/wild/kanto_water.asm:54-59`), 6% rate: L35 TENTACOOL / L30 TENTACOOL / L35 TENTACRUEL.
- No `def_grass_wildmons ROUTE_20` row exists in `data/wild/kanto_grass.asm` - there is
no land grass on this route.
- Fishing: `FISHGROUP_OCEAN`, same three rod tables as Route 21 above.
---
### MAP_SEAFOAM_GYM
- Script: `maps/SeafoamGym.asm`
- Blocks: `maps/SeafoamGym.blk`
- Header: `data/maps/maps.asm:195` -> `TILESET_CAVE, INDOOR, LANDMARK_SEAFOAM_ISLANDS, MUSIC_GYM, TRUE (phone calls blocked), PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:179` `map_const SEAFOAM_GYM, 5, 4` (group `CINNABAR` = 6, map id 4). 10x8 tile coordinates - smaller than the 20x18 screen, which is the walkthrough's "this gym doesn't even fit the entire screen".
- Attributes / border: `data/maps/attributes.asm:513` `map_attributes SeafoamGym, SEAFOAM_GYM, $09`; no connections
- There is **no** `MAP_SEAFOAM_ISLANDS` dungeon in Gen 2 - `constants/map_constants.asm`
has only `SEAFOAM_GYM` in the whole Seafoam family.
**Warps** (`def_warp_events`, `maps/SeafoamGym.asm:161-162`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 5 | 5 | `ROUTE_20` | 1 |
**Coord events**
None (`maps/SeafoamGym.asm:164`). The map does declare one scene script,
`scene_script SeafoamGymNoopScene` (`:7`), and the asm itself annotates it
`; unusable` - the body at `:11` is a bare `end`.
**BG events**
None (`maps/SeafoamGym.asm:166`).
**Object events** (`maps/SeafoamGym.asm:168-170`)
| const | sprite | x | y | movement | type | sight | script label | event flag |
|---|---|---|---|---|---|---|---|---|
| `SEAFOAMGYM_BLAINE` | `SPRITE_BLAINE` | 5 | 2 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | 0 | `SeafoamGymBlaineScript` | -1 (always present) |
| `SEAFOAMGYM_GYM_GUIDE` | `SPRITE_GYM_GUIDE` | 6 | 5 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_SCRIPT` | 0 | `SeafoamGymGuideScript` | `EVENT_SEAFOAM_GYM_GYM_GUIDE` |
**Scripts of interest**
- `SeafoamGymBlaineScript` (`maps/SeafoamGym.asm:14`, sym `53:516d`). Control flow:
1. `faceplayer`, `opentext`
2. `checkflag ENGINE_VOLCANOBADGE` / `iftrue .FightDone` - the entire re-talk guard is
the badge engine flag, **not** an event flag.
3. `writetext BlaineIntroText`, `waitbutton`, `closetext`
4. `winlosstext BlaineWinLossText, 0` (no loss text)
5. `loadtrainer BLAINE, BLAINE1`, `startbattle`
6. `iftrue .ReturnAfterBattle` - i.e. if the battle result is non-zero (you lost /
drew) it *skips* `appear SEAFOAMGYM_GYM_GUIDE`
7. `appear SEAFOAMGYM_GYM_GUIDE` - clears `EVENT_SEAFOAM_GYM_GYM_GUIDE`, which is how
the guide "comes in late"
8. `.ReturnAfterBattle`: `reloadmapafterbattle`, `setevent EVENT_BEAT_BLAINE`,
`opentext`, `writetext ReceivedVolcanoBadgeText`, `playsound SFX_GET_BADGE`,
`waitsfx`, **`setflag ENGINE_VOLCANOBADGE`**, `writetext BlaineAfterBattleText`,
`waitbutton`, `closetext`, `end`
- No `verbosegiveitem` anywhere: **Blaine gives no TM**, only the badge.
- Note the ordering quirk: `setevent EVENT_BEAT_BLAINE` / `setflag ENGINE_VOLCANOBADGE`
are on the shared `.ReturnAfterBattle` path, reached whether or not you won, but
`startbattle` does not return to the script on a whiteout, so in practice this is
only reached after a win.
- `SeafoamGymGuideScript` (`maps/SeafoamGym.asm:46`, sym `53:51a3`) - branches on
`EVENT_TALKED_TO_SEAFOAM_GYM_GUIDE_ONCE`, first line is the apology
("CINNABAR GYM was gone, so I didn't know where to find you"), then a second line
forever after.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_VOLCANOBADGE` | `constants/engine_flags.asm:53` (Kanto badge 7; `wKantoBadges` bit 6 per `constants/ram_constants.asm:270`) | checked at `maps/SeafoamGym.asm:17`, set at `:34` | the badge itself; also gates the re-talk branch |
| `EVENT_BEAT_BLAINE` | `constants/event_flags.asm:721` | set at `maps/SeafoamGym.asm:29` | not read by any other map in the checkout; it is the record, not the gate |
| `EVENT_SEAFOAM_GYM_GYM_GUIDE` | `constants/event_flags.asm:1305` | set at boot (`engine/events/std_scripts.asm:553`), cleared by `appear` at `maps/SeafoamGym.asm:26` | while set, the guide object does not exist |
| `EVENT_TALKED_TO_SEAFOAM_GYM_GUIDE_ONCE` | `constants/event_flags.asm:212` | `maps/SeafoamGym.asm:49`, `:54` | first-talk vs repeat text |
What VOLCANOBADGE actually does mechanically: `data/types/badge_type_boosts.asm:19`
`db FIRE ; VOLCANOBADGE`, consumed by `DoBadgeTypeBoosts` (`engine/battle/misc.asm:146`,
called from `engine/battle/effect_commands.asm:1261`) - a +12.5% damage boost on the
player's FIRE-type moves. It grants no field move.
**Items**
None.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm`) | script label | rematch/phone |
|---|---|---|---|---|---|
| `BLAINE, BLAINE1` | BLAINE ($2e), `constants/trainer_constants.asm:469-470` | 1 | `BlaineGroup` (sym `0e:6e70`) at `parties.asm:2303`, `TRAINERTYPE_MOVES`: **L45 MAGCARGO** (CURSE, SMOG, FLAMETHROWER, ROCK_SLIDE), **L45 MAGMAR** (THUNDERPUNCH, FIRE_PUNCH, SUNNY_DAY, CONFUSE_RAY), **L50 RAPIDASH** (QUICK_ATTACK, FIRE_SPIN, FURY_ATTACK, FIRE_BLAST) | `SeafoamGymBlaineScript` | no rematch, no phone |
Class attributes (`data/trainers/attributes.asm:275-279`): AI items are
**`MAX_POTION, FULL_HEAL`** (not two Full Restores); base reward 25; AI mask
`AI_BASIC | AI_SETUP | AI_SMART | AI_AGGRESSIVE | AI_CAUTIOUS | AI_STATUS | AI_RISKY`,
`CONTEXT_USE | SWITCH_SOMETIMES`. Payout 25 x 50 x 4 = **5000**, matching the walkthrough.
`BLAINE` is in `KantoGymLeaders` (`data/trainers/leaders.asm`), so the battle uses the
gym-leader music and awards `HAPPINESS_GYMBATTLE`.
**Wild encounters**
None. The map is `INDOOR` with `TILESET_CAVE`; its `FISHGROUP_SHORE` header entry is the
default and has no reachable water.
---
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Everything in this section is across open sea (Pallet -> Route 21 -> Cinnabar -> Route 20 -> gym door) | `engine/events/overworld.asm:469` `TrySurfOW` (walk-into-water path): `ld de, ENGINE_FOGBADGE` / `CheckEngineFlag` at `:490`, then `ld d, SURF` / `CheckPartyMove`. Menu path is `SurfFunction` `.TrySurf` at `:340` with the same `ENGINE_FOGBADGE`. `CheckBadge` itself is at `:50`. | FOGBADGE + a party mon that knows SURF | already held long before Kanto (Johto badge 4) |
| Fly to Cinnabar Island | `data/maps/flypoints.asm:28`; the flag is `ENGINE_FLYPOINT_CINNABAR`, set by `CinnabarIslandFlypointCallback` (`maps/CinnabarIsland.asm:10`) | STORMBADGE + FLY + having stood on Cinnabar Island once | walk in once |
| Blue is not in Viridian Gym | `maps/ViridianGym.asm:183-184` object rows carry `EVENT_VIRIDIAN_GYM_BLUE`, set at `engine/events/std_scripts.asm:552` | - | `clearevent EVENT_VIRIDIAN_GYM_BLUE` at the tail of `CinnabarIslandBlue` (`maps/CinnabarIsland.asm:23`) - **this section is a hard prerequisite for the Viridian Gym leader** |
| Route 19 (east of Route 20) is sealed by six boulder blocks | `maps/Route19.asm:15` `Route19ClearRocksCallback`, a `MAPCALLBACK_TILES` callback that lays `changeblock` rock blocks while `EVENT_CINNABAR_ROCKS_CLEARED` is clear | - | `setevent EVENT_CINNABAR_ROCKS_CLEARED` in `Route20ClearRocksCallback` (`maps/Route20.asm:13`), i.e. simply loading Route 20 once from the Cinnabar side |
| Blaine will not re-battle | `maps/SeafoamGym.asm:17` `checkflag ENGINE_VOLCANOBADGE` / `iftrue .FightDone` | - | none in vanilla; `.FightDone` is terminal |
| The gym guide does not exist until Blaine is beaten | object row event flag `EVENT_SEAFOAM_GYM_GYM_GUIDE` (`maps/SeafoamGym.asm:170`), set at `engine/events/std_scripts.asm:553` | beat Blaine | `appear SEAFOAMGYM_GYM_GUIDE` (`maps/SeafoamGym.asm:26`), only on the win branch |
Nothing in this section needs CUT, STRENGTH, WHIRLPOOL, WATERFALL, FLASH or a key item.
## 4. Bot checklist
Ordered, machine-actionable. `(g,m)` is `(map group, map id)` from
`constants/map_constants.asm`. All coordinates are the raw asm map-event values.
1. `PALLET_TOWN`: SURF south across the connection into `ROUTE_21 (6,7)`.
Pre: FOGBADGE + SURF in the party. Post: none.
2. `ROUTE_21`: battle `TrainerSwimmerfNikki` at (11,16) - sight radius 3, so she will
trigger on approach. Party L28 SEEL x3 + L28 DEWGONG. Post: `EVENT_BEAT_SWIMMERF_NIKKI`, +560 money.
3. `ROUTE_21`: battle `TrainerFisherArnold` at (14,22) - sight radius 1, faces up.
Party L34 TENTACRUEL. Post: `EVENT_BEAT_FISHER_ARNOLD`, +1360 money.
4. `ROUTE_21`: surf west then south to `TrainerSwimmermSeth` at (2,30) - sight radius 4,
faces right. Party L29 QUAGSIRE / L29 OCTILLERY / L32 QUAGSIRE.
Post: `EVENT_BEAT_SWIMMERM_SETH`, +256 money.
(Both trainers are avoidable - all three are `OBJECTTYPE_TRAINER` with a finite sight
radius, none are on a coord event.)
5. `ROUTE_21`: continue south across the connection into `CINNABAR_ISLAND (6,8)`.
Post: `MAPCALLBACK_NEWMAP` sets `ENGINE_FLYPOINT_CINNABAR`; respawn point becomes
`SPAWN_CINNABAR` = (11,12).
6. `CINNABAR_ISLAND`: step on warp 1 at (11,11) -> `CINNABAR_POKECENTER_1F (6,1)`.
Talk to `CINNABARPOKECENTER1F_NURSE` at (3,1) to heal. Optional: the two flavour NPCs
at (7,6) and (2,4). Exit via warp 1/2 at (3,7)/(4,7).
7. `CINNABAR_ISLAND`: optional - face (9,1) and press A for the hidden **RARE_CANDY**.
Pre: `EVENT_CINNABAR_ISLAND_HIDDEN_RARE_CANDY` clear + a free bag slot.
Post: flag set. (Not in the walkthrough.)
8. `CINNABAR_ISLAND`: walk to `CINNABARISLAND_BLUE` at (9,6) and talk.
Pre: `EVENT_BLUE_IN_CINNABAR` clear. Expect a long `writetext` (about 13 pages of
`para` breaks - budget A presses accordingly), then `SFX_WARP_TO` + a `teleport_from`
movement, then the object vanishes.
Post: `EVENT_BLUE_IN_CINNABAR` set (via `disappear`), `EVENT_VIRIDIAN_GYM_BLUE`
**cleared**. Do not skip this step: it is the only unlock for the Viridian Gym leader.
9. `CINNABAR_ISLAND`: optional signs at (12,11), (9,11), (7,7).
10. `CINNABAR_ISLAND`: SURF east across the connection into `ROUTE_20 (6,6)`.
Post: `MAPCALLBACK_NEWMAP` sets `EVENT_CINNABAR_ROCKS_CLEARED` (silent; unseals
Route 19 for the next section).
11. `ROUTE_20`: battle `TrainerSwimmermCameron` at (12,13) - sight radius 3.
Party L34 MARILL. Post: `EVENT_BEAT_SWIMMERM_CAMERON`, +272 money.
12. `ROUTE_20`: optional sign at (37,11). Then step onto warp 1 at (38,7) ->
`SEAFOAM_GYM (6,4)`.
13. `SEAFOAM_GYM`: walk to `SEAFOAMGYM_BLAINE` at (5,2) and talk.
Pre: `ENGINE_VOLCANOBADGE` clear (otherwise you get `.FightDone` only).
Battle: `loadtrainer BLAINE, BLAINE1` - L45 MAGCARGO / L45 MAGMAR / L50 RAPIDASH,
AI holds MAX_POTION + FULL_HEAL.
Post: `EVENT_BEAT_BLAINE` set, `EVENT_SEAFOAM_GYM_GYM_GUIDE` cleared (guide appears
at (6,5)), **`ENGINE_VOLCANOBADGE` set**, +5000 money. No TM, no item.
14. `SEAFOAM_GYM`: optional - talk to `SEAFOAMGYM_GYM_GUIDE` at (6,5).
Post: `EVENT_TALKED_TO_SEAFOAM_GYM_GUIDE_ONCE`.
15. `SEAFOAM_GYM`: exit via warp 1 at (5,5) back to `ROUTE_20` warp 1.
Follow-up (next section): Route 20 continues east past Lori (45,13) and Nicole (52,8)
into `ROUTE_19`, whose rock blocks are now gone because step 10 set
`EVENT_CINNABAR_ROCKS_CLEARED`.
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Kanto map headers, dimensions, connections, warp / coord / bg / object tables | `src/import/RomExtractorGen2.lua` (all 26 map groups, `MAP_GROUP_COUNT = 26` at `:47`, `MapGroupPointers` walk at `:728`) | implemented - fully data-driven, nothing Cinnabar-specific needed |
| `MAPCALLBACK_NEWMAP` (`CinnabarIslandFlypointCallback`, `Route20ClearRocksCallback`) | `src/world/gen2/World.lua:5659` `self:runMapCallback("MAPCALLBACK_NEWMAP")`, in `HandleNewMap` order (`:5646`) | implemented; asserted end-to-end by `tests/drivers/gold_map_callbacks.lua` |
| `MAPCALLBACK_TILES` + `changeblock` (the Route 19 rocks that this section unseals) | `src/world/gen2/World.lua:5662`; `changeblock` opcode at `src/script/gen2/Opcodes.lua:127` | implemented |
| `setflag ENGINE_FLYPOINT_CINNABAR` -> flyable | `src/world/gen2/FieldMoves.lua:367` (`LANDMARK_CINNABAR_ISLAND` / `SPAWN_CINNABAR` / flag 62), `World:setEngineFlag` at `src/world/gen2/World.lua:1326` | implemented |
| `checkevent` / `setevent` / `clearevent`, `checkflag` / `setflag` | `src/script/gen2/Vm.lua:187-229` | implemented - id-keyed stores, `engineFlags` kept separate from `wEventFlags` |
| `appear` / `disappear` writing the object's event flag (Blue vanishing, the guide arriving) | `src/script/gen2/Opcodes.lua:115-116`, handled in `src/script/gen2/Vm.lua` | implemented |
| Hidden item `BGEVENT_ITEM` (Cinnabar RARE_CANDY at 9,1) | `src/world/gen2/HiddenItems.lua` (`BGEVENT_ITEM = 7` at `:22`), extractor branch at `src/import/RomExtractorGen2.lua:2986` | implemented |
| `loadtrainer` / `startbattle` / `winlosstext` / `reloadmapafterbattle` / `endifjustbattled` (the whole Blaine script) | `src/script/gen2/Vm.lua:806`, `:817`, `:886`, `:918` | implemented, incl. the `iftrue` on `wBattleResult` that guards `appear SEAFOAMGYM_GYM_GUIDE` |
| Gym-leader battle music / victory music for BLAINE | `src/battle/gen2/BattleMusic.lua:21`, `src/battle/gen2/Battle.lua:82` (both list `BLAINE = true`) | implemented |
| Prize money = base x last-mon level x 4 (560 / 1360 / 256 / 272 / 5000) | `src/battle/gen2/Prize.lua` (header documents `ComputeTrainerReward` and the `ld c, 4` loop; `Prize.reward`) | implemented, incl. the Mom's-savings split |
| Trainer sight radii (3 / 4 / 1 on Route 21) | `src/world/gen2/Trainers.lua` | implemented |
| SURF gate on FOGBADGE + party move | `src/world/gen2/FieldMoves.lua:106` (`SURF = "FOG"`), `:481` `surfFromMenu`, `:678` overworld path | implemented; driven by `tests/drivers/gold_water_moves.lua` |
| Ocean wild/fish tables for Routes 20/21 | extracted by `src/import/RomExtractorGen2.lua`, consumed by `src/battle/gen2/Encounter.lua` | implemented - correct by construction from the ROM |
| Radio "Place & Name" un-hiding BLAINE after the Kanto badge sweep | `src/ui/gen2/Pokegear.lua:172` (`PNP_HIDDEN`), `:178` `PNP_HIDDEN_BEAT_KANTO`, `:1265` | implemented |
| `setflag ENGINE_VOLCANOBADGE` reaching the trainer card / `VAR_BADGES` count | `src/script/gen2/Vm.lua:208-229` writes `save.engineFlags[53]`; but `VAR_BADGES` (`src/world/gen2/World.lua:1243-1244`) counts `save.player.badges` + `save.player.kantoBadges`, and `src/ui/gen2/TrainerCard.lua:395` reads `player.badges`. Nothing in `src/` ever writes `player.kantoBadges` (only initialised at `src/core/gen2/Save.lua:119`). | **partial** - Blaine's own re-talk guard works (same `engineFlags` id is read back by `checkflag`), but the badge does not show on the trainer card and does not increment `VAR_BADGES` |
| `BadgeTypeBoosts` (VOLCANOBADGE = +12.5% FIRE damage) | no Gen 2 implementation - `grep -rn "badge" src/battle/gen2/` returns nothing; the only badge-boost code is the Gen 1 `src/battle/Damage.lua:35` | **missing** |
| `teleport_from` (`$4c`) movement for Blue's exit | `src/script/gen2/Movement.lua:22` `decodeByte` - `$4c` matches no family branch and falls through to `{ kind = "nop" }` | **partial** - Blue disappears with the sound but without the rise-and-vanish animation |
| End-to-end driver coverage for this stretch | `tests/drivers/gold_*.lua` (25 drivers, all Johto: boot, walk, warp, trainer, ice path, roamers, ...) | **missing** - no Cinnabar / Route 20 / Route 21 / Seafoam Gym driver exists |
## 6. Unresolved / verify by hand
- **Blaine's movesets in the walkthrough are not the pokegold movesets.** The FAQ
describes Magcargo with Yawn / Recover / Overheat, Magmar with Confuse Ray, and
Rapidash with Flare Blitz / Bounce. `data/trainers/parties.asm:2303-2308` gives
MAGCARGO (CURSE, SMOG, FLAMETHROWER, ROCK_SLIDE), MAGMAR (THUNDERPUNCH, FIRE_PUNCH,
SUNNY_DAY, CONFUSE_RAY) and RAPIDASH (QUICK_ATTACK, FIRE_SPIN, FURY_ATTACK,
FIRE_BLAST). Only CONFUSE_RAY overlaps; Overheat, Flare Blitz, Bounce and Yawn do not
exist in `constants/move_constants.asm` at all for this generation. **Species and
levels match exactly** (L45 / L45 / L50), so treat the strategy prose as written
against a different (modernised) build and the asm as authoritative.
- **"Blaine will have two Full Restores available."** `data/trainers/attributes.asm:276`
is `db MAX_POTION, FULL_HEAL`. Two different items, neither of them a Full Restore.
The count of *uses* is AI-driven, not a fixed two.
- **"You now have 15 badges!"** Nothing in the asm counts badges as an ordinal; Kanto
gyms have no enforced order (`ENGINE_VOLCANOBADGE` is one bit,
`constants/engine_flags.asm:53`). The 15 is walkthrough prose that assumes a specific
visiting order.
- **Route 21 wild list is abridged in the walkthrough.** The FAQ lists Tentacool and
Tangela. `data/wild/kanto_grass.asm:1005` also has **MR__MIME** in all three
time slots, and `data/wild/kanto_water.asm:61` also has **TENTACRUEL**. Route 20's
list is likewise incomplete (TENTACRUEL is there too).
- **"Surf south on Route 21 ... head way left then down."** The trainer coordinates
confirm the shape of the route (16 -> 22 -> 30, then far west), but the actual walkable
water path is block data in `maps/Route21.blk` and was not decoded here.
- **"You should see a small cave at the Seafoam Islands."** The only asm anchor is
`warp_event 38, 7` on `maps/Route20.asm:117` plus `bg_event 37, 11` for the gym sign.
The cave mouth graphic itself is block data in `maps/Route20.blk`; there is no
Seafoam Islands dungeon map in Gen 2 (`constants/map_constants.asm` has only
`SEAFOAM_GYM`), so the walkthrough's "Seafoam Islands" is a landmark name
(`data/maps/landmarks.asm:94` `LANDMARK_SEAFOAM_ISLANDS`) rather than a walkable
dungeon.
- **EXP figures.** The per-mon EXP numbers in the FAQ (600 for a L28 Seel, 1056 for a
L28 Dewgong, 1492 for a L34 Tentacruel) are consistent with the vanilla
`base_exp * level / 7` times the 1.5x trainer-battle bonus, but nothing in
`data/trainers/parties.asm` stores them and they still vary with participation, so
they are not reproducible from the asm as written. The **money** figures all reproduce
exactly and are recorded above.
- **`EVENT_BEAT_BLAINE` has no reader.** Grepping `maps/`, `data/`, `engine/`, `home/`
and `constants/` finds it only at its definition (`constants/event_flags.asm:721`) and
its single `setevent` (`maps/SeafoamGym.asm:29`). If the port needs a "Blaine beaten"
predicate for anything, `ENGINE_VOLCANOBADGE` is the flag the cart actually branches on.
@@ -0,0 +1,460 @@
# Section 28 - Routes 19-20 and Viridian City Gym
Source: `../section-28-routes-19-20-and-viridian-city-gym.txt` (the FAQ numbers this chapter "34 > Routes 19-20 and Viridian City Gym")
Maps covered: `MAP_ROUTE_20`, `MAP_ROUTE_19`, `MAP_ROUTE_19_FUCHSIA_GATE`, `MAP_VIRIDIAN_CITY`, `MAP_VIRIDIAN_POKECENTER_1F`, `MAP_VIRIDIAN_GYM`
Badges / key milestones in this section: EARTHBADGE (`ENGINE_EARTHBADGE`) from Blue in Viridian Gym, `EVENT_BEAT_BLUE`, five optional Swimmer battles on Routes 19/20. This is the 16th badge, which is what `maps/OaksLab.asm` reads to open Mt. Silver in a later section.
Map group / id pairs a bot needs (from `constants/map_constants.asm`):
| Map constant | group | id |
|---|---|---|
| `MAP_ROUTE_19_FUCHSIA_GATE` | 6 (`CINNABAR`, `constants/map_constants.asm:175`) | 3 |
| `MAP_ROUTE_19` | 6 | 5 |
| `MAP_ROUTE_20` | 6 | 6 |
| `MAP_VIRIDIAN_CITY` | 23 (`VIRIDIAN`, `constants/map_constants.asm:420`) | 3 |
| `MAP_VIRIDIAN_GYM` | 23 | 4 |
| `MAP_VIRIDIAN_POKECENTER_1F` | 23 | 9 |
Coordinate convention reminder: `warp_event` / `coord_event` / `bg_event` / `object_event` all take `x, y` in map cell coordinates, 0-based from the top-left (`macros/scripts/maps.asm:63`, `:79`, `:97`, `:113`). A map that is `W, H` blocks is `2W, 2H` cells. `changeblock` also takes cell coordinates and halves them internally (`engine/overworld/scripting.asm:2030` `Script_changeblock`, `home/map.asm:2099` `GetBlockLocation`).
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 0 | `MAP_SEAFOAM_GYM` | `maps/SeafoamGym.asm` (previous section) | - | its exit warp back to Route 20 warp 1 | Blaine's gym sits on Route 20; the previous section ends there |
| 1 | `MAP_ROUTE_20` | `maps/Route20.asm` | Seafoam Gym exit -> Route 20 warp 1 `(38, 7)`, or the west connection from Cinnabar Island | east map connection to Route 19 (`data/maps/attributes.asm:285`, offset -9) | "East on Route 20 and to Route 19 are optional ... Surf down and right to face the swimmer in the water" (Lori, then Nicole) |
| 2 | `MAP_ROUTE_19` | `maps/Route19.asm` | west map connection from Route 20 (`data/maps/attributes.asm:289`, offset 9) | Fly (walkthrough), or warp 1 `(7, 3)` -> `ROUTE_19_FUCHSIA_GATE` warp 3, or the north connection to Fuchsia City | "You'll find a drowning guy next to a girl" (Tucker, Dawn, then Jerome). "Fly back to Viridian City instead" |
| 2a | `MAP_ROUTE_19_FUCHSIA_GATE` | `maps/Route19FuchsiaGate.asm` | Route 19 warp 1 | warps 1/2 `(4, 0)` / `(5, 0)` -> `FUCHSIA_CITY` warps 10 / 11 | Only referenced by the walkthrough's "blocked ... south of Fuchsia City" line; see section 6 |
| 3 | `MAP_VIRIDIAN_CITY` | `maps/ViridianCity.asm` | Fly (`ENGINE_FLYPOINT_VIRIDIAN`, spawn `(23, 26)`) | warp 5 `(23, 25)` -> Pokecenter; warp 1 `(32, 7)` -> gym | "fly back to Viridian City ... First, heal at the Pokémon Center" |
| 4 | `MAP_VIRIDIAN_POKECENTER_1F` | `maps/ViridianPokecenter1F.asm` | Viridian warp 5 | warps 1/2 `(3, 7)` / `(4, 7)` -> Viridian warp 5 | Heal before and after Blue |
| 5 | `MAP_VIRIDIAN_GYM` | `maps/ViridianGym.asm` | Viridian warp 1 `(32, 7)` | warps 1/2 `(4, 17)` / `(5, 17)` -> Viridian warp 1 | "GYM LEADER BLUE ... You get: Earth Badge, 5,800G" |
Spillover: the closing line "There are still a few things you can do before you clear Kanto" points at `maps/OaksLab.asm:26` (`readvar VAR_BADGES` / `ifequal NUM_BADGES, .OpenMtSilver`), which belongs to the next section. Nothing past the Viridian Gym exit is described here.
## 2. Maps
### MAP_ROUTE_20
- Script: `maps/Route20.asm`
- Blocks: `maps/Route20.blk` (`data/maps/blocks.asm:118` `Route20_Blocks`)
- Header: `data/maps/maps.asm:197` -> `TILESET_KANTO`, `ROUTE`, `LANDMARK_ROUTE_20`, `MUSIC_ROUTE_3`, phone `FALSE`, `PALETTE_AUTO`, `FISHGROUP_OCEAN`
- Dimensions: `constants/map_constants.asm:181` `map_const ROUTE_20, 30, 9` (30x9 blocks = 60x18 cells)
- Connections: `data/maps/attributes.asm:283-285` - west `CinnabarIsland` (`CINNABAR_ISLAND`, offset 0), east `Route19` (`ROUTE_19`, offset -9). No north/south.
- Map events block: `Route20_MapEvents`, sym `4e:4ecd`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 38 | 7 | `SEAFOAM_GYM` | 1 |
**Coord events** (`def_coord_events`) - none (`def_coord_events` is empty).
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 37 | 11 | `BGEVENT_READ` | `CinnabarGymSign` (sym `4e:4d3a`) -> `CinnabarGymSignText` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE20_SWIMMER_GIRL1` | `SPRITE_SWIMMER_GIRL` | 52 | 8 | `SPRITEMOVEDATA_SPINRANDOM_FAST` (radius 0,0) | `OBJECTTYPE_TRAINER`, sight 3, `PAL_NPC_GREEN` | `TrainerSwimmerfNicole` (sym `4e:4cfe`) | -1 (always present) |
| `ROUTE20_SWIMMER_GIRL2` | `SPRITE_SWIMMER_GIRL` | 45 | 13 | `SPRITEMOVEDATA_SPINRANDOM_FAST` (radius 0,0) | `OBJECTTYPE_TRAINER`, sight 3, `PAL_NPC_GREEN` | `TrainerSwimmerfLori` (sym `4e:4d12`) | -1 |
| `ROUTE20_SWIMMER_GUY` | `SPRITE_SWIMMER_GUY` | 12 | 13 | `SPRITEMOVEDATA_SPINRANDOM_FAST` (radius 0,0) | `OBJECTTYPE_TRAINER`, sight 3, `PAL_NPC_RED` | `TrainerSwimmermCameron` (sym `4e:4d26`) | -1 |
**Scripts of interest**
- `Route20_MapScripts` (`maps/Route20.asm:6`): no scene scripts; one callback, `callback MAPCALLBACK_NEWMAP, Route20ClearRocksCallback`.
- `Route20ClearRocksCallback` (`maps/Route20.asm:12`, sym `4e:4cfa`): two opcodes, `setevent EVENT_CINNABAR_ROCKS_CLEARED` then `endcallback`. This is the single most load-bearing fact on this map: merely *entering* Route 20 for the first time permanently clears the Route 19 boulders. There is no other setter of that flag anywhere in `maps/`.
- The three trainer scripts are the stock pattern: `trainer <CLASS>, <ID>, <EVENT>, SeenText, BeatenText, 0, .Script`, and `.Script` is `endifjustbattled / opentext / writetext <AfterBattleText> / waitbutton / closetext / end`. No items, no flags beyond the trainer's own beat flag.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_CINNABAR_ROCKS_CLEARED` | `constants/event_flags.asm:214` | set by `Route20ClearRocksCallback`; read by `Route19ClearRocksCallback`, `Route19Fisher1Script`, `Route19Fisher2Script`, `Route19FuchsiaGateOfficerScript` | Once set, the six Route 19 boulder blocks are never placed |
| `EVENT_BEAT_SWIMMERF_NICOLE` | `constants/event_flags.asm:487` | `TrainerSwimmerfNicole` | One-time battle |
| `EVENT_BEAT_SWIMMERF_LORI` | `constants/event_flags.asm:488` | `TrainerSwimmerfLori` | One-time battle |
| `EVENT_BEAT_SWIMMERM_CAMERON` | `constants/event_flags.asm:957` | `TrainerSwimmermCameron` | One-time battle; the walkthrough never mentions him |
**Items** - none on this map (the only `bg_event` is a readable sign; no `hiddenitem`, no item balls).
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `SWIMMERF` / `LORI` | 27 (`constants/trainer_constants.asm:375`) | LORI = `SwimmerFGroup` entry 15 | `data/trainers/parties.asm:1781` `SwimmerFGroup` (sym `0e:69c7`), `TRAINERTYPE_NORMAL`: L32 STARMIE, L32 STARMIE | `TrainerSwimmerfLori` | none (no `phone_call` / rematch entry) |
| `SWIMMERF` / `NICOLE` | 27 | NICOLE = `SwimmerFGroup` entry 14 | `TRAINERTYPE_NORMAL`: L29 MARILL, L29 MARILL, L32 LAPRAS | `TrainerSwimmerfNicole` | none |
| `SWIMMERM` / `CAMERON` | 26 (`constants/trainer_constants.asm:352`) | CAMERON = `SwimmerMGroup` entry 17 | `data/trainers/parties.asm:1640` `SwimmerMGroup` (sym `0e:68a8`), `TRAINERTYPE_NORMAL`: L34 MARILL | `TrainerSwimmermCameron` | none |
Prize money: `data/trainers/attributes.asm:227` gives class Swimmerm base reward 2, `:233` gives Swimmerf base reward 5, no held items. `engine/battle/read_trainer_party.asm:300` `ComputeTrainerReward` = base x level of the last mon, and `engine/battle/core.asm:2340-2362` pays that quarter out four times, so the wallet figure is `4 x base x level`: Lori and Nicole `4 x 5 x 32 = 640` (matches the FAQ), Cameron `4 x 2 x 34 = 272`.
**Wild encounters**
- Surfing: `data/wild/kanto_water.asm:54` `def_water_wildmons ROUTE_20`, encounter rate `6 percent`: L35 TENTACOOL, L30 TENTACOOL, L35 TENTACRUEL. No morn/day/nite split (water tables have a single slot list).
- No entry in `data/wild/kanto_grass.asm` (the map has no grass), none in `data/wild/treemons.asm`.
- Fishing: header fish group is `FISHGROUP_OCEAN` (`data/maps/maps.asm:197`), `data/wild/fish.asm` `.Ocean_Old` / `.Ocean_Good` / `.Ocean_Super` (lines 42-55): Old = MAGIKARP/MAGIKARP/TENTACOOL L10; Good = MAGIKARP L20, TENTACOOL L20, CHINCHOU L20, time group 2; Super = CHINCHOU L40, time group 3, TENTACRUEL L40, LANTURN L40.
### MAP_ROUTE_19
- Script: `maps/Route19.asm`
- Blocks: `maps/Route19.blk` (`data/maps/blocks.asm:48` `Route19_Blocks`)
- Header: `data/maps/maps.asm:196` -> `TILESET_KANTO`, `ROUTE`, `LANDMARK_ROUTE_19`, `MUSIC_ROUTE_3`, phone `FALSE`, `PALETTE_AUTO`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:180` `map_const ROUTE_19, 10, 18` (10x18 blocks = 20x36 cells)
- Connections: `data/maps/attributes.asm:287-289` - north `FuchsiaCity` (`FUCHSIA_CITY`, offset 0), west `Route20` (`ROUTE_20`, offset 9). No south/east.
- Map events block: `Route19_MapEvents`, sym `4e:535c`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 7 | 3 | `ROUTE_19_FUCHSIA_GATE` | 3 |
**Coord events** (`def_coord_events`) - none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 11 | 13 | `BGEVENT_READ` | `Route19Sign` (sym `4e:4fa0`) -> `Route19SignText` |
| 11 | 1 | `BGEVENT_READ` | `CarefulSwimmingSign` (sym `4e:4fa3`) -> `CarefulSwimmingSignText` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE19_SWIMMER_GIRL` | `SPRITE_SWIMMER_GIRL` | 9 | 23 | `SPRITEMOVEDATA_STANDING_LEFT` (radius 0,0) | `OBJECTTYPE_TRAINER`, sight **0**, `PAL_NPC_GREEN` | `TrainerSwimmerfDawn` (sym `4e:4f28`) | -1 |
| `ROUTE19_SWIMMER_GUY1` | `SPRITE_SWIMMER_GUY` | 13 | 28 | `SPRITEMOVEDATA_SPINRANDOM_FAST` (radius 0,0) | `OBJECTTYPE_TRAINER`, sight 3, `PAL_NPC_RED` | `TrainerSwimmermHarold` (sym `4e:4f3c`) | -1 |
| `ROUTE19_SWIMMER_GUY2` | `SPRITE_SWIMMER_GUY` | 11 | 17 | `SPRITEMOVEDATA_SPINRANDOM_FAST` (radius 0,0) | `OBJECTTYPE_TRAINER`, sight 3, `PAL_NPC_RED` | `TrainerSwimmermJerome` (sym `4e:4f50`) | -1 |
| `ROUTE19_SWIMMER_GUY3` | `SPRITE_SWIMMER_GUY` | 8 | 23 | `SPRITEMOVEDATA_STANDING_UP` (radius 0,0) | `OBJECTTYPE_TRAINER`, sight **0**, `PAL_NPC_RED` | `TrainerSwimmermTucker` (sym `4e:4f64`) | -1 |
| `ROUTE19_FISHER1` | `SPRITE_FISHER` | 9 | 5 | `SPRITEMOVEDATA_STANDING_DOWN` (radius 0,0) | `OBJECTTYPE_SCRIPT` (sight field 1, unused) | `Route19Fisher1Script` (sym `4e:4f78`) | -1 |
| `ROUTE19_FISHER2` | `SPRITE_FISHER` | 11 | 5 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (radius 1,0) | `OBJECTTYPE_SCRIPT` (sight field 1, unused) | `Route19Fisher2Script` (sym `4e:4f8c`) | -1 |
Note for a bot: Tucker `(8, 23)` and Dawn `(9, 23)` both have sight range 0, so they never initiate a battle by line of sight. They must be talked to, which is exactly the walkthrough's "Talk to the guy for a battle" / "talk to the girl to battle her". Harold and Jerome have sight 3 and will pull the player in.
**Scripts of interest**
- `Route19_MapScripts` (`maps/Route19.asm:9`): no scene scripts; one callback, `callback MAPCALLBACK_TILES, Route19ClearRocksCallback`.
- `Route19ClearRocksCallback` (`maps/Route19.asm:15`, sym `4e:4f09`): `checkevent EVENT_CINNABAR_ROCKS_CLEARED` / `iftrue .Done`, otherwise six `changeblock` calls writing block `$7a` (a rock) at cells `(6, 6)`, `(8, 6)`, `(10, 6)`, `(12, 8)`, `(4, 8)`, `(10, 10)` - i.e. blocks `(3, 3)`, `(4, 3)`, `(5, 3)`, `(6, 4)`, `(2, 4)`, `(5, 5)`. Block `$7a` in `data/tilesets/kanto_collision.asm:123` is `tilecoll WALL, WALL, WALL, WALL`, so all four quadrants are solid; these are impassable terrain, not Strength boulders. Because this is `MAPCALLBACK_TILES` it re-runs on every map load, and because `EVENT_CINNABAR_ROCKS_CLEARED` is set by simply walking onto Route 20, by the time this section runs the rocks are already gone.
- `Route19Fisher1Script` / `Route19Fisher2Script` (`maps/Route19.asm:71` / `:87`): `faceplayer / opentext / checkevent EVENT_CINNABAR_ROCKS_CLEARED / iftrue .RocksCleared`. Flavor only, no flags written, no items.
- The four trainer scripts follow the same `endifjustbattled` pattern as Route 20's.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_CINNABAR_ROCKS_CLEARED` | `constants/event_flags.asm:214` | read by `Route19ClearRocksCallback` and both Fisher scripts | Gate on whether the north half of Route 19 is walkable |
| `EVENT_BEAT_SWIMMERF_DAWN` | `constants/event_flags.asm:485` | `TrainerSwimmerfDawn` | One-time battle |
| `EVENT_BEAT_SWIMMERM_HAROLD` | `constants/event_flags.asm:941` | `TrainerSwimmermHarold` | One-time battle; not mentioned by the walkthrough |
| `EVENT_BEAT_SWIMMERM_JEROME` | `constants/event_flags.asm:954` | `TrainerSwimmermJerome` | One-time battle |
| `EVENT_BEAT_SWIMMERM_TUCKER` | `constants/event_flags.asm:955` | `TrainerSwimmermTucker` | One-time battle |
**Items** - none. Both `bg_event`s are `BGEVENT_READ` signs; there is no `hiddenitem` and no item ball on this map.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `SWIMMERM` / `TUCKER` | 26 | `SwimmerMGroup` entry 15 | `TRAINERTYPE_NORMAL`: L30 SHELLDER, L34 CLOYSTER | `TrainerSwimmermTucker` | none |
| `SWIMMERF` / `DAWN` | 27 | `SwimmerFGroup` entry 12 | `TRAINERTYPE_NORMAL`: L34 SEAKING | `TrainerSwimmerfDawn` | none |
| `SWIMMERM` / `JEROME` | 26 | `SwimmerMGroup` entry 14 | `TRAINERTYPE_NORMAL`: L26 SEADRA, L28 TENTACOOL, L30 TENTACRUEL, L28 GOLDEEN | `TrainerSwimmermJerome` | none |
| `SWIMMERM` / `HAROLD` | 26 | `SwimmerMGroup` entry 1 | `TRAINERTYPE_NORMAL`: L32 REMORAID, L30 SEADRA | `TrainerSwimmermHarold` | none |
Prize money using the same `4 x base x last-mon level` rule: Tucker `4 x 2 x 34 = 272`, Dawn `4 x 5 x 34 = 680`, Jerome `4 x 2 x 28 = 224`, Harold `4 x 2 x 30 = 240`. The first three match the FAQ exactly.
**Wild encounters**
- Surfing: `data/wild/kanto_water.asm:47` `def_water_wildmons ROUTE_19`, encounter rate `6 percent`: L35 TENTACOOL, L30 TENTACOOL, L35 TENTACRUEL.
- No grass table, no headbutt table.
- Fishing: `FISHGROUP_SHORE` (`data/maps/maps.asm:196`), `data/wild/fish.asm` `.Shore_Old` / `.Shore_Good` / `.Shore_Super` (lines 28-40): Old = MAGIKARP/MAGIKARP/KRABBY L10; Good = MAGIKARP L20, KRABBY L20 (x2), time group 0; Super = KRABBY L40, time group 1, KRABBY L40, KINGLER L40.
### MAP_ROUTE_19_FUCHSIA_GATE
Included because the walkthrough's "blocked by those boulders south of Fuchsia City" line points at it. The walkthrough never actually enters it.
- Script: `maps/Route19FuchsiaGate.asm`
- Blocks: `maps/Route19FuchsiaGate.blk` (`data/maps/blocks.asm:204` `Route19FuchsiaGate_Blocks`)
- Header: `data/maps/maps.asm:194` -> `TILESET_GATE`, `GATE`, `LANDMARK_ROUTE_19`, `MUSIC_ROUTE_3`, phone `FALSE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:178` `map_const ROUTE_19_FUCHSIA_GATE, 5, 4` (10x8 cells)
- Connections: none (`data/maps/attributes.asm:512` has no connection rows)
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 0 | `FUCHSIA_CITY` | 10 |
| 2 | 5 | 0 | `FUCHSIA_CITY` | 11 |
| 3 | 4 | 7 | `ROUTE_19` | 1 |
| 4 | 5 | 7 | `ROUTE_19` | 1 |
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE19FUCHSIAGATE_OFFICER` | `SPRITE_OFFICER` | 0 | 4 | `SPRITEMOVEDATA_STANDING_RIGHT` (radius 0,0) | `OBJECTTYPE_SCRIPT`, `PAL_NPC_BLUE` | `Route19FuchsiaGateOfficerScript` | -1 |
**Scripts of interest**
- `Route19FuchsiaGateOfficerScript` (`maps/Route19FuchsiaGate.asm:9`): `faceplayer / opentext / checkevent EVENT_CINNABAR_ROCKS_CLEARED / iftrue .RocksCleared`, then one of two texts. He is at `(0, 4)`, off to the side of the `(4, ...)` / `(5, ...)` walkway, so he blocks nothing. There is no coord event and no movement-blocking script in this gate: the officer is flavor, and the actual road closure is the `changeblock` rocks on Route 19 itself.
### MAP_VIRIDIAN_CITY
- Script: `maps/ViridianCity.asm`
- Blocks: `maps/ViridianCity.blk` (`data/maps/blocks.asm:235` `ViridianCity_Blocks`)
- Header: `data/maps/maps.asm:457` -> `TILESET_KANTO`, `TOWN`, `LANDMARK_VIRIDIAN_CITY`, `MUSIC_VIRIDIAN_CITY`, phone `FALSE`, `PALETTE_AUTO`, `FISHGROUP_POND`
- Dimensions: `constants/map_constants.asm:423` `map_const VIRIDIAN_CITY, 20, 18` (40x36 cells)
- Connections: `data/maps/attributes.asm:259-261` - north `Route2` (`ROUTE_2`, offset 5), south `Route1` (`ROUTE_1`, offset 10), west `Route22` (`ROUTE_22`, offset 4). No east.
- Map events block: `ViridianCity_MapEvents`, sym `4e:4486`
- Fly target: `data/maps/flypoints.asm:19` `db LANDMARK_VIRIDIAN_CITY, SPAWN_VIRIDIAN`; `data/maps/spawn_points.asm:15` `spawn VIRIDIAN_CITY, 23, 26` (the Pokecenter doorstep)
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 32 | 7 | `VIRIDIAN_GYM` | 1 |
| 2 | 21 | 9 | `VIRIDIAN_NICKNAME_SPEECH_HOUSE` | 1 |
| 3 | 23 | 15 | `TRAINER_HOUSE_1F` | 1 |
| 4 | 29 | 19 | `VIRIDIAN_MART` | 2 |
| 5 | 23 | 25 | `VIRIDIAN_POKECENTER_1F` | 1 |
**Coord events** (`def_coord_events`) - none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 17 | 17 | `BGEVENT_READ` | `ViridianCitySign` |
| 27 | 7 | `BGEVENT_READ` | `ViridianGymSign` (sym `4e:4053`) - "LEADER: ... The rest of the text is illegible" |
| 19 | 1 | `BGEVENT_READ` | `ViridianCityWelcomeSign` |
| 21 | 15 | `BGEVENT_READ` | `TrainerHouseSign` |
| 24 | 25 | `BGEVENT_READ` | `ViridianCityPokecenterSign` (`jumpstd PokecenterSignScript`) |
| 30 | 19 | `BGEVENT_READ` | `ViridianCityMartSign` (`jumpstd MartSignScript`) |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `VIRIDIANCITY_GRAMPS1` | `SPRITE_GRAMPS` | 18 | 5 | `SPRITEMOVEDATA_WANDER` (radius 2,2) | `OBJECTTYPE_SCRIPT` | `ViridianCityCoffeeGramps` | -1 |
| `VIRIDIANCITY_GRAMPS2` | `SPRITE_GRAMPS` | 30 | 8 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT`, `PAL_NPC_BLUE` | `ViridianCityGrampsNearGym` (sym `4e:401e`) | -1 |
| `VIRIDIANCITY_FISHER` | `SPRITE_FISHER` | 6 | 23 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT`, `PAL_NPC_RED` | `ViridianCityDreamEaterFisher` (sym `4e:4032`) | -1 |
| `VIRIDIANCITY_YOUNGSTER` | `SPRITE_YOUNGSTER` | 17 | 21 | `SPRITEMOVEDATA_WANDER` (radius 3,3) | `OBJECTTYPE_SCRIPT`, `PAL_NPC_GREEN` | `ViridianCityYoungsterScript` | -1 |
**Scripts of interest**
- `ViridianCityFlypointCallback` (`maps/ViridianCity.asm:13`, sym `4e:4005`), hooked as `callback MAPCALLBACK_NEWMAP`: `setflag ENGINE_FLYPOINT_VIRIDIAN / endcallback`. A bot cannot Fly here until it has physically entered Viridian City once.
- `ViridianCityGrampsNearGym` (`maps/ViridianCity.asm:34`): `checkevent EVENT_BLUE_IN_CINNABAR`; if true, the "Are you going to battle the LEADER?" line. This is a free read-out of whether Blue has been moved into the gym - see the gate table.
- `ViridianCityDreamEaterFisher` (`maps/ViridianCity.asm:50`): `checkevent EVENT_GOT_TM42_DREAM_EATER / iftrue .GotDreamEater`, else `writetext ... / promptbutton / verbosegiveitem TM_DREAM_EATER / iffalse .NoRoomForDreamEater / setevent EVENT_GOT_TM42_DREAM_EATER`. The walkthrough never mentions this; it is a free TM on the map the walkthrough tells you to fly to.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_FLYPOINT_VIRIDIAN` | `constants/engine_flags.asm:68` | set by `ViridianCityFlypointCallback` | Precondition for the walkthrough's "fly back to Viridian City" |
| `EVENT_BLUE_IN_CINNABAR` | `constants/event_flags.asm:1303` | read by `ViridianCityGrampsNearGym` and `ViridianPokecenter1FCooltrainerMScript`; it is also `CINNABARISLAND_BLUE`'s object event flag | While clear, Blue is standing on Cinnabar Island and has not yet moved to the gym |
| `EVENT_GOT_TM42_DREAM_EATER` | `constants/event_flags.asm:223` | `ViridianCityDreamEaterFisher` | One-time TM42 |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `TM_DREAM_EATER` (TM42) | Talk to `VIRIDIANCITY_FISHER` at `(6, 23)` | `ViridianCityDreamEaterFisher` `verbosegiveitem` | `EVENT_GOT_TM42_DREAM_EATER` |
**Trainers** - none on the overworld map.
**Wild encounters** - no `kanto_grass.asm` or `kanto_water.asm` entry for `VIRIDIAN_CITY`. Fishing group is `FISHGROUP_POND` (`data/maps/maps.asm:457`).
### MAP_VIRIDIAN_POKECENTER_1F
- Script: `maps/ViridianPokecenter1F.asm`
- Blocks: `data/maps/blocks.asm:361` `ViridianPokecenter1F_Blocks`
- Header: `data/maps/maps.asm:463` -> `TILESET_POKECENTER`, `INDOOR`, `LANDMARK_VIRIDIAN_CITY`, `MUSIC_POKEMON_CENTER`, phone `FALSE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:429` `map_const VIRIDIAN_POKECENTER_1F, 5, 4` (10x8 cells)
- Connections: none
- Respawn point: `data/maps/spawn_points.asm:12` `spawn VIRIDIAN_POKECENTER_1F, 5, 3`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `VIRIDIAN_CITY` | 5 |
| 2 | 4 | 7 | `VIRIDIAN_CITY` | 5 |
| 3 | 0 | 7 | `POKECENTER_2F` | 1 |
**Coord events** - none. **BG events** - none.
**Object events**
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `VIRIDIANPOKECENTER1F_NURSE` | `SPRITE_NURSE` | 3 | 1 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `ViridianPokecenter1FNurseScript` | -1 |
| `VIRIDIANPOKECENTER1F_COOLTRAINER_M` | `SPRITE_COOLTRAINER_M` | 8 | 4 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (radius 1,0) | `OBJECTTYPE_SCRIPT`, `PAL_NPC_RED` | `ViridianPokecenter1FCooltrainerMScript` | -1 |
| `VIRIDIANPOKECENTER1F_COOLTRAINER_F` | `SPRITE_COOLTRAINER_F` | 5 | 3 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_SCRIPT`, `PAL_NPC_BLUE` | `ViridianPokecenter1FCooltrainerFScript` | -1 |
| `VIRIDIANPOKECENTER1F_BUG_CATCHER` | `SPRITE_BUG_CATCHER` | 1 | 6 | `SPRITEMOVEDATA_STANDING_RIGHT` | `OBJECTTYPE_SCRIPT`, `PAL_NPC_GREEN` | `ViridianPokecenter1FBugCatcherScript` | -1 |
**Scripts of interest**
- `ViridianPokecenter1FNurseScript` (`maps/ViridianPokecenter1F.asm:12`): `jumpstd PokecenterNurseScript` (`engine/events/std_scripts.asm:54`). This is the "heal at the Pokémon Center" beat, both before and after Blue.
- `ViridianPokecenter1FCooltrainerMScript` (`maps/ViridianPokecenter1F.asm:15`): another `checkevent EVENT_BLUE_IN_CINNABAR` read-out ("There are no GYM TRAINERS at the VIRIDIAN GYM"), which corroborates the walkthrough's "There aren't any other trainers in this gym".
### MAP_VIRIDIAN_GYM
- Script: `maps/ViridianGym.asm`
- Blocks: `maps/ViridianGym.blk` (`data/maps/blocks.asm:877` `ViridianGym_Blocks`)
- Header: `data/maps/maps.asm:458` -> `TILESET_TRAIN_STATION`, `INDOOR`, `LANDMARK_VIRIDIAN_CITY`, `MUSIC_GYM`, phone `TRUE`, `PALETTE_DAY`, `FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:424` `map_const VIRIDIAN_GYM, 5, 9` (10x18 cells)
- Connections: none (`data/maps/attributes.asm:657`)
- Map events block: `ViridianGym_MapEvents`, sym `5f:43de`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 17 | `VIRIDIAN_CITY` | 1 |
| 2 | 5 | 17 | `VIRIDIAN_CITY` | 1 |
**Coord events** (`def_coord_events`) - none. There is no trip-wire in this gym; Blue is a pure talk-to trigger.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 3 | 13 | `BGEVENT_READ` | `ViridianGymStatue` (sym `5f:4047`) |
| 6 | 13 | `BGEVENT_READ` | `ViridianGymStatue` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `VIRIDIANGYM_BLUE` | `SPRITE_BLUE` | 5 | 3 | `SPRITEMOVEDATA_STANDING_DOWN` (radius 0,0) | `OBJECTTYPE_SCRIPT`, palette 0 | `ViridianGymBlueScript` (sym `5f:4002`) | `EVENT_VIRIDIAN_GYM_BLUE` |
| `VIRIDIANGYM_GYM_GUIDE` | `SPRITE_GYM_GUIDE` | 7 | 13 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT`, `PAL_NPC_BLUE` | `ViridianGymGuideScript` (sym `5f:4033`) | `EVENT_VIRIDIAN_GYM_BLUE` |
Both objects carry `EVENT_VIRIDIAN_GYM_BLUE` as their event flag, so while that flag is **set** the gym is completely empty. `EVENT_VIRIDIAN_GYM_BLUE` is set by `InitializeEventsScript` at new game (`engine/events/std_scripts.asm:438`, the `setevent` on line 552) and is cleared only by `CinnabarIslandBlue`.
**Scripts of interest**
- `ViridianGymBlueScript` (`maps/ViridianGym.asm:10`, sym `5f:4002`), the whole badge beat, in order:
1. `faceplayer` / `opentext`
2. `checkflag ENGINE_EARTHBADGE` / `iftrue .FightDone` (sym `5f:402d`) - repeat visits get `LeaderBlueEpilogueText` only
3. `writetext LeaderBlueBeforeText` / `waitbutton` / `closetext`
4. `winlosstext LeaderBlueWinText, 0` (loss text pointer 0 = use the default)
5. `loadtrainer BLUE, BLUE1` / `startbattle` / `reloadmapafterbattle`
6. `setevent EVENT_BEAT_BLUE`
7. `opentext` / `writetext Text_ReceivedEarthBadge` / `playsound SFX_GET_BADGE` / `waitsfx`
8. `setflag ENGINE_EARTHBADGE`
9. `writetext LeaderBlueAfterText` / `waitbutton` / `closetext` / `end`
No `verbosegiveitem`: unlike Johto leaders, the Kanto leaders in GSC hand out no TM. The badge itself is the only reward besides money.
- `ViridianGymGuideScript` (`maps/ViridianGym.asm:39`, sym `5f:4033`): `checkevent EVENT_BEAT_BLUE / iftrue .ViridianGymGuideWinScript`. Pure flavor, no `learnmove`, no items.
- `ViridianGymStatue` (`maps/ViridianGym.asm:55`, sym `5f:4047`): `checkflag ENGINE_EARTHBADGE / iftrue .Beaten`; before the badge, `jumpstd GymStatue1Script` (`engine/events/std_scripts.asm:638`), after it `gettrainername STRING_BUFFER_4, BLUE, BLUE1` then `jumpstd GymStatue2Script` (`:646`). A cheap read-back a bot can use to confirm the badge landed.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_EARTHBADGE` | `constants/engine_flags.asm:54` (in `wKantoBadges`) | read by `ViridianGymBlueScript` and `ViridianGymStatue`; set by `ViridianGymBlueScript` | The 16th badge. Post-condition of this section |
| `EVENT_BEAT_BLUE` | `constants/event_flags.asm:722` | set by `ViridianGymBlueScript`, read by `ViridianGymGuideScript` | Set *before* the badge flag, between `reloadmapafterbattle` and `playsound` |
| `EVENT_VIRIDIAN_GYM_BLUE` | `constants/event_flags.asm:1304` | set by `InitializeEventsScript` (`engine/events/std_scripts.asm:552`); cleared by `CinnabarIslandBlue` (`maps/CinnabarIsland.asm:23`) | While set, both gym objects are absent. This is the real gate on the whole section |
| `EVENT_BLUE_IN_CINNABAR` | `constants/event_flags.asm:1303` | object flag of `CINNABARISLAND_BLUE` (`maps/CinnabarIsland.asm:143`), set by that script's `disappear` | Once set, Blue is gone from Cinnabar and (necessarily) present in the gym |
**Items** - none. No item balls, no `hiddenitem`, no TM reward.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `BLUE` / `BLUE1` | 40 (`constants/trainer_constants.asm:640`) | `BLUE1` (`:641`) | `data/trainers/parties.asm:3154` `BlueGroup` (sym `0e:75ff`), `TRAINERTYPE_MOVES` | `ViridianGymBlueScript` | none (no phone entry; the map header's phone column is `TRUE` but no `phone_call` targets him) |
`BlueGroup` party, verbatim:
| # | level | species | moves |
|---|---|---|---|
| 1 | 56 | PIDGEOT | QUICK_ATTACK, WHIRLWIND, WING_ATTACK, MIRROR_MOVE |
| 2 | 54 | ALAKAZAM | DISABLE, RECOVER, PSYCHIC_M, REFLECT |
| 3 | 56 | RHYDON | FURY_ATTACK, SANDSTORM, ROCK_SLIDE, EARTHQUAKE |
| 4 | 58 | GYARADOS | TWISTER, HYDRO_PUMP, RAIN_DANCE, HYPER_BEAM |
| 5 | 58 | EXEGGUTOR | SUNNY_DAY, LEECH_SEED, EGG_BOMB, SOLARBEAM |
| 6 | 58 | ARCANINE | ROAR, SWIFT, FLAMETHROWER, EXTREMESPEED |
Class attributes (`data/trainers/attributes.asm:383`): items `FULL_RESTORE, FULL_RESTORE` (the AI will use two Full Restores), base reward 25, AI `AI_BASIC | AI_SETUP | AI_SMART | AI_AGGRESSIVE | AI_CAUTIOUS | AI_STATUS | AI_RISKY`, `CONTEXT_USE | SWITCH_SOMETIMES`. Payout `4 x 25 x 58 = 5800`, matching the FAQ. `BLUE` is listed in `KantoGymLeaders` (`data/trainers/leaders.asm:31`), which is what selects the gym-leader battle/victory music and awards `HAPPINESS_GYMBATTLE`.
Walkthrough claims worth flagging against the party data: Pidgeot's flying move is `WING_ATTACK`, not Air Slash (which does not exist in Gen 2); Exeggutor's moves are `SUNNY_DAY, LEECH_SEED, EGG_BOMB, SOLARBEAM`, so it has neither Psychic nor Hypnosis; Arcanine's switch move is `ROAR`, Pidgeot's is `WHIRLWIND`.
**Wild encounters** - none (indoor map).
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Reaching Routes 19/20 at all | `engine/events/overworld.asm:322` `SurfFunction` -> `.TrySurf` (`ld de, ENGINE_FOGBADGE / call CheckBadge`), and the tile-facing path `engine/events/overworld.asm:469` `TrySurfOW` (`ENGINE_FOGBADGE` via `CheckEngineFlag`, then `CheckPartyMove` for `SURF`) | FOGBADGE + a party member knowing SURF | Both routes are open water; without Surf there is no approach |
| Boulders across northern Route 19 | `maps/Route19.asm:15` `Route19ClearRocksCallback`, six `changeblock ..., $7a` under `checkevent EVENT_CINNABAR_ROCKS_CLEARED`; block `$7a` is `WALL` on all four quadrants (`data/tilesets/kanto_collision.asm:123`) | `EVENT_CINNABAR_ROCKS_CLEARED` | Set unconditionally by `maps/Route20.asm:12` `Route20ClearRocksCallback` on `MAPCALLBACK_NEWMAP`, i.e. by setting foot on Route 20 |
| Blue is not in the Viridian Gym | Object event flag `EVENT_VIRIDIAN_GYM_BLUE` on both `maps/ViridianGym.asm:183-184` objects; set at new game by `engine/events/std_scripts.asm:552` inside `InitializeEventsScript` | Flag must be **clear** | `maps/CinnabarIsland.asm:14` `CinnabarIslandBlue`: talk to Blue on Cinnabar Island; the script runs `disappear CINNABARISLAND_BLUE` (setting `EVENT_BLUE_IN_CINNABAR`) then `clearevent EVENT_VIRIDIAN_GYM_BLUE`. This happens in an earlier section, but a bot that skipped it will find an empty gym |
| Flying to Viridian City | `maps/ViridianCity.asm:13` `ViridianCityFlypointCallback` (`setflag ENGINE_FLYPOINT_VIRIDIAN`), plus `engine/events/overworld.asm:529` `FlyFunction` (`ld de, ENGINE_STORMBADGE`) and the landmark row `data/maps/flypoints.asm:19` | STORMBADGE + a party member knowing FLY + Viridian visited once on foot | Walk into Viridian City once (Route 1 north, Route 2 south, or Route 22 east). Otherwise reach the gym overland |
| Re-fighting Blue | `maps/ViridianGym.asm:13` `checkflag ENGINE_EARTHBADGE / iftrue .FightDone` | - | Not possible; the battle is one-shot and there is no rematch or phone entry |
| (Next section) Mt. Silver | `maps/OaksLab.asm:26` `readvar VAR_BADGES / ifequal NUM_BADGES, .OpenMtSilver` -> `setevent EVENT_OPENED_MT_SILVER` | All 16 badges, EARTHBADGE last | Beating Blue here is what satisfies it |
Note on the Route 19 approach: the officer in `maps/Route19FuchsiaGate.asm` never blocks a tile, and `maps/Route19.asm` has no coord events. There is no NPC blocker anywhere in this section - every obstacle is either terrain (`changeblock`) or a hidden object (`EVENT_VIRIDIAN_GYM_BLUE`).
## 4. Bot checklist
Preconditions for the whole section: `ENGINE_FOGBADGE` + SURF in party, `EVENT_VIRIDIAN_GYM_BLUE` clear, `ENGINE_FLYPOINT_VIRIDIAN` set (or an overland plan to Viridian).
| # | Map | Target | Input intent | Precondition | Postcondition |
|---|---|---|---|---|---|
| 1 | `MAP_ROUTE_20` | anywhere on the map | enter the map (warp from Seafoam Gym warp 1 at `(38, 7)`, or the Cinnabar Island east connection) | - | `EVENT_CINNABAR_ROCKS_CLEARED` set by `Route20ClearRocksCallback` |
| 2 | `MAP_ROUTE_20` | `ROUTE20_SWIMMER_GIRL2` at `(45, 13)` | surf into her sight cone (range 3) or talk | `EVENT_BEAT_SWIMMERF_LORI` clear | battle Lori (2x L32 STARMIE); `EVENT_BEAT_SWIMMERF_LORI` set, +640 |
| 3 | `MAP_ROUTE_20` | `ROUTE20_SWIMMER_GIRL1` at `(52, 8)` | surf into sight (range 3) or talk | `EVENT_BEAT_SWIMMERF_NICOLE` clear | battle Nicole (L29 MARILL, L29 MARILL, L32 LAPRAS); flag set, +640 |
| 3b | `MAP_ROUTE_20` | `ROUTE20_SWIMMER_GUY` at `(12, 13)` | optional, west end, skipped by the FAQ | `EVENT_BEAT_SWIMMERM_CAMERON` clear | battle Cameron (L34 MARILL); flag set, +272 |
| 4 | `MAP_ROUTE_20` -> `MAP_ROUTE_19` | east map edge | surf east across the connection (offset -9) | - | now on `MAP_ROUTE_19` |
| 5 | `MAP_ROUTE_19` | `ROUTE19_SWIMMER_GUY3` at `(8, 23)` | **talk** (sight 0, will not initiate) | `EVENT_BEAT_SWIMMERM_TUCKER` clear | battle Tucker (L30 SHELLDER, L34 CLOYSTER); flag set, +272 |
| 6 | `MAP_ROUTE_19` | `ROUTE19_SWIMMER_GUY3` again | talk | `EVENT_BEAT_SWIMMERM_TUCKER` set | `SwimmermTuckerAfterBattleText` (the "drowning" line) |
| 7 | `MAP_ROUTE_19` | `ROUTE19_SWIMMER_GIRL` at `(9, 23)` | **talk** (sight 0) | `EVENT_BEAT_SWIMMERF_DAWN` clear | battle Dawn (L34 SEAKING); flag set, +680 |
| 8 | `MAP_ROUTE_19` | `ROUTE19_SWIMMER_GUY2` at `(11, 17)` | surf north into sight (range 3) or talk | `EVENT_BEAT_SWIMMERM_JEROME` clear | battle Jerome (L26 SEADRA, L28 TENTACOOL, L30 TENTACRUEL, L28 GOLDEEN); flag set, +224 |
| 8b | `MAP_ROUTE_19` | `ROUTE19_SWIMMER_GUY1` at `(13, 28)` | optional, south-east, not in the FAQ | `EVENT_BEAT_SWIMMERM_HAROLD` clear | battle Harold (L32 REMORAID, L30 SEADRA); flag set, +240 |
| 9 | `MAP_ROUTE_19` -> `MAP_VIRIDIAN_CITY` | Pokegear map -> Fly -> Viridian | use FLY | `ENGINE_STORMBADGE`, FLY in party, `ENGINE_FLYPOINT_VIRIDIAN` | player at `VIRIDIAN_CITY (23, 26)` (`data/maps/spawn_points.asm:15`) |
| 10 | `MAP_VIRIDIAN_CITY` | warp 5 at `(23, 25)` | step onto the warp | - | in `MAP_VIRIDIAN_POKECENTER_1F` |
| 11 | `MAP_VIRIDIAN_POKECENTER_1F` | `VIRIDIANPOKECENTER1F_NURSE` at `(3, 1)` | talk, accept heal | - | party healed (`PokecenterNurseScript`) |
| 11b | `MAP_VIRIDIAN_CITY` | `VIRIDIANCITY_FISHER` at `(6, 23)` | talk (optional, not in the FAQ) | `EVENT_GOT_TM42_DREAM_EATER` clear, bag space | receive `TM_DREAM_EATER`; flag set |
| 12 | `MAP_VIRIDIAN_CITY` | warp 1 at `(32, 7)` | step onto the warp | - | in `MAP_VIRIDIAN_GYM` at `(4, 17)` / `(5, 17)` |
| 13 | `MAP_VIRIDIAN_GYM` | `VIRIDIANGYM_GYM_GUIDE` at `(7, 13)` | talk (optional) | `EVENT_VIRIDIAN_GYM_BLUE` clear | advice text only |
| 14 | `MAP_VIRIDIAN_GYM` | save | save the game (the FAQ says so explicitly) | - | - |
| 15 | `MAP_VIRIDIAN_GYM` | `VIRIDIANGYM_BLUE` at `(5, 3)`, approach from below | talk | `EVENT_VIRIDIAN_GYM_BLUE` clear, `ENGINE_EARTHBADGE` clear | battle `BLUE / BLUE1` (6 mons, L54-58, 2 Full Restores); on win `EVENT_BEAT_BLUE` set, `ENGINE_EARTHBADGE` set, +5800 |
| 16 | `MAP_VIRIDIAN_GYM` | `VIRIDIANGYM_BLUE` again | talk | `ENGINE_EARTHBADGE` set | `LeaderBlueEpilogueText` ("You'd better not lose until I beat you") |
| 17 | `MAP_VIRIDIAN_GYM` | `bg_event (3, 13)` or `(6, 13)` | read the statue | `ENGINE_EARTHBADGE` set | `GymStatue2Script` with Blue's name - a cheap badge assertion |
| 18 | `MAP_VIRIDIAN_GYM` -> `MAP_VIRIDIAN_CITY` -> Pokecenter | warp 1/2 `(4, 17)` / `(5, 17)`, then Viridian warp 5 | walk, warp, heal | - | 16 badges, section complete |
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Map header, blocks, warps, coord/bg/object events for all six maps | `src/import/RomExtractorGen2.lua:782` `readMapEvents`, `:899` (map assembly) | implemented - the tables are read out of the ROM generically, so no per-map data is hand-written |
| `MAPCALLBACK_NEWMAP` / `MAPCALLBACK_TILES` dispatch (needed for both rock callbacks and the Viridian fly point) | `src/world/gen2/World.lua:5659`, `:5664` | implemented |
| `changeblock` (Route 19 boulders) | `src/script/gen2/Vm.lua:1002` (halves x/y to block coords, calls `changeBlockFn`) | implemented |
| `setevent` / `clearevent` / `checkevent`, object-event hide flags | `src/world/gen2/Events.lua`, `src/script/gen2/Vm.lua` | implemented |
| `setflag` / `checkflag` on `ENGINE_*` (EARTHBADGE, FLYPOINT_VIRIDIAN) | `src/script/gen2/Vm.lua:212-223`, `:1584` | implemented |
| Surf field move + FOGBADGE check | `src/world/gen2/FieldMoves.lua:101` (badge table), `:482` (`SurfFunction` port) | implemented |
| Fly field move + STORMBADGE + flypoint gating | `src/world/gen2/FieldMoves.lua:501-505` | implemented |
| Trainer sight cones / `OBJECTTYPE_TRAINER` (sight 3 swimmers, sight 0 Tucker and Dawn) | `src/world/gen2/Trainers.lua:98` `Trainers.sees` | implemented |
| `SPRITEMOVEDATA_SPINRANDOM_FAST` swimmers | `src/world/gen2/Npc.lua:23`, `:37`, `:68` | implemented |
| `loadtrainer` / `startbattle` / `winlosstext` / `reloadmapafterbattle` / `endifjustbattled` | `src/script/gen2/Vm.lua`, `src/script/gen2/Opcodes.lua` | implemented |
| Prize money, including the 4x split and Bank of Mom | `src/battle/gen2/Prize.lua:82` `Prize.reward`, `:181` (four quarters) | implemented |
| Gym-leader battle/victory music via `GymLeaders` / `KantoGymLeaders` | `src/battle/gen2/BattleMusic.lua:15-33` | implemented |
| Kanto badge page on the trainer card (EARTHBADGE displayed) | `src/ui/gen2/TrainerCard.lua:90`, `:161` | implemented |
| `verbosegiveitem` (TM42 in Viridian City) | `src/script/gen2/Vm.lua` (`verbosegiveitem`) | implemented |
| A driver that actually plays this stretch (Routes 19/20 surf trainers, Blue) | none - `tests/drivers/` has `gold_trainer_smoke.lua`, `gold_water_moves.lua`, `gold_map_callbacks.lua`, but nothing that reaches Kanto Route 19/20 or Viridian Gym | missing |
| Section-specific regression coverage for `EVENT_CINNABAR_ROCKS_CLEARED` sequencing | not found by grep in `tests/` | missing |
Every "implemented" row above is a generic engine capability, not a hand-port of these maps. Nothing in `src/` names `ROUTE_19`, `ROUTE_20` or `VIRIDIAN_GYM` (the only grep hits are `src/save_convert/data/event_flags_yellow.lua`, which is Gen 1 save conversion data and unrelated). So this section should work if the extractor and VM work, but it has never been exercised end to end.
## 6. Unresolved / verify by hand
1. **The walkthrough's boulder claim contradicts the asm.** "You can't go further north because you'll be blocked by those boulders south of Fuchsia City" is only true while `EVENT_CINNABAR_ROCKS_CLEARED` is clear. `maps/Route20.asm:12` `Route20ClearRocksCallback` sets that event on `MAPCALLBACK_NEWMAP` with no condition at all, and the walkthrough routes the player through Route 20 to reach Route 19. By the time the player is standing on Route 19 the six `changeblock` rocks are never placed, so northern Route 19 and the Fuchsia gate at `(7, 3)` are open. Treat the FAQ line as stale prose, not a gate. (What is genuinely true is that the FAQ's suggested Fly is faster than swimming back.)
2. **Route 19's north connection vs. the gate warp.** `data/maps/attributes.asm:288` declares `connection north, FuchsiaCity, FUCHSIA_CITY, 0`, and there is *also* a warp to `ROUTE_19_FUCHSIA_GATE`. I did not decode `maps/Route19.blk` to determine whether the top block row is actually walkable, so I cannot say whether the north connection is reachable or purely decorative. Verify by hand before a bot relies on either route out.
3. **Two trainers the walkthrough omits.** `TrainerSwimmermCameron` on Route 20 at `(12, 13)` (L34 MARILL) and `TrainerSwimmermHarold` on Route 19 at `(13, 28)` (L32 REMORAID, L30 SEADRA) both exist with sight range 3. The FAQ says "that's all the trainers" for Route 19, which is wrong. A bot surfing the full map will get pulled into both.
4. **Blue's move set does not match the FAQ's strategy text.** The FAQ names Air Slash (Pidgeot) and Psychic + Hypnosis (Exeggutor); `data/trainers/parties.asm:3154` gives Pidgeot `QUICK_ATTACK, WHIRLWIND, WING_ATTACK, MIRROR_MOVE` and Exeggutor `SUNNY_DAY, LEECH_SEED, EGG_BOMB, SOLARBEAM`. Air Slash does not exist in Gen 2. Do not build AI expectations from the FAQ prose here.
5. **The FAQ's "Blue is one of only two trainers tougher than Champion Lance"** is a subjective claim with no asm counterpart; nothing in `data/trainers/` ranks trainers.
6. **`EVENT_VIRIDIAN_GYM_BLUE` is set in a prior section.** The walkthrough never mentions meeting Blue on Cinnabar Island, but `maps/CinnabarIsland.asm:23` is the only `clearevent` for that flag anywhere in the tree. If an earlier section's document does not cover `CinnabarIslandBlue`, this is an orphaned precondition and a bot will find an empty Viridian Gym with no in-game hint beyond `ViridianCityGrampsNearGym`.
7. **Blue's map header sets the phone column to `TRUE`** (`data/maps/maps.asm:458`, the `ViridianGym` row). I found no `phone_call` or rematch entry naming `BLUE`, so I believe this is inert, but I did not exhaustively read `data/phone/`.
@@ -0,0 +1,785 @@
# Section 29 - Routes 22-28
Source: `../section-29-routes-22-28.txt` (the FAQ numbers this chapter "35 > Routes 22-28")
Maps covered: `MAP_PALLET_TOWN`, `MAP_OAKS_LAB`, `MAP_VIRIDIAN_CITY`, `MAP_ROUTE_22`,
`MAP_VICTORY_ROAD_GATE`, `MAP_ROUTE_28`, `MAP_ROUTE_28_STEEL_WING_HOUSE`, and the
east edge of `MAP_SILVER_CAVE_OUTSIDE` (+ `MAP_SILVER_CAVE_POKECENTER_1F`).
Badges / key milestones in this section: no badge. The milestone is
`EVENT_OPENED_MT_SILVER`, set by Prof. Oak once `VAR_BADGES == NUM_BADGES` (16).
Items: TM42 Dream Eater, TM47 Steel Wing, hidden Rare Candy.
Two structural facts the walkthrough hides, both verified against the block data
and both load-bearing for a bot:
1. **Oak's line is not flavour - it is the gate.** `EVENT_OPENED_MT_SILVER`
*removes* the black belt standing at `VICTORY_ROAD_GATE` (7,5). That NPC
occupies the only tile joining the gate's central corridor to its western
vestibule, i.e. to the two `ROUTE_28` warps. Until Oak sets the flag, Route 28
and Mt. Silver are physically unreachable. See section 3.
2. **`object_event`'s trailing `EVENT_*` HIDES the object when the flag is SET.**
`CheckObjectFlag` (`engine/overworld/map_objects_2.asm:32-61`) jumps to
`.masked` when `EventFlagAction`/`CHECK_FLAG` returns non-zero. The macro
comment in `macros/scripts/maps.asm:132` ("event flag: an `EVENT_*` constant,
or -1 to always appear") does not state the polarity and is easy to read
backwards. The port already has this right
(`src/world/gen2/Events.lua:40-44`).
Coordinates below are walk cells (the raw numbers in the `warp_event` /
`bg_event` / `object_event` rows), origin top-left, x east, y south.
---
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 0 | `MAP_PALLET_TOWN` | `maps/PalletTown.asm` | Fly, `SPAWN_PALLET` = (5,6) (`data/maps/spawn_points.asm:14`) | warp 3 at (12,11) -> `OAKS_LAB` warp 1 | "fly back to Pallet Town" |
| 1 | `MAP_OAKS_LAB` | `maps/OaksLab.asm` | warp 1 at (4,11) (warp 2 at (5,11) is the twin tile) | same warps back to `PALLET_TOWN` warp 3 | talk to `Oak` at (4,2); with 16 badges this runs `.OpenMtSilver` and sets `EVENT_OPENED_MT_SILVER` |
| 2 | `MAP_VIRIDIAN_CITY` | `maps/ViridianCity.asm` | Fly, `SPAWN_VIRIDIAN` = (23,26) (`data/maps/spawn_points.asm:15`), which is the Pokecenter warp tile | west map edge, cells y=14..17 at x=0 | "fly back to Viridian City, and then head west to Route 22" |
| 2a | `MAP_VIRIDIAN_CITY` (Cut pocket) | same | Cut the tree at (8,22), approached from (9,22) facing LEFT | the same tile back east | the TM42 Dream Eater fisher at (6,23). The FAQ files this under "Items on Route 22"; it is **not** on Route 22 |
| 3 | `MAP_ROUTE_22` | `maps/Route22.asm` | east map edge at x=39, y=6..9 (`connection east, ViridianCity, VIRIDIAN_CITY, -4`) | warp 1 at (13,5) -> `VICTORY_ROAD_GATE` warp 1 | "keep heading north, west, then north" |
| 4 | `MAP_VICTORY_ROAD_GATE` | `maps/VictoryRoadGate.asm` | warp 1 at (17,7) (warp 2 at (18,7) is the twin) | warp 7 at (1,7) / warp 8 at (2,7) -> `ROUTE_28` warp 2 | "heading west instead of north" |
| 5 | `MAP_ROUTE_28` (main body) | `maps/Route28.asm` | warp 2 at (33,5) | west map edge at x=0, y=12..13 (`connection west, SilverCaveOutside, SILVER_CAVE_OUTSIDE, -9`) | "head west towards Mount Silver"; the Route 28 sign is read from (30,5) |
| 6 | `MAP_SILVER_CAVE_OUTSIDE` | `maps/SilverCaveOutside.asm` | east map edge at x=39, y=30..31 | warp 1 at (23,19) -> `SILVER_CAVE_POKECENTER_1F` warp 1 | "There's also a Pokémon Center here". Sets `ENGINE_FLYPOINT_SILVER_CAVE` on entry |
| 7 | `MAP_SILVER_CAVE_POKECENTER_1F` | `maps/SilverCavePokecenter1F.asm` | warp 1 at (3,7) (twin at (4,7)) | same | heal; the last Pokecenter before Mt. Silver |
| 8 | `MAP_SILVER_CAVE_OUTSIDE` (east shelf) | same | Cut (31,24) then Cut (34,23) | east map edge at x=39, y=21 | "cut the trees east" |
| 9 | `MAP_ROUTE_28` (isolated west strip) | `maps/Route28.asm` | west map edge at x=0, y=3 | warp 1 at (7,3) -> `ROUTE_28_STEEL_WING_HOUSE` warp 1 | the strip that holds the Steel Wing house door and the hidden Rare Candy |
| 10 | `MAP_ROUTE_28_STEEL_WING_HOUSE` | `maps/Route28SteelWingHouse.asm` | warp 1 at (2,7) (twin at (3,7)) | same | `Celebrity` at (2,3) gives TM47 Steel Wing |
| 11 | `MAP_SILVER_CAVE_OUTSIDE` warp 2 at (18,11) -> `SILVER_CAVE_ROOM_1` | `maps/SilverCaveRoom1.asm` | - | - | **next section** (Mt. Silver / Red). Stop here. |
The FAQ's chapter title says "Routes 22-28" but the prose never enters Route 23,
24, 25, 26 or 27. Routes 26/27 are section 16's, Route 23 is section 17/18's.
## 2. Maps
### MAP_PALLET_TOWN
- Script: `maps/PalletTown.asm` (included from `data/maps/scripts.asm:171`)
- Blocks: `maps/PalletTown.blk` (`data/maps/blocks.asm:88`)
- Header: `data/maps/maps.asm:312`
`map PalletTown, TILESET_KANTO, TOWN, LANDMARK_PALLET_TOWN, MUSIC_PALLET_TOWN, FALSE, PALETTE_AUTO, FISHGROUP_OCEAN`
- Attributes: `data/maps/attributes.asm:271` `map_attributes PalletTown, PALLET_TOWN, $0f`
- Dimensions: `constants/map_constants.asm:288` `map_const PALLET_TOWN, 10, 9` -> 10x9 blocks = 20x18 cells. Group `PALLET` (13), map id 2.
- Connections: `connection north, Route1, ROUTE_1, 0`; `connection south, Route21, ROUTE_21, 0`
- Callbacks: `callback MAPCALLBACK_NEWMAP, PalletTownFlypointCallback` (sym `4e:46ac`) -> `setflag ENGINE_FLYPOINT_PALLET`
**Warps** (`def_warp_events`, `maps/PalletTown.asm:73-76`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 5 | 5 | `REDS_HOUSE_1F` | 1 |
| 2 | 13 | 5 | `BLUES_HOUSE` | 1 |
| 3 | 12 | 11 | `OAKS_LAB` | 1 |
**Coord events** - `def_coord_events` is empty.
**BG events** (`def_bg_events`, `maps/PalletTown.asm:80-84`)
| x | y | type | script/item |
|---|---|---|---|
| 7 | 9 | `BGEVENT_READ` | `PalletTownSign` (sym `4e:46b6`) |
| 3 | 5 | `BGEVENT_READ` | `RedsHouseSign` |
| 13 | 13 | `BGEVENT_READ` | `OaksLabSign` (sym `4e:46bc`) |
| 11 | 5 | `BGEVENT_READ` | `BluesHouseSign` |
**Object events** (`def_object_events`, `maps/PalletTown.asm:86-88`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `PALLETTOWN_TEACHER` | `SPRITE_TEACHER` | 3 | 8 | `SPRITEMOVEDATA_WANDER` (r 2,2) | `OBJECTTYPE_SCRIPT` | `PalletTownTeacherScript` | -1 |
| `PALLETTOWN_FISHER` | `SPRITE_FISHER` | 12 | 14 | `SPRITEMOVEDATA_WALK_LEFT_RIGHT` (r 2,0) | `OBJECTTYPE_SCRIPT` | `PalletTownFisherScript` | -1 |
**Route from the Fly landing to the lab door** (BFS over `PalletTown.blk` +
`data/tilesets/kanto_collision.asm`): (5,6) -> (9,6) -> (9,12) -> (12,12) ->
(12,11). No ledges, no field moves.
---
### MAP_OAKS_LAB
- Script: `maps/OaksLab.asm` (`data/maps/scripts.asm:341`)
- Blocks: `maps/OaksLab.blk` (`data/maps/blocks.asm:892`)
- Header: `data/maps/maps.asm:316`
`map OaksLab, TILESET_LAB, INDOOR, LANDMARK_PALLET_TOWN, MUSIC_POKEMON_TALK, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Attributes: `data/maps/attributes.asm:579` `map_attributes OaksLab, OAKS_LAB, $00`, no connections
- Dimensions: `constants/map_constants.asm:292` `map_const OAKS_LAB, 5, 6` -> 5x6 blocks = 10x12 cells. Group `PALLET` (13), map id 6.
- Callbacks / scene scripts: both empty (`OaksLabNoopScene` is marked unreferenced).
**Warps** (`maps/OaksLab.asm:260-262`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 4 | 11 | `PALLET_TOWN` | 3 |
| 2 | 5 | 11 | `PALLET_TOWN` | 3 |
**Coord events** - none.
**BG events** (`maps/OaksLab.asm:266-282`)
| x | y | type | script/item |
|---|---|---|---|
| 6,7,8,9 | 1 | `BGEVENT_READ` | `OaksLabBookshelf` -> `jumpstd DifficultBookshelfScript` |
| 0,1,2,3 | 7 | `BGEVENT_READ` | `OaksLabBookshelf` |
| 6,7,8,9 | 7 | `BGEVENT_READ` | `OaksLabBookshelf` |
| 4 | 0 | `BGEVENT_READ` | `OaksLabPoster1` |
| 5 | 0 | `BGEVENT_READ` | `OaksLabPoster2` |
| 9 | 3 | `BGEVENT_READ` | `OaksLabTrashcan` |
| 0 | 1 | `BGEVENT_READ` | `OaksLabPC` |
**Object events** (`maps/OaksLab.asm:284-288`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `OAKSLAB_OAK` | `SPRITE_OAK` | 4 | 2 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `Oak` (sym `59:58c3`) | -1 |
| `OAKSLAB_SCIENTIST1` | `SPRITE_SCIENTIST` | 1 | 8 | `WALK_LEFT_RIGHT` (r 1,0), `PAL_NPC_BLUE` | `OBJECTTYPE_SCRIPT` | `OaksAssistant1Script` (sym `59:590a`) | -1 |
| `OAKSLAB_SCIENTIST2` | `SPRITE_SCIENTIST` | 8 | 9 | `WALK_UP_DOWN` (r 0,1), `PAL_NPC_BLUE` | `OBJECTTYPE_SCRIPT` | `OaksAssistant2Script` | -1 |
| `OAKSLAB_SCIENTIST3` | `SPRITE_SCIENTIST` | 1 | 4 | `WANDER` (r 1,1), `PAL_NPC_BLUE` | `OBJECTTYPE_SCRIPT` | `OaksAssistant3Script` | -1 |
Oak stands at (4,2); the player talks to him from (4,3) facing UP. Cell (4,3) is
`COLL_FLOOR` in `data/tilesets/lab_collision.asm`, and (4,11)/(5,11) are the
`WARP_CARPET_DOWN` pair, so the approach is a straight walk up the middle of the
room.
**Scripts of interest**
`Oak` (`maps/OaksLab.asm:15-54`, sym `59:58c3`) - the only script in this section
that writes a story flag:
```
faceplayer / opentext
checkevent EVENT_OPENED_MT_SILVER ; already done -> .CheckPokedex
iftrue .CheckPokedex
checkevent EVENT_TALKED_TO_OAK_IN_KANTO
iftrue .CheckBadges
writetext OakWelcomeKantoText / promptbutton
setevent EVENT_TALKED_TO_OAK_IN_KANTO
.CheckBadges:
readvar VAR_BADGES
ifequal NUM_BADGES, .OpenMtSilver ; 16
ifequal NUM_JOHTO_BADGES, .Complain ; 8 -> "you're not collecting KANTO BADGES"
sjump .AhGood ; 9..15 -> "come see me when you get them all"
.OpenMtSilver:
writetext OakOpenMtSilverText / promptbutton
setevent EVENT_OPENED_MT_SILVER
sjump .CheckPokedex
.CheckPokedex:
writetext OakLabDexCheckText / waitbutton
special ProfOaksPCBoot
writetext OakLabGoodbyeText / waitbutton / closetext / end
```
Bot-relevant: the branch is `ifequal`, not `ifgreater`, and `VAR_BADGES` is the
**popcount of both badge bytes** (`engine/overworld/variables.asm` ->
`CountSetBits` over `wJohtoBadges` + `wKantoBadges`; the port mirrors this at
`src/world/gen2/World.lua:1240-1245`). So the flag is only set on a visit with
exactly 16 badges; 9-15 badges gives `.AhGood` and no flag. Every arm falls
through to `special ProfOaksPCBoot`, which prints the Pokedex rating - budget
several textbox advances for it.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_OPENED_MT_SILVER` | `constants/event_flags.asm:1265` | set by `Oak`; read by `Oak` and by the `VICTORYROADGATE_BLACK_BELT1` object row | **the section's gate.** While clear, the black belt at `VICTORY_ROAD_GATE` (7,5) exists and blocks the ROUTE_28 warps |
| `EVENT_TALKED_TO_OAK_IN_KANTO` | `constants/event_flags.asm:224` | set/read by `Oak` | first-visit greeting only, no gameplay effect |
---
### MAP_VIRIDIAN_CITY
- Script: `maps/ViridianCity.asm` (`data/maps/scripts.asm:168`)
- Blocks: `maps/ViridianCity.blk` (`data/maps/blocks.asm:235`)
- Header: `data/maps/maps.asm:457`
`map ViridianCity, TILESET_KANTO, TOWN, LANDMARK_VIRIDIAN_CITY, MUSIC_VIRIDIAN_CITY, FALSE, PALETTE_AUTO, FISHGROUP_POND`
- Attributes: `data/maps/attributes.asm:259` `map_attributes ViridianCity, VIRIDIAN_CITY, $0f`
- Dimensions: `constants/map_constants.asm:423` `map_const VIRIDIAN_CITY, 20, 18` -> 20x18 blocks = 40x36 cells. Group `VIRIDIAN` (23), map id 3.
- Connections: `connection north, Route2, ROUTE_2, 5`; `connection south, Route1, ROUTE_1, 10`; `connection west, Route22, ROUTE_22, 4`
- Callbacks: `callback MAPCALLBACK_NEWMAP, ViridianCityFlypointCallback` (sym `4e:4005`) -> `setflag ENGINE_FLYPOINT_VIRIDIAN`
**Warps** (`maps/ViridianCity.asm:219-224`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 32 | 7 | `VIRIDIAN_GYM` | 1 |
| 2 | 21 | 9 | `VIRIDIAN_NICKNAME_SPEECH_HOUSE` | 1 |
| 3 | 23 | 15 | `TRAINER_HOUSE_1F` | 1 |
| 4 | 29 | 19 | `VIRIDIAN_MART` | 2 |
| 5 | 23 | 25 | `VIRIDIAN_POKECENTER_1F` | 1 |
**Coord events** - `def_coord_events` is empty.
**BG events** (`maps/ViridianCity.asm:228-234`)
| x | y | type | script/item |
|---|---|---|---|
| 17 | 17 | `BGEVENT_READ` | `ViridianCitySign` |
| 27 | 7 | `BGEVENT_READ` | `ViridianGymSign` |
| 19 | 1 | `BGEVENT_READ` | `ViridianCityWelcomeSign` |
| 21 | 15 | `BGEVENT_READ` | `TrainerHouseSign` |
| 24 | 25 | `BGEVENT_READ` | `ViridianCityPokecenterSign` -> `jumpstd PokecenterSignScript` |
| 30 | 19 | `BGEVENT_READ` | `ViridianCityMartSign` -> `jumpstd MartSignScript` |
**Object events** (`maps/ViridianCity.asm:236-240`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `VIRIDIANCITY_GRAMPS1` | `SPRITE_GRAMPS` | 18 | 5 | `WANDER` (r 2,2) | `OBJECTTYPE_SCRIPT` | `ViridianCityCoffeeGramps` (sym `4e:4009`) | -1 |
| `VIRIDIANCITY_GRAMPS2` | `SPRITE_GRAMPS` | 30 | 8 | `STANDING_DOWN`, `PAL_NPC_BLUE` | `OBJECTTYPE_SCRIPT` | `ViridianCityGrampsNearGym` (sym `4e:401e`) | -1 |
| `VIRIDIANCITY_FISHER` | `SPRITE_FISHER` | 6 | 23 | `STANDING_DOWN`, `PAL_NPC_RED` | `OBJECTTYPE_SCRIPT` | `ViridianCityDreamEaterFisher` (sym `4e:4032`) | -1 |
| `VIRIDIANCITY_YOUNGSTER` | `SPRITE_YOUNGSTER` | 17 | 21 | `WANDER` (r 3,3), `PAL_NPC_GREEN` | `OBJECTTYPE_SCRIPT` | `ViridianCityYoungsterScript` (sym `4e:404d`) | -1 |
**Scripts of interest**
`ViridianCityDreamEaterFisher` (`maps/ViridianCity.asm:50-65`, sym `4e:4032`):
```
faceplayer / opentext
checkevent EVENT_GOT_TM42_DREAM_EATER
iftrue .GotDreamEater
writetext ViridianCityDreamEaterFisherText / promptbutton
verbosegiveitem TM_DREAM_EATER
iffalse .NoRoomForDreamEater ; bag full -> flag NOT set, retryable
setevent EVENT_GOT_TM42_DREAM_EATER
.GotDreamEater: writetext ...GotDreamEaterText / waitbutton
.NoRoomForDreamEater: closetext / end
```
`TM_DREAM_EATER` is item `$ea` (`constants/item_constants.asm:263`, the 42nd
`add_tm` after `DEF TM01 EQU const_value` at line 219 - the two bare `const`
rows `ITEM_C3` and `ITEM_DC` do not consume TM numbers, which is why TM42 is
Dream Eater and TM47 is Steel Wing).
**Cut gate (verified from block data)**
`ViridianCity.blk` block (4,11) is `$34`, whose `tilecoll` row is
`CUT_TREE, FLOOR, WALL, WALL` (`data/tilesets/kanto_collision.asm`, block `34`),
so the `COLL_CUT_TREE` quadrant is the top-left = **cell (8,22)**. A BFS over the
collision grid says the fisher's pocket (x=4..7, y=22..26) has exactly one
non-ledge entrance and it is that tile. Route:
- Pokecenter door (23,25) -> step down to (23,26)
- (23,26) -> (14,26) -> (14,23) -> (10,23) -> (10,22) -> (9,22)
- face LEFT at (9,22), Cut (8,22)
- (8,22) -> (7,22) -> (6,22) -> (6,23) faces the fisher at... he *is* at (6,23);
stand at (6,22) and face DOWN, or (7,23) and face LEFT.
The pocket's south edge (x=4..7, y=26) is `COLL_HOP_DOWN`, i.e. one-way out.
The only other `$34`/`$60`/`$32`/`$33`/`$35` block on this map is (7,2) =
cell (14,4), which is not on any route in this section.
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| TM42 Dream Eater | talk to the fisher at (6,23) after Cutting (8,22) | `ViridianCityDreamEaterFisher` | `EVENT_GOT_TM42_DREAM_EATER` (`constants/event_flags.asm:223`) |
**Trainers** - none on this map.
**Wild encounters** - `FISHGROUP_POND` per the header; no `def_grass_wildmons
VIRIDIAN_CITY` row is needed by this section.
---
### MAP_ROUTE_22
- Script: `maps/Route22.asm` (`data/maps/scripts.asm:169`) - 28 lines, one sign and one warp, nothing else
- Blocks: `maps/Route22.blk` (`data/maps/blocks.asm:310`, 180 bytes)
- Header: `data/maps/maps.asm:456`
`map Route22, TILESET_KANTO, ROUTE, LANDMARK_ROUTE_22, MUSIC_ROUTE_3, FALSE, PALETTE_AUTO, FISHGROUP_POND`
- Attributes: `data/maps/attributes.asm:264` `map_attributes Route22, ROUTE_22, $2c`
- Dimensions: `constants/map_constants.asm:422` `map_const ROUTE_22, 20, 9` -> 20x9 blocks = 40x18 cells. Group `VIRIDIAN` (23), map id 2.
- Connections: `connection east, ViridianCity, VIRIDIAN_CITY, -4` (only one). Crossing east from `(39, y)` lands on Viridian City `(0, y + 8)`; crossing west from Viridian `(0, y)` lands on Route 22 `(39, y - 8)`.
- Scene scripts / callbacks: both empty.
**Warps** (`maps/Route22.asm:19-20`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 13 | 5 | `VICTORY_ROAD_GATE` | 1 |
**Coord events** - none.
**BG events** (`maps/Route22.asm:24-25`)
| x | y | type | script/item |
|---|---|---|---|
| 15 | 7 | `BGEVENT_READ` | `VictoryRoadEntranceSign` (sym `4e:44f9`) - read from (14,7) facing RIGHT |
**Object events** - `def_object_events` is empty. **There are no trainers and no
item balls on Route 22**, which matches the FAQ. The FAQ's "Items on Route 22:
TM42 Dream Eater" is wrong; that TM is a Viridian City NPC gift (above).
**Traversal (verified by BFS over `Route22.blk` + `kanto_collision.asm`)**
The map is a set of shelves joined by one-way ledges. Entering from Viridian at
`(39, 6..9)`, the *only* path to the gate warp is:
```
(39,7) -> (35,7) -> (35,11)
(35,12) is COLL_HOP_DOWN -> hop to (35,14) [ledge, one-way]
(35,14) -> (33,14) -> (33,13) -> (33,12) [the one non-ledge gap]
(33,12) -> (33,11) grass -> (31,8) -> (31,7) -> (31,5) [the y=4..5 plaza]
(31,5) -> (18,5)
(18,6) is COLL_HOP_DOWN -> hop to (18,8) [ledge, one-way]
(18,8) grass -> (18,12) -> (14,12) -> (14,8) -> (13,7) -> (13,6) -> (13,5) warp 1
```
Ledge semantics: a `COLL_HOP_*` tile is `LAND_TILE` and can be *walked onto*;
pressing the matching direction while standing on it jumps **two** cells, over
the wall beneath. There is no ledge-free route: `(18,7)` and `(35,13)` are
`COLL_WALL`.
Reachable tall grass in that component: `(18..21, 8..11)` and `(30..33, 8..11)`.
Surfable water adjacent to it: `(22..25, 6)` from the plaza row y=5, and
`(22..25, 9)` from the (22..25,10..11) shelf.
**Wild encounters** - `data/wild/kanto_grass.asm:1033`, `def_grass_wildmons ROUTE_22`,
encounter rate `10 percent` in all three time slots. Slot probabilities are
30/30/20/10/5/4/1 (`data/wild/probabilities.asm:6-15`).
| slot | morn | day | nite |
|---|---|---|---|
| 1 (30%) | L3 RATTATA | L3 RATTATA | L3 RATTATA |
| 2 (30%) | L3 SPEAROW | L3 SPEAROW | L3 RATTATA |
| 3 (20%) | L5 SPEAROW | L5 SPEAROW | L5 RATTATA |
| 4 (10%) | L4 DODUO | L4 DODUO | L4 RATTATA |
| 5 (5%) | L6 PONYTA | L6 PONYTA | L6 PONYTA |
| 6 (4%) | L7 FEAROW | L7 FEAROW | L7 RATTATA |
| 7 (1%) | L7 FEAROW | L7 FEAROW | L7 RATTATA |
Water (`data/wild/kanto_water.asm:68`, `def_water_wildmons ROUTE_22`, rate
`2 percent`, slot probabilities 60/30/10): L10 POLIWAG, L5 POLIWAG, L10 POLIWHIRL.
The FAQ's "#060 Poliwag (surfing)" is right; it omits Doduo, Ponyta and Fearow
from the grass list.
Fishing: `FISHGROUP_POND` (`data/wild/fish.asm:72-85`) - Old rod MAGIKARP/POLIWAG,
Good rod MAGIKARP/POLIWAG + a time group, Super rod POLIWAG/MAGIKARP + a time group.
Headbutt: Route 22 has **no** row in `data/wild/treemon_maps.asm`.
---
### MAP_VICTORY_ROAD_GATE
- Script: `maps/VictoryRoadGate.asm` (`data/maps/scripts.asm:452`)
- Blocks: `maps/VictoryRoadGate.blk` (`data/maps/blocks.asm:943`, 90 bytes)
- Header: `data/maps/maps.asm:467`
`map VictoryRoadGate, TILESET_GATE, GATE, LANDMARK_ROUTE_26, MUSIC_INDIGO_PLATEAU, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
(note the landmark is `LANDMARK_ROUTE_26`, not Route 22 or 28 - relevant if a
bot keys anything off the Pokegear location readout)
- Attributes: `data/maps/attributes.asm:666` `map_attributes VictoryRoadGate, VICTORY_ROAD_GATE, $00`, no connections
- Dimensions: `constants/map_constants.asm:433` `map_const VICTORY_ROAD_GATE, 10, 9` -> 10x9 blocks = 20x18 cells. Group `VIRIDIAN` (23), map id 13.
- Scene variable: `data/maps/scenes.asm:16` `scene_var VICTORY_ROAD_GATE, wVictoryRoadGateSceneID` (sym `01:d6bf`)
- Scene scripts (`maps/VictoryRoadGate.asm:7-9`): `SCENE_VICTORYROADGATE_BADGE_CHECK` = 0 -> `VictoryRoadGateNoop1Scene` (bare `end`); `SCENE_VICTORYROADGATE_NOOP` = 1 -> `VictoryRoadGateNoop2Scene` (bare `end`). Both constants are minted by the `scene_script` macro's `scene_const` (`macros/scripts/maps.asm:12-33`), which is why grepping `constants/` for them finds nothing.
**Warps** (`maps/VictoryRoadGate.asm:101-109`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 17 | 7 | `ROUTE_22` | 1 |
| 2 | 18 | 7 | `ROUTE_22` | 1 |
| 3 | 9 | 17 | `ROUTE_26` | 1 |
| 4 | 10 | 17 | `ROUTE_26` | 1 |
| 5 | 9 | 0 | `VICTORY_ROAD` | 1 |
| 6 | 10 | 0 | `VICTORY_ROAD` | 1 |
| 7 | 1 | 7 | `ROUTE_28` | 2 |
| 8 | 2 | 7 | `ROUTE_28` | 2 |
**Coord events** (`maps/VictoryRoadGate.asm:111-112`)
| scene | x | y | script label | effect |
|---|---|---|---|---|
| `SCENE_VICTORYROADGATE_BADGE_CHECK` (0) | 10 | 11 | `VictoryRoadGateBadgeCheckScript` (sym `5f:4fdc`) | `turnobject PLAYER, LEFT` then the officer's speech; `< 8` Johto badges -> `applymovement PLAYER, VictoryRoadGateStepDownMovement` (one step DOWN); 8 badges -> `setscene SCENE_VICTORYROADGATE_NOOP` and the trip-wire never fires again |
A bot arriving from Route 22 (warp 1/2) never crosses (10,11): the east-west hall
is at y=5 and the badge check sits in the southern stub from Route 26. The check
is therefore *not* a gate on this section's route.
**BG events** - `def_bg_events` is empty.
**Object events** (`maps/VictoryRoadGate.asm:116-119`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `VICTORYROADGATE_OFFICER` | `SPRITE_OFFICER` | 8 | 11 | `STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `VictoryRoadGateOfficerScript` (sym `5f:4fe2`) | -1 |
| `VICTORYROADGATE_BLACK_BELT1` | `SPRITE_BLACK_BELT` | 7 | 5 | `STANDING_RIGHT` | `OBJECTTYPE_SCRIPT` | `VictoryRoadGateLeftBlackBeltScript` (sym `5f:5000`) | `EVENT_OPENED_MT_SILVER` |
| `VICTORYROADGATE_BLACK_BELT2` | `SPRITE_BLACK_BELT` | 12 | 5 | `STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `VictoryRoadGateRightBlackBeltScript` (sym `5f:5003`) | `EVENT_FOUGHT_SNORLAX` |
**Geometry (from `VictoryRoadGate.blk` + `data/tilesets/gate_collision.asm`)**
The building is a plus: one 20-wide, **one-cell-tall** hall at y=5, a north stub
(x=8..11, y=0..4) to Victory Road, a south stub (x=8..11, y=6..17) to Route 26
with the officer's counter at y=10..13, and two 4-wide vestibules at y=6..7 -
west (x=0..3, the Route 28 warps) and east (x=16..19, the Route 22 warps).
Cells (7,5) and (12,5) are the only tiles joining the centre to the west and east
vestibules respectively, and each is exactly where a black belt stands.
BFS from the Route 22 warp (17,7), toggling the two NPCs:
| NPCs present | reach `ROUTE_28` warp (1,7) | reach Victory Road door (9,0) | reach badge coord (10,11) |
|---|---|---|---|
| neither | yes | yes | yes |
| left only (`EVENT_OPENED_MT_SILVER` clear) | **no** | yes | yes |
| right only (`EVENT_FOUGHT_SNORLAX` clear) | no | no | no |
| both | no | no | no |
**Trainers** - none.
---
### MAP_ROUTE_28
- Script: `maps/Route28.asm` (`data/maps/scripts.asm:131`) - one sign, one hidden item, no NPCs
- Blocks: `maps/Route28.blk` (`data/maps/blocks.asm:15`, 180 bytes)
- Header: `data/maps/maps.asm:396`
`map Route28, TILESET_KANTO, ROUTE, LANDMARK_ROUTE_28, MUSIC_INDIGO_PLATEAU, FALSE, PALETTE_AUTO, FISHGROUP_POND`
- Attributes: `data/maps/attributes.asm:174` `map_attributes Route28, ROUTE_28, $2c`
- Dimensions: `constants/map_constants.asm:366` `map_const ROUTE_28, 20, 9` -> 20x9 blocks = 40x18 cells. Group `SILVER` (19), map id 1.
- Connections: `connection west, SilverCaveOutside, SILVER_CAVE_OUTSIDE, -9`. Crossing west from `(0, y)` lands on Silver Cave Outside `(39, y + 18)`; crossing east from Silver Cave Outside `(39, y)` lands on Route 28 `(0, y - 18)`.
- Scene scripts / callbacks: both empty. **Route 28 has no flypoint callback** - the Fly destination near Mt. Silver is `SILVER_CAVE_OUTSIDE`.
**Warps** (`maps/Route28.asm:19-21`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 7 | 3 | `ROUTE_28_STEEL_WING_HOUSE` | 1 |
| 2 | 33 | 5 | `VICTORY_ROAD_GATE` | 7 |
**Coord events** - none.
**BG events** (`maps/Route28.asm:25-27`)
| x | y | type | script/item |
|---|---|---|---|
| 31 | 5 | `BGEVENT_READ` | `Route28Sign` (sym `4a:4d4d`) - read from (30,5) facing RIGHT |
| 25 | 2 | `BGEVENT_ITEM` | `Route28HiddenRareCandy` (sym `4a:4d50`) = `hiddenitem RARE_CANDY, EVENT_ROUTE_28_HIDDEN_RARE_CANDY` |
`RARE_CANDY` is item `$20` (`constants/item_constants.asm:40`). The hidden-item
row is three bytes (`dwb flag, item`), not a script; the port carries it on the
bg event as `hiddenItem = { item, event }` (`src/world/gen2/HiddenItems.lua:7-12`).
**Object events** - `def_object_events` is empty. No trainers, no item balls.
**Geometry: Route 28 is two disconnected halves**
BFS over `Route28.blk` from the Victory Road Gate landing (33,6):
- the **main body** reaches the sign approach (30,5) and the west edge only at
y=12..13 -> Silver Cave Outside (39, 30..31);
- it does **not** reach (7,3), (25,3) or any of y=3..4.
BFS from the west edge at (0,3) - the **isolated north-west strip** -
reaches the house door (7,3) and the hidden-item approach (25,3), and then falls
one-way into the main body over the `COLL_HOP_DOWN` row at y=4 (x=4..9, landing
in the grass at y=6). So:
- entering the strip: only from `SILVER_CAVE_OUTSIDE (39,21)` -> `ROUTE_28 (0,3)`,
which itself needs two Cuts (see below);
- inside the strip: (0,3) -> (5,3) -> (5,4) -> (7,4) -> north into the door (7,3);
and (7,4) -> (21,4) -> (21,3) -> (25,3), then face UP for the hidden Rare Candy
at (25,2);
- leaving the strip: hop any of the y=4 ledges (x=4..9) down into the main body,
which is one-way. Re-entering means going round through Silver Cave Outside again.
Main-body route from the gate to Silver Cave Outside: (33,6) -> (28,6) -> (28,8),
hop (27,8)->(27,10), west along y=10 to (18,10) -> (18,9) -> (10,9) -> (10,10) ->
(8,10), hop (8,10)->(8,12), west along y=12 to (0,12).
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| Rare Candy | hidden, face (25,2) from (25,3) and press A, or use the Itemfinder | `Route28HiddenRareCandy` bg_event | `EVENT_ROUTE_28_HIDDEN_RARE_CANDY` (`constants/event_flags.asm:173`) |
The FAQ lists "Items: TM47 Steel Wing" for Route 28 and never mentions the Rare
Candy. TM47 is in the house, not on the route.
**Wild encounters** - `data/wild/kanto_grass.asm:1227`, `def_grass_wildmons ROUTE_28`,
rate `10 percent` all day. The table is `IF DEF(_GOLD) / ELIF DEF(_SILVER)`:
| slot | Gold morn/day | Gold nite | Silver morn/day | Silver nite |
|---|---|---|---|---|
| 1 (30%) | L39 TANGELA | L39 TANGELA | L39 TANGELA | L39 TANGELA |
| 2 (30%) | L40 PONYTA | L40 PONYTA | L40 PONYTA | L40 PONYTA |
| 3 (20%) | L40 URSARING | L40 URSARING | L40 DONPHAN | L40 DONPHAN |
| 4 (10%) | L42 RAPIDASH | L40 SNEASEL | L42 RAPIDASH | L40 SNEASEL |
| 5 (5%) | L41 DODUO | L42 RAPIDASH | L41 DODUO | L42 RAPIDASH |
| 6 (4%) | L43 DODRIO | L42 RAPIDASH | L43 DODRIO | L42 RAPIDASH |
| 7 (1%) | L43 DODRIO | L42 RAPIDASH | L43 DODRIO | L42 RAPIDASH |
So on **Gold** the FAQ's Donphan does not appear (Ursaring takes that slot), and
**Sneasel is nite-only**. Tangela is the 30% lead slot at every time of day,
which makes the FAQ's "consider catching a Tangela" cheap to satisfy.
Water (`data/wild/kanto_water.asm:110`, rate `2 percent`): L40 POLIWAG, L35
POLIWAG, L40 POLIWHIRL. Fishing: `FISHGROUP_POND`, same table as Route 22.
Headbutt: `data/wild/treemon_maps.asm:9` `treemon_map ROUTE_28, TREEMON_SET_NONE`
- headbutting a Route 28 tree yields nothing.
---
### MAP_ROUTE_28_STEEL_WING_HOUSE
- Script: `maps/Route28SteelWingHouse.asm` (`data/maps/scripts.asm:388`)
- Blocks: `data/maps/blocks.asm:181` (`Route28SteelWingHouse_Blocks`)
- Header: `data/maps/maps.asm:399`
`map Route28SteelWingHouse, TILESET_HOUSE, INDOOR, LANDMARK_ROUTE_28, MUSIC_AZALEA_TOWN, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Attributes: `data/maps/attributes.asm:624`, no connections
- Dimensions: `constants/map_constants.asm:369` `map_const ROUTE_28_STEEL_WING_HOUSE, 4, 4` -> 8x8 cells. Group `SILVER` (19), map id 4.
- Scene scripts: one `scene_script Route28SteelWingHouseNoopScene` marked "unusable" in the source; body is a bare `end`.
**Warps** (`maps/Route28SteelWingHouse.asm:74-76`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 2 | 7 | `ROUTE_28` | 1 |
| 2 | 3 | 7 | `ROUTE_28` | 1 |
**Coord events** - none.
**BG events** (`maps/Route28SteelWingHouse.asm:80-82`)
| x | y | type | script/item |
|---|---|---|---|
| 0 | 1 | `BGEVENT_READ` | `CelebrityHouseBookshelf` -> `jumpstd MagazineBookshelfScript` |
| 1 | 1 | `BGEVENT_READ` | `CelebrityHouseBookshelf` |
**Object events** (`maps/Route28SteelWingHouse.asm:84-86`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE28STEELWINGHOUSE_CELEBRITY` | `SPRITE_COOLTRAINER_F` | 2 | 3 | `STANDING_DOWN`, `PAL_NPC_GREEN` | `OBJECTTYPE_SCRIPT` | `Celebrity` (sym `5c:4be9`) | -1 |
| `ROUTE28STEELWINGHOUSE_FEAROW` | `SPRITE_MOLTRES` | 6 | 5 | `SPRITEMOVEDATA_POKEMON`, `PAL_NPC_BROWN` | `OBJECTTYPE_SCRIPT` | `CelebritysFearow` (sym `5c:4c06`) | -1 |
The pet is a Fearow but reuses `SPRITE_MOLTRES` recoloured with `PAL_NPC_BROWN`;
`CelebritysFearow` does `cry FEAROW`. Worth knowing if a bot asserts on sprite ids.
**Scripts of interest**
`Celebrity` (`maps/Route28SteelWingHouse.asm:14-31`, sym `5c:4be9`):
```
faceplayer / opentext
checkevent EVENT_GOT_TM47_STEEL_WING
iftrue .AlreadyGotItem
writetext CelebrityText1 / promptbutton
verbosegiveitem TM_STEEL_WING
iffalse .Done ; bag full -> flag NOT set, retryable
setevent EVENT_GOT_TM47_STEEL_WING
.Done: closetext / end
```
Note the shape differs from the Dream Eater fisher: there is no "thanks" text
after a successful give, the script just closes.
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| TM47 Steel Wing (`$ef`) | talk to `Celebrity` at (2,3) from (2,4) facing UP | `Celebrity` | `EVENT_GOT_TM47_STEEL_WING` (`constants/event_flags.asm:125`) |
---
### MAP_SILVER_CAVE_OUTSIDE (east edge only - the rest is the next section)
- Script: `maps/SilverCaveOutside.asm` (`data/maps/scripts.asm:124`)
- Blocks: `maps/SilverCaveOutside.blk` (`data/maps/blocks.asm:559`, 360 bytes)
- Header: `data/maps/maps.asm:397`
`map SilverCaveOutside, TILESET_KANTO, TOWN, LANDMARK_SILVER_CAVE, MUSIC_INDIGO_PLATEAU, FALSE, PALETTE_AUTO, FISHGROUP_POND`
- Attributes: `data/maps/attributes.asm:164` `map_attributes SilverCaveOutside, SILVER_CAVE_OUTSIDE, $2c`
- Dimensions: `constants/map_constants.asm:367` `map_const SILVER_CAVE_OUTSIDE, 20, 18` -> 40x36 cells. Group `SILVER` (19), map id 2.
- Connections: `connection east, Route28, ROUTE_28, 9`
- Callbacks: `callback MAPCALLBACK_NEWMAP, SilverCaveOutsideFlypointCallback` (sym `49:5f1a`) -> `setflag ENGINE_FLYPOINT_SILVER_CAVE`. Spawn `SPAWN_MT_SILVER` = (23,20) (`data/maps/spawn_points.asm:39`), i.e. the tile below the Pokecenter door - this is the "fly back to the Pokémon Center on Route 28" the FAQ means.
**Warps** (`maps/SilverCaveOutside.asm:27-29`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 23 | 19 | `SILVER_CAVE_POKECENTER_1F` | 1 |
| 2 | 18 | 11 | `SILVER_CAVE_ROOM_1` | 1 |
**BG events** (`maps/SilverCaveOutside.asm:33-36`)
| x | y | type | script/item |
|---|---|---|---|
| 24 | 19 | `BGEVENT_READ` | `MtSilverPokecenterSign` (sym `49:5f1e`) |
| 17 | 13 | `BGEVENT_READ` | `MtSilverSign` (sym `49:5f21`) |
| 9 | 25 | `BGEVENT_ITEM` | `SilverCaveOutsideHiddenFullRestore` (sym `49:5f24`) = `hiddenitem FULL_RESTORE, EVENT_SILVER_CAVE_OUTSIDE_HIDDEN_FULL_RESTORE` (`constants/event_flags.asm:194`) - **next section's**, and not reachable from the east side without Surf |
**Coord events / object events** - both empty.
**The two Cut trees (verified from block data)**
`data/collision/field_move_blocks.asm:24-31`, `CutTreeBlockPointers.kanto`, lists
the cuttable blocks for `TILESET_KANTO`: `$0b -> $0a` (grass, animation 1) and
`$32 -> $6d`, `$33 -> $6c`, `$34 -> $6f`, `$35 -> $4c`, `$60 -> $6e` (trees,
animation 0). `SilverCaveOutside.blk` contains exactly two of them:
| block coord | block id | `tilecoll` row | `COLL_CUT_TREE` cell | replacement block |
|---|---|---|---|---|
| (17,11) | `$60` | `FLOOR, WALL, CUT_TREE, WALL` | **(34,23)** | `$6e` |
| (15,12) | `$35` | `FLOOR, CUT_TREE, WALL, FLOOR` | **(31,24)** | `$4c` |
BFS from the Route 28 landing (39,30):
- with no cuts: the Pokecenter door (23,19), the cave mouth (18,11) and the
approach tile (30,24) are reachable; (39,21) is **not**;
- after cutting (31,24) only: (34,24) becomes reachable, (39,21) still not;
- after cutting both: (39,21) is reachable -> `ROUTE_28 (0,3)`.
Route from the Pokecenter door to the house, waypoint by waypoint:
```
(23,19) door -> (23,20) -> (25,20) -> (25,22) -> (30,22) -> (30,24)
face RIGHT, Cut (31,24)
(31,24) -> (31,25) -> (34,25) -> (34,24)
face UP, Cut (34,23)
(34,23) -> (34,21) -> (39,21) -> cross east -> ROUTE_28 (0,3)
(0,3) -> (5,3) -> (5,4) -> (7,4) -> face UP into warp 1 at (7,3)
```
---
### MAP_SILVER_CAVE_POKECENTER_1F
- Script: `maps/SilverCavePokecenter1F.asm` (`data/maps/scripts.asm:387`)
- Header: `data/maps/maps.asm:398`
`map SilverCavePokecenter1F, TILESET_POKECENTER, INDOOR, LANDMARK_SILVER_CAVE, MUSIC_POKEMON_CENTER, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Attributes: `data/maps/attributes.asm:623`
- Dimensions: `constants/map_constants.asm:368` `map_const SILVER_CAVE_POKECENTER_1F, 5, 4` -> 10x8 cells. Group `SILVER` (19), map id 3.
**Warps** (`maps/SilverCavePokecenter1F.asm:33-36`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `SILVER_CAVE_OUTSIDE` | 1 |
| 2 | 4 | 7 | `SILVER_CAVE_OUTSIDE` | 1 |
| 3 | 0 | 7 | `POKECENTER_2F` | 1 |
**Object events** (`maps/SilverCavePokecenter1F.asm:42-44`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `SILVERCAVEPOKECENTER1F_NURSE` | `SPRITE_NURSE` | 3 | 1 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `SilverCavePokecenter1FNurseScript` (sym `5c:4b2b`) -> `jumpstd PokecenterNurseScript` | -1 |
| `SILVERCAVEPOKECENTER1F_GRANNY` | `SPRITE_GRANNY` | 1 | 5 | `STANDING_LEFT` (r 2,1) | `OBJECTTYPE_SCRIPT` | `SilverCavePokecenter1FGrannyScript` (sym `5c:4b2e`) | -1 |
No mart clerk on this map - the FAQ's "fly to Mahogany Town's Pokemart to buy
some" is correct precisely because there is nothing to buy here.
---
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Route 28 / Mt. Silver | `maps/VictoryRoadGate.asm:118` object row `VICTORYROADGATE_BLACK_BELT1` at (7,5) + `engine/overworld/map_objects_2.asm:32` `CheckObjectFlag` | `EVENT_OPENED_MT_SILVER` set, which hides the NPC and frees the only tile between the gate's centre and its west vestibule | talk to `Oak` (`maps/OaksLab.asm:15`) with `VAR_BADGES == NUM_BADGES` (16; `constants/ram_constants.asm:273`) |
| Route 22 from the gate | `maps/VictoryRoadGate.asm:119` object row `VICTORYROADGATE_BLACK_BELT2` at (12,5) | `EVENT_FOUGHT_SNORLAX` (`constants/event_flags.asm:1266`) | set long before this section (Kanto). Listed because it uses the same mechanism and a bot that clears flags for testing will wall itself in |
| Victory Road Gate south corridor | `maps/VictoryRoadGate.asm:112` coord event (10,11) -> `VictoryRoadGateBadgeCheckScript` | `readvar VAR_BADGES` `ifgreater NUM_JOHTO_BADGES - 1` i.e. >= 8 badges | already satisfied; and the Route 22 -> Route 28 path never touches (10,11) |
| Dream Eater fisher (Viridian) | `data/collision/field_move_blocks.asm:24` `.kanto` block `$34` at Viridian block (4,11) -> `COLL_CUT_TREE` at cell (8,22) | HM01 Cut in the party **and** `ENGINE_HIVEBADGE` | `engine/events/overworld.asm:1741` `TryCutOW`: `CheckPartyMove CUT`, then `ld de, ENGINE_HIVEBADGE / call CheckEngineFlag`; failure runs `CantCutScript` |
| Steel Wing house / Route 28 west strip | two `COLL_CUT_TREE` cells on `SILVER_CAVE_OUTSIDE`: (31,24) (block `$35`) and (34,23) (block `$60`) | same Cut + `ENGINE_HIVEBADGE` check | as above. Both trees are required - cutting only (31,24) does not open (39,21) |
| Fly to Pallet / Viridian / Mt. Silver | `engine/events/overworld.asm:543-545` `FlyFunction.TryFly`: `ld de, ENGINE_STORMBADGE / call CheckBadge`, then `GetMapEnvironment` / `CheckOutdoorMap` | STORMBADGE + HM02 Fly + the destination's `ENGINE_FLYPOINT_*` bit | `ENGINE_FLYPOINT_PALLET` (`constants/engine_flags.asm:67`), `ENGINE_FLYPOINT_VIRIDIAN` (68), `ENGINE_FLYPOINT_SILVER_CAVE` (90), each set by that map's `MAPCALLBACK_NEWMAP` the first time you walk in |
| Surf (Route 22 / Route 28 water) | `engine/events/overworld.asm:340` and `:490`, `ld de, ENGINE_FOGBADGE` | FOGBADGE + HM03 Surf | optional in this section |
| Route 22 traversal | `Route22.blk` collision: `(35,12)` and `(18,6)` are `COLL_HOP_DOWN` with `COLL_WALL` beneath | ledge hopping (walk onto the ledge tile, then press the matching direction to jump two cells) | no item needed, but there is **no** ledge-free route to the gate warp |
| Route 28 west strip re-entry | `Route28.blk` y=4 ledge row (x=4..9) | one-way; falling off it drops you into the main body | re-enter via `SILVER_CAVE_OUTSIDE (39,21)` |
## 4. Bot checklist
Preconditions for the whole section: 16 badges, HM01 Cut + HIVEBADGE, HM02 Fly +
STORMBADGE, `EVENT_FOUGHT_SNORLAX` set, `ENGINE_FLYPOINT_PALLET` and
`ENGINE_FLYPOINT_VIRIDIAN` already visited.
| # | Map | Target | Intent | Precondition | Postcondition |
|---|---|---|---|---|---|
| 1 | anywhere outdoor | `SPAWN_PALLET` | Fly | STORMBADGE, `ENGINE_FLYPOINT_PALLET` | player at `PALLET_TOWN` (5,6) |
| 2 | `PALLET_TOWN` | (12,11) | walk (5,6)->(9,6)->(9,12)->(12,12)->(12,11) | - | warp to `OAKS_LAB` (4,11) |
| 3 | `OAKS_LAB` | (4,3) facing UP | walk, then talk to `OAKSLAB_OAK` | `VAR_BADGES == 16` | `EVENT_OPENED_MT_SILVER` set; expect `EVENT_TALKED_TO_OAK_IN_KANTO` on the first visit and a `ProfOaksPCBoot` dex-rating textbox chain |
| 4 | `OAKS_LAB` | (4,11) | walk into warp 1 | - | back on `PALLET_TOWN` (12,11) |
| 5 | anywhere outdoor | `SPAWN_VIRIDIAN` | Fly | `ENGINE_FLYPOINT_VIRIDIAN` | player at `VIRIDIAN_CITY` (23,26) |
| 6 (optional) | `VIRIDIAN_CITY` | (9,22) facing LEFT | walk (23,26)->(14,26)->(14,23)->(10,23)->(10,22)->(9,22), then Cut | Cut + HIVEBADGE, `EVENT_GOT_TM42_DREAM_EATER` clear | block at (8,22) replaced with `$6f` |
| 7 (optional) | `VIRIDIAN_CITY` | `VIRIDIANCITY_FISHER` at (6,23), talk from (6,22) facing DOWN | talk | bag has room | TM42 Dream Eater; `EVENT_GOT_TM42_DREAM_EATER` set |
| 8 | `VIRIDIAN_CITY` | west edge, x=0 at y=15 | walk (0,17)->(0,15) then step LEFT | - | map change to `ROUTE_22` (39,7) |
| 9 | `ROUTE_22` | (35,12) | walk (39,7)->(35,7)->(35,11)->(35,12) | - | standing on a `COLL_HOP_DOWN` tile |
| 10 | `ROUTE_22` | press DOWN | ledge hop | - | player at (35,14) |
| 11 | `ROUTE_22` | (33,12) | walk (35,14)->(33,14)->(33,13)->(33,12) | - | - |
| 12 | `ROUTE_22` | (31,5) | walk (33,12)->(33,11)->(31,11)->(31,8)->(31,7)->(31,6)->(31,5) - crosses the tall grass at (30..33, 8..11) | - | expect wild encounters, 10% per step in grass |
| 13 | `ROUTE_22` | (18,6) | walk west along y=5 to (18,5), then DOWN onto (18,6) | - | standing on a ledge tile |
| 14 | `ROUTE_22` | press DOWN | ledge hop | - | player at (18,8), in the west grass patch |
| 15 | `ROUTE_22` | (13,5) | walk (18,8)->(18,12)->(14,12)->(14,8)->(13,8)->(13,7)->(13,6)->(13,5) | - | warp to `VICTORY_ROAD_GATE` (17,7) |
| 16 | `VICTORY_ROAD_GATE` | (1,7) | walk (17,7)->(17,5)->west along y=5->(1,5)->(1,7) | `EVENT_OPENED_MT_SILVER` **and** `EVENT_FOUGHT_SNORLAX` set (both black belts hidden) | warp to `ROUTE_28` (33,5) |
| 17 | `ROUTE_28` | (0,12) | walk (33,5)->(33,6)->(28,6)->(28,8), hop (27,8)->(27,10), west y=10 to (18,10)->(18,9)->(10,9)->(10,10)->(8,10), hop ->(8,12), west to (0,12) | - | map change to `SILVER_CAVE_OUTSIDE` (39,30) |
| 18 | `SILVER_CAVE_OUTSIDE` | (23,20) | walk north-west to the Pokecenter door | - | `ENGINE_FLYPOINT_SILVER_CAVE` set by `MAPCALLBACK_NEWMAP` |
| 19 | `SILVER_CAVE_OUTSIDE` | (23,19) | step UP into warp 1 | - | `SILVER_CAVE_POKECENTER_1F` (3,7) |
| 20 | `SILVER_CAVE_POKECENTER_1F` | nurse at (3,1), talk from (3,2) facing UP | heal | - | party restored |
| 21 | `SILVER_CAVE_POKECENTER_1F` | (3,7) | step DOWN into warp 1 | - | `SILVER_CAVE_OUTSIDE` (23,19) |
| 22 | `SILVER_CAVE_OUTSIDE` | (30,24) facing RIGHT | walk (23,20)->(25,20)->(25,22)->(30,22)->(30,24), then Cut | Cut + HIVEBADGE | (31,24) replaced with `$4c` |
| 23 | `SILVER_CAVE_OUTSIDE` | (34,24) facing UP | walk (31,24)->(31,25)->(34,25)->(34,24), then Cut | Cut + HIVEBADGE | (34,23) replaced with `$6e` |
| 24 | `SILVER_CAVE_OUTSIDE` | (39,21) | walk (34,23)->(34,21)->(39,21), step RIGHT | - | map change to `ROUTE_28` (0,3) |
| 25 | `ROUTE_28` | (25,3) facing UP | walk (0,3)->(5,3)->(5,4)->(7,4)->(21,4)->(21,3)->(25,3), press A | `EVENT_ROUTE_28_HIDDEN_RARE_CANDY` clear | Rare Candy; flag set |
| 26 | `ROUTE_28` | (7,3) | walk back west along y=4 to (7,4), step UP | - | warp to `ROUTE_28_STEEL_WING_HOUSE` (2,7) |
| 27 | `ROUTE_28_STEEL_WING_HOUSE` | `Celebrity` at (2,3), talk from (2,4) facing UP | talk | bag has room, `EVENT_GOT_TM47_STEEL_WING` clear | TM47 Steel Wing; flag set |
| 28 | `ROUTE_28_STEEL_WING_HOUSE` | (2,7) | step DOWN into warp 1 | - | `ROUTE_28` (7,3) |
| 29 | `ROUTE_28` | any y=4 ledge (x=4..9) | walk to it and press DOWN | - | one-way drop into the Route 28 main body (grass at y=6) - the strip cannot be re-entered from here |
| 30 | - | shopping run | Fly to Mahogany, buy, Fly back to `SPAWN_MT_SILVER` | `ENGINE_FLYPOINT_SILVER_CAVE` set in step 18 | ready for `SILVER_CAVE_OUTSIDE` warp 2 at (18,11) - next section |
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Map headers, dimensions, warps / coord / bg / object event tables for all of these maps | `src/import/RomExtractorGen2.lua:787-862` (`warps`, `coordEvents`, `bgEvents`, `objects`), consumed by `src/world/gen2/Map.lua` | implemented, and generic - no per-map work needed |
| Map connections and the offset landing maths | `src/world/gen2/Map.lua:71-92` `Map.connectionLanding` | implemented (same block-offset maths this doc's route uses) |
| `object_event` event-flag masking (the black-belt gate) | `src/world/gen2/Events.lua:40-44` `Events:objectVisible`, called from `src/world/gen2/World.lua:5106` and `:5122` | implemented, with the correct "flag set -> hidden" polarity |
| `Oak`'s opcode chain (`checkevent` / `readvar` / `setevent` / `special`) | `src/script/gen2/Vm.lua:187` (`checkevent`), `:670` (`readvar`), `src/world/gen2/World.lua:1240-1245` (`VAR_BADGES` = popcount of both badge bytes) | implemented |
| `special ProfOaksPCBoot` (the dex rating after every Oak conversation) | `src/script/gen2/Specials.lua:1778` `H.ProfOaksPCBoot` | implemented, including the `OakRatings` table and the fanfare wait |
| `verbosegiveitem` for TM42 / TM47, including the bag-full `iffalse` arm | `src/script/gen2/Vm.lua:490-498` | implemented |
| `hiddenitem` (Route 28 Rare Candy) via `BGEVENT_ITEM` | `src/world/gen2/HiddenItems.lua` (whole file), wired at `src/world/gen2/World.lua:30` and `:3423` | implemented |
| `jumpstd` for `PokecenterNurseScript` / `MagazineBookshelfScript` / `DifficultBookshelfScript` / `PokecenterSignScript` / `MartSignScript` | `src/script/gen2/Vm.lua:742-747` | implemented (std scripts are extracted and run through the same VM) |
| Cut: badge + party-move check, `CutTreeBlockPointers` lookup, block replacement | `src/world/gen2/FieldMoves.lua:101-107` (badge map), `:183-237` (`field_move_blocks.asm` transcribed), `:454`, `:605-619` | implemented |
| Fly: STORMBADGE check, flypoint table, `ENGINE_FLYPOINT_*` bits | `src/world/gen2/FieldMoves.lua:336-380` (`FLYPOINTS`, incl. `SPAWN_PALLET` 52, `SPAWN_VIRIDIAN` 53, `SPAWN_MT_SILVER` 75), `src/ui/TownMap.lua:146-208`, `src/world/gen2/World.lua:4292` | implemented |
| Scene scripts / `setscene` / coord-event dispatch (Victory Road Gate badge check) | `src/world/gen2/World.lua:470` (`mapScenes`), `:5013-5033` | implemented |
| `cry FEAROW` on the celebrity's pet | `src/script/gen2/Vm.lua:621` | implemented |
| **Ledge hopping (`COLL_HOP_DOWN` / `HOP_LEFT` / `HOP_RIGHT` / `HOP_DOWN_LEFT` / `HOP_DOWN_RIGHT`)** | `src/world/gen2/Player.lua:50-83` `Player:tryMove` - plain single-cell step, no `CheckLedge` equivalent; `src/world/gen2/Permissions.lua` maps `$a0..$a7` to `LAND` so the tiles are walkable but never jump | **missing.** Route 22 cannot be crossed without it: `(35,12)` and `(18,6)` are ledges over `COLL_WALL`, and there is no alternative route to the gate warp. Route 28's west strip also exits only by ledge |
| Route 22 / Route 28 / Silver Cave wild tables | generic (`data/wild/*` extracted by `RomExtractorGen2.lua`) | implemented as data; not separately verified for these three maps |
| A driver that walks any of this | `tests/drivers/gold_*.lua` (25 drivers; none touches Kanto or Mt. Silver) | missing |
## 6. Unresolved / verify by hand
- **"Items on Route 22: TM42 Dream Eater"** - the asm puts the Dream Eater NPC on
`MAP_VIRIDIAN_CITY` at (6,23) (`maps/ViridianCity.asm:239`). `maps/Route22.asm`
has an empty `def_object_events` and no item bg event at all. The FAQ's prose
("If you cut the tree to the west of Viridian City") is right; its item header
is wrong.
- **"There's also a Pokémon Center here" (Route 28)** - there is no Pokecenter on
`MAP_ROUTE_28`. The nearest one is `MAP_SILVER_CAVE_POKECENTER_1F`, entered from
`MAP_SILVER_CAVE_OUTSIDE` warp 1 at (23,19), one map west.
- **"Items [Route 28]: TM47 Steel Wing"** - TM47 is inside
`MAP_ROUTE_28_STEEL_WING_HOUSE`. Route 28 itself has one item, the hidden Rare
Candy at (25,2), which the FAQ never mentions.
- **"#232 Donphan (Silver only)"** - correct as far as it goes, but the FAQ omits
what Gold puts in that slot (Ursaring, 20%) and omits Doduo/Dodrio entirely.
It also does not say Sneasel is nite-only.
- **Route 22 Pokemon list** - the FAQ lists only Spearow, Rattata and Poliwag.
`data/wild/kanto_grass.asm:1033` also has Doduo (10%), Ponyta (5%) and Fearow
(4%+1%) in the morn/day tables.
- **The FAQ's "keep heading north, west, then north"** is a fair prose summary but
omits the two mandatory ledge hops. The waypoint list in section 4 was derived
by BFS over `Route22.blk` against `data/tilesets/kanto_collision.asm` with the
ledge rule "step onto a `COLL_HOP_*` tile, then a matching press moves two
cells". That rule was inferred from the collision permission table
(`$a0..$a7` are `LAND_TILE`) and from the fact that every ledge on these maps
has `COLL_WALL` directly beyond it; the exact `DoPlayerMovement` ledge branch
in `engine/overworld/player_movement.asm` was **not** read line by line.
Worth confirming before a driver depends on the two-cell jump distance.
- **Route 28's "unreachable" rows** - `SILVER_CAVE_OUTSIDE`'s east edge is also
walkable at y=14..18 and y=33..35, which map to `ROUTE_28` y=-4..0 and y=15..17.
Those Route 28 rows are outside the component reachable from either the gate or
the west strip, so they look like filler; not chased further.
- **Mt. Silver entry itself** - `maps/SilverCaveOutside.asm` has no coord event,
no callback check and no NPC guarding warp 2 at (18,11), and
`EVENT_OPENED_MT_SILVER` is referenced in exactly two places
(`maps/OaksLab.asm:18,43` and `maps/VictoryRoadGate.asm:118`). So the flag gates
the *corridor*, not the cave mouth. If a save is hacked past the gate NPC, the
cave is open.
- **Party/level requirements** - the FAQ's "at least 10 Revives, 10 Hyper Potions,
5 Escape Ropes, 5 Max Repels, and 50 Ultra Balls" is advice, not a coded gate.
Nothing in the asm checks the bag here.
@@ -0,0 +1,611 @@
# Section 30 - Lapras
Source: `../section-30-lapras.txt`
Maps covered: `MAP_UNION_CAVE_1F`, `MAP_UNION_CAVE_B1F`, `MAP_UNION_CAVE_B2F`
(approach only: `MAP_ROUTE_32`, whose warp into the cave is cited but which
belongs to section 03)
Badges / key milestones in this section: no badge. The milestone is the **static
level-20 Lapras** on Union Cave B2F, which only exists on a Friday and only once
per in-game day (`ENGINE_UNION_CAVE_LAPRAS`). Five optional trainers
(Andrew, Calvin, Nick, Gwen, Emma) and two item balls sit on the way in.
This is a *revisit* section: Union Cave 1F/B1F were first walked in section 03.
Everything here is reachable only with Surf, i.e. after the Fog Badge.
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 1 | `MAP_ROUTE_32` | `maps/Route32.asm` | walked south from Violet City (`connection south, Route32, ROUTE_32, 0` in `data/maps/attributes.asm`) | `warp_event 6, 79, UNION_CAVE_1F, 4` | "head to Violet City and then go south through Route 32 into Union Cave" |
| 2 | `MAP_UNION_CAVE_1F` | `maps/UnionCave1F.asm` | warp 4 at `17, 3` (from Route 32) | `warp_event 3, 33, UNION_CAVE_B1F, 4` | walk past Daniel / Russell / Bill, surf the south-west lake, take the bottom-left ladder |
| 3 | `MAP_UNION_CAVE_B1F` | `maps/UnionCaveB1F.asm` | warp 4 at `3, 33` | `warp_event 17, 31, UNION_CAVE_B2F, 1` | Pokemaniac Andrew stands at `5, 32` right next to the arrival ladder; surf east to Calvin at `17, 30`, then the ladder beside him |
| 4 | `MAP_UNION_CAVE_B2F` | `maps/UnionCaveB2F.asm` | warp 1 at `5, 3` | same warp back up, or Escape Rope / Dig | Hyper Potion, Nick, Gwen, Emma, then the Lapras object at `11, 31` |
The walkthrough ends inside Union Cave B2F ("You can then use Escape Rope to
leave Union Cave"); it does not spill into a neighbouring section's map.
## 2. Maps
### MAP_UNION_CAVE_1F
- Script: `maps/UnionCave1F.asm`
- Blocks: `maps/UnionCave1F.blk`
- Header: `data/maps/maps.asm:107` -> `map UnionCave1F, TILESET_CAVE, CAVE, LANDMARK_UNION_CAVE, MUSIC_UNION_CAVE, TRUE, PALETTE_NITE, FISHGROUP_LAKE`
(the `TRUE` field is the phone-service flag: **no phone calls in Union Cave**)
- Dimensions: `constants/map_constants.asm:94` -> `map_const UNION_CAVE_1F, 10, 18` (10x18 blocks = 20x36 walk cells)
- Attributes: `data/maps/attributes.asm:426` -> `map_attributes UnionCave1F, UNION_CAVE_1F, $09`
- Connections: none (no `connection` rows follow the attributes line)
- Map group: `MapGroup_Dungeons` (`data/maps/maps.asm:77`)
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 5 | 19 | `UNION_CAVE_B1F` | 3 |
| 2 | 3 | 33 | `UNION_CAVE_B1F` | 4 |
| 3 | 17 | 31 | `ROUTE_33` | 1 |
| 4 | 17 | 3 | `ROUTE_32` | 4 |
**Coord events** (`def_coord_events`)
None - the block is empty.
**BG events** (`def_bg_events`)
None. (`UnionCave1FUnusedSign` / `UnionCave1FUnusedSignText` exist in the file
but are marked `; unreferenced` and are not in any `bg_event` row.)
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `UNIONCAVE1F_POKEFAN_M1` | `SPRITE_POKEFAN_M` | 4 | 4 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER`, sight 1 | `TrainerHikerDaniel` | -1 |
| `UNIONCAVE1F_SUPER_NERD` | `SPRITE_SUPER_NERD` | 4 | 21 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_TRAINER`, sight 2 | `TrainerPokemaniacLarry` | -1 |
| `UNIONCAVE1F_POKEFAN_M2` | `SPRITE_POKEFAN_M` | 15 | 8 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerHikerRussell` | -1 |
| `UNIONCAVE1F_FISHER1` | `SPRITE_FISHER` | 16 | 31 | `SPRITEMOVEDATA_STANDING_UP` | `OBJECTTYPE_TRAINER`, sight 4 | `TrainerFirebreatherRay` | -1 |
| `UNIONCAVE1F_FISHER2` | `SPRITE_FISHER` | 15 | 15 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER`, sight 2 | `TrainerFirebreatherBill` | -1 |
| `UNIONCAVE1F_POKE_BALL1` | `SPRITE_POKE_BALL` | 17 | 21 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `UnionCave1FGreatBall` | `EVENT_UNION_CAVE_1F_GREAT_BALL` |
| `UNIONCAVE1F_POKE_BALL2` | `SPRITE_POKE_BALL` | 4 | 2 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `UnionCave1FPotion` | `EVENT_UNION_CAVE_1F_POTION` |
| `UNIONCAVE1F_POKE_BALL3` | `SPRITE_POKE_BALL` | 4 | 17 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `UnionCave1FXAttack` | `EVENT_UNION_CAVE_1F_X_ATTACK` |
| `UNIONCAVE1F_POKE_BALL4` | `SPRITE_POKE_BALL` | 12 | 33 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `UnionCave1FAwakening` | `EVENT_UNION_CAVE_1F_AWAKENING` |
**Scripts of interest**
- `UnionCave1F_MapScripts` - `def_scene_scripts` and `def_callbacks` are both
empty. Nothing on 1F is time- or flag-gated; the floor is pure walking.
- `PokemaniacLarryAfterBattleText` and `FirebreatherBillAfterBattleText` are the
in-game hints the walkthrough is acting on: *"Every Friday, you can hear
#MON roars from deep inside the cave."* and *"On weekends, you can hear
strange roars from deep in the cave."* Only Larry's Friday line matches the
code (see B2F callback below).
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_UNION_CAVE_1F_GREAT_BALL` | `constants/event_flags.asm` | itemball object row | set once the ball is taken; masks the object |
| `EVENT_UNION_CAVE_1F_POTION` | `constants/event_flags.asm` | itemball object row | same |
| `EVENT_UNION_CAVE_1F_X_ATTACK` | `constants/event_flags.asm` | itemball object row | same |
| `EVENT_UNION_CAVE_1F_AWAKENING` | `constants/event_flags.asm` | itemball object row | same |
| `EVENT_BEAT_HIKER_DANIEL`, `EVENT_BEAT_HIKER_RUSSELL`, `EVENT_BEAT_POKEMANIAC_LARRY`, `EVENT_BEAT_FIREBREATHER_BILL`, `EVENT_BEAT_FIREBREATHER_RAY` | `constants/event_flags.asm` | 3rd argument of each `trainer` macro row | set = already beaten, no eyesight trigger |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `GREAT_BALL` | item ball at 17,21 | `UnionCave1FGreatBall` (`itemball GREAT_BALL`) | `EVENT_UNION_CAVE_1F_GREAT_BALL` |
| `POTION` | item ball at 4,2 | `UnionCave1FPotion` | `EVENT_UNION_CAVE_1F_POTION` |
| `X_ATTACK` | item ball at 4,17 | `UnionCave1FXAttack` | `EVENT_UNION_CAVE_1F_X_ATTACK` |
| `AWAKENING` | item ball at 12,33 | `UnionCave1FAwakening` | `EVENT_UNION_CAVE_1F_AWAKENING` |
**Trainers** (all optional on this pass; the walkthrough only mentions walking past them)
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `HIKER`, `DANIEL` | `HIKER` | `DANIEL` | `"DANIEL@"` | `TrainerHikerDaniel` | phone service disabled on this map (`TRUE` in the header) |
| `HIKER`, `RUSSELL` | `HIKER` | `RUSSELL` | `"RUSSELL@"` | `TrainerHikerRussell` | as above |
| `POKEMANIAC`, `LARRY` | `POKEMANIAC` | `LARRY` | `"LARRY@"` | `TrainerPokemaniacLarry` | as above |
| `FIREBREATHER`, `BILL` | `FIREBREATHER` | `BILL` | `"BILL@"` | `TrainerFirebreatherBill` | as above |
| `FIREBREATHER`, `RAY` | `FIREBREATHER` | `RAY` | `"RAY@"` | `TrainerFirebreatherRay` | as above |
(Party rows for these five were not transcribed - they belong to section 03 and
the walkthrough tells the bot to walk past them, not fight them.)
**Wild encounters**
Grass/cave, `data/wild/johto_grass.asm:444` `def_grass_wildmons UNION_CAVE_1F`,
rate `6 percent` for morn/day/nite alike. Gold list (identical across all three
time slots):
| slot | level | species |
|---|---|---|
| 1 | 6 | `GEODUDE` |
| 2 | 6 | `SANDSHREW` (Silver: `RATTATA`) |
| 3 | 5 | `ZUBAT` |
| 4 | 4 | `RATTATA` |
| 5 | 7 | `ZUBAT` |
| 6 | 6 | `ONIX` |
| 7 | 6 | `ONIX` |
Water, `data/wild/johto_water.asm:12` `def_water_wildmons UNION_CAVE_1F`, rate
`2 percent`: `15 WOOPER`, `20 QUAGSIRE`, `15 QUAGSIRE`.
Fishing group is `FISHGROUP_LAKE` (header row).
### MAP_UNION_CAVE_B1F
- Script: `maps/UnionCaveB1F.asm`
- Blocks: `maps/UnionCaveB1F.blk`
- Header: `data/maps/maps.asm:108` -> `map UnionCaveB1F, TILESET_CAVE, CAVE, LANDMARK_UNION_CAVE, MUSIC_UNION_CAVE, TRUE, PALETTE_NITE, FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:95` -> `map_const UNION_CAVE_B1F, 10, 18`
- Attributes: `data/maps/attributes.asm:427` -> `map_attributes UnionCaveB1F, UNION_CAVE_B1F, $09`
- Connections: none
- Symbols: `44:5415 UnionCaveB1F_MapScripts`, `44:56df UnionCaveB1F_MapEvents` (`pokegold.sym`)
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 3 | `RUINS_OF_ALPH_OUTSIDE` | 7 |
| 2 | 3 | 11 | `RUINS_OF_ALPH_OUTSIDE` | 8 |
| 3 | 7 | 19 | `UNION_CAVE_1F` | 1 |
| 4 | 3 | 33 | `UNION_CAVE_1F` | 2 |
| 5 | 17 | 31 | `UNION_CAVE_B2F` | 1 |
**Coord events** (`def_coord_events`)
None.
**BG events** (`def_bg_events`)
None.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `UNIONCAVEB1F_POKEFAN_M1` | `SPRITE_POKEFAN_M` | 10 | 4 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER`, sight 1 | `TrainerHikerPhillip` | -1 |
| `UNIONCAVEB1F_POKEFAN_M2` | `SPRITE_POKEFAN_M` | 17 | 10 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerHikerLeonard` | -1 |
| `UNIONCAVEB1F_SUPER_NERD1` | `SPRITE_SUPER_NERD` | 5 | 32 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerPokemaniacAndrew` | -1 |
| `UNIONCAVEB1F_SUPER_NERD2` | `SPRITE_SUPER_NERD` | 17 | 30 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_TRAINER`, sight 3 | `TrainerPokemaniacCalvin` | -1 |
| `UNIONCAVEB1F_POKE_BALL1` | `SPRITE_POKE_BALL` | 2 | 16 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `UnionCaveB1FTMSwift` | `EVENT_UNION_CAVE_B1F_TM_SWIFT` |
| `UNIONCAVEB1F_BOULDER` | `SPRITE_BOULDER` | 7 | 10 | `SPRITEMOVEDATA_STRENGTH_BOULDER` | `OBJECTTYPE_SCRIPT` | `UnionCaveB1FBoulder` | -1 |
| `UNIONCAVEB1F_POKE_BALL2` | `SPRITE_POKE_BALL` | 17 | 23 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `UnionCaveB1FXDefend` | `EVENT_UNION_CAVE_B1F_X_DEFEND` |
**Scripts of interest**
- `UnionCaveB1F_MapScripts` - both `def_scene_scripts` and `def_callbacks` are
empty. Nothing on B1F is gated.
- `TrainerPokemaniacAndrew` (`44:5417`) -
`trainer POKEMANIAC, ANDREW, EVENT_BEAT_POKEMANIAC_ANDREW, PokemaniacAndrewSeenText, PokemaniacAndrewBeatenText, 0, .Script`.
`.Script` is the boilerplate after-battle path:
`endifjustbattled / opentext / writetext PokemaniacAndrewAfterBattleText / waitbutton / closetext / end`.
No flags beyond the beaten flag, no items, no warps.
- `TrainerPokemaniacCalvin` (`44:542b`) - identical shape with
`EVENT_BEAT_POKEMANIAC_CALVIN` and `PokemaniacCalvinAfterBattleText`
("You demonstrated on me!" is `PokemaniacCalvinBeatenText`, matching the
walkthrough quote).
- `UnionCaveB1FBoulder` - `jumpstd StrengthBoulderScript`. Not on the
walkthrough's path to Lapras (it sits at `7, 10`, up near the Ruins of Alph
ladders), but it is the one Strength-gated object on the floor.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_BEAT_POKEMANIAC_ANDREW` | `constants/event_flags.asm:751` | `TrainerPokemaniacAndrew` | set after the win; suppresses the eyesight trigger |
| `EVENT_BEAT_POKEMANIAC_CALVIN` | `constants/event_flags.asm:752` | `TrainerPokemaniacCalvin` | same |
| `EVENT_BEAT_HIKER_PHILLIP`, `EVENT_BEAT_HIKER_LEONARD` | `constants/event_flags.asm` | the two Hiker `trainer` rows | not on the Lapras path |
| `EVENT_UNION_CAVE_B1F_TM_SWIFT` | `constants/event_flags.asm` | itemball object row | one-time TM_SWIFT |
| `EVENT_UNION_CAVE_B1F_X_DEFEND` | `constants/event_flags.asm` | itemball object row | one-time X_DEFEND |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `TM_SWIFT` | item ball at 2,16 | `UnionCaveB1FTMSwift` | `EVENT_UNION_CAVE_B1F_TM_SWIFT` |
| `X_DEFEND` | item ball at 17,23 | `UnionCaveB1FXDefend` | `EVENT_UNION_CAVE_B1F_X_DEFEND` |
Neither is mentioned by the walkthrough; both are on the way to the B2F ladder.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `POKEMANIAC`, `ANDREW` | `POKEMANIAC` (`$1e`, `constants/trainer_constants.asm:245`) | `ANDREW` (id 2) | `"ANDREW@", TRAINERTYPE_NORMAL` at `data/trainers/parties.asm:1086`: L24 `MAROWAK`, L24 `MAROWAK` | `TrainerPokemaniacAndrew` | no phone (map header phone flag `TRUE`) |
| `POKEMANIAC`, `CALVIN` | `POKEMANIAC` | `CALVIN` (id 3) | `"CALVIN@", TRAINERTYPE_NORMAL` at `data/trainers/parties.asm:1092`: L26 `KANGASKHAN` | `TrainerPokemaniacCalvin` | no phone |
| `HIKER`, `PHILLIP` | `HIKER` | `PHILLIP` | `"PHILLIP@"` | `TrainerHikerPhillip` | off-path |
| `HIKER`, `LEONARD` | `HIKER` | `LEONARD` | `"LEONARD@"` | `TrainerHikerLeonard` | off-path |
`TRAINERTYPE_NORMAL` means level+species only: no custom moves, no held items.
Prize money: `POKEMANIAC` base reward is `db 15` (`data/trainers/attributes.asm:179`
`; Pokemaniac`). `ComputeTrainerReward` (`engine/battle/read_trainer_party.asm:300`)
computes `base * wCurPartyLevel` where the level is the **last** party row, and
`WinTrainerBattle` (`engine/battle/core.asm:2292`) pays that out four times
(`ld c, 4` at the `.okay` loop). So Andrew = 15*24*4 = **1440**, Calvin =
15*26*4 = **1560** - both match the walkthrough exactly.
**Wild encounters**
Grass/cave, `data/wild/johto_grass.asm:499` `def_grass_wildmons UNION_CAVE_B1F`,
rate `6 percent` morn/day/nite. Gold list (same in all three slots):
| slot | level | species |
|---|---|---|
| 1 | 8 | `GEODUDE` |
| 2 | 8 | `SANDSHREW` (Silver: `RATTATA`) |
| 3 | 7 | `ZUBAT` |
| 4 | 8 | `ONIX` |
| 5 | 9 | `ZUBAT` |
| 6 | 6 | `RATTATA` |
| 7 | 6 | `RATTATA` |
Water, `data/wild/johto_water.asm:19`, rate `2 percent`: `15 WOOPER`,
`20 QUAGSIRE`, `15 QUAGSIRE`.
### MAP_UNION_CAVE_B2F
This is the section's real content.
- Script: `maps/UnionCaveB2F.asm`
- Blocks: `maps/UnionCaveB2F.blk` (`2b:466d UnionCaveB2F_Blocks`)
- Header: `data/maps/maps.asm:109` -> `map UnionCaveB2F, TILESET_CAVE, CAVE, LANDMARK_UNION_CAVE, MUSIC_UNION_CAVE, TRUE, PALETTE_NITE, FISHGROUP_SHORE`
(note: **`FISHGROUP_SHORE` here, not `FISHGROUP_LAKE` like the two floors above**)
- Dimensions: `constants/map_constants.asm:96` -> `map_const UNION_CAVE_B2F, 10, 18` (20x36 walk cells)
- Attributes: `data/maps/attributes.asm:428` -> `map_attributes UnionCaveB2F, UNION_CAVE_B2F, $09`
- Connections: none
- Symbols: `44:5759 UnionCaveB2F_MapScripts`, `44:575e UnionCaveB2FLaprasCallback`, `44:5770 UnionCaveLapras`, `44:59cb UnionCaveB2F_MapEvents`, `25:595d UnionCaveB2F_MapAttributes`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 5 | 3 | `UNION_CAVE_B1F` | 5 |
One warp. There is no second exit: leaving is either back up this ladder, or
Escape Rope / Dig (the walkthrough's suggestion).
**Coord events** (`def_coord_events`)
None - the block is empty. Nothing on B2F is a trip-wire; every event is an
object you walk into the sight line of or press A on.
**BG events** (`def_bg_events`)
None.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `UNIONCAVEB2F_ROCKER` | `SPRITE_ROCKER` | 17 | 23 | `SPRITEMOVEDATA_STANDING_LEFT` | `OBJECTTYPE_TRAINER`, sight 5, `PAL_NPC_RED` | `TrainerCooltrainermNick` | -1 |
| `UNIONCAVEB2F_COOLTRAINER_F1` | `SPRITE_COOLTRAINER_F` | 5 | 13 | `SPRITEMOVEDATA_SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER`, sight 1, `PAL_NPC_RED` | `TrainerCooltrainerfGwen` | -1 |
| `UNIONCAVEB2F_COOLTRAINER_F2` | `SPRITE_COOLTRAINER_F` | 3 | 28 | `SPRITEMOVEDATA_STANDING_DOWN` | `OBJECTTYPE_TRAINER`, sight 2, `PAL_NPC_RED` | `TrainerCooltrainerfEmma` | -1 |
| `UNIONCAVEB2F_POKE_BALL1` | `SPRITE_POKE_BALL` | 16 | 2 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `UnionCaveB2FElixer` | `EVENT_UNION_CAVE_B2F_ELIXER` |
| `UNIONCAVEB2F_POKE_BALL2` | `SPRITE_POKE_BALL` | 12 | 19 | `SPRITEMOVEDATA_STILL` | `OBJECTTYPE_ITEMBALL` | `UnionCaveB2FHyperPotion` | `EVENT_UNION_CAVE_B2F_HYPER_POTION` |
| `UNIONCAVEB2F_LAPRAS` | `SPRITE_SURF` | 11 | 31 | `SPRITEMOVEDATA_SWIM_WANDER`, radius x=1 y=1, `PAL_NPC_BLUE` | `OBJECTTYPE_SCRIPT` | `UnionCaveLapras` | `EVENT_UNION_CAVE_B2F_LAPRAS` |
Notes a bot needs:
- Lapras uses `SPRITE_SURF` (`constants/sprite_constants.asm:87`, id 53) - the
same overworld sprite the player rides. It is not a Lapras sprite.
- `SPRITEMOVEDATA_SWIM_WANDER` with radius 1,1 means the object **drifts within
one cell of `11, 31`**. A driver must not hard-code an exact tile to face; it
should walk to the neighbourhood and press A at whichever adjacent water cell
the sprite is occupying.
- Its event flag is `EVENT_UNION_CAVE_B2F_LAPRAS`. `CheckObjectFlag`
(`engine/overworld/map_objects_2.asm:31`) masks an object when the flag is
**set**, so "Lapras present" = flag clear.
**Scripts of interest**
`UnionCaveB2F_MapScripts` (`44:5759`)
```
def_scene_scripts ; empty
def_callbacks
callback MAPCALLBACK_OBJECTS, UnionCaveB2FLaprasCallback
```
`UnionCaveB2FLaprasCallback` (`44:575e`) - the whole Friday rule, six opcodes:
```
checkflag ENGINE_UNION_CAVE_LAPRAS
iftrue .NoAppear
readvar VAR_WEEKDAY
ifequal FRIDAY, .Appear
.NoAppear:
disappear UNIONCAVEB2F_LAPRAS
endcallback
.Appear:
appear UNIONCAVEB2F_LAPRAS
endcallback
```
Control flow, spelled out:
1. `checkflag ENGINE_UNION_CAVE_LAPRAS` - a **daily** engine flag
(`constants/engine_flags.asm:107`, in the `wDailyFlags2` block; bit
`DAILYFLAGS2_UNION_CAVE_LAPRAS_F` = 1, `constants/ram_constants.asm:315`).
Set means "already caught/fought it today" -> `.NoAppear`.
2. `readvar VAR_WEEKDAY` (`constants/script_constants.asm:59`, id `$0b`) then
`ifequal FRIDAY` (`constants/ram_constants.asm:218`, `FRIDAY` = 5, Sunday-based).
Only Friday reaches `.Appear`.
3. `appear` clears the object's event flag (`Script_appear`,
`engine/overworld/scripting.asm:879`, `ld b, 0 ; clear`); `disappear` sets it
(`Script_disappear`, `:887`, `ld b, 1 ; set`).
Because the callback is `MAPCALLBACK_OBJECTS`, it re-runs on **every map load**
of B2F (`engine/overworld/map_setup.asm:81` `farcall LoadObjectMasks` follows
it). So a bot that walks out and back in on a non-Friday will find Lapras gone.
`UnionCaveLapras` (`44:5770`) - the encounter itself:
```
faceplayer
cry LAPRAS
loadwildmon LAPRAS, 20
startbattle
disappear UNIONCAVEB2F_LAPRAS
setflag ENGINE_UNION_CAVE_LAPRAS
reloadmapafterbattle
end
```
Load-bearing details:
- `loadwildmon LAPRAS, 20` - a level **20 wild** Lapras, matching the
walkthrough. There is no `writevar VAR_BATTLETYPE` here, so unlike Ho-Oh /
Lugia / the Red Gyarados this is an **ordinary wild battle**: no forced item,
no forced shiny, no `BATTLETYPE_CANLOSE` mercy.
- `disappear` and `setflag` run **unconditionally after the battle**, with no
`iffalse` / `checkflag` guard on the outcome. So Lapras is consumed for the
day whether you caught it, KO'd it, ran, or blacked out mid-battle. That is
exactly why the walkthrough says to save first - a soft reset is the only
recovery.
- `setflag ENGINE_UNION_CAVE_LAPRAS` is the daily flag, not a permanent event.
`CheckDailyResetTimer` (`engine/overworld/time.asm:88`) zeroes `wDailyFlags1`
and `wDailyFlags2` whole once a day has elapsed, so it comes back next Friday.
`TrainerCooltrainermNick` (`44:577f`), `TrainerCooltrainerfGwen` (`44:5793`),
`TrainerCooltrainerfEmma` (`44:57a7`) - all three are the plain
`trainer <class>, <id>, EVENT_BEAT_*, <seen>, <beaten>, 0, .Script` shape with
an `endifjustbattled / opentext / writetext ...AfterBattleText / waitbutton /
closetext / end` follow-up. No items, no warps, no flags beyond their own
`EVENT_BEAT_*`.
`CooltrainerfEmmaAfterBattleText` is the in-map hint for the whole section:
*"Just once a week, a #MON comes to the water's edge. / I wanted to see that
#MON..."*
`UnionCaveB2FElixer` = `itemball ELIXER`; `UnionCaveB2FHyperPotion` =
`itemball HYPER_POTION` (`macros/scripts/maps.asm:155`, two raw bytes
`db item, quantity`, not bytecode).
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_UNION_CAVE_LAPRAS` | `constants/engine_flags.asm:107` (backed by `data/events/engine_flags.asm:117` -> `wDailyFlags2` bit `DAILYFLAGS2_UNION_CAVE_LAPRAS_F`) | read by `UnionCaveB2FLaprasCallback`, set by `UnionCaveLapras` | "Lapras already used up today". Cleared wholesale by `CheckDailyResetTimer` (`engine/overworld/time.asm:88-97`) |
| `EVENT_UNION_CAVE_B2F_LAPRAS` | `constants/event_flags.asm:1282` | written by the callback's `appear`/`disappear` | object visibility mask. **Clear = visible** (`engine/overworld/map_objects_2.asm:31` `CheckObjectFlag`) |
| `VAR_WEEKDAY` | `constants/script_constants.asm:59` (`$0b`) | `readvar` in the callback | `wCurDay`, 0 = `SUNDAY` .. 6 = `SATURDAY`; `FRIDAY` = 5 (`constants/ram_constants.asm:218`) |
| `EVENT_BEAT_COOLTRAINERM_NICK` | `constants/event_flags.asm:857` | `TrainerCooltrainermNick` | beaten flag |
| `EVENT_BEAT_COOLTRAINERF_GWEN` | `constants/event_flags.asm:877` | `TrainerCooltrainerfGwen` | beaten flag |
| `EVENT_BEAT_COOLTRAINERF_EMMA` | `constants/event_flags.asm:891` | `TrainerCooltrainerfEmma` | beaten flag |
| `EVENT_UNION_CAVE_B2F_ELIXER` | `constants/event_flags.asm:1025` | itemball object row | one-time |
| `EVENT_UNION_CAVE_B2F_HYPER_POTION` | `constants/event_flags.asm:1026` | itemball object row | one-time |
There are no `SCENE_*` values on this map: `def_scene_scripts` is empty, so
`wMapScene` is never consulted here.
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `ELIXER` | item ball at 16,2 | `UnionCaveB2FElixer` (`itemball ELIXER`) | `EVENT_UNION_CAVE_B2F_ELIXER` |
| `HYPER_POTION` | item ball at 12,19 | `UnionCaveB2FHyperPotion` (`itemball HYPER_POTION`) | `EVENT_UNION_CAVE_B2F_HYPER_POTION` |
The walkthrough names only the Hyper Potion ("Land on the right side to grab a
Hyper Potion"). The Elixer at `16, 2` sits near the arrival ladder at `5, 3` and
is never mentioned - free pickup for a bot on the way in.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `COOLTRAINERM`, `NICK` | `COOLTRAINERM` (`$1b`, `constants/trainer_constants.asm:184`) | `NICK` (id 1) | `"NICK@", TRAINERTYPE_MOVES` at `data/trainers/parties.asm:742` | `TrainerCooltrainermNick` | no phone (map phone flag `TRUE`) |
| `COOLTRAINERF`, `GWEN` | `COOLTRAINERF` (`$1c`, `:205`) | `GWEN` (id 1) | `"GWEN@", TRAINERTYPE_NORMAL` at `:867` | `TrainerCooltrainerfGwen` | no phone |
| `COOLTRAINERF`, `EMMA` | `COOLTRAINERF` | `EMMA` (id 15) | `"EMMA@", TRAINERTYPE_NORMAL` at `:954` | `TrainerCooltrainerfEmma` | no phone |
Parties verbatim:
`NICK` (`TRAINERTYPE_MOVES` - custom movesets, no items):
| # | level | species | moves |
|---|---|---|---|
| 1 | 26 | `CHARMANDER` | `EMBER`, `SMOKESCREEN`, `RAGE`, `SCARY_FACE` |
| 2 | 26 | `SQUIRTLE` | `WITHDRAW`, `WATER_GUN`, `BITE`, `CURSE` |
| 3 | 26 | `BULBASAUR` | `LEECH_SEED`, `POISONPOWDER`, `SLEEP_POWDER`, `RAZOR_LEAF` |
`GWEN` (`TRAINERTYPE_NORMAL`):
| # | level | species |
|---|---|---|
| 1 | 26 | `EEVEE` |
| 2 | 22 | `FLAREON` |
| 3 | 22 | `VAPOREON` |
| 4 | 22 | `JOLTEON` |
`EMMA` (`TRAINERTYPE_NORMAL`): L28 `POLIWHIRL`, single mon.
Prize money (`base * last-party-level * 4`, see the B1F note): `COOLTRAINERM`
and `COOLTRAINERF` both have `db 12 ; base reward`
(`data/trainers/attributes.asm:161` `; Cooltrainerm`, `:167` `; Cooltrainerf`).
Nick = 12*26*4 = **1248**, Gwen = 12*22*4 = **1056** (last row is the L22
Jolteon), Emma = 12*28*4 = **1344**. All three match the walkthrough.
AI: both Cooltrainer classes carry the full
`AI_BASIC | AI_SETUP | AI_SMART | AI_AGGRESSIVE | AI_CAUTIOUS | AI_STATUS | AI_RISKY`
set and `SWITCH_SOMETIMES`; Pokemaniac is the weaker
`AI_BASIC | AI_SETUP | AI_OFFENSIVE | AI_AGGRESSIVE | AI_STATUS`. None of the
five hold items (`db NO_ITEM, NO_ITEM`).
**Wild encounters**
Grass/cave, `data/wild/johto_grass.asm:554` `def_grass_wildmons UNION_CAVE_B2F`,
rate `4 percent`. This table is **not** split by version, and all three
time-of-day slots are identical:
| slot | level | species |
|---|---|---|
| 1 | 22 | `ZUBAT` |
| 2 | 22 | `RATICATE` |
| 3 | 22 | `GOLBAT` |
| 4 | 21 | `GEODUDE` |
| 5 | 20 | `RATTATA` |
| 6 | 23 | `ONIX` |
| 7 | 23 | `ONIX` |
Water (the surf legs the walkthrough spends most of its time on),
`data/wild/johto_water.asm:26` `def_water_wildmons UNION_CAVE_B2F`, rate
`4 percent` (double the 1F/B1F water rate):
| slot | level | species |
|---|---|---|
| 1 | 15 | `TENTACOOL` |
| 2 | 20 | `QUAGSIRE` |
| 3 | 20 | `TENTACRUEL` |
Fishing group is `FISHGROUP_SHORE` (`data/maps/maps.asm:109`). No headbutt or
rock-smash tables apply - it is a cave with no trees.
The walkthrough's Max Repel advice: `REPEL_STEPS` in the port mirrors
`RepelEffect / SuperRepelEffect / MaxRepelEffect`; MAX_REPEL is 250 steps. With
the B2F encounters capped at level 23 and the lead mon typically above that,
Repel suppresses essentially all of the grass rolls.
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Cannot cross any of the water on 1F / B1F / B2F | `engine/events/overworld.asm:322` `SurfFunction.TrySurf` (`ld de, ENGINE_FOGBADGE / call CheckBadge / jr c, .nofogbadge`), and the walk-into-water path `TrySurfOW` at `:469` | **Fog Badge** plus a party mon that knows `SURF` | beat Morty in Ecruteak Gym (section 07) |
| Lapras object is absent 6 days out of 7 | `maps/UnionCaveB2F.asm:15` `UnionCaveB2FLaprasCallback` -> `readvar VAR_WEEKDAY / ifequal FRIDAY` | in-game weekday must equal `FRIDAY` (`constants/ram_constants.asm:218`, value 5) | set the GB clock / wait for Friday. Re-checked on every B2F map load (`MAPCALLBACK_OBJECTS`) |
| Lapras is absent again after one encounter | same callback, first two opcodes: `checkflag ENGINE_UNION_CAVE_LAPRAS / iftrue .NoAppear`; the flag is set by `UnionCaveLapras` | flag must be clear | `CheckDailyResetTimer` (`engine/overworld/time.asm:88`) zeroes `wDailyFlags1`+`wDailyFlags2` once a real day has passed |
| Lapras is consumed even on a failed attempt | `UnionCaveLapras`: `startbattle` is followed unconditionally by `disappear` + `setflag` - no `iffalse` on `wBattleResult` | none - this is the failure mode, not a gate | save before pressing A; soft-reset on a bad outcome |
| Only one way out of B2F | `maps/UnionCaveB2F.asm` `def_warp_events` has exactly one row (`5, 3 -> UNION_CAVE_B1F, 5`) | - | Escape Rope / Dig short-circuits the return walk (B2F's tileset is `TILESET_CAVE`) |
| Boulder at B1F `7, 10` | `UnionCaveB1FBoulder` -> `jumpstd StrengthBoulderScript` | Strength / Plain Badge | not on the Lapras path; noted for completeness |
Nothing in this section checks a badge count, a key item, or an `EVENT_*`
story flag. The only two real gates are **Surf** and **Friday**.
## 4. Bot checklist
Preconditions for the whole section: Fog Badge, a party mon with `SURF`, the
in-game weekday equal to `FRIDAY`, `ENGINE_UNION_CAVE_LAPRAS` clear, several
Ultra Balls, a status move (Sleep/Paralysis) and a **save written immediately
before the Lapras press**.
| # | Map | Target | Input intent | Precondition | Postcondition |
|---|---|---|---|---|---|
| 1 | `MAP_ROUTE_32` | warp at `6, 79` | walk onto the warp | on Route 32 | now in `MAP_UNION_CAVE_1F` at `17, 3` (warp 4) |
| 2 | `MAP_UNION_CAVE_1F` | - | use `MAX_REPEL` from the pack | have one | 250 steps of encounter suppression |
| 3 | `MAP_UNION_CAVE_1F` | avoid sight lines of `UNIONCAVE1F_POKEFAN_M2` (`15, 8`, sight 3, facing left) and `UNIONCAVE1F_FISHER2` (`15, 15`, sight 2, spinner) | walk south-west | - | `EVENT_BEAT_*` untouched if avoided; the spinner is not reliably avoidable, budget for the battle |
| 4 | `MAP_UNION_CAVE_1F` | south-west water | press A facing water, or walk into it (`TrySurfOW`) | Fog Badge + `SURF` | `wPlayerState = PLAYER_SURF` |
| 5 | `MAP_UNION_CAVE_1F` | warp 2 at `3, 33` | step onto it | - | now in `MAP_UNION_CAVE_B1F` at `3, 33` (warp 4) |
| 6 | `MAP_UNION_CAVE_B1F` | `UNIONCAVEB1F_SUPER_NERD1` at `5, 32` (facing left, sight 3) | walk into the sight line / talk | - | battle Pokemaniac Andrew (2x L24 Marowak); sets `EVENT_BEAT_POKEMANIAC_ANDREW`, +1440 |
| 7 | `MAP_UNION_CAVE_B1F` | surf east along the y~30 water | walk east on water | surfing | reach the eastern shelf |
| 8 | `MAP_UNION_CAVE_B1F` | `UNIONCAVEB1F_SUPER_NERD2` at `17, 30` (facing left, sight 3) | walk into the sight line | - | battle Pokemaniac Calvin (L26 Kangaskhan); sets `EVENT_BEAT_POKEMANIAC_CALVIN`, +1560 |
| 9 | `MAP_UNION_CAVE_B1F` | optional item ball `17, 23` | press A | `EVENT_UNION_CAVE_B1F_X_DEFEND` clear | `X_DEFEND` in bag, flag set |
| 10 | `MAP_UNION_CAVE_B1F` | warp 5 at `17, 31` | step onto it | - | now in `MAP_UNION_CAVE_B2F` at `5, 3`; `MAPCALLBACK_OBJECTS` runs `UnionCaveB2FLaprasCallback` |
| 11 | `MAP_UNION_CAVE_B2F` | **assert** `UNIONCAVEB2F_LAPRAS` is present | read the object list | weekday == FRIDAY and `ENGINE_UNION_CAVE_LAPRAS` clear | if absent, abort the run - nothing later will fix it |
| 12 | `MAP_UNION_CAVE_B2F` | item ball `16, 2` | press A | `EVENT_UNION_CAVE_B2F_ELIXER` clear | `ELIXER` in bag (walkthrough omits this one) |
| 13 | `MAP_UNION_CAVE_B2F` | surf south from the entrance shelf | walk south on water | surfing | approach the y~19-23 shelves |
| 14 | `MAP_UNION_CAVE_B2F` | item ball `12, 19` (right-hand shore) | land, press A | `EVENT_UNION_CAVE_B2F_HYPER_POTION` clear | `HYPER_POTION` in bag |
| 15 | `MAP_UNION_CAVE_B2F` | `UNIONCAVEB2F_ROCKER` at `17, 23` (facing left, sight 5 - the longest on the floor) | walk into the sight line | - | battle Cooltrainer Nick (L26 Charmander/Squirtle/Bulbasaur, custom moves); sets `EVENT_BEAT_COOLTRAINERM_NICK`, +1248 |
| 16 | `MAP_UNION_CAVE_B2F` | cross to the left shore, head north to `UNIONCAVEB2F_COOLTRAINER_F1` at `5, 13` (spinner, sight 1) | walk adjacent | - | battle Cooltrainer Gwen (L26 Eevee, L22 Flareon/Vaporeon/Jolteon); sets `EVENT_BEAT_COOLTRAINERF_GWEN`, +1056 |
| 17 | `MAP_UNION_CAVE_B2F` | walk the left path south to `UNIONCAVEB2F_COOLTRAINER_F2` at `3, 28` (facing down, sight 2) | walk into the sight line | - | battle Cooltrainer Emma (L28 Poliwhirl); sets `EVENT_BEAT_COOLTRAINERF_EMMA`, +1344 |
| 18 | `MAP_UNION_CAVE_B2F` | **save** | open menu -> SAVE | Lapras still present | recoverable checkpoint |
| 19 | `MAP_UNION_CAVE_B2F` | surf east/south-east from `3, 28` toward `11, 31` | walk on water | surfing | the Lapras object drifts within +/-1 cell of `11, 31` (`SWIM_WANDER`, radius 1,1) - re-scan its live position each step |
| 20 | `MAP_UNION_CAVE_B2F` | press A facing `UNIONCAVEB2F_LAPRAS` | talk | object present | `UnionCaveLapras` runs: `cry LAPRAS`, `loadwildmon LAPRAS, 20`, `startbattle` |
| 21 | battle | L20 wild Lapras | inflict sleep/paralysis, chip to yellow/red, throw `ULTRA_BALL`s | - | on any exit: `disappear` + `setflag ENGINE_UNION_CAVE_LAPRAS`, then `reloadmapafterbattle` |
| 22 | - | verify Lapras in party/box | read party | - | if not caught, **reload the step-18 save**; do not walk out and back in, the callback will not re-place it |
| 23 | `MAP_UNION_CAVE_B2F` | use `ESCAPE_ROPE` | pack -> use | in a cave tileset | back at the last Pokemon Center door |
Sight-line arithmetic for steps 6/8/15/16/17: `Trainers.sees` in the port
(`src/world/gen2/Trainers.lua:98`) is the transcription of `home/trainers.asm` -
the NPC must be facing the player's direction and the Chebyshev-free straight
distance must be `<= sight`.
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Union Cave 1F/B1F/B2F map data (headers, blocks, warps, objects) | `src/import/RomExtractorGen2.lua` (generic map/manifest extraction; per-map entries land in the private `data/generated/` cache) | implemented - data-driven, no per-map code |
| `MAPCALLBACK_OBJECTS` dispatch on map load | `src/world/gen2/World.lua:5700` (`self:runMapCallback("MAPCALLBACK_OBJECTS")`, deliberately *before* the sprite rebuild) | implemented |
| `readvar VAR_WEEKDAY` | `src/world/gen2/World.lua:101` (`VAR_WEEKDAY = 0x0b`) and `:1222` (`if varId == VAR_WEEKDAY then return self:weekday() end`) | implemented |
| `checkflag` / `setflag` on `ENGINE_*` | `src/script/gen2/Vm.lua:195` / `:208` | implemented |
| `ENGINE_UNION_CAVE_LAPRAS` as a daily flag | `src/core/gen2/Apricorns.lua:84` (`{ id = 88, name = "ENGINE_UNION_CAVE_LAPRAS" }` in the wDailyFlags1/2 id list) and `:344` (`save.dailyFlags = {}` on the daily wipe) | implemented |
| `appear` / `disappear` object masking | `src/script/gen2/Opcodes.lua:115-116`, consumed by the World object rebuild | implemented |
| `cry` | `src/script/gen2/Vm.lua:621` | implemented |
| `loadwildmon` + `startbattle` + `reloadmapafterbattle` (the whole static-encounter shape) | `src/script/gen2/Vm.lua:836`, `:817`, `:886` | implemented |
| Wild catch rate / Ultra Ball / status bonus | `src/battle/gen2/Catching.lua` (transcribes `PokeBallEffect`, including the two cart bugs) | implemented |
| Water encounter tables while surfing | `src/world/gen2/FieldMoves.lua:175` ("standing in a cave rolls the grass list; surfing rolls the water" ...) + `src/battle/gen2/Encounter.lua` | implemented |
| Surf gate on `ENGINE_FOGBADGE` | `src/world/gen2/FieldMoves.lua:106` (`SURF = "FOG"`) and `FieldMoves.surfFromMenu` at `:481` | implemented |
| Trainer eyesight approach + battle | `src/world/gen2/Trainers.lua:98` (`Trainers.sees`), `src/world/gen2/World.lua:5212` / `:5227` / `:5242` | implemented |
| Trainer prize money (base x level x 4, Mom's split) | `src/battle/gen2/Prize.lua:82` (`Prize.reward`), `:92` (`Prize.rewardLevel`) | implemented - and it already documents the `ld c, 4` that makes the walkthrough's figures right |
| Repel step counting (the Max Repel advice) | `src/world/gen2/World.lua:317` (`REPEL_STEPS = { REPEL = 100, SUPER_REPEL = 200, MAX_REPEL = 250 }`), `:3366` | implemented |
| **Item ball pickup** (`OBJECTTYPE_ITEMBALL`, i.e. the B2F Elixer / Hyper Potion and every ball on 1F/B1F) | extractor stores the item at `src/import/RomExtractorGen2.lua:2969` (`obj.itemball = readItemBall(...)`) and deliberately assigns **no** `scriptKey`; `World:talk` only dispatches on `npc.def.scriptKey` (`src/world/gen2/World.lua:5276`) | **missing** - no consumer of `obj.itemball` exists anywhere under `src/`. Pressing A on a Poke Ball object currently does nothing |
| Escape Rope from a cave | `src/ui/PartyMenu.lua:634` is the Gen 1 DIG/ESCAPE_ROPE path; no Gen 2 equivalent found under `src/core/gen2/` or `src/world/gen2/` | **unverified / likely missing for Gen 2** - a driver should warp out manually |
| A driver exercising the weekday-gated `MAPCALLBACK_OBJECTS` shape | `tests/drivers/gold_map_callbacks.lua:117-156` covers the same pattern on Route 29 (Tuscany, Tuesday) | implemented for the pattern, **not** for Union Cave B2F specifically - no `tests/drivers/gold_*` driver mentions Union Cave or Lapras |
Honest summary: everything the Lapras encounter itself needs is in place
(callback dispatch, weekday var, daily flags, `loadwildmon`/`startbattle`,
catching). The two gaps that would bite a bot on this route are **item balls
are not pickable** and **no Gen 2 Escape Rope**, neither of which blocks Lapras.
## 6. Unresolved / verify by hand
- **"Lapras only appears on Friday" vs. the 1F NPC hints.** The code checks
exactly `FRIDAY` (`UnionCaveB2FLaprasCallback`). But
`FirebreatherBillAfterBattleText` on 1F says *"On weekends, you can hear
strange roars from deep in the cave"* while `PokemaniacLarryAfterBattleText`
says *"Every Friday"*. Larry is right, Bill's line is flavour that contradicts
the callback. The walkthrough follows Larry and is correct.
- **"once you've gotten HM Surf".** The asm imposes no HM/badge check on the
Lapras callback or on `UnionCaveLapras`; Surf is a *reachability* requirement
(B2F's Lapras sits at `11, 31` surrounded by water and the approach legs on
1F/B1F cross water), enforced only by `SurfFunction.TrySurf`'s
`ENGINE_FOGBADGE` check. The walkthrough's phrasing implies a scripted gate
that does not exist.
- **EXP figures.** The walkthrough quotes per-mon EXP (637 for a L24 Marowak,
975 for a L26 Kangaskhan, 355/511/924/928/933/786, and two "?" entries). EXP
is computed at runtime from base EXP x level, not stored per trainer, so none
of these could be pinned to a table row. They are plausible but unverified
here; `data/pokemon/base_stats/*.asm` would be the place to check.
- **Party ordering.** The walkthrough lists Nick as Charmander / Bulbasaur /
Squirtle and Gwen as Eevee / Vaporeon / Jolteon / Flareon. `parties.asm` has
Nick as Charmander / **Squirtle** / **Bulbasaur** (`:742`) and Gwen as Eevee /
**Flareon** / **Vaporeon** / **Jolteon** (`:867`). The sets match; the order
does not. A bot planning lead matchups should use the asm order (it is also
what decides the prize level: Gwen's last row is Jolteon L22).
- **"Head almost all the way through till you see the guy on the cliff still
spinning around, which is the third trainer that you spot."** Read against the
1F object table this is `UNIONCAVE1F_FISHER2` (Firebreather Bill,
`15, 15`, `SPRITEMOVEDATA_SPINRANDOM_FAST`), with Hiker Daniel (`4, 4`) and
Hiker Russell (`15, 8`) as the first two. That reading is an inference from
coordinates and movement data, not something the asm states; the exact walking
order depends on the `.blk` layout, which was not decoded here.
- **Which 1F ladder the walkthrough means.** "Down and to the left is a lake.
Surf past it and head down the ladder" is read as 1F warp 2 (`3, 33` ->
`UNION_CAVE_B1F` warp 4 at `3, 33`), because Andrew stands at B1F `5, 32`,
immediately beside that arrival point. 1F warp 1 (`5, 19` -> B1F warp 3 at
`7, 19`) would land nowhere near him. Verify against the block layout if a
driver's pathing disagrees.
- **B2F "right side" / "left side" landings.** The prose distinguishes a right
and a left shore; the object coordinates (`12, 19` Hyper Potion and `17, 23`
Nick on the right, `5, 13` Gwen and `3, 28` Emma on the left) are consistent
with that, but the actual water/land split comes from `UnionCaveB2F.blk`,
which was not decoded.
- **Elixer at B2F `16, 2`.** Present in the asm, absent from the walkthrough.
Not a contradiction, just an omission worth picking up.
@@ -0,0 +1,715 @@
# Section 31 - Mount Silver and Red
Source: `../section-31-mount-silver-and-red.txt`
Maps covered: `MAP_SILVER_CAVE_OUTSIDE`, `MAP_SILVER_CAVE_POKECENTER_1F`,
`MAP_SILVER_CAVE_ROOM_1`, `MAP_SILVER_CAVE_ROOM_2`, `MAP_SILVER_CAVE_ITEM_ROOMS`,
`MAP_SILVER_CAVE_ROOM_3` (plus `MAP_VICTORY_ROAD_GATE`, cited only as the gate
that lets you reach Route 28 in the first place).
Badges / key milestones in this section: no badge. The milestone is the `RED`
trainer battle in `maps/SilverCaveRoom3.asm` and the `credits` opcode it ends
on - the last scripted event in the game.
Conventions reminder: disassembly paths are relative to the pokegold checkout
root; port paths are relative to this repo root; coordinates are the raw asm
values, i.e. **map cell coordinates**, 0-based from the map's top-left, the same
space `warp_event` / `bg_event` / `coord_event` use. `object_event` rows are
written in that same space (the `+ 4` in the `object_event` macro,
`macros/scripts/maps.asm` line 130, is the internal RAM border offset, not
something you add when reading the file).
---
## 1. Route order
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 0 | `MAP_ROUTE_28` | `maps/Route28.asm` (previous section) | `VictoryRoadGate` warps 7/8 at (1,7)/(2,7) | west map connection | "Head west and up from there and you'll be on Mt. Silver." Route 28 itself belongs to the previous section; only the hop off its west edge is in scope here. |
| 1 | `MAP_SILVER_CAVE_OUTSIDE` | `maps/SilverCaveOutside.asm` | east connection from `Route28` (`data/maps/attributes.asm:165`, `connection east, Route28, ROUTE_28, 9`) | warp 2 at (18,11) | Arrive on Mt. Silver; `MAPCALLBACK_NEWMAP` registers the fly point. Optional detour into the Pokémon Center. |
| 2 | `MAP_SILVER_CAVE_POKECENTER_1F` | `maps/SilverCavePokecenter1F.asm` | `SilverCaveOutside` warp 1 at (23,19) | warps 1/2 at (3,7)/(4,7) | Heal before the climb; the granny is pure flavour. |
| 3 | `MAP_SILVER_CAVE_ROOM_1` | `maps/SilverCaveRoom1.asm` | `SilverCaveOutside` warp 2 at (18,11) | warp 2 at (15,1) | "When you get in, use HM Flash." Escape Rope, X Accuracy, Max Elixer, two hidden items. |
| 4 | `MAP_SILVER_CAVE_ROOM_2` | `maps/SilverCaveRoom2.asm` | `SilverCaveRoom1` warp 2 at (15,1) -> lands on warp 1 at (17,31) | warp 2 at (11,5) | The Surf + Waterfall floor. Two side warps into `SilverCaveItemRooms`. |
| 5 | `MAP_SILVER_CAVE_ITEM_ROOMS` | `maps/SilverCaveItemRooms.asm` | `SilverCaveRoom2` warp 3 at (13,21) or warp 4 at (23,3) | warps 1/2 at (13,3)/(7,15) | "use Surf on the lake, and then use Waterfall, you can go through an entrance to grab a Full Restore." Two *disconnected* rooms in one map. |
| 6 | `MAP_SILVER_CAVE_ROOM_3` | `maps/SilverCaveRoom3.asm` | `SilverCaveRoom2` warp 2 at (11,5) -> lands on warp 1 at (9,33) | (no exit warp other than back to Room 2) | "keep going up, and you'll find a trainer." Red at (9,10); beating him runs `credits`. |
After `credits` the game does not return to the overworld normally: `Script_credits`
(`engine/overworld/scripting.asm:2215`) farcalls `RedCredits`
(`engine/events/halloffame.asm:34`), which sets `wSpawnAfterChampion = SPAWN_RED`
and jumps to `Credits`. `FinishContinueFunction` (`engine/menus/intro_menu.asm`)
sees `SPAWN_RED` on the way back out and calls `SpawnAfterRed`, which sets
`wDefaultSpawnpoint = SPAWN_MT_SILVER` and `hMapEntryMethod = MAPSETUP_WARP`, i.e.
you respawn at `data/maps/spawn_points.asm` -> `spawn SILVER_CAVE_OUTSIDE, 23, 20`,
the cell directly below the Pokémon Center door.
---
## 2. Maps
### MAP_SILVER_CAVE_OUTSIDE
- Script: `maps/SilverCaveOutside.asm`
- Blocks: `maps/SilverCaveOutside.blk` (20 x 18 blocks = 40 x 36 cells)
- Header: `data/maps/maps.asm:397` ->
`map SilverCaveOutside, TILESET_KANTO, TOWN, LANDMARK_SILVER_CAVE, MUSIC_INDIGO_PLATEAU, FALSE, PALETTE_AUTO, FISHGROUP_POND`
(fields per the `map` macro at `data/maps/maps.asm:1`: tileset, environment,
location, music, phone-service flag, time-of-day palette, fishing group)
- Dimensions / attributes: `constants/map_constants.asm:367`
(`map_const SILVER_CAVE_OUTSIDE, 20, 18`), group `SILVER` = 19, map id 2
- Connections: `data/maps/attributes.asm:164-165` -
`map_attributes SilverCaveOutside, SILVER_CAVE_OUTSIDE, $2c` /
`connection east, Route28, ROUTE_28, 9`. No north/south/west connection.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 23 | 19 | `SILVER_CAVE_POKECENTER_1F` | 1 |
| 2 | 18 | 11 | `SILVER_CAVE_ROOM_1` | 1 |
**Coord events** (`def_coord_events`)
None (the `def_coord_events` block is empty).
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 24 | 19 | `BGEVENT_READ` | `MtSilverPokecenterSign` -> `jumpstd PokecenterSignScript` |
| 17 | 13 | `BGEVENT_READ` | `MtSilverSign` -> `MtSilverSignText` ("MT.SILVER") |
| 9 | 25 | `BGEVENT_ITEM` | `SilverCaveOutsideHiddenFullRestore` -> `hiddenitem FULL_RESTORE, EVENT_SILVER_CAVE_OUTSIDE_HIDDEN_FULL_RESTORE` |
**Object events** (`def_object_events`)
None. `def_object_events` is the last line of the file with no rows after it -
there is not a single NPC or item ball on the outdoor map.
**Scripts of interest**
- `SilverCaveOutside_MapScripts` (`47`-bank sibling; symbol `49:5f15`) has an
empty `def_scene_scripts` and one callback:
`callback MAPCALLBACK_NEWMAP, SilverCaveOutsideFlypointCallback`.
- `SilverCaveOutsideFlypointCallback` (`49:5f1a`) is two opcodes:
`setflag ENGINE_FLYPOINT_SILVER_CAVE` / `endcallback`. Simply *entering* the
map unlocks Fly to Mt. Silver; there is no NPC or sign to talk to.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `ENGINE_FLYPOINT_SILVER_CAVE` | `constants/engine_flags.asm:90` | set by `SilverCaveOutsideFlypointCallback` | Fly destination `LANDMARK_SILVER_CAVE` (`data/maps/flypoints.asm:16` -> `SPAWN_MT_SILVER`) becomes selectable. Postcondition of first entry. |
| `EVENT_SILVER_CAVE_OUTSIDE_HIDDEN_FULL_RESTORE` | `constants/event_flags.asm:194` | `hiddenitem` at bg_event (9,25) | One-shot guard on the hidden Full Restore. |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `FULL_RESTORE` | hidden, needs a face-and-A or ITEMFINDER sweep at (9,25) | `bg_event 9, 25, BGEVENT_ITEM, SilverCaveOutsideHiddenFullRestore` | `EVENT_SILVER_CAVE_OUTSIDE_HIDDEN_FULL_RESTORE` |
**Trainers**
None.
**Wild encounters**
`data/wild/johto_grass.asm:2319`, `def_grass_wildmons SILVER_CAVE_OUTSIDE`,
encounter rate `10 percent` for morn/day/nite alike. Gold column
(`IF DEF(_GOLD)`); the Silver column swaps `URSARING` -> `DONPHAN` and is
otherwise identical.
| slot | morn | day | nite |
|---|---|---|---|
| 1 | 41 TANGELA | 41 TANGELA | 41 TANGELA |
| 2 | 42 PONYTA | 42 PONYTA | 42 PONYTA |
| 3 | 42 URSARING (Silver: DONPHAN) | 42 URSARING (Silver: DONPHAN) | 42 URSARING (Silver: DONPHAN) |
| 4 | 44 RAPIDASH | 44 RAPIDASH | 44 RAPIDASH |
| 5 | 41 DODUO | 41 DODUO | 38 SNEASEL |
| 6 | 43 DODRIO | 43 DODRIO | 42 SNEASEL |
| 7 | 43 DODRIO | 43 DODRIO | 42 SNEASEL |
Water (`data/wild/johto_water.asm:271`, `def_water_wildmons SILVER_CAVE_OUTSIDE`,
`2 percent`): 35 POLIWHIRL / 40 POLIWHIRL / 35 POLIWAG.
Fishing group `FISHGROUP_POND` (`data/wild/fish.asm:15`). Headbutt:
`data/wild/treemon_maps.asm:39` -> `TREEMON_SET_NONE`, so no headbutt table.
### MAP_SILVER_CAVE_POKECENTER_1F
- Script: `maps/SilverCavePokecenter1F.asm`
- Blocks: `maps/SilverCavePokecenter1F.blk` (symbol `2b:42e8 SilverCavePokecenter1F_Blocks`)
- Header: `data/maps/maps.asm:398` ->
`map SilverCavePokecenter1F, TILESET_POKECENTER, INDOOR, LANDMARK_SILVER_CAVE, MUSIC_POKEMON_CENTER, FALSE, PALETTE_DAY, FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:368` (`map_const SILVER_CAVE_POKECENTER_1F, 5, 4`), group `SILVER` = 19, map id 3
- Connections: none
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 3 | 7 | `SILVER_CAVE_OUTSIDE` | 1 |
| 2 | 4 | 7 | `SILVER_CAVE_OUTSIDE` | 1 |
| 3 | 0 | 7 | `POKECENTER_2F` | 1 |
**Coord events** / **BG events**: both blocks are empty.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `SILVERCAVEPOKECENTER1F_NURSE` | `SPRITE_NURSE` | 3 | 1 | `SPRITEMOVEDATA_STANDING_DOWN`, radius 0/0, hours -1/-1, pal 0 | `OBJECTTYPE_SCRIPT` | `SilverCavePokecenter1FNurseScript` (`jumpstd PokecenterNurseScript`) | -1 (always) |
| `SILVERCAVEPOKECENTER1F_GRANNY` | `SPRITE_GRANNY` | 1 | 5 | `SPRITEMOVEDATA_STANDING_LEFT`, radius 2/1, hours -1/-1, pal 0 | `OBJECTTYPE_SCRIPT` | `SilverCavePokecenter1FGrannyScript` (`jumptextfaceplayer`) | -1 (always) |
**Flags and events / Items / Trainers**: none. **Wild encounters**: none.
### MAP_SILVER_CAVE_ROOM_1
- Script: `maps/SilverCaveRoom1.asm` (symbols `47:41e9 SilverCaveRoom1_MapScripts`, `47:41f7 SilverCaveRoom1_MapEvents`)
- Blocks: `maps/SilverCaveRoom1.blk` (10 x 18 blocks = 20 x 36 cells)
- Header: `data/maps/maps.asm:144` ->
`map SilverCaveRoom1, TILESET_DARK_CAVE, CAVE, LANDMARK_SILVER_CAVE, MUSIC_LIGHTHOUSE, TRUE, PALETTE_DARK, FISHGROUP_LAKE`
(the `TRUE` is the phone-service flag: no phone calls in here)
- Dimensions: `constants/map_constants.asm:131` (`map_const SILVER_CAVE_ROOM_1, 10, 18`), group `DUNGEONS` = 3, map id 66
- Connections: none (dungeon interior)
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 33 | `SILVER_CAVE_OUTSIDE` | 2 |
| 2 | 15 | 1 | `SILVER_CAVE_ROOM_2` | 1 |
**Coord events** (`def_coord_events`)
None.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 16 | 23 | `BGEVENT_ITEM` | `SilverCaveRoom1HiddenDireHit` -> `hiddenitem DIRE_HIT, EVENT_SILVER_CAVE_ROOM_1_HIDDEN_DIRE_HIT` |
| 17 | 12 | `BGEVENT_ITEM` | `SilverCaveRoom1HiddenUltraBall` -> `hiddenitem ULTRA_BALL, EVENT_SILVER_CAVE_ROOM_1_HIDDEN_ULTRA_BALL` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `SILVERCAVEROOM1_POKE_BALL1` | `SPRITE_POKE_BALL` | 4 | 9 | `SPRITEMOVEDATA_STILL`, radius 0/0, hours -1/-1, pal 0 | `OBJECTTYPE_ITEMBALL` | `SilverCaveRoom1MaxElixer` (`itemball MAX_ELIXER`) | `EVENT_SILVER_CAVE_ROOM_1_MAX_ELIXER` |
| `SILVERCAVEROOM1_POKE_BALL2` | `SPRITE_POKE_BALL` | 15 | 29 | `SPRITEMOVEDATA_STILL`, radius 0/0, hours -1/-1, pal 0 | `OBJECTTYPE_ITEMBALL` | `SilverCaveRoom1XAccuracy` (`itemball X_ACCURACY`) | `EVENT_SILVER_CAVE_ROOM_1_X_ACCURACY` |
| `SILVERCAVEROOM1_POKE_BALL3` | `SPRITE_POKE_BALL` | 5 | 30 | `SPRITEMOVEDATA_STILL`, radius 0/0, hours -1/-1, pal 0 | `OBJECTTYPE_ITEMBALL` | `SilverCaveRoom1EscapeRope` (`itemball ESCAPE_ROPE`) | `EVENT_SILVER_CAVE_ROOM_1_ESCAPE_ROPE` |
**Scripts of interest**
`SilverCaveRoom1_MapScripts` has an empty `def_scene_scripts` and an empty
`def_callbacks`. Nothing scripted happens on this floor; the three labels above
are two-byte `itemball` payloads, not scripts (the `itemball`/`hiddenitem`
macros lay down data that the object/bg dispatcher reads, they are not `ScriptVM`
entry points).
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_SILVER_CAVE_ROOM_1_MAX_ELIXER` | `constants/event_flags.asm:1082` | object_event row (hide-when-set) | Set once the ball at (4,9) is taken. |
| `EVENT_SILVER_CAVE_ROOM_1_X_ACCURACY` | `constants/event_flags.asm:1083` | object_event row | Ball at (15,29). |
| `EVENT_SILVER_CAVE_ROOM_1_ESCAPE_ROPE` | `constants/event_flags.asm:1084` | object_event row | Ball at (5,30). |
| `EVENT_SILVER_CAVE_ROOM_1_HIDDEN_DIRE_HIT` | `constants/event_flags.asm:164` | `hiddenitem` | Hidden Dire Hit at (16,23). |
| `EVENT_SILVER_CAVE_ROOM_1_HIDDEN_ULTRA_BALL` | `constants/event_flags.asm:165` | `hiddenitem` | Hidden Ultra Ball at (17,12). |
Polarity note that matters for every object row in this section:
`CheckObjectFlag` (`engine/overworld/map_objects_2.asm:31-58`) masks (hides) the
object when its `MAPOBJECT_EVENT_FLAG` is **set**, and `Script_disappear`
(`engine/overworld/scripting.asm:887`) is exactly "set that flag". So a set flag
means gone, a clear flag means present.
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `MAX_ELIXER` | item ball at (4,9) | `SilverCaveRoom1MaxElixer` | `EVENT_SILVER_CAVE_ROOM_1_MAX_ELIXER` |
| `X_ACCURACY` | item ball at (15,29) | `SilverCaveRoom1XAccuracy` | `EVENT_SILVER_CAVE_ROOM_1_X_ACCURACY` |
| `ESCAPE_ROPE` | item ball at (5,30) | `SilverCaveRoom1EscapeRope` | `EVENT_SILVER_CAVE_ROOM_1_ESCAPE_ROPE` |
| `DIRE_HIT` | hidden at (16,23) | bg_event | `EVENT_SILVER_CAVE_ROOM_1_HIDDEN_DIRE_HIT` |
| `ULTRA_BALL` | hidden at (17,12) | bg_event | `EVENT_SILVER_CAVE_ROOM_1_HIDDEN_ULTRA_BALL` |
**Trainers**: none.
**Wild encounters**
`data/wild/johto_grass.asm:1297`, `def_grass_wildmons SILVER_CAVE_ROOM_1`,
`6 percent` morn/day/nite. All three time slices are identical here.
| slot | all times (Gold) | Silver difference |
|---|---|---|
| 1 | 42 ONIX | - |
| 2 | 44 URSARING | 44 DONPHAN |
| 3 | 43 GRAVELER | - |
| 4 | 43 GRAVELER | - |
| 5 | 45 GOLBAT | - |
| 6 | 20 LARVITAR | - |
| 7 | 15 LARVITAR | - |
No water table for Room 1. Fishing group `FISHGROUP_LAKE`.
**Terrain notes** (derived by expanding `maps/SilverCaveRoom1.blk` through
`data/tilesets/cave_collision.asm`; `TilesetDarkCaveColl` and `TilesetCaveColl`
are the same label pair in `gfx/tilesets.asm:106-107`, so the dark-cave floor
uses the ordinary cave collision table)
- Entry from outside lands on (9,33); a short corridor runs up to the long
east-west floor strip at y=32 spanning x=2..16.
- The Escape Rope at (5,30) sits **on a ledge cell**. Gen 2 ledges are not
"hop over the tile in front"; `.TryJump`
(`engine/overworld/player_movement.asm:354`) reads `wPlayerTileCollision`, the
tile the player is *standing on*, so you walk onto the ledge row, grab the
ball, then press the ledge's direction (`DOWN` for `COLL_HOP_DOWN`) to jump
two cells down onto y=32. That is exactly the walkthrough's "drop down the
ledge to get the Escape Rope. Jump back down."
- (11,27) and (13,27) carry `WARP_CARPET_DOWN` collision but there is **no**
`warp_event` at either coordinate. Treat them as decoration, not exits.
### MAP_SILVER_CAVE_ROOM_2
- Script: `maps/SilverCaveRoom2.asm` (symbols `47:4238`, `47:423d`)
- Blocks: `maps/SilverCaveRoom2.blk` (15 x 18 blocks = 30 x 36 cells)
- Header: `data/maps/maps.asm:145` ->
`map SilverCaveRoom2, TILESET_CAVE, CAVE, LANDMARK_SILVER_CAVE, MUSIC_LIGHTHOUSE, TRUE, PALETTE_NITE, FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:132` (`map_const SILVER_CAVE_ROOM_2, 15, 18`), group `DUNGEONS` = 3, map id 67
- Connections: none
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 17 | 31 | `SILVER_CAVE_ROOM_1` | 2 |
| 2 | 11 | 5 | `SILVER_CAVE_ROOM_3` | 1 |
| 3 | 13 | 21 | `SILVER_CAVE_ITEM_ROOMS` | 1 |
| 4 | 23 | 3 | `SILVER_CAVE_ITEM_ROOMS` | 2 |
**Coord events** (`def_coord_events`)
None.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 14 | 31 | `BGEVENT_ITEM` | `SilverCaveRoom2HiddenMaxPotion` -> `hiddenitem MAX_POTION, EVENT_SILVER_CAVE_ROOM_2_HIDDEN_MAX_POTION` |
**Object events**: none (`def_object_events` is the last line, no rows).
**Scripts of interest**
Empty `def_scene_scripts`, empty `def_callbacks`. Room 2 is pure terrain.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_SILVER_CAVE_ROOM_2_HIDDEN_MAX_POTION` | `constants/event_flags.asm:166` | `hiddenitem` at (14,31) | One-shot; the tile is right next to the Room 1 entrance warp at (17,31). |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `MAX_POTION` | hidden at (14,31) | bg_event | `EVENT_SILVER_CAVE_ROOM_2_HIDDEN_MAX_POTION` |
**Trainers**: none.
**Wild encounters**
Grass: `data/wild/johto_grass.asm:1352`, `def_grass_wildmons SILVER_CAVE_ROOM_2`,
`6 percent` morn/day/nite.
| slot | morn | day | nite |
|---|---|---|---|
| 1 | 45 QUAGSIRE | 45 QUAGSIRE | 45 QUAGSIRE |
| 2 | 48 GOLDUCK | 48 GOLDUCK | 48 GOLDUCK |
| 3 | 47 URSARING (Silver: DONPHAN) | 47 URSARING (Silver: DONPHAN) | 47 URSARING (Silver: DONPHAN) |
| 4 | 45 QUAGSIRE | 45 QUAGSIRE | **45 MISDREAVUS** |
| 5 | 48 GOLBAT | 48 GOLBAT | 48 GOLBAT |
| 6 | 20 LARVITAR | 20 LARVITAR | 20 LARVITAR |
| 7 | 15 LARVITAR | 15 LARVITAR | 15 LARVITAR |
Water: `data/wild/johto_water.asm:100`, `def_water_wildmons SILVER_CAVE_ROOM_2`,
`2 percent`: 35 SEAKING / 40 SEAKING / 35 GOLDEEN. Fishing group `FISHGROUP_LAKE`.
**Terrain notes** (expanded from `maps/SilverCaveRoom2.blk` +
`data/tilesets/cave_collision.asm`; both item-room warps are behind water)
- You arrive at (17,31) and stand on the south floor shelf, y=30..31, x=14..24.
- **South lake / Max Revive branch.** Surf west from (14,30) onto the water at
x=4..13, y=30..31. The waterfall band is x=6..11, y=28..29; Waterfall up from
it reaches the water pocket at y=22..27. Come ashore at (11,25), walk north to
the floor pocket x=9..14, y=22..24, and step onto the warp at **(13,21)** ->
`SILVER_CAVE_ITEM_ROOMS` warp 1 -> `MAX_REVIVE`.
- **North lake / Full Restore branch.** From the mid-map floor at (11,11) step
down onto the water at (11,12); the lake runs x=10..13 y=12..15 and x=12..21
y=14..15. The second waterfall band is x=18..21, y=10..13; Waterfall up lands
in the pool at x=18..21, y=8..9. Come ashore at (21,7), walk up through the
x=19..24 chamber to (23,4), and step onto the warp at **(23,3)** ->
`SILVER_CAVE_ITEM_ROOMS` warp 2 -> `FULL_RESTORE`. This is the walkthrough's
"use Surf on the lake, and then use Waterfall ... an entrance to grab a Full
Restore."
- The Room 3 entrance is the `CAVE` cell at (11,5), reached on foot from the
same north-central floor.
- The waterfall band at x=4..7, y=0..5 in the north-west corner has water below
it (y=6..7) that is walled off on every side. It looks decorative; nothing in
the asm routes through it.
### MAP_SILVER_CAVE_ITEM_ROOMS
- Script: `maps/SilverCaveItemRooms.asm` (symbols `47:42b7`, `47:42bd`)
- Blocks: `maps/SilverCaveItemRooms.blk` (10 x 9 blocks = 20 x 18 cells)
- Header: `data/maps/maps.asm:147` ->
`map SilverCaveItemRooms, TILESET_CAVE, CAVE, LANDMARK_SILVER_CAVE, MUSIC_LIGHTHOUSE, TRUE, PALETTE_NITE, FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:134` (`map_const SILVER_CAVE_ITEM_ROOMS, 10, 9`), group `DUNGEONS` = 3, map id 69
- Connections: none
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 13 | 3 | `SILVER_CAVE_ROOM_2` | 3 |
| 2 | 7 | 15 | `SILVER_CAVE_ROOM_2` | 4 |
**Coord events** / **BG events**: both empty.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `SILVERCAVEITEMROOMS_POKE_BALL1` | `SPRITE_POKE_BALL` | 6 | 3 | `SPRITEMOVEDATA_STILL`, radius 0/0, hours -1/-1, pal 0 | `OBJECTTYPE_ITEMBALL` | `SilverCaveItemRoomsMaxRevive` (`itemball MAX_REVIVE`) | `EVENT_SILVER_CAVE_ITEM_ROOMS_MAX_REVIVE` |
| `SILVERCAVEITEMROOMS_POKE_BALL2` | `SPRITE_POKE_BALL` | 15 | 11 | `SPRITEMOVEDATA_STILL`, radius 0/0, hours -1/-1, pal 0 | `OBJECTTYPE_ITEMBALL` | `SilverCaveItemRoomsFullRestore` (`itemball FULL_RESTORE`) | `EVENT_SILVER_CAVE_ITEM_ROOMS_FULL_RESTORE` |
**Scripts of interest**
Empty scene scripts and callbacks; the two labels are `itemball` payloads.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_SILVER_CAVE_ITEM_ROOMS_MAX_REVIVE` | `constants/event_flags.asm:1085` | object_event row | Ball at (6,3), north room. |
| `EVENT_SILVER_CAVE_ITEM_ROOMS_FULL_RESTORE` | `constants/event_flags.asm:1086` | object_event row | Ball at (15,11), south room. |
**Items**
| item | how obtained | source | one-time flag |
|---|---|---|---|
| `MAX_REVIVE` | item ball at (6,3) | `SilverCaveItemRoomsMaxRevive` | `EVENT_SILVER_CAVE_ITEM_ROOMS_MAX_REVIVE` |
| `FULL_RESTORE` | item ball at (15,11) | `SilverCaveItemRoomsFullRestore` | `EVENT_SILVER_CAVE_ITEM_ROOMS_FULL_RESTORE` |
**Trainers**: none.
**Wild encounters**
`data/wild/johto_grass.asm:1462`, `def_grass_wildmons SILVER_CAVE_ITEM_ROOMS`,
`6 percent`. Same shape as Room 2 but the night swap lands in slot 1 instead of
slot 4: morn/day slot 1 is 45 QUAGSIRE, nite slot 1 is **45 MISDREAVUS**; slot 4
stays 45 QUAGSIRE at all times. Slots 2/3/5/6/7 are 48 GOLDUCK / 47 URSARING
(Silver: DONPHAN) / 48 GOLBAT / 20 LARVITAR / 15 LARVITAR. No water table.
**Terrain notes**
The map is two disconnected chambers in one 20 x 18 grid, which is why both of
its warps go back to Room 2:
- North chamber, floor y=2..3 spanning x=4..15, warp 1 at (13,3), Max Revive at (6,3).
- South chamber, floor y=10..15 spanning roughly x=4..17, warp 2 at (7,15), Full
Restore at (15,11).
### MAP_SILVER_CAVE_ROOM_3
- Script: `maps/SilverCaveRoom3.asm` (symbols `47:425c SilverCaveRoom3_MapScripts`, `47:425e Red`, `47:429f SilverCaveRoom3_MapEvents`)
- Blocks: `maps/SilverCaveRoom3.blk` (10 x 18 blocks = 20 x 36 cells)
- Header: `data/maps/maps.asm:146` ->
`map SilverCaveRoom3, TILESET_CAVE, CAVE, LANDMARK_SILVER_CAVE, MUSIC_LIGHTHOUSE, TRUE, PALETTE_DAY, FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:133` (`map_const SILVER_CAVE_ROOM_3, 10, 18`), group `DUNGEONS` = 3, map id 68
- Connections: none
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 33 | `SILVER_CAVE_ROOM_2` | 2 |
**Coord events** (`def_coord_events`)
None. Red is *not* a trip-wire; you have to walk into him.
**BG events** (`def_bg_events`)
None.
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `SILVERCAVEROOM3_RED` | `SPRITE_RED` | 9 | 10 | `SPRITEMOVEDATA_STANDING_UP`, radius 0/0, hours -1/-1, pal `PAL_NPC_RED` | `OBJECTTYPE_SCRIPT` (sight range 0) | `Red` | `EVENT_RED_IN_MT_SILVER` |
Note the type: Red is `OBJECTTYPE_SCRIPT`, **not** `OBJECTTYPE_TRAINER`. He has
no sight cone, no `loadtrainer` in his object row, and no "trainer spotted"
exclamation. You must stand next to him and press A.
**Scripts of interest**
`Red` (`maps/SilverCaveRoom3.asm:9`, symbol `47:425e`), in order:
1. `special FadeOutMusic`
2. `faceplayer` / `opentext` / `writetext RedSeenText` ("…" / "…") / `waitbutton` / `closetext`
3. `winlosstext RedWinLossText, RedWinLossText` - the same "…" text for both outcomes
4. `loadtrainer RED, RED1`
5. `startbattle`
6. `dontrestartmapmusic`
7. `reloadmapafterbattle`
8. `special FadeOutMusic`
9. `opentext` / `writetext RedLeavesText` / `waitbutton` / `closetext`
10. `special FadeOutToBlack` / `special ReloadSpritesNoPalettes`
11. `disappear SILVERCAVEROOM3_RED` - sets `EVENT_RED_IN_MT_SILVER`
12. `pause 15` / `special FadeInFromBlack` / `pause 30`
13. `special HealParty`
14. `reanchormap`
15. `credits`
16. `end`
There is no `checkevent`/`iftrue` guard anywhere in the script: the object flag
is the only gate, and the script is unconditional once you talk to him. There is
also no `applymovement` - "He flies away" in the walkthrough is prose; the asm
just fades to black and `disappear`s him.
`credits` is opcode `$a0` (`engine/overworld/scripting.asm:227`).
`Script_credits` (`engine/overworld/scripting.asm:2215`) farcalls `RedCredits`
(`engine/events/halloffame.asm:34`), which fades to white, sets
`wSpawnAfterChampion = SPAWN_RED`, and jumps into `Credits`; on return the
script is force-ended and `MAPSTATUS_DONE` is loaded.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_RED_IN_MT_SILVER` | `constants/event_flags.asm:1284` | **set** by `InitializeEventsScript` (`engine/events/std_scripts.asm:526`) at new game; **cleared** by `maps/HallOfFame.asm:36`; **set** again by `disappear SILVERCAVEROOM3_RED` inside `Red` | Set = Red hidden (see the `CheckObjectFlag` polarity note above). So: absent on a fresh save, present after your first Hall of Fame induction, gone once beaten, and present again after every subsequent Hall of Fame. That is the asm behind "If you want to fight Red again, beat the Elite Four again and then he'll be back at Mt. Silver." |
No `SCENE_*` values: `def_scene_scripts` is empty for this map (`wSilverCaveRoom3SceneID` exists in WRAM at `01:d6ea` but nothing writes a non-zero scene to it).
**Items**: none.
**Trainers**
| const | class | id | party (`data/trainers/parties.asm` label) | script label | rematch/phone |
|---|---|---|---|---|---|
| `RED1` | `RED` (`trainerclass RED ; 3f` = 63, `constants/trainer_constants.asm:637`) | 1 | `RedGroup` (`data/trainers/parties.asm:3143`, symbol `0e:75d5`) | `Red` | No phone number, no rematch table; he re-appears only via `EVENT_RED_IN_MT_SILVER` being cleared at the Hall of Fame |
`RedGroup`, `db "RED@", TRAINERTYPE_MOVES` - four explicit moves per mon, in
send-out order:
| # | level | species | moves |
|---|---|---|---|
| 1 | 81 | PIKACHU | CHARM, QUICK_ATTACK, THUNDERBOLT, THUNDER |
| 2 | 73 | ESPEON | MUD_SLAP, REFLECT, SWIFT, PSYCHIC_M |
| 3 | 75 | SNORLAX | AMNESIA, SNORE, REST, BODY_SLAM |
| 4 | 77 | VENUSAUR | SUNNY_DAY, GIGA_DRAIN, SYNTHESIS, SOLARBEAM |
| 5 | 77 | CHARIZARD | FLAMETHROWER, WING_ATTACK, SLASH, FIRE_SPIN |
| 6 | 77 | BLASTOISE | RAIN_DANCE, SURF, BLIZZARD, WHIRLPOOL |
Class attributes (`data/trainers/attributes.asm:377-381`, the `; Red` block):
- items: `FULL_RESTORE, FULL_RESTORE` - **two** slots, not three
- base reward: `25`
- AI: `AI_BASIC | AI_SETUP | AI_SMART | AI_AGGRESSIVE | AI_CAUTIOUS | AI_STATUS | AI_RISKY`
- item/switch: `CONTEXT_USE | SWITCH_SOMETIMES`
Prize money: `ComputeTrainerReward`
(`engine/battle/read_trainer_party.asm:300`) computes
`wBattleReward = base_reward * wCurPartyLevel`, and `wCurPartyLevel` at that
point is the level of the **last** party row, i.e. Blastoise's 77. So
`25 * 77 = 1925`. `WinTrainerBattle` (`engine/battle/core.asm:2292`) then pays it
out with `ld c, 4` - four additions - giving **7700**, which matches the
walkthrough's "You get: 7700G" exactly. (Amulet Coin doubles it via
`.DoubleReward` before the split; Mom's savings take a share of the four
quarters.)
**Wild encounters**
`data/wild/johto_grass.asm:1407`, `def_grass_wildmons SILVER_CAVE_ROOM_3`,
`6 percent`, identical across morn/day/nite:
| slot | species |
|---|---|
| 1 | 51 GOLBAT |
| 2 | 48 ONIX |
| 3 | 48 GOLBAT |
| 4 | 50 URSARING (Silver: DONPHAN) |
| 5 | 51 GOLDUCK |
| 6 | 20 LARVITAR |
| 7 | 15 LARVITAR |
No water table for Room 3.
**Terrain notes**
A single long north-south corridor. From the entrance warp at (9,33) the path
runs straight up the x=9..10 column to the wide chamber at y=9..16, then to the
top room; Red stands at (9,10), squarely in the corridor mouth, so you cannot
pass him without triggering the fight. (11,33) also carries `WARP_CARPET_DOWN`
collision but has no `warp_event`.
---
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| Reaching Route 28 at all (the north passage of the gate) | `maps/VictoryRoadGate.asm:19` `VictoryRoadGateBadgeCheckScript` / `VictoryRoadGateOfficerScript` (symbols `5f:4fdc` / `5f:4fe2`), fired by `coord_event 10, 11, SCENE_VICTORYROADGATE_BADGE_CHECK, VictoryRoadGateBadgeCheckScript` | `readvar VAR_BADGES` / `ifgreater NUM_JOHTO_BADGES - 1` - strictly more than 7 Johto badges | On pass: `setscene SCENE_VICTORYROADGATE_NOOP`, so the check never fires again. On fail: `applymovement PLAYER, VictoryRoadGateStepDownMovement` shoves you back one step. Note the west warps to Route 28 at (1,7)/(2,7) are *below* the trip-wire row; the badge scene guards the north corridor. |
| Mt. Silver "opened" flavour | `maps/OaksLab.asm:18-45` `Oak` -> `.CheckBadges` / `.OpenMtSilver` | `readvar VAR_BADGES` / `ifequal NUM_BADGES` (all 16) when you talk to Prof. Oak | `setevent EVENT_OPENED_MT_SILVER` (`constants/event_flags.asm:1265`). The only other reader is `maps/VictoryRoadGate.asm:118`, where it is the hide-flag of the left Black Belt at (7,5). **No script in any Mt. Silver map reads it**, and it does not appear in a `checkevent` on any warp. |
| Room 1 darkness | `engine/tilesets/timeofday_pals.asm:113` `ReplaceTimeOfDayPals` -> `.NeedsFlash`, driven by `PALETTE_DARK` in `data/maps/maps.asm:144` | `STATUSFLAGS_FLASH_F` in `wStatusFlags`, set by `BlindingFlash` (`engine/events/field_moves.asm:12`) | Use HM05 Flash. `FlashFunction` (`engine/events/overworld.asm:271`) needs `ENGINE_ZEPHYRBADGE` **and** `wTimeOfDayPalset == DARKNESS_PALSET`. Rooms 2/3 and the Item Rooms are `PALETTE_NITE`/`PALETTE_DAY`, so Flash is only needed for Room 1. It is a visibility gate, not a movement gate. |
| Room 2 lakes | `SurfFunction.TrySurf` (`engine/events/overworld.asm:322-345`) | `ENGINE_FOGBADGE` + a party mon with SURF + facing a `WATER_TILE` | Fog Badge (Morty). Blocks both Item Rooms entrances and everything past y<30 on the west lake. |
| Room 2 waterfalls | `WaterfallFunction.TryWaterfall` (`engine/events/overworld.asm:611`) and `TryWaterfallOW` (`engine/events/overworld.asm:687`), gated by `CheckMapCanWaterfall` | `ENGINE_RISINGBADGE`, party mon with WATERFALL, player facing UP, and `wTileUp` a waterfall tile | Rising Badge (Clair). Without it neither item room is reachable: the north branch (Full Restore at (23,3)) and the south branch (Max Revive at (13,21)) both sit above a waterfall band. |
| Red himself | `maps/SilverCaveRoom3.asm:62` object row's event flag | `EVENT_RED_IN_MT_SILVER` must be **clear** | Cleared by `maps/HallOfFame.asm:36`, i.e. by completing the Elite Four / Lance and being inducted. Before that the object is masked by `CheckObjectFlag` and Room 3 is an empty corridor. |
Not a gate, worth knowing: nothing in this section checks Strength, Whirlpool,
Cut or Rock Smash. The walkthrough's "bring all the HMs" is advice, not a
requirement - Flash, Surf and Waterfall are the only three the asm cares about,
and Flash only for comfort.
---
## 4. Bot checklist
Preconditions for the whole section: `EVENT_BEAT_ELITE_FOUR` set (Hall of Fame
done, which is what clears `EVENT_RED_IN_MT_SILVER`), 8 Johto badges for the
Victory Road Gate scene, `ENGINE_FOGBADGE` + `ENGINE_RISINGBADGE` for the item
rooms, `ENGINE_ZEPHYRBADGE` + Flash for Room 1 visibility.
1. `MAP_VICTORY_ROAD_GATE` - walk onto (10,11) if the scene is still
`SCENE_VICTORYROADGATE_BADGE_CHECK`; talk through
`VictoryRoadGateOfficerScript`. Pre: `VAR_BADGES > 7`. Post: scene becomes
`SCENE_VICTORYROADGATE_NOOP`. Then take warp 7 or 8 at (1,7)/(2,7) to
`MAP_ROUTE_28`.
2. `MAP_ROUTE_28` - cross west onto the map connection. Post: now on
`MAP_SILVER_CAVE_OUTSIDE`. (Route 28 body belongs to the previous section.)
3. `MAP_SILVER_CAVE_OUTSIDE` - entering fires
`SilverCaveOutsideFlypointCallback`. Post: `ENGINE_FLYPOINT_SILVER_CAVE` set;
Fly to `LANDMARK_SILVER_CAVE` now works, and the respawn point is (23,20).
4. `MAP_SILVER_CAVE_OUTSIDE` - optional: face (9,25) and press A (or run the
ITEMFINDER) for the hidden `FULL_RESTORE`. Pre:
`EVENT_SILVER_CAVE_OUTSIDE_HIDDEN_FULL_RESTORE` clear. Post: it is set.
5. `MAP_SILVER_CAVE_OUTSIDE` - optional heal: warp 1 at (23,19) ->
`MAP_SILVER_CAVE_POKECENTER_1F`, talk to `SILVERCAVEPOKECENTER1F_NURSE` at
(3,1), leave by warp 1 at (3,7).
6. `MAP_SILVER_CAVE_OUTSIDE` - step on warp 2 at (18,11). Post: on
`MAP_SILVER_CAVE_ROOM_1` at (9,33).
7. `MAP_SILVER_CAVE_ROOM_1` - use Flash from the party menu (`FlashFunction`).
Pre: `ENGINE_ZEPHYRBADGE`, HM05 on a party mon, `wTimeOfDayPalset ==
DARKNESS_PALSET`. Post: `STATUSFLAGS_FLASH_F` set.
8. `MAP_SILVER_CAVE_ROOM_1` - walk to `SILVERCAVEROOM1_POKE_BALL3` at (5,30) and
press A (approach along the ledge row from above, x=2..6 at y=30). Post:
`ESCAPE_ROPE` in the bag, `EVENT_SILVER_CAVE_ROOM_1_ESCAPE_ROPE` set. Then
press DOWN while standing on the ledge to hop to y=32.
9. `MAP_SILVER_CAVE_ROOM_1` - `SILVERCAVEROOM1_POKE_BALL2` at (15,29) ->
`X_ACCURACY`, flag `EVENT_SILVER_CAVE_ROOM_1_X_ACCURACY`.
10. `MAP_SILVER_CAVE_ROOM_1` - `SILVERCAVEROOM1_POKE_BALL1` at (4,9) ->
`MAX_ELIXER`, flag `EVENT_SILVER_CAVE_ROOM_1_MAX_ELIXER`.
11. `MAP_SILVER_CAVE_ROOM_1` - optional hidden items: (17,12) `ULTRA_BALL`,
(16,23) `DIRE_HIT`.
12. `MAP_SILVER_CAVE_ROOM_1` - step on warp 2 at (15,1). Post: on
`MAP_SILVER_CAVE_ROOM_2` at (17,31).
13. `MAP_SILVER_CAVE_ROOM_2` - optional hidden `MAX_POTION` at (14,31), two
cells west of where you land.
14. `MAP_SILVER_CAVE_ROOM_2` (Full Restore branch) - walk to (11,11), face down,
Surf onto (11,12). Follow the lake east to x=18..21 at y=14..15, face UP on
y=10 and use Waterfall. Come ashore at (21,7), walk to (23,4), step up onto
the warp at (23,3). Post: on `MAP_SILVER_CAVE_ITEM_ROOMS` at (7,15).
15. `MAP_SILVER_CAVE_ITEM_ROOMS` - `SILVERCAVEITEMROOMS_POKE_BALL2` at (15,11)
-> `FULL_RESTORE`, flag `EVENT_SILVER_CAVE_ITEM_ROOMS_FULL_RESTORE`. Leave
by warp 2 at (7,15).
16. `MAP_SILVER_CAVE_ROOM_2` (Max Revive branch, optional, not in the
walkthrough) - from the landing shelf walk to (14,30), Surf west, Waterfall
up the x=6..11 band at y=28..29, come ashore at (11,25), walk to (13,22) and
step up onto the warp at (13,21). Take
`SILVERCAVEITEMROOMS_POKE_BALL1` at (6,3) -> `MAX_REVIVE`. Leave by warp 1
at (13,3).
17. `MAP_SILVER_CAVE_ROOM_2` - step on warp 2 at (11,5). Post: on
`MAP_SILVER_CAVE_ROOM_3` at (9,33).
18. `MAP_SILVER_CAVE_ROOM_3` - walk north up the x=9..10 corridor to (9,11),
face UP, press A on `SILVERCAVEROOM3_RED` at (9,10). Pre:
`EVENT_RED_IN_MT_SILVER` clear. This runs `Red`.
19. Battle `RED` / `RED1`. Six mons, levels 81/73/75/77/77/77, two Full Restores
on the AI. Post on victory: 7700 money (before Amulet Coin / Mom split),
`EVENT_RED_IN_MT_SILVER` set by `disappear`, party healed by
`special HealParty`, then `credits`.
20. After the credits the game re-enters at `SPAWN_MT_SILVER` =
`SILVER_CAVE_OUTSIDE` (23,20) with `hMapEntryMethod = MAPSETUP_WARP`. Save
here (the walkthrough's "After the credits roll, save").
---
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| Map headers / warps / bg events / object events for all six maps | `src/import/RomExtractorGen2.lua` (warp + coord + bg + object decode around lines 785-861 and 2959-2990) | implemented - the maps are decoded generically from the ROM, so no Silver Cave specific data is hand-written anywhere |
| `itemball` object rows (Escape Rope, X Accuracy, Max Elixer, Max Revive, Full Restore) | `src/import/RomExtractorGen2.lua:2874`, `:2968` (`readItemBall`) | implemented |
| `hiddenitem` bg rows (Full Restore, Dire Hit, Ultra Ball, Max Potion) | `src/world/gen2/HiddenItems.lua` | implemented |
| `MAPCALLBACK_NEWMAP` flypoint callback | `src/world/gen2/Map.lua` + driver `tests/drivers/gold_map_callbacks.lua` | implemented |
| `ENGINE_FLYPOINT_SILVER_CAVE` -> Fly destination | `src/world/gen2/FieldMoves.lua:355` (`LANDMARK_SILVER_CAVE` / `SPAWN_MT_SILVER` / flag 75), `src/ui/gen2/Pokegear.lua:1478` | implemented |
| Flash (dark-cave palette gate) | `src/world/gen2/FieldMoves.lua:465-475` (`flashFromMenu`), badge table at `:105` | implemented |
| Surf | `src/world/gen2/FieldMoves.lua:477-500`, `src/world/gen2/Permissions.lua` | implemented |
| Waterfall (`CheckMapCanWaterfall`, `.CheckContinueWaterfall`) | `src/world/gen2/FieldMoves.lua:253-266`, `:528-540` | implemented |
| Gen 2 ledge hop (`.TryJump` / `HI_NYBBLE_LEDGES`) - needed for the Escape Rope drop in Room 1 | no gen2 implementation found; `src/world/gen2/Player.lua` is a plain grid stepper and `src/world/gen2/Permissions.lua` only models LAND/WATER/WALL. `src/world/OverworldController.lua:1330` `checkLedgeHop` is the **Gen 1** model (`data/tilesets/ledge_tiles.asm`, standing-tile + front-tile pair) and does not match Gen 2's standing-tile-nybble rule | missing |
| `loadtrainer` / `startbattle` / `winlosstext` | `src/script/gen2/Vm.lua:806`, `:817`; opcodes in `src/script/gen2/Opcodes.lua:99-105` | implemented |
| `disappear` | `src/script/gen2/Opcodes.lua:115`, handled in `src/script/gen2/Vm.lua` | implemented |
| `special FadeOutMusic` / `FadeOutToBlack` / `FadeInFromBlack` / `ReloadSpritesNoPalettes` / `HealParty` | `src/script/gen2/Specials.lua:450`, `:999`, `:1001`, `:1025`, `:1066` | implemented |
| `credits` opcode as a terminal script op | `src/script/gen2/Opcodes.lua:172`, `:194` | implemented |
| Red-credits path and post-credits respawn at Mt. Silver | `src/core/gen2/HallOfFame.lua:48-53`, `:239-259` (`markRedCredits`, `POST_CREDITS_SPAWN.SPAWN_RED = "SPAWN_MT_SILVER"`), test `tests/gen2_halloffame_test.lua:171` | implemented |
| Credits reel itself | `src/ui/Credits.lua`, driver `tests/drivers/gold_halloffame_shots.lua` | implemented (shared with the Hall of Fame credits) |
| Prize money (base reward x last level, x4, Mom split) | `src/battle/gen2/Prize.lua` | implemented - the `ld c, 4` factor is modelled explicitly, which is what makes Red's 7700 come out right |
| Trainer AI item use (Red's two Full Restores) | `src/battle/gen2/Ai.lua:1294`, `:1369-1375` | implemented |
| Wild tables incl. the morn/day/nite split | `src/import/RomExtractorGen2.lua:3720-3845` (`JohtoGrassWildMons` / `JohtoWaterWildMons` / `FishGroups`) | implemented |
| Trainer parties incl. `RedGroup` | `src/import/RomExtractorGen2.lua:3859-4001` (`TrainerGroups`) | implemented |
| A driver that actually reaches Red | none - the closest are `tests/drivers/gold_trainer_smoke.lua` and `tests/drivers/gold_halloffame_shots.lua` | missing |
---
## 6. Unresolved / verify by hand
- **"Items in Mount Silver: Escape Rope, Full Restore, Protein."** There is no
`PROTEIN` anywhere in `maps/SilverCaveOutside.asm`, `SilverCaveRoom1.asm`,
`SilverCaveRoom2.asm`, `SilverCaveRoom3.asm` or `SilverCaveItemRooms.asm`. The
full real list is Max Elixer, X Accuracy, Escape Rope, Max Revive, Full Restore
(balls) plus hidden Full Restore, Dire Hit, Ultra Ball, Max Potion. The
walkthrough's summary list is also missing the X Accuracy and Max Elixir it
later tells you to pick up.
- **"Pokémon in Mount Silver: ... #232 Donphan"** - Donphan is the *Silver*
column of every one of these `def_grass_wildmons` blocks. In Gold the same
slots are Ursaring. Same file, `IF DEF(_GOLD)` / `ELIF DEF(_SILVER)`.
- **"Red will have three Full Restores"** - `data/trainers/attributes.asm:378`
gives the RED class exactly two item slots, both `FULL_RESTORE`. Could not find
a third anywhere.
- **Red's party order.** The walkthrough lists Pikachu, Espeon, Blastoise,
Snorlax, Charizard, Venusaur. `RedGroup` orders them Pikachu, Espeon, Snorlax,
Venusaur, Charizard, Blastoise. Only the lead matches. The listed EXP yields
(1422 / 3081 / 3465 / 2475 / 3448 / 3432) are not stored anywhere as data -
they are computed from base experience and level at runtime - so they could not
be confirmed against a table.
- **"Espeon is a dark type that cannot be hit with ghost moves."** Espeon is
Psychic in `data/pokemon/base_stats/espeon.asm`; the ghost-immunity claim
describes Umbreon/Dark. Strategy prose, not asm, but a bot following it would
make bad move choices.
- **"He flies away."** The `Red` script has no `applymovement` and no fly
animation - `special FadeOutToBlack`, `disappear`, `special FadeInFromBlack`.
Cosmetic discrepancy only.
- **Which of the two Item Rooms the walkthrough means.** The text mentions only
the Full Restore, which I traced to `SilverCaveRoom2` warp 4 at (23,3) ->
`SilverCaveItemRooms` warp 2 at (7,15). That mapping is derived from expanding
`maps/SilverCaveRoom2.blk` and `maps/SilverCaveItemRooms.blk` through
`data/tilesets/cave_collision.asm`, not from an explicit statement in any asm
file. The Surf/Waterfall requirement on both branches comes from the same
derivation. Worth confirming in-game before a bot commits to the route.
- **`EVENT_OPENED_MT_SILVER` does nothing on these maps.** The walkthrough (and
common knowledge) says you need all 16 badges and a word with Prof. Oak to
enter Mt. Silver. `maps/OaksLab.asm` does set the flag on 16 badges, but the
only consumer in the whole tree is the hide-flag of a Black Belt in
`maps/VictoryRoadGate.asm:118`. I could not find a `checkevent` on that flag
guarding any warp, coord event or NPC on the Route 28 / Silver Cave path. If
the 16-badge requirement is real it is enforced somewhere I did not locate -
verify by hand before assuming a bot with 8 Johto badges can walk in.
- **Two `WARP_CARPET_DOWN` cells with no `warp_event`**: (11,27) and (13,27) in
Room 1, and (11,33) in Room 3. Collision says "warp", the event table says
nothing. Assumed decorative; not tested.
@@ -0,0 +1,823 @@
# Section 32 - Raikou, Entei, and Suicune
Source: `../section-32-raikou-entei-and-suicune.txt`
Maps covered: `MAP_ROUTE_29`, `MAP_ROUTE_30`, `MAP_ROUTE_31`, `MAP_ROUTE_32`,
`MAP_ROUTE_33`, `MAP_ROUTE_34`, `MAP_ROUTE_35`, `MAP_ROUTE_36`, `MAP_ROUTE_37`,
`MAP_ROUTE_38`, `MAP_ROUTE_39`, `MAP_ROUTE_42`, `MAP_ROUTE_43`, `MAP_ROUTE_44`,
`MAP_ROUTE_45`, `MAP_ROUTE_46` (the sixteen `RoamMaps` entries), plus
`MAP_VIOLET_CITY` as the Fly anchor the walkthrough names.
Badges / key milestones in this section: none. The milestone is three catches:
`RAIKOU`, `ENTEI`, `SUICUNE`. Every one of them clears its own roam slot
permanently.
This section is a *mechanic*, not a walk. There is no scripted trigger anywhere
on these sixteen maps that has anything to do with the beasts; the entire
feature lives in `engine/overworld/wildmons.asm`,
`engine/battle/core.asm` and `data/wild/roammon_maps.asm`. Section 2 therefore
opens with a "roam engine" block before the per-map blocks, because that block
is the part a bot actually has to implement.
---
## 1. Route order
The walkthrough gives a policy, not an itinerary: cross route boundaries as fast
as possible, because every crossing is one `UpdateRoamMons` call. The named
concrete loop is the Violet City / Route 36 hub.
| # | Map constant | asm file | Enter via | Leaves via | Why (walkthrough beat) |
|---|---|---|---|---|---|
| 0 | `MAP_BURNED_TOWER_B1F` | `maps/BurnedTowerB1F.asm` | (earlier section) | (earlier section) | `special InitRoamMons` at `ReleaseTheBeasts` is the only thing that ever creates the three roamers. Out of scope here; owned by the Burned Tower section. |
| 1 | `MAP_VIOLET_CITY` | `maps/VioletCity.asm` | Fly (`MAPSETUP_TELEPORT`) | west connection | "fly to Violet City, then stay on a bike and keep switching between Routes 35, 36, and 37". Note the Fly itself runs `JumpRoamMons`, i.e. it *scatters* the beasts before the loop starts. |
| 2 | `MAP_ROUTE_36` | `maps/Route36.asm` | west connection from Violet City | south / north connections | Route 36 is the four-way roam junction (`ROUTE_35`, `ROUTE_31`, `ROUTE_32`, `ROUTE_37`) and the only map with a direct edge to Violet City. |
| 3 | `MAP_ROUTE_35` | `maps/Route35.asm` | south connection from Route 36 | north connection back | Half of the "left right left right" oscillation. Each crossing = one `UpdateRoamMons`. |
| 4 | `MAP_ROUTE_37` | `maps/Route37.asm` | north connection from Route 36 | south connection back | Other half of the oscillation, and Entei's starting route. |
| 5 | `MAP_ROUTE_42` | `maps/Route42.asm` | west connection from Ecruteak City | east connection to Mahogany Town | "go to Route 42 with the Max Repel". Raikou's starting route, the other four-way roam junction, and it carries a free `ULTRA_BALL` item ball. |
| - | `MAP_SILVER_CAVE_OUTSIDE` / `MAP_SILVER_CAVE_ROOM_*` | `maps/SilverCaveOutside.asm` etc. | - | - | "sharing EXP at Mount Silver" to raise the False Swipe Scyther. Mount Silver belongs to a later section; only the level ceiling (see section 3) matters here. |
Spill-over note: the walkthrough's Master Ball aside points at the Elm /
Radio Tower reward, and its Scyther training points at Mount Silver. Both are
other sections' maps; nothing about them is transcribed here.
---
## 2. Maps
### The roam engine (not a map)
This is the part of the section that is code rather than map data. Everything
below was read out of the files named.
**State: `roam_struct`** (`macros/ram.asm`, 7 bytes each, three of them)
| field | symbol (Raikou slot) | address | meaning |
|---|---|---|---|
| Species | `wRoamMon1Species` | `01:dd1a` | `0` once caught or defeated, which is what retires the slot forever |
| Level | `wRoamMon1Level` | `01:dd1b` | always 40, never changes (no EXP in the struct) |
| MapGroup | `wRoamMon1MapGroup` | `01:dd1c` | `GROUP_N_A` when retired |
| MapNumber | `wRoamMon1MapNumber` | `01:dd1d` | `MAP_N_A` when retired |
| HP | `wRoamMon1HP` | `01:dd1e` | **one byte**; `0` means "stats not rolled yet" |
| DVs | `wRoamMon1DVs` | `01:dd1f` | rolled once, on the first encounter, then kept |
Entei is slot 2 (`wRoamMon2Species` `01:dd21`), Suicune slot 3
(`wRoamMon3Species` `01:dd28`). Slot order is load bearing:
`CheckEncounterRoamMon` indexes the structs by a random 0..2 and
`GetRoamMonHP` walks them by species.
Player-position backup: `wRoamMons_CurMapGroup` `01:dd30`,
`wRoamMons_LastMapGroup` `01:dd32` (`_BackUpMapIndices`, `0a:6942`).
**Creation: `InitRoamMons`** (`engine/overworld/wildmons.asm:488`, `0a:67d7`)
Called exactly once, from `maps/BurnedTowerB1F.asm:65`, at the tail of
`ReleaseTheBeasts` right after `setscene SCENE_BURNEDTOWERB1F_NOOP` and
`setevent EVENT_RELEASED_THE_BEASTS`. It writes:
| slot | species | level | starting map |
|---|---|---|---|
| 1 | `RAIKOU` | 40 | `MAP_ROUTE_42` (`GROUP_ROUTE_42`) |
| 2 | `ENTEI` | 40 | `MAP_ROUTE_37` (`GROUP_ROUTE_37`) |
| 3 | `SUICUNE` | 40 | `MAP_ROUTE_38` (`GROUP_ROUTE_38`) |
HP is written as `0` (`xor a ; generate new stats`).
**The map graph: `RoamMaps`** (`data/wild/roammon_maps.asm`, `0a:695b`,
`NUM_ROAMMON_MAPS` = 16). Transcribed verbatim:
| start map | reachable maps |
|---|---|
| `ROUTE_29` | `ROUTE_30`, `ROUTE_46` |
| `ROUTE_30` | `ROUTE_29`, `ROUTE_31` |
| `ROUTE_31` | `ROUTE_30`, `ROUTE_32`, `ROUTE_36` |
| `ROUTE_32` | `ROUTE_36`, `ROUTE_31`, `ROUTE_33` |
| `ROUTE_33` | `ROUTE_32`, `ROUTE_34` |
| `ROUTE_34` | `ROUTE_33`, `ROUTE_35` |
| `ROUTE_35` | `ROUTE_34`, `ROUTE_36` |
| `ROUTE_36` | `ROUTE_35`, `ROUTE_31`, `ROUTE_32`, `ROUTE_37` |
| `ROUTE_37` | `ROUTE_36`, `ROUTE_38`, `ROUTE_42` |
| `ROUTE_38` | `ROUTE_37`, `ROUTE_39`, `ROUTE_42` |
| `ROUTE_39` | `ROUTE_38` |
| `ROUTE_42` | `ROUTE_43`, `ROUTE_44`, `ROUTE_37`, `ROUTE_38` |
| `ROUTE_43` | `ROUTE_42`, `ROUTE_44` |
| `ROUTE_44` | `ROUTE_42`, `ROUTE_43`, `ROUTE_45` |
| `ROUTE_45` | `ROUTE_44`, `ROUTE_46` |
| `ROUTE_46` | `ROUTE_45`, `ROUTE_29` |
The file's own comment: "Notably missing are Route 40 and Route 41, which are
water routes." That is the walkthrough's "the exceptions are Routes 40 and 41".
**Movement: `UpdateRoamMons`** (`engine/overworld/wildmons.asm:578`, `0a:6859`)
`.Update` per live beast, then `_BackUpMapIndices`. One random byte does double
duty:
- `and %00011111`; if the result is zero (1 in 32) it calls `JumpRoamMon`
instead, i.e. a completely random `RoamMaps` entry.
- otherwise `and %11` of that same masked value is the connection index. An
index `>=` the entry's connection count re-rolls, and so does a candidate
equal to `wRoamMons_LastMapGroup`/`wRoamMons_LastMapNumber`, which is the map
the player was on *before* the one they are on now. That last check is what
stops a beast shadowing a player who paces one boundary.
Because the jump test and the index share one byte, they are not independent
rolls.
**When `UpdateRoamMons` runs** (`data/maps/setup_scripts.asm`)
| map setup script | contains `UpdateRoamMons`? | reached by |
|---|---|---|
| `MapSetupScript_Connection` | yes (line 93) | walking across a map connection, `engine/overworld/events.asm:1013 reloadend MAPSETUP_CONNECTION` |
| `MapSetupScript_Train` | yes (line 121) | magnet train, and everything that falls into it |
| `MapSetupScript_Door` | yes, by falling through into `_Train` | normal door/stair warps, `engine/overworld/events.asm:993`; Dig / Escape Rope, `engine/events/overworld.asm:836` |
| `MapSetupScript_Fall` | yes, by falling through `_Door` into `_Train` | hole falls |
| `MapSetupScript_Warp` | **no** | `warp` script opcode (`engine/overworld/scripting.asm:1958`), whiteout (`engine/events/whiteout.asm:21`), new game |
| `MapSetupScript_ReloadMap` | no | `reloadmap` |
| `MapSetupScript_Teleport` | no, runs `JumpRoamMons` instead (line 29) | Fly (`engine/events/overworld.asm:604`), Teleport (`:921`) |
So the walkthrough's "they change route when you change route or enter/exit a
city, cave, or building" is exactly right: connections and door warps both
update. A scripted `warp` does not.
**Scattering: `JumpRoamMons`** (`:677`, `0a:68e0`) / `JumpRoamMon` (`:710`,
`0a:6919`). Picks a uniformly random `RoamMaps` entry, re-rolling only while it
lands on the player's own current map. Two callers:
- `MapSetupScript_Teleport` (Fly / Teleport). This is the walkthrough's "Flying
will reset the roaming legendary's location".
- `engine/menus/intro_menu.asm:283`, `farcall JumpRoamMons`, on **Continue**.
Loading a save scatters all three.
**Meeting one: `CheckEncounterRoamMon`** (`:531`, `0a:681a`), called from the
very top of `ChooseWildEncounter` (`0a:66ab`), which itself is called from
`TryWildEncounter` (`0a:6643`) *after* the map's encounter-rate roll has already
passed. Order of gates:
1. `CheckOnWater` -> on water, no roamer. (Suicune cannot be met while surfing.)
2. One random byte, `cp 100` -> 100/256 pass.
3. `and %00000011`, `jr z` -> 3/4 of those pass. Running total 75/256, about
29.3%, which the asm comments itself.
4. `dec a` -> slot 0, 1 or 2, evenly.
5. That one slot's stored map group/number must equal the player's. There is
**no re-roll onto another beast**, so two beasts on your route still get one
roll each.
6. On success: `wTempWildMonSpecies` = the beast, `wCurPartyLevel` = 40,
`wBattleType` = `BATTLETYPE_ROAMING`, and `ChooseWildEncounter` returns carry
straight to `.startwildbattle`, skipping the map's own slot table entirely. A
roamer *replaces* the normal encounter.
**Sweet Scent is a separate, better door.** `engine/events/sweet_scent.asm:31`
`SweetScentEncounter` runs `CanEncounterWildMon`, then only checks that
`GetMapEncounterRate` is **nonzero** (`ld a, b / and a / jr z`), then calls
`ChooseWildEncounter` directly. It never performs the percentage roll and never
reaches `CheckRepelEffect`. So every Sweet Scent use in grass on a roam route is
a guaranteed encounter with a flat ~9.8% chance (1/3 of 75/256) of being the
specific beast standing there. The walkthrough does not mention this.
**Battle behaviour** (`engine/battle/core.asm`)
- DVs, `:5832`-`:5870`: if the struct's HP byte is nonzero the stored DVs are
reused, otherwise fresh DVs are rolled and written back. A beast you chase all
game is one individual.
- HP, `:6068`-`:6090`: same test. Zero takes `.InitRoamHP`, which writes the
mon's full HP into the struct on the *first* encounter. Non-zero loads the
stored value into `wEnemyMonHP + 1`. This is the walkthrough's "once you've
weakened a legendary once, it will remain at that HP". The comment notes only
the low byte is stored because Raikou and Entei are under 256 HP at level 40.
- `TryEnemyFlee` (`:711`, `0f:44fa`), called at the head of the enemy's half of
the turn in both orders (`Battle_EnemyFirst:823`, `Battle_PlayerFirst:885`).
It bails (beast stays) only if: trainer battle, `SUBSTATUS_CANT_RUN` on the
player (Mean Look / Spider Web), a live `wEnemyWrapCount`, or the enemy is
`FRZ` or asleep. Otherwise `AlwaysFleeMons` (`data/wild/flee_mons.asm`,
`0f:4568`) is exactly `RAIKOU, ENTEI, SUICUNE` and it flees with no roll at
all. This is "the roaming legendaries will all run after one attack or one
ball throw": a failed ball sets `wBattlePlayerAction = BATTLEPLAYERACTION_USEITEM`,
`DetermineMoveOrder` sends it to `.player_first`, and `Battle_PlayerFirst`
still reaches `call TryEnemyFlee`.
Note **paralysis does not stop the flee**; only sleep and freeze do.
- `BattleEnd_HandleRoamMons` (`:8237`, `0f:76ac`): on `WIN` (which covers a
catch) it zeroes HP, sets the map pair to `GROUP_N_A` / `MAP_N_A` and zeroes
the species, retiring the slot. On anything else it banks `wEnemyMonHP + 1`
into the struct and calls `UpdateRoamMons`. Its `.not_roaming` tail is the
other half: after **any** other wild battle, `BattleRandom and $f` gives a
1-in-16 chance the beasts move anyway.
**Tracking: `FindNest`** (`engine/overworld/wildmons.asm:28`, `0a:6560`). Its
`.RoamMon1/.RoamMon2/.RoamMon3` tails append the roamer's current map to the
nest list whenever `wNamedObjectIndex` matches the slot's species, and only for
the Johto pass (`ld a, e / and a / jr nz, .kanto` returns before them). The one
caller is `engine/pokegear/pokegear.asm:2430`, inside `Pokedex_GetArea`
(`24:5c7f`), i.e. the **Pokedex AREA screen**, which is drawn with the Pokegear
town-map engine and headed `<MON>'S NEST`. See section 6 for the wording
mismatch with the walkthrough.
**Catch rate** (`engine/items/item_effects.asm`, `PokeBallEffect` `03:6926`)
- All three beasts have `db 3 ; catch rate`
(`data/pokemon/base_stats/raikou.asm`, `entei.asm`, `suicune.asm`).
- `MASTER_BALL` short-circuits to `.catch_without_fail` before any multiplier.
- `UltraBallMultiplier` (`03:6c92`) is `sla b`, a flat x2 -> effective 6.
- `FastBallMultiplier` (`03:6dbc`) carries a documented bug: it is *intended* to
x4 anything in the three `FleeMons` tables, but the `jr nz, .next` where the
author meant `jr nz, .loop` limits it to the **first three entries of
`SometimesFleeMons`** (Magnemite, Grimer, Tangela). Fast Balls are worthless
on the beasts in Gold/Silver.
- Status: the `.statuscheck` block is also buggy. `+10` for `FRZ`/`SLP` works;
the `BRN`/`PSN`/`PAR` `+5` branch is unreachable because the `ld a,
[wEnemyMonStatus]` reload is commented out. **Paralysis gives no catch bonus
at all**, which contradicts the walkthrough's "asleep or paralyzed".
---
### Roam map set (the other twelve)
These twelve maps carry no roamer-specific script or object; they matter only as
graph nodes and as places to stand in grass. Header rows are from
`data/maps/maps.asm`, dimensions from `constants/map_constants.asm`.
| Map constant | asm file | group | W x H (blocks) | tileset / environment / music | header line |
|---|---|---|---|---|---|
| `MAP_ROUTE_29` | `maps/Route29.asm` | `NEW_BARK` | 30 x 9 | `TILESET_JOHTO` / `ROUTE` / `MUSIC_ROUTE_29` | `data/maps/maps.asm:474` |
| `MAP_ROUTE_30` | `maps/Route30.asm` | `CHERRYGROVE` | 10 x 27 | `TILESET_JOHTO` / `ROUTE` / `MUSIC_ROUTE_30` | `:508` |
| `MAP_ROUTE_31` | `maps/Route31.asm` | `CHERRYGROVE` | 20 x 9 | `TILESET_JOHTO` / `ROUTE` / `MUSIC_ROUTE_30` | `:509` |
| `MAP_ROUTE_32` | `maps/Route32.asm` | `VIOLET` | 10 x 45 | `TILESET_JOHTO` / `ROUTE` / `MUSIC_ROUTE_30` | `:247` |
| `MAP_ROUTE_33` | `maps/Route33.asm` | `AZALEA` | 10 x 9 | `TILESET_JOHTO_MODERN` / `ROUTE` / `MUSIC_ROUTE_30` | `:231` |
| `MAP_ROUTE_34` | `maps/Route34.asm` | `GOLDENROD` | 10 x 27 | `TILESET_JOHTO_MODERN` / `ROUTE` / `MUSIC_ROUTE_36` | `:268` |
| `MAP_ROUTE_38` | `maps/Route38.asm` | `OLIVINE` | 20 x 9 | `TILESET_JOHTO` / `ROUTE` / `MUSIC_ROUTE_37` | `:61` |
| `MAP_ROUTE_39` | `maps/Route39.asm` | `OLIVINE` | 10 x 18 | `TILESET_JOHTO` / `ROUTE` / `MUSIC_ROUTE_37` | `:62` |
| `MAP_ROUTE_43` | `maps/Route43.asm` | `LAKE_OF_RAGE` | 10 x 27 | `TILESET_JOHTO` / `ROUTE` / `MUSIC_LAKE_OF_RAGE` | `:241` |
| `MAP_ROUTE_44` | `maps/Route44.asm` | `MAHOGANY` | 30 x 9 | `TILESET_JOHTO` / `ROUTE` / `MUSIC_LAKE_OF_RAGE` | `:73` |
| `MAP_ROUTE_45` | `maps/Route45.asm` | `BLACKTHORN` | 10 x 45 | `TILESET_JOHTO` / `ROUTE` / `MUSIC_ROUTE_36` | `:185` |
| `MAP_ROUTE_46` | `maps/Route46.asm` | `BLACKTHORN` | 10 x 18 | `TILESET_JOHTO` / `ROUTE` / `MUSIC_ROUTE_36` | `:186` |
Excluded on purpose, and confirmed absent from `RoamMaps`:
`MAP_ROUTE_40` (`maps/Route40.asm`, `CIANWOOD`, 10 x 18,
`data/maps/maps.asm:442`) and `MAP_ROUTE_41` (`maps/Route41.asm`, `CIANWOOD`,
25 x 27, `:443`).
---
### MAP_VIOLET_CITY
- Script: `maps/VioletCity.asm`
- Header: `data/maps/maps.asm:251` -> `TILESET_JOHTO`, `TOWN`,
`LANDMARK_VIOLET_CITY`, `MUSIC_VIOLET_CITY`, phone `FALSE`, `PALETTE_AUTO`,
`FISHGROUP_POND`
- Dimensions: `constants/map_constants.asm:230`, `map_const VIOLET_CITY, 20, 18`
- Connections (`data/maps/attributes.asm:127`): south `Route32` (`ROUTE_32`, 0),
**west `Route36` (`ROUTE_36`, 0)**, east `Route31` (`ROUTE_31`, 9)
Included only because it is the walkthrough's Fly anchor and the one city with a
direct edge onto a roam route. It is not itself a roam map, so arriving here
never gives a beast encounter; it only ticks `UpdateRoamMons` on the way in and
out.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 17 | `VIOLET_MART` | 2 |
| 2 | 18 | 17 | `VIOLET_GYM` | 1 |
| 3 | 30 | 17 | `EARLS_POKEMON_ACADEMY` | 1 |
| 4 | 3 | 15 | `VIOLET_NICKNAME_SPEECH_HOUSE` | 1 |
| 5 | 31 | 25 | `VIOLET_POKECENTER_1F` | 1 |
| 6 | 21 | 29 | `VIOLET_KYLES_HOUSE` | 1 |
| 7 | 23 | 5 | `SPROUT_TOWER_1F` | 1 |
| 8 | 39 | 24 | `ROUTE_31_VIOLET_GATE` | 1 |
| 9 | 39 | 25 | `ROUTE_31_VIOLET_GATE` | 2 |
Every one of these nine is a door warp, i.e. `MAPSETUP_DOOR`, i.e. one
`UpdateRoamMons` in and one on the way out. Stepping in and out of the Pokecenter
is a legal, very short roam tick.
**Coord events** (`def_coord_events`): none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 24 | 20 | `BGEVENT_READ` | `VioletCitySign` |
| 15 | 17 | `BGEVENT_READ` | `VioletGymSign` |
| 24 | 8 | `BGEVENT_READ` | `SproutTowerSign` |
| 27 | 17 | `BGEVENT_READ` | `EarlsPokemonAcademySign` |
| 32 | 25 | `BGEVENT_READ` | `VioletCityPokecenterSign` |
| 10 | 17 | `BGEVENT_READ` | `VioletCityMartSign` |
| 37 | 14 | `BGEVENT_ITEM` | `VioletCityHiddenHyperPotion` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| (1) | `SPRITE_FISHER` | 13 | 16 | `SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT` | `VioletCityEarlScript` | `EVENT_VIOLET_CITY_EARL` |
| (2) | `SPRITE_LASS` | 28 | 28 | `WANDER` | `OBJECTTYPE_SCRIPT` | `VioletCityLassScript` | -1 |
| (3) | `SPRITE_SUPER_NERD` | 24 | 14 | `WANDER` | `OBJECTTYPE_SCRIPT` | `VioletCitySuperNerdScript` | -1 |
| (4) | `SPRITE_GRAMPS` | 17 | 20 | `WALK_LEFT_RIGHT` | `OBJECTTYPE_SCRIPT` | `VioletCityGrampsScript` | -1 |
| (5) | `SPRITE_YOUNGSTER` | 5 | 18 | `SPINRANDOM_SLOW` | `OBJECTTYPE_SCRIPT` | `VioletCityYoungsterScript` | -1 |
| (6) | `SPRITE_FRUIT_TREE` | 14 | 29 | `STILL` | `OBJECTTYPE_SCRIPT` | `VioletCityFruitTree` | -1 |
| (7) | `SPRITE_POKE_BALL` | 4 | 1 | `STILL` | `OBJECTTYPE_ITEMBALL` | `VioletCityPPUp` | `EVENT_VIOLET_CITY_PP_UP` |
| (8) | `SPRITE_POKE_BALL` | 35 | 5 | `STILL` | `OBJECTTYPE_ITEMBALL` | `VioletCityRareCandy` | `EVENT_VIOLET_CITY_RARE_CANDY` |
**Wild encounters**: none relevant (town, no grass on the walked path).
---
### MAP_ROUTE_36
- Script: `maps/Route36.asm`
- Header: `data/maps/maps.asm:249` -> `TILESET_JOHTO`, `ROUTE`,
`LANDMARK_ROUTE_36`, `MUSIC_ROUTE_36`, phone `FALSE`, `PALETTE_AUTO`,
`FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:228`, `map_const ROUTE_36, 30, 9`
- Connections (`data/maps/attributes.asm:206`): north `Route37` (`ROUTE_37`, 10),
south `Route35` (`ROUTE_35`, 0), east `VioletCity` (`VIOLET_CITY`, 0)
- Roam edges (`RoamMaps`): `ROUTE_35`, `ROUTE_31`, `ROUTE_32`, `ROUTE_37`. One
of the two four-way junctions, so a beast standing here has the widest spread.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 18 | 8 | `ROUTE_36_NATIONAL_PARK_GATE` | 3 |
| 2 | 18 | 9 | `ROUTE_36_NATIONAL_PARK_GATE` | 4 |
| 3 | 47 | 13 | `ROUTE_36_RUINS_OF_ALPH_GATE` | 1 |
| 4 | 48 | 13 | `ROUTE_36_RUINS_OF_ALPH_GATE` | 2 |
**Coord events** (`def_coord_events`): none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 29 | 1 | `BGEVENT_READ` | `Route36TrainerTips2` |
| 45 | 11 | `BGEVENT_READ` | `RuinsOfAlphNorthSign` |
| 55 | 7 | `BGEVENT_READ` | `Route36Sign` |
| 21 | 7 | `BGEVENT_READ` | `Route36TrainerTips1` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE36_YOUNGSTER1` | `SPRITE_YOUNGSTER` | 20 | 12 | `STANDING_RIGHT` | `OBJECTTYPE_TRAINER` (sight 2) | `TrainerPsychicMark` | -1 |
| `ROUTE36_YOUNGSTER2` | `SPRITE_YOUNGSTER` | 31 | 14 | `STANDING_LEFT` | `OBJECTTYPE_TRAINER` (sight 5) | `TrainerSchoolboyAlan1` | -1 |
| `ROUTE36_WEIRD_TREE` | `SPRITE_WEIRD_TREE` | 35 | 9 | `SUDOWOODO` | `OBJECTTYPE_SCRIPT` | `SudowoodoScript` | `EVENT_ROUTE_36_SUDOWOODO` |
| `ROUTE36_LASS1` | `SPRITE_LASS` | 51 | 8 | `WALK_LEFT_RIGHT` | `OBJECTTYPE_SCRIPT` | `Route36LassScript` | -1 |
| `ROUTE36_FISHER` | `SPRITE_FISHER` | 44 | 9 | `STANDING_LEFT` | `OBJECTTYPE_SCRIPT` | `Route36RockSmashGuyScript` | -1 |
| `ROUTE36_FRUIT_TREE` | `SPRITE_FRUIT_TREE` | 21 | 4 | `STILL` | `OBJECTTYPE_SCRIPT` | `Route36FruitTree` (`FRUITTREE_ROUTE_36`) | -1 |
| `ROUTE36_ARTHUR` | `SPRITE_YOUNGSTER` | 46 | 6 | `WANDER` | `OBJECTTYPE_SCRIPT` | `ArthurScript` | `EVENT_ROUTE_36_ARTHUR_OF_THURSDAY` |
**Scripts of interest**
- `Route36ArthurCallback` (`MAPCALLBACK_OBJECTS`): `readvar VAR_WEEKDAY`,
`ifequal THURSDAY` -> `appear ROUTE36_ARTHUR`, else `disappear`. Irrelevant to
the beasts but it is the map's only callback, so a bot that emulates map
callbacks must run it.
- `SudowoodoScript`: the one thing on this map that can physically block the
Route 36 loop. `checkitem SQUIRTBOTTLE`; without it the tree just shakes and
the tile stays solid. With it: `loadwildmon SUDOWOODO, 20`, `startbattle`,
`setevent EVENT_FOUGHT_SUDOWOODO`, then `disappear ROUTE36_WEIRD_TREE` on
either branch (`DidntCatchSudowoodo` also disappears it). Once
`EVENT_ROUTE_36_SUDOWOODO` is cleared the tile is free for good.
**Flags and events**
| constant | defined in | read/written by | meaning for a bot |
|---|---|---|---|
| `EVENT_ROUTE_36_SUDOWOODO` | `constants/event_flags.asm:1178` | `object_event` visibility; cleared by `disappear` in `SudowoodoScript` | while set, x=35,y=9 is impassable |
| `EVENT_FOUGHT_SUDOWOODO` | `constants/event_flags.asm` | set in `WateredWeirdTreeScript` | gates the Rock Smash gift and the lass's line |
| `EVENT_ROUTE_36_ARTHUR_OF_THURSDAY` | `constants/event_flags.asm` | `Route36ArthurCallback` | day-of-week NPC |
**Wild encounters** (`data/wild/johto_grass.asm`, `def_grass_wildmons ROUTE_36`,
rates `10 percent` morn/day/nite, Gold column)
| slot | morn | day | nite |
|---|---|---|---|
| 1 | 12 `NIDORAN_M` | 12 `NIDORAN_M` | 12 `NIDORAN_M` |
| 2 | 12 `NIDORAN_F` | 12 `NIDORAN_F` | 12 `NIDORAN_F` |
| 3 | 13 `PIDGEY` | 14 `PIDGEY` | 13 `HOOTHOOT` |
| 4 | 13 `GROWLITHE` | 13 `GROWLITHE` | 13 `GROWLITHE` |
| 5 | 13 `STANTLER` | 13 `STANTLER` | 13 `STANTLER` |
| 6 | 15 `PIDGEY` | 15 `GROWLITHE` | 15 `HOOTHOOT` |
| 7 | 15 `PIDGEY` | 15 `GROWLITHE` | 15 `HOOTHOOT` |
(Silver swaps `GROWLITHE` for `VULPIX` and reverses the Nidoran pair.) Max wild
level 15, which matters for the Max Repel plan in section 3.
---
### MAP_ROUTE_35
- Script: `maps/Route35.asm`
- Header: `data/maps/maps.asm:248` -> `TILESET_JOHTO`, `ROUTE`,
`LANDMARK_ROUTE_35`, `MUSIC_ROUTE_36`, phone `FALSE`, `PALETTE_AUTO`,
`FISHGROUP_POND`
- Dimensions: `constants/map_constants.asm:227`, `map_const ROUTE_35, 10, 18`
- Connections (`data/maps/attributes.asm:202`): north `Route36` (`ROUTE_36`, 0),
south `GoldenrodCity` (`GOLDENROD_CITY`, -5)
- Roam edges: `ROUTE_34`, `ROUTE_35` -> `ROUTE_34`, `ROUTE_36`
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 9 | 33 | `ROUTE_35_GOLDENROD_GATE` | 1 |
| 2 | 10 | 33 | `ROUTE_35_GOLDENROD_GATE` | 2 |
| 3 | 3 | 5 | `ROUTE_35_NATIONAL_PARK_GATE` | 3 |
**Coord events** (`def_coord_events`): none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 1 | 7 | `BGEVENT_READ` | `Route35Sign` |
| 11 | 31 | `BGEVENT_READ` | `Route35Sign` |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| (1) | `SPRITE_YOUNGSTER` | 3 | 19 | `STANDING_RIGHT` | `OBJECTTYPE_TRAINER` (4) | `TrainerCamperIvan` | -1 |
| (2) | `SPRITE_YOUNGSTER` | 8 | 20 | `STANDING_DOWN` | `OBJECTTYPE_TRAINER` (3) | `TrainerCamperElliot` | -1 |
| (3) | `SPRITE_LASS` | 7 | 20 | `STANDING_DOWN` | `OBJECTTYPE_TRAINER` (3) | `TrainerPicnickerBrooke` | -1 |
| (4) | `SPRITE_LASS` | 11 | 24 | `STANDING_LEFT` | `OBJECTTYPE_TRAINER` (3) | `TrainerPicnickerKim` | -1 |
| (5) | `SPRITE_YOUNGSTER` | 14 | 28 | `STANDING_DOWN` | `OBJECTTYPE_TRAINER` (0) | `TrainerBirdKeeperBryan` | -1 |
| (6) | `SPRITE_FISHER` | 2 | 10 | `SPINCOUNTERCLOCKWISE` | `OBJECTTYPE_TRAINER` (2) | `TrainerFirebreatherWalt` | -1 |
| (7) | `SPRITE_BUG_CATCHER` | 16 | 7 | `STANDING_DOWN` | `OBJECTTYPE_TRAINER` (3) | `TrainerBugCatcherArnie` | -1 |
| (8) | `SPRITE_SUPER_NERD` | 5 | 10 | `SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` (2) | `TrainerJugglerIrwin` | -1 |
| (9) | `SPRITE_OFFICER` | 5 | 6 | `STANDING_DOWN` | `OBJECTTYPE_SCRIPT` | `TrainerOfficerDirk` | -1 |
| (10) | `SPRITE_FRUIT_TREE` | 2 | 25 | `STILL` | `OBJECTTYPE_SCRIPT` | `Route35FruitTree` (`FRUITTREE_ROUTE_35`) | -1 |
| (11) | `SPRITE_POKE_BALL` | 13 | 16 | `STILL` | `OBJECTTYPE_ITEMBALL` | `Route35TMRollout` (`TM_ROLLOUT`) | `EVENT_ROUTE_35_TM_ROLLOUT` |
**Scripts of interest**
- `TrainerOfficerDirk` (`maps/Route35.asm:223`): an `OBJECTTYPE_SCRIPT`, not a
sight trainer. `checktime NITE` -> only battles at night, guarded by
`EVENT_BEAT_OFFICER_DIRK`, `loadtrainer OFFICER, DIRK`. A bot oscillating
Route 35 / Route 36 at night will be pulled into this battle once. Note that
a normal wild-or-trainer battle is *not* wasted time: `BattleEnd_HandleRoamMons`'s
`.not_roaming` tail moves the beasts 1 time in 16.
**Wild encounters** (`data/wild/johto_grass.asm`, `def_grass_wildmons ROUTE_35`,
rates `10 percent` all three, Gold column)
| slot | morn | day | nite |
|---|---|---|---|
| 1 | 12 `NIDORAN_M` | 12 `NIDORAN_M` | 12 `NIDORAN_M` |
| 2 | 12 `NIDORAN_F` | 12 `NIDORAN_F` | 12 `NIDORAN_F` |
| 3 | 14 `DROWZEE` | 14 `DROWZEE` | 14 `DROWZEE` |
| 4 | 10 `ABRA` | 10 `ABRA` | 10 `ABRA` |
| 5 | 14 `PIDGEY` | 14 `PIDGEY` | 14 `HOOTHOOT` |
| 6 | 10 `DITTO` | 10 `DITTO` | 10 `DITTO` |
| 7 | 12 `YANMA` | 12 `YANMA` | 12 `YANMA` |
Max wild level 14.
---
### MAP_ROUTE_37
- Script: `maps/Route37.asm`
- Header: `data/maps/maps.asm:250` -> `TILESET_JOHTO`, `ROUTE`,
`LANDMARK_ROUTE_37`, `MUSIC_ROUTE_36`, phone `FALSE`, `PALETTE_AUTO`,
`FISHGROUP_SHORE`
- Dimensions: `constants/map_constants.asm:229`, `map_const ROUTE_37, 10, 9`
- Connections (`data/maps/attributes.asm:211`): north `EcruteakCity`
(`ECRUTEAK_CITY`, -5), south `Route36` (`ROUTE_36`, -10)
- Roam edges: `ROUTE_36`, `ROUTE_38`, `ROUTE_42`. Entei's starting map.
**Warps** (`def_warp_events`): **none**. Route 37 is reached only by connection,
which makes it the cheapest possible `UpdateRoamMons` tick in the loop: one step
across the boundary and one step back.
**Coord events** (`def_coord_events`): none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 5 | 3 | `BGEVENT_READ` | `Route37Sign` |
| 4 | 2 | `BGEVENT_ITEM` | `Route37HiddenEther` (`hiddenitem ETHER, EVENT_ROUTE_37_HIDDEN_ETHER`) |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| `ROUTE37_WEIRD_TREE1` | `SPRITE_WEIRD_TREE` | 6 | 12 | `STANDING_DOWN` | `OBJECTTYPE_TRAINER` (1) | `TrainerTwinsAnnandanne1` | -1 |
| `ROUTE37_WEIRD_TREE2` | `SPRITE_WEIRD_TREE` | 7 | 12 | `STANDING_DOWN` | `OBJECTTYPE_TRAINER` (1) | `TrainerTwinsAnnandanne2` | -1 |
| `ROUTE37_YOUNGSTER` | `SPRITE_YOUNGSTER` | 9 | 6 | `STANDING_LEFT` | `OBJECTTYPE_TRAINER` (3) | `TrainerPsychicGreg` | -1 |
| `ROUTE37_FRUIT_TREE1` | `SPRITE_FRUIT_TREE` | 13 | 5 | `STILL` | `OBJECTTYPE_SCRIPT` | `Route37FruitTree1` (`FRUITTREE_ROUTE_37_1`) | -1 |
| `ROUTE37_SUNNY` | `SPRITE_BUG_CATCHER` | 16 | 8 | `WANDER` | `OBJECTTYPE_SCRIPT` | `SunnyScript` | `EVENT_ROUTE_37_SUNNY_OF_SUNDAY` |
| `ROUTE37_FRUIT_TREE2` | `SPRITE_FRUIT_TREE` | 16 | 5 | `STILL` | `OBJECTTYPE_SCRIPT` | `Route37FruitTree2` (`FRUITTREE_ROUTE_37_2`) | -1 |
| `ROUTE37_FRUIT_TREE3` | `SPRITE_FRUIT_TREE` | 15 | 7 | `STILL` | `OBJECTTYPE_SCRIPT` | `Route37FruitTree3` (`FRUITTREE_ROUTE_37_3`) | -1 |
**Scripts of interest**
- `Route37SunnyCallback` (`MAPCALLBACK_OBJECTS`): `readvar VAR_WEEKDAY`,
`ifequal SUNDAY` -> `appear ROUTE37_SUNNY`, else `disappear`.
- `SunnyScript`: `verbosegiveitem MAGNET` guarded by
`EVENT_GOT_MAGNET_FROM_SUNNY` and `EVENT_MET_SUNNY_OF_SUNDAY`.
**Wild encounters** (`data/wild/johto_grass.asm`, `def_grass_wildmons ROUTE_37`,
rates `10 percent` all three, Gold column)
| slot | morn | day | nite |
|---|---|---|---|
| 1 | 13 `PIDGEY` | 13 `PIDGEY` | 13 `SPINARAK` |
| 2 | 15 `STANTLER` | 15 `STANTLER` | 15 `STANTLER` |
| 3 | 15 `PIDGEY` | 15 `PIDGEY` | 15 `HOOTHOOT` |
| 4 | 14 `GROWLITHE` | 14 `GROWLITHE` | 14 `GROWLITHE` |
| 5 | 15 `PIDGEY` | 15 `PIDGEOTTO` | 15 `SPINARAK` |
| 6 | 15 `PIDGEY` | 16 `GROWLITHE` | 15 `SPINARAK` |
| 7 | 15 `PIDGEY` | 16 `GROWLITHE` | 15 `SPINARAK` |
Max wild level 16.
---
### MAP_ROUTE_42
- Script: `maps/Route42.asm`
- Header: `data/maps/maps.asm:72` -> `TILESET_JOHTO`, `ROUTE`,
`LANDMARK_ROUTE_42`, `MUSIC_LAKE_OF_RAGE`, phone `FALSE`, `PALETTE_AUTO`,
`FISHGROUP_LAKE`
- Dimensions: `constants/map_constants.asm:60`, `map_const ROUTE_42, 30, 9`
- Connections (`data/maps/attributes.asm:231`): west `EcruteakCity`
(`ECRUTEAK_CITY`, -9), east `MahoganyTown` (`MAHOGANY_TOWN`, 0)
- Roam edges: `ROUTE_43`, `ROUTE_44`, `ROUTE_37`, `ROUTE_38`. The other four-way
junction, and Raikou's starting map. This is the walkthrough's Max Repel spot.
**Warps** (`def_warp_events`)
| idx | x | y | destination map | dest warp |
|---|---|---|---|---|
| 1 | 0 | 8 | `ROUTE_42_ECRUTEAK_GATE` | 3 |
| 2 | 0 | 9 | `ROUTE_42_ECRUTEAK_GATE` | 4 |
| 3 | 10 | 5 | `MOUNT_MORTAR_1F_OUTSIDE` | 1 |
| 4 | 28 | 9 | `MOUNT_MORTAR_1F_OUTSIDE` | 2 |
| 5 | 46 | 7 | `MOUNT_MORTAR_1F_OUTSIDE` | 3 |
**Coord events** (`def_coord_events`): none.
**BG events** (`def_bg_events`)
| x | y | type | script/item |
|---|---|---|---|
| 4 | 10 | `BGEVENT_READ` | `Route42Sign1` |
| 7 | 5 | `BGEVENT_READ` | `MtMortarSign1` |
| 45 | 9 | `BGEVENT_READ` | `MtMortarSign2` |
| 54 | 8 | `BGEVENT_READ` | `Route42Sign2` |
| 16 | 11 | `BGEVENT_ITEM` | `Route42HiddenMaxPotion` (`hiddenitem MAX_POTION, EVENT_ROUTE_42_HIDDEN_MAX_POTION`) |
**Object events** (`def_object_events`)
| const | sprite | x | y | movement | type | script label | event flag |
|---|---|---|---|---|---|---|---|
| (1) | `SPRITE_FISHER` | 40 | 10 | `STANDING_LEFT` | `OBJECTTYPE_TRAINER` (1) | `TrainerFisherChris` | -1 |
| (2) | `SPRITE_POKEFAN_M` | 51 | 9 | `SPINRANDOM_FAST` | `OBJECTTYPE_TRAINER` (3) | `TrainerHikerBenjamin` | -1 |
| (3) | `SPRITE_SUPER_NERD` | 47 | 8 | `STANDING_DOWN` | `OBJECTTYPE_TRAINER` (3) | `TrainerPokemaniacShane` | -1 |
| (4) | `SPRITE_FRUIT_TREE` | 27 | 16 | `STILL` | `OBJECTTYPE_SCRIPT` | `Route42FruitTree1` (`FRUITTREE_ROUTE_42_1`) | -1 |
| (5) | `SPRITE_FRUIT_TREE` | 28 | 16 | `STILL` | `OBJECTTYPE_SCRIPT` | `Route42FruitTree2` (`FRUITTREE_ROUTE_42_2`) | -1 |
| (6) | `SPRITE_FRUIT_TREE` | 29 | 16 | `STILL` | `OBJECTTYPE_SCRIPT` | `Route42FruitTree3` (`FRUITTREE_ROUTE_42_3`) | -1 |
| (7) | `SPRITE_POKE_BALL` | 6 | 4 | `STILL` | `OBJECTTYPE_ITEMBALL` | `Route42UltraBall` (`itemball ULTRA_BALL`) | `EVENT_ROUTE_42_ULTRA_BALL` |
| (8) | `SPRITE_POKE_BALL` | 33 | 8 | `STILL` | `OBJECTTYPE_ITEMBALL` | `Route42SuperPotion` (`itemball SUPER_POTION`) | `EVENT_ROUTE_42_SUPER_POTION` |
**Items**
| item | how obtained | source (label / bg_event / hidden) | one-time flag |
|---|---|---|---|
| `ULTRA_BALL` | item ball at x=6, y=4 | `Route42UltraBall` | `EVENT_ROUTE_42_ULTRA_BALL` |
| `SUPER_POTION` | item ball at x=33, y=8 | `Route42SuperPotion` | `EVENT_ROUTE_42_SUPER_POTION` |
| `MAX_POTION` | hidden, x=16, y=11 | `Route42HiddenMaxPotion` | `EVENT_ROUTE_42_HIDDEN_MAX_POTION` |
**Wild encounters** (`data/wild/johto_grass.asm`, `def_grass_wildmons ROUTE_42`,
rates `10 percent` all three, Gold column)
| slot | morn | day | nite |
|---|---|---|---|
| 1 | 15 `MANKEY` | 15 `MANKEY` | 15 `MANKEY` |
| 2 | 13 `MAREEP` | 13 `MAREEP` | 13 `MAREEP` |
| 3 | 14 `SPEAROW` | 14 `SPEAROW` | 14 `ZUBAT` |
| 4 | 16 `SPEAROW` | 16 `SPEAROW` | 16 `ZUBAT` |
| 5 | 15 `FLAAFFY` | 15 `FLAAFFY` | 15 `FLAAFFY` |
| 6 | 17 `FLAAFFY` | 17 `FLAAFFY` | 17 `FLAAFFY` |
| 7 | 17 `FLAAFFY` | 17 `FLAAFFY` | 17 `FLAAFFY` |
Max wild level 17. Every grass slot on this map is below 39, which is precisely
why the walkthrough's Max Repel plan works here (section 3).
---
### Trainers on the four detailed maps
Constants from `constants/trainer_constants.asm`, parties from
`data/trainers/parties.asm` (group and index in the file's own comment).
| const | class | id | party (`data/trainers/parties.asm`) | script label | map |
|---|---|---|---|---|---|
| `MARK` | `PSYCHIC_T` | `PSYCHIC_T (7)` | 13 `ABRA` (Teleport, Flash), 13 `ABRA` (Teleport, Flash), 15 `KADABRA` (Teleport, Kinesis, Confusion) | `TrainerPsychicMark` | Route 36 |
| `ALAN1` | `SCHOOLBOY` | `SCHOOLBOY (3)` | 16 `TANGELA` | `TrainerSchoolboyAlan1` | Route 36 |
| `IVAN` | `CAMPER` | `CAMPER (3)` | 10 `DIGLETT`, 10 `ZUBAT`, 14 `DIGLETT` | `TrainerCamperIvan` | Route 35 |
| `ELLIOT` | `CAMPER` | `CAMPER (4)` | 13 `SANDSHREW`, 15 `MARILL` | `TrainerCamperElliot` | Route 35 |
| `BROOKE` | `PICNICKER` | `PICNICKER (3)` | 16 `PIKACHU` (ThunderShock, Growl, Quick Attack, Double Team) | `TrainerPicnickerBrooke` | Route 35 |
| `KIM` | `PICNICKER` | `PICNICKER (4)` | 15 `VULPIX` | `TrainerPicnickerKim` | Route 35 |
| `BRYAN` | `BIRD_KEEPER` | `BIRD_KEEPER (3)` | 12 `PIDGEY`, 14 `PIDGEOTTO` | `TrainerBirdKeeperBryan` | Route 35 |
| `WALT` | `FIREBREATHER` | `FIREBREATHER (6)` | 11 `MAGMAR`, 13 `MAGMAR` | `TrainerFirebreatherWalt` | Route 35 |
| `ARNIE1` | `BUG_CATCHER` | `BUG_CATCHER (8)` | 15 `VENONAT` | `TrainerBugCatcherArnie` | Route 35 |
| `IRWIN1` | `JUGGLER` | `JUGGLER (1)` | 2 / 6 / 10 / 14 `VOLTORB` | `TrainerJugglerIrwin` | Route 35 |
| `DIRK` | `OFFICER` | (see `OfficerGroup`) | night only | `TrainerOfficerDirk` | Route 35 |
| `ANNANDANNE1` | `TWINS` | `TWINS (2)` | 16 `CLEFAIRY` (Growl, Encore, DoubleSlap, Metronome), 16 `JIGGLYPUFF` (Sing, Defense Curl, Pound, Disable) | `TrainerTwinsAnnandanne1` | Route 37 |
| `ANNANDANNE2` | `TWINS` | `TWINS (3)` | same two, order swapped | `TrainerTwinsAnnandanne2` | Route 37 |
| `GREG` | `PSYCHIC_T` | `PSYCHIC_T (5)` | 17 `DROWZEE` (Hypnosis, Disable, Dream Eater) | `TrainerPsychicGreg` | Route 37 |
| `CHRIS1` | `FISHER` | `FISHER (7)` | 18 `QWILFISH` | `TrainerFisherChris` | Route 42 |
| `BENJAMIN` | `HIKER` | `HIKER (6)` | 14 `DIGLETT`, 14 `GEODUDE`, 16 `DUGTRIO` | `TrainerHikerBenjamin` | Route 42 |
| `SHANE` | `POKEMANIAC` | `POKEMANIAC (4)` | 16 `NIDORINA`, 16 `NIDORINO` | `TrainerPokemaniacShane` | Route 42 |
All of these are one-and-done (`EVENT_BEAT_*` in the `trainer` macro row). By
this point in the game they are already beaten and are not obstacles.
---
### The beasts as battle opponents
`data/pokemon/base_stats/{raikou,entei,suicune}.asm`, level from
`InitRoamMons`, moves from `data/pokemon/evos_attacks.asm` (last four learned at
or before 40).
| species | types | base HP/Atk/Def/Spe/SpA/SpD | catch rate | level 40 moveset |
|---|---|---|---|---|
| `RAIKOU` | Electric | 90 / 85 / 75 / 115 / 115 / 100 | 3 | `LEER`, `THUNDERSHOCK`, `ROAR`, `QUICK_ATTACK` |
| `ENTEI` | Fire | 115 / 115 / 85 / 100 / 90 / 75 | 3 | `LEER`, `EMBER`, `ROAR`, `FIRE_SPIN` |
| `SUICUNE` | Water | 100 / 75 / 115 / 85 / 90 / 115 | 3 | `LEER`, `WATER_GUN`, `ROAR`, `GUST` |
All three know **Roar**. `BattleCommand_ForceSwitch`'s wild-target branch
(`engine/battle/effect_commands.asm:4913`, `.force_player_switch` at `:5010`
-> `.wild_succeed_playeristarget` at `:5041`) compares
`wCurPartyLevel` against `wBattleMonLevel` and, when the wild level is greater
or equal, succeeds unconditionally: `wForcedSwitch` = TRUE and `wBattleResult` =
DRAW, ending the battle on the spot. Against a lead at level 40 or below that is
a guaranteed battle-ender if the beast picks Roar. The DRAW result then routes
through `BattleEnd_HandleRoamMons`'s non-WIN arm, so the beast keeps its damage
and moves.
---
## 3. Blockers and gates
| Gate | Enforced by (file:label) | Requirement | Unlock condition |
|---|---|---|---|
| The beasts do not exist at all | `maps/BurnedTowerB1F.asm:65` `special InitRoamMons`, guarded by `coord_event 9, 5, SCENE_BURNEDTOWERB1F_RELEASE_THE_BEASTS, ReleaseTheBeasts` | walk the Burned Tower B1F trigger | `EVENT_RELEASED_THE_BEASTS` set, `SCENE_BURNEDTOWERB1F_NOOP` |
| No roamer on water | `engine/overworld/wildmons.asm:534` `CheckEncounterRoamMon` `call CheckOnWater / jr z, .DontEncounterRoamMon` | be on a land tile | none, this is absolute |
| No roamer off the sixteen routes | `data/wild/roammon_maps.asm` `RoamMaps` + the map compare in `CheckEncounterRoamMon:546` | stand on the beast's stored map | none |
| No roamer where there is no wild table | `engine/overworld/wildmons.asm:270` `LoadWildMonDataPointer` returning nc -> `.nowildbattle` before `CheckEncounterRoamMon` runs | map must have grass/water data | none |
| No roamer in the Bug Contest | `engine/overworld/events.asm:1194` `ChooseWildEncounter_BugContest` never calls `CheckEncounterRoamMon` | leave the contest | none |
| Route 36 tile x=35, y=9 blocked | `maps/Route36.asm` `SudowoodoScript`, `checkitem SQUIRTBOTTLE` | `SQUIRTBOTTLE` in bag, then win/lose/flee the level 20 Sudowoodo | `EVENT_ROUTE_36_SUDOWOODO` cleared by `disappear` |
| Beast escapes after one player action | `engine/battle/core.asm:711` `TryEnemyFlee` + `data/wild/flee_mons.asm` `AlwaysFleeMons` | trap it: `SUBSTATUS_CANT_RUN` (Mean Look / Spider Web), a live `wEnemyWrapCount`, or inflict `SLP` / `FRZ` | any one of those four makes `TryEnemyFlee` return no-carry |
| Repel filters the *wild*, not the beast | `engine/overworld/wildmons.asm:366` `CheckRepelEffect`: `ld a, [wCurPartyLevel] / cp [hl] / jr nc, .encounter` where `hl` is the first non-fainted party mon's level | roamer level is 40, so the lead must be **level 40 or lower** or the roamer is repelled too | Route 42 grass tops out at level 17, so a level 39 lead plus `MAX_REPEL` (250 steps, `engine/items/item_effects.asm:2058`) blocks every normal encounter and passes the beast. A level 41+ lead blocks the beast as well. |
| Fast Ball does nothing here | `engine/items/item_effects.asm:986` `FastBallMultiplier`, documented bug (`jr nz, .next` instead of `jr nz, .loop`) | use `ULTRA_BALL` (x2, `03:6c92`) or `MASTER_BALL` (short-circuit) | none, the bug is in the shipped ROM |
| Paralysis gives no catch bonus | `engine/items/item_effects.asm:344` `.statuscheck`, the `ld a, [wEnemyMonStatus]` reload is commented out | `SLP` or `FRZ` only (+10 to the rate) | none |
| Ultra Ball / Max Repel supply | `data/items/marts.asm:216 MartBlackthorn`, `:384 MartIndigoPlateau` | money | both marts stock `ULTRA_BALL` and `MAX_REPEL` |
Nothing in this section is badge- or HM-gated on its own. The HMs needed to
reach individual roam routes (Surf for Route 41's neighbours, Whirlpool, etc.)
belong to their own sections; all sixteen roam maps are walkable by the time the
beasts are loose.
---
## 4. Bot checklist
Preconditions for the whole section: `EVENT_RELEASED_THE_BEASTS` set,
`save.roamers` (port) / the three `roam_struct`s (cart) populated, `BICYCLE` in
the bag, `SQUIRTBOTTLE` already used on Route 36.
1. **Locate.** Map `MAP_*` any, action: open Pokedex -> select `RAIKOU` /
`ENTEI` / `SUICUNE` -> AREA. Precondition: species is in the *seen* dex
(set by the first encounter, not by the Burned Tower cutscene).
Postcondition: the blinking nest icon is the beast's current map
(`FindNest.RoamMon1/2/3`). If the species is retired the icon is absent.
2. **Anchor.** Fly to `MAP_VIOLET_CITY`. Warning: the Fly itself runs
`JumpRoamMons`, so any location read from step 1 is stale afterwards. Do step
1 *after* the Fly, not before.
3. **Oscillate (cheap ticks).** From Violet City walk west onto `MAP_ROUTE_36`
(connection, `data/maps/attributes.asm:209`). Then repeat: step north onto
`MAP_ROUTE_37` and back, step south onto `MAP_ROUTE_35` and back. Each single
boundary crossing is one `MapSetupScript_Connection` and therefore one
`UpdateRoamMons`. Route 37 has zero warps, so the boundary step is the only
thing that happens there.
4. **Alternative tick.** Any door warp works too: on `MAP_VIOLET_CITY` step onto
warp 5 (x=31, y=25, `VIOLET_POKECENTER_1F`) and back out. That is
`MAPSETUP_DOOR` -> `MapSetupScript_Train` -> `UpdateRoamMons`, and it also
heals.
5. **Fish for the encounter.** Stand in grass on the map the beast is on and
walk. Each step that passes `CanEncounterWildMon` and the map's 10% rate roll
then has a 75/256 roamer roll, of which 1/3 selects your beast.
Postcondition: `wBattleType = BATTLETYPE_ROAMING`.
6. **Better: Sweet Scent.** Same tile, use `SWEET_SCENT`. It skips the 10% roll
and the repel check entirely (`engine/events/sweet_scent.asm:31`), so every
use is an encounter and ~9.8% of uses are the specific beast. This is the
highest-throughput option and the walkthrough does not mention it.
7. **Repel filter (optional).** Lead a party mon of level 39 or 40 (a raised
Scyther works; it learns `FALSE_SWIPE` at level 18 per
`data/pokemon/evos_attacks.asm:1674`, and at 39 its auto-generated four are
False Swipe / Agility / Wing Attack / Slash). Use `MAX_REPEL` (250 steps).
Precondition: lead level <= 40. Postcondition: only the level 40 beast can
still trigger.
8. **Save.** Before every encounter. Battle outcomes are irreversible: a `WIN`
retires the slot for the rest of the file.
9. **In battle, turn 1 only.** Options, in order of value:
- `MASTER_BALL` -> guaranteed, but there is only one and there are three
beasts.
- Trap first (`MEAN_LOOK` / `SPIDER_WEB`, or a wrap move), which makes
`TryEnemyFlee` return no-carry and buys unlimited turns. This is the only
way to legitimately chip HP and land status.
- Otherwise: one `FALSE_SWIPE` (leaves 1 HP, banked into the struct for next
time) **or** one `ULTRA_BALL`. Either way the beast is gone at the end of
the turn.
- Do not count on `Roar`: if the beast picks it against a lead of level 40 or
lower the battle ends immediately as a DRAW.
10. **After the battle.** `BattleEnd_HandleRoamMons` has already banked HP and
moved the beast. Return to step 1.
11. **Repeat per beast.** Three independent slots. Retiring one does not affect
the other two; their `RoamMaps` walk is unchanged.
---
## 5. Port coverage
| Beat | Port file | Status |
|---|---|---|
| `roam_struct` on the save (species/level/map/hp/dvs, three slots in Raikou/Entei/Suicune order) | `src/core/gen2/Roamers.lua:41-47`, `:184-217` | implemented |
| `InitRoamMons` reachable through the real special dispatch | `src/script/gen2/Specials.lua:1843-1848`, `src/core/gen2/Roamers.lua:196` | implemented |
| `RoamMaps` graph, all 16 rows, Routes 40/41 excluded | `src/core/gen2/Roamers.lua:58-75` | implemented, but hardcoded: `Roamers.mapTable` looks for `encounters.roamMaps` and the extractor does not emit it yet (`:77-85`) |
| `.Update` shared-byte jump/index roll, last-map re-roll | `src/core/gen2/Roamers.lua:142-158` | implemented |
| `JumpRoamMon` random entry, re-roll off the player's map | `src/core/gen2/Roamers.lua:117-126` | implemented |
| `_BackUpMapIndices` | `src/core/gen2/Roamers.lua:223-234` | implemented |
| `UpdateRoamMons` on `CONNECTION` / `DOOR` / `FALL` / `TRAIN` | `src/world/gen2/World.lua:211-215` (`MAPSETUP_ROAM_UPDATE`), `:2626-2633` | implemented, and the set matches `data/maps/setup_scripts.asm` exactly |
| `JumpRoamMons` on `TELEPORT` (Fly / Teleport) | `src/world/gen2/World.lua:215`, `:2617-2624` | implemented |
| `JumpRoamMons` on **Continue** (`engine/menus/intro_menu.asm:283`) | - | **missing**: `Roamers.jumpAll` has exactly one caller, `roamMonsBeforeLoad`, which is keyed on `MAPSETUP_TELEPORT`. Loading a save does not scatter the beasts. |
| `CheckEncounterRoamMon` (water gate, 100/256, `and %11`, slot map compare, no re-roll) | `src/core/gen2/Roamers.lua:288-301`; called from `src/world/gen2/World.lua:3043` and `:3790` | implemented |
| Roamer check ordered **after** the map's encounter-rate roll | `src/world/gen2/World.lua:3043` vs `:3053-3060` | **partial**: the port runs `Roamers.checkEncounter` *before* `Encounter.triggers`, so a beast can appear on a step whose 10% roll would have failed. The asm order is `TryWildEncounter.EncounterRate` -> `ChooseWildEncounter` -> `CheckEncounterRoamMon`. Beast encounter rate is inflated by roughly 1/rate. |
| Sweet Scent path also consults the roamers, and skips the rate roll | `src/world/gen2/World.lua:3785-3796` | implemented, and the "nonzero rate only" check at `:3786` matches `engine/events/sweet_scent.asm:38` |
| `CheckRepelEffect` (lead level vs `wCurPartyLevel`) | - | **missing**: `save.repelSteps` counts down in `src/world/gen2/StepEvents.lua:98-101` and prints the wore-off text, but nothing in `World:tryWildEncounter` consults it. The walkthrough's Max Repel plan is a no-op in the port today. |
| DV roll on first encounter, DVs kept afterwards | `src/core/gen2/Roamers.lua:311-328` | implemented |
| HP banked in a single byte, reloaded on the next encounter | `src/core/gen2/Roamers.lua:182`, `:311-328`, `:341-354` | implemented |
| `BattleEnd_HandleRoamMons` WIN/caught retires the slot | `src/core/gen2/Roamers.lua:345-350`, wired at `src/world/gen2/World.lua:4475-4478` | implemented |
| `.not_roaming` 1-in-16 drift after any other wild battle | `src/core/gen2/Roamers.lua:359-363` | implemented |
| `AlwaysFleeMons` / `OftenFleeMons` / `SometimesFleeMons` and the shared roll byte | `src/core/gen2/Roamers.lua:370-380`, `src/battle/gen2/Battle.lua:1639-1651` | implemented |
| Sleep / freeze suppress the flee | `src/battle/gen2/Battle.lua:1641-1642` | implemented |
| `SUBSTATUS_CANT_RUN` (Mean Look) and `wEnemyWrapCount` suppress the flee | - | **missing**, and the port says so itself at `src/battle/gen2/Battle.lua:1632-1635`. There is currently no way to trap a beast, which removes the only legitimate multi-turn strategy. |
| Failed ball still costs the turn, so the beast flees | `src/ui/gen2/BattleState.lua:900-903` -> `Battle:takeTurn{kind="item"}` -> `Battle:tryEnemyFlee` (`:1877`) | implemented |
| `MASTER_BALL` never fails; `ULTRA_BALL` x2 | `src/battle/gen2/Catching.lua:29-30`, `:62-63` | implemented |
| `FastBallMultiplier` bug (x4 only for Magnemite/Grimer/Tangela) | `src/battle/gen2/Catching.lua:39` (`FAST_BALL = 1`) | **partial**: the port is correct for the beasts (flat x1) but drops the x4 the three buggy species do get |
| Pokedex AREA screen shows the roamer's current map (`FindNest.RoamMon1/2/3`) | `src/ui/TownMap.lua:167-198` | **missing**: nest mode scans only the map wild-slot tables for the species; there is no roamer branch, so a located beast never appears on the AREA map |
| `BattleCommand_ForceSwitch` (Roar from a wild beast ends the battle as a DRAW) | - | **missing**: `EFFECT_FORCE_SWITCH` exists only as an AI scoring hook (`src/battle/gen2/Ai.lua:559`); there is no effect handler, so a roamer's Roar does not end the battle |
| Headless coverage | `tests/gen2_world_test.lua:2203-2271`, `tests/gen2_roamers_test.lua`, `tests/drivers/gold_roamers.lua` | implemented: the driver runs the real `InitRoamMons` special, asserts the three starting routes, and exercises `MAPSETUP_DOOR` ticks |
---
## 6. Unresolved / verify by hand
1. **"They will show up on the map in Pokegear."** The only caller of `FindNest`
is `engine/pokegear/pokegear.asm:2430`, inside `Pokedex_GetArea` (`24:5c7f`),
whose header string is `'S NEST` (`.PlaceString_MonsNest`). That is the
**Pokedex AREA** screen, which is *drawn* with the Pokegear town map code but
is not reached from the Pokegear menu. Nothing in `_TownMap`
(`engine/pokegear/pokegear.asm:1709`) plots roamers. Treat the walkthrough's
wording as loose.
2. **"After the initial encounter ... they will show up."** `FindNest` itself has
no "encountered" gate; it only compares `wNamedObjectIndex` with the slot's
species. The real gate is that you must be able to select the species in the
Pokedex at all, i.e. it must be SEEN. I could not find anything in
`maps/BurnedTowerB1F.asm` that marks the three as seen during
`ReleaseTheBeasts` (it only `appear`s sprites and plays cries), so in practice
the first battle is what unlocks the AREA view. Not proven by a single
`SetSeenMon` call; worth a hardware check.
3. **"asleep or paralyzed."** Sleep and freeze are worth `+10` to the catch rate
(`engine/items/item_effects.asm:344`). Paralysis is worth **nothing** because
of the commented-out `ld a, [wEnemyMonStatus]` reload in the same block, which
the file itself flags as a bug. The advice is half wrong on the cart.
4. **"they occasionally will jump a few routes."** The asm's only "jump" is the
1-in-32 `and %00011111 / jr z, JumpRoamMon` branch inside `.Update`, which is
not "a few routes" but a uniformly random one of the sixteen. The observed
behaviour matches loosely; the mechanism does not.
5. **"as quickly as every 10 seconds."** No timer anywhere in the roam code. Roam
movement is purely event-driven (map setup scripts and battle ends). The ten
seconds is a human estimate of how long two boundary crossings take.
6. **Roar vs the level-39 Scyther plan.** `BattleCommand_ForceSwitch`'s wild
branch reads `wCurPartyLevel` for the *wild* mon's level and compares it with
`wBattleMonLevel`; at 40 vs 39 it takes `.wild_succeed_playeristarget`
unconditionally. I did not trace whether `wCurPartyLevel` is still the wild
level at that point after a mid-battle player switch (several routines write
that variable), so the "guaranteed Roar" claim is asserted from the code as
written and should be confirmed on hardware before a bot relies on it.
7. **Bike and encounter rate.** The walkthrough leans on biking. `CanEncounterWildMon`
(`engine/overworld/events.asm:1164`) and `GetMapEncounterRate` have no bicycle
term, so the bike changes travel speed only, not encounter odds. Stated here
because the opposite is widely believed.
8. **Suicune's starting route vs the walkthrough's framing.** `InitRoamMons`
places Suicune on `ROUTE_38`, not at the Tin Tower or Route 42. The section
text never states a starting route, so there is no contradiction, but a bot
that assumes "Suicune is the one you chase last" has no asm basis for it.
9. **Route dimensions vs event coordinates.** `map_const` widths/heights are in
blocks; `warp_event` / `object_event` / `bg_event` coordinates are in the
2x-finer walk grid (e.g. `ROUTE_35` is 10 x 18 blocks and carries a warp at
y=33). Every coordinate in this document is the raw asm value, unconverted.
@@ -0,0 +1,119 @@
==============================================================================
==============================================================================
==============================================================================
(c06)
------------------------------------------------
---- 6 > Chikorita, Cyndaquil, or Totodile? ----
------------------------------------------------
Video Playthrough Link (Introduction through Violet City Gym):
https://www.youtube.com/watch?v=dqJtkx8DcE0
|-------------|
|New Bark Town|
|-------------|
Pokémon found in New Bark Town:
#152 Chikorita
#155 Cyndaquil
#158 Totodile
Items found in New Bark Town:
Everstone
Pokegear
When you first start, you will be in New Bark Town. Choose new game and start.
Afterwards, Professor Oak will ask you the time. First answer what hour it is
and what minute it is, so you are setting the game's time.
There is a default time, which is 10:00 AM, but you may set the time for
whatever you'd like it to be. Preferably, it should be the time that it is where
you live, because then it'd make more sense. They have it well-programmed on
Gold/Silver that even if you don't have the GBA on, the time will keep running.
Then, Professor Oak will welcome you to the world of Pokémon like the older
versions of Pokémon and introduces you to Professor Elm. Basically Pokémon
cannot be lived without in the worlds of Johto and Kanto. Then he asks you what
your name is, so input that. Choose something as your name and your Pokémon
Gold/Silver adventure finally unfolds!
You are in your room at the beginning. Exit the room, and you will meet your
mom. She tells you that your Pokegear is back from the repair shop. You get the
Pokegear. Pokegear is used to call people on a phone and also see the day and
time of the day. Then, when she asks you to input the day of the week, put that
in. You can even put in Daylight Savings Time. How cool! Remember, that's the
cycle from April to October. The other half of the year is not. The Phone is
part of the Pokegear, so simply go there and phone whoever. Kinda cool,
actually. She'll want you to call from time to time.
Leave your house and head left to Professor Elm's lab. Notice the guy standing
left of it in that little corner. That will be your rival. If you talk to him,
he'll push you out of the way. So rude. Anyway, go into Elm's lab. He'll want
you to do him a favor by raising up a Pokémon. Win-win situation, no? He rants
on about Mr. Pokémon's real discovery which wouldn't make any sense yet. You get
to pick your starter Pokémon. Which Pokémon should you take?
|------------|
|THE STARTERS|
|------------|
As I mentioned before, your starter Pokémon choice really matters. You can't get
the other two starters short of restarting your game save, getting a second game
(like the other version), or trading someone else for one. Which Pokémon should
YOU choose? Let's analyze all three starters. Thard_Verad notes that the
evolution levels vary in this game:
"Chikorita -16-> Bayleef -32-> Meganium
Cyndaquil -14-> Quilava -36-> Typhlosion
Totodile -18-> Croconaw -30-> Feraligatr
As you can see, Cyndaquil is first to evolve into its intermediate form, but
slowest to reach its final form. Totodile is the other way
around, and Chikorita is in the middle."
Here are additional thoughts below on each type.
CHIKORITA: Grass Type.
Chikorita is the hardest starter to start with and will struggle against the
first two gyms. Chikorita will also struggle a bit against the fourthgym. And
Team Rocket likes using poison types, which have a type advantage against grass
types. Expert trainers should use Chikorita if they want a challenge.
Moves to Keep:
Tackle (35 PWR) => Body Slam (85 PWR)
Growl => Poison Powder => HM04 Strength (80 PWR)
Razor Leaf (55 PWR) => Solar Beam (120 PWR)
Reflect => Synthesis
CYNDAQUIL: Fire Type.
Cyndaquil evolves to its second form faster than the other two starters and has
a powerful Flamethrower move in its final form. Cyndaquil is the best starter
for the first two gyms, but slows down a bit after that because Cyndaquil only
really has fire and normal type moves.
Moves to Keep:
Tackle (35 PWR) => Quick Attack (40 PWR)
Leer => Swift (60 PWR) => HM04 Strength (80 PWR)
SmokeScreen => Flamethrower (95 PWR)
Ember (40 PWR) => Flame Wheel (60 PWR)
TOTODILE: Water Type.
Totodile is a great choice for speedrunning. When evolved, it can make use of
two powerful HMs: Surf AND Strength. Totodile also eventually gets Bite, a dark
type move, for balance.
Moves to Keep:
Scratch (40 PWR) => Slash (70 PWR) => Hydro Pump (120 PWR)
Leer => Bite (60 PWR)
Rage (20 PWR) => HM04 Strength (80 PWR)
Water Gun (40 PWR) => HM03 Surf (95 PWR)
@@ -0,0 +1,369 @@
==============================================================================
==============================================================================
==============================================================================
(c07)
-----------------------------------------------
---- 7 > Cherrygrove City and Routes 29-31 ----
-----------------------------------------------
Video Playthrough Link (Introduction through Violet City Gym):
https://www.youtube.com/watch?v=dqJtkx8DcE0
After you get your starting Pokémon, He says that Mr. Pokémon lives somewhere
past Cherrygrove, the next city. Then you get Elm's phone number. He'll call you
a few times over the course of the game. And just like in the Red, Blue, and
Yellow games, the professor's assistant gives you something. In this case, it's
a free potion. Potions can be used to heal your Pokémon up to 20 HP. Now leave
the lab!
|-------------|
|New Bark Town|
|-------------|
After choosing your Pokémon carefully, it seems your stupid rival's still
outside the lab. Head west onto Route 29, where your first battles with wild
Pokémon will begin.
|--------|
|Route 29|
|--------|
Pokémon found on Route 29:
#016 Pidgey
#019 Rattata
#161 Sentret
#163 Hoothoot
#187 Hoppip
Items found on Route 29:
Berry
Pink Bow
Potion
|-----------|-----------|--------|------------------|
| Chikorita | Cyndaquil |Totodile| THE |
|-----------|-----------|--------| STARTING |
| Tackle | Tackle | Scratch| Pokémon |
| Growl | Leer | Leer | TYPES |
|-----------|-----------|--------|------------------|
|Move Type PP Power Description |
|---------------------------------------------------|
|Tackle Normal 35 35 A full-body charge attack. |
|Scratch Normal 35 40 Scratches with sharp claw. |
|Growl Normal 40 -- Reduces the foe's attack. |
|Leer Normal 30 -- Reduces the foe's defense. |
|---------------------------------------------------|
Here you will fight all different varieties of Pokémon, depending what time of
day it is, or what day it is. Note that the battle system looks all cool now
because it looks far more obvious how much HP you have, and how much EXP you
need your Pokémon to get before they advance to the next level. The EXP meter is
below the HP meter. Also note the new gender differences in Pokémon.
In any case, you'll want your starting Pokémon to be at Level 8 by the time you
reach Cherrygrove City, so keep training. If you're new to the Pokémon world,
you might also want to talk to the trainers on the path, as they give good
advice. When you're on Route 29, to the north is Route 46. You can go there and
visit if you'd like, but it'll be a pain to defeat any Pokémon there because you
only have normal moves. The berry is in the tree in the northwest, the potion is
in the northeast.
Thard_Verad adds, "Berry and Apricorn trees regenerate every day so don't be
afraid to pick them. And go back tomorrow and pick them again!"
Remember to have your starting Pokémon at Level 8 before you pass through
Cherrygrove City. You really need the experience. Keep heading west until you
arrive in...
|----------------|
|Cherrygrove City|
|----------------|
Items found in Cherrygrove City:
Map Card
See that man at the entrance to the city? Talk to him and he will show you
around the city and some key Pokémon concepts, as well as places, like the
Pokémon Center, the PoKeMart, the routes, the sea, and his house. Most
importantly, you get the Map Card when he gives you it at the end. Then go into
the Pokémon Center, and heal your starting Pokémon by talking to the nurse.
Don't bother trying to get Pokeballs because they aren't being sold just yet.
Head west then north up Route 30 when you're at Level 8. If you're not, go back
to Route 29, fight till your starter is about to faint, heal again at the
Pokémon Center in Cherrygrove City , and then head up Route 30, since you can't
buy anything from the PoKeMart yet.
|--------|
|Route 30|
|--------|
Pokémon found on Route 30:
#010 Caterpie
#011 Metapod
#016 Pidgey
#019 Rattata
#165 Ledyba (Silver only, morning only)
Items found on Route 30:
Antidote
Berry (2X)
Mystery Egg
PoKeDeX
PSNCure Berry
Welcome to Route 30! Reading the sign, you'll notice this is the midpoint route
between Cherrygrove City and Violet City. On this route, your starter Pokémon
should reach Level 10 by the time you get back to Cherrygrove City. Go a bit up
from the sign and you'll find a house with a tree next to it. Grab the berry
next to it and then enter the house for another berry.
Don't actually use them, though. Potions are better most of the time. Just above
the house is an Antidote. Take that and head up the right fork. On the left
fork, you'd see a guy that will tell you that he trains his Pokémon, as well as
two boys battling on the path. You can't pass that area just yet. Going up the
right path, you'll eventually spot a house. To the left is a tree containing a
PSNCure Berry. Take that, then enter the house.
You'll find Mr. Pokémon and Professor Oak inside. Mr. Pokémon gives you the
Mystery Egg, which Professor Elm wants to see. Apparently, the Daycare Service
gave Mr. Pokémon that Mystery Egg. This is the reason you must go back to
Professor Elm later, because he knows the evolutionary stuff even better than
Professor Oak! Wow, where'd he come from? Professor Oak wants you to help him
track down more Pokémon, and gives you the Pokedex. How dependable you are! It's
basically an encyclopedia, dictionary, and recorder all in one, if that makes
any sense. Professor Oak has to go for a radio show in Goldenrod, but Mr.
Pokémon will heal your Pokémon before you leave.
Right when you leave the door, Professor Elm will give you a call on your cell
phone. He tells you that something terrible has happened in the lab. Now that
you're Professor Elm's 911 phone contact, you'll want to quickly head back to
New Bark Town. Consider training up your Pokémon to Level 10 before you even
exit Cherrygrove City onto Route 29. Keep training and heal at Cherrygrove's
Pokémon Center if you need to first.
|----------------|
|Cherrygrove City|
|----------------|
Heal at the Pokémon Center. Do not forget this. After your Level 10 Pokémon is
prepared for combat, exit Cherrygrove City to the east. Something stops you. You
notice that it's your rival. He is currently named ???. I've never met anyone
with that name before. I wonder who named him that? He calls you a wimp, but I
think he's the wimp around here. Your Level 10 Starter should completely destroy
his Level 5 one. XD
|------------------------|
|Rival |
|Level 5 Starter (84 EXP)|
|You get: 300G |
|------------------------|
Well, that was an easy battle. You probably lost less than 10HP. Now that you've
beaten him, he gets past you and now you can exit to Route 29 again. No way
he'll be the world's best Pokémon trainer.
Moving forward, you will want to be cognizant of starter type weaknesses. Think
of it like paper-rock-scissors. Water beats fire. Fire beats grass. And grass
beats water. Your rival will always have the starter that can use super
effective moves against your starter. Keep that in mind as you play through the
game.
|--------|
|Route 29|
|--------|
Pokémon found on Route 29:
#016 Pidgey
#019 Rattata
#161 Sentret
#163 Hoothoot
#187 Hoppip
Keep heading east to New Bark Town. Keep training, too. Your starter should be
Level 11 by the time you reach New Bark, go back through Route 29, and back to
Cherrygrove.
|-------------|
|New Bark Town|
|-------------|
Go back to Professor Elm's laboratory and talk with the police officer in his
lab. The police officer says he has talked to Professor Elm, and when you note
that you've seen this person, you get to name your rival, the one who you just
battled and had the Pokémon stolen from Professor Elm. Name him whatever name
you feel like naming him. That's supposed to help the investigation, apparently.
He goes by the name Silver in the Pokémon community, though.
Talk to Professor Elm and give him the discovery from Mr. Pokémon, the Mystery
Egg. Professor Elm is very surprised at you receiving a PoKeDeX. Only the
trainers with the best potential get one, after all. You're amazing already!
Professor Elm informs you that he thinks you have what it takes to try to get
the eight badges for the Pokémon Johto League.
When you try to leave the lab, Professor Elm's aide will come up to you again
and give you five Pokeballs to begin your journey. Excellent. Now go right to
your home and talk to your mom, who will save your money for you. Now your
journey truly begins. Head west back onto Route 29.
|--------|
|Route 29|
|--------|
Pokémon found on Route 29:
#016 Pidgey
#019 Rattata
#161 Sentret
#163 Hoothoot
#187 Hoppip
Start heading west back to Cherrygrove City again. Before you can even go
anywhere, the first trainer will come up to you and teach you how to catch
Pokémon, so if you need the tutorial, by all means let him teach you. It's a
demonstration of him catching a Level 5 Rattata with a Pokeball. Nothing
special, really. Catch both a Hoothoot and Rattata on this route or Route 46.
Rattata's Super Fang can be a useful move early on. And Hoothoot can use the
ater HM Fly.
In the Items pocket, note that there are now 4 sections. The 2nd one contains
the Pokeballs. Before you even fight a Pidgey, reach Level 11. Then, you are
able to do more damage and keep the Level 3 Pidgey low on HP. Capture it. Don't
go to Cherrygrove City yet. First, make a visit north onto Route 46.
|--------|
|Route 46|
|--------|
I know it doesn't look like much, but here there's a better chance of finding a
Rattata. Don't use your Level 11 Starter to catch it, you'll just kill it in a
hit. Instead, use Pidgey to weaken it, then capture it. An advanced strategy
would be to keep leveling up your starter Pokémon and have an imbalanced Pokémon
lineup. However, if you want to use a more balanced lineup, this could be your
2nd Pokémon that you use in your game playthrough, as its super fang is super
useful. Weaken it to the point where Pidgey is also weakened, then capture it.
Head back onto Route 29, then west to Cherrygrove City.
|----------------|
|Cherrygrove City|
|----------------|
Heal at the Pokémon Center, then head north onto Route 30! If you want a more
balanced Pokémon lineup where you level up more than one Pokémon, move that
Pokémon to the #1 spot in your lineup. That way, Rattata will appear in battle
first. You'll want to switch to your starter on turn 1, and then your starter
will defeat the other Pokémon. Then both your Pokémon get equal experience and
the Pokémon you are leveling up will grow quickly. Do this even in trainer
battles.
|--------|
|Route 30|
|--------|
Pokémon found on Route 30:
#010 Caterpie
#011 Metapod
#013 Weedle
#014 Kakuna
#016 Pidgey
#042 Zubat
#06? Poliwag
#163 Hoot Hoot
#165 Ledyba (Silver only, morning only)
#167 Spinarak (Gold only, night only)
OK, this time, you'll want to steer away from the right path but rather to the
left path. On the left path, you'll meet some resistance. The two boys that were
fighting earlier will both challenge you to battles, since they've already
battled each other.
Youngster Joey
Level 4 Rattata (48 EXP)
You get: 64G
Note that now some of your money goes to your mom for safekeeping now. Move up
to the next trainer for the next battle!
Youngster Mikey
Level 2 Rattata (24 EXP)
Level 4 Rattata (48 EXP)
You get: 64G
Go pass the lady to the third trainer to fight!
Bug Catcher Don
Level 3 Caterpie (33 EXP)
Level 3 Caterpie (33 EXP)
You get: 48G
Advance north onto Route 31.
|--------|
|Route 31|
|--------|
Pokémon found on Route 31:
#010 Caterpie
#011 Metapod
#013 Weedle
#014 Kakuna
#016 Pidgey
#042 Zubat
#06? Poliwag
#069 Bellsprout
#163 Hoot Hoot
#165 Ledyba (Silver only, morning only)
#167 Spinarak (Gold only, night only)
Items found on Route 31:
Antidote
Bitter Berry
Pokeball
Potion
You'll find the entrance to Dark Cave at the beginning of Route 31, as well as a
Potion. The Dark Cave won't really do you any good till you have the HM Flash,
so wait it out and grab that Potion to the left of it. When you see two
trainers, head south to where the item is and fight!
Bug Catcher Wade
Level 2 Caterpie (20 EXP)
Level 2 Caterpie (20 EXP)
Level 3 Weedle (33 EXP)
Level 2 Caterpie (20 EXP)
You get: 32G
I'm not really clear on how these Bug Catchers are catching the bugs. Are they
using their nets, or Pokeballs? Grab the Pokeball below the trainer, head back
up to grab the Bitter Berry from the tree, then head west, through the house-ish
entry, and into Violet City.
|-----------|
|Violet City|
|-----------|
Pokémon found in Violet City:
#95 Onix
Items found in Violet City:
PRZCure Berry
TM 31
Heal your Pokémon. There is a GYM there in Violet City that uses Flying type
Pokémon. Don't go there just yet, head over far north to a tower called the
Sprout Tower. The experience you get up there will be very useful for the gym.
(c08)
-----------------------------------------------
@@ -0,0 +1,150 @@
---- 8 > Sprout Tower and Violet City Gym ----
-----------------------------------------------
Video Playthrough Link (Introduction through Violet City Gym):
https://www.youtube.com/watch?v=dqJtkx8DcE0
|------------|
|Sprout Tower|
|------------|
Pokémon found in Sprout Tower:
#019 Rattata
#069 Bellsprout
#092 Gastly
Items found in Sprout Tower:
Escape Rope
HM05
Parlyz Heal
Potion
X Accuracy
This tower is called Sprout Tower, and it's obviously named that because the
sages inside mostly use Bellsprouts. I wonder what rituals the sages go through
with those Bellsprouts. It should give you plenty of experience. Head up the
stairs to Floor 2. Continue up the path and you'll see Sage Nico, and he'll
challenge you to a battle.
Sage Nico
Level 3 Bellsprout (54 EXP)
Level 3 Bellsprout (54 EXP)
Level 3 Bellsprout (54 EXP)
You get: 96G
Continue along the path and down the stairs. Grab the item below you. It is
Parlyz Heal. Head back up and left along the path to fight Sage Chow.
Sage Chow
Level 3 Bellsprout (54 EXP)
Level 3 Bellsprout (54 EXP)
Level 3 Bellsprout (54 EXP)
You get: 96G
Go up the stairs and along the path till you see YET another sage, after you
grab that X Defense above you.
Sage Edmond
Level 3 Bellsprout (54 EXP)
Level 3 Bellsprout (54 EXP)
Level 3 Bellsprout (54 EXP)
You get: 96G
Climb up the stairs, grab the item near you (a Potion), then fight every sage up
in turn.
Sage Jin
Level 6 Bellsprout (108 EXP)
You get: 192G
Next up is another sage. Use your starter for backing.
Sage Neal
Level 6 Bellsprout (108 EXP)
You get: 192G
Move up to the third sage on the floor and fight him.
Sage Troy
Level 7 Bellsprout (126 EXP)
Level 7 Hoothoot (106 EXP)
You get: 224G
When you move up, you'll see some sort of shaking up above you. Your rival is
there, after beating the main sage of the tower. He has been given the HM Flash
but the sage criticizes him for treating his Pokémon too harshly and he gets
enraged. He escapes with the Escape Rope. Go up to the item to the right of the
monk. In reality, it is a...
Level 3 Rattata (24 EXP)
Now move up to take on the leader of the sages itself. The final battle of the
Sprout Tower challenge commences.
Sage Li
Level 7 Bellsprout (126 EXP)
Level 10 Hoothoot (122 EXP)
Level 7 Bellsprout (126 EXP)
You get: 320G
For beating the main guy of the tower, you get your reward, the Flash HM (HM05).
Flash is a flashlight, it's not something else. Get your mind out of the gutter.
You'll need the 1st badge to use it, however. Go back to the Rattata you faced
before, and now it's an Escape Rope. You may use that to get out of the tower or
by climbing down it. Get out of the tower and go back to the Pokémon Center of
Violet City.
|-----------|
|Violet City|
|-----------|
Well, you should be around an hour into the game now. Now, you'll want to head
northwest in the city to the Violet City Gym. After all that training, things
are gonna look awfully ridiculously easy in here, so you'll know you're doing
the right thing. And best of all, you didn't even need to run on a treadmill at
the school gym. Head up to fight your trainer battles first.
Bird Keeper Abe
Level 9 Spearow (111 EXP)
You get: 216G
Head up the path to the second trainer.
Bird Keeper Rod
Level 7 Pidgey (82 EXP)
Level 7 Pidgey (82 EXP)
You get: 168G
Go back to the Pokémon Center to heal, then you're ready to battle, so head up
the zig-zag pathway to Falkner. Falkner is ready to show the magnificent power
of bird Pokémon. I'm not convinced. If you chose Cyndaquil or Totodile, this
should be pretty easy. If you have Chikorita, I would suggest evolving it first
or using a non-grass Pokémon (like a Rattata) take this gym.
---
GYM LEADER FALKNER
Level 7 Pidgey (82 EXP)
Level 9 Pidgeotto (217 EXP)
You get: Zephyr Badge, $900, & TM31 (Mud Slap)
---
Strategy VERSUS Pidgey: Use your highest-powered move from your starter Pokémon,
or consider using Rattata's quick attacks if you have Chikorita and haven't
evolved it yet.
Strategy VERSUS Pidgeotto: Use your highest-powered move from your starter
Pokémon, or consider using Rattata's quick attacks if you have Chikorita and
haven't evolved it yet. Pidgeotto will have a Gust attack that does a bit more
damage.
The Zephyr Badge gives your Pokémon a slight attack boost. That's cool and you
have one badge now.
==============================================================================
==============================================================================
==============================================================================
(c09)
------------------------------------------
@@ -0,0 +1,312 @@
---- 9 > Ruins of Alph and Union Cave ----
------------------------------------------
Video Playthrough Link (Violet City, Ruins Alph, Route 32, Union Cave, to Azalea
Town): https://www.youtube.com/watch?v=7v-n0Z0CQsg
Video Playthrough Link (Slowpoke Well, Azalea Town Gym, Ilex Forest, Goldenrod
City Gym): https://www.youtube.com/watch?v=LSEriFoAxC0
|-----------|
|Violet City|
|-----------|
Right after you leave the gym, you should receive a phone call from Professor
Elm. He tells you to pick up something important at the Pokémon Center from his
aide. Apparently you are now doing volunteer work for the Professor for no extra
pay. When you get to the Pokémon Center, you will see Professor Elm's aide, the
guy by the counter. He will give you a Pokémon egg. The egg will hatch in about
1,000 steps. Leave the Violet City Gym and go to the Pokémon Center. It is
Togepi in egg form. There is one way to not keep Togepi on your team while it's
in egg form. Just put it in one of the Pokémon boxes (while it's an egg form)
and keep it there.
Heal, buy at least 10 Pokeballs, then head west from the Violet City Gym.
Continue on the path north and then west until you enter Route 36.
|--------|
|Route 36|
|--------|
Talking to the two people on the route, you'll find out that a supposed tree is
blocking the way. Ignore that for now and head south past the house to the Ruins
of Alph.
|-------------|
|Ruins of Alph|
|-------------|
Pokémon found in or outside the Ruins of Alph:
#177 Natu
#194 Wooper (Surfing)
#201 Unown
#235 Smeargle
Item found in the Ruins of Alph:
Unown Pokedex
After you go through the route-changing station, enter the first cave on the
right that you see. Talk to the two people inside, go up to the left panel, and
then solve the Kabuto puzzle. The corner pieces should be obvious, so start from
there and work to the middle. It will look circular, but the eyes point
out...remember that. You are then dropped to the next floor, and you will find
yourself in another cave you could enter on the left side.
This will allow you to catch a new Pokémon, #201 Unown, in its 28 forms. The
forms include all 26 letters of the English alphabet, plus two punctuation marks
(? and !). The lower passageway eventually leads up to a ladder that you can
climb up. But you might have fun catching some of the Unown alphabet while
you're here.
Thanks to Thard_Verad for correcting the typing for Unown, who also comments:
"It's also a terrible addition to one's party. Bad stats and can only learn
Hidden Power."
If you catch at least three different Unown letters in the Ruins of Alph when
you exit the cave at the bottom with the ladder, a professor assistant will
greet you and give you an Unown Pokedex add-on.
For when you later have access to radio stations, Thard_Verad comments, "There's
a mysterious radio signal in the Ruins of Alph. It's on frequency 213.5 if you
want to listen."
There's another puzzle you can do here once you have HM03 Surf. When you do,
surf across the narrow lake south of where you were (where you can catch a
Wooper, by the way) to the other side. Enter that part of the Ruins of Alph and
you can solve an Aerodactyl puzzle. It'll be easier to the corner and side
pieces first because it will come with that white background.
After exiting the cave, go south, and then east. You will come up to another
route changing house. Welcome to Route 32.
|--------|
|Route 32|
|--------|
Pokémon found in Route 32:
#019 Rattata
#023 Ekans
#041 Zubat
#069 Bellsprout
#179 Mareep
#187 Hoppip
#194 Wooper
Items found in Route 32:
Great Ball
Miracle Seed
Poison Barb
Potion
Old Rod
Repel
TM05
After exiting the house, go a little east and then up into Violet City to grab
that PRZCure Berry, then head back south onto Route 32. Talk to the first guy
you see and you will get Miracle Seed. Grass moves will then be increased in
power by 20%. Keep heading south and getting easy experience points fighting
silly Pokémon like Ekans.
Depending on what starter you have, you may want to catch both a Bellsprout and
a Wooper (starting with Cyndaquil), just Wooper (starter being Chikorita), or
just Bellsprout (starter being Totodile), for the HMs Cut and Surf later on. Use
quick attack from a non-starter Pokémon to a yellow or HP range, then catch
those Pokémon. Wooper will be a bit harder to find than Bellsprout. Then keep
going down till you see the first trainer.
Youngster Albert
Level 6 Rattata (72 EXP)
Level 8 Zubat (91 EXP)
You get: 128G
Now there's a fork. Take the left path and get the Repel, then head back to the
right to talk to the girl who will fight you while being on the phone. So
talkie.
Picnicker Liz
Level 9 Nidoran (F) (110 EXP)
You get: 180G
Talk to her again and you'll even get her phone number and vice versa. Anyway,
head down and take the grassy eastern route along the lake. Go down and talk to
the first fisherman you reach for a nice battle. Easy battle versus Magikarps
who can barely splash, except for the Level 15 one.
Fisher Justin
Level 5 Magikarp (21 EXP)
Level 5 Magikarp (21 EXP)
Level 15 Magikarp (63 EXP)
Level 5 Magikarp (21 EXP)
You get: 200G
Talk to the fisherman to the left of Justin and you'll have an actual fight.
Fisher Henry
Level 8 Poliwag (128 EXP)
Level 8 Poliwag (132 EXP)
You get: 320G
Keep going south till that next fisherman spots you.
Fisher Ralph
Level 10 Goldeen (237 EXP)
You get: 400G
Exhausted with all the battles now? Take your trip back north to that PRZCure
Berry you saw earlier, after your Mom gives you that call. She apparently buys
something with your money. You can check that at the Pokémon Center.
Thard_Verad adds, "Mom will buy something once your savings reach certain
thresholds. The items are pretty forgettable, but the dolls (AFAIK) are unique
to her shopping trips."
Anyway, heal, withdraw the item (a Super Potion), then come back down the same
route, but take the LEFT path instead and immediately you see a trainer that
wants to fight you. Oh no!
Camper Roland
Level 9 Nidoran (M) (115 EXP)
You get: 180G
Go south then back up to grab the Great Ball, then head south once more to fight
the trainer below you.
Youngster Gordon
Level 10 Wooper (111 EXP)
You get: 160G
Go back up the right/fishing route to talk to Fisher Ralph to exchange phone
numbers. Keep going down the route and someone wants a million dollars for a
Slowpoke tail. Go up to the girl and she'll give you a Poison Barb. Now heal at
that Pokémon Center in the middle of nowhere. Some rando fisher will give you an
Old Rod. Keep heading south and there's yet another trainer. Finally a trainer
that knows you have one badge!
Bird Keeper Peter
Level 6 Pidgey (70 EXP)
Level 6 Pidgey (70 EXP)
Level 8 Spearow (99 EXP)
To get: 192G
Well, that was easy. Now that you're at the Union Cave entrance, you should
enter Union Cave.
|----------|
|Union Cave|
|----------|
Pokémon in Union Cave:
#019 Rattata
#041 Zubat
#074 Geodude
Items in Union Cave:
Potion
X Attack
TM39 Swift
Now that you're finished with Route 32, it's time to bring the fighting into
Union Cave. There isn't that much to do in Union Cave, however.
Your lineup (if you are balancing out a starter and Rattata) should now be:
Level 3 Pidgey
Level 4 Wooper
Level 5 Mystery Egg
Level 7 Bellsprout
Level 15 Rattata
Level 15 Starter
Keep heading along the path westward and you'll see a lot of silly, silly
Geodudes. Take the Great Ball, and go south to fight your first trainer battle
in Union Cave.
Hiker Daniel
Level 11 Onix (252 EXP)
You get: 352G
Head east to the next trainer, who will ask if you are truly ready for Azalea's
gym. Show that you are!
Hiker Russell
Level 4 Geodude (82 EXP)
Level 6 Geodude (109 EXP)
Level 8 Geodude (147 EXP)
You get: 256G
Firebreather Bill
Level 6 Koffing (145 EXP)
Level 6 Koffing (145 EXP)
You get: 288G
Grab the X Attack below Bill, then head back up to Bill and go left. Don't miss
the item to the far left. It's a Potion. Go south and then up the ledge.
PoKeManiac Larry
Level 10 Slowpoke (211 EXP)
You get: 600G
Now go up and down the ladder. Head left to get TM39 Swift, then back east past
the ledge you went down, to get the item X Defend. Head back thus the way you
came, head down the ledge, and grab the Awakening Item to the southeast. Then go
east to fight the last trainer.
Firebreather Ray
Level 9 Vulpix (121 EXP)
You get: 432G
Afterwards, you can go right and down and out the door to Route 33.
Note that when you have obtained HMs Strength and Surf you should return back to
Union Cave and explore the lower levels. If you do you will find a Lapras, who
only appears on Friday, plus the entrance to more of the Ruins of Alph.
|--------|
|Route 33|
|--------|
Pokémon found in Route 33:
#019 Rattata
#021 Spearow
#023 Ekans
#041 Zubat
#187 Hoppip
Items found in Route 33:
PSNCure Berry
After exiting the Union Cave, go a little down to grab the PSNCure Berry from
the tree, then head west. You'll meet just one trainer in the way: Hiker
Anthony. He says he's got plenty of energy left to battle you.
Hiker Anthony
Level 11 Geodude (202 EXP)
Level 11 Machop (207 EXP)
You get: 352G
Now that he is defeated, head west into Azalea Town.
If you chose to switch leveling up your starter and Rattata, your lineup should
be around:
Level 17 Starter
Level 16 Rattata
Level 8 Bellsprout
Level 5 Mystery Egg
Level 4 Wooper
Level 3 Pidgey
==============================================================================
==============================================================================
==============================================================================
(c10)
----------------------------------------------
@@ -0,0 +1,155 @@
---- 10 > Slowpoke Well & Azalea Town Gym ----
----------------------------------------------
Video Playthrough Link (Slowpoke Well, Azalea Town Gym, Ilex Forest, Goldenrod
City Gym): https://www.youtube.com/watch?v=LSEriFoAxC0
|-----------|
|Azalea Town|
|-----------|
Items found in Azalea Town:
Lure Ball
TM49
Wht Apricorn
When you enter the town, you should notice that one Team Rocket member is
guarding a well-like structure. Ignore this for now, but remember where it is.
At about this time, you might get a call from Liz the Picnicker and she tells
you that Moo Moo Milk can be bought at Moo Moo Farm for good health and beauty.
Anyway, keep rolling up to the northernmost house in town by heading west then
north in the city. He is Kurt and he informs you about the disbanding and return
of Team Rocket. Kurt goes to "give them a lesson in pain". Anyway, go back to
the well-like structure after you heal at the Pokémon Center and you'll see that
the Team Rocket guard has gone, or more like, demolished by Kurt. Now enter the
Slowpoke Well!
|-------------|
|Slowpoke Well|
|-------------|
You'll notice Kurt took a tumble down the well, so it's up to you now. Head up
to start the war on Team Rocket and its members, especially the guy Kurt had a
shouting match with.
Rocket Grunt (M)
Level 9 Rattata (109 EXP)
Level 9 Rattata (109 EXP)
You get: 360G
Talk to him again and you'll learn that they're chopping off Slowpoke tails for
cash. Move on up the stairs for Battle #2.
Rocket Grunt (F)
Level 9 Zubat (103 EXP)
Level 11 Ekans (145 EXP)
You get: 440G
Move left to grab the super potion to the left of her, then head down the stairs
into another battle.
Rocket Grunt (M)
Level 7 Rattata (85 EXP)
Level 9 Zubat (103 EXP)
Level 9 Zubat (103 EXP)
You get: 360G
Talk to the Slowpoke above you. It had its tail cut off. Poor thing, and there's
mail on it? Anyway, move left to face the fourth Team Rocket member.
Rocket Grunt (M)
Level 14 Koffing (342 EXP)
You get: 560G
He, the leader of this band of Team Rocket members, tells you that they were
working underground for the past three years. Looks true, doesn't it? Team
Rocket leaves and Kurt talks to you. He returns you to his house in Azalea Town.
|-----------|
|Azalea Town|
|-----------|
Items found in Azalea Town:
Lure Ball
TM49
Wht Apricorn
Talk to Kurt again and he'll give you a Lure Ball for free. Whee! Go right above
his house and grab the Wht Apricorn, then heal at the Pokémon Center.
Thard_Verad comments, "Kurt only accepts ONE Apricorn at a time. Unfortunately
you can't give him multiples of the same kind."
After you heal, head to the Azalea Town Gym, and the home of the Bug types. By
now, you're 2 to 2 1/2 hours into the game. Anyway, now's the time to enter the
gym and face Bugsy and her apprentices. Rattata will be a MAJOR help here. Go up
and Amy (the left girl) and May (the right girl) will challenge you. Remember,
Quilava is extra-effective here if you have him. Otherwise, Rattata will still
stun through this gym.
Twins Amy & May
Level 10 Spinarak (115 EXP)
Level 10 Ledyba (115 EXP)
You get: 200G
Head up the left side to meet your next opponent. Someone knows you saved the
Slowpokes.
Bug Catcher Josh
Level 13 Paras (195 EXP)
You get: 208G
Continue around the circular center to the top center person. Good luck!
Bug Catcher Benny
Level 7 Weedle (78 EXP)
Level 9 Kakuna (136 EXP)
Level 12 Beedrill (408 EXP)
You get: 192G
Continue around the circle, clockwise, to the final one of the five trainers
lying around the gym leader's circle. Time to beat Al!
Bug Catcher Al
Level 12 Caterpie (135 EXP)
Level 12 Weedle (133 EXP)
You get: 200G
Depending on whether Rattata and have your starter have the HP, you can either
go back and heal or just go straight to fighting Gym Leader Bugsy. In any case,
put Rattata on the front line, then your starter as backup. Yay for Bugsy. She
may have the Bug Pokémon, but you have the counters!
---
GYM LEADER BUGSY
Level 14 Metapod (215 EXP)
Level 14 Kakuna (213 EXP)
Level 16 Scyther (640 EXP)
You get: Hive Badge, 1,600G, & TM49 (Fury Cutter)
---
Strategy VERSUS Metapod: Use the highest-powered attack on your highest level
Pokémon. Metapod's easy. If you have Cyndaquil, use Ember, as fire type moves
are super effective against bug types.
Strategy VERSUS Kakuna: Use the highest-powered attack on your highest level
Pokémon. But watch out for Kakuna's poison sting. You might want to use an
Antidote. If you have Cyndaquil, use Ember, as fire type moves are super
effective against bug types.
Strategy VERSUS Scyther: Use the highest-powered attack on your highest level
Pokémon. If you have Cyndaquil, use Ember, as fire type moves are super
effective against bug types. Scyther may take multiple hits to take down.
So now, you have the second badge which lets all Pokémon up to Level 30 obey you
regardless of whatever, and you can use the HM01 Cut. You also get TM49, Fury
Cutter. This lets Pokémon have stronger attacks over time. Whee.
==============================================================================
==============================================================================
==============================================================================
(c11)
-------------------------------------------------
@@ -0,0 +1,395 @@
---- 11 > Ilex Forest and Goldenrod City Gym ----
-------------------------------------------------
Video Playthrough Link (Slowpoke Well, Azalea Town Gym, Ilex Forest, Goldenrod
City Gym): https://www.youtube.com/watch?v=LSEriFoAxC0
|-----------|
|Azalea Town|
|-----------|
Now leave the gym and heal your Pokémon at the Pokémon Center. Go west and try
to exit the town and you'll run into your rival. Yay for you! He doesn't believe
you beat Team Rocket, but let the battle begin.
|--------------------------|
|Rival |
|Level 12 Gastly (240 EXP) |
|Level 16 Starter (489 EXP)|
|Level 14 Zubat (162 EXP) |
|You get: 960G |
|--------------------------|
Pokémon VERSUS Gastly: Use the highest-powered attack on your highest level
Pokémon that is NOT a normal type move, as ghost types do not get damaged by
normal moves. You can't use normal moves with Gastly, but you can use grass,
fire, or water.
Pokémon VERSUS Starter: Use the highest-powered attack that aren't the type of
your starter.
Pokémon VERSUS Zubat: Use the highest-powered attack on your highest level
Pokémon.
Well, at least he admits, "I only lost because my Pokémon were weak." He talks
tougher than his Pokémon really are. Uncle Ralph calls again about taking care
of Pokémon and taking out a Zubat. Anyway, heal again, then continue going west
into Ilex Forest!
Defeat your rival and head the way you were going, west. You'll enter Ilex
Forest.
|-----------|
|Ilex Forest|
|-----------|
Pokémon found in Ilex Forest:
#010 Caterpie
#011 Metapod
#013 Weedle
#041 Zubat
#043 Oddish
#046 Paras
Items found in Ilex Forest:
HM01 (Cut)
Revive
TM02 (Headbutt)
TM12 (Sweet Scent)
Keep heading north up the path, then follow the path eastward. Talk to the first
guy you see. He is standing next to a bush that you can cut. He tells you that
his boss's Farfetch'd got away. He's sooo irresponsible. Keep following the path
and you'll find that scary Farfetch'd. XD
Anyway, if you talk to the Farfetch'd, it'll run away. Far, far away. Keep on
following it and talking to it. Eventually, it'll turn back the opposite
direction so make sure you're "pushing" it back the way you came. Grab the
Revive you see while you're at it. It will then lead you to the man and his
boss.
Anyway, talk to the boss and he will give you HM01 Cut! You'll want to equip Cut
on Bellsprout right away, and that is in the fourth section of your backpack.
Now use HM01 Cut on that bush to keep moving up the path. A new feature is that
you can just go up to the bush, press A, and it will get cut down if you have a
Pokémon with Cut in your lineup. You can do the same later with other HMs like
Strength (with boulders), Surf (with water), and Whirlpool (with, well,
whirlpools).
Now you enter the Ilex Forest Shrine, and the Ilex Forest Maze. Keep heading
along the path and grab the X Attack you see, and then the Antidote you spot
later on the path going east. To the northeast is an Ether, but go right first.
Talk to the trainer and you'll get TM02 Headbutt, which allows you to headbutt
other people. Just kidding. It's for your Pokémon and I am a serious person.
Headbutt lets you shake the little trees that are everywhere in the game,
sometimes revealing sleeping Pokémon. Then, head back left from there and talk
to that other trainer for a battle!
Bug Catcher Wayne
Level 8 Ledyba (122 EXP)
Level 10 Paras (150 EXP)
You get: 160G
Keep going along the path and into the building to the north. Talk to the lady
behind the counter and you'll get TM12 Sweet Scent. Walk through and you'll get
on Route 34!
|--------|
|Route 34|
|--------|
Pokémon found in Route 34:
#019 Rattata
#063 Abra
#096 Drowzee
#132 Ditto
Items found in Route 34:
Soft Sand
Welcome to Route 34. Here, you'll fight even more trainers. Head right up and
you'll enter a match right away.
Youngster Samuel
Level 7 Rattata (85 EXP)
Level 10 Sandshrew (198 EXP)
Level 8 Spearow (99 EXP)
Level 8 Spearow (99 EXP)
You get: 128G
Keep heading up the path to face the guy who sent his Pokémon to the Daycare
Center!
PoKeFan Brandon
Level 13 Snubbull (175 EXP)
You get: 1040G
Continue along the path to face that Youngster, who says he's the best in his
class at Pokémon.
Youngster Ian
Level 10 Mankey (157 EXP)
Level 12 Diglett (207 EXP)
You get: 192G
Youngster Ian will want to trade phone numbers with you. Now head down through
some of the wild grass to battle Picnicker Gina.
Picnicker Gina
Level 9 Hoppip (142 EXP)
Level 9 Hoppip (142 EXP)
Level 12 Bulbasaur (163 EXP)
You get: 240G
Head up from Youngster Ian and you'll find this building to the right. Enter it
and talk. Thard_Verad notes correctly that only in the Pokémon Crystal Version
does the Daycare have a free egg and that "there's a PC in the corner of the
Daycare" if you need it.
Inside the Daycare/Breeding Center with the old man and woman, you can talk to
the man and he will take one of your Pokémon to level up. Talking to the woman
will do the same thing. It will cost 100G to take them out, and 100G per level
up, but their stats won't be as good as if you trained them yourself, obviously.
To breed Pokémon you must give the old man a Male Pokémon and give the old woman
a Female Pokémon (or vice-versa) that have the same little icon or sprite. Or
they must be compatible at breeding.
Then exit the building and go walk around for a little while. Come back when you
see the man standing outside with your two Pokémon. Talk to him and answer yes,
he will give you an Egg. If you put in Pokémon of different #'s, for example
Pidgey and Hoothoot, the resulting egg will be the same species as the female
Pokémon and with an Attack that the Male had. Just think of the strange
combinations.
Head back out as you can't really do much here yet. If it's night time, you can
fight the officers. Thanks to Thard_Verad for pointing out that the Police
Officers will only battle you at night.
Officer Keith
Level 17 Growlithe (331 EXP)
You get: 680G
Move up to take on the final trainer on the route.
Camper Todd
Level 14 Psyduck (240 EXP)
You get: 280G
Great! Now, talk to the same guy again and you'll exchange phone numbers. He's
great. He'll call you later about his awesome looking Psyduck and that he beat
some tough Rattata.
Then, let's go into Goldenrod City, the LARGEST city in the game, or something
like that.
If you focused on just leveling up your starter, your lineup might look like
this:
Level 25 Starter Evolution
Level 6 Bellsprout
Level 6 Wooper
Level 5 Rattata
Level 5 Unown Egg
If you balanced leveling up your Starter and Raticate:
Level 22 Starter Evolution
Level 20 Raticate
Level 8 Bellsprout
Level 5 Mystery Egg
Level 5 Pidgey
Level 4 Wooper
|--------------|
|Goldenrod City|
|--------------|
Pokémon found in Goldenrod City:
#66 Machop (Through Trading)
#133 Eevee (Get from Bill)
Pokémon/Items in Goldenrod's Game Corner: (Coins)
Abra 200 TM 25 5500
Ekans/Sandshrew 700 TM 14 5500
Dratini 2100 TM 38 5500
Items found in Goldenrod City:
Bicycle
Coin Case
HP Up
Radio Card
TM 21
TM 27
TM 45
Squirtwater
Goldenrod City has got to be the biggest city in the game. There's lots to do in
the city. Heal at the Pokémon Center, then cross the road to the right to the
Goldenrod City Department Store. There's a beautiful assortment of items on
every floor:
Level 2: Potions, Heals, Great Balls, Escape Ropes, Repels
Level 3: X Items (temporary stat boosts)
Level 4: Stat increase items
Level 5: TM02, TM33, TM41, TM48 (includes Drownzee-Machop trade and Sunday
giveaway of TMs)
Level 6: Vending Machine heal items
Thard_Verad adds, "Super Potions are a scam. As soon as you reach Goldenrod, you
can go to the top of the Dept. Store and replace all your Potions with Fresh
Water (same price, 2.5x stronger) and Super Potions with Lemonade (half price,
60% stronger) If only you could buy in bulk...."
Additionally, Thard_Verad comments, "After you get TM08 Rock Smash, it will be
added to the Dept. Store's lineup."
Next, you'll want to head to Bill's house. Go through the path just above the
Pokémon Center and it should be the first house you see. Talk to the girl that's
closer to the door and you'll get Bill's phone number. Calling it will tell you
how many Pokémon you have.
After that, we'll go down under the city for some street fighting. Go to the
house below Bill's, and it turns out to be the entrance to the underground. Go
downstairs. Go up and you'll face a Game Corner cheater.
Super Nerd Eric
Level 11 Grimer (211 EXP)
Level 11 Grimer (211 EXP)
You get: 352G
Head up to face PoKeManiac Isaac.
PoKeManiac Isaac
Level 12 Lickitung (325 EXP)
You get: 720
Grab the Coin Case above him. You'll need it for the Game Corner because
apparently, nothing else can carry their special coins. Pass by the three
strange shops, including a Pokémon Salon, and stop in front of the guy that's
there.
Super Nerd Teru
Level 7 Magnemite (133 EXP)
Level 11 Voltorb (241 EXP)
Level 7 Magnemite (133 EXP)
Level 9 Magnemite (171 EXP)
You get: 288G
Head up to face another trainer.
PoKeManiac Donald
Level 10 Slowpoke (211 EXP)
Level 10 Slowpoke (211 EXP)
You get: 600G
Head to the right and you'll see the door is locked, so come back and go up the
stairs above Donald. Come back out and go to the Pokémon Center to heal.
Now head out of the gym and we'll hit the east side of town (below the train
tracks) for a Bike. Keep heading down that path till you hit a dead end and
there's a house. It's the Bike Shop. Talk to the manager inside and he tells you
that you can borrow a bike if you advertise for him. You can access the bike
from your backpack. This makes no sense - is it a foldable bike? Eh, whatever.
It's in the 3rd section of your backpack, so start using it to cruise through
areas. Once you get out, go to your bicycle and press Sel, which means the item
will be equipped for use on the Select Button. Now you can simply just press
Select and you'll be on your bike.
To the west is a train station. The train conductor stands there and says no
train is coming, but they're still paying him to be there. Now, you'll want to
visit the Radio Tower in the western side of town. It is a black building just
south of the train tracks. Talk to the middle man for possible ID matches, then
talk to the woman on the right for the Radio Quiz.
Question 1: Can the Town Map be displayed on a Pokegear? (YES)
Question 2: Can Nidorina be female only? (YES)
Question 3: Does Kurt, the Pokeball crafts-man, use Aprikorn? (NO)
Question 4: Maikarp won't learn any TM move? (YES)
Question 5: Prof. Oak's Pokémon Talk is a very popular program? Is Marie the
co-host of the show? (NO, it's Mary)
Now you'll get the Radio Card for your Pokegear, and can listen to the radio
anytime. There are three radio channels, including Pokémon Music 7.5 with DJ Ben
and the 8.5 Lucky Channel. Thard_Verad notes correctly that the Buena meeting
and Blue Card are only available in the Pokémon Crystal Version. Head back out
now.
Now, let's head for the gym for that 3rd badge, shall we? We'll go back to the
PoKeMart for supplies later. The gym is in the northeast part of town. This
gym's trainer all use Normal Pokémon.
Enter the gym and head up to face Beauty Victoria.
Beauty Victoria
Level 9 Sentret (109 EXP)
Level 13 Sentret (157 EXP)
Level 17 Sentret (207 EXP)
You get: 1496G
Easy battle. Go around her and to the right side of the gym to face the other
Beauty now.
Beauty Samantha
Level 16 Meowth (235 EXP)
Level 16 Meowth (235 EXP)
You get: 1408G
Start heading left along the path towards the center, for another battle.
Lass Carrie
Level 18 Snubbull (243 EXP)
You get: 432G
Go up to fight the last trainer before the gym leader, Lass Bridget. She loves
to use Jigglypuff and has 3 of them, so expect to get hit by sleep spells.
Lass Bridget
Level 15 Jigglypuff (243 EXP)
Level 15 Jigglypuff (243 EXP)
Level 15 Jigglypuff (243 EXP)
You get: 360G
Now, me being me, I'd just go straight into the battle against the gym leader
without items or healing, but I'd recommend going back to the Pokémon Center to
heal before the battle.
---
GYM LEADER WHITNEY
Level 18 Clefairy (261 EXP)
Level 20 Miltank (856 EXP)
You get: Plain Badge, 2,000G, & TM45 (Attract)
---
Strategy VERSUS Clefairy: Use your highest powered attack from your starter.
Strategy VERSUS Miltank: The most troublesome thing about facing a Miltank is
that it heals with milk and it has an attack that makes your Pokémon get woozy.
Your Pokémon are then unable to attack the Miltank for a few turns and Miltank
just slams you with Stomps and Rollouts. Use your highest powered attacks from
your highest level Pokémon. Flying type moves are also super effective against
Miltank, and rock Pokémon tend to absorb moves from Miltank better than the
rest.
Whitney won't give you the Plain Badge at first because she will be upset that
you've been "so serious". Anyway, she refuses to give you your badge. The
trainer below you will talk to you. Then talk to Whitney again. She will now
give you the Plain Badge. It will let your Pokémon use Strength outside of
battle and it boosts your Pokémon's Speed. She'll also give you TM45 Attract.
==============================================================================
==============================================================================
==============================================================================
(c12)
-------------------------------------------------
@@ -0,0 +1,292 @@
---- 12 > Bug Catching Contest and Sudowoodo ----
-------------------------------------------------
Video Playthrough Link (Goldenrod City, Routes 35-37, to Ecruteak City):
https://www.youtube.com/watch?v=x0E8PnSbA5o
Head to the house just to the right of the gym. Talk to the girl you talked to
before and it turns out her sister is right in the same house. Talk to her
sister and you'll get the Squirtbottle. You'll need that for a later part of the
story. No, you won't use it on a Squirtle. What would that even do, anyway?
Then, we're ready to head into Route 35 after Liz's offer for a rematch on Route
32. Ride your bike north through the building. It's cool how you can just ride a
bike through, but isn't that a safety hazard?
In the route changing house towards Route 35, if you have a Pokémon lineup spot
open, a man can give you a Spearow named Kenya. You're supposed to get the mail
that Spearow is carrying to Route 31. Or you can just keep the Spearow with the
mail and not do anything with the mail. It's your life. You do you.
Thard_Verad adds, "You can keep Kenya the Spearow and still complete the side
quest. Go catch a spare Spearow and move the Mail from Kenya to your own bird."
|------------------------|
|Route 35 & National Park|
|------------------------|
Pokémon found in Route 35:
#016 Pidgey
#029 Nidoran (F)
#032 Nidoran (M)
#063 Abra
#096 Drowzee
#132 Ditto
#163 Hoothoot
#193 Yanma
Items found in Route 35:
Mystery Berry
TM04 Rollout
The short Route 35 is home to a few trainers to fight along with. This is a
great place to train up your Pokémon. Thard_Verad adds, "If you farm up Mystery
Berries, they can supplement any Ethers you bring to the Elite Four."
Picnicker Kim
Level 15 Vulpix (202 EXP)
You get: 300G
Next up is Camper Elliot. He thinks he's amazing because he has a girlfriend.
Camper Elliot
Level 13 Sandshrew (258 EXP)
Level 15 Marill (186 EXP)
You get: 300G
Then you talk to Picnicker Brooke and she's less enthused that her boyfriend's
Pokémon are so bad. "My boyfriend's weak, so I can't rely on him." XD
Picnicker Brooke
Level 16 Pikachu (280 EXP)
You get: 320G
Move up to the next trainer!
Camper Ivan
Level 10 Diglett (172 EXP)
Level 10 Zubat (115 EXP)
Level 14 Diglett (243 EXP)
You get: 280G
Keep moving up the short Route 35. Irwin will have ridiculously easy Voltorbs to
beat.
Juggler Irwin
Level 2 Voltorb (43 EXP)
Level 6 Voltorb (132 EXP)
Level 10 Voltorb (220 EXP)
Level 14 Voltorb (309 EXP)
You get: 560G
Now go up to the trainer at the left.
Firebreather Walt
Level 11 Magmar (393 EXP)
Level 13 Magmar (465 EXP)
You get: 624G
There's also an officer just outside the checking station. Show him that you
mean business.
Officer Dirk
Level 14 Growlithe (273 EXP)
Level 14 Growlithe (273 EXP)
You get: 560G
Head up through the checking station on Route 35 and you'll be in National Park.
|-------------|
|National Park|
|-------------|
Pokémon found in National Park:
#010 Caterpie
#011 Metapod
#012 Butterfree
#013 Weedle
#014 Kakuna
#015 Beedrill
#016 Pidgey
#046 Paras
#048 Venonat
#123 Scyther
#127 Pinsir
#163 Hoot Hoot
#191 Sunkern
Items found in National Park:
Berry (Consolation Prize)
Everstone (2nd Prize)
Gold Berry (3rd Prize)
Parlyz Heal
Quick Claw
Sun Stone (1st Prize)
TM28
There are some rare Pokémon here that makes the detour worthwhile. Scyther and
Pinsir are rare, and Nincada and Scyther can learn False Swipe. You'll also have
your version's bug type (Caterpie or Weedle). However these Pokémon are only
found during the Bug Catching Contest, and the contest only happens on Tuesdays,
Thursdays, and Saturdays.
Pokémon at the Bug Catching Contest:
Caterpie/Weedle
Nincada
Scyther
Pinsir
Volbeat
Wurmple
You can decide which Pokémon that you use in the contest, but you must place it
in the first spot in your Pokémon lineup. The Pokémon here are generally around
Levels 28-31. You will get 20 Sport Balls and 20 minutes. In order to win the
contest, you will have to catch a rare Pokémon. Scyther and Pinsir are worth the
most points. If you catch a Scyther at full HP, it may be worth around 342
points. If you win the contest, you can win a Shiny Stone!
Thard_Verad adds: "During the Bug Catching Contest, your score also takes the
bug's HP into consideration. The best plan of attack is
to not attack. Bring a Pokemon with Sleep Powder and try to catch Scyther/Pinsir
while it's asleep but at full health."
You can stay for the Bug-Catching Contest if you'd like and it is the right day.
Otherwise or afterwards, talk to the lady on the bench for Quick Claw, then head
up past the first part of the National Park and fight Pokefan Beverly.
PoKeFan Beverly
Level 14 Snubbull (189 EXP)
You get: 1120G
Head northeast to fight Schoolboy Jack!
Schoolboy Jack
Level 12 Oddish (199 EXP)
Level 15 Voltorb (330 EXP)
You get: 480G
Here in the park you'll also notice that there is high grass. This gives you
a bigger chance of encountering wild Pokémon. Head back to the left of
Beverly, then go northwest to fight the Lass.
Lass Krise
Level 12 Oddish (199 EXP)
Level 15 Cubone (279 EXP)
You get: 360G
If you go up and to the right, you can fight the last Pokefan in the National
Park.
PokeFan William
Level 14 Raichu (366 EXP)
You get: 1120G
How did these Pokefans get so rich? Next, head east and a little south to reach
Route 36. Have a Repel ready, then head onto Route 36.
|-------------------|
|Route 36 & Route 37|
|-------------------|
Pokémon found on Routes 36 & 37:
#016 Pidgey
#017 Pidgeotto
#029 Nidoran (F)
#032 Nidoran (M)
#037 Vulpix (Silver only)
#058 Growlithe (Gold only)
#163 Hoothoot
#165 Ledyba (Silver only, morning only)
#167 Spinarak (Gold only, night only)
#234 Stantler
Items found on Routes 36 & 37:
Blk Apricorn
Blu Apricorn
Hard Stone
Ice Berry
Red Apricorn
TM08 Rock Smash
Routes 36 & 37 are home to even more trainers. Grab the Ice Berry from the tree
you see right away, then head down to face Psychic Mark.
Psychic Mark
Level 13 Abra (202 EXP)
Level 13 Abra (202 EXP)
Level 15 Kadabra (465 EXP)
You get: 480G
Head down from Mark through the small path into Route 35 once again.
|--------|
|Route 35|
|--------|
Cut the tree blocking your way with A, then fight the trainer you see right
away.
Bug Catcher Arnie
Level 15 Venonat (240 EXP)
You get: 240G
Keep going down the grassy route till you reach another trainer. Fight him.
Bird Keeper Bryan
Level 12 Pidgey (141 EXP)
Level 14 Pidgeotto (339 EXP)
You get: 336G
Drive up to the left side and grab the item you saw earlier, TM04 Rollout. Then,
head back to Arnie and talk to her for her phone number. Afterwards, you can cut
the bush and head back onto Route 36.
|--------|
|Route 36|
|--------|
Welcome back, I guess. You'll want to head east now and take on the next
trainers.
Schoolboy Alan
Level 16 Tangela (566 EXP)
You get: 512G
Talk to Floria, the girl nearby, now. While she goes away to get the
Squirtbottle, you'll want to talk to Alan for his phone number before you run
after her back to Goldenrod City. Arnie also gives you a call at about that time
with Pidgey. Not sure why it's so important that you learn about Pidgey. Keep
heading northeast and you'll get to the tree Pokémon, Sudowoodo.
Now that you're at the strange looking tree, SAVE! You'll want to catch it even
though you never once dreamed of catching something that looks like a lamp post.
Now that you have the Squirtbottle, approach the tree and use the Squirtbottle
on it. The tree will then attack and you'll learn it's a Level 20 Sudowoodo.
It's the only one in the game, so catch it or else. Or else what, you may ask?
Well, I'm not going to inflict physical torture on you. You'll just have to live
with the grim reality that you did not catch Sudowoodo. Use same-type moves on
your starter Pokémon to send it down into the red HP range, then use a Great
Ball. You do not want to knock it out. You're here to capture it.
Sudowoodo is a good addition to your team. Once you have captured Sudowoodo,
talk to the guy to the east. He will give you TM08, Rock Smash
for getting Sudowoodo out of the way. Now head east from Sudowoodo back into
Violet City.
Also note that on Thursday, Arthur of Thursday will appear. He will give you the
Hard Stone item just for talking to him.
==============================================================================
==============================================================================
==============================================================================
(c13)
-------------------------------------------------
@@ -0,0 +1,361 @@
---- 13 > Burned Tower and Ecruteak City Gym ----
-------------------------------------------------
Video Playthrough Link (Goldenrod City, Routes 35-37, to Ecruteak City):
https://www.youtube.com/watch?v=x0E8PnSbA5o
Video Playthrough Link (Burned Tower, Ecruteak City Gym, & Olivine City
Lighthouse): https://www.youtube.com/watch?v=W0IMjwjGPO8
|-----------|
|Violet City|
|-----------|
Now that Sudowoodo has damaged your Pokémon, heal at the Pokémon Center. You can
also now deposit Togepi and withdraw Sudowoodo to have it on your team.
If you focused on just leveling up your starter, your lineup might look like
this:
Level 31 Feraligatr
Level 20 Sudowoodo
Level 14 Drowzee
Level 12 Nidoran
Level 10 Spearow
Level 6 Bellsprout
If you balanced leveling up your Starter and Raticate:
Level 25 Raticate
Level 25 Starter Evolution
Level 20 Sudowoodo
Level 20 Eevee
Level 8 Bellsprout
Odd Egg
Now, head back west onto Route 36, then go northwest onto Route 37.
|--------|
|Route 37|
|--------|
Pokémon found in Route 36 & 37:
#016 Pidgey
#017 Pidgeotto
#029 Nidoran (F)
#032 Nidoran (M)
#037 Vulpix (Silver only)
#058 Growlithe (Gold only)
#163 Hoothoot
#165 Ledyba (Silver only, morning only)
#167 Spinarak (Gold only, night only)
#234 Stantler
Items found in Route 36 & 37:
Blk Apricorn
Blu Apricorn
Hard Stone
Ice Berry
Red Apricorn
TM08 Rock Smash
First, you're battling some twins. If you start using your Sudowoodo here, Rock
Throw is the best move, but Low Kick can be a good alternative fighting move.
Twins Ann & Anne
Level 16 Jigglypuff (259 EXP)
Level 16 Clefairy (232 EXP)
You get: 320G
The bike shop calls again. Apparently, you can keep the bicycle because you
raised sales. Look at you, you're such an amazing salesman. We salute your
marketing efforts. Now, head up to fight the Psychic.
Psychic Greg
Level 17 Drowzee (370 EXP)
You get: 544G
Head back south of the Twins, then go east to the three apricorn trees. You may
want to clear your items at the Ecruteak City Pokémon Center before you grab
those items, so let's head to sunny Ecruteak City.
If it's Sunday, you can also find Sunny of Sunday here and she'll give you a
Magnet item.
|-------------|
|Ecruteak City|
|-------------|
Enter the Pokémon Center. You will be unable to move for a second, making you
wonder if you have managed to freeze time. Alas, you do not have that ability
yet. Sad times. It's actually because Bill is getting his Pokémon healed. He's
finished adjustments on his Time Capsule, allowing you to trade Pokémon between
the Red/Blue/Yellow and Gold/Silver/Crystal games. But you'll need to have
caught something like 151 Pokémon first in Gold/Silver/Crystal before you can
actually make the trade.
Heal at the Pokémon Center, and empty your small backpack of all your berries,
apricorns, and unused TMs into the PC. Make sure you CLEAR one Pokémon spot in
your team so that you can accept a free new Pokémon. Now, bike your way back
down to Route 37. Grab the apricorns you didn't grab earlier (Red, Black, and
Blue Apricorns). Then, keep biking down Route 36, through the National Park, and
through Route 35 back to Goldenrod City. With your newfound biking skills, I
think you should be ready for the Tour de France in no time.
|--------------|
|Goldenrod City|
|--------------|
Head to the southwest portion of the city and enter Bill's house. Now that
you've babysat a Togepi, he will give you an Eevee to take care of. I personally
think Eevee should be named Eevily. What a fantastic name, if I do say so
myself. Eevee can evolve into five other Pokémon: Jolteon (with a thunder
stone), Flareon (with a fire stone), Vaporeon (with a water stone), and Espeon
and Umbreon (based on high friendship and time of day - Espeon in the day,
Umbreon at night). Now, head back north onto Route 35.
If you focused on just leveling up your starter, your lineup might look like
this:
Level 31 Feraligatr
Level 20 Eevee
Level 20 Sudowoodo
Level 14 Drowzee
Level 10 Spearow
Level 6 Bellsprout
If you balanced leveling up your Starter and Raticate:
Level 25 Raticate
Level 25 Starter Evolution
Level 20 Sudowoodo
Level 20 Eevee
Level 8 Bellsprout
Odd Egg
If you are building a balanced team, you could have Sudowoodo and Eevee exchange
experience points for a while from now on, until the fourth gym. In any case,
ride all the way back up to Ecruteak City, and don't get lost. Use the Pokegear
if you need to. There isn't a GPS in this game.
|-------------|
|Ecruteak City|
|-------------|
Items found in Ecruteak City:
HM03 Surf
Itemfinder
TM30
You've reached Ecruteak City with an Eevee this time! Congratulations! Actually,
this isn't a prize booth, as much as you want it to be one. With your new Eevee
with you, enter the Dance Theater just above the Pokémon Center. Talk to the
creepy old man near the door and he will tell you that if you defeat all five
Kimono Girls, you will get a gift. I mean, he's creepy, but the challenge is
still worth your while.
Head up the left side of the stage and battle the girl in green. It's time to
take on every Eevee evolution, one by one. The first is Flareon, a fire type.
Kimono Girl Naoko
Level 17 Flareon (720 EXP)
You get: 1224G
The girl in blue has Espeon, the psychic evolution to Eevee.
Kimono Girl Sayo
Level 17 Espeon (717 EXP)
You get: 1224G
Next is the girl in red, who has Umbreon, the dark evolution to Eevee.
Kimono Girl Zuki
Level 17 Umbreon (717 EXP)
You get: 1224G
The one that's next to the right in blue has Vaporeon, the water evolution to
Eevee.
Kimono Girl Kuni
Level 17 Vaporeon (714 EXP)
You get: 1224G
The final Kimono Girl has Jolteon, the electric evolution to Eevee.
Kimono Girl Miki
Level 17 Jolteon (717 EXP)
You get: 1224G
Now, talk to the same old man again. You'll get HM03 Surf for your efforts. You
can have almost any water type Pokémon learn it, plus a few others. But not
Magikarp. That fish just flops all over the place. Teach HM03 Surf to any
Pokémon that can learn it, because it's one of the best water moves for battle
and you can use it in the overworld.
Leave the house and then head southwest to the gym. Once you see it, go to the
building to the right of it and talk to the man inside. He will give you
Itemfinder if you answer Yes to his question. It's amazing how many items you
can get in this game just by hearing people blather on about themselves and just
say Yes.
Heal at the Pokémon Center, then head to the northwest part of Ecruteak City and
into the Burned Tower. I suggest bringing at least 3 Escape Ropes, 3 Repels, and
10 Great Balls with you.
|------------|
|Burned Tower|
|------------|
Pokémon at the Burning Tower:
#019 Rattata
#041 Zubat
#109 Koffing
Equipping the TM Rock Smash will give you the HP Up at the northeastern part of
the floor, but is it really worth it to use up that TM this early? Think about
your future. Head left and all the way around the spiral and you will now fight
your rival in a battle. He's gonna get pwned again by your superior lineup. He
has a Magnemite now, but you have two extra Level 21+ Pokémon.
|-------------------------------------------|
|Rival |
|Level 20 Haunter (540 EXP) |
|Level 18 Magnemite (342 EXP) |
|Level 20 Zubat (231 EXP) |
|Level 22 Bayleef/Quilava/Croconaw (673 EXP)|
|You get: 1,320G |
|-------------------------------------------|
Use anything other than normal and fighting moves against Haunter. Magnemite's a
steel type, so fire moves are good, while normal moves are bad. Fighting moves
like low kick help. Rock throws are great against Zubat. Against his starter
evolution, you can use the highest-powered move from your highest level Pokémon
that isn't the type of your Pokémon. Piece of cake. I love cake.
Your rival says, after losing to you, “Theres no challenge in it.” After your
victory, you can grab a HP Up by dropping down the northwest hole.
|-------------|
|Ecruteak City|
|-------------|
Heal your Pokémon just before entering the Ecruteak City Gym. You'll probably be
around 4 1/4 to 5 hours into the game. Pat yourself on the back for your
commitment to fighting with fictitious pixelated creatures. Good job!
If you're focusing on only leveling up your starter, your lineup should look
something like this:
Level 33 Starter Evolution
Level 20 Sudowoodo
Level 20 Eevee
If you're building up a more balanced lineup, it may look something like this:
Level 21 Sudowoodo
Level 21 Eevee
Level 25 Starter Evolution
Level 26 Raticate
Let's enter the gym now. Go straight up to fight Trainer 1. Every trainer will
use Ghost type Pokémon, meaning that you can't use any normal or fighting moves
from your Raticate or Eevee. So, you'll have to lean into using your starter
evolution and potentially Sudowoodo (rock throws). Your Pokémon will get licked
by ghosts. I hope your Pokémon likes the experience because it will get licked a
bunch of times.
Sage Ping
Level 16 Gastly (325 EXP)
Level 16 Gastly (325 EXP)
Level 16 Gastly (325 EXP)
Level 16 Gastly (325 EXP)
Level 16 Gastly (325 EXP)
You get: 512G
Now head exactly TWO steps right, then FOUR steps up to fight Trainer 2. Doing
just about anything else will make you fall off the platform. Keep those rock
throws going against Grace.
Medium Grace
Level 20 Haunter (540 EXP)
Level 20 Haunter (540 EXP)
You get: 800G
Talk to Grace again and basically, the path is the zig-zag. You must face every
trainer before you face the gym leader. Go three left, 2 up to Trainer 3.
Sage Jeffrey
Level 22 Haunter (594 EXP)
You get: 704G
Time to take on the final trainer before Morty. Go right 3, then up 2. As a test
of what's to come, this is almost the same lineup as the gym leader, except
Morty has higher levelled and stronger Pokémon.
Medium Martha
Level 18 Gastly (360 EXP)
Level 20 Haunter (540 EXP)
Level 20 Gastly (406 EXP)
You get: 800G
You can move up two squares to get past the platform to get to the gym leader.
But consider healing at the Pokémon Center before fighting the gym leader. I
guess you don't have to, but you're reading this guide and if you're not
following suggestions, why are you here? Oh, sorry, I digress. To get back, fall
off the path anywhere and it'll drop you back to the gym entrance.
Heal at the Pokémon Center and then return to the gym, then take the zig-zag
path back to Morty. Save before you fight Morty in case you lose and want to
re-try the battle quickly. Talk to Morty, and he'll reference some kind of
vision of legendaries. I don't know what he was talking about, as I wasn't
really listening. He's kind of boring.
Battle time - let's go! You'll want to beware of Pokémon that can use Hypnosis
to put your Pokémon to sleep, and then eat its dreams. If you picked Chikorita,
this could be a more challenging fight where you might need some assistance from
your Sudowoodo or other Pokémon.
---
GYM LEADER MORTY
Level 21 Gastly (427 EXP)
Level 23 Haunter (567 EXP)
Level 25 Gengar (1017 EXP)
Level 23 Haunter (621 EXP)
You get: Fog Badge, 2,300G, & TM30 (Shadow Ball)
---
Strategy VERSUS Gastly: You can always use your same-type attack from your
starter evolution. Alternatively, a single rock throw from Sudowoodo should and
would down Gastly.
Strategy VERSUS Haunters: Use the same-type attack from your starter evolution
or use two rock throws from Sudowoodo.
Strategy VERSUS Gengar: Gengar is really nasty because it has Hypnosis and then
Dream Eater to cap on it, but do as many rock throws from Sudowoodo as you can
before you pull out your starter evolution. Use the same-type attack from your
starter evolution to finish off Gengar.
Phew, you won. That's pretty good, considering how many times you got cursed.
Half of the League has fallen to you. All Level 50 and below Pokémon obey you,
and now you can use HM03 Surf out of battle. TM30 Shadow Ball is a really good
ghost move you can equip on a ghost Pokémon, but keep in mind that it's a
one-use item.
Now that you can equip and use HM03 Surf, you should equip it on every water
Pokémon that can use it. It's a 95 power move, making it one of the best attacks
you can use during the game playthrough. Surf is also a great move to use in the
overworld to get across bodies of water. And once you have HM03 Surf, you can
also go back to Union Cave on a Friday to catch a Level 20 Lapras (see Section
36 of this guide).
==============================================================================
==============================================================================
==============================================================================
(c14)
-------------------------------------------------------
@@ -0,0 +1,584 @@
---- 14 > Olivine Lighthouse and Cianwood City Gym ----
-------------------------------------------------------
Video Playthrough Link (Burned Tower, Ecruteak City Gym, & Olivine City
Lighthouse): https://www.youtube.com/watch?v=W0IMjwjGPO8
Video Playthrough Link (Routes 40-43, Cianwood City Gym, Olivine City Gym, Lake
of Rage): https://www.youtube.com/watch?v=HQoKpSr3VaE
Heal, then head west onto Route 38. Arnie calls again. I'm pretty sure these
trainers keep calling randomly because they're extroverts and they're lonely.
Thard_Verad adds, "So far as I can tell, it's roughly every 20-30 minutes, but
the trainer who calls and what they call about is random. Sometimes it's just to
chat, others want a rematch, a few might tell you about an outbreak of a
particular Pokemon. (Personally I'm waiting for news of a Remoraid outbreak)."
Thard_Verad also notes that trainers will only call about gifts in the Pokémon
Crystal Version.
Well, go through the building into Route 38.
|--------|
|Route 38|
|--------|
Pokémon found in Route 38:
#019 Rattata
#020 Raticate
#052 Meowth (Silver only)
#081 Magnemite
#082 Farfetch'd
#128 Tauros
#241 Miltank
Items found in Route 38:
Berry
You're heading west. There are trainers, like on almost every route. I know,
that part is repetitive. Beep beep boop.
Bird Keeper Toby
Level 15 Doduo (307 EXP)
Level 16 Doduo (328 EXP)
Level 17 Doduo (349 EXP)
You get: 408G
Go left and you'll get a Berry, then return to just before the checkpoint house.
Instead of going west, go northwest and you'll face a loner.
Sailor Harry
Level 19 Wooper (211 EXP)
You get: 760G
Head left through the grass to face another trainer. She says, "If you're really
good, I want you to teach me things." That, I can do!
Lass Dana
Level 18 Flaaffy (450 EXP)
Level 18 Psyduck (307 EXP)
You get: 432G
Talk to Dana again and you will be able to switch phone numbers with her. Well,
she seems popular and fairly technologically savvy. I mean, who even has a cell
phone in this region? She'll call you from time to time to tell you that her
Flaaffy is in good shape.
Now head left to face the 4th trainer on this route.
Beauty Valerie
Level 17 Hoppip (268 EXP)
Level 17 Skiploom (495 EXP)
You get: 1496G
Go northwest and you'll run into:
Schoolboy Chad
Level 19 Mr. Mime (553 EXP)
You get: 608G
Mom calls saying she bought you an adorable doll. But is it adorable? That's for
you to decide. I just hope it's not a creepy doll that she thinks is adorable.
Now move southward now to face Beauty Olivia. Now head west onto Route 39.
|--------|
|Route 39|
|--------|
Pokémon found in Route 39:
#019 Rattata
#020 Raticate
#052 Meowth (Silver only)
#081 Magnemite
#082 Farfetch'd
#128 Tauros
#241 Miltank
Items found in Route 39:
Mint Berry
TM13
Welcome to Route 39! It's the home of Moo Moo Farm. Here, moles pop out of the
road randomly and you'll fall off your bike. Oh wait, that's Mario Kart 64.
Sorry, my bad. It can be hard to keep track sometimes. Anyway, there are four
trainers ready to lose you to here on Route 39.
Psychic Norman
Level 17 Slowpoke (360 EXP)
Level 20 Slowpoke (423 EXP)
You get: 640G
Check out the Moo Moo Farm to the west of him. You can get Moo Moo Milk there
for 500G. It's a little expensive, but cheaper than a
Super Potion and more effective. But before you can buy milk, you will first
have to feed berries to their Miltank so that it gets better. Once you do that,
you'll get TM13! For now, grab the Mint Berry, remember this place exists, and
come back later with those berries. Head southward.
PoKeFan Derek
Level 17 Pikachu (298 EXP)
You get: 1360G
Continue down...
PoKeFan Ruth
Level 17 Pikachu (298 EXP)
You get: 1360G
Continue southward to yet another trainer...
Sailor Eugene
Level 17 Poliwhirl (477 EXP)
Level 17 Raticate (421 EXP)
Level 19 Krabby (468 EXP)
You get: 760G
Head southward into...
|------------|
|Olivine City|
|------------|
Pokémon found in Olivine City:
#100 Voltorb
Items found in Olivine City:
Good Rod
HM04 Strength
TM23
As soon as you enter Olivine City and, when you are about to pass by the gym
your rival will appear. He will not challenge you to a match (because he knows
he's going to lose ^_^). Instead, he will tell you that Jasmine the Gym Leader
is currently at the lighthouse tending to a sick Pokémon. He thinks the gym
leader is a weakling because he has always favored strong Pokémon.
As you zig zag your way down Olivine City, go into the house to the southeast of
the gym. You can apparently do that everywhere because no one locks their doors
in this foreign land. Talk to the fisherman and he will give you a Good Rod,
which can catch more than just Magikarps that can only splash.
Then heal at the Pokémon Center and talk to the sailor at the table for HM04
Strength! All that biking you did isn't going to help you move boulders. That's
mostly cardio and strong legs, man. But now you can move boulders!
Now that you have and can use HM04 Strength, it is beneficial to equip HM04
Strength on any Pokémon at this stage that you can, because it's an 80-power
move that's worth using. Strength is also a great move to use in the overworld
to get past boulders in dungeons. You'll need to go to a Move Deleter to remove
it later if you want to improve your post-game moves. However, HM04 Strength is
all around stellar, and it's going to be better than most moves that Pokémon
naturally learn or can learn from TMs during your game playthrough.
After you get Strength (HM04) for free, go up the Olivine City Lighthouse in the
southeast part of town (not the dock). You'll spot it when you see it. It's
basically the only skyscraper in the city because no one can afford to build
bigger buildings in Johto. Let's go in and fight!
|-----------------------|
|Olivine City Lighthouse|
|-----------------------|
Items found in Olivine City Lighthouse:
Ether
Great Ball
Rare Candy
Super Potion
TM34 Swagger
OK, you're going to have to climb up a few floors to even meet Jasmine. You'll
need to switch back and forth between going clockwise and counterclockwise on
each floor. Wait, do people still know what analog clocks work anymore? Think
about going around in a circle in one direction and then back in the other
direction.
Follow the path up to the second floor. You might get a call from Liz about guys
like Falkner. I don't know, people call about the weirdest things. I guess the
real reason here is not to give strangers your phone number. Keep going till you
see another trainer.
Gentleman Alfred
Level 20 Noctowl (693 EXP)
You get: 1440G
Keep advancing and move along the path to face Trainer 2.
Sailor Huey
Level 18 Poliwag (297 EXP)
Level 18 Poliwhirl (504 EXP)
You get: 720G
Go up to the next floor. You can avoid fighting the next trainer, but you should
probably pick up the EXP.
Bird Keeper Theo
Level 17 Pidgey (199 EXP)
Level 15 Pidgey (175 EXP)
Level 19 Pidgey (223 EXP)
Level 15 Pidgey (175 EXP)
Level 15 Pidgey (175 EXP)
You get: 360G
You'll probably want to heal Eevee up a bit now. Keep following the path to the
4th trainer battle!
Gentleman Preston
Level 18 Growlithe (351 EXP)
Level 18 Growlithe (351 EXP)
You get: 1296G
Head up the stairs to the 4th floor and head directly left for trainer battle 5.
She seems to be twitchy about things with Jasmine, almost like she wants to find
Jasmine and kidnap her.
Lass Connie
Level 21 Marill (261 EXP)
You get: 504G
If you talk to Connie again, it sounds like she actually cares about Connie.
Phew! Head south now, around on the path to the next trainer.
Sailor Kent
8Level 18 Krabby (442 EXP)
Level 20 Krabby (492 EXP)
You get: 800G
Todd will now call and say that Goldenrod finally has cheap sales. Woot! Head up
the stairs to fifth floor, or whatever it is. I'm not writing this guide because
I'm good at counting. I didn't promise that anywhere. After you get up, head
downward to grab TM34 Swagger, then go up to fight another trainer.
Bird Keeper Denis
Level 18 Spearow (223 EXP)
Level 18 Spearow (223 EXP)
Level 20 Fearow (693 EXP)
You get: 432G
Keep going and grab the Rare Candy, then go back down to Floor 4. There's a hole
next to Lass Connie. This would be a lawsuit anywhere else, but it's Pokémon and
your main character can jump down floors unscathed because he's the hero of the
story. Then, you'll find another trainer.
Sailor Terrell
Level 20 Poliwhirl (561 EXP)
You get: 800G
Grab the Ether right next to him, head up the stairs, and head up even more
stairs. Fight the trainer below you.
Sailor Ernest
Level 18 Machop (339 EXP)
Level 18 Machop (339 EXP)
Level 18 Poliwhirl (504 EXP)
You get: 720G
Go down and get the Great Ball, then climb the stairs again. We are finally on
the top level, Level 6. Talk to the person on that level, and it is Gym Leader
Jasmine. She asks you to get medicine for Ampharos for her. Afterwards, grab the
Super Potion left of Jasmine, then go to the right and jump down the holes on
the right side of every floor to drop back down to the bottom of the
lighthouses.
|------------|
|Olivine City|
|------------|
Heal at the Pokémon Center. Make sure you have a water Pokémon that can learn
HM03 Surf. If you don't have one, you'll have to catch one with a Good Rod on
the following route, Route 40.
Teach HM03 Surf to any Pokémon that can learn it, which will primarily be water
type Pokémon, because it's one of the best water moves for battle and you can
use it in the overworld. You'll need to go to a Move Deleter to remove it later
if you want to improve your post-game moves. However, HM03 Surf is all around
stellar, and it's going to be better than most moves that Pokémon naturally
learn or can learn from TMs during your game playthrough. Note that Magikarp
can't learn Surf. That fish just flops all over the place. But Krabby can. How
does a trainer ride a tiny Krabby? No one really knows.
Now exit, and enter the PoKeMart to restock on items, like Great Balls. Now head
west from the Pokémon Center onto Route 40!
|--------|
|Route 40|
|--------|
Pokémon found in Route 40:
#072 Tentacool
#073 Tentacruel
#098 Krabby
#213 Shuckle
#226 Mantine (Gold only)
Head to about where you see the swimmer in the water, then activate Surf by
clicking A at the water. It's time for the sea battles to begin! Talk to the
swimmer and let's begin a fight!
Swimmer Simon
Level 20 Tentacool (450 EXP)
Level 20 Tentacool (450 EXP)
You get: 160G
Swim southwest till you see a swimmer that's a girl. Face her next.
Swimmer Elaine
Level 21 Staryu (477 EXP)
You get: 420G
Head down till you see another girl a little southeast. Let's take another water
battle!
Swimmer Paula
Level 19 Staryu (430 EXP)
Level 19 Shellder (394 EXP)
You get: 380G
Head southeast again to encounter another guy there, the final swimmer on the
route.
Swimmer Randall
Level 18 Shellder (373 EXP)
Level 20 Wartotle (612 EXP)
Level 18 Shellder (373 EXP)
You get: 144G
Now, swim south onto Route 41.
|--------|
|Route 41|
|--------|
Pokémon found in Route 41:
#072 Tentacool
#073 Tentacruel
#098 Krabby
#213 Shuckle
#226 Mantine (Gold only)
Route 41 should give your Pokémon (like Eevee) lots of training for experience.
If you go down the right side:
You'll see a boy swimming around, or more like, spinning around. I feel like
spinning in the water like that should be in the Summer Olympics. He seems to be
the first trainer you've met to even have six Pokémon.
Swimmer George
Level 16 Tentacool (360 EXP)
Level 17 Tentacool (382 EXP)
Level 16 Tentacool (360 EXP)
Level 19 Staryu (430 EXP)
Level 17 Tentacool (382 EXP)
Level 19 Remoraid (316 EXP)
You get: 152G
Liz then calls you about her Nidoran pwning a Rattata. Yay! Continue going south
from George and you'll spot another swimmer. Either that, or she spots you.
Those are really the only two options. There isn't a third.
Swimmer Kara
Level 20 Staryu (453 EXP)
Level 20 Starmie (886 EXP)
You get: 400G
Keep heading south along the smaller and smaller path.
If you go down the left side:
You'll be going between some reefs and whirlpools. Keep going down to fight more
swimmers.
Swimmer Charlie
Level 21 Shellder (436 EXP)
Level 21 Tentacool (427 EXP)
Level 19 Tentacruel (834 EXP)
You get: 152G
If you go west from here:
Swimmer Wendy
Level 21 Horsea (373 EXP)
Level 21 Horsea (373 EXP)
You get: 420G
If you go south/southwest instead of west:
Swimmer Susie
Level 20 Psyduck (342 EXP)
Level 22 Goldeen (522 EXP)
You get: 440G
Swimmer Berke
Level 23 Qwilfish (492 EXP)
You get: 184G
Swimmer Denise
Level 22 Seel (471 EXP)
You get: 440G
Head to the east of her.
Swimmer Kirk
Level 20 Gyarados (916 EXP)
Level 20 Gyarados (916 EXP)
You get: 160G
The two sides will eventually merge. You will EVENTUALLY spot a swimmer. Fight
him, the guy who talks of the Whirl Islands. The Krabby could do some damage
against lower-level Pokémon.
Swimmer Mathew
Level 23 Krabby (565 EXP)
You get: 184G
You will reach land after that battle. Welcome to Cianwood City.
|-------------|
|Cianwood City|
|-------------|
Pokémon found in Cianwood City:
#213 Shuckle (Get for free)
Items found in Cianwood City:
HM02 Fly
Secret Potion
TM01
Cianwood is pretty small, and you will not be spending much time here. Heal at
the Pokémon Center and clear a space so you can acquire another Pokémon. This
may be a good opportunity to see which of your Pokémon can learn HMs Surf and
Strength. You'll still want to have one Pokémon that can use HM01 Cut.
To the left of the Pokémon Center is a house where a PokeManiac will give you
Level 15 Shuckie the Shuckle if you talk to him so that your thief rival won't
steal that too. You can return it later if you'd like. You're good at taking
care of Pokémon, right? I assume you are. You probably were playing with
Tomagachis before you turned this game on. Remember to feed your pets and give
your pets water!
Thard_Verad adds, "You can keep Shuckie the Shuckle if it likes you enough when
you try to return it. Let any Shuckle hold a Berry as you run around. Eventually
it will turn into Berry Juice. (Restores 20 HP) Berry Juice is nice if you do a
Little Cup battle."
Now head to the building below that. It's the Pharmacy. A guy inside the house,
wearing sunglasses will give you some medicine, a.k.a. Secretpotion. You will
give this back to Jasmine in Olivine for Ampharos to light back the lighthouse.
If you visit the home in the top part of town, he will offer to take pictures of
your Pokémon. You can then print them out if you have the Gameboy Printer.
Anyways, it's now time to battle in the Cianwood City Gym and face Chuck, the
fighting gym leader. Now enter the gym.
If you're only leveling up your starter, here's what your lineup might look
like:
Level 40 Starter Evolution
Level 21 Sudowoodo
Level 21 Tentacool
Level 20 Eevee
Level 15 Shuckle
Level 10 Spearow "Kenya"
Your lineup should now look like this:
Level 27 Starter Evolution
Level 27 Sudowoodo
Level 28 Raticate
Level 29 Eevee
Level 15 Shuckle
Let's go! Head up and you'll fight all the Blackbelts. Fighting type moves are
super effective against normal and rock types, so try not to use them where
possible against Chuck. Your evolved starter will do well. If you've been
leveling up one Pokémon, you can just repeat your strongest move and it should
sweep the gym. If you have a more balanced lineup, it may be tougher to do as
much damage.
Blackbelt Yoshi
Level 27 Hitmonlee (804 EXP)
You get: 648G
Before you can react, the other Blackbelt fights you. You got double teamed.
Blackbelt Lao
Level 27 Hitmonchan (810 EXP)
You get: 648G
Keep moving up.
Blackbelt Nob
Level 25 Machop (471 EXP)
Level 25 Machoke (565 EXP)
You get: 600G
Use Strength to push your way to the last trainer before the gym leader.
Blackbelt Lung
Level 23 Mankrey (364 EXP)
Level 23 Mankey (364 EXP)
Level 25 Primeape (798 EXP)
You get: 600G
Now that you've fought every trainer, leave the gym, heal at the Pokémon Center,
and then return to the gym. To get past the boulders, you need a Pokémon with HM
Strength. Then push the left and right boulders each up one, and move the center
boulder left or right.
Save before the battle. If you aren't regularly saving while playing this game,
I'm not sure what to tell you. You must have a lot of confidence in your
batteries or in your abilities to win battles without resetting. Congrats on
your confidence. I'm sure that it will be worth something someday.
---
GYM LEADER CHUCK
Level 27 Primeape (861 EXP)
Level 30 Poliwrath (1188 EXP)
You get: Storm Badge, 3000G, & TM01 (Dynamic-Punch)
---
Strategy VERSUS Primeape: If you're just leveling up your starter, just use the
strongest move. Flying Pokémon could also be effective because flying moves beat
fighting. Raticate's hyper fangs can also work well.
Strategy VERSUS Poliwrath: Use the strongest move if you're using your starter.
If you're using a more balanced team, you may need to use a variety of
high-power moves, like low kicks from Sudowoodo or hyper fangs from Raticate. A
combination of Pokémon unleashing damage on Poliwrath should be able to take it
down. It's a takedown.
With the victory over Chuck, you have FIVE badges, another TM (Dynamic-Punch),
and more moolah. Woot. You're also within easy range for a 6th badge. Level 70
and under Pokémon all obey you and now you can use HM02 Fly, which you'll be
getting in just a second. Dynamic Punch is the deadly punch Poliwrath gave you
earlier, but it has crappy aim and will only work half the time.
Talk to the lady outside the gym and she will give you HM02 Fly after she
notices your fifth badge. Apparently, she's Chuck's wife and wants him to stop
being so chubby. I think that's more a matter of diet than training at this
point. Chuck is probably eating too many chips and drinking too much soda.
Teach HM02 Fly to all your bird Pokémon (flying or otherwise) as it's a good
70-power move in a playthrough that can also get you quickly around the map.
You'll want to return to Olivine now so that you can give the medicine to
Jasmine and the Ampharos.
==============================================================================
==============================================================================
==============================================================================
(c15)
-------------------------------
@@ -0,0 +1,81 @@
---- 15 > Olivine City Gym ----
-------------------------------
Video Playthrough Link (Routes 40-43, Cianwood City Gym, Olivine City Gym, Lake
of Rage): https://www.youtube.com/watch?v=HQoKpSr3VaE
Make sure you've got a bird Pokémon using HM02 Fly. If you are leveling up an
Eevee to try to evolve it into an Espeon or Umbreon, fly to Goldenrod City.
Otherwise, fly straight back to Olivine City and skip the next paragraph.
|--------------|
|Goldenrod City|
|--------------|
Welcome back. Here, we'll want to go into the Goldenrod City underground, so
head to the southwestern portion of town and do so. Go in and you'll find the
three shops again. Talk to the man who is running the second/middle shop. Pay
the 500G for Eevee to get groomed. You want Eevee happy for evolution, right? =)
Eevee's happiness meter should get a nice boost. Now get out of there and fly
back to Olivine City, then enter the Olivine City Lighthouse.
|------------------|
|Olivine Lighthouse|
|------------------|
Climb up the Olivine Lighthouse in the EXACT same way you came up. This means
falling to the left of that one lass. Once you get all the way up, talk to
Jasmine and say Yes to the fact that Ampharos will be cured by the medicine. Now
Jasmine will return to her gym, so that's good. Now drop down out of the
Lighthouse like you did before and re-enter Olivine City.
|------------|
|Olivine City|
|------------|
You have two choices here. You can fight the gym, or try to gain more experience
first by flying to Ecruteak City and go through the Lake of Rage and then come
back (which is in the next parts of this guide). I'm not your mom, who keeps
buying random items with your money. You do have choices in life. If you want to
get a nice shiny Pokémon to assist you in your next fight first, take that. If
you want to see how you fare now, you can go straight into the Olivine City Gym
to take on Jasmine.
Enter the Olivine City Gym. This is a steel-based gym with electric elements.
Your starter should do okay even if there's a type disadvantage. Sudowoodo and
Raticate will do well too, although the Gyarados that you can get at the Lake of
Rage will fare the best with Dragon Rage.
Keep moving up in the gym. You will find there are no apprentices of any kind.
Jasmine does not take apprentices. She's too good for that. Jasmine remembers
your help and thanks you, then remembers that she's a gym leader and that she
needs to be serious. Save before the battle, then put either your starter
Pokémon, Raticate, or Gyarados in the front position when you start fighting.
---
GYM LEADER JASMINE
Level 30 Magnemite (571 EXP)
Level 30 Magnemite (571 EXP)
Level 35 Steelix (1470 EXP)
You get: Mineral Badge, 3,500G, & TM23 (Iron Tail)
---
Strategy VERSUS Magnemites: Use the strongest starter move, fire, or fighting.
Beware of thunder waves that can paralyze your Pokémon. Magnemite actually does
pretty decent damage with Sonicboom.
Strategy VERSUS Steelix: Use the strongest starter move, fire, or fighting.
Beware of thunder waves that can paralyze your Pokémon. Iron Tail can do a
decent amount of damage.
Woot! That's your sixth badge! This gym leader also thinks you're better now
(some gym leaders are arrogant), and with the Mineral Badge your Pokémon's
defense is raised slightly. You also get TM23, Iron Tail, the attack Steelix
used.
==============================================================================
==============================================================================
==============================================================================
(c16)
--------------------------------------------------
@@ -0,0 +1,223 @@
---- 16 > The Lake of Rage and Shiny Gyarados ----
--------------------------------------------------
Video Playthrough Link (Routes 40-43, Cianwood City Gym, Olivine City Gym, Lake
of Rage): https://www.youtube.com/watch?v=HQoKpSr3VaE
Video Playthrough Link (Mahogany Town & Lake of Rage):
https://www.youtube.com/watch?v=z5_tkLJ0fvw
|-------------|
|Ecruteak City|
|-------------|
Fly to Ecruteak City, then head east onto Route 42. Bring Pokémon that can use
HMs Surf and Cut.
|--------|
|Route 42|
|--------|
Pokémon found in Route 42:
#021 Spearow
#041 Zubat
#056 Mankey (Gold only)
#179 Mareep
#180 Flaaffy
Items found in Route 42:
Grn Apricorn
Pnk Apricorn
Super Potion
Ultra Ball
Ylw Apricorn
Grab the Ultra Ball you see right after you enter the route. Head a little east.
I suggest waiting to go into Mt. Mortar until you've gotten HM07 Waterfall so
you can have the full Mt. Mortar experience. It's like going for fast food
rather than your luxury dining experience. I'd suggest taking the luxury dining
experience here.
Instead, head east and surf across that lake now, on a Tentacool, Wooper,
Krabby, or whatever. I'm not exactly sure how you fit on any of these Pokémon,
given that you are larger than each Pokémon. But you fit like it's a small boat
custom built for your comfort. Then, use Cut below and grab the three Apricorns
(Green, Pink, and Yellow). Keep going and then grab the Super Potion.
|--------|
|Route 42|
|--------|
Surf across the second lake and talk to the fisherman for a battle.
Fisher Chris
Level 18 Qwilfish (385 EXP)
You get: 720G
You'll see two more trainers. Fight them both.
PoKeManiac Shane
Level 16 Nidorina (400 EXP)
Level 16 Nidorino (403 EXP)
You get: 960G
I'm not sure why the PoKeManiac needed to be so xenophobic, but he was easy to
beat. Now fight the final trainer on the route.
Hiker Benjamin
Level 14 Diglett (243 EXP)
Level 14 Geodude (258 EXP)
Level 16 Dugtrio (523 EXP)
You get: 512G
Now ride east into Mahogany Town.
|-------------|
|Mahogany Town|
|-------------|
Items found in Mahogany Town:
TM16
Heal at the Pokémon Center, then explore Mahogany Town. It's small in size, but
big in swindling. The guy in the house in the north is trying to sell you
Slowpoketails for 9800G. Suspicious. Then if you try to go east, the guy wants
you to head up to the Lake of Rage and tries to sell you Ragecandybars. If you
buy one, that's cool, but he still won't let you pass. So rude.
Well, I guess it's time to head north onto Route 43. Make sure you have at least
15 Great Balls before you head up, because there are a lot of catching
opportunities on Route 43 and at the Lake of Rage.
|---------------------------|
|Route 43 & The Lake of Rage|
|---------------------------|
Pokémon found in Route 43:
#017 Pidgeotto
#048 Venonat
#164 Noctowl
#179 Mareep
#180 Flaaffy
#230 Girafarig
Items found in Route 43:
Bitter Berry
Max Ether
Head up the path to fight some five trainers. Let's commence.
Camper Spencer
Level 17 Sandshrew (337 EXP)
Level 18 Sandslash (592 EXP)
Level 19 Zubat (219 EXP)
You get: 380G
As you go through Route 43 you will see a building blocking the right side of
the road. Don't go through it. Team Rocket will charge you 1,000G to go through.
You must be frugal! You've gotten rich taking money from random trainers on
shady routes, but you won't stay rich if you give Team Rocket your hard earned
money.
Instead, you should go the long way up to the Lake of Rage. As you near the end
of Route 43 you will find two routes to the Lake of Rage. The left path will
bring you two some items (be sure to have a Pokémon with HM01 Cut), and you will
also find Wesley on Wednesday. He will give you the Blackbelt item, which powers
up Fighting attacks. The right path will lead to the Lake of Rage. Head up for
more battling. Remember to grab the Max Ether, too, which is to the right of
Picnicker Tiffany.
Picnicker Tiffany
Level 20 Clefairy (291 EXP)
You get: 400G
Keep moving up.
PoKeManiac Brent
Level 19 Lickitung (516 EXP)
You get: 1140G
You're trying to save civilization from a dangerous threat on the Lake of Rage,
but naturally, this is the time your Mom calls you. Mom's bought another item
that you apparently might like. Keep moving up the path. Two more trainers to
go. Haha, the next guy mentions your rival making fun of his Pokémon. He's not
just a criminal, he's also mean! Shocking.
PoKeManiac Ron
Level 19 Nidoking (793 EXP)
You get: 1140G
Keep going north!
PoKeManiac Ben
Level 19 Slowbro (667 EXP)
You get: 1140G
Bike down the left side past a few of the first trainers, then go as far left as
you can. You'll come across another tr`ainer:
Fisher Marvin
Lv 10 Magikarp (42 EXP)
Lv 10 Gyarados (457 EXP)
Lv 15 Magikarp (63 EXP)
Lv 15 Gyarados (? EXP)
You get: 600G
Now, ride your bike up the west side through the forest. You can use HM01 Cut on
various trees to get Max Ether and TM43 (far northeast). If you go to the far
northwest through the forest, you'll find a house in the middle of nowhere. The
person there will give you TM10 Hidden Power. Now, head back the way that you
came, then north to the Lake of Rage.
|------------|
|Lake of Rage|
|------------|
Items found at the Lake of Rage:
Blackbelt
Max Ether
Red Scale
TM10 Hidden Power
TM43 Secret Power
If you talk to the people around, they will tell you that something strange has
been going on. If you surf into the Lake of Rage you will find a Gyarados, but
this is no ordinary Gyarados. This is a Level 30 red Gyarados. It is special
because it is an off-brand color "shiny" Pokémon. Color variations of any
Pokémon are typically rare, and therefore cool and desirable. If only people
felt the same way about other people and things that look different in our
society.
You should save before the battle and make sure you catch it without knocking it
out. If you are using your best Pokémon, you might not want to use its best
moves. Use Great Balls to try to catch it. Once you've caught it, you've now
become a shiny Pokémon trainer. Congrats on this elusive accomplishment.
After you capture the Red Gyarados you will receive the Red Scale. If you bring
that item to Mr. Pokémon he will give you the EXP Share item. When you reach the
Lake of Rage, talk to someone with a cape on him. He's looking at a sign. When
someone is looking at a sign, it is a sign you should interrupt his train of
thought and talk to him.
It turns out that he is Lance, the dragon trainer from Blue/Red/Yellow. He's the
fourth Elite Four member, remember? What do you mean you haven't played the
original games? Go and play one of them after you've beaten this game! Anyway,
talk to him, and agree to help him investigate the matter. I have no idea what
this entails but people like when you say Yes, and he did say that you were a
trainer of "considerable skill." You should be quite proud.
It seems like there's now a problem in Mahogany Town. You can fly back to
Mahogany Town.
==============================================================================
==============================================================================
==============================================================================
(c17)
--------------------------------------------------------
@@ -0,0 +1,334 @@
---- 17 > Team Rocket Hideout and Mahogany Town Gym ----
--------------------------------------------------------
Video Playthrough Link (Mahogany Town & Lake of Rage):
https://www.youtube.com/watch?v=z5_tkLJ0fvw
Video Playthrough Link (Mahogany Town Gym):
https://www.youtube.com/watch?v=CoPpbons5bY
|-------------|
|Mahogany Town|
|-------------|
Heal at the Pokémon Center and withdraw your Gyarados so that it's in your
lineup. If you head to the middle of the town where a guy is selling things,
you'll find Lance inside basically forcing his way through that house. He'll
show you the secret stairway to Team Rocket's Hideout.
If you're just leveling up your Starter, your lineup should look something like
this:
Level 44 Starter Evolution
Level 30 Gyarados
Level 21 Sudowoodo
Level 20 Eevee
Level 21 Tentacool
Level 10 Kenya the Spearow
If a more balanced lineup, it should now look something like this:
Level 27 Starter Evolution
Level 28 Sudowoodo
Level 30 Eevee
Level 30 Gyarados
Level 31 Raticate
Level 5 Pidgey
|-------------------|
|Team Rocket Hideout|
|-------------------|
Items in the Team Rocket Hideout:
Ice Heal
Hyper Potion
Nugget
Protein
X Accuracy
TM46
You and Lance must now stop Team Rocket's operations, but you're still left on
your own. Head downstairs and left after you do so. But you have been spotted.
You intruder!
Rocket Grunt (M)
Level 17 Drowzee (368 EXP)
Level 19 Zubat (219 EXP)
You get: 760G
A second Rocket Grunt runs out to greet you. Hooray. You see, every time you
pass one of those statues, two Team Rocket grunts will fight you.
Rocket Grunt (M)
Level 16 Zubat (184 EXP)
Level 17 Grimer (327 EXP)
Level 18 Rattata (219 EXP)
You get: 720G
See? You really should read the dialogue. I'm not saying this as a hypocrite who
doesn't read some of the dialogue himself. Anyway, you'll spot another statute
while heading left. If you pass by that statue, you'll fight the same two Grunts
again, with the Drowzee and Zubat, and the Zubat, Grimer, and Rattata. If you go
down further, you'll fight a Rocket Grunt with many Rattatas. You could say that
he's a Rattata connoisseur.
Rocket Grunt (M)
Level 16 Rattata (195 EXP)
Level 16 Rattata (195 EXP)
Level 16 Rattata (195 EXP)
Level 16 Rattata (195 EXP)
You get: 640G
Does Team Rocket have an undercover Zubat, Grimer, and Rattata breeding
operation? I mean, probably. No one wants to admit that publicly, because that's
embarrassing. Anyway, if you head back past that statute and go down, you'll see
a pattern of zig-zags that avoids the statutes and leads to a scientist with
glasses. We know he's smart because he's wearing glasses. Let's fight him and
get him to give us some answer.
Scientist Jed
Level 20 Magnemite (381 EXP)
Level 20 Magnemite (381 EXP)
Level 20 Magnemite (381 EXP)
You get: 2000G
Talk to the scientist again. He'll tell you about Warp Panels that can take you
back to the entrance. I told you he was smart. Then go to the computer next to
him and press A to turn off the switch for the statues. Good thing the scientist
isn't a security expert. Now, no more Team Rocket Grunts will swarm you every
time you reach a statue.
You can now roam around the floor like you own the place. Pick up the X
Accuracy, Hyper Potion, and Nugget. Then make your way around to the
southwest part of the room and take the staircase down to the next floor
(Negative Floor 2). Lance, who is not only a Pokémon champion but also a healer,
will meet with you and heal all your Pokémon. What a nice, kind-hearted guy.
Then head to the right and beat both Rocket Grunts.
Rocket Grunt (M)
Level 18 Venonat (288 EXP)
Level 18 Venonat (288 EXP)
You get: 720G
Now head to the right.
Rocket Grunt (M)
Level 18 Golbat (658 EXP)
You get: 720G
Head downstairs to Negative Floor 3. Lance says we need two passwords to unlock
the doors on this level. Unfortunately, one of the passwords is not 123456. But
if you beat the right Rocket Grunts, they'll spill the beans. Fortunately for
you, the hero, they are both on this level. Go up and left and you can pick up a
Protein and an Ice Heal. Then loop all the way back down and to the right, go
past the trainer that Lance beat up for more intel, and go up to fight the
scientist.
Scientist Ross
Level 22 Koffing (537 EXP)
Level 22 Koffing (537 EXP)
You get: 2200G
Head up to fight the Team Rocket Grunt above that. She says she MIGHT have the
password. She does! You've got to give that to the Team Rocket Grunts. They
aren't liars. They are, in fact, people of integrity.
Rocket Grunt (F)
Level 18 Ekans (238 EXP)
Level 18 Gloom (508 EXP)
You get: 720G
Now that you've beat her, you have the password after you talk to her. It's
SLOWPOKETAIL. I know, it's shocking. Then head left for your next battle.
Scientist Mitch
Level 24 Ditto (313 EXP)
You get: 2400G
Head left to the final trainer to the left of the table. Fight him.
Rocket Grunt (M)
Level 19 Raticate (471 EXP)
You get: 760G
Talk to him again now that you've beat him, and he'll give you his password
(RATICATE TAIL). Get the items (Dire Hit and Full Heal), go up the stairs at the
upperright part of the floor, then head left on Negative Floor 2. Fight the Team
Rocket member that spots you...or that you reach.
Rocket Grunt (M)
Level 17 Rattata (207 EXP)
Level 17 Rattata (207 EXP)
Level 17 Zubat (196 EXP)
You get: 680G
Head up the stairs to Negative Floor 1. Head right and then before you reach the
locked door, your rival will come and talk to you. He can't battle you though,
cause Lance just destroyed him. "He said I don't love and trust my Pokémon
enough." Press A at the door to open it and go into Giovanni's office. No idea
where Giovanni is, but there's a Rocket Executive that you can square off with.
Rocket Executive (M)
Level 22 Zubat (253 EXP)
Level 24 Raticate (595 EXP)
Level 22 Koffing (537 EXP)
You get: 1584G
He quickly runs off after he loses. Talk to the Murkrow next to the Rocket
Executive and it'll give you the final password, HAIL GIOVANNI. Now head to the
second stairs that you did NOT come out of, and grab TM46 from there, then go
back down to the same floor as where you just beat the executive.
Head back the way you came, past the spinning Team Rocket member, and go back
upstairs. Go back down to the floor with the gate blocking you from entering the
area with machines inside. We're going ALL the way back to where you met Lance
for the second time within the Team Rocket hideout. It's the second floor where
Lance healed your Pokémon.
You'll find a passworded door right there. Open it, and you'll quickly get
surrounded by two Team Rocket executives. Now you'd have to take both
executives, but Lance comes just in time. This time, you're fighting the female
Team Rocket boss while Giovanni's apprentice takes on Lance. Spreading the fun?
The battle isn't too difficult though, so just defeat her with any combo.
Rocket Executive (F)
Level 23 Arbok (724 EXP)
Level 23 Gloom (649 EXP)
Level 25 Murkrow (573 EXP)
You get: 1800G
She later says that you would be powerful enough to be a Rocket Executive. Well,
not quite. You're strong enough to run all of Team Rocket. But you are a kind
soul. You're here to do good for the world. After the Team Rocket Executives
lose, they run away like the losers that they are. Lance will then talk to you
and he'll want you to help him destroy the Electrodes supplying the base with
electricity.
Lance runs to the right to take the Electrodes on the right, so you'll take the
left. The Electrodes will typically try to Self Destruct, so move your Sudowoodo
into the first spot temporarily as Sudowoodo can absorb a lot of damage. If
you're lucky enough to have any Electrode not use Self Destruct, you can try to
catch it with a Great Ball.
Level 23 Electrode (492 EXP)
Level 23 Electrode (492 EXP)
Level 23 Electrode (492 EXP)
The Lake is saved, and Lance thinks you're the hero. Why thank you, Lance. You
are the hero that the world deserves! Lance gives you HM06 Waterpool, an item he
just found lying around in the hideout. Excellent. You'll need the 7th badge
from the Mahogany City Gym to use it, though. Follow Lance's way out to the
stairs, then use the warp next to it to head out of the hideout. You know your
way out now.
|-------------|
|Mahogany Town|
|-------------|
Heal at the Pokémon Center, then enter the Mahogany Town Gym. I hope all those
trips to the ice rink as a kid pay off, because you'll be sliding all over the
place. This gym will actually have lower-level Pokémon than Jasmine. Fire and
grass moves will be effective. It's an ice gym!
Skate to the left to fight the very first trainer.
Boarder Ronald
Level 24 Seel (513 EXP)
Level 24 Seel (513 EXP)
Level 25 Dewgong (942 EXP)
You get: 1728G
Slide up twice, then right one, then down one, then left one to fight another
trainer.
Boarder Douglas
Level 24 Shellder (498 EXP)
Level 25 Cloyster (1087 EXP)
Level 24 Shellder (498 EXP)
You get: 1728G
Talk to the trainer again to get Gym Leader Pyrce's secret. He meditates under a
waterfall every day. When you're listening to music as you meditate, it's
actually a recording from Pryce's special waterfall. Now slide down, then to the
right.
Boarder Brad
Level 26 Swinub (433 EXP)
Level 26 Swinub (433 EXP)
You get: 1872G
Slide once to the right, then down to face another trainer.
Skier Clarissa
Level 28 Dewgong (1056 EXP)
You get: 2016G
Head up twice, then slide left to the final trainer.
Skier Roxanne
Level 28 Jynx (822 EXP)
You get: 2016G
Slide down and out of the gym to heal at the Pokémon Center before you return to
fight the gym leader. Go to just left of the left pillar then go up, up, right,
down, left, up and finally right. You will be standing in front of the gym
leader in your ice skates. Your time falling down at the ice rink as a kid did
pay off after all!
Good luck against Pryce. This will be a piece of cake after fighting Jasmine. He
thinks he's amazing because he has been fighting since before you were born. But
you're an underage kid who's been biking around the world on a rental bike. How
could you be stopped?
If you are focusing on leveling up your starter, your lineup should be around:
Level 47 Starter Evolution
Level 30 Gyarados
Level 22 Sudowoodo
Level 21 Tentacool
Level 20 Eevee
Level 10 Kenya the Spearow
If you have a balanced lineup, your lineup should be around:
Level 31 Raticate
Level 31 Gyarados
Level 31 Bayleef or Quilava, or Croconaw
Level 31 Eevee
Level 31 Sudowoodo
Level 5 Pidgey
---
GYM LEADER PRYCE
Level 27 Seel (577 EXP)
Level 29 Dewgong (1093 EXP)
Level 31 Piloswine (1062 EXP)
You get: Glacier Badge, 3,100G, & TM16 (Icy Wind)
---
Strategy VERSUS Seel: Use the highest-powered attack from your highest level
Pokémon that is not a fire or water type. Grass moves are ideal. Finally, a gym
that somewhat favors anyone who started with Chikorita!
Strategy VERSUS Dewgong: Use the highest-powered attack from your highest level
Pokémon that is not a fire or water type. Grass moves are still ideal.
Strategy VERSUS Piloswine: Pilowsine is a bit tougher. It is higher level and
more defensive. But it is also slow. Use the highest-powered attack from your
highest level Pokémon that is not a fire or water type. Grass moves are still
ideal.
You're worthy of the badge, and you get the SEVENTH badge. W00t! This raises
your Pokémon's special stats, you get more mooooney, and you have TM16 Icy Wind.
You can now use Whirlpool, once you find that in a dark cavern. Skate your way
to the bottom of the gym, then exit. One more badge to go!
==============================================================================
==============================================================================
==============================================================================
(c18)
--------------------------------------
@@ -0,0 +1,324 @@
---- 18 > Team Rocket Radio Tower ----
--------------------------------------
Video Playthrough Link (Team Rocket in Goldenrod City, & Route 44):
https://www.youtube.com/watch?v=UBCRvlP4Vqw
You will now get a phone call by Elm. This is informing you that Goldenrod City
has been overrun by Team Rocket! Wow! How could Team Rocket invade two cities at
once? It's truly a spectacular accomplishment for the villains of our story.
Heal at the Pokémon Center, then fly to the city on whatever bird Pokémon you've
decided can carry the weight of yourself and your bike.
|--------------|
|Goldenrod City|
|--------------|
You will now see that Team Rocket has pretty much overrun the whole city. You're
now 10 years old, so you're totally capable of retaking the city. Go inside the
Radio Tower. This is where all of the Team Rocket trouble is going on. Head
inside and towards the stairs to fight your first Rocket Grunt foe.
Rocket Grunt (M)
Level 24 Raticate (595 EXP)
Level 24 Raticate (595 EXP)
You get: 960G
Easy. Keep heading up the stairs to the second floor and head left.
Rocket Grunt (F)
Level 26 Arbok (819 EXP)
You get: 1040G
Talk to the male Rocket Grunt next to her for the third battle.
Rocket Grunt (M)
Level 21 Rattata (256 EXP)
Level 21 Rattata (256 EXP)
Level 23 Rattata (280 EXP)
Level 23 Rattata (280 EXP)
Level 23 Rattata (280 EXP)
You get: 920G
Keep moving left. You'll meet yet another Rocket Grunt that's supposed to be
guarding the place. But I don't see him moving his hands around. If he can't do
that, he definitely can't stop Yao Ming from dunking the ball.
Rocket Grunt (M)
Level 26 Zubat (300 EXP)
Level 26 Zubat (300 EXP)
You get: 1040G
He claims Team Rocket's comeback is nearing. Keep heading left.
Rocket Grunt (M)
Level 23 Grimer (442 EXP)
Level 23 Grimer (442 EXP)
Level 25 Muk (840 EXP)
You get: 1000G
Head up to the third floor. There are four trainers left to fight. Keep moving.
You like to move it move it.
Rocket Grunt (M)
Level 26 Weezing (963 EXP)
You get: 1040G
Head up towards the fourth floor.
Rocket Grunt (M)
Level 23 Koffing (561 EXP)
Level 23 Grimer (442 EXP)
Level 23 Zubat (265 EXP)
Level 23 Rattata (280 EXP)
You get: 920G
If you talk to him again, he notes that he failed his duties and will be docked
pay for this. Take that! Keep going to the right and you'll encounter a
scientist named Marc.
Scientist Marc
Level 27 Magnemite (514 EXP)
Level 27 Magnemite (514 EXP)
Level 27 Magnemite (514 EXP)
You get: 2700G
Two more trainers. Go up to the fourth floor. Keep going to another Rocket
Grunt.
Rocket Grunt (M)
Level 22 Zubat (253 EXP)
Level 24 Golbat (879 EXP)
Level 22 Grimer (423 EXP)
You get: 880G
Now go up to the scientist to fight him.
Scientist Rich
Level 30 Porygon (835 EXP)
You get: 3000G
Now, head up to the fifth floor. The battle against the mini-boss will begin
now.
Rocket Executive (M)
Level 30 Koffing (732 EXP)
Level 30 Koffing (732 EXP)
Level 30 Koffing (732 EXP)
Level 32 Weezing (1185 EXP)
Level 30 Koffing (732 EXP)
Level 30 Koffing (732 EXP)
You get: 2160G
You beat Team Rocket here, but it's not over yet. The Rocket Executive gives you
the Basement Key. The director is now in the Underground Warehouse now. He's
been kidnapped or something. Leave the Radio Tower and then heal at the Pokémon
Center. Then head to the northwestern part of town and enter the
northwesternmost building. This is the other Underground entrance.
Head down the stairs. While you're in the Underground, you can stop by the
Pokémon Salon to give Eevee a Happiness boost. Now, head down the hallway that
had the locked door you saw WAY back. Enter it now that you have the Basement
Key, then head down the stairs. Save now.
Try going left and your rival will meet you in another battle. He thinks he can
take care of Team Rocket alone and tries to get rid of you.
|------------------------------------------|
|Rival |
|Level 30 Golbat (1092 EXP) |
|Level 32 Starter Evolution (1425-1440 EXP)|
|Level 28 Magnemite (534 EXP) |
|Level 30 Haunter (810 EXP) |
|Level 32 Sneasel (904 EXP) |
|You get: 1920G |
|------------------------------------------|
If you're just leveling up your starter, any high-powered attack should do,
other than not using your same-type attack on the starter evolution and instead
picking a normal type move.
If you're using a balanced team, Sudowoodo's rock slides would work well against
Golbat and Magnemite. Gyarados would do well against the fire or water starters.
Use your starter's same-type attacks against Haunter. Against Sneasel, you can
use the strongest power moves or consider Sudowoodo's low kicks.
Your rival has no clue why he lost. He's not the sharpest tool in the shed. He
thinks of Lance's earlier comments and doubts himself. Yay! He still has his
dream that will never succeed. Now, go back and heal at the Pokémon Center, then
take on the Rocket Grunts. Believe me, there are so many Rocket Grunts that
you'll want to. I hope that by this point, you've learned to trust me. I am a
trustworthy person. After you heal, return and take on the first Rocket Grunt.
Rocket Grunt (M)
Level 27 Rattata (328 EXP)
You get: 1080G
Move on left.
Rocket Grunt (M)
Level 23 Muk (772 EXP)
Level 23 Koffing (560 EXP)
Level 25 Rattata (304 EXP)
You get: 1000G
Move on left...
Rocket Grunt (M)
Level 24 Koffing (585 EXP)
Level 24 Muk (807 EXP)
You get: 960G
Now, see the three switches next to each Rocket member you defeated? Go back to
the 1st trainer, and switch the one next to him, then the one next to the 2nd
trainer, then the one to the 3rd trainer. So, you should have turned on the
switches in the same order you beat the trainers. You can now go down from the
third trainer to fight an unexpected foe.
Burglar Eddie
Level 26 Growlithe (500 EXP)
Level 24 Koffing (585 EXP)
You get: 2112G
Now turn off switches 3 and 2 (the last two you did), and then snag the item you
see. It is a Full Heal. Exit to the stairs then come back up so the switches are
reset, and turn them on in the opposite direction (3, 2, then 1) from the left
to the right. This will FINALLY clear a route. Head down along the route and
you'll fight another burglar. You might ask, how did the burglars trap
themselves in the middle of a Team Rocket hideout? I don't know, I'm not a mind
reader! I can't read burglars' minds!
Thard_Verad observed, "How did the burglars get trapped in the Radio Tower
basement? Maybe they were sneaking around when a Grunt flipped the switches? :P"
Burglar Duncan
Level 23 Koffing (561 EXP)
Level 23 Koffing (561 EXP)
Level 25 Magmar (894 EXP)
You get: 2024G
Head right to fight the other Rocket Grunt.
Rocket Grunt (F)
Level 25 Gloom (706 EXP)
Level 25 Gloom (706 EXP)
You get: 1000G
OK, now go through the door, then keep going on the beaten path and eventually a
Rocket Grunt will spot you.
Rocket Grunt (M)
Level 24 Raticate (594 EXP)
Level 24 Golbat (879 EXP)
You get: 960G
Grab the Max Ether to the right and continue on the path up. Next battle!
Rocket Grunt (M)
Level 26 Grimer (501 EXP)
Level 23 Weezing (852 EXP)
You get: 920G
Now, head up the stairs to grab the Amulet Coin, which after equipped will be
able to DOUBLE the amount of gold you get from each battle on a Pokémon. You'll
finally be a rich 10-year old! Head back down the stairs and then follow the
path until you reach the boss of this freak place.
Rocket Grunt (M)
Level 25 Koffing (610 EXP)
Level 25 Koffing (610 EXP)
You get: 1000G
Talk to him again and he'll tell you they're basically holding on until the
return of Giovanni. Carry on and take TM35, which is next to the Director. Talk
to the Director who is now freed thanks to you. You receive a Card Key now.
Remember the part of the Radio Tower that hasn't been liberated by you yet? Now
it's time to retake that area. So your job is to save the Pokémon worldwide.
Exit by the way you came. When you get blocked, use the switch. Heal at the
Pokémon Center. Access the PC in here and deposit a few items from your first
pocket of your pack, cause it's obvious you'll be getting even more items.
Deposit EVERYTHING you won't be using IN any battles.
Then, return to the Radio Tower. Head up to Floor 2, then Floor 3. Head to the
very right part of the room where you'll spot a shutter thing. Go to the switch
and put in the key. Head right to face the first Rocket.
Rocket Grunt (M)
Level 24 Raticate (595 EXP)
Level 26 Koffing (634 EXP)
You get: 1040G
Go up the stairs to Floor 4, then fight the next Grunt.
Rocket Grunt (F)
Level 21 Ekans (279 EXP)
Level 21 Ekans (279 EXP)
Level 24 Gloom (678 EXP)
Level 23 Oddish (384 EXP)
You get: 960G
Attempt to go up to the fifth floor and you'll have your third battle. No one
apparently has a name in Team Rocket. They're all anonymous. They must remove
people's names at the time of initiation. Sad times.
Rocket Executive (M)
Level 36 Golbat (1318 EXP)
You get: 2592G
And so goes the Team Rocket fortress. Whee! Advance on to Floor 5 and fight the
same boss you fought before. It's amazing how easy Team Rocket is to beat,
right? Aww, they're such softies inside. ^_^. Here, you'll want to use primarily
the strongest attacks from your starter Pokémon, unless it's grass, in which
case you should use normal.
Rocket Executive (F)
Level 32 Arbok (1008 EXP)
Level 32 Murkrow (733 EXP)
Level 32 Vileplume (1261 EXP)
You get: 2304G
She still wishes you were part of Team Rocket. Why wouldn't you use your
strength to do bad things instead? But we all want things we can't have.
Go up to the other Executive. You may recall that Lance beat him last time. He
wants to call back Giovanni by taking over the Radio Tower. You'd think that
Team Rocket leaders could afford cell phones, but they're not the tech savvy
times. You can stop him with water, ground, rock, or fighting moves. Sudowoodo's
rock slides can be super effective.
Rocket Executive (M)
Level 33 Houndour (805 EXP)
Level 33 Koffing (805 EXP)
Level 35 Houndoom (1530 EXP)
You get: 2520G, Demise of Team Rocket
Like Giovanni, the Rocket Executive disbands Team Rocket. Well, good job! There
goes the menace of Team Rocket. The director will come up to greet and thank
you. He gives you a Clear Bell. Now you can exit the Radio Tower.
If you're just leveling up your Starter, your lineup should look something like
this:
Level 53 Starter Evolution
Level 30 Gyarados
Level 21 Sudowoodo
Level 20 Eevee
Level 21 Tentacool
Level 10 Kenya the Spearow
If a more balanced lineup, it should now look something like this:
Level 35 Starter Evolution
Level 34 Sudowoodo
Level 33 Gyarados
Level 33 Raticate
Level 32 Eevee
Level 9 Pidgey
==============================================================================
==============================================================================
==============================================================================
(c19)
-----------------------------------------------
@@ -0,0 +1,317 @@
---- 19 > Ice Path and Blackthorn City Gym ----
-----------------------------------------------
Video Playthrough Link (Team Rocket in Goldenrod City, & Route 44):
https://www.youtube.com/watch?v=UBCRvlP4Vqw
Video Playthrough Link (Ice Path): https://www.youtube.com/watch?v=evrwXY9s1uo
Video Playthrough Link (Blackthorn City Gym & Dark Cave):
https://www.youtube.com/watch?v=pWBRqXMdut8
|--------|
|Route 44|
|--------|
Pokémon on Route 44:
Poliwag (water)
Items on Route 44:
Burnt Berry
Ultra Ball
Take the elevator and stairs back down, leave the building, fly back to Mahogany
Town, heal at the Pokemon Center, and head east onto Route 44.
Psychic Phil
Level 24 Natu (565 EXP)
Level 24 Kadabra (807 EXP)
You get: 832G
Head up to grab the Burnt Berry from the tree, then the item east of that
trainer (Max Repel). Continue heading east and talk to the Fisherman. Also, you
get another call from your Mom. Should you really have trusted my advice at the
beginning of this walkthrough to give your mom access to your bank funds? No
comment.
Fisher Edgar
Level 25 Remoraid (417 EXP)
Level 25 Remoraid (417 EXP)
You get: 1000G
Keep heading east...
Cooltrainer Cybil
Level 25 Butterfree (854 EXP)
Level 25 Bellossom (985 EXP)
You get: 1200G
Keep heading east along this strange route after you beat her...
Cooltrainer Allen
Level 27 Chameleon (820 EXP)
You get: 1296G
Wow, that's two straight trainers that give you respect. You got game. Go pat
yourself on the back. Continue along the path, and then when you can go up, grab
the Ultra Ball. Then head along the northern route around the lake and you'll
find more trainers.
PoKeManiac Zach
Level 27 Rhyhorn (780 EXP)
You get: 1620G
Then talk to the fisherman to the west. He screams at you for losing a Poliwag
that he was trying to catch. Kind of sounds like it was his fault for not paying
attention, am I right?
Fisher Wilton
Level 23 Goldeen (546 EXP)
Level 23 Goldeen (546 EXP)
Level 25 Seaking (910 EXP)
You get: 1000G
Now head back east...
Bird Keeper Vance
Level 25 Pidgeotto (604 EXP)
Level 25 Pidgeotto (604 EXP)
You get: 600G
Now, you'll be heading into the Ice Path. Good luck with the puzzles! You got
this! By this, I mean a puzzle game in the middle of a role-playing game. Fly
back to Mahogany Town to heal at the Pokémon Center. Make sure you have a
Pokémon with HM Strength, because your 10-year old character isn't buff enough
to move boulders physically or with his mind. Then ride all the way east back
down to the route to a cave called the Ice Path.
|--------|
|Ice Path|
|--------|
Pokémon found in the Ice Path:
#042 Golbat
#124 Jynx
#220 Swinub
#225 Delibird (Silver only)
Items found in the Ice Path:
Full Heal
Iron
Nevermeltice
PP Up
HM07 Waterfall
TM44 Rest
The Ice Path has several puzzles. Follow the ice path. The first one's simple:
right, down, right, down. Then, go up the stairs, follow the route, go down the
stairs, follow the route, and you'll hit your first ice maze. Go up, left, up,
right, up, right, down, left, up, left, down, right, down, right, up, and then
right. Also, get ambushed by Golbats and Swinubs. Aren't those Swinubs so cute?
Maybe it's not so bad to get ambushed by them after all.
When you complete this first ice maze, get HM07 Waterfall. To do so, head right
(from the bottom square), up, right, left, down, left, and right to take the HM.
Afterwards, just go left, down, and left to get out of the area. HM07 Waterfall
is a good move that you can teach to water types later. Head up the stairway and
along the icy path. Head down the stairs and then down the ladder.
The next puzzle is in the next room. There are four boulders and four holes that
the boulders can fall into. You'll want a Pokémon with the HM Strength.
Let's get the first boulder down the southwest hole. Move the bottom boulder
right, then 6 squares down, 3 squares to the left, 1 square up, 1 square to the
left, and 2 squares up. There goes one boulder.
Now let's get the second boulder. Go all the way to the right and then up. Move
that boulder 1 square to the left, 4 squares down, 1 square to the right, 2
squares down, and then 5 squares down.
The bottom path has another boulder. You've moved the right one down, now let's
move the left one up. Push this boulder two up, then one left, and two up.
Simply push it two to the right, then up, and then left to smack it in. Fall
through the hole below it and then go left, up, and right to snag the item, Full
Heal! Head down the ladder then rock smash the rock near you. Grab the
Nevermeltice, then head right and go up, down the stairs, and with the ladder
going down, to the next stage.
Finally, there's the remaining one. Move that one two to the left, then three
up, then push that one back down one. Then push it left one and down one.
Head to the right side, and then you'll want to slide down to the part before
you fall off. Find the stairway, then go the opposite direction and slide down.
Slide right and you have the TM for Rest, TM44. Now go left and up, then go back
around. This time, go up the stairway and down the other one and you'll reach an
island of solid ice in which you can go up the ladder.
Move towards the right and down the stairway. Then go left, down, left, and then
down. Simply head left on the weird slippery ice path to get that Iron. Once you
head back out, go right, up, right, down, left, up, left, up, and then left.
Woot. You're done. Climb up the steps and go up the ladder.
Go up even more steps to get the Protein, then slide down at the very left to
grab a PP Up. Slide back up and drop anywhere in the middle of the area and then
go down from there. Walk out of the cave and finally, into Blackthorn City!
|---------------|
|Blackthorn City|
|---------------|
Head southwest and heal your Pokémon at the Pokémon Center. In the northeast
house, there's a lady that would trade her Rhydon for a Dragonair. In the house
in the south, the Move Deleter can delete HM moves from your Pokémon.
|---------------|
|Blackthorn City|
|---------------|
When you reach Blackthorn City, make sure you restock on Ultra Balls so you're
back up to 30 Ultra Balls. Then heal your Pokémon at the Pokémon Center and
deposit any extraneous items. In the northeast house, there's a lady that would
trade her Rhydon for a Dragonair. In the house in the south, the Move Deleter
can delete HM moves from your Pokémon.
Head up to the top of the city to reach the gym. This is a dragon-based gym, the
first of its kind!
If you're just leveling up your Starter, your lineup should look something like
this:
Level 57 Starter Evolution
Level 30 Gyarados
Level 22 Sudowoodo
Level 21 Tentacool
Level 10 Kenya the Spearow
Level 6 Bellsprout
If you have a balanced lineup, your team might look like this:
Level 36 Gyarados
Level 35 Raticate
Level 35 Quilava
Level 15 Dratini
Level 4 Wooper
Head left to fight your first trainer battle. If you are just using your starter
Pokémon, the HM move Strength should take out every trainer in the gym,
including the gym leader. The advice below would be for a more balanced lineup.
If you're doing that, Gyarados should do pretty good damage with that Dragon
Rage.
Cooltrainer Paul
Level 34 Dratini (487 EXP)
Level 34 Dratini (487 EXP)
Level 34 Dratini (487 EXP)
You get: 1632G
Move up above him and head up the stairs. You'll find out you need to move even
more boulders. Use any Pokémon that you have that has Strength. For the two
above you, move the right one up two, and the left one down two and into the
hole. Now head down to fight...
Cooltrainer Fran
Level 37 Seadra (1228 EXP)
You get: 1776G
You'll see two more boulders nearby. Push the right one down three, and the left
one up nine, then to the right two to make that fall. Head up to fight the last
dragon trainer on this level.
Cooltrainer Cody
Level 34 Horsea (604 EXP)
Level 36 Seadra (1195 EXP)
You get: 1728G
Push the boulder right next to you three to the right, then move the other one
down one. Easy. Now, head down the stairs you saw earlier next to the second
hole you encountered. Head two up, then to the left to fight that trainer.
Cooltrainer Mike
Level 37 Dragonair (1141 EXP)
You get: 1776G
Head across the boulder path you just created and then to the final trainer
before Clair.
Cooltrainer Lola
Level 34 Dratini (487 EXP)
Level 36 Dragonair (1110 EXP)
You get: 1728G
Now that you've finished beating everyone, use the warps to return to the
entrance to heal at the Pokémon Center, if needed.
Save before you battle, and make sure you have a few Revives, Full Heals, and
Hyper Potions in case you need it. Clair says she can hold her own against the
Elite Four as the dragon master. We'll see about that, my good friend!
---
GYM LEADER CLAIR
Level 37 Dragonair (1141 EXP)
Level 37 Dragonair (1141 EXP)
Level 40 Kingdra (1773 EXP)
Level 37 Dragonair (1141 EXP)
You get: Rising Badge, 4,000G, TM24 (Dragon Breath)
---
Strategy VERSUS Dragonairs: Use HM Strength to do a lot of damage, or Body Slam
if you have that on a Meganium. Try not to use grass, fire, or water moves, as
the Dragonairs resist those moves. Beware: Dragonairs can use Fire Blast, which
can do a decent amount of damage against a Meganium, and Thunder Wave, which can
paralyze your Pokémon from being able to attack.
Strategy VERSUS Kingdra: Kingdra is a water Pokémon that can use dragon moves
like Dragon Pulse. Grass and normal moves are ideal here from your starter.
This would be your 8th badge, but Clair won't give you it just yet. Apparently,
she's a sore loser and she won't give you the badge until you get the Dragon
Fang in the Dragon's Den. Now, leave the gym.
|------------|
|Dragon's Den|
|------------|
Pokémon in Dragon's Den:
#129 Magikarp
#147 Dratini
Items in Dragon's Den:
TM24 Dragonbreath
The Dragon's Den is located across the sea above the gym. Make sure you have in
your lineup a water Pokémon that can use the HMs Surf and Waterfall. Heal at the
Pokémon Center, then head in talking to the bodyguard in front of it. Does that
guy just stand out there every day? That's a pretty boring work shift. Go in,
head down the ladder, and go through the door, and then you'll get to battle.
Surf around counterclockwise from the left side and you'll spot a Whirlpool.
Remove it, then keep surfing eastward. Eventually, you'll find Dragon Fang just
lying on the ground. Clair will then come and give you the Rising Badge and TM24
(Dragon Breath). She insists that it's not her own breath. Maybe it's the breath
one of our Dragonairs. No one will ever know. It's a state secret.
Clair then gives you instructions to go to the Pokémon League, by heading from
New Bark Town and surfing east to the Pokémon League. She tells you that she'll
feel even worse if you lost at the Pokémon League because she lost to you so you
must win.
Now head back out the way you came and when you leave the cave (at the
entrance), Professor Elm will give you a call. If you swing by the lab, you'll
get something really awesome. However, Elm still has his last starter Pokémon
just sitting out there, waiting to be stolen by your thief Rival again. Elm
doesn't really know how to properly store his valuables. Head to the Pokémon
Center and heal your Pokémon.
==============================================================================
==============================================================================
==============================================================================
(c20)
----------------------------------------------
@@ -0,0 +1,193 @@
---- 20 > Ho-Oh (Gold) and Lugia (Silver) ----
----------------------------------------------
Video Playthrough Link (Whirl Islands to Lugia):
https://www.youtube.com/watch?v=cfCBHjF5hFU
Make sure you buy Ultra Balls at the Pokemart so you have at least 30 Ultra
Balls. You will need them to catch a rare legendary Pokémon and they have very
low catch rates. You should also consider having a few Revives and Hyper
Potions, too. Now, you can either head to Ecruteak City (if you have Gold
version) or Olivine City (Silver version). I will cover both below.
At this stage, you can only get Ho-oh or Lugia depending on what version you
have. You'll have a chance to catch the other later in the game.
Gold = Ho-oh
Silver = Lugia
Thard_Verad adds, "Apriballs - Three of them are broken: Fast Ball only works on
three species (NOT Raikou, Entei, or Suicune); Love Ball works on same gender,
not opposite; Moon Ball checks the wrong item and therefore never gets any
boosted catch rate. Level, Lure, Heavy, and Friend all work as intended."
|------------------------|
|Tin Tower - Gold Version|
|------------------------|
Pokémon in Bell Tower:
Rattata
Go back to Ecruteak City and then go into the Bell Tower entry house. Talk to
the man and he'll let you through because you have the Ecruteak City Badge. Then
follow up the trail to the tower itself.
Go up and to the left and another bald man will let you through because you have
the beautiful Rainbow Wing. You can then go up the ladder. Keep going and wild
Rattatas will start converging on you like zombies. Consider a Max Repel or two
to drive away all the zombies. Then go up the ladder, hop left for a Full Heal,
then hop all the right. Then hop all the way left, then keep hopping right. On
the middle platform, you might get stuck a bit because you don't see the next
jump easily, but it's the second square from the top. You'll make it to the
northeast side and you can go up the ladder. You are now a true bunny. Are you a
bad bunny?
On the next level, jump down, left, down, left, down, down to get to the Ultra
Ball. Jump right, down, left, down, down, and you'll get to a PP Up. If you go
up the ladder, it won't get you anywhere, so no need to do that. You can
continue with jumps left (x5), down, right (x2), down, left (x2), down, left
(x2) to get an Escape Rope. You'll be next to another ladder. That ladder will
also take you nowhere. Jump right, right, left, left, and you'll be on the
northwest side where you do want to take that ladder up.
On the next level, jump right twice, go all the way to the right and down, jump
down, left, left, and you'll get a Rare Candy. You can then return to the
previous level's bottom right corner and go all the way back up to the top left
corner to get back up to the beginning of the level. Then jump right and right
again, then down, left, down (x2), and take the right down (x4) up to the next
level (Left down takes you back to the bottom left ladder from the previous
level).
On the next level, go up and you'll get a Max Potion. Then go right across the
bridge, up, and then left across the upper (not middle) bridge to get a Full
Heal. Then hop down twice to get up the next ladder.
On the next level, go up, go through the next two warps, jump on the top side
left (x8), down (x3), right (x3), and through that warp to get to a Max Elixir.
Go back through the warp and jump right (x4) to return to the start. Warp back
through the top right warp and then back through the other warp you went
through. You'll be back to the very start of this paragraph. Hop to the right
(x4) to get a Max Revive. Then hop down (x5), go left and down, warp, cross the
bridge down, and warp again.
Go left and you'll get a HP Up. Keep going left and down and warp again. Then
cross the bridge up left and take the warp. Go up the ladder, cross the planks
down, then go up the ladder and you'll be in front of a Level 40 Ho-Oh. I
suggest that you save before you fight Ho-Oh because this is the only chance
you'll be able to get Ho-oh in the entire game. If you knock it out on a
critical hit, lose all your Pokémon and black out, or run out of balls to throw
at it, you would want to return to your save and retry.
The Ho-Oh in-game music gives off great vibes. As we all know, the vibes are
what matters. We must have the vibes. Ho-Oh is a fire/flying type Pokemon with
Safeguard and Ancient Power (damage w/ stat boosts), Punishment, and Sacred
Fire. Use your highest level Pokémon to whittle down Ho-Oh's HP first without
knocking it out, then try to throw as many Ultra Balls as you can at it.
Because the catch rate is only about 2% at low health, even with Ultra Balls,
this may involve a dozen or more resets. Ultimately, you'll want to maximize
your chances by throwing many Ultra Balls at it. You'll want to do so when it is
lower on HP (yellow or red on the HP meter), and ideally when it is asleep. Use
Revives and Hyper Potions as is helpful to keep your Pokémon alive to keep
throwing balls at it. However, all things even, focus on using Ultra Balls when
Ho-Oh is low on health, even when it could knock out your Pokémon, to increase
the number of chances yo have. You can slightly increase your catch chances if
you put Ho-Oh to sleep or paralyze it.
After you catch Ho-Oh, go back down the stairs and then you can use an Escape
Rope to leave. You can then fly back to Blackthorn City. Be sure to also
withdraw Ho-Oh at a Pokémon Center so that you have Ho-Oh as part of your
lineup.
|------------------------------|
|Whirl Islands - Silver Version|
|------------------------------|
Fly to Olivine City. Make sure you have a Pokémon on your team that has Flash,
like Bellsprout. Also make sure you teach HM06 Whirlpool to a Pokémon. I suggest
Tentacool or Krabby, who can use Strength, Surf, Whirlpool, and Waterfall.
You'll need eight badges in order to use Whirlpool.
Pokémon at the Whirl Islands:
#041 Zubat
#086 Seel
#098 Krabby
#116 Horsea
#117 Seadra
Items at the Whirl Islands:
Calcium
Escape Rope
Guard Specs
Max Revive x2
Ultra Ball
Surf southwest and west until you reach the northeast cave of the Whirl Islands.
I suggest using a Max Repel to reduce the number of Tentacool encounters on the
way there. The whirlpool is just northwest of where you fought Swimmer Kara. You
can press A on any whirlpool to remove it temporarily from the map. It's
incredible how much stamina these swimmers have, just staying in the water days
after you last fought them.
Once you find the northeast cave entrance, enter it and you'll see darkness. Use
HM Flash, then head up and leap the ledge to the right on that bike. You'll see
a Ultra Ball on the cliff. To get to it and continue on your journey, leap down
another ledge to the right, go up the steps, grab the Max Revive, and head down
the ladder. Head south, then jump to the left. Keep jumping down and you'll get
an Escape Rope. Woo!
Now go down and then left, then up the ladder. You can now head out the door to
breathe, but that's pointless. Why would you do that? Instead, surf east, then
grab the Guard Specs. Then head down the ladder, head up along the path, head
down, and then up the cliff. It's like a roller coaster, I know.
There's yet another item here. It's a Calcium. Now, on that cliff, head down to
the ladder and take it. Now, go down out the door. That's most of the items in
the Whirl Islands.
Now that you've gone from the northeast to the southeast portion of the Whirl
Islands, go counterclockwise and surf your way southwest out of the reefs and
then Whirlpool out. Head east, then north, and you'll find Swimmer Kara again.
Take that Whirlpool out and head back into the entrance that you originally came
from.
Use Flash again. Drop down the first cliff again, but this time, drop down the
top cliff rather than the bottom cliff. Then head down the ladder. Continue
along the path south and head down that ladder. Grab the Max Revive to the left
of you, then surf south. There are two items on the cliff but you can't reach
those.
Instead, surf down the waterfall, then head left and through the door. Head in,
and surf north to Lugia. Save before you interact with Lugia. I suggest that you
save before you fight him because this is the only chance you'll be able to get
Lugia in the entire game. If you knock it out on a critical hit, lose all your
Pokémon and black out, or run out of balls to throw at it, you would want to
return to your save and retry.
Because the catch rate is only about 2% at low health, even with Ultra Balls,
this may involve a dozen or more resets. Ultimately, you'll want to maximize
your chances by throwing many Ultra Balls at it. You'll want to do so when it is
lower on HP (yellow or red on the HP meter), and ideally when it is asleep.
Focus on using Ultra Balls when Lugia is low on health, even when it could knock
out your Pokémon. Use Revives and Hyper Potions as is helpful to keep your
Pokémon alive to keep throwing balls at it. You can slightly increase your catch
chances if you put Lugia to sleep or paralyze it.
Thard_Verad adds, "Lugia is just heavy enough for Heavy Balls to have higher
catch rates than Ultra Balls. Give those a try."
After you catch Lugia, you can use an Escape Rope to leave. You can then fly
back to Mahogany Town. Be sure to also withdraw Lugia at the Pokémon Center so
that you have Lugia as part of your lineup.
==============================================================================
==============================================================================
==============================================================================
(c21)
-----------------------------------------
@@ -0,0 +1,222 @@
---- 21 > Mount Mortar and Dark Cave ----
-----------------------------------------
Video Playthrough Link (Mt. Mortar): https://www.youtube.com/watch?v=eOMK3CvQT8Q
Video Playthrough Link (Blackthorn City Gym & Dark Cave):
https://www.youtube.com/watch?v=pWBRqXMdut8
Video Playthrough Link (Route 45 Part 1):
https://www.youtube.com/watch?v=y-5IXC6iwII
Video Playthrough Link (Route 45, 46, & Tohjo Falls):
https://www.youtube.com/watch?v=VGAzyc8aM8o
Video Playthrough Link (Route 27 Part 1):
https://www.youtube.com/watch?v=eL4FDqyquSY
Video Playthrough Link (Route 27 Part 2):
https://www.youtube.com/watch?v=dwPL_w2_xE0
Video Playthrough Link Route 26 & Victory Road):
https://www.youtube.com/watch?v=Jign264AzYI
Video Playthrough Link (Elite Four & Champion Battles):
https://www.youtube.com/watch?v=VcFLopf6A28
|-------------|
|Mahogany Town|
|-------------|
Head to the Pokémon Center and heal your Pokémon. Make sure you have Pokémon
that can use the HMs Fly, Flash, Surf, and Waterfall (and Strength if you want
to do the optional areas which can get you additional items). You'll also want
to clear up one spot (so you only have 5 Pokémon) so that you have space to
accept a Pokémon in Mt. Mortar. Also carry a few Max Repels and Escape Ropes,
which you can get from the Pokemart. Then, take Route 42 westward. Alternative,
fly to Ecruteak City, then take Route 42 eastward. Either way, you'll come
across three cave entrances to Mt. Mortar.
|----------|
|Mt. Mortar|
|----------|
Pokémon in Mt. Mortar:
#020 Raticate
#067 Machoke
#075 Graveler
#118 Goldeen
#119 Seaking
#236 Tyrogue (only one!)
Items in Mt. Mortar:
Carbos
Dragon Scale
Escape Rope x2
Full Restore
Hyper Potion x2
Max Ether
Max Potion
Max Revive
PP Up
Rare Candy
TM40 Aerial Ace
First, Mt. Mortar has three entrances: a west entrance, a middle entrance (Surf
required), and an east entrance. Regardless of which entrance you use, you'll
want to use HM Flash, or you won't be able to see anything.
The west entrance and east entrance lead to each other, as well as a giant room
where you can zig zag around, use Strength, and get two items, to include a
Hyper Potion and Escape Rope. I'd suggest using a Max Repel to get through this
section if you want to check it out, because there are a lot of random Pokémon
encounters.
Where you really want to go is through the central entrance, which requires your
surfing east from either the west entrance (easier), or by surfing west from the
east entrance. Even if you want to catch some of the Pokémon in Mount Mortar,
I'd still suggest using at least one Max Repel to avoid some of the numerous
random Pokémon encounters.
If you head up and down the ladder, then go down the steps, you can surf
clockwise around and reach a Hyper Potion and a Max Ether.
The path where you can get to a new Pokémon is in fact through the central
entrance, but instead of going up and down the ladder, you take one step to the
side, go as far left/right as you can, and then Surf upward. You'll spot a big
waterfall. Use Waterfall on it, then surf all the way to the top, get back on
land, and then head through the door.
Head back down the cliff, then go right. You'll hit a dead end, so surf to the
right. There's two routes. The left one takes you to a dead end but to a Max
Potion. Go all the way back to the entrance of this cave area, then surf left to
a Rare Candy. Go back to the lake where you can land to the left or to the right
and take the right path this time. Surf across that lake, go on the path all the
way to the left, then surf up and land again to the right.
Keep heading right. There's two routes. Take the bottom one to get TM40 first,
then go to the top one to continue. Head all the way up and surf up. Grab the
Escape Rope, then continue along the path around the bend to the left. When you
see an item, go around to grab the Dragon Scale, then continue heading left.
Head left all the way up the ladder...phew, that's FINALLY two floors. Two more
to go.
Run/bike down the steps, then head around and grab the free Iron on the cliff.
Head back to just before the steps and head east. Go along the path and take the
left intersection down the cliff. You'll see another item. Get the Max Revive,
then go back up and continue left. At the end of the path you can head down the
ladder.
Now that you're back on an isolated area of Floor 2, head east and talk to the
Karate King. If you beat him, you'll get to return to your beloved babysitting
duties with the award of a new Pokémon!
Blackbelt Kiyo
Level 34 Hitmonlee (1012 EXP)
Level 34 Hitmonchan (1020 EXP)
You get: 816G
After your victory, you will be given a free Tyrogue. This is the baby version
of Hitmontop, Hitmonlee, and Hitmonchan. If you go down the stairs from the
ladder you came through, you can move the rock with HM Strength to get out. But
make life easier and use one of the Escape Ropes you brought to get out of this
place.
|---------------|
|Blackthorn City|
|---------------|
Fly back to Blackthorn City. You'll now be heading down to the Dark Cave, which,
is, well, dark. Bring a Pokémon that can use HM05 Flash, then use it so you can
actually see what's in Dark Cave. Or just stumble around blindly. I guess it's
your prerogative.
Also, bring a Pokémon that can use HM03 Surf because your main character can't
swim across water. I guess at the young age of 10 years old, s/he just didn't
have the chance to take any swimming lessons. When you're ready, you'll want to
go south towards Route 45.
If you're just leveling up your Starter, your lineup should look something like
this:
Level 57 Starter Evolution
Level 30 Gyarados
Level 22 Sudowoodo
Level 21 Tentacool
Level 10 Kenya the Spearow
Level 6 Bellsprout
If you have a balanced lineup, your team might look like this:
Level 36 Gyarados
Level 35 Raticate
Level 35 Quilava
Level 15 Dratini
Level 4 Wooper
|---------|
|Dark Cave|
|---------|
Pokémon found in the Dark Cave:
#041 Zubat
#042 Golbat
#074 Geodude
#075 Graveler (it explodes and goes BOOM)
#129 Magikarp (only on water, it can't flop onto land)
#202 Wobbuffet
Items found in the Dark Cave:
Blackglasses
Full Heal
Hyper Potion
Revive
TM 13
Okay, instead of taking a Route 45 path, enter the Dark Cave to the left before
there are even any jumps. Also, bring a Pokémon that can use HM03 Surf because
your main character can't swim across water. I guess at the young age of 10
years old, s/he just didn't have the chance to take any swimming lessons.
Consider using a Max Repel at some point in Dark Cave. There are cool Pokémon to
catch, such as the rarer and wonderful Wobbuffett, but there are only so many
Geodudes you can look at.
Follow the route down to the sea, then surf down, then go up. Go left and down
the steps, then head left and up to that guy. He'll give you
Blackglasses because he wants you to forget that you saw him. Dark-type moves
are increased in power with it.
Now, you can use your best Tony Hawk skateboarding skills by jumping off ledges.
Jump all the way south and then head out the door to another part of the cave.
Surf down, then rock smash the rock next to you. Grab the Potion you see, then
head out the door onto Route 31. Fly back to Blackthorn City, heal at the
Pokemon Center, then head back south to actually take Route 45.
If your lineup leans on your starter, it might look something like this:
Level 57 Starter Evolution
Level 30 Gyarados
Level 22 Sudowoodo
Level 21 Tentacool
Level 10 Kenya the Spearow
Level 6 Bellsprout
If you're going for a balanced lineup, it might look something like this:
Level 15 Dratini
Level 5 Togepi
Level 10 Wooper
Level 36 Eevee
Level 36 Raticate
Level 36 Typhlosion
==============================================================================
==============================================================================
==============================================================================
(c22)
----------------------------------------
@@ -0,0 +1,394 @@
---- 22 > Routes 45, 46, 26, and 27 ----
----------------------------------------
Video Playthrough Link (Route 45 Part 1):
https://www.youtube.com/watch?v=y-5IXC6iwII
Video Playthrough Link (Route 45, 46, & Tohjo Falls):
https://www.youtube.com/watch?v=VGAzyc8aM8o
Video Playthrough Link (Route 27 Part 1):
https://www.youtube.com/watch?v=eL4FDqyquSY
Video Playthrough Link (Route 27 Part 2):
https://www.youtube.com/watch?v=dwPL_w2_xE0
Video Playthrough Link Route 26 & Victory Road):
https://www.youtube.com/watch?v=Jign264AzYI
Video Playthrough Link (Elite Four & Champion Battles):
https://www.youtube.com/watch?v=VcFLopf6A28
|--------|
|Route 45|
|--------|
Pokémon found in Route 45:
#074 Geodude
#075 Graveler
#207 Gligar (Gold only)
#216 Teddiursa (Gold only)
#227 Skarmory (Silver only, legendary catch rate)
#231 Phanpy (Silver only)
Items found in Route 45 & 46:
Berry
Dire Hit
Elixer
Max Potion
Mystery Berry
PRZCure Berry
Revive
X Special
There are two roads diverged in a yellow wood. If you take the one less traveled
by, it will make all the difference.
Haha, just kidding. But Routes 45 and 46 do have forks in the road. If you want
to fight every trainer and grab every item, you can fly back to Blackthorn City
to go down the other forks in the road.
LEFT PATH:
The left path is an entrance to the Dark Cave. You can keep going down for an
Elixir. There's also one trainer you can battle.
Hiker Parry
Level 29 Onix (670 EXP)
You get: 1856G
MIDDLE PATH:
The right path splits into both a middle path and a right path.
Hiker Erik
Level 24 Machop (385 EXP)
Level 24 Machop (385 EXP)
Level 27 Graveler (774 EXP)
You get: 1728G
RIGHT PATH:
Cooltrainer Ryan
Level 25 Pidgeot (920 EXP)
Level 27 Electrabuzz (901 EXP)
You get: 1296G
There's a Max Potion south of Ryan. There's one more trainer:
Cooltrainer Kelly
Level 27 Marill (920 EXP)
Level 24 Warortle (735 EXP)
Level 24 Warortle (735 EXP)
You get: 1152G
COMBINED PATH:
The path combines after Cooltrainer Kelly, then splits off again. The left path
has nothing, so take the right path.
RIGHT PATH:
Blackbelt Kenji
Level 28 Machoke (776 EXP)
You get: 672G
Talk to him again and he will give you his phone number. Look at you, getting
everyone's digits just like that. You're so cool. The path splits off again here
into a left and a right path.
LEFT PATH:
You can grab a Revive and a Nugget. Nom nom. Oh wait, these are gold nuggets,
not chicken nuggets. Please save them for resale instead. There are also two
trainers here.
Hiker Timothy
Level 27 Diglett (468 EXP)
Level 27 Dugtrio (885 EXP)
You get: 864G
Why do I do things? Because they're there. I guess it's a thing to do.
Camper Quentin
Level 27 Fearow (1041 EXP)
Level 30 Primeape (957 EXP)
Level 30 Tauros (1350 EXP)
You get: 600G
You can drop down the right side to grab the Mystery Berry.
RIGHT PATH:
Taking the right path eventually leads you to Trainer Bailey.
|--------|
|Route 46|
|--------|
Pokémon found in Route 46:
#019 Rattata
#021 Spearow
#074 Geodude
There are, again, the two paths. Upon reaching Route 46 you will notice an
entrance to a cave. This is the Dark Cave you saw WAY back in Route 31.
LEFT PATH:
Camper Ted
Level 17 Mankey
You get: 680G
You can pick up a Dire Hit to the left of Ted. You can go down to fight a
Picnicker.
Picnicker Erin
Level 16 Ponyta (520 EXP)
Level 16 Ponyta (520 EXP)
You get: 640G
Grab the Berry and PrzCure Berry to the left of it, then drop down just below
the cave.
RIGHT PATH:
You'll fight Hiker Bailey, a Geodude hoarder.
Hiker Bailey
Level 13 Geodude (238 EXP)
Level 13 Geodude (238 EXP)
Level 13 Geodude (238 EXP)
Level 13 Geodude (238 EXP)
Level 13 Geodude (238 EXP)
You get: 582G
Here, you can fly back up to Blackthorn City, heal at the Pokémon Center, and
take different paths down Routes 45 and 46 so that you can also face the other
trainers and get the other items. If you don't care about that or you've already
done that, you can just fly to New Bark Town to skip having to bike Route 29.
This is, by the way, how people stop getting exercise. They end up taking better
forms of transportation that make them lazy and they suddenly don't do cardio
anymore. Sad times.
|-------------|
|New Bark Town|
|-------------|
Talk to Professor Elm in his lab. He'll give you a Master Ball. You are the
lucky winner of the Master Ball sweepstakes. Since you have gotten eight badges,
you can now enter the Pokémon League. Yay! Now go into your own house and if you
talk to your mom, you can withdraw money (which is a whole fifth of what you've
gotten over time). Visit your room if you'd like. You've been gone from a long
time from home.
Now, fly back to Cherrygrove City and withdraw Togepi temporarily. Fly back to
New Bark Town, talk to Elm again, and you'll get an Everstone. Now fly back to
Cherrygrove City and put your original Pokémon back in the lineup. Also deposit
any items that you don't need in your PC. Head to the east of the town, then
surf eastward onto Route 27.
|--------|
|Route 27|
|--------|
Pokémon found on Route 27:
#020 Raticate
#024 Arbok
#028 Sandslash
#072 Tentacool (in water)
#077 Ponyta
#084 Doduo
#085 Dodrio
#195 Quagsire
Items found on Route 27:
TM22 Solar Beam
TM37 Sandstorm
Are you finished building up your Pokémon and with doing everything else you
want to do in Johto, at least for now? If so, it's time to head towards the
Elite Four. Bring the team you want that also has all the HMs you need.
Now surf eastward from New Bark Town. When you land on the other side, the first
guy you see will tell you that you've entered Kanto. If you've played the
original games (Red, Blue, or Yellow), you'll recognize that the land looks very
familiar. If you access your Pokegear, it will show that you are now in Kanto!
You'll see a cave entrance. Don't go in yet, because caves are scary! Instead,
surf southeast of the cave entrance and you'll find a Rare Candy. I'm not really
sure that you should be feeding your Pokémon candy off the shore of a lake. It
doesn't seem sanitary, but it's rare so I guess that makes it okay. I tend to
save my Rare Candies but they level up your Pokémon by one level. Surf back to
shore and then enter the cave into Tohjo Falls.
|-----------|
|Tohjo Falls|
|-----------|
Pokémon found in Tohjo Falls:
#019 Rattata
#079 Slowpoke
#119 Seaking
You'll notice a Waterfall. Head along the sea route and down the other
waterfall. Those are the falls. Did you expect more? I know, the name sounded
more grand than it really was. Now head out the door you see back to Route 27.
|--------|
|Route 27|
|--------|
A trainer will immediately battle you right after you exit the Tohjo Falls.
Cooltrainer Megan
Level 32 Bulbasaur (438 EXP)
Level 32 Ivysaur (966 EXP)
Level 32 Venusaur (1425 EXP)
You get: 1536G
Enter the house, and speak to the lady there. If one of your Pokémon is happy
she will give you TM37 Sandstorm. Then head east and surf across the lake.
Cooltrainer Blake
Level 33 Magneton (1138 EXP)
Level 31 Exeggcute (651 EXP)
Level 31 Quagsire (909 EXP)
You get: 1488G
Keeping head east and you'll battle a few more trainers.
Cooltrainer Brian
Level 35 Sandslash (1222 EXP)
You get: 1680G
Psychic Gilbert
Level 30 Starmie (1330 EXP)
Level 30 Exeggcute (630 EXP)
Level 34 Girafarig (1084 EXP)
You get: 1088G
Surf down from this trainer along the Coral till you find a Whirlpool. Remove
the Whirlpool then land on the island with the item.
Bird Keeper Jose
Level 35 Farfetch'd (705 EXP)
You get: 816G
You could exchange phone numbers with him, but you'll be out of room on your
list. Grab the item above him (TM22 Solar Beam), then surf back to the bridge.
Then keep heading along the path and you'll fight the final trainer of the
route.
Cooltrainer Reena
Level 31 Starmie (1374 EXP)
Level 31 Starmie (1374 EXP)
Level 33 Nidoqueen (1371 EXP)
You get: 1488G
After this battle, you can head east to Route 26.
|--------|
|Route 26|
|--------|
Pokémon found on Route 26:
#020 Raticate
#024 Arbok
#028 Sandslash
#077 Ponyta
#084 Doduo
#085 Dodrio
#195 Quagsire
Items found on Route 26:
Ice Berry
Max Elixir
Welcome to Route 26. Your first battle is against a fisherman. His name is
Scott.
Fisher Scott
Level 30 Qwilfish (642 EXP)
Level 30 Qwilfish (642 EXP)
Level 34 Seaking (1237 EXP)
You get: 1360G
Keep moving up the right path to fight more trainers.
Psychic Richard
Level 36 Espeon (1519 EXP)
You get: 1152G
Keep heading up till you find a house. Talk to the woman in there and she will
heal all your Pokémon for free. She's basically a one-person Pokémon Center.
Don't you just love free veterinarian health care? If you've ever had to pay vet
bills, you'll come to appreciate this lady very much. Give her a hearty round of
applause.
Cooltrainer Joyce
Level 36 Pikachu (631 EXP)
Level 32 Blastoise (1440 EXP)
You get: 1536G
Head up and you can grab the Ice Berry. You can move up further and then fight
another trainer who thinks he's cool. I think I'm pretty cool, but I don't go
around telling people that I am.
If you go down the left path at this point, you will find a house with Monica's
journal. Why are you snooping around? Okay, fine, I won't judge. It's a list!
Lists are amazing. It's all the trainers you can get swag from, along with the
days and locations that they will be:
Monday: Monica, Route 40
Tuesday: Tuscany, Route 29
Wednesday: Wesley, Lake of Rage
Thursday: Arthur, Route 36
Friday: Frieda, Route 32
Saturday: Santos, Blackthorn City
Sunday: Sunny, Route 37
Once you get back to where you were, the right path leads you to more trainers.
Cooltrainer Gaven
Level 32 Victreebel (1309 EXP)
Level 32 Krabby (1411 EXP)
Level 32 Flareon (1357 EXP)
You get: 1536G
Cooltrainer Jake
Level 33 Parasect (902 EXP)
Level 35 Golduck (1305 EXP)
You get: 1680G
You can go up, drop down the left side, and do your best BMX bike tricks, and
grab the Max Elixir. You are using your bike, right? You're singularly
responsible for that bike shop's business, so get on it! Afterwards, bike back
down and get your Pokémon healed again with the free health care lady. Then go
back up the right side past Jake and you'll encounter one more trainer.
Cooltrainer Beth
Level 36 Rapidash (1478 EXP)
You get: 1728G
If you were to talk to her again you could switch numbers, but it's probably out
of room. Keep heading up, and you will eventually find yourself in the checking
station to the Elite Four. The guard will check your badges and if you have all
eight will let you pass, head in the door and into Victory Road. You can also
pick up an Max Elixir if you take the jumps back to heal.
==============================================================================
==============================================================================
==============================================================================
(c23)
---------------------------
@@ -0,0 +1,99 @@
---- 23 > Victory Road ----
---------------------------
Video Playthrough Link Route 26 & Victory Road):
https://www.youtube.com/watch?v=Jign264AzYI
Video Playthrough Link (Elite Four & Champion Battles):
https://www.youtube.com/watch?v=VcFLopf6A28
|------------|
|Victory Road|
|------------|
Pokémon at Victory Road:
#042 Golbat
#075 Graveler
#111 Rhyhorn
#232 Donphan (Silver only)
Items found in Victory Road:
Full Heal
Full Restore
Max Revive
TM26 Earthquake
X Special
Victory Road is nothing like it used to be. This eastern half of Victory Road is
now much easier now. There are no puzzles to solve now. There are also no
trainers stalking you as you roam around the dungeon. Head up the stairway and
up the cliff. If you go right, you can pick up a Full Heal and Max Revive and
then return to the cliff. If you go left, you can keep going and then go up the
ladder. Have you ever wondered why the cliffs have guardrails and why dungeons
have ladders? It has been a mystery to me.
On the second floor, go right up the cliff, then head down. Going left takes you
to a X Special. Head right again and then up along the path, and that'll lead
you up to another ladder.
Save when you get to the third floor. Go up the cliff to your left, go down the
ladder, grab the Full Restore, and jump down. Head back up the ladder to the
third floor.
Now, take the path to the right. You'll spot a door to leave the cave. Before
exiting the cave, your Rival and challenge you to a rematch. He overconfidently
exclaims that you're so much weaker than he is even though he has never beaten
you in his life.
|------------------------------------------|
|Rival |
|Level 34 Sneasel (961 EXP) |
|Level 36 Golbat (1318 EXP) |
|Level 34 Magneton (1207 EXP) |
|Level 38 Starter Evolution (1693-1710 EXP)|
|Level 35 Haunter (945 EXP) |
|Level 35 Kadabra (1087 EXP) |
|You get: 2280G |
|------------------------------------------|
Strategy VERSUS Sneasel: If you're using your starter, use your highest power
move. If you're sticking with a balanced lineup, use Eevee and its quick attacks
to finish it off.
Strategy VERSUS Golbat: If you're using your starter, use your highest power
move. If you're sticking with a balanced lineup, use Raticate's
hyper fangs and a quick attack.
Strategy VERSUS Magneton: Magneton is a steel type, so it will absorb normal
moves. If you have Typhlosion, fire wheel is effective. Otherwise, stick with
the strongest move you've got.
Strategy VERSUS Starter Evolution: If you're using your starter, use your
highest power move that isn't an attack from your starter's type. If you're
using a balanced lineup, use Raticate and have it hit with hyper fans.
Strategy VERSUS Haunter: Use your highest power move that isn't a normal or
fighting attack, as those will be ineffective.
Strategy VERSUS Kadabra: Use your highest power move that isn't a fighting move.
Woop woop! Oh, sorry, I was imitating the victory celebration from a Wooper.
Congrats on beating your Rival one more time. Your rival finally understands he
sucks and all, but he hasn't given up hope. It's commendable, truly. That
arrogance really keeps him going. It's a beautiful story of a thief trying to
live his dreams. He retreats, and now you can advance out of the cave.
But before you do that, go left instead of out the door, and fall down the hole
to get TM26 Earthquake. Earthquake is one of the best ground moves in the game.
Then drop down the ledge and head back up the ladder to get back to the third
floor. Go back through the third floor to leave the dungeon with an extra TM in
your pocket.
==============================================================================
==============================================================================
==============================================================================
(c24)
-----------------------------
@@ -0,0 +1,296 @@
---- 24 > Pokémon League ----
-----------------------------
Video Playthrough Link Route 26 & Victory Road):
https://www.youtube.com/watch?v=Jign264AzYI
Video Playthrough Link (Elite Four & Champion Battles):
https://www.youtube.com/watch?v=VcFLopf6A28
|--------------|
|POKEMON LEAGUE|
|--------------|
Route 32 is very short, and you'll immediately be at the Pokémon League building
at the Indigo Plateau. You'll now be able to fly to this location directly. The
left side of the building is the Pokémon Center, while the right side is the
Pokemart. If you want to head back to Johto, you can talk to the old man with
the Abra next to the Pokémon Center portion of Indigo Plateau's HQ and Abra will
teleport you back.
Now, your goal is to take on five powerful trainers in a row. You mostly want to
have high-power moves of different types, plus priority moves (like Quick
Attack), status moves (like paralysis or sleep), and recovery moves (like
Recover or Leech Seed) in your lineup. Note that you do NOT need to have every
HM in your lineup in these fights, so you can leave Cut and Whirlpool off the
lineup for now.
You don't need any Pokémon that can use HMs. However, certain HMs are great to
teach to your Pokémon because they are also strong attacks. Surf is 95 Power,
Strength is 80 Power, Waterfall is 80 Power, and Fly is 80 Power. Consider
teaching these moves to every Pokémon in your six-Pokémon lineup if they can
learn them. You can go back to the Move Deleter in Blackthorn City later in the
game if you want to replace the moves later.
Here's a potential lineup in which you're just leveling up your starters, along
with their power levels:
Feraligatr - SURF (95), STRENGTH (80), Hydro Pump (120), Bite (60)
Lugia - Aero Blast (100/5 PP), SURF (95), FLY (80), Recover
Donphan - STRENGTH (80), Fury Attack (15x2-5), Rollout (30), Flail
Gyarados - Surf (95), WATERFALL (80), Strength (80), Bite (60 flinch)
Raticate - Quick Attack (40), Hyper Fang (80 flinch), STRENGTH (80), CUT (50)
Sudowoodo - Strength (80), Rock Throw (50), Low Kick (50 flinch), Flail
If you want a balanced lineup, your Pokémon will all need to be at least Level
40 or higher, because your next opponents will all have Pokemon from Level 40
through Level 50. The best training ground would be back in Victory Road,
although Routes 46 and 47 aren't bad, either. If you trade a Red Scale to Mr.
Pokémon, he will give you EXP Share, which will share all EXP that you gain with
every Pokémon in your lineup so you don't have to keep switching Pokémon back
and forth.
If you're leveling up an Eevee, put Eevee at the start of your lineup to
increase its happiness towards an Espeon/Umbreon evolution. Eevee can evolve to
Espeon (a psychic type) in the daytime, and an Umbreon (dark type) in the night
time. This can take a while as you can only the requisite happiness through
leveling up, walking around a lot, and visiting the Goldenrod City salon guy.
Goldenrod City also has extra TMs you can buy, so you can buy Thunder Punch and
Fire Punch and use it on Pokémon and still be able to reuse those TMs in the
future.
If you have a balanced lineup, your lineup to the Elite Four should be at least
something like the following:
Level 47 Espeon
Level 41 Dragonair
Level 40 Gyarados
Level 40 Sudowoodo
Level 40 Typhlosion
Level 38 Raticate
When you do feel ready for the ultimate challenge, make sure you have at least
ten Hyper Potions, ten Revives, and ten Full Heals. You can sell any Potions,
Super Potions, and Paralyz Heal/Awakenings etc. to make room for the more
versatile Hyper Potions and Full Heals.
You can't go back to the Pokémon Center to heal between battles, and no one will
heal your Pokémon, so having items on hand will be helpful for HP loss, KOs, or
paralysis situations. You've traveled across the land, searching far and wide.
Now it's time.
Fully heal your Pokémon and save after every trainer battle at the Elite Four.
Keep in mind that you cannot readily replenish your PP, as ethers are single
use. You'll want to balance out your PP so that you don't run out of any move
too early through the battles. Use type-advantaged moves were possible, and if
you can use a lower-powered move to knock out a Pokémon, do that. It is a battle
of endurance, and it is a grind. Allocate an hour to fighting the Pokémon
League. The trainers will sap your energy.
The first Elite Four member, Will, uses psychic Pokémon. Avoid fighting moves.
Dark moves will be effective.
|-----------------------------|
|Elite Four Will |
|Level 40 Xatu (1465 EXP) |
|Level 41 Jynx (1203 EXP) |
|Level 41 Slowbro (1440 EXP) |
|Level 42 Xatu (1539 EXP) |
|Level 41 Exeggutor (1861 EXP)|
|You get: 4200G |
|-----------------------------|
Strategy VERSUS Xatus: Use the most high-powered move that you've got that's not
a fighting move.
Strategy VERSUS Jynx: Use the most high-powered move that you've got. Jynx is
also an ice type, so you can also use fire moves.
Strategy VERSUS Slowbro: Use the most high-powered move that you've got. Slowbro
is also a water type, so you can also use a strong electric type move.
Strategy VERSUS Exeggutor: Use the most high-powered move that you've got.
Exeggutor is also a grass type, so you can also use fire moves.
Next up is Elite Four Koga. He was the poison type Fuchsia City Gym Leader in
Red, Blue, and Yellow, and now he's become an Elite Four member. Poison types
are super effective against grass types, so don't put a Meganium or other grass
type in. Instead, focus on using ground and psychic type moves.
|------------------------------|
|Elite Four Koga |
|Level 40 Ariados (1147 EXP) |
|Level 43 Forretress (1086 EXP)|
|Level 41 Venomoth (1212 EXP) |
|Level 42 Muk (1413 EXP) |
|Level 44 Crobat (1923 EXP) |
|You get: 4400G |
|------------------------------|
Koga has three Full Restores that he may use.
Strategy VERSUS Ariados - Ariados is both a poison and bug type. This means you
can fry Ariados with fire or flying type moves alongside the typical psychic
moves.
Strategy VERSUS Forretress - Use the most high-powered move that you've got
that's not a grass move. Psychic moves are super effective.
Strategy VERSUS Venomoth - Use the most high-powered move that you've got that's
not a grass move. Psychic moves are super effective. Since it's also a flying
type, you can also use electric or ice moves to take it down. Beware of
Venomoth's Confuse Ray. Not that you could stop it even if I told you it was
coming.
Strategy VERSUS Muk - Use the most high-powered move that you've got that's not
a grass move. Psychic moves are super effective. Muk has Toxic, so Muk can
poison your Pokémon.
Strategy VERSUS Crobat - Use the most high-powered move that you've got that's
not a grass move. Psychic moves are super effective. Since it's also a flying
type, you can also use electric or ice moves to take it down.
The third Elite Four member is Bruno. He has mostly fighting type Pokémon but
threw in a rock type in his lineup just for funsies. Fighting types are super
effective against normal, ice, rock, dark, and steel types. The best moves to
use against them are flying and psychic type moves. Avoid using dark moves
against fighting types.
|------------------------------|
|Elite Four Bruno |
|Level 42 Hitmontop (1242 EXP) |
|Level 42 Hitmonchan (1260 EXP)|
|Level 42 Hitmonlee (1242 EXP) |
|Level 43 Onix (994 EXP) |
|Level 46 Machamp (1902 EXP) |
|You get: 4600G |
|------------------------------|
Strategy VERSUS Hitmontop: The good news about battling fighting types is that
you can use pretty much any move against them. Use your
highest power move from your best Pokémon against it. Flying and psychic moves
also work well.
Strategy VERSUS Hitmonchan: Hitmonchan is still a fighting type, but it can be
more of a challenge to fight if it gets off its electric, fire, or ice punches.
The strategy otherwise holds. Use your highest power move from your best Pokémon
against it. Flying and psychic moves also work well.
Strategy VERSUS Hitmonlee: Hitmonlee's similar to Hitmontop. Use your highest
power move from your best Pokémon against it. Flying and psychic moves also work
well. Watch out for Swagger, which will confuse your Pokémon.
Strategy VERSUS Onix: Onix is the one rock type. Use water, grass, fighting,
ground, or steel moves to beat it. Use your highest power move from your best
Pokémon that isn't a normal move.
Strategy VERSUS Machamp: Machamp is the same as the other fighting types. Use
your highest power move from your best Pokémon against it. Flying and psychic
moves also work well.
You've got one more Elite Four member to face. She wants to be entertained by
you. I think you can handle that. Your Pokémon are quite entertaining and they
can entertain by winning the battle!
|-----------------------------|
|Elite Four Karen |
|Level 42 Umbreon (1773 EXP) |
|Level 42 Vileplume (1656 EXP)|
|Level 45 Gengar (1831 EXP) |
|Level 44 Murkrow (1008 EXP) |
|Level 47 Houndoom (2053 EXP) |
|You get: 4700G |
|-----------------------------|
Karen has one Full Restore that she can use.
Strategy VERSUS Umbreon: Umbreon is a dark type. It is weak to fighting and bug
moves and is immune to psychic moves. You should be able to just use your
highest-powered attack to beat Umbreon. Beware of Umbreon's Confuse Ray, which
will confuse your Pokémon.
Strategy VERSUS Vileplume: Vileplume is a grass and poison type. It is weak to
fire and psychic moves. You should be able to just use your highest-powered
attack to beat Vileplume, which may be flame wheels from Typhlosion if that was
your starter.
Strategy VERSUS Gengar: Gengar's a ghost type that's weak against ghost and dark
moves. Don't use any normal attacks on it because it has no effect. Otherwise,
use your highest-powered attack that's not a normal, poison, or bug move against
Gengar.
Strategy VERSUS Murkrow: Murkrow's a dark and flying type. Like Umbreon, it is
weak to flying and bug moves and immune to psychic moves. As a flying type,
Murkrow is also immune to ground moves. Otherwise, you can use your
highest-powered attack against Murkrow. Consider using ice or electric moves as
they are super effective.
Strategy VERSUS Houndoom: Houndoom's a fire and dark type who will be holding a
Sitrus Berry. You should be able to just use your highest-powered attack if it's
not a psychic, fire, or grass move.
Congrats on beating the Elite Four. Guess you're now more elite than them now!
Now, heal all your Pokémon. The final challenge for Pokémon League champion
begins. Three of Champion Lance's Pokémon are actual dragon types, while the
rest are essentially dragons but not dragon types.
|------------------------------|
|Champion Lance |
|Level 44 Gyarados (2017 EXP) |
|Level 47 Dragonite (2194 EXP) |
|Level 47 Dragonite (2194 EXP) |
|Level 46 Aerodactyl (1990 EXP)|
|Level 46 Charizard (2059 EXP) |
|Level 50 Dragonite (2335 EXP) |
|You get: 5000G |
|------------------------------|
Lance has three Full Restores on hand for the battle.
Strategy VERSUS Gyarados: Gyarados is two levels higher than in the original
game. Gyarados is a water and flying type, not a dragon type. Avoid using fire,
water, or ground moves against it. Use the highest powered move from your
highest level Pokémon otherwise. Electric and rock moves would be super
effective.
Strategy VERSUS Dragonites: Do not use fire, water, grass, or ground type moves
due to Dragonite's dual dragon/flying typing. You can otherwise use the highest
powered move of any other type from your highest level Pokémon. Ice and rock
types are particularly useful moves against the Dragonites, especially because
dragon and flying types are both weak against ice moves.
The Dragonites are strong. They have a powerful Hyper Beam move (although it
must recharge), it can paralyze your Pokémon with Thunder Wave, and it also has
strong Dragon Rush and Thunder attacks. The Level 50 Dragonite has Safeguard and
Outrage instead. Have Full Heals on hand
for the fight. One of the Dragonites may be holding a Sitrus Berry.
Strategy VERSUS Aerodactyl: Aerodactyl is a rock and flying type, not a dragon
type. Use the highest powered move from your highest level Pokémon otherwise.
Ice and water moves are ideal.
Strategy VERSUS Charizard: Charizard's weaknesses as a Fire/Flying type are
Water, Electric, and Rock, especially Water and Rock.
Congratulations! You have defeated the Elite Four! Lance doesn't seem angry that
he lost but rather, happy that you were the one to become the new champ. He
congratulates you.
After defeating the Johto League Elite Four members, you've beaten the first
half of the game. A news reporter named Mary wants to interview you, but Lance
shoos you into the other room to be inducted into the Hall of Fame. Guess Mary
won't get to interview you after all! Professor Oak also arrives just to hang
out. Lance will then enter your entire Pokémon lineup into the Hall of Fame,
even the ones that sat there on the bench and watched your Lv 66 starter do all
the hard work.
The credits will roll, while playing some snappy music, and finally the game
will say "The End". Don't turn your DS off yet, because the game isn't actually
over.
==============================================================================
==============================================================================
==============================================================================
(c25)
-----------------------------------------------
@@ -0,0 +1,246 @@
---- 25 > S.S. Aqua and Vermilion City Gym ----
-----------------------------------------------
Video Playthrough Link (Vermilion City & Saffron City Gyms):
https://www.youtube.com/watch?v=5Hsa1kt1CyM
|-------------|
|New Bark Town|
|-------------|
After the credits finish rolling, you will arrive back in New Bark Town. Go into
Prof. Elm's lab because he wants to show you something. He'll even waste
previous cell phone minutes to call you. Now that's dedication! Go to his lab,
where someone could still go and steal the last starter Pokémon that he hasn't
given to another 10-year old trainer. Prof. Elm will give you a S.S. Ticket!
|----------|
|Saint Aqua|
|----------|
Item on Saint Aqua:
Meal Coat
Welcome to the second half of the game. Now that you've beaten the Pokémon
League, you can go to Kanto, the world of the original Pokémon Red, Blue, and
Yellow games. You can visit all the cities again, and fight most of the gym
leaders again, too! Kanto is not particularly linear. We'll take a route that is
about minimizing travel fatigue and avoiding underleveled teams.
You have a decision point going into Kanto that you should be aware of. You'll
be relatively overleveled through most of Kanto. This means you can skip most of
the trainer battles, if you wanted to speedrun or get to your next game in the
backlog. However, there is a significant level spike at the end of the game.
Therefore, I would still suggest that you fight most or all the trainers, or the
end of the game could be a challenging one for you.
A balanced lineup that you could take to Kanto is:
Level 49 Espeon
Level 42 Typhlosion
Level 41 Dragonair
Level 41 Sudowoodo
Level 40 Gyarados
Level 38 Raticate
Fly to Olivine City. Go south from the Pokémon Center to a docking pay called
the Olivine Port Fast Ship Pier. Go through the door, take the stairs down
twice. Then keep going down and talk to the guy at the entrance to board the
Fast Ship S.S. Aqua. As with the S.S. Anne from the Red, Blue, and Yellow games,
you'll fight Pokémon trainers bored with sea travel while the ship sails.
The ship will begin sailing to Kanto. Go down a few steps and an old man will
bump into you. He tells you that his granddaughter is missing. Head left into
the room you see, and you'll start fighting trainers.
Hiker Noland
Level 31 Sandslash (1081 EXP)
Level 33 Golem (1251 EXP)
You get: 1056G
You'll see three other cabins on the top part of the ship. The cabin on the far
left is yours, and you can go to that cabin's bed heal all your Pokémon. No
other cabin on the north side currently has trainers. You can head to the bottom
half of the ship and down to the left side to fight another trainer.
Firebreather Lyle
Level 28 Koffing (684 EXP)
Level 31 Flareon (1314 EXP)
Level 28 Koffing (684 EXP)
You get: 1344G
If you head two cabins to the right, you'll find another trainer to battle.
PoKeFan Colin
Level 32 Delibird (1254 EXP)
You get: ???G
The same cabin also has twin trainers.
Twins Meg & Peg
Level 31 Teddiursa (823 EXP)
Level 31 Phanpy (823 EXP)
You get: 620G
Head to the cabin to the right and you'll speak to the old man again. Go
downstairs (next to his room) and a sailor will block you like he's blocking you
from dunking on him. Talk to him and you'll learn that there's a sailor who's
not pulling his weight at work. Go back down towards your cabin but into the
cabin that's next to yours. Apparently a sailor has been napping on the job.
That's wage theft!
Sailor Stanly
Level 31 Machop (583 EXP)
Level 33 Machoke (1032 EXP)
Level 26 Psyduck (445 EXP)
You get: 1040G
Defeat the sailor and he'll return to work. Then go back downstairs and the
sailor boss will let you pass. He also tells you that a little girl passed by
there as if it wasn't his responsibility to make sure little children don't run
around the whole ship. Head left, then down, and you'll face another trainer.
Juggler Fritz
Level 29 Mr. Mime (844 EXP)
Level 29 Machoke (906 EXP)
Level 29 Magmar (1036 EXP)
You get: 1160G
Head up, left, and then go down into the next "room."
Sailor Jeff
Level 32 Raticate (795 EXP)
Level 32 Raticate (795 EXP)
You get: 1280G
Now head up and left and talk to the girl that's just staring at a wall for some
reason. She tells you that she's soooo bored that she'll battle you.
Picnicker Debra
Level 33 Seaking (1201 EXP)
You get: 660G
Head down from Debra and up the stairs. Then go through the door and talk to the
girl. She'll find out that her grandpa is worried about her. Then she teleports
back to her grandfather's cabin and the grateful grandfather gives you a Metal
Coat. Okay, that wasn't really teleporting, but time travel is just as
wonderful. Now, the ship will arrive in the Kanto. Go back up to where you
entered and the guy will stop blocking the door.
|--------------|
|Vermilion City|
|--------------|
Item in Vermilion City:
Rare Candy
Now that you get off the S.S. Aqua, and you find yourself at Vermilion City! Go
down the stairs, then up the stairs and out the door. You'll see your first
glimpse of the Vermilion City three years later. Follow the route north and
you're out of the Vermilion Port. Note the new Pokémon music for Kanto. It's an
interesting remix. The Fast Ship sails Wednesday and Sunday if you want to
return for another cruise.
North of the gym is the Pokémon Club house. As with the original games, the
Chairman of the Pokémon Fan Club is there. If you listen to him, he'll babble on
forever about his wonderful Pokémon. Eventually, he'll give you a Rare Candy as
compensation for listening to him express his feelings.
The northwest part of town has the Pokémon Center. Heal, make sure you have a
Pokémon with Cut, and cut down the tree blocking the gym. Look around and you'll
see that the gym looks pretty much the same and other features from Red, Blue,
and Yellow are still here.
Anyway, there's a power outage or something, because his usual puzzle from
Red/Blue/Yellow is gone. There are three trainers before Lt. Surge. This is a
gym with mostly electric Pokémon where you can usually just use the most
high-powered moves, although the Magnemites and Magnetons are also steel types.
Fire and fighting moves are preferred against them.
Guitarist Vincent
Level 27 Magnemite (514 EXP)
Level 33 Voltorb (727 EXP)
Level 32 Magnemite (609 EXP)
Level 32 Magnemite (609 EXP)
You get: 1024G
Before you can even move, you'll face an Electrode-hoarding trainer who likes
his Electrodes to go boom boom. You can either knock out all the Electrodes in
one hit before they explode, or use a rock type that can withstand the
self-destructing nature of these ball-shaped Pokémon.
Juggler Horton
Level 33 Electrode (1060 EXP)
Level 33 Electrode (1060 EXP)
Level 33 Electrode (1060 EXP)
Level 33 Electrode (1060 EXP)
You get: 1320G
Head right, then up to face the next trainer.
Gentleman Gregory
Level 37 Pikachu (649 EXP)
Level 33 Flaaffy (826 EXP)
You get: 2376G
You can heal before your gym battle or go straight into it, but save first. He
says he'll zap you like he did in war against his enemies. Every Pokémon can
confuse or paralyze your Pokémon, so try to avoid using two-turn moves that are
doubly affected by that if paralysis or confusion is in effect.
---
GYM LEADER LT. SURGE
Level 44 Raichu (1149 EXP)
Level 40 Magneton (1380 EXP)
Level 46 Electrabuzz (1537 EXP)
Level 40 Electrode (1285 EXP)
Level 40 Electrode (1285 EXP)
You get: Thunder Badge, 4600G
---
Lt. Surge has a Full Restore that he can use to fully restore the health and
status of one of his Pokémon.
Strategy VERSUS Raichu: Use the most high-powered move from your highest level
Pokémon that. Alternatively, use rock or ground moves which are super effective
against electric Pokémon. Ground Pokémon will be immune to its electric attacks.
Raichu has Thunder Wave, and will likely use it to paralyze your Pokémon. If
your Pokémon is paralyzed, you may want to avoid using a two-turn move because
the chances of that connecting are significantly reduced.
Strategy VERSUS Magneton: Use the most high-powered moves from your highest
level Pokémon. Ground Pokémon will be immune to its electric attacks. Magneton
is also a steel type, so fire and fighting moves are ideal. Magneton can use
Supersonic, which can confuse your Pokémon. If your Pokémon is paralyzed or
confused, you may want to avoid using a two-turn move because the chances of
that connecting are significantly reduced.
Strategy VERSUS Electrabuzz: Use the most high-powered move from your highest
level Pokémon that. Alternatively, use rock or ground moves which are super
effective against electric Pokémon. Ground Pokémon will be immune to its
electric attacks. Electabuzz has Thunder Wave, and will likely use it to
paralyze your Pokémon. If your Pokémon is paralyzed, you may want to avoid using
a two-turn move because the chances of that connecting are significantly
reduced.
Strategy VERSUS Electrodes: Use the most high-powered move from your highest
level Pokémon that. Alternatively, use rock or ground moves which are super
effective against electric Pokémon. Ground Pokémon will be immune to its
electric attacks. Electrode has Thunder Wave, and will likely use it to paralyze
your Pokémon. Electrode may use Double Team, an annoying move that increases
evasion. If your Pokémon is paralyzed, you may want to avoid using a two-turn
move because the chances of that connecting are significantly reduced,
especially if Electrode has already used Double Team.
Lt. Surge will give you the Thunder Badge and 4,600G. Thunder Badge increases
Pokémon speed. Yay! Congrats on your first badge in Kanto. You are the hero of
your own story.
==============================================================================
==============================================================================
==============================================================================
(c26)
-------------------------------
@@ -0,0 +1,135 @@
---- 26 > Saffron City Gym ----
-------------------------------
Video Playthrough Link (Vermilion City & Saffron City Gyms):
https://www.youtube.com/watch?v=5Hsa1kt1CyM
|-------|
|Route 6|
|-------|
Heal at the Pokémon Center, then head north onto Route 6. Route 6 is kind of a
barren wasteland. There's a large man who blocks the entire entrance to the
Underground Path. He looks like he can take you down because you're too weak to
move your own boulders. He says that the Power Plant has issues right now, so
you can't take the Magnet Train between Saffron City to Goldenrod City yet. So
make your way all the way up through the checkpoint house and you'll be into
Saffron City.
|------------|
|Saffron City|
|------------|
Items in Saffron City:
Focus Band
TM29 Psychic
Ride your trusty bike to the southeast side of Saffron City, which is the
Pokémon version of Tokyo. There's a house labeled Mr. Psychic's House. Mr.
Psychic is there. He does some magic tricks and gets you TM29 Psychic, one of
the best Psychic moooves in the game.
Next, enter Silph Co. This is the multi-story building above the Pokémon Center
where the main character battles Team Rocket in Red, Blue, and Yellow. Talk to
the man blocking the pathway up to the first floor and he'll give you Up-Grade.
It's amazing how many people are employed in this land that just block doorways
and stairways.
On the northwest side of town is the Magnet Train, which you can take between
Johto and Kanto. It's currently closed because the Power Plant is down.
Now, head out and go to the upper-right corner of Saffron City. There's a
PokeMart if you want to buy or sell items. You're probably low on item space at
this point, and your bike takes up the same amount of room as a tiny potion in
your backpack. Ah, such is life. Further north, you'll spot two gyms. The former
fighting gym has a Focus Band you can pick up and attach to a Pokémon for
battle.
Afterwards, head to the gym on the right. It's Saffron City's official gym.
You'll want to avoid using fighting moves there, while high-powered and dark
moves are going to be your friends. Your playable character doesn't really have
friends other than those trainers who call you out of the blue to tell you about
how they've dressed up their Nidoran in pretty clothing, so you take what you
can get. The teleporter puzzles are back from the Red, Blue, and Yellow games.
Medium Rebecca
Level 35 Drowzee (765 EXP)
Level 35 Hypno (1237 EXP)
You get: 1400G
Head left through the teleporter, then fight Psychic Franklin.
Psychic Franklin
Level 37 Kadabra (1149 EXP)
You get: 1184G
Head left through the teleporter and fight your next Psychic.
Psychic Jared
Level 32 Mr. Mime (735 EXP)
Level 32 Exeggcute (672 EXP)
Level 32 Exeggcute (672 EXP)
You get: 1120G
Head left through the teleporter, then right to another medium. She can see into
your soul, because that's the kind of thing you tell strangers the first time
you talk to them.
Medium Doris
Level 34 Slowpoke (720 EXP)
Level 36 Slowbro (1264 EXP)
You get: 1440G
So, you've got two choices here. You can go back and heal, or not go back and
heal. I know, so many choices. If you want to go straight into the fight, just
go to the bottom left teleporter and you'll go straight to the gym leader.
If you want to go back to the Pokémon Center to heal first, embrace the maze
life. Head right through the teleporter, then left, then left/right (whichever
way works) till you find that one trainer again (Franklin), then go the opposite
teleporter direction to outside the gym to heal at the Center, then head back
in. I know, I got confused even trying to explain it. After healing, you'd head
through the 1st teleporter, then go up, left, down, and then down.
If you have a balanced lineup, it might look something like this:
Level 49 Espeon
Level 43 Sudowoodo
Level 43 Typhlosion
Level 42 Raticate
Level 41 Dragonair
Level 41 Gyarados
You're now at Sabrina. She says, “I knew you were coming. Three years ago I had
a vision of your arrival.” That's pretty cool. Maybe she can read you your
fortune without you having to pay for another tarot card reader. Those readings
sure don't come cheap.
---
GYM LEADER SABRINA
Level 46 Espeon (1941 EXP)
Level 46 Mr. Mime (1339 EXP)
Level 48 Alakazam (1912 EXP)
You get: Marsh Badge, 4800G
---
Strategy VERSUS Espeon: Use your highest powered move, ideally with your
starter. You could also use a Pokémon that bites. Nom nom.
Strategy VERSUS Mr. Mime: Use your highest powered move, ideally with your
starter.
Strategy VERSUS Alakazam: Use your highest powered move, ideally with your
starter.
Sabrina says that your power far exceeds what I foresaw. You now have 10 badges!
She'll give you a Marsh Badge and 4,800G. Now to get out of the gym, head out
the only teleporter, then go right, right, up, and right.
==============================================================================
==============================================================================
==============================================================================
(c27)
------------------------------------------------
@@ -0,0 +1,304 @@
---- 27 > Power Plant and Cerulean City Gym ----
------------------------------------------------
Video Playthrough Link (Routes 9-10 & 24, Rock Tunnel, & Kanto Power Plant):
https://www.youtube.com/watch?v=Gr2hFv0JgM8
Video Playthrough Link (Route 25, Cerulean City Gym, Lavender Town, & Celadon
City Gym): https://www.youtube.com/watch?v=bxbOG0kR4QE
|-------|
|Route 5|
|-------|
Items on Route 5:
Cleanse Tag
Heal at the Pokémon Center, then head north through the route-changing station
and north onto Route 5. There are no trainers here, just that man who's blocking
the entire door to the Underground Path. If you bike all the way up to Cerulean
City, you can hop down some ledges on your BMX bike to the old Daycare Center
from Red, Blue, and Yellow. Inside is a lady who will give you a Cleanse Tag to
ward off shadows. Now, actually head north into Cerulean City.
|-------------|
|Cerulean City|
|-------------|
You can talk to the people in Cerulean City but no one will give you any items
here. The Bike Shop from Red, Blue, and Yellow has moved to Goldenrod City.
That's why you have to show off your bike in Kanto for the bike sales of a shop
owner that you don't really care about, but you like getting free things.
Enter the Cerulean City Gym and go up and you'll find that no one is there.
Where's Misty? Make sure you have Pokémon that can use Cut and Surf, then head
east onto Route 9 through the northeast part of town.
|-------|
|Route 9|
|-------|
Pokémon found on Route 9:
#019 Rattata
#020 Raticate
#021 Spearow
#022 Fearow
#056 Mankey (Gold only)
#057 Primeape (Gold only)
Cut the tree at the entrance. Keep heading east and you'll fight some trainers.
Picnicker Edna
Level 30 Nidorina (751 EXP)
Level 34 Raichu (888 EXP)
You get: 680G
Go above her and you'll find another trainer.
Camper Sid
Level 32 Dugtrio (1048 EXP)
Level 29 Poliwrath (1149 EXP)
Level 29 Primeape (925 EXP)
You get: 580G
Head back down and right to another trainer.
Camper Dean
Level 33 Golduck (1230 EXP)
Level 31 Sandslash (1081 EXP)
You get: 620G
Head right on the green, bottom path for a bit and you'll face a hiker.
Hiker Sidney
Level 34 Dugtrio (1114 EXP)
Level 32 Onix (739 EXP)
You get: 1024G
Head back up to Camper Dean, and keep going up and to the right to another
hiker.
Hiker Tim
Level 31 Graveler (889 EXP)
Level 31 Graveler (889 EXP)
Level 31 Graveler (889 EXP)
You get: 992G
Head east for your final battle on the route.
Picnicker Heidi
Level 32 Skiploom (931 EXP)
Level 32 Skiploom (931 EXP)
You get: 640G
Head further east and you'll end up on the northern part of Route 10.
|--------|
|Route 10|
|--------|
Pokémon found on Route 10:
#020 Raticate
#021 Spearow
#022 Fearow
#100 Voltorb
#118 Goldeen (Water)
#119 Seaking (Water)
#125 Electabuzz
#195 Quagsire
There's a Pokémon Center in the middle of nowhere. The Rock Tunnel is to your
left, but don't enter just yet. Instead, head back up and surf down the river
until you hit a dead end and you'll be at the Power Plant.
|-----------------|
|Kanto Power Plant|
|-----------------|
Talk to the power plant employees, including the manager on the right side.
Someone stole the Machine Part powering the Magnet Plan. Some are depressed, and
others are furious. Thievery!! As you try to leave, they'll learn that a
mysterious man in Cerulean that may have done the thievery. Fly back to Cerulean
City.
|-------------|
|Cerulean City|
|-------------|
Head into the Cerulean City gym and a Team Rocket member will run into you.
He'll speak in broken English. It's pretty funny. He'll leave the gym. Head up
to Route 24.
|--------|
|Route 24|
|--------|
Pokémon on Route 24:
#063 Abra
#069 Bellsprout
#070 Weepinbell
You'll find the Team Rocket guy, with his strange grammar, on what was the
Nugget Bridge. He's pretty easy to beat, but what a fun guy.
Rocket Grunt
Level 30 Golbat (1098 EXP)
You get: 1200G
After that ridiculously easy victory, he'll tell you to look for the Machine
Part in the middle of the Cerulean Gym. You could go to the gym now, but it
makes more sense to keep walking up to Route 25.
|--------|
|Route 25|
|--------|
Pokémon on Route 25:
#063 Abra
#069 Bellsprout
Items on Route 25:
Nugget
Potion
Protein
If you take the northwest path, you'll find a Potion under a bush. Can you
really trust medication from a random bush? No harm, no foul, I guess. You can
then head east where six schoolyard bullies want to beat you up under the guise
of giving you a prize.
Schoolboy Dudley
Level 35 Oddish (585 EXP)
You get: 1120G
Lass Ellen
Level 30 Wigglytuff (700 EXP)
Level 34 Granbull (1296 EXP)
You get: 816G
Schoolboy Joe
Level 33 Tangela (1173 EXP)
Level 33 Vaporeon (1386 EXP)
You get: 1056G
Lass Laura
Level 28 Gloom (792 EXP)
Level 31 Pidgeotto (750 EXP)
Level 31 Bellossom (1221 EXP)
You get: 744G
Before you can even move, there comes the fifth trainer, pouncing on his prey.
Just kidding, he's actually the prey.
Camper Lloyd
Level 34 Nidoking (1420 EXP)
You get: 680G
Lass Shannon
Level 29 Paras (435 EXP)
Level 29 Paras (435 EXP)
Level 32 Parasect (877 EXP)
You get: 768G
Where's the prize? Did they lie to you? That's really mean. Keep going right and
you'll fight some random person who was just waiting for your Pokémon to get
weakened before fighting you.
Super Nerd Pat
Level 36 Porygon (1002 EXP)
You get: 1152G
After you beat the Super Nerd, keep going right and that guy will give you a
Nugget. They sell for 5,000G at the Pokemarts.
Cut the tree above Pat and then grab a free Protein. Head on east to what was
Bill's place in Red, Blue, and Yellow. You'll see that Misty is on a date. Ooh
la la. The guy quickly runs away when he sees you. You could outrun him on a
bike but you're a kind soul who wouldn't do such a thing. But Misty is pissed.
You're a pest now. But she does spot your shiny Johto badges and say that she
would battle you.
Now, enter Bill's House and you'll find his grandfather there. We don't know
where Bill is, but at least he didn't turn into a Pokémon. If you show his
grandfather certain Pokémon like Growlithe, Lickitung, Oddish, Pichu, or Staryu,
he'll give you the respective rewards of a Fire Stone, an Everstone, a Leaf
Stone, a Thunder Stone, and a Water Stone.
Now, fly back to Cerulean City.
|-------------|
|Cerulean City|
|-------------|
Item in Cerulean City Gym:
Machine Part
Heal at the Pokémon Center. Then head into the Cerulean City Gym. This is a
water type gym, so grass and electric type moves work well. Or you can just use
your highest-powered moves throughout the gym. Head on the left path to fight
the first swimmer.
Swimmer Briana
Level 35 Seaking (1275 EXP)
Level 35 Seaking (1275 EXP)
You get: 700G
You'll be next to a lake that is four squares (2x2). Press A on the top left
water square and you'll get the Machine Part that the Team Rocket Grunt
previously hid. Now, head on the right path to face another swimmer.
Swimmer Parker
Level 32 Horsea (568 EXP)
Level 32 Horsea (568 EXP)
Level 35 Seadra (1162 EXP)
You get: 280G
Move up to take on the final trainer before Misty.
Swimmer Diana
Level 37 Golduck (1378 EXP)
You get: 740G
If you need to heal at the Pokémon Center, do so. In any case, you can now fight
Misty for the 11th gym badge, you pest.
---
GYM LEADER MISTY
Level 42 Golduck (1566 EXP)
Level 42 Quagsire (1233 EXP)
Level 44 Lapras (2064 EXP)
Level 47 Starmie (2083 EXP)
You get: Cascade Badge, 4,700G
---
Strategy VERSUS Golduck: It seems that Misty has finally evolved her Psyduck.
Golduck has a move that can disable one of your Pokémon's four moves. Use your
highest powered move, ideally with your starter. You can also use grass or
electric moves. Don't use fire or water moves.
Strategy VERSUS Lapras: Use your highest powered move, ideally with your
starter. You can also use grass or electric moves. Don't use fire or water
moves.
Strategy VERSUS Starmie: Use your highest powered move, ideally with your
starter. You can also use grass, electric, or dark moves, as Starmie is also a
psychic move. Don't use fire, water, or fighting moves.
Strategy VERSUS Quagsire: Use your highest powered move, ideally with your
starter. You can also use grass or electric moves. Don't use fire or water
moves. Beware of Quagsire's Earthquake if you have Typhlosion.
After Misty falls, you get your 11th badge and 4,700G. Nice!
==============================================================================
==============================================================================
==============================================================================
(c28)
-----------------------------------------------
@@ -0,0 +1,313 @@
---- 28 > Rock Tunnel and Celadon City Gym ----
-----------------------------------------------
Video Playthrough Link (Route 25, Cerulean City Gym, Lavender Town, & Celadon
City Gym): https://www.youtube.com/watch?v=bxbOG0kR4QE
|-----------|
|Power Plant|
|-----------|
Item in Power Plant:
TM07 Zap Cannon
Heal at the Pokémon Center and make sure you have a Pokémon in your lineup that
has the HMs Cut, Surf, and Flash. Head east on Route 9 and surf to the Power
Plant again. Head in and talk to the Manager, and he'll give you a reward for
the Machine Part: TM07 Zap Cannon! Now surf back up to Route 10 and enter Rock
Tunnel this time.
|-----------|
|Rock Tunnel|
|-----------|
Pokémon in Rock Tunnel:
Cubone
Geodude
Kangaskhan
Zubat
Items in Rock Tunnel:
Iron
PP Up
Revive
TM47 Steel Wing
You'll want to use HM Flash to get through the Rock Tunnel. Head along the path
to the right and go down the ladder. Then head left. Keep heading left at the
intersection to get an Iron, then head back and up and to the right on the curvy
path.
Head up the ladder, then head down and right down another ladder. Head up from
there to grab the Revive, then come back down and go left and down for a PP Up.
Head up the ladder at the leftmost part of that section of the "tunnel". Move
down and left and you can grab TM47 Steel Wing from the cliff. Then bike down
and out of the cave and you'll be on the south side of Route 10.
|--------|
|Route 10|
|--------|
Welcome to the other side of Route 10. There are two trainers.
Hiker Jim
Level 35 Machamp (1447 EXP)
You get: 1120G
Go down and to the left and you'll find one more trainer to battle.
PoKeFan Robert
Level 33 Quagsire (967 EXP)
You get: 2640G
Keep going down and you'll be in Lavender Town.
|-------------|
|Lavender Town|
|-------------|
Lavender Town is still creepy, but a bit less so. They used to have a Pokémon
Tower in the Red, Blue, and Yellow games on the northeast part of town that's
now the Kanto Radio Tower. You can walk in and talk to the director who thanks
you for saving his job. You're so amazing that you can take out international
criminal organizations and save people's jobs! He will thank you by giving you
an EXPN Card for the Pokegear Radio and you can now tune into Kanto channels.
You'll also find Mr. Fuji in the Soul House to the south of the Kanto Radio
Tower, and the Name Rater in the southern house.
|-------|
|Route 8|
|-------|
Item on Route 8:
PRZCure Berry
There are five trainers here. You can pick up a PRZCure Berry when you enter the
route. Otherwise, you can fight a bunch of trainers on your way west.
Super Nerd Tom
Level 32 Magnemite (609 EXP)
Level 32 Magnemite (609 EXP)
Level 32 Magnemite (609 EXP)
You get: 1024G
Super Nerd Sam
Level 34 Grimer (655 EXP)
Level 34 Muk (1143 EXP)
You get: 1088G
You'll now encounter three bikers by the Kanto Pokémon Federation. They sound
scarier than they really are. First up is Biker Zeke, who collects ball-shaped
Pokémon that do chain smoking.
Biker Zeke
Level 32 Koffing (781 EXP)
Level 32 Koffing (781 EXP)
You get: 1024G
Biker Harris
Level 34 Flareon (1441 EXP)
You get: 1088G
Talk to him again and he says "Wiped out by some punk from Johto." Now fight the
next trainer, who has a whole collection of Koffings. You could say that he's a
true Koffing connoisseur.
Biker Dwayne
Level 27 Koffing (658 EXP)
Level 28 Koffing (684 EXP)
Level 29 Koffing (708 EXP)
Level 30 Koffing (732 EXP)
You get: 960G
The mighty Kanto Pokémon Federation has quickly gone down. Yay! Now head to
Saffron City and heal at the Pokémon Center.
|------------|
|Saffron City|
|------------|
Head to the northwest of town, to the left of Silph Co. Now barge inside that
family's home and walk up the stairs. You'll see Mimic Girl from Red, Blue, and
Yellow. Talk to her and she'll imitate you, then ask for a PokeDoll. If you can
give her the one she wants, you'll get the Rail Pass, which will make travel
back to Johto nearly instant.
|--------------|
|Vermilion City|
|--------------|
Now, fly back to Vermilion City. Enter the Pokémon Lovers Club in the west part
of the city. in this city. It's the same one in where you got the Rare Candy.
Instead, talk to the guy near the Clefairy. Turns out the PokeDoll IS the
Clefairy. He'll just give you the doll, no questions asked. What a trusting
person. Now, fly back to Saffron.
|------------|
|Saffron City|
|------------|
Re-enter the Mimic Girl's house and up to her room/floor. Talk to her and give
it back, and she'll give you a Magnet Train Pass. How does she even have a
Magnet Train Pass to hand over to you? She's just a little girl! It's for
unlimited use on the Magnet Train that is at the northwest corner of the city,
to the left of that route-changing house.
|-------|
|Route 7|
|-------|
Pokémon on Route 7:
#017 Pidgeotto
#037 Vulpix
#052 Meowth
Head west from Saffron City to Celadon City by taking Route 7. The Saffron City
guard will check your Pokedex as ID. That's pretty weird, man. Anyway, there are
no trainers on this route, so just walk into Celadon City. There are no guards
on the Celadon City side. They don't hold prejudices against Saffron City
residents like that. Apparently, the Underground Path is also sealed due to all
the hooligan battles.
|------------|
|Celadon City|
|------------|
Items in Celadon City:
Leftovers
TM03 Curse
Heal at the Pokémon Center. To your left is a building with a front entrance and
a back entrance. It's the Celadon Mansion. It's where you can get Eevee in Red,
Blue, and Yellow. Head up three stairs, then go into the room. Talk to the guy
and he will tell you a scary story. We know it's scary because some biker rolls
down Cycling Road and most importantly, the guy tells you that it is. At the
end, he'll give you TM03 Curse. Now head back down the few stairs and out the
door. If you go through the front entrance, you'll meet the game designers. They
put pixelated versions of themselves in the game. Groovy!
The Celadon Department Store is in the northwest side of town. Some guy says it
has a similar layout as the Goldenrod City Department Store, but he's not an
observant person, unlike you. You'll find that there are different TMs and some
things are on different floors here.
Level 2: Potions, Heals, Ultra Balls, Escape Ropes, Repels
Level 3: TM10, TM11, TM17, TM18, TM37 Sandstorm
Level 4: Pokedolls, Mail
Level 5: Stat increase & X Items (temporary stat boosts)
Level 6: Vending Machine heal items
You can play slots in the middle of town, because it's important to start
teaching 10-year olds how to start gambling. The Celadon Game Corner allows for
a coin exchange for prizes in the building to the right. There are people there,
but you'd want to talk to the two counters to exchange coins for prizes, which
can be TMs or Pokémon:
TM32 1500
TM29 3500
TM15 7500
Mr. Mime 3333
Eevee 6666
Porygon 9999
The Celadon City Restaurant is on the south-central portion of the city. Like
all the best items in Pokémon, you should look in the trash can. You'll get
Leftovers, an excellent item which heals your Pokémon every turn. If you then go
out and head right to a dead end, press A and you'll get a random PP Up! Yay!
Head to the southwest part of town with a Pokémon that can use the HM01 Cut and
you'll arrive at Erika's gym. An old man lurks outside Erika's gym saying, "This
gym is great! Only girls are allowed here!" He's totally not creepy at all.
You'll face a few trainers in Erika's gym. High-power, fire, and ice type moves
are good, while grass and water types will not be effective.
Twins Jo & Zoe
Level 35 Victreebel (1432 EXP)
Level 35 Vileplume (1380 EXP)
You get: 700G
Picnicker Tanya
Level 37 Exeggutor (1680 EXP)
You get: 740G
"Do you think a girls-only gym is rare? Only girls are allowed here!"
Lass Michelle
Level 32 Skiploom (931 EXP)
Level 34 Jumpluff (1281 EXP)
Level 33 Hoppip (522 EXP)
You get: 816G
Head up to face the final minor trainer of the gym.
Beauty Julia
Level 32 Paras (480 EXP)
Level 32 Exeggcute (672 EXP)
Level 35 Parasect (960 EXP)
You get: 3080G
The lineup (from Elite 4) that you would have at this point is:
Level 44 Sudowoodo
Level 45 Dragonair
Level 45 Raticate
Level 47 Typhlosion
Level 47 Gyarados
Level 49 Espeon
Leave the gym, fly to the Pokémon Center (it's faster) and heal, return to
Julia, then go up to Erika. Save, then it's time to battle!
---
GYM LEADER ERIKA
Level 42 Tangela (1494 EXP)
Level 46 Victreebel (1882 EXP)
Level 46 Bellossom (1813 EXP)
Level 41 Jumpluff (1545 EXP)
You get: Rainbow Badge, 4,600G, TM19 Giga Drain
---
Erika will have three Full Restores that she can use in battle to heal her
Pokémon back to full health and normal status.
Strategy VERSUS Tangela: Use your highest-powered attack from your starter. If
you have a high enough level Pokémon with fire or ice attacks, like Typhlosion
or Ho-Oh, you can utilize them.
Strategy VERSUS Victreebel: Use your highest-powered attack from your starter.
If you have a high enough level Pokémon with fire or ice attacks, like
Typhlosion or Ho-Oh, you can utilize them.
Strategy VERSUS Bellossom: Bellossom looks like it will happily dance for you,
but it has Synthesis, so it can heal half of its health every turn for quite a
while. It can be pretty annoying. It also has SolarBeam, which is a solid
two-turn grass move that could really damage your Water Pokémon. Use your
highest-powered attack from your starter. If you have a high enough level
Pokémon with fire or ice attacks, like Typhlosion or Ho-Oh, you can utilize
them.
Strategy VERSUS Jumpluff: Yes, as much as it looks like a water type, it's a
grass type. Use your highest-powered attack from your starter. If you have a
high enough level Pokémon with fire or ice attacks, like Typhlosion or Ho-Oh,
you can utilize them.
Erika congratulates you and gives you your 12th badge. Yay! You're amazing! Give
yourself a pat on the back, or do a hair flip. TM19 Giga Drain is a good move,
but unfortunately, it's only a 5 PP move. I am deeply saddened by this. I know
you are, too, because how could it not be sad? It's a travesty. Anyway, now walk
out the convoluted maze out the gym.
==============================================================================
==============================================================================
==============================================================================
(c29)
------------------------------------------------
@@ -0,0 +1,468 @@
---- 29 > Routes 11-19 and Fuchsia City Gym ----
------------------------------------------------
Video Playthrough Link (Routes 11-13):
https://www.youtube.com/watch?v=bZQxBI2v0As
Video Playthrough Link (Routes 14-15):
https://www.youtube.com/watch?v=-zeVjtH0UWI
Video Playthrough Link (Routes 16-19 & Fuchsia City Gym):
https://www.youtube.com/watch?v=YYq_0WosMX4
|--------|
|Route 12|
|--------|
Pokémon on Route 12:
#072 Tentacool (water)
#129 Magikarp (water)
Fly to Lavender Town, then go south. If you talk to the fishermen, you'll
distract them from their fishing and they'll fight you with their catches from
their fishing rods.
Fisher Kyle
Level 28 Seaking (1020 EXP)
Level 31 Poliwhirl (870 EXP)
Level 31 Seaking (1128 EXP)
You get: 1240G
Fisher Martin
Level 32 Remoraid (534 EXP)
Level 32 Remoraid (534 EXP)
You get: 1280G
After fighting Fisher Martin, you'll have a chance to take the route westward.
Do so.
|--------|
|Route 11|
|--------|
Pokémon on Route 11:
#019 Rattata
#096 Drowzee
#097 Hypno
Items on Route 11:
Berry
Revive
Follow the curvy road and you'll fight four trainers. Why did the road have to
be so curvy? No idea. You could have at least gotten a roller coaster experience
out of it or something.
Psychic Herman
Level 30 Exeggcute (630 EXP)
Level 30 Exeggutor (1362 EXP)
Level 30 Exeggcute (630 EXP)
You get: 960G
Keep going left and you'll run into your old classmate Owen. Did you forget who
he was? Yeah, me too.
Youngster Owen
Level 35 Growlithe (682 EXP)
You get: 560G
Young trainer, follow the route up and left and you will find salvation. Or at
least another youngster just like yourself.
Youngster Jason
Level 33 Sandslash (1152 EXP)
Level 33 Crobat (1441 EXP)
You get: 528G
If you take the path up and then right, you can grab a Berry and also a Revive
from the bush. Your parents probably told you not to pick up wild berries, and
yet, here we are, disobeying our parents. Nice work. Now, head back towards
Youngster Jason and go further west and you'll face one more trainer.
Psychic Fidel
Level 34 Xatu (1245 EXP)
You get: 1088G
Head west and you'll find that a Snorlax is blocking you from entering Diglett's
Cave or reaching Vermilion. Remember this. You'll come back to that later, my
dear friend. Now, head back east to Route 12.
|--------|
|Route 12|
|--------|
Items on Route 12:
Calcium
Super Rod
Head down, then talk to the fisherman.
Fisher Stephen
Level 25 Magikarp (106 EXP)
Level 25 Magikarp (106 EXP)
Level 31 Qwilfish (663 EXP)
Level 31 Tentacruel (1360 EXP)
You get: 1240G
Head south and go into the house. Talk to the guy who is the Fishing Guru's
younger brother. Answer Yes to his question and you'll get a Super Rod! Woot.
Anyway, keep heading down Route 12.
Fisher Barney
Level 30 Gyarados (1375 EXP)
Level 30 Gyarados (1375 EXP)
Level 30 Gyarados (1375 EXP)
You get: 1200G
Head down the path and you'll see an item that is blocked by a tree. Cut down
the tree so you can get a single item: Calcium. I'm sure that was worth all the
environmental damage you just caused. Now, head south onto Route 13.
|--------|
|Route 13|
|--------|
Pokémon on Route 13:
#017 Pidgeotto
#030 Nidorina
#187 Hoppip
Head south. Fight the left one first.
Bird Keeper Perry
Level 34 Farfetch'd (684 EXP)
You get: 816G
Then the right guy.
Bird Keeper Bret
Level 32 Pidgeotto (774 EXP)
Level 32 Fearow (1110 EXP)
You get: 768G
Now the left guy. It's like a dance. Left, right, left. It takes two to tango.
This third trainer hoards mice.
PoKeFan Joshua
Level 23 Pikachu (403 EXP)
Level 23 Pikachu (403 EXP)
Level 23 Pikachu (403 EXP)
Level 23 Pikachu (403 EXP)
Level 23 Pikachu (403 EXP)
Level 23 Pikachu (403 EXP)
You get: 1840G
Head around that trainer, then head left.
PoKeFan Alex
Level 29 Nidoking (1210 EXP)
Level 29 Slowking (1018 EXP)
Level 29 Seaking (1056 EXP)
You get: 2320G
Head left again in the weird maze to face a hiker.
Hiker Kenny
Level 27 Sandslash (942 EXP)
Level 29 Graveler (832 EXP)
Level 31 Golem (1174 EXP)
Level 29 Graveler (832 EXP)
You get: 928G
|--------|
|Route 14|
|--------|
Pokémon on Route 14:
#030 Nidorina
#195 Quagsire
Welcome to Route 14. If you need to heal on HP and PP, feel free to fly back to
Lavender Town, heal at the Pokemon Center, and then bike back down. Otherwise,
you can keep going. You'll spot a trainer as you ride down. Just above him,
there's a tree that can be cut. Cut it and go left for a fight.
PoKeFan Trevor
Level 33 Psyduck (565 EXP)
You get: 2640G
Cut the tree above him and you have a chance to trade a Chansey for an
Aerodactyl with the female trainer there. You can also try to catch wild Pokémon
in this small patch of grass. Otherwise, head right to face the trainer you
skipped.
PoKeFan Carter
Level 29 Bulbasaur (397 EXP)
Level 29 Squirtle (409 EXP)
Level 29 Charmander (403 EXP)
You get: 2320G
Head down to face...
Bird Keeper Roy
Level 29 Fearow (1006 EXP)
Level 35 Fearow (1215 EXP)
You get: 840G
Head left and cut the tree, onto...
|--------|
|Route 15|
|--------|
Pokémon on Route 15:
#017 Pidgeotto
#030 Nidorina
#033 Nidorino
Cut the tree on your way west. Grab the PP Up as you ride west, then bike back
to where you cut the tree, go down, and then go west to fight six straight
trainers. You discovered a whole line of teachers and schoolboys on a field
trip. They're way too excited to get into schoolyard fights with you.
Schoolboy Johnny
Level 29 Bellsprout (522 EXP)
Level 33 Victreebel (1350 EXP)
Level 31 Weepinbell (1002 EXP)
You get: 1056G
Teacher Colette
Level 36 Clefairy (523 EXP)
You get: 2592G
Schoolboy Billy
Level 27 Paras (405 EXP)
Level 27 Paras (405 EXP)
Level 27 Poliwhirl (757 EXP)
Level 35 Ditto (457 EXP)
You get: 1120G
Teacher Hillary
Level 32 Aipom (643 EXP)
Level 36 Cubone (670 EXP)
You get: 2592G
Schoolboy Tommy
Level 32 Xatu (1171 EXP)
Level 34 Alakazam (1354 EXP)
You get: 1088G
Schoolboy Kipp
Level 27 Voltorb (595 EXP)
Level 27 Magnemite (514 EXP)
Level 31 Voltorb (684 EXP)
Level 31 Magneton (1069 EXP)
You get: 992G
|------------|
|Fuchsia City|
|------------|
Item in Fuchsia City:
Burnt Berry
You'll learn from various people that the Safari Zone in Fuchsia City has closed
since three years ago when Red, Blue, and Yellow took place. The warden fled the
premises. If you go to the building, even the door is gone. What happened to it?
You can still pick up a Burnt Berry as a consolation prize in the northwest part
of town.
You can heal at the Pokémon Center and go fight the gym. Or you can heal and
then get more experience first on Routes 16, 17 (Cycling Road), and 18. The
choice is up to you. I'm not your teacher. Well, I guess I might be, if you're
reading this guide. I'd suggest checking out Routes 16-18 to check it out, if
nothing else. The new Kanto region is worth exploring.
|--------|
|Route 16|
|--------|
Pokémon on Route 16:
#022 Fearow
#088 Grimer
Fly to Celadon City and go west. You can use Cut on the tree to reach it; it's
north of the route-changing building. If you enter the one building there and
come back out, the tree will grow back and you'll have to cut it again. Guess
that environmental destruction doesn't actually last that long in this world.
|--------|
|Route 17|
|--------|
Pokémon on Route 17:
#022 Fearow
#088 Grimer
You could skip Route 16 and go straight through the route-changing building to
Route 17. This is Cycling Road, so get on your bike. Remember you need to
advertise for your friend, the Goldenrod City Bike Shop dude. It's still
possible to bike uphill but you're better off biking downhill. Bike down and
you'll run into your first biker. Possibly literally. It's okay, because
trainers in this world can get hit by a bike and hold you back like they're a
wall.
Biker Riley
Level 34 Weezing (1260 EXP)
You get: 1088G
After this point, there will be branching left and right paths. Head down the
left route and you'll fight your next biker.
Biker Glenn
Level 28 Koffing (684 EXP)
Level 30 Magmar (1072 EXP)
Level 32 Weezing (1185 EXP)
You get: 1024G
You'll get another chance to stay left or go right. When you see a bridge across
the river to the right, cross it and fight the next biker.
Biker Joel
Level 32 Magmar (1144 EXP)
Level 32 Magmar (1144 EXP)
You get: 1024G
If you talk to him afterwards, he says, "I look cool, but I'm weak, so I'm not
really cool." I'm not sure that's how the coolness factor works but I have no
reason to question him. Now, keep rolling down until you run into the last
trainer.
Biker Charles
Level 30 Koffing (732 EXP)
Level 30 Weezing (1111 EXP)
Level 30 Charmeleon (912 EXP)
You get: 960G
Roll down to the end of Cycling Road and you'll be on Route 18.
|--------|
|Route 18|
|--------|
Pokémon on Route 18:
#022 Fearow
#088 Grimer
Head east and you'll face some guy that...hey, he almost has my name!
Bird Keeper Bob
Level 34 Noctowl (1179 EXP)
You get: 816G
He tells you that the shortcut to Celadon is the uphill Cycling Road. That
doesn't sound wise. Now head down into the grass to fight the last trainer on
the road to Fuchsia City.
Bird Keeper Boris
Level 30 Doduo (616 EXP)
Level 32 Dodrio (1083 EXP)
Level 28 Doduo (576 EXP)
You get: 768G
Now head east into Fuchsia City!
|------------|
|Fuchsia City|
|------------|
Fuchsia is such a hard color to spell. I always think the s is before the c.
Anyway, you can go to the southwest part of town again to heal at the Pokémon
Center, and then enter the gym. Head in and you'll find that everyone looks the
same. They're all imitating Janine. Janine's located in the middle-left side of
the gym. But it's good to battle everyone for the experience and we can sort of
see the walls.
High-powered moves and psychic moves are good for Janine's gym. Head up and talk
to the first trainer.
Lass Linda
Level 30 Bulbasaur (411 EXP)
Level 34 Venusaur (1515 EXP)
Level 32 Ivysaur (966 EXP)
You get: 816G
Now head to the gym entrance (but don't exit the gym) and head to the far right
and up. Then, fight that trainer. Ignore the fact that she says she's Janine.
She isn't.
Picnicker Cindy
Level 36 Nidoqueen (1495 EXP)
You get: 720G
Head up then left, then talk to that trainer. It should be obvious by now that
the real Janine wouldn't be spinning around. :P
Camper Barry
Level 36 Nidoking (1503 EXP)
You get: 720G
Now head down and face the fourth trainer.
Lass Alice
Level 30 Gloom (847 EXP)
Level 34 Arbok (1071 EXP)
Level 30 Gloom (847 EXP)
You get: 720G
At this point, you'll see the real Janine. Save, and heal if you need to. Janine
is Koga's daughter, and has taken over for Dad after he joined the Elite Four.
Janine fights like a weaker version of Koga.
---
GYM LEADER JANINE
Level 36 Crobat (1573 EXP)
Level 33 Ariados (946 EXP)
Level 39 Venomoth (1152 EXP)
Level 36 Weezing (1333 EXP)
Level 36 Weezing (1333 EXP)
You get: Soul Badge, 3,900G, TM06 (Toxic)
---
Strategy VERSUS Crobat: Use the highest-powered move from your starter, keeping
in mind that Meganium will be weak against poison moves in this gym. Also
acceptable are psychic moves which have a type advantage against poison.
Crobat's also a flying type, so you can also use ice and electric moves like
Blizzard or Thunder.
Strategy VERSUS Espeon: Unleash psychic from Espeon. Use the highest-powered
move from your starter. Also acceptable are psychic moves which have a type
advantage against poison.
Strategy VERSUS Ariados: Use the highest-powered move from your starter, keeping
in mind that Meganium will be weak against poison moves in this gym. Also
acceptable are psychic moves which have a type advantage against poison. Ariados
is also a bug, so fire moves from a Pokémon like Ho-Oh (e.g., Sacred Fire) or
Typhlosion (e.g., Flame Wheel) will be super effective.
Strategy VERSUS Venomoth: Use the highest-powered move from your starter,
keeping in mind that Meganium will be weak against poison moves in this gym.
Also acceptable are psychic moves which have a type advantage against poison.
Venomoth is also a bug, so fire moves from a Pokémon like Ho-Oh (e.g., Sacred
Fire) or Typhlosion (e.g., Flame Wheel) will be super effective.
Strategy VERSUS Weezings: Use the highest-powered move from your starter,
keeping in mind that Meganium will be weak against poison moves in this gym.
Also acceptable are psychic moves which have a type advantage against poison.
Thirteen gyms down, three to go. You have defeated Janine. Congrats! She'll even
give you TM06 Toxic, which is a poison that zaps enemy HP. She hopes to become
better than Koga and you.
==============================================================================
==============================================================================
==============================================================================
(c30)
------------------------------------------
@@ -0,0 +1,192 @@
---- 30 > Snorlax and Pewter City Gym ----
------------------------------------------
Video Playthrough Link (Snorlax, Route 2, Pewter City, & Tin Tower):
https://youtu.be/l9CjbBMD7Uk
Video Playthrough Link (Ho-Oh, Pewter City Gym, Routes 1-4, & Pallet Town):
https://www.youtube.com/watch?v=pFgyFZqai_I
Anyway, head south onto Route 19...
|--------|
|Route 19|
|--------|
You'll find your way blocked. What happened? Well, boulders have washed ashore
in a recent event. I'll tell you more about it later, it's a spoiler kinda. Now
fly back to Vermilion City.
|-------|
|Snorlax|
|-------|
Using your upgraded radio, go east to Diglett's Cave. Keep around those four
Ultra Balls from before, you'll need them. When you're up to the Snorlax, save
(wow, I haven't said that for ages). You'll find out that Snorlax is sleeping.
Wake it up by going into your Pokegear option and going to your radio. Turn the
radio up to 20, the maximum frequency, then exit the Pokegear menu. Then press A
and talk to Snorlax. That maximum frequency is the PoKeFlute, so the Snorlax
will wake up in an angry mood.
Keep it down at red HP or low yellow HP and throw as many Ultra Balls at it. If
you can use a status move like paralysis on it (e.g. Dragonair's Thunder Wave),
that helps, or catch Snorlax while it's still asleep from Rest. But it might
take a few resets.
Snorlax is a great Pokémon to catch. It is one of the best Pokémon in the game,
and it's Level 50. You'll be able to add this to your team immediately. It is
highly defensive and bulky, and it can sleep for two turns using Rest to heal
all of its HP and status effects. Snorlax also comes with Leftovers, an amazing
item that heals a bit of HP after every attack. You can replace its Rollout move
with HM Strength to inflict extra damage.
After the battle with Snorlax, the route is cleared to Diglett's Cave and
through Route 8. Go back to the Pokémon Center in Vermilion City to withdraw the
Snorlax so that it is part of your lineup. It already has a Leftovers attached!
Now, enter the cave entrance that Snorlax blocked. This is Diglett's Cave.
|--------------|
|Diglett's Cave|
|--------------|
Pokémon in Diglett's Cave:
#050 Diglett
#051 Dugtrio
There are no trainers in here, but you can capture both Diglett and Dugtrio here
if you'd like. Head down the ladder, then ride your bike up and then up that
ladder. Exit the door and you'll be on Route 2.
|-------|
|Route 2|
|-------|
Items on Route 2:
Carbos
Dire Hit
Elixir
Nugget
PSNCure Berry
Welcome to the western part of Kanto. Head up and right to grab the Carbos.
Then, go down to the house and talk to the guy inside for a Nugget for being a
visitor. Cut the tree to the south and shift past the route-changing house.
You'll still be in Route 2. Keep heading down and you'll get an Elixir. Then,
cut the tree just left of it and head north and you'll face a trainer.
Bug Catcher Rob
Level 32 Beedrill (1089 EXP)
Level 32 Butterfree (1096 EXP)
You get: 512G
Keep moving up.
Bug Catcher Doug
Level 34 Ariados (975 EXP)
You get: 544G
Continue moving up and you can get Dire Hit, a PSNCure Berry, and even a Max
Potion in Viridian Forest. Keep moving up.
Bug Catcher Ed
Level 30 Beedrill (1021 EXP)
Level 30 Beedrill (1021 EXP)
Level 30 Beedrill (1021 EXP)
You get: 480G
If you keep heading north, you'll end up in Pewter City.
|-----------|
|Pewter City|
|-----------|
Items in Pewter City:
Ice Berry
Mint Berry
Rainbow/Silver Wing
Welcome to Pewter City! Talk to the man just to the right of the Pokemart and
you'll get the Silver Wing, which will allow you to go back to
Johto and catch the other Pokémon (Lugia or Ho-Oh) that you haven't already
caught. Now heal and deposit any items you'd like. The Pokémon Center has a lady
that allows you to trade a Gloom for a Rapidash. The northeast side of town has
an Ice Berry and a Mint Berry. And the northwest side has the former Museum of
Science which is now closed. They even filled the doorway in. So sad.
If you have a team that focuses on leveling up your starter, you might have
something like this:
Level 77 Feraligatr
Level 50 Snorlax
Level 40 Lugia
Level 33 Donphan (Silver)
Level 30 Gyarados
Level 28 Raticate
If you're fielding more of a balanced lineup, you might have a lineup like this:
Level 51 Espeon
Level 50 Snorlax
Level 48 Starter Evolution
Level 47 Gyarados
Level 46 Dragonair
Level 46 Sudowoodo
For this gym, you'll ideally want to use your starter (if Meganium or
Feraligatr) for this gym, and use their highest-powered grass or water moves.
Camper Jerry
Level 37 Sandslash (1291 EXP)
You get: 740G
Save, heal at the Pokemon Center and come back, then talk to Brock to fight him
for the 14th gym badge. Rock types may have a lot of defense, but not against
grass or water moves. If you have Typhlosion as your starter and a lower-level
Lugia (from SoulSilver), Brock may take you a bit longer to beat. All of Brock's
Pokémon are 10-11 levels higher than in the original.
---
GYM LEADER BROCK
Level 41 Graveler (1176 EXP)
Level 42 Omastar (1791 EXP)
Level 41 Rhyhorn (1185 EXP)
Level 44 Onix (1017 EXP)
Level 42 Kabutops (1809 EXP)
You get: Boulder Badge, 4,200G
---
Strategy VERSUS Graveler: Use your highest-powered move from your highest level
Pokémon. Ideally, this would be a grass or water move (Razor Leaf/SolarBeam or
Surf/Hydro Pump) rather than a fire or normal move.
Strategy VERSUS Omastar: Use your highest-powered move from your highest level
Pokémon. Ideally this is a grass move, not a fire or normal move. Omastar is a
water and rock type, but a water move will still do regular damage against it.
Strategy VERSUS Rhyhorn: Use your highest-powered move from your highest level
Pokémon. Ideally, this would be a grass or water move (Razor Leaf/SolarBeam or
Surf/Hydro Pump) rather than a fire or normal move.
Strategy VERSUS Onix: Use your highest-powered move from your highest level
Pokémon. Ideally, this would be a grass or water move (Razor Leaf/SolarBeam or
Surf/Hydro Pump) rather than a fire or normal move.
Strategy VERSUS Kabutops: Use your highest-powered move from your highest level
Pokémon. Ideally this is a grass move, not a fire or normal move. Omastar is a
water and rock type, but a water move will still do regular damage against it.
There you have it: 14 badges! You are then given the Boulder Badge and some
money. The badge makes your Pokémon even more powerful. Like the other gym
leaders, Brock is a little upset that you beat his "hard-rock defense, but happy
to have battled you. Now exit the gym and heal at the Pokémon Center.
==============================================================================
==============================================================================
==============================================================================
(c31)
----------------------------------------------
@@ -0,0 +1,179 @@
---- 31 > Lugia (Gold) and Ho-Oh (Silver) ----
----------------------------------------------
Video Playthrough Link (Ho-Oh, Pewter City Gym, Routes 1-4, & Pallet Town):
https://www.youtube.com/watch?v=pFgyFZqai_I
Video Playthrough Link (Seafoam Islands Gym, Routes 19-22, 28, & Viridian City
Gym): https://youtu.be/wgql5WCnx4g
Now, you have a chance to snatch the other Pokémon you missed earlier...Lugia or
Ho-oh. Head back through with the Magnet Train back to Johto, then go to
wherever you need to go.
|----------------------------|
|Whirl Islands - Gold Version|
|----------------------------|
Fly to Olivine City. Make sure you have a Pokémon on your team that has Flash,
like Bellsprout. Also make sure you teach HM06 Whirlpool to a Pokémon. I suggest
Tentacool or Krabby, who can use Strength, Surf, Whirlpool, and Waterfall.
You'll need eight badges in order to use Whirlpool.
Pokémon at the Whirl Islands:
#041 Zubat
#086 Seel
#098 Krabby
#116 Horsea
#117 Seadra
Items at the Whirl Islands:
Calcium
Escape Rope
Guard Specs
Max Revive x2
Ultra Ball
Surf southwest and west until you reach the northeast cave of the Whirl Islands.
I suggest using a Max Repel to reduce the number of Tentacool encounters on the
way there. The whirlpool is just northwest of where you fought Swimmer Kara. You
can press A on any whirlpool to remove it temporarily from the map. It's
incredible how much stamina these swimmers have, just staying in the water days
after you last fought them.
Once you find the northeast cave entrance, enter it and you'll see darkness. Use
HM Flash, then head up and leap the ledge to the right on that bike. You'll see
a Ultra Ball on the cliff. To get to it and continue on your journey, leap down
another ledge to the right, go up the steps, grab the Max Revive, and head down
the ladder. Head south, then jump to the left. Keep jumping down and you'll get
an Escape Rope. Woo!
Now go down and then left, then up the ladder. You can now head out the door to
breathe, but that's pointless. Why would you do that? Instead, surf east, then
grab the Guard Specs. Then head down the ladder, head up along the path, head
down, and then up the cliff. It's like a roller coaster, I know.
There's yet another item here. It's a Calcium. Now, on that cliff, head down to
the ladder and take it. Now, go down out the door. That's most of the items in
the Whirl Islands.
Now that you've gone from the northeast to the southeast portion of the Whirl
Islands, go counterclockwise and surf your way southwest out of the reefs and
then Whirlpool out. Head east, then north, and you'll find Swimmer Kara again.
Take that Whirlpool out and head back into the entrance that you originally came
from.
Use Flash again. Drop down the first cliff again, but this time, drop down the
top cliff rather than the bottom cliff. Then head down the ladder. Continue
along the path south and head down that ladder. Grab the Max Revive to the left
of you, then surf south. There are two items on the cliff but you can't reach
those.
Instead, surf down the waterfall, then head left and through the door. Head in,
and surf north to Lugia. Save before you interact with Lugia. I suggest that you
save before you fight him because this is the only chance you'll be able to get
Lugia in the entire game. If you knock it out on a critical hit, lose all your
Pokémon and black out, or run out of balls to throw at it, you would want to
return to your save and retry.
Because the catch rate is only about 2% at low health, even with Ultra Balls,
this may involve a dozen or more resets. Ultimately, you'll want to maximize
your chances by throwing many Ultra Balls at it. You'll want to do so when it is
lower on HP (yellow or red on the HP meter), and ideally when it is asleep.
Focus on using Ultra Balls when Lugia is low on health, even when it could knock
out your Pokémon. Use Revives and Hyper Potions as is helpful to keep your
Pokémon alive to keep throwing balls at it. You can slightly increase your catch
chances if you put Lugia to sleep or paralyze it.
Thard_Verad adds, "Lugia is just heavy enough for Heavy Balls to have higher
catch rates than Ultra Balls. Give those a try."
Catch it, then use the Escape Rope you grabbed earlier to get out of here.
|--------------------------|
|Tin Tower - Silver Version|
|--------------------------|
Pokémon in Tin Tower:
#019 Rattata
Go back to Ecruteak City and then go into the Bell Tower entry house. Talk to
the man and he'll let you through because you have the Ecruteak City Badge. Then
follow up the trail to the tower itself.
Go up and to the left and another bald man will let you through because you have
the beautiful Rainbow Wing. You can then go up the ladder. Keep going and wild
Rattatas will start converging on you like zombies. Consider a Max Repel or two
to drive away all the zombies. Then go up the ladder, hop left for a Full Heal,
then hop all the right. Then hop all the way left, then keep hopping right. On
the middle platform, you might get stuck a bit because you don't see the next
jump easily, but it's the second square from the top. You'll make it to the
northeast side and you can go up the ladder. You are now a true bunny. Are you a
bad bunny?
On the next level, jump down, left, down, left, down, down to get to the Ultra
Ball. Jump right, down, left, down, down, and you'll get to a PP Up. If you go
up the ladder, it won't get you anywhere, so no need to do that. You can
continue with jumps left (x5), down, right (x2), down, left (x2), down, left
(x2) to get an Escape Rope. You'll be next to another ladder. That ladder will
also take you nowhere. Jump right, right, left, left, and you'll be on the
northwest side where you do want to take that ladder up.
On the next level, jump right twice, go all the way to the right and down, jump
down, left, left, and you'll get a Rare Candy. You can then return to the
previous level's bottom right corner and go all the way back up to the top left
corner to get back up to the beginning of the level. Then jump right and right
again, then down, left, down (x2), and take the right down (x4) up to the next
level (Left down takes you back to the bottom left ladder from the previous
level).
On the next level, go up and you'll get a Max Potion. Then go right across the
bridge, up, and then left across the upper (not middle) bridge to get a Full
Heal. Then hop down twice to get up the next ladder.
On the next level, go up, go through the next two warps, jump on the top side
left (x8), down (x3), right (x3), and through that warp to get to a Max Elixir.
Go back through the warp and jump right (x4) to return to the start. Warp back
through the top right warp and then back through the other warp you went
through. You'll be back to the very start of this paragraph. Hop to the right
(x4) to get a Max Revive. Then hop down (x5), go left and down, warp, cross the
bridge down, and warp again.
Go left and you'll get a HP Up. Keep going left and down and warp again. Then
cross the bridge up left and take the warp. Go up the ladder, cross the planks
down, then go up the ladder and you'll be in front of a Level 70 Ho-Oh. I
suggest that you save before you fight Ho-Oh because this is the only chance
you'll be able to get Ho-oh in the entire game. If you knock it out on a
critical hit, lose all your Pokémon and black out, or run out of balls to throw
at it, you would want to return to your save and retry.
The Ho-Oh in-game music gives off great vibes. As we all know, the vibes are
what matters. We must have the vibes. Ho-Oh is a fire/flying type Pokemon with
Safeguard and Ancient Power (damage w/ stat boosts), Punishment, and Sacred
Fire. Use your highest level Pokémon to whittle down Ho-Oh's HP first without
knocking it out, then try to throw as many Ultra Balls as you can at it.
Because the catch rate is only about 2% at low health, even with Ultra Balls,
this may involve a dozen or more resets. Ultimately, you'll want to maximize
your chances by throwing many Ultra Balls at it. You'll want to do so when it is
lower on HP (yellow or red on the HP meter), and ideally when it is asleep. Use
Revives and Hyper Potions as is helpful to keep your Pokémon alive to keep
throwing balls at it. However, all things even, focus on using Ultra Balls when
Ho-Oh is low on health, even when it could knock out your Pokémon, to increase
the number of chances yo have. You can slightly increase your catch chances if
you put Ho-Oh to sleep or paralyze it.
After you catch Ho-Oh, go back down the stairs and then you can use an Escape
Rope to leave. Then, ride the Magnet Train back to Kanto and fly back to Pewter
City.
==============================================================================
==============================================================================
==============================================================================
(c32)
----------------------------------------
@@ -0,0 +1,232 @@
---- 32 > Mount Moon and Routes 1-4 ----
----------------------------------------
|-------|
|Route 3|
|-------|
Pokémon on Route 3:
#019 Rattata
#023 Ekans
#024 Arbok
As you follow the path east on Route 3, you'll face four trainers.
Youngster Warren
Level 35 Fearow (1215 EXP)
You get: 560G
Head up, then right to...
Youngster Jimmy
Level 33 Raticate (819 EXP)
Level 33 Arbok (1039 EXP)
You get: 528G
Head on right to...
Firebreather Otis
Level 29 Magmar (1036 EXP)
Level 32 Weezing (1185 EXP)
Level 29 Magmar (1036 EXP)
You get: 1392G
Head right and along the path, then up to...
Firebreather Burt
Level 32 Koffing (781 EXP)
Level 32 Slugma (534 EXP)
You get: 1536G
Before you head into Mount Moon, heal back at the Pewter City Pokémon Center,
come back, and then save, because you're about to have another major battle.
|----------|
|Mount Moon|
|----------|
Pokémon at Mt. Moon:
#041 Zubat
#046 Parus
#074 Geodude
When you get to Mt. Moon, you will encounter your rival for the first time in a
long time. This may or may not be the final time you battle him, but he has
levelled up his Pokémon and finally has six Pokémon. At this point, you can use
any high-powered moves from your highest-level Pokémon that the Rival's Pokémon
doesn't resist, and you'll be fine.
|------------------------------------------|
|Rival |
|Level 41 Sneasel (1159 EXP) |
|Level 42 Golbat (1539 EXP) |
|Level 45 Starter Evolution (2005-2025 EXP)|
|Level 41 Magneton (1414 EXP) |
|Level 43 Gengar (1750 EXP) |
|Level 43 Alakazam (1713 EXP) |
|You get: 4500G |
|------------------------------------------|
Strategy VERSUS Sneasel: Sneasel is a dark type, so use the highest-powered
attack on your highest-level Pokémon that isn't a psychic move.
Strategy VERSUS Golbat: Golbat is a poison and flying type. Use the
highest-powered attack on your highest-level Pokémon that isn't a ground move.
Golbat is a flying type that resists ground moves. Electric moves will be super
effective. Beware of Confuse Ray, as that will confuse your Pokémon and take
away attack opportunities.
Strategy VERSUS Starter Evolution: The starter evolution will have the type
advantage on your starter, so don't use same-type attacks from your own starter.
Use the most high-powered attack on your highest-level Pokémon (likely Strength
or Body Slam). Or you can utilize Lugia/Ho-Oh to do the requisite water damage
against Typhlosion or fire damage against Meganium.
Strategy VERSUS Magneton: Use the most high-powered attack on your highest-level
Pokémon. Magneton does resist most types of moves due to its steel typing, but
fire and fighting moves would be strong against it.
Strategy VERSUS Gengar: Gengar is a ghost and poison type. Use the most
high-powered attack on your highest-level Pokémon that isn't a normal or
fighting move, as Gengar will completely resist those attacks as a ghost type.
Strategy VERSUS Alakazam: Alakazam is a pure psychic type. Use the most
high-powered attack on your highest-level Pokémon that isn't a fighting or
psychic move.
Your rival's not nearly as confident as he was at the beginning of the game,
after losing to you over and over. Save after you beat the Rival, and heal back
at a Pokémon Center if that's useful. There are two ways past Mount Moon. If you
go right from your Rival battle, up the ladder, and then out, you can go to a
shop outside that sells Fresh Water, Soda Pop, and Lemonade. If you then go down
into another cave entrance and down the ladder, you'll make your way through.
Alternatively, if you go down from the Rival battle, then to the right, and go
out that door, you'll end up in the same spot as going the path above. Either
way, you can face up to three trainers on your east on Route 4.
Thard_Verad adds, "Monday nights at Mt. Moon, you might catch Clefairy dancing
at the lake. Bring Rock Smash." Also, "After you defeat Silver at Mt. Moon, you
can challenge him at Indigo Plateau once a day on Monday and Wednesday. You can
also visit Dragon Den on Tuesday or Thursday and see him training."
|-------|
|Route 4|
|-------|
Pokémon on Route 4:
#019 Rattata
#021 Spearow
#024 Arbok
Item on Route 4:
HP Up
Route 4 has three trainer battles as you head east.
Picnicker Hope
Level 34 Flaaffy (852 EXP)
You get: 680G
Bird Keeper Hank
Level 12 Pidgey (141 EXP)
Level 34 Pidgeot (1252 EXP)
You get: 816G
Picnicker Sharon
Level 31 Furret (769 EXP)
Level 33 Rapidash (1357 EXP)
You get: 660G
Head east, then up to grab the HP Up. Now, you can fly back to Pewter City to
heal at the Pokémon Center, head south onto Route 2 and south through Viridian
Forest, and you'll be in Viridian City.
|-------------|
|Viridian City|
|-------------|
North from the Pokémon Center is the One-on-One Trainer House. This is probably
your easiest way of getting experience every day. You can go in here every day
to fight Trainer Cal, or a placeholder for Cal. Simply go downstairs in that
house to do so. Head left after you go downstairs and the lady will talk to you.
Say Yes and you can battle Cal after you go down. He will have the final
evolutions of the three starters.
Thard_Verad adds, "Pokemon Trainer Cal is a placeholder. If you Mystery Gift
with anyone, they can show up at the Trainer House instead."
Pokémon Trainer Cal
Level 50 Meganium (2227 EXP)
Level 50 Typhlosion (2238 EXP)
Level 50 Feraligatr (2250 EXP)
You get: 5000G
Strategy VERSUS Meganium: Meganium is a grass type. Avoid using grass or water
moves. Use fire or flying moves from Typhlosion, Ho-Oh, or Lugia against
Meganium. Keep in mind that Meganium has Toxic, and Toxic can inflict poison
damage on your Pokémon every turn.
Strategy VERSUS Typhlosion: Typhlosion is a fire type and its best move is
Flamethrower, although its fighting-move Focus Blast also packs a punch. Avoid
using fire or grass moves. Use water moves, like Surf, Waterfall, Hydro Pump
from Feraligatr, Lugia, or Gyarados. Rock moves like Ho-Oh's AncientPower can
also be effective.
Strategy VERSUS Feraligatr: Feraligatr is a strong water type. Avoid using fire
or water moves. Use grass or electric moves against Feraligatr. Keep in mind
that Feraligatr can use Ice Punch against Meganium and all the flying
legendaries (Ho-Oh, Lugia, and others that you catch later). Additionally,
Feraligatrs water moves can still be effective against electric type Pokémon
despite being weak to electric attacks.
Then head south onto Route 1 towards Pallet Town.
|-------|
|Route 1|
|-------|
Pokémon on Route 1:
#016 Pidgey
#162 Furret
Item on Route 1:
Bitter Berry
Unlike in Red, Blue, and Yellow, there are two Pokémon trainers. You can grab
the Bitter Berry on the way down to fight the two trainers.
Schoolboy Danny
Level 31 Jynx (909 EXP)
Level 31 Electrabuzz (1035 EXP)
Level 31 Magmar (1108 EXP)
You get: 992G
Cooltrainer Quinn
Level 38 Ivysaur (1147 EXP)
Level 38 Starmie (1684 EXP)
You get: 1824G
Now head south into Pallet Town!
|-----------|
|Pallet Town|
|-----------|
Talk with Professor Oak in his lab (the biggest building). He notices your Kanto
badges. He tells you that he'll give you a prize if you get all the badges, so
remember that. The houses for Red and Blue are here. You can now fly to Pallet
Town. Now, surf south onto Route 21.
==============================================================================
==============================================================================
==============================================================================
(c33)
------------------------------------------------------
@@ -0,0 +1,133 @@
---- 33 > Cinnabar Island and Seafoam Islands Gym ----
------------------------------------------------------
|--------|
|Route 21|
|--------|
Pokémon on Route 21:
#072 Tentacool
#114 Tangela
Surf south on Route 21. There are a few trainers. Nikki, an avid Seel
enthusiast, wants you to put sunblock on her.
Swimmer Nikki
Level 28 Seel (600 EXP)
Level 28 Seel (600 EXP)
Level 28 Seel (600 EXP)
Level 28 Dewgong (1056 EXP)
You get: 560G
There's a fisherman here. He used his Super Rod to catch you, and you didn't
even notice. Please be more aware of your surroundings.
Fisher Arnold
Level 34 Tentacruel (1492 EXP)
You get: 1360G
Head way left then down to...
Swimmer Seth
Level 29 Quagsire (850 EXP)
Level 29 Octillery (1018 EXP)
Level 32 Quagsire (939 EXP)
You get: 256G
Head down from him and you'll get to Cinnabar Island.
|---------------|
|Cinnabar Island|
|---------------|
Cinnabar Island is in a sad state of affairs for its former residents, who fled
the premises. The volcano has done very well for itself. So you were on Team
Volcano, you're a monster, but also, you probably won a lot of money in Team
Rocket's black market gambling scene. You can now enter the Pokémon Center and
heal your Pokémon. Talk to the two people there and they'll tell you that the
volcano erupted a year ago (two years after Red, Blue, and Yellow).
You'll notice someone looking at what's left of it. It's Blue! He was the
Champion of the Pokémon League until Red beat him. He knows you want a challenge
but he's not in the mood for one yet. He laments, "The volcano erupts, and just
like that, a whole town disappears." I never thought of Blue as a sentimental
guy, but here we are. Blue then levitates and flies off into space, presumably
with Alakazam. Then, surf east to Route 20.
|--------|
|Route 20|
|--------|
Pokémon on Route 20:
#072 Tentacool
Head east and you'll come across one trainer.
Swimmer Cameron
Level 34 Marill (421 EXP)
You get: 272G
Now head on east to the Seafoam Islands.
|-------------------|
|Seafoam Islands Gym|
|-------------------|
Since Cinnabar Island's volcano erupted and destroyed Blaine's gym, it's moved
to the Seafoam Islands. It's now in the middle of nowhere. You should see a
small cave at the Seafoam Islands. Go inside and it's Blaine's remote, makeshift
gym. This gym doesn't even fit the entire screen. The poor guy's become a hermit
and a caveman. I just heard him grunting and working on his cave painting.
---
GYM LEADER BLAINE
Level 45 Magcargo (1485 EXP)
Level 45 Magmar (1609 EXP)
Level 50 Rapidash (2056 EXP)
You get: Volcano Badge, 5,000G
---
Blaine will have two Full Restores available to use on his Pokémon.
Strategy VERSUS Magcargo: Magcargo has Yawn to put your Pokémon asleep, Recover
to get back half its HP in one turn, and Overheat, a pretty strong fire move
that can do significant damage to a starter Pokémon like Meganium and also cause
burn damage. Don't use Meganium, because it takes damage faster than it can deal
to Magcargo. You can use a strong normal move from your strongest Pokémon that
isn't Meganium, like HM Strength, against Magcargo. Additionally, Ho-Oh's
AncientPower is a super-effective rock move against fire types which also raises
Ho-Oh's special stats. Or you can use Lugia's Hydro Pump/Surf, as water moves
are super effective against Magcargo. Feraligatr's water type moves will be
super effective against all Pokémon in this gym.
Strategy VERSUS Magmar: Magmar has a Confuse Ray move that can prevent your
Pokémon from attacking. Don't use Meganium, because it takes damage faster than
it can deal to Magmar. You can use a strong normal move from your strongest
Pokémon that isn't Meganium, like HM Strength, against Magcargo. Additionally,
Ho-Oh's AncientPower is a super-effective rock move against fire types which
also raises Ho-Oh's special stats. Or you can use Lugia's Hydro Pump/Surf, as
water moves are super effective against Magmar.
Strategy VERSUS Rapidash: Rapidash has the moves Flare Blitz (which also speeds
Rapidash up) and Bounce (which is a two-turn Fly type move). Don't use Meganium,
because it takes damage faster than it can deal to Magmar. Additionally, avoid
using two-turn moves, because Bounce can result in your low-PP moves missing
entirely. You can use a strong normal move from your strongest Pokémon that
isn't Meganium, like HM Strength, against Magcargo. Additionally, Ho-Oh's
AncientPower is a super-effective rock move against fire types which also raises
Ho-Oh's special stats. Or you can use Lugia's Hydro Pump/Surf, as water moves
are super effective against Magmar.
Blaine will give you the Volcano Badge and 5,000G. You now have 15 badges! You
can talk to the guy that comes in, but he's late. Word doesn't travel quickly
across the sea. Now exit the cave.
==============================================================================
==============================================================================
==============================================================================
(c34)
-------------------------------------------------
@@ -0,0 +1,134 @@
---- 34 > Routes 19-20 and Viridian City Gym ----
-------------------------------------------------
Video Playthrough Link (Seafoam Islands Gym, Routes 19-22, 28, & Viridian City
Gym): https://youtu.be/wgql5WCnx4g
|--------|
|Route 20|
|--------|
East on Route 20 and to Route 19 are optional, but if you take that route,
you'll get to fight a few more trainers. Surf down and right to face the swimmer
in the water.
Swimmer Lori
Level 32 Starmie (1419 EXP)
Level 32 Starmie (1419 EXP)
You get: 640G
Head right, then up, to one more trainer.
Swimmer Nicole
Level 29 Marill (360 EXP)
Level 29 Marill (360 EXP)
Level 32 Lapras (1501 EXP)
You get: 640G
Head right and up to Route 19!
|--------|
|Route 19|
|--------|
You'll find a drowning guy next to a girl. Talk to the guy for a battle.
Swimmer Tucker
Level 30 Shellder (622 EXP)
Level 34 Cloyster (1479 EXP)
You get: 272G
Talk to him again. Yep, seems like he is drowning. Now. talk to the girl to
battle her. She's apparently disgusted by wimpy people like her boyfriend.
Swimmer Dawn
Level 34 Seaking (1237 EXP)
You get: 680G
There's one more trainer. Head up, then right a little.
Swimmer Jerome
Level 26 Seadra (862 EXP)
Level 28 Tentacool (630 EXP)
Level 30 Tentacruel (1317 EXP)
Level 28 Goldeen (666 EXP)
You get: 224G
And that's all the trainers! You can't go further north because you'll be
blocked by those boulders south of Fuchsia City. So, fly back to Viridian City
instead.
|-------------|
|Viridian City|
|-------------|
First, heal at the Pokémon Center. Blue is no pushover. He's not a one-trick
pony. He has more than one type of Pokémon. He's one of only two trainers that
are tougher than Champion Lance in the whole game. There aren't any other
trainers in this gym, though, so Blue appears to have filled the whole gym with
Legos.
Save before you talk to Blue. He doesn't think that highly of you conquering
Johto. He may have a point, as his Pokemon are much higher level than the Johto
gym leaders and even the other Kanto gym leaders. But you can prove him wrong
right here. You'll want to make sure you're hitting with high powered attacks
for the most part, but also familiar with type strengths and weaknesses.
---
GYM LEADER BLUE
Level 56 Pidgeot (2064 EXP)
Level 54 Alakazam (2151 EXP)
Level 56 Rhydon (2448 EXP)
Level 58 Gyarados (2659 EXP)
Level 58 Exeggutor (2634 EXP)
Level 58 Arcanine (2646 EXP)
You get: Earth Badge, 5,800G
---
Strategy VERSUS Pidgeot: Pidgeot is a normal and flying type which will use Air
Slash as one of its flying type moves. Pidgeot can be hit with rocks or
electrocuted. Okay, that sure sounds violent when I put it that way. Like
Arcanine, Pidgeot also has a switch-out move named Whirlwind, which can scramble
your lineup. Use your highest-powered attack from your highest level Pokémon
that is not a ground type move.
Strategy VERSUS Alakazam: Alakazam's a psychic type, so don't use fighting moves
against it. Otherwise, you can use your highest powered attack (e.g., HM
Strength), which is likely coming from your starter evolution.
Strategy VERSUS Rhydon: Rhydon is a rock and ground type. Use your
highest-powered attack from your highest level Pokémon that is not a normal
move. Good moves to use against Rhydon include any of Meganium's grass moves,
water moves like Hydro Pump or HM Surf from Lugia or Feraligatr, as well as ice
and fighting moves.
Strategy VERSUS Gyarados: Gyarados is a water and flying type, which is news to
everyone, because it is a dragon type and can't fly. Use your highest-powered
attack from your highest level Pokémon that is not a ground type move. If one of
your Pokémon has TM Thunder (e.g., Ho-Oh), you can use it to great effect here.
Strategy VERSUS Exeggutor: Two of Exeggutor's favorite moves are Psychic and
Hypnosis. Beware of Exeggutor's Hypnosis, which will put your Pokémon to sleep.
Use your highest-powered attack from your highest level Pokémon that isn't a
grass or psychic type move, like Body Slam or Strength. Fire moves work well
against this grass and psychic type. Water types like Feraligatr could run into
a bit more trouble due to the type weakness.
Strategy VERSUS Arcanine: Arcanine is a fire type, so don't use grass or fire
type moves. Water moves like HM Surf work well, plus any high-powered move of
other types generally.
With Blue defeated, you now have all 16 badges. Blue is completely surprised but
gives you the badge. He says he'll beat you back one day. Talk to him again and
he says you must not lose until he beats you.
Head out of the gym and heal at the Pokémon Center. The game still isn't over!
There are no credits yet. There are still a few things you can do before you
clear Kanto.
==============================================================================
==============================================================================
==============================================================================
(c35)
---------------------------
@@ -0,0 +1,80 @@
---- 35 > Routes 22-28 ----
---------------------------
Video Playthrough Link (Mount Silver & Boss Battle vs. Red):
https://www.youtube.com/watch?v=ubjgTa41WxU
Now, fly back to Pallet Town.
|-----------|
|Pallet Town|
|-----------|
Again, it's time to enter Professor Oak's lab. Talk to him and he'll be
impressed and make arrangements for you to head to Mount Silver. Only the best
trainers are allowed in because of the ferocity of wild Pokémon. Now, fly back
to Viridian City, and then head west to Route 22.
|--------|
|Route 22|
|--------|
Pokémon on Route 22:
#021 Spearow
#022 Rattata
#060 Poliwag (surfing)
Items on Route 22:
TM42 Dream Eater
We're making our journey west now. If you cut the tree to the west of Viridian
City, you can talk to a guy that gives you TM42 Dream Eater.
There are no Pokémon trainers on this route, so just keep heading north, west,
then north. Head through that gate you went through earlier going to Indigo
Plateau, but this time, you will be heading west instead of north. Do so and
enter Route 28.
|--------|
|Route 28|
|--------|
Pokémon on Route 28:
#077 Ponyta
#078 Rapidash
#114 Tangela
#215 Sneasel
#232 Donphan (Silver only)
Items:
TM47 Steel Wing
As you head west towards Mount Silver, you can catch a few high-level Pokémon,
although they aren't as high level as the last few legendaries you just caught.
Consider catching a Tangela. It can learn Stun Spore which helps with Pokémon
catches, plus it can use HM02 Cut and HM05 Flash.
There's also a Pokémon Center here that you should heal at before moving on.
It's the last one you can get to before Mount Silver. From there, you can cut
the trees east and go into a house to get TM47 Steel Wing from the lady there.
|-------------------------|
|Final Dungeon Preparation|
|-------------------------|
Before you enter Mount Silver, this would be a good opportunity to do any final
things before you challenge the final dungeons and boss. Make sure you've got at
least 10 Revives, 10 Hyper Potions, 5 Escape Ropes, 5 Max Repels, and 50 Ultra
Balls. You can fly to Mahogany Town's Pokemart to buy some if you need more. You
can fly back to the Pokémon Center on Route 28 just outside of Mount Silver when
you're ready.
==============================================================================
==============================================================================
==============================================================================
(c36)
---------------------
@@ -0,0 +1,72 @@
---- 36 > Lapras ----
---------------------
|------|
|Lapras|
|------|
We already have five of the nine special Pokémon of the game (two of five
legendaries): Sudowoodo, Snorlax, Gyarados (Shiny Red), Lugia, and Ho-oh. What
now? Thard_Verad comments, "Lapras only appears on Friday." If it's a Friday,
head to Violet City and then go south through Route 32 into Union Cave. Level 20
Lapras becomes available on a Friday once you've gotten HM Surf.
In Union Cave, use a Max Repel to reduce the number of wild encounters as you
navigate towards Lapras. Head along the path and past the first two trainers.
Head almost all the way through till you see the guy on the cliff still spinning
around, which is the third trainer that you spot. Down and to the left is a
lake. Surf past it and head down the ladder, then fight the guy next to the
lake. He should be very easy to beat.
PoKeManiac Andrew
Level 24 Marowak (637 EXP)
Level 24 Marowak (637 EXP)
You get: 1440G
With Andrew defeated, surf east and you'll fight another trainer.
PoKeManiac Calvin
Level 26 Kangaskhan (975 EXP)
You get: 1560G
After you beat him, he exclaims, "You demonstrated on me!" Head down the ladder,
then surf southward. Land on the right side to grab a Hyper Potion and fight a
trainer.
Cooltrainer Nick
Level 26 Charmander (? EXP)
Level 26 Bulbasaur (355 EXP)
Level 26 Squirtle (? EXP)
You get: 1248G
Then land on the left side. Head up to fight another spinning trainer.
Cooltrainer Gwen
Level 26 Eevee (511 EXP)
Level 22 Vaporeon (924 EXP)
Level 22 Jolteon (928 EXP)
Level 22 Flareon (933 EXP)
You get: 1056G
Head down the path a long way and you'll fight one more trainer.
Cooltrainer Emma
Level 28 Poliwhirl (786 EXP)
You get: 1344G
Then surf east from Emma and you'll encounter a Level 20 Lapras. Lapras isn't a
legendary. But treat it like it is one when you're trying to catch it by saving
before you interact with Lapras. If you knock it out on a critical hit, lose all
your Pokémon and black out, or run out of balls to throw at it, you would want
to return to your save and retry. Ultimately, you'll want to maximize your
chances by throwing many Ultra Balls at it. You'll want to do so when it is
lower on HP (yellow or red on the HP meter), and ideally when it is asleep or
paralyzed by a move like Stun Spore. You can then use Escape Rope to leave Union
Cave.
==============================================================================
==============================================================================
==============================================================================
(c37)
-----------------------------------
@@ -0,0 +1,153 @@
---- 37 > Mount Silver and Red ----
-----------------------------------
|------------|
|Mount Silver|
|------------|
Pokémon in Mount Silver:
#042 Golbat
#055 Golduck
#075 Graveler
#232 Donphan
#246 Larvitar
Items in Mount Silver:
Escape Rope
Full Restore
Protein
Head west and up from there and you'll be on Mt. Silver. Bring your best team
with all the HMs (particularly Surf, Waterfall, and Flash). Bring 30 Ultra Balls
so you can catch the strongest wild Pokémon in any dungeon. Consider having a
few Max Repels ready in case you get frustrated with the numerous wild Pokémon
encounters here. I'd suggest trying to catch many of the Pokémon in here,
though, as they are high-level Pokémon and a few are great to catch.
Larvitar would be a good Pokémon to catch because its most-evolved form,
Tyranitar, is an excellent Pokémon. Golduck may also be good if you want a
higher-level water Pokémon that can also learn HMs Surf, Strength, Waterfall,
and Whirlpool.
When you get in, use HM Flash. Head up, then go left and drop down the ledge to
get the Escape Rope. Jump back down and go right instead of up. Go up the steps
to the cliff and grab the X Accuracy.
Head back down the cliff, and go up the middle. This time, go right at the
intersection. Head a bit to the right, go up the steps, and then up the cliff.
Go along the path, head on right, then up another cliff to the north. Continue
along the cliff route, and then when there's an intersection to the left and
right, take the left, drop down and get the Max Elixir. Drop down the ledges all
the way back to ground level, then again, and then go back up the cliff.
Next, go, left, up, and right. If you go down, use Surf on the lake, and then
use Waterfall, you can go through an entrance to grab a Full Restore. If you
then return across the lake, you can go through the cave door, keep going up,
and you'll find a trainer.
Your final lineup, if you've been leveling up your starter only, might look like
this:
Level 82 Starter Evolution
Level 70 Ho-Oh/Lugia
Level 50 Snorlax
Level 48 Golduck
Level 40 Ho-Oh/Lugia
Level 39 Tangela
Your final lineup, if balanced, might look like this:
Level 60 Lugia
Level 54 Espeon
Level 53 Dragonair
Level 53 Sudowoodo
Level 53 Starter Evolution
Level 50 Gyarados
You're about to face by far the strongest trainer in the game. Red is the final
boss. Make sure you have HMs Surf and Strength equipped on any high-level
Pokémon that can make use of them. Both HMs can be used an unlimited number of
times and even now, they likely do more damage than almost any move that a
Pokémon can learn during your game playthrough. You can go back to the Move
Deleter in Blackthorn City later in the game if you want to replace the moves
later.
|-----------------------------|
|Red |
|Level 81 Pikachu (1422 EXP) |
|Level 73 Espeon (3081 EXP) |
|Level 77 Blastoise (3465 EXP)|
|Level 75 Snorlax (2475 EXP) |
|Level 77 Charizard (3448 EXP)|
|Level 77 Venusaur (3432 EXP) |
|You get: 7700G |
|-----------------------------|
Red will have three Full Restores that he can use. You will likely need to use
several Hyper Potions and Revives to keep your Level 70+ Pokémon in the fight.
Strategy VERSUS Pikachu: Pikachu is an electric type Pokémon that cannot damage
rock or ground type Pokémon. Pikachu is super effective against water type
Pokémon like Feraligatr. However, despite being the highest level Pokémon that
Red has, it is pretty frail. You can use your highest powered attack from your
starter evolution (even if it is a water move) or the highest powered attack
from Lugia or Ho-Oh, whichever Pokémon has the higher level.
Strategy VERSUS Espeon: Espeon is a dark type that cannot be hit with ghost
moves. You can use your highest powered attack from your starter evolution or
the highest powered attack from Lugia or Ho-Oh, whichever Pokémon has the higher
level.
Strategy VERSUS Blastoise: Blastoise is a water type Pokémon, and the most
evolved form of Squirtle. Blastoise is strong against fire type Pokémon like
Typhlosion and Ho-Oh. Blastoise is technically weak against grass type Pokémon,
but it also has Blizzard, a very strong ice type move, that is super effective
both against Meganium as well as flying types like Ho-Oh and Lugia. The best
moves to use against Blastoise are your highest-level grass moves (e.g. from
Meganium) or your highest-level electric moves that can be taught to a Pokémon
(like Ho-Oh). If you don't have those as Level 70+ Pokémon, use the highest
powered attack from your highest-level Pokémon that isn't Typhlosion or Ho-Oh.
Strategy VERSUS Charizard: Charizard is a fire and flying type Pokémon, and the
most evolved form of Charmander. Charizard is strong against grass type Pokémon
like Meganium, so don't use Meganium. Charizard can still take damage from fire
type attacks from Typhlosion and Ho-Oh. Avoid using grass and fire type attacks
against Charizard. The best moves to use against Charizard are your
highest-level water type move (Hydro Pump or Surf) from your best Pokémon that
can use it (Feraligatr or Lugia), or your highest-level electric move that can
be taught to a Pokémon (like Ho-Oh). If you don't have those as Level 70+
Pokémon, use the highest powered attack from your highest-level Pokémon that
isn't Meganium (e.g. Typhlosion or Ho-Oh).
Strategy VERSUS Snorlax: Snorlax is Ash's strongest Pokémon, and takes the
longest to defeat. Snorlax is bulky and will keep using Rest to restore its
health. A rock, ground, or steel type Pokémon might take a bit more damage. Use
the highest powered attack from your starter evolution or the highest powered
attack from Lugia or Ho-Oh, whichever Pokémon has the higher level.
Strategy VERSUS Venusaur: Venusaur is a grass and poison type Pokémon, and the
most evolved form of Bulbasaur. Venusaur is a slow and bulky Pokémon that can
use Synthesis to heal half its HP. Venusaur will be strong against water types
like Feraligatr, and will take damage well from grass attacks from Meganium.
Avoid using grass and water attacks against Venusaur. The best moves to use
against Venusaur will be your fire, flying, or psychic moves that you can have
Typhlosion, Ho-Oh, or Lugia utilize.
Red is down. You're the best trainer in the Johto and Kanto worlds combined, at
this time. After defeating Red, he'll give you the money and then the silent
treatment. He flies away, and then the credits start rolling. After the credits
roll, save.
The game is over, for the most part. You can still catch the Legendary Dogs at
this time if you haven't already. If you want to fight Red again, beat the Elite
Four again and then he'll be back at Mt. Silver, for experience or whatever.
==============================================================================
==============================================================================
==============================================================================
(c38)
-----------------------------------------
@@ -0,0 +1,61 @@
---- 38 > RAIKOU, ENTEI, AND SUICUNE ----
-----------------------------------------
They will only be in grasses, which means it'll never be in water, caves, trees,
Ilex Forest, or National Park. The exceptions are Routes 40 and 41. They change
route when you change route or enter/exit a city, cave, or building. Get on a
bike and start biking everywhere. Do this on every route you run across. Run
across all Johto. Check everywhere. Be a free spirit! Every time you enter a new
city or switch routes, the legendaries move one route, although they
occasionally will jump a few routes.
There are advantages to training up a Scyther. It has False Swipe, which allows
you to knock a Pokémon's HP down to 1 HP without knocking it out. You can do so
by having your Elite Four Pokémon doing the fighting and sharing EXP at Mount
Silver. Train till Scyther is Level 39, then go to Route 42 with the Max Repel.
Once you've had your original encounter with a roaming legendary, it becomes
possible to track where it is. After the initial encounter with a roaming
legendary Pokémon, they will show up on the map in Pokegear. Flying will reset
the roaming legendary's location, which is only really good if you are far from
the roaming legendary.
It's better to keep switching between routes. BebeDuckie gives this advice: "You
go between the same city and route like a few steps left right left right in the
same spot, and eventually get the roaming Pokémon near you without having to fly
everywhere."
In Johto, it is helpful to fly to Violet City, then stay on a bike and keep
switching between Routes 35, 36, and 37 so that the legendaries can quick reset
their locations, as quickly as every 10 seconds. You can get a lot of repeat
encounters quickly once you've located the legendary Pokémon before and keep
switching quickly back and forth between the same routes.
Save every now and then when you get close to a roaming legendary. If you knock
it out, you can go back to the save file and try to catch it again. The roaming
legendaries will all run after one attack or one ball throw. This can be
frustrating, but go in with the expectation that you'll have to encounter it a
dozen or more times before you can catch it. The good news is that once you've
weakened a legendary once, it will remain at that HP the next time you encounter
it.
You could consider a Master Ball here, but keep in mind that there are three
roaming legendaries, so you can only use a Master Ball on one of them. Because
the catch rate is only about a few percent at low health, even with Ultra Balls,
this may involve many encounters or even resets. Ultimately, you'll want to
maximize your chances by throwing many Ultra Balls at it. You'll want to do so
when it is lower on HP (low yellow or ideally red on the HP meter), and ideally
when it is asleep or paralyzed although it's hard to get these moves in prior to
the roaming legendary running off.
As long as you are persistent, you will eventually catch each roaming legendary.
If you knock it out a bunch of times or run out of Ultra Balls, you can go back
to your save and try again. After you've caught each roaming legendary, remember
to save!
==============================================================================
==============================================================================
==============================================================================
(c39)
-------------------------------------------
+13 -10
View File
@@ -24,9 +24,10 @@ for the launcher to acknowledge on refocus.
`RomImporter` then imports on refocus / Choose:
- **ROMs** via `findPendingRom`: only a 1 MiB `.gb` whose SHA-1 maps to a
version that is **not** yet ready counts as pending. A leftover
`picked_rom.gb` from Red therefore cannot block Blue's Choose (issue #167).
- **ROMs** via `findPendingRom`: only a 1 MiB (Red/Blue/Yellow) or 2 MiB
(Gold) `.gb`/`.gbc` whose SHA-1 maps to a version that is **not** yet ready
counts as pending. A leftover `picked_rom.gb` from Red therefore cannot
block Blue's Choose (issue #167).
- **Mods** via `findPendingMod`: Prefer `picked_mod.zip`, or (on Choose) any
other `.zip` at the save-dir root (USB copy).
- **Saves** via `findPendingSav`: Prefer `picked_save.sav`, or (on Choose) any
@@ -41,13 +42,15 @@ same system picker and install the chosen archive on return.
## Tab structure
`self.tab` is one of `"red"`, `"blue"`, `"yellow"`, `"mods"`. The tab bar
draws one chip per game plus a MODS chip and rebuilds `self.tabRects` every
frame so `mousepressed` can dispatch clicks; switching tabs mid-import is
allowed (a dropped ROM still routes by SHA-1 regardless of which tab shows).
On **NX**, **Scan again** is stricter: it only starts an import whose SHA-1
matches the open game tab, so a shared `imports/` folder with Red+Yellow
cannot jump Yellow → Red.
`self.tab` is one of `"red"`, `"blue"`, `"yellow"`, `"gold"`, `"mods"`,
`"find"`. The tab bar draws one chip per game plus MODS / FIND chips and
rebuilds `self.tabRects` every frame so `mousepressed` can dispatch clicks;
switching tabs mid-import is allowed (a dropped ROM still routes by SHA-1
regardless of which tab shows). Yellow uses the bright gold rail colour;
Gold (Gen 2) uses a deeper amber so the two stay distinct. On **NX**,
**Scan again** is stricter: it only starts an import whose SHA-1 matches the
open game tab, so a shared `imports/` folder with Red+Yellow cannot jump
Yellow → Red.
- A game tab (`_drawGamePanel`) shows the ROM card, the SAVE FILES card, the
Play button, and the SAVE SLOT card in a responsive two-column grid (see
+202
View File
@@ -0,0 +1,202 @@
# Linux arm64 (aarch64) AppImage
Releases ship `gen1recomp-<version>-linux-arm64.AppImage` alongside the
existing x86_64 `gen1recomp-<version>-linux.zip`. It targets 64-bit ARM
desktop Linux: Raspberry Pi 4/5 running Raspberry Pi OS, Armbian and other
SBC distros, arm64 VMs on Apple Silicon, Ampere/Graviton desktops, and the
aarch64 handhelds that run a full distro.
> The Anbernic RG34XXSP has its own PortMaster-style pack
> (`gen1recomp-*-rg34xxsp-stockos64-mod.zip`, see
> [anbernic-rg34xxsp.md](anbernic-rg34xxsp.md)). That one bundles PortMaster's
> LÖVE runtime and expects the device's own SDL; this AppImage is the generic
> desktop-Linux artifact and shares nothing with it but the `game.love`.
## For players
```sh
chmod +x gen1recomp-*-linux-arm64.AppImage
./gen1recomp-*-linux-arm64.AppImage
```
Then use **Import ROM** in the launcher to point it at your own legal Red /
Blue / Yellow cartridge dump, exactly as on every other platform.
If your system has no FUSE (`dlopen(): error loading libfuse.so.2`), either
install it (`sudo apt install libfuse2`) or run without it:
```sh
./gen1recomp-*-linux-arm64.AppImage --appimage-extract-and-run
```
### What the host has to provide
Very little, and this is enforced by an assertion in the build rather than by
good intentions. The only libraries the AppImage requires at startup are:
```
glibc 2.29+ libstdc++ libfreetype6 zlib
```
Everything else — OpenGL/Mesa, X11, Wayland, KMSDRM, ALSA, PulseAudio — is
**dlopened**, so it is used when present and skipped when absent. That means
one image runs on a full desktop, on a Wayland-only session, on a
KMSDRM-only handheld with no X server, and on a box with ALSA but no
PulseAudio, without a different build for each.
That property does not come for free from Debian's packages, and getting it
is most of what the build below is doing; see
[Why five libraries are built from source](#why-five-libraries-are-built-from-source).
## For builders
```sh
scripts/build_linux_arm64.sh --version 0.1.0
```
Output:
```
dist/linux-arm64/gen1recomp-<version>-linux-arm64.AppImage
dist/linux-arm64/gen1recomp-<version>-linux-arm64.AppImage.sha256
```
Useful flags: `--game-love PATH` reuses an already-packed payload (CI does
this so every platform ships identical bytes), `--rebuild-image` forces the
builder container to rebuild, `--clean-cache` throws away the pinned
downloads and the compiled LÖVE prefix.
### Requirements
An **aarch64 host** with **docker or podman**. A Raspberry Pi 5 is the
reference machine (a cold build takes about 10 minutes on one — six libraries
plus the engine; rebuilds reuse the cached prefix and take seconds). Apple Silicon with Docker
Desktop and GitHub's `ubuntu-24.04-arm` runner both work too.
The script refuses to run on x86_64 rather than falling back to qemu-user
emulation: that path takes hours and has produced miscompiled LuaJIT.
### Why this is not just another `scripts/build.sh` target
`scripts/build.sh linux` downloads LÖVE's official `love-11.5-x86_64.AppImage`,
unpacks its squashfs, drops `game.love` in, and glues it back together. That
trick is not available here — **LÖVE publishes no aarch64 binary at all.** The
11.5 release has win32, win64, macOS, Android, iOS and one x86_64 AppImage,
and that is the entire list.
So this build compiles LÖVE 11.5 from the official `linux-src` tarball and
assembles the AppImage from scratch. Every pinned input — the LÖVE source, the
five libraries built alongside it, and the AppImage type-2 runtime — is
SHA-256 verified on the host before the container ever sees it, and the
container itself runs with no network access.
### Why the build happens in a Debian bullseye container
glibc is backward compatible but not forward compatible: a binary linked
against glibc 2.41 will not start on a system with 2.31, and there is no way
to fix that after the fact. Compiling on the oldest base we support is
therefore the only thing that makes one artifact work everywhere.
Bullseye (glibc 2.31) is that base. The resulting binaries actually come out
needing only **glibc 2.29** and **GLIBCXX_3.4.21**, so the AppImage covers
everything from Ubuntu 20.04 and Raspberry Pi OS bullseye through current
trixie.
This is a statement about the *compile environment*, not about where the
artifact runs — building on your own newer distro would silently raise that
floor and strand every user on an older one, with no symptom until they
download it. CI enforces the floor: `linux-arm64-build` fails if the highest
required glibc symbol version climbs above 2.31.
### Why five libraries are built from source
SDL2, OpenAL, libtheora, libogg/libvorbis and libmpg123 are compiled rather
than installed from bullseye. In every case the reason is *correctness*, not
a newer version number — Debian builds these for a system where every
dependency is installed and co-versioned, which is the opposite of an
AppImage's situation. Each one broke the build in a different way, and all
three failure modes are now assertions that fail the build instead of
shipping.
**1. Hard-linked backends (SDL2, OpenAL).** Debian's `libSDL2` lists
`libpulse`, `libasound`, `libX11` and `libwayland-client` as `DT_NEEDED`
resolved by the loader at startup, not dlopened. An AppImage bundling it
refuses to start unless the host has *all four*. It appeared to work in
testing only because a desktop Pi has all four; a headless CI runner is what
exposed it. Debian's OpenAL does the same via `libsndio`, which itself
hard-links `libasound`. Built from source with `--enable-*-shared` and
`ALSOFT_DLOPEN`, both dlopen their backends instead.
**2. A stray link (libtheora).** Debian's `libtheoradec.so.1` is linked
against `libcairo.so.2` — a packaging artifact, since a video decoder has no
business drawing vector graphics — and cairo drags in X11, xcb, fontconfig
and freetype. `--disable-examples` produces a `libtheoradec` needing only
`libogg`.
**3. SONAME collision with the host (ogg, vorbis, mpg123).** The subtle one.
OpenAL dlopens ALSA, ALSA's config loads its PulseAudio hook plugin, and that
plugin pulls the *host's* `libsndfile` into our process. `libsndfile` links
`libogg`, `libvorbis` and `libmpg123` — the same three we bundle. The loader
resolves a SONAME exactly once per process, so the host's `libsndfile` binds
to *our* copies:
```
openal -> libasound -> libasound_module_conf_pulse -> libsndfile (host, new)
`-> mpg123_info2 -> libmpg123 (ours, bullseye 1.26)
```
`mpg123_info2` arrived in mpg123 1.32, so the plugin failed to relocate, ALSA
config collapsed, and the game ran with **no audio device at all**. Not
bundling these instead would make `libogg`/`libvorbis`/`libmpg123` mandatory
host packages; building them current means our copies *satisfy* the host's
`libsndfile` rather than starving it.
The same collision is why the font stack — freetype, fontconfig, libpng,
brotli, zlib — is left to the host entirely. Bundling a bullseye freetype
2.10.4 meant a host `libcairo` could not find `FT_Get_Transform` (added in
2.11) and the game died at startup. Leaving the whole stack to the host keeps
it self-consistent, while `liblove` — compiled against 2.10.4 — only ever
asks for symbols every supported host already has.
The general rule this all reduces to: **never bundle a library the host's own
stack may also load, unless yours is at least as new as theirs.**
### CI
Three jobs, path-gated on `scripts/build_linux_arm64.sh`,
`scripts/linux-arm64/`, `scripts/pack_love.sh` and this document:
- **`linux-arm64-selftest`** (`ubuntu-latest`, x86_64) — offline gate. Checks
the pins are real digests on a dated tag rather than the moving
`continuous` one, that the Dockerfile still builds on bullseye, that the
exclude list still classifies known sonames correctly, that AppRun still
launches `game.love` with `--fused`, and that the host-arch guard actually
fires. Needs no container and no arm64 machine.
- **`linux-arm64-build`** (`ubuntu-24.04-arm`) — the real build, then extracts
the artifact and asserts the layout, that every bundled object resolves
under AppRun's `LD_LIBRARY_PATH`, and that the glibc floor is still ≤ 2.31.
Uploads the AppImage for 7 days.
- **release**`linux-arm64` runs on `ubuntu-24.04-arm`, reuses the shared
`game.love` from the `love-payload` job, and the AppImage is staged and
published like every other release asset.
Unlike the Switch job, none of this needs secrets or self-hosted hardware, so
it runs on fork PRs too.
### Updating the pins
Both pins live in `scripts/linux-arm64/common.sh`:
- `LOVE_VERSION` / `LOVE_SRC_SHA256` — bumping any version invalidates the
cached prefix automatically (its name is keyed by every source version at
once, so a partial rebuild cannot mix vintages). Check that bullseye still
has `-dev` packages new enough for the new release; `build_appimage.sh`
asserts every optional module actually linked, because LÖVE's `configure`
exits 0 and silently drops a module when one is missing.
- `SDL2_*`, `OPENAL_*`, `THEORA_*`, `OGG_*`, `VORBIS_*`, `MPG123_*` — the
source-built libraries. Bumping these is usually safe and occasionally
necessary: `libmpg123` in particular must stay at least as new as what a
target host's `libsndfile` expects, which is asserted for `mpg123_info2`.
- `APPIMAGE_RUNTIME_TAG` / `APPIMAGE_RUNTIME_SHA256` — always a dated tag
from [AppImage/type2-runtime](https://github.com/AppImage/type2-runtime/releases).
The selftest fails the build if this ever points at `continuous`.
+846
View File
@@ -0,0 +1,846 @@
# Mods and Gen 2 (Gold)
The mod API is one API across both generations. Hook names, event names,
registry names and the `mod.*` facade are shared on purpose: a mod that runs on
Red should be able to run on Gold without learning a second vocabulary.
What differs is how much of it Gold can actually serve, and that is why Gen 2
support is something a mod **declares** rather than something it inherits.
## What you can rely on today
The short version, for an author deciding what to write:
- **Every registry name, hook name and event name means the same thing in both
games.** Nothing is prefixed, renamed or repurposed per generation. Where Gen
2 genuinely carries more, the record or the payload gains a *field*.
- **40 of the 46 registries are available on Gold.** 17 keep their Gen 1 target
outright (`commands`, `tokens`, `growth_rates`, `battle_sprite_scales` and
`render_pipelines` among them), 16 route to a Gen 2 table under the same
name, 6 are Gen 2-only systems Red has no counterpart for, and `migrations`
is a code registry with no data target in either game. The other 6 are gated,
and are listed below with the consumer change each one still needs.
- **A registry with no home in a generation is reported, never silently
merged.** The write is taken, dropped, and named once per mod in the same
error feed the mod manager shows -- in both directions, so a Red boot writing
to `decorations` is told exactly as a Gold boot writing to `map_scripts` is.
- **40 event names and 43 hook names have a call site in both generations**, so
one subscription serves both games. `tests/engine/gate_gen2_mod_api.lua`
reads those names back out of the source and fails if a site is renamed or
deleted on either side, and fails again if a new shared site appears without
being listed here.
- **24 further names are Gen 2-only** (friendship, breeding, the Pokegear, the
radio, Pokerus, the roamers, Kurt, the Bug Contest, the Unown puzzle, mail,
held items, shininess, gender, and the five cards of the GS boot cinema).
They are plain names, not a `gen2.` namespace, so if Red ever grows the
system the name is already right.
- **Every Gen 2 seam is guarded** by `Runtime.wants` / `Runtime.wantsHook`, so
a boot with no mod subscribed allocates nothing at any of them.
- **A mod is loaded on Gold only if it says so.** See `gen2compat` below.
`src/mods/Schemas.lua` is authoritative for routing;
`tests/engine/gate_gen2_mod_api.lua` holds this document to it.
## Declaring which games a mod is for
```json
{
"id": "my_mod",
"name": "My Mod",
"version": "1.0.0",
"entry": "main.lua",
"api": 2,
"games": ["gen1", "gen2"]
}
```
`games` is an optional array of version ids (`"red"`, `"blue"`, `"yellow"`,
`"gold"`), generations (`"gen1"`, `"gen2"`, case-insensitive) or `"all"`.
`src/mods/ModTargets.lua` resolves the tokens off `GameVersion.ORDER` and
`GameVersion.generation`, so nothing anywhere restates the game list.
`Manifest.validate` stores the resolved, ORDER-sorted ids on `manifest.games`
and **derives** `manifest.gen2compat` from them, which is the one field the
loader's gate reads.
Nothing moves on disk for any of this. A mod is installed once, into
`mods/<id>/`, and that directory serves every game: there is no `mods/gen1/`
and no per-generation copy. Targeting is declared, not filed.
`"gen2compat": true` is the legacy spelling and is still accepted. It is purely
additive -- it *adds* the Gen 2 games to whatever `games` says -- so no shipped
manifest can lose a game it already ran on. A manifest with neither key is Gen
1 only, which is exactly what it always meant. An unknown token warns and is
dropped under `api` 1 and refuses the manifest under `api` 2; a `games` array
that names no game this engine knows falls back to the default rather than
orphaning the mod; a non-array `games` is a hard error.
Every token is enforced, per game. `Loader:_gateGeneration` gates on
`ModTargets.supports(manifest, version, generation)`, the same call both mod
surfaces make, so `"games": ["blue"]` really does not load on Red and the
loader's skip line is the launcher's line, `For Blue, not Red`. A manifest with
no `games` and no `gen2compat` still covers every Gen 1 game, so nothing
written before the key existed changes behavior.
On a Gold boot, a mod claiming no Gen 2 game is **not loaded at all**: no
registrations, no subscriptions, no entry chunk. The manager still lists it,
showing `ENABLED (NOT THIS GAME)` and the reason, and the player's enable flag
is left alone so it comes straight back on Red.
Both mod surfaces derive what they show from `ModTargets` rather than from
their own copy of the rule. The launcher's mod panel carries a `Show for:` game
chip row and a per-mod tag (`GEN 1`, `GEN 1+2`, `RED/GOLD`), greyed with `Not
for this game` and the detail `For Gen 1, not Gold` when the mod does not run
on the selected game; the in-game manager shows the same verdict as
`ENABLED (NOT THIS GAME)` plus an inert `FOR GEN 1+2` row on the detail screen.
The launcher asks the same question of a mod's dependencies: one whose hard
dependency does not run on the selected game reads `Needs <id> (not for Gold)`,
matching the loader's contagious skip.
A separate overlay, `options.modsByVersion[version][id]`, is where a per-game
enable flag will live. It is a preview: `SaveData.PER_VERSION_MODS` is `false`,
so `SaveData.modScope` answers nil for every caller and the launcher panel, the
in-game manager and the loader all read *and* write the one shared
`options.mods` flag. Nothing consults the overlay for enablement until that
flips, which is deliberate: the overlay is plantable from an imported
`.g1rmodlist`, and a reader scoped differently from the writers would show a
mod set no boot would honour.
That is deliberate. Gold reimplements the battle engine, the overworld, the
script VM and the save format, so a Gen 1 mod dropped into a Gold boot would
find a small fraction of its call sites live. A mod that half-applies reads to
a player as a broken mod. Not running is the honest state, and naming a Gen 2
game is the author saying "I have tested this there".
Adding a Gen 2 game does not opt out of anything on Gen 1, because `games` is a
union: `["gen1", "gen2"]` covers everything it covered before. What does change
is that the gate now runs on a Gen 1 boot too, so a manifest that names *only*
Gen 2 games no longer loads on Red, Blue or Yellow. Say `["all"]` or list both
generations if you want both.
Two riders. **A hard dependency that does not run here takes the dependent down
with it**, as a skip rather than a failure and carrying the dependency's own
wording (`depends on X, which does not run here (For Blue, not Red)`), so the
whole chain has to cover the same games. And **the claim is yours, not the last
word**: it is the manager's `TRY HERE ANYWAY` row that lets a player run a mod
whose author never opted in, which is the only route for a mod written before
the field existed. The override is per game -- `options.modsGen2[id]` is a
`{ [version] = true }` table, so forcing a mod onto Red does not force it onto
Gold, and a legacy `options.modsGen2[id] = true` reads as "the Gen 2 games",
the only set it could ever have affected. It applies on the next boot; a forced
mod loads normally and keeps a note saying it was never verified here. Where
the choice cannot be persisted the manager says `COULD NOT SAVE` instead of
promising a restart.
If you are writing new code, still prefer the API: take the live game from
`mod.game` (or the `game.ready` payload, or a `ui.*` hook's first argument) and
the world from `mod.world`. Those are the names that mean the same thing in
both games. What follows is for the mods that were written before Gold existed
and reach past it.
## Gen 1 module facades
A mod with `engine_internals` reaches engine modules by name, and under Gold
those names used to resolve to Gen 1 modules nothing instantiates -- so the
patch landed on dead code and the mod was inert with no symptom but silence.
On a Gen 2 boot, **a require made from a mod's own chunk is answered by an
adapter**: the Gen 1 API, backed by Gen 2 internals. `src/mods/Gen2Compat.lua`
is the table, `src/mods/Loader.lua`'s require shim is where the swap happens,
and `tests/engine/gate_gen2_mod_facade.lua` holds both to it. Engine code is
not affected -- the shim only substitutes when the calling chunk is outside the
engine tree, so `src/render/PaletteFX.lua` still gets the real Gen 1 module on
both generations.
Fifteen names are served. **alias** means the adapter *is* the Gen 2 module, so
a monkey-patch, a `rawset` sentinel and a `getmetatable(x) == M` check all land
on the table Gold runs; **facade** means a translating wrapper over it.
| the Gen 1 name a mod requires | kind | what it gets on Gold |
| --- | --- | --- |
| `src.core.Game` | facade | a live proxy onto the Game2 instance |
| `src.world.OverworldController` | facade | over `src/world/gen2/World.lua`; `World:step` / `:interact` / `:interactBody` dispatch through it |
| `src.world.Map` | alias | `src/world/gen2/Map.lua`, grown Gen 1's statics and instance methods |
| `src.world.NPC` | alias | `src/world/gen2/Npc.lua`; `NPC.new` sniffs the Gen 1 argument order |
| `src.pokemon.Boxes` | facade | over `src/core/gen2/Boxes.lua`, plus Gen 1's `COUNT` / `CAPACITY` / `ensure` / `active` / `deposit` |
| `src.battle.BattleState` | facade | over `src/ui/gen2/BattleState.lua`, write-through |
| `src.ui.PartyMenu` | facade | over `src/ui/gen2/PartyMenu.lua`, write-through |
| `src.world.WorldAPI` | alias | `src/world/gen2/WorldAPI.lua` |
| `src.world.PikachuFollower` | alias | `src/world/gen2/Follower.lua` |
| `src.script.ScriptRunner` | facade | over `src/script/gen2/Vm.lua` |
| `src.ui.OptionsMenu` | facade | over `src/ui/gen2/OptionsMenu.lua`, write-through |
| `src.world.FieldDefaults` | facade | the `playerSprites` answer, and a named refusal for the rest |
| `src.world.Collision` | facade | `DELTA` / `target` / `occupied` / `canMove` |
| `src.ui.StartMenu` | facade | over `src/ui/gen2/StartMenu.lua`, write-through |
| `src.ui.BoxMenu` | alias | `src/ui/gen2/PcMenu.lua` |
Two entries in that table are not the pairing they look like.
`src.ui.BoxMenu` resolves to `PcMenu`, not to `src/ui/gen2/BoxMenu.lua`: Gen 1's
`BoxMenu` is Bill's PC *top menu*, whose Gold counterpart is `PcMenu`, while
Gold's `BoxMenu` is the withdraw/deposit *list* Gen 1 builds inline. And
`src.script.ScriptRunner` is served narrowly rather than fully: `scanLabels`
and `validate` forward verbatim, with the default verb lookup swapped to
`game.data.commands` so a script of Gen 1 built-ins cannot validate clean and
then run as nothing, while the lifecycle half is a thin handle onto the one
`world.vm` with `resume` and `update` refused rather than double-driving it.
The `script.started` / `script.ended` / `script.command` seams are the
supported route and already work on Gold.
`src.script.Commands` and `src.ui.OptionRows` have **no** adapter and are the
two names a require of which still lands in the boot error feed the manager
shows, with the module named. Both load fine under Gold and both are traps: the
first hands back 61 Gen 1 verbs none of which Gold can run, the second paints
Red's four-box options chrome over Gold's single 18x16 one.
`docs/preparing-your-mod-for-gen2.md` is the migration guide for an author
working through this, and `python3 tools/modkit.py gen2check <id>` reports a
mod's own findings against the coverage table below.
Three rules the adapters keep, because a plausible wrong answer is worse than
the module being missing:
- **Live, never a snapshot.** A mod captures `require("src.core.Game")` at file
scope, before a save or a world exists. The facade is a proxy that reads the
live instance on every touch, so `Game.save` is nil during the entry chunk
and correct forever after. It aliases the two names Gold spells differently
(`Game.overworld` is `Game2.world`, `Game.writeOptions` is
`Game2:persistOptions`) and the one data table that was renamed
(`game.data.sprites` is `data.gen2Sprites`).
- **A member with no backing says so.** `game.data.field` does not exist on
Gold, so it reads nil *and* logs once, naming the mods holding the facade.
`BattleState.newWild` is absent rather than invented, because a `newWild`
that took a species and a level would be a lie about what Gold's battle
screen is.
- **One stable table for the run.** Where the Gen 2 arm can serve the name
outright the adapter *is* that module, so a mod's monkey-patch, its
`rawset` sentinel and its `==` idempotency check all land on the table Gold
actually runs.
### What the adapter says it covers
The adapter publishes its own coverage, versioned by
`Gen2Compat.COVERAGE_VERSION` (1), and `modkit gen2check` consumes that table
rather than a second copy of the same knowledge:
```lua
Gen2Compat.modules() -- the 15 names, sorted
Gen2Compat.serves(name) -- boolean
Gen2Compat.memberStatus(name, member) -- "backed" | "warned" | "absent" | nil
Gen2Compat.coverage(name) -- a fresh table per call:
-- { module, kind = "facade"|"alias", target, members = { [name] = status },
-- notes = { [name-or-topic] = "one line" } }
```
The status vocabulary is frozen at three values, and a member listed as both
resolves to the weaker claim:
| status | means |
| --- | --- |
| `backed` | present, and it does the Gen 1 job on Gold |
| `warned` | present, answers nil or degrades, and names itself once with the mod attributed |
| `absent` | deliberately not served; a nil read is the honest failure |
Today that is 288 backed, 32 warned and 161 absent across the fifteen modules.
`notes` keys are documentation topics rather than a member list -- dotted paths
(`save.money`), field names (`warpAt`), hook names (`hook ui.pc.items`) and
bare topics (`identity`, `iteration`, `rawset`) all appear there. `members` is
the authoritative set, and a member it does not record is not a promise either
way: on an alias it resolves to whatever the Gen 2 module has, on a
write-through facade it falls to the Gen 2 class, on the `src.core.Game` facade
it reads nil and says so, and on the `src.world.OverworldController` facade it
reads nil silently.
**The follower.** Gold's cart has no trailing companion at all, so
`src/world/gen2/Follower.lua` is new Gen 2 code rather than a facade: the
entity, the trail loop, and a `shouldSpawn` a mod replaces. `World:step` calls
`Follower.update(game, world)` once per logic frame after the body, and
`World:setMap` calls `Follower.onMapEntered` before it emits `map.entered` --
the same two call sites `src/world/OverworldController.lua` gives the Gen 1
arm, which is what makes a Gen 1 follower mod's wrappers tick.
Vanilla never spawns one: `shouldSpawn` answers false until something replaces
it. `Follower.setShouldSpawn(fn)` is the supported way, and it writes the same
file-local the Gen 1 mods reach through `debug.setupvalue` on the upvalue named
`shouldSpawn`, so the two cannot disagree.
Two Gen 2 engine changes came with it, both general rather than follower-only:
an entity with `passable` set never blocks a step (the Gen 1 name and meaning,
`src/world/Collision.lua`), and `World:rebuildPeople` now preserves **guests** --
anything in the people list it did not put there. A rebuild runs on every zoom
and every time-of-day roll, so without that a follower vanished at the top of
the hour.
**What the facades cannot fix.** A mod that allow-lists version strings
(`GameVersion.get() == "red" or ...`) excludes itself from Gold by construction,
and no adapter should special-case it. Neither is a Gen 1 screen id: Gold's
builtins carry a `Gen2` prefix, so a mod matching `id == "BoxMenu"` matches
nothing. A write to a field on a live Gen 2 menu instance is inert where Gen 1
read it back (`menu.onSwitch`, `menu.swapFrom`, `StartMenu`'s box geometry),
and `map.warpAt` is a name collision rather than a rename -- Gen 1's is a table
keyed by cell, Gold's is a method, so indexing or iterating it raises. All of
these are mod-side edits, each with a route that works on both generations;
`docs/preparing-your-mod-for-gen2.md` walks through them.
## What works on Gold today
**Screens.** The `screens` registry serves both generations. Gold's screens
are registered under `Gen2`-prefixed ids so a mod that replaces Gold's party
menu does not also replace Red's; `Screens.GEN2_IDS` in `src/ui/Screens.lua`
is the full list. Every screen Gold opens goes through an id, including the
boot cinema and the START menu.
**Asset overrides.** `overrides/` shadowing and asset transforms work
unchanged: Gold's screens load art through `src/render/Assets.lua`, the same
choke point Gen 1 uses.
**Content registries at the shared path.** `pokemon`, `moves`, `items`,
`type_chart`, `strings`, `font`, `screens`, `commands`, `tokens`,
`growth_rates`, `battle_sprite_scales`, `render_pipelines`, and the audio
family (`audio`, `music`, `sfx`, `cries`, `map_songs`). These keep their Gen 1
target path, so one mod source targets both generations.
The last two are the newest and each carries one caveat worth stating before
you write against it:
- **`battle_sprite_scales`.** `src/ui/gen2/BattleState.lua:imageScale` walks
the merged table for a record whose `path` matches the pic being drawn,
skipping the registry's own `_owners` row, and `picScale` falls through to
the species record's `battleScaleFront` / `battleScaleBack` after it -- the
same image-then-species-then-default order Gen 1 resolves in. Because the key
is the asset path it also reaches the pics that are nobody's species: the
player's trainer back, the DUDE's, an opponent's frontpic. The **default**
differs and is not a registry record either side: Red's 32x32 back pics draw
at 2x, Gold's 48x48 ones fill their 6x6 box at 1x, so a scale that looks
right on Red is twice as large on Gold. At any scale the pic stays centred in
its box and standing on the same ground line.
- **`render_pipelines`.** `src/core/Game2.lua:load` installs
`src/render/Pipelines.lua` on Gold's dataset *after* `mods:load`, so the
merged table is the one it walks, and `Game2:draw` composites the
whole-frame half through `Pipelines.wantsPresent` / `Pipelines.present` with
the Gen 1 ctx keys (`width`, `height`, `scale`, `dpi`, `dpiX`, `dpiY`). The
**`drawWorld` half is inert on Gold**: its overworld draws straight to the
window rather than into a canvas the way `src/world/OverworldController.lua`
hands one to `Pipelines.drawWorld`. A drawWorld-only pipeline is not left
switched on and drawing nothing -- `Game2:load` retires a restored level for
one, leaving `options.pipelines` untouched so the mode comes back the day
Gold grows a world canvas. Gold also has no OPTION row for a pipeline
(`Pipelines.rows` is read only from `src/ui/OptionsMenu.lua`), so a Gold
player reaches one by its `hotkey`.
**Content registries at a Gen 2 path.** `maps`, `tilesets`, `sprites`, `text`,
`encounters`, `trainers`, `palettes`, `icons`, `battle_anims`, `constants`,
`statuses`, `move_effects`, `item_effects`, `balls`, `ai_classes` and
`evolution_methods`. Same registry name, same verbs, a Gen 2 table underneath
(`data.gen2Maps`, `data.gen2Encounters`, `data.gen2Statuses`, ...).
`src/core/Game2.lua` loads the extracted ones into `game.data` before it
calls `mods:load`, and every consumer takes them by reference and never
copies, so what a mod merges is what the game walks: a registered map is a map
Gold can warp into, a patched tileset is the one `Map.new` reads, a patched
encounter table is the one the grass rolls.
The battle-rule six are the newer half and work slightly differently: there is
no table on disk for them at all. They come into existence *as* the merge, and
each consumer reads a record through a lookup that falls back to its own module
records when no loader ran, so a mod-free Gold boot behaves identically:
| registry | who reads it |
| --- | --- |
| `statuses` | `Battle.statusRecordFor` / `statusPenaltyFor`, `Catching.statusBonus`, `ItemEffects.healClassOf` |
| `move_effects` | `Battle.moveEffectRecordFor` (`useMove`'s dispatch) |
| `balls` | `Catching.recordFor` |
| `ai_classes` | `Ai.layersFor` (the ten `scoring.asm` passes, plus mod layers) |
| `evolution_methods` | `Evolution.methodFor` |
| `item_effects` | `ItemEffects.recordFor` / `partyAction` |
`src/mods/Builtins.lua` seeds those six with **Gold's** records under Gen 2
rather than Red's. It has to: both games call it `GREAT_BALL`, and Red's record
carries no `multiplier`, so seeding Red's would leave Gold's x1.5 reading nil.
**Content registries that exist because Gold does.** Six systems Red has no
counterpart for, so there is no Gen 1 table to share and none of these carries
a Gen 1 target at all. The routed Gen 2 path is their only home, and
`Schemas.GEN1` gates them on a Red boot the way `Schemas.GEN2` gates
`map_scripts` on a Gold one -- reported, not silently merged.
| registry | id space | who reads it |
| --- | --- | --- |
| `held_items` | item ids | `ItemEffects.heldItemFor`; the merged rows are written back onto `data.items` for `Battle:itemDef` |
| `phone_contacts` | `PHONE_*` (`data.gen2Constants.phoneContactOrder`) | `Phone.useRegistry`, folded onto the contact table |
| `decorations` | `"deco:<n>"` | `Decorations.attributes`, the single read point for an attribute row |
| `apricorns` | apricorn item ids | `Apricorns.useRegistry`, which rebuilds all three lookups and Kurt's menu order |
| `landmarks` | `LANDMARK_*` | `Nests.landmarkId` / `Nests.landmark`, which resolve a map header's landmark byte |
| `radio_channels` | station ids | `MapRadio.channelRecord`, which puts a registered station on the dial |
`Game2:load` calls `Phone.useRegistry`, `Decorations.useRegistry`,
`Apricorns.useRegistry` and `ItemEffects.applyHeldItems` immediately after
`mods:load`, so the merge is live before the first frame. `landmarks` and
`radio_channels` need no such call: their consumers take `data` at call time.
`landmarks` merges onto the cache's own `gen2Landmarks.landmarks` and
`held_items` onto the view `Game2` builds from `data.items`, so both fold
against the vanilla row -- a `register` for an existing id collides, a
`patch` stacks. The other four come into existence as the merge, seeded from
their module's literals by `src/mods/Builtins.lua`.
Four honest limits on that surface:
- `held_items` reaches the battle by being written back onto `data.items`, so a
held row for an id with no `data.items` record lands nowhere. To invent a
held item, register the `items` record too. The write-back is a diff against
a pre-merge snapshot, which is what lets `items` and `held_items` compose
instead of one reverting the other.
- `decorations` ids are `"deco:<n>"`, not `DECO_*` names: the cart's decoration
constants are a bare `const_def` block with no name table behind them, so
there is nothing in the ROM to spell them by. `battle_anims` addresses its
unnamed rows the same way. `n` is the attribute row's index, which is
`wMenuSelection`.
- `phone_contacts` does not register the four `PHONE_UNUSED` `const_skip` holes
(contact bytes 8, 9, 10 and 25). The manifest gives all four the same id, and
one id cannot key four rows. They stay copies of the wrong-number filler,
which is what the cart does with them.
- `radio_channels` and `phone_contacts` register *content*, not new UI: a
registered station gets a dial position and a name, and a registered contact
gets a row the Pokegear indexes, but neither invents a screen.
**Record shapes.** A registry whose Gen 2 records genuinely differ carries a
Gen 2 schema beside its Gen 1 one (`gen2Fields` / `gen2Keys` / `gen2Write` in
`src/mods/Schemas.lua`, resolved by `Schemas.shapeFor`). The registry name, the
verbs and wherever possible the ids stay shared; only the record changes. The
differences an author meets:
- **`pokemon`.** Gen 2 splits `special` into `specialAttack` /
`specialDefense`, names the level-up table `levelMoves` and the pic size
`picSize`, has no separate `level1Moves`, and points an evolution at `into`
rather than `species`. It also carries the breeding block (`eggGroups`,
`eggMoves`, `eggSteps`, `genderRatio`) and the wild held-item pair.
- **`encounters`.** The id is the encounter *kind*, not the map:
`mod.content.encounters:patch("grass", { ROUTE_29 = { rates = { NITE = 40 } } })`.
A map's row carries a `rates` set per time of day and one slot list.
`fishGroups`, `trees` / `treeSets`, `rocks`, `bugContest` and `roamMaps` are
ids of their own.
- **`trainers`.** The id is the trainer *class*, and the record is
`{ name, index, attributes, baseMoney, encounterMusic, trainers, items }`,
with one entry per named trainer of the class. The registry writes one level
in, into `data.gen2Trainers.classes`, so the call shape is unchanged.
- **`icons`.** Two id forms in one registry, routed by the `ICON_` prefix a
sheet name carries: a species id names an assignment (a string, the sheet's
name), an `ICON_*` id names a sheet.
- **`palettes`, `battle_anims`, `constants`.** The id is a subtable of the
target: `pokemon` / `trainers` / `bg` / `objects` / `roofs` for palettes,
`scripts` / `moves` / `objects` / `framesets` / `oamsets` / `gfx` for
battle_anims, and one of Gold's 42 ordered ROM name lists (plus `mapGroups`,
`trainerClassMembers`, `types`) for constants. Those lists are ordered and
position *is* the id a script byte resolves through, so they replace rather
than append.
Four more id-space notes, because the records at those paths came out of a
Gen 2 ROM:
- Gold's `text` ids are ROM pointer strings such as `"55:4067"`, not the
`TEXT_*` names Red uses. `override` them by pointer; there is no name table.
- A Gen 2 tileset carries its walkability as `collision` where Gen 1 says
`walkable`. Both fields validate; only `collision` is read on Gold.
- A Gen 2 warp row carries `destGroup` / `destMapNum` beside the `destMap` /
`destWarp` pair Gen 1 also has. Both are optional in the shared schema, so a
Gen 1 warp row and a Gen 2 one both validate, and patching one of Gold's own
maps does not mean restating the ROM's map-group numbers.
- Gold writes `"burn"` / `"sleep"` into `mon.status` where Red writes `BRN` /
`SLP`. The `statuses` registry is the same registry; only the ids differ, and
they have to.
**`mod.commands`.** Works on Gold. `src/script/gen2/Vm.lua` runs the cart's own
bytecode, so there is no opcode byte to hand a mod -- the seam is a row the
cart cannot write. `Opcodes.MOD_COMMAND` (`"modcommand"`) is an op *name* with
no byte behind it, and the VM dispatches it through the same merged
`data.commands` table Gen 1's runner resolves by name. Two row shapes reach it:
```lua
{ op = "modcommand", verb = "mymod:shake", args = { 4, 2 } } -- native
{ "mymod:shake", 4, 2 } -- Gen 1 row
```
The second is the Gen 1 row shape verbatim, so one row list can serve both
games as long as every row in it is the mod's own verb. The handler is called
`fn(ctx, unpack(args))` with `ctx.vm` where Gen 1 has `ctx.runner`; it may
block on `ctx.vm:showText` / `:waitFrames`, and its return value speaks Gen 1's
control vocabulary (`"end"`, a row number, or nil). A missing or raising verb
is warned once per name and the rest of the list still runs. The engine's own
Gen 1 verbs are **not** seeded on Gold: a row-list verb handed Gold's ctx would
find no runner on it, so `data.commands` under Gen 2 is the mod verbs alone.
**`mod.save`, `mod.options`, `mod.log`, `mod.assets`, `mod.find`, exports.**
Generation-agnostic; nothing to adapt.
**`mod.world`.** Same method set, resolved against Gold's world
(`src/world/gen2/WorldAPI.lua`). Two differences show through and are
documented on the module: Gold's world is not a stack state, and Gen 2 event
flags are numeric ids into `wEventFlags` rather than string keys.
`spawnNpc` / `removeNpc` append onto the map def's own object list, the way the
Gen 1 arm does, so a spawned actor is pooled, drawn, walked and talked to like
an extracted one and survives a map reload; it is not serialized, so a mod
respawns on `map.entered`. `queueScript` takes a small allowlist of verbs Gold
has its own entry points for (`start_battle "wild" species level`, `warp`,
`text`, `setflag`, `clearflag`) and refuses a list containing anything else
**by name, before the first row runs**, so a mod never gets a half-run queue.
`marchInPlace` still has no Gen 2 equivalent (the Gen 2 movement stream has no
byte for it) and returns `nil, reason` rather than approximating one.
**Hooks and events that fire on Gold.** Every name below is the Gen 1 name
carrying the Gen 1 payload keys, because Gold's call sites reuse them rather
than defining a parallel vocabulary; where Gen 2 carries more, the payload
gains a field instead of the name gaining a prefix.
- *Engine-wide, from the shared modules:* `game.ready`, `screen.pushed`,
`screen.popped`, `screen.render_visible`, `music.started`, `music.stopped`,
`music.select`, `music.volume`, `sound.played`, `zoom.range`,
`assets.transformed`, `mods.loaded`, `mod.options_changed`.
- *Overworld (`src/world/gen2/`):* `map.entered`, `map.exited`,
`map.reloaded`, `player.warped`, `world.stepped`, `world.interacted`,
`world.npc_spawned`, `world.trainer_engaged`, `world.blacked_out`,
`world.block_replaced`, `world.boulder_moved`, `world.tod_changed`,
`world.object_toggled`, `flag.changed`; hooks `warp.destination`,
`movement.collision`, `movement.speed`, `encounter.roll`,
`encounter.species`, `encounter.fishing`, `world.tod`, `map.palette`,
`fieldmove.eligibility`. `flag.changed` carries the numeric `wEventFlags`
id under Gen 1's `name` key, which is the one payload difference the
numeric flag space forces.
- *Menus (`src/ui/gen2/`):* `ui.start_menu.items`, `ui.title_menu.items`,
`ui.options.rows`, `ui.party.submenu`, `ui.naming.grid`, `ui.pc.items`,
`ui.list_menu`, `transition.style`. `ui.list_menu` covers Gold's script
menus (`ScriptMenu.lua`); the `Chrome.List` widget the START and title
menus draw with does not raise it yet, so those two are composed through
their own hooks only.
- *The Oak speech (`src/ui/gen2/OakSpeech.lua`):* `intro.oak_speech.started`,
`intro.oak_speech.step`, `intro.oak_speech.answered`,
`intro.oak_speech.finished`, and the `intro.oak_speech.build` hook. Gold has
a real Oak speech, so it is the same extension point rather than a second
one: same names, same payload keys, same moments in the sequence. The beats
are a data table with the same step vocabulary (`say` / `pic` / `name` /
`choice` / `yesno` / `shrink` / `fn`, plus Gold's own `initclock` and
`demo`), and the step *ids* match Gen 1's wherever the moment is the same --
`oak_welcome`, `demo_mon`, `world_spiel`, `ask_player_name`, `name_player`,
`legend`, `shrink` -- so `ModUI.insertStepBefore(steps, "name_player", ...)`
lands in the right place in both games. The two ids with no Gen 1
counterpart are Gold's own beats, `init_clock` (the `farcall InitClock` the
speech opens with) and `oak_study` (the return to Oak for `_OakText5`). Gold
has no rival-naming or name-confirmation beats, so it raises no anchors for
them: the rival is named by `CopScript` in `maps/ElmsLab.asm`, hours later.
- *Battle (`src/battle/gen2/`):* `battle.started`, `battle.ended`,
`battle.turn_started`, `battle.turn_ended`, `battle.move_used`,
`battle.damage_dealt`, `battle.fainted`, `battle.status_inflicted`,
`battle.battler_switched`, `battle.ball_thrown`, `battle.exp_gained`,
`pokemon.level_up`, `pokemon.move_learned`; hooks `battle.damage`,
`battle.crit`, `battle.accuracy`, `battle.turn_order`,
`battle.enemy_action`, `battle.run`, `battle.exp_award`, `exp.gain`,
`catch.rate`, `trainer.party`, `battle.overlay`, `battle.low_health_alarm`
and `battle.catch_exp`. One payload difference: Gen 1's vanilla
`battle.low_health_alarm` link reads `ctx.battle.data`, and Gold's battle
screen has no `.data` field, so the Gen 2 site **adds** `ctx.data` beside the
Gen 1 keys. A mod that calls `nextFn` is unaffected; one that reaches through
`ctx.battle.data` instead gets nil on Gold.
- *The catch and the evolution:* `pokemon.caught`, `pokemon.evolved`; hook
`evolution.check`. `src/ui/gen2/BattleState.lua:pushCaught` emits
`pokemon.caught` once the mon is in the party or the box, and
`src/core/gen2/Evolution.lua` emits `pokemon.evolved` from `apply` and wraps
each row's decision in `evolution.check`. The hook passes `data` where Gen 1
passes `game`; positions 2-4 (mon, row, trigger) match.
- *The frame (`src/core/Game2.lua`):* hooks `input.step`, `input.pointer`,
`render.zones`, `render.compose`, `render.letterbox`, `render.hud`. Each sits
at the same moment `src/core/Game.lua` and `src/render/Renderer.lua` raise it
-- the logic tick before the pad is read, a pointer the touch overlay gets
first refusal on, the palette zone list handed to the present pass, the
letterbox, and the finished playfield rect -- and carries the same payload.
`render.hud`'s `gameX` / `gameY` really is where Gold's dialogue boxes and
menus land, because `Chrome.fitScale` / `fitOrigin` and `World:fitScale`
compute the same number. `render.zones` is handed `nil` in GBC mode (Gold
computes no zone of its own there) and the engine's own one-rect list in
CLASSIC mode; a rect that clamps to nothing is skipped rather than throwing,
which is what `src/render/Renderer.lua:scissorClamped` does on the Gen 1 side.
- *Sprites (`src/pokemon/Sprites.lua`, shared):* `pokemon.sprite`,
`pokemon.icon` and `player.sprite`. `pokemon.icon` is reached from
`src/ui/gen2/PartyMenu.lua` through the shared module, so it is one call site
serving both games. `player.sprite` is raised by `Sprites.playerPic`, which
Gold's battle back pic (`src/ui/gen2/BattleState.lua`), Hall of Fame and
intro call with an already-resolved path: Gold's trainer art is not in
`field.playerPics`, so the path is found first and the hook raised over it,
with the Gen 1 `ctx` keys (`side`, `kind`, `demo`, `battle`, `data`)
unchanged. The Gen 2 trainer card is the one player-art read still outside
it: its portrait is a tile sheet that also carries the frame tiles, not a
swappable pic.
`pokemon.sprite` has a second site of its own in
`src/ui/gen2/BattleState.lua`, which adds `letter` (Unown) and `shiny` to the
Gen 1 ctx keys -- both concepts Red does not have.
- *Save and the script VM:* `save.created`, `save.loaded`, `save.loading`,
`save.writing`; hooks `save.write`, `save.new_game`, `script.command`, and
the `script.started` / `script.ended` pair off `src/script/gen2/Vm.lua`.
`script.command` reports a mod's own row under the name `"modcommand"` with
the row's real operands, and may rewrite them, on the same path it wraps a
cart row.
## New in Gen 2
These have no Gen 1 analogue -- Red has no friendship byte, no day care egg,
no Pokegear, no radio, no held items -- so they are the only places a new name
is justified. They are **live**, guarded by `Runtime.wants` /
`Runtime.wantsHook`, and each is driven through a real bus by
`tests/engine/gen2_new_seams.lua`.
### Events
| event | raised from | payload |
| --- | --- | --- |
| `happiness.changed` | `Happiness` (`ChangeHappiness`, `StepHappiness`) | `mon`, `event`, `reason` (`"event"` / `"step"`), `delta`, `from`, `to` |
| `breeding.egg_created` | `Breeding` (`DayCare_InitBreeding`) | `egg`, `mother`, `father`, `compatibility`, `stepsToEgg` |
| `egg.hatched` | `Breeding` | `mon`, `egg`, `slot`, `species`, `nickname` |
| `phone.call_received` | `PhoneRing.script` | `call`, `contact`, `name`, `className`, `special`, `scriptKey` |
| `clock.day_changed` | `Clock` | `day`, `previous`, `reason` |
| `pokerus.infected` | `Pokerus` | `party`, `slot`, `mon`, `strain`, `days`, `source` |
| `roamer.moved` | `Roamers` | `index`, `slot`, `species`, `from`, `to`, `reason` |
| `roamer.encountered` | `Roamers` | `index`, `slot`, `species`, `level`, `mapId` |
| `apricorn.converted` | `Apricorns` (Kurt) | `apricorn`, `ball`, `event` |
| `bug_contest.scored` | `BugContest` | `mon`, `score`, `place`, `results` |
| `unown.unlocked` | `Unown` (`UpdateUnownDex`) | `letter`, `name`, `word`, `count` |
| `radio.channel` | `MapRadio` | `station`, `channel`, `name`, `source` |
| `mail.written` | `Mail` | `entry`, `slot`, `mon`, `message`, `author`, `source` |
| `mail.read` | `Mail` | `entry`, `message`, `author`, `top`, `bottom` |
| `intro.boot.copyright` | `CopyrightSplash:enter` | `screen`, `game` |
| `intro.boot.gamefreak` | `GameFreakPresents:enter` | `screen`, `game` |
| `intro.boot.movie` | `GoldSilverIntro:enter` | `screen`, `game` |
| `intro.boot.movie_ended` | `GoldSilverIntro:finish` | `screen`, `game`, `skipped`, `frames` |
| `intro.boot.title` | `TitleState:enter` | `screen`, `game` |
The four `intro.boot.*` cards are the GS boot cinema, and they are the one part
of Gold's intro with no Gen 1 moment to share a name with: Red boots into
`IntroMovie` with no copyright card, no GAME FREAK splash and no attract movie.
The Oak speech immediately after them is the opposite case and reuses
`intro.oak_speech.*` verbatim (see the shared table above).
Each card raises its name the frame it comes up, because that is the moment a
mod can act on. Only the movie has an `_ended` name, and only because it
carries a fact nothing downstream does -- `skipped` is the difference between a
player who watched all 2335 frames and one who pressed START. The other three
cards chain straight into the next card, whose own event is their end.
`delta` on `happiness.changed` is `to - from`, not the table's column, because
the 0 and $ff carry clamps are part of what the cart applied: a mon at 254
gaining "5" gained 1.
`clock.day_changed` compares against a process-local latch, so the first read
after a boot has nothing to compare against and raises nothing. That is by
design; it is a day *change*, not a day report.
`unown.unlocked` is raised from `UpdateUnownDex` -- a form first entering the
`#DEX` list -- not from the four `ENGINE_UNLOCKED_UNOWNS_*` puzzle flags. Those
flags are written by the cart's own `setflag`, so there is no Lua transition at
the puzzle solve to hang a second event on yet.
`mail.read` rides `Mail.lines` with a per-struct latch, because the read page
redraws every frame. The latch is re-armed by `Mail.get` / `Mail.mailbox`,
which is how both readers pick the letter they are about to open, so reopening
the same letter raises a second event.
### Hooks
| hook | wraps | ctx | vanilla answer |
| --- | --- | --- | --- |
| `held_item.trigger` | `Battle:heldEffect` | `battle`, `mon`, `item`, `def`, `effect`, `parameter`, `trigger` | `ctx.effect, ctx.parameter` |
| `breeding.compatibility` | `Breeding.compatibility` | `data`, `mon1`, `mon2`, `dayCare` | the vanilla byte |
| `phone.contact_list` | `Phone`'s `wPhoneList` read | called `(save, list)`, the shape the other list hooks use | the same list |
| `shiny.roll` | `Mon` | `dvs`, `species`, `def`, `level` | the DV-derived boolean |
| `gender.roll` | `Mon` | `def`, `dvs`, `ratio`, `species`, `level` | the DV-derived gender |
`held_item.trigger` is one hook over eight call sites, because on the cart
those eight *are* one routine (`GetUserItem` / `GetOpponentItem` loading b and
c, and the caller comparing b against the `HELD_*` it cares about). `trigger`
says which comparison is about to happen: `"priority"` (Quick Claw),
`"damage"` (Scope Lens and the type-boost family), `"endure"` (Focus Band),
`"flinch"` (King's Rock), `"accuracy"` (BrightPowder), `"confuse"`,
`"residual"` (the end-of-turn Leftovers / Berry / cure arm), and `"check"` for
any other read. Return nil to make the item do nothing at that trigger, or
another `HELD_*` name to substitute one -- every call site compares against a
name, so substitution is the whole mechanism.
`held_item.trigger` wraps the *read*, so a mod can suppress or substitute an
effect from any item. Defining a **new** held item is the `held_items`
registry's job, and the two compose: register the row, then steer it from the
hook.
`phone.contact_list` refuses an answer of the wrong length or with an unknown
contact id (unknown ids blank to 0 on purpose, so the Pokegear never indexes a
nil). It reorders and blanks the ten save slots; registering a contact id the
game does not know is `phone_contacts`' job.
`shiny.roll` does not override a forced-shiny battle (`opts.shiny`), which is
how the cart's own scripted shiny Gyarados stays shiny.
## Registries with no Gen 2 home
Writing to one of these while Gold is running takes the write, drops it, and
reports it once per mod into the same error feed the manager shows. It is not
fatal: a mod that supports both generations registers its Gen 1 content
unconditionally and still loads the half that applies. The report is worded
from the boot's own generation, because the gating runs both ways.
`rulesets`, `transitions`, `field`, `text_pointers`, `link_fields`,
`map_scripts`.
`Schemas.GEN2` in `src/mods/Schemas.lua` is the authoritative table, and
`tests/engine/gate_gen2_mod_api.lua` holds it to the catalog.
The list used to have three causes behind it and now has one. "No Data path
exists" closed when the overworld tables stopped loading off disk into World
fields. "The shape differs" closed when a registry gained the option of
carrying a Gen 2 record schema beside its Gen 1 one. What is left is one cause:
**Gold reimplements the system without reading a registry.** The Gen 1 target
is still built and merged into, but nothing in a Gold boot ever looks at it, so
routing the registry would be a merge into a table with no reader -- exactly
the silent no-op the gate exists to prevent. Closing one of these is a consumer
change in the Gen 2 module first and a routing row second:
- `rulesets`: no Gen 2 ruleset dispatch exists.
- `transitions`: Gold draws its own battle intro
(`src/ui/gen2/BattleTransition.lua`), and its `STYLES` is a boolean *set* of
the four cart wipes (`spin`, `speckle`, `zoom`, `sine`) rather than the
`{ frames, draw, sound, flash }` record this registry carries. There is no
styleDef lookup for a registered id to reach, so a mod style would fail the
`STYLES` membership test and fall back to vanilla -- routing it would be the
silent no-op, not the fix.
- `field`: the Gen 1 overworld's data grab bag. Gold's equivalents live in
`data.gen2Maps` and the VM's own tables.
- `text_pointers`: Gen 1's `TEXT_*` indirection. Gold's text *is* pointers.
- `link_fields`: link play is Gen 1 only.
- `map_scripts`: `data.gen2Scripts` is the cart's bytecode pool keyed by ROM
pointer, and a Lua row list merged into it is not something
`src/script/gen2/Vm.lua` can run. Routing it needs a Gen 2 side dispatcher in
`World`, not just the verb table `mod.commands` already has. The
`script.started` / `script.ended` / `script.command` seams do fire, so a mod
observes and can veto a script it cannot yet author whole.
Four of this list closed after it was written, and how they closed is the
pattern for the rest:
- **`growth_rates`** now routes to the SHARED Gen 1 target. Gold's curves are
coefficient rows in the extracted `pokemon.lua`, so `src/mods/Builtins.lua`'s
Gen 2 registrant wraps each as the `{ expForLevel }` record Gen 1's registry
uses, and `src/battle/gen2/Mon.lua:growthFor` is the one accessor all six
readers go through (`Mon` twice, `BattleState`, `SummaryMenu`, `Breeding`,
`ItemEffects`). One record shape, one id space, one mod source for both
games. Because it is routed, the `pokemon` schema's `growthRate` reference is
now checked rather than skipped, and it resolves: both sides say
`GROWTH_MEDIUM_SLOW`.
- **`tokens`** was on the list by mistake rather than by cause. `TextBox.new`
runs `TextBox.substitute` on every box in both generations and `substitute`
reads `game.data.tokens`, so the shared target was live on Gold the whole
time. A `{NAME}` a mod registers expands in the world, the menus and the VM's
pages alike.
- **`battle_sprite_scales`** closed consumer-first, the `growth_rates` way:
`src/ui/gen2/BattleState.lua` grew `imageScale` / `picScale`, a faithful
mirror of Gen 1's `BattleState.imageBattleScale` / `resolveBattleScale` down
to skipping `_owners` and the image-then-species-then-default order, so the
registry now routes to the SHARED Gen 1 path and one record serves both
games. Only the default is generation-specific, and neither side reads that
from the registry.
- **`render_pipelines`** closed because the reader moved, not the registry:
`src/core/Game2.lua:load` installs `src/render/Pipelines.lua` on Gold's
merged dataset after `mods:load` and `Game2:draw` composites `present`. The
`drawWorld` half is still inert, which is why this one is worth reading the
caveat above for -- it is routed on the strength of the half that works, and
Gold retires a drawWorld-only level rather than pretending.
## Hooks and events Gold does not raise yet
Gold has its own draw path, intro, evolution and sprite lookups, so the call
sites in those Gen 1 modules are not on Gold's path. The names are not taken
and not reserved for Gen 1: when a Gen 2 call site lands it uses the existing
name and the existing payload, plus fields where Gen 2 genuinely carries more
(the split special stats, held items on a trainer roster).
The list is much shorter than it was. What is outstanding, in descending value:
- `pokemon.before_give` / `pokemon.received`: Gold has no give-mon seam of its
own yet.
- `link.*` and `trade.completed`: a Gold boot offers no link menu at all. The
Gen 2 fingerprint and handshake exist (`src/link/Fingerprint.lua` hashes a
Gen 2 surface and a cross-generation pairing is refused by name), but nothing
in `src/ui/gen2/` opens onto the protocol, so these raise nowhere.
Four groups that used to sit here have since landed and moved to the shared
table above: the frame seams (`render.compose` / `render.hud` /
`render.letterbox` / `render.zones`, `input.step` / `input.pointer`), the three
battle seams (`battle.overlay`, `battle.low_health_alarm`,
`battle.catch_exp`), the two sprite lookups (`pokemon.sprite`,
`pokemon.icon`), and the catch/evolution trio (`pokemon.caught`,
`pokemon.evolved`, `evolution.check` -- `src/ui/gen2/BattleState.lua` emits
`pokemon.caught` from `pushCaught` once the mon is in the party or the box, and
`src/core/gen2/Evolution.lua` emits `pokemon.evolved` from `apply` and wraps
each row's decision in `evolution.check`).
Three partial coverages worth knowing about, because "the hook exists" is not
the same as "the hook sees everything":
- `encounter.roll` / `encounter.species` are wired into the grass/water step,
`randomwildmon`, the Bug Contest and SWEET SCENT, but **not** into
`World:tryHeadbutt`, `World:rockMonEncounter` or `Roamers.checkEncounter`.
Those three read row shapes that are not `{ species, level }` slot lists, so
a mod that reskins encounters misses headbutt trees, rock smash and the
roamers.
- `src/ui/gen2/BattleState.lua` builds a flat `opts` for `Catching.attempt`
with no `data` in it, so a mod-registered ball is readable through
`Catching.recordFor` but is not yet resolved at the real throw site.
- Three Gold UI files carry their own copy of the status HUD labels the merged
`statuses` records now hold as `hudLabel`, so a mod status shows no label in
the battle HUD, the party menu or the summary page until they read
`Battle.statusRecordFor(data, status).hudLabel`. The values are identical
today, so nothing vanilla is affected.
## Gen 2 tables with no registry
`Game2:load` assigns 24 `data.gen2*` tables and 12 of them are registry-backed,
so twelve sit in `game.data` on a Gold boot with no registry pointing at them:
`gen2Marts`, `gen2Roofs`, `gen2StdScripts`, `gen2EventTables` (the phone book,
in-game trades, elevator labels, decoration descriptions), `gen2InitialEvents`,
`gen2Pokedex`, `gen2MenuGfx`, `gen2Intro`, `gen2Credits`, `gen2Diploma`,
`gen2Trade`, and `gen2Scripts` (which the `map_scripts` registry does reach, so
it is the one of the twelve that is not out of reach). Naming registries for the
rest is new API surface rather than a routing change, so it is deliberately not
done yet.
## Testing a Gen 2 mod
Static first. `gen2check` reads the manifest, scans every `.lua` the package
carries and cross-references what it finds against the coverage table above:
```sh
python3 tools/modkit.py gen2check my_mod # or a path
python3 tools/modkit.py gen2check my_mod --notes # + the caveat on each backed member
```
It reports one of `will load`, `will load but degrade` or `will not work`, with
a `MK4xx` finding per site and an `unresolved:` note, carrying a file and a
line, for every reach a static scan could not follow. Exit 0 clean, 1 on a
fatal finding (or any finding under `--strict`), 2 on usage; `--json` emits the
whole batch as one document, and `--quiet` prints the findings alone, so a
clean mod prints nothing and the exit code is the answer. The rule ladder is
`MK400`-`MK410` and is listed in `tools/modkit.py`'s section header.
Then the headless harness, which takes the generation without booting Gold:
```lua
local run = T.sdk.loadMod("mods/my_mod", { generation = 2 })
T.eq(run.mod and run.mod.state, "loaded",
"runs on gen 2: " .. tostring(run.mod and run.mod.skipReason))
T.eq(#run.errors, 0, "and loads with no boot errors")
```
Everything else is the production path: same loader, same validate, same
topological sort, same merge. Assert the state as well as the error count: a
gate skip is deliberately not an error, so `#run.errors == 0` passes for a mod
that never ran a line.
Neither substitutes for a real Gold boot, and the two output channels there are
not the same. The adapter's own warnings (`Gen2Compat.warnOnce`) go to the log
only, each attributed to the mod holding the facade. The boot error feed the
manager shows is `loader.errors`: a failed mod, a duplicate id, a registry with
no Gen 2 target, a cross-validation problem, and a require for a Gen 1 module
the adapter does not serve. A skipped mod and a degraded member are on neither
list, by design.
+226 -5
View File
@@ -12,12 +12,35 @@ The modding book lives on the
- [Registry reference](https://github.com/bryanthaboi/gen1recomp/wiki/Reference-Registries)
— every registry, generated from `src/mods/Schemas.lua`.
Regenerate the reference straight into a wiki checkout:
Regenerate the reference. With no argument it writes in-repo, to
`docs/modding/reference/registries.md`; name a wiki checkout to write the
wiki's own page name into it instead:
```sh
luajit tools/gen_registry_docs.lua
luajit tools/gen_registry_docs.lua ../gen1recomp.wiki
```
## Mods and Gold (Gen 2)
The mod API is one API across both generations, but Gold runs its own battle
engine, overworld, script VM and save format, so a mod says which games it is
for and Gold serves a declared subset of the surface.
- [`docs/preparing-your-mod-for-gen2.md`](preparing-your-mod-for-gen2.md)
the migration guide: what breaks, the `games` manifest key, the module
adapter, the patterns no adapter can fix, and a worked before/after.
- [`docs/mod-api-gen2-compat.md`](mod-api-gen2-compat.md)
the reference: every registry, hook and event, whether Gold serves it, and
the record-shape differences where it does.
Start with the checker, which reads your manifest and scans your Lua against
the adapter's own coverage table:
```sh
python3 tools/modkit.py gen2check mods/my_mod
```
## Editing maps in Tiled
Maps are data, not assets, so they can be authored in a real map editor and
@@ -35,6 +58,16 @@ An edited vanilla map becomes a `mod.content.maps:patch` carrying only the
fields that moved; a new map becomes a `:register`. See
`docs/new-features.md` and the extension's own README.
## Read-only map overviews
`mod.world:mapOverview()` returns collision `rows` at map-cell resolution,
optional visual `tileRows` at 2x resolution, and optional `tileDetailRows` at
4x resolution. Visual rows contain Game Boy shades from `"0"` (lightest) to
`"3"` (darkest); their matching width and height fields describe the grid.
`markers` contains active `{ kind, x, y }` points in map-cell coordinates for
`warp`, visible `item`, and untaken `hidden` locations. All fields are
read-only snapshots; mods choose which layers to render.
## Rendering pipelines
Most registries hand the engine *content*. `render_pipelines` hands it
@@ -100,6 +133,44 @@ Three rules worth knowing:
Returning `nil` from `drawWorld` is a normal answer meaning "not this
frame"; the engine draws the vanilla world instead.
## Variable-size overworld sprites
The `sprites` registry keeps the vanilla 16x16 grounded walker as its default,
but a mod can describe any frame rectangle and anchor for player characters,
NPCs, followers, mounts, vehicles, bosses, or other field actors:
```lua
mod.content.sprites:register("SPRITE_COMPANION", {
image = "mods/example/companion.png", -- one frame per row
frames = 6,
walker = true,
frameWidth = 32,
frameHeight = 32,
anchorX = 16, -- frame-relative bottom-center anchor
anchorY = 32,
})
```
`frameWidth` and `frameHeight` are sheet pixels. `anchorX` and `anchorY` are
measured from each frame's top-left; when omitted they default to the frame's
horizontal center and bottom edge, so a larger sprite grows upward while its
feet stay on the same world cell. Omitting all four fields is exactly the
vanilla 16x16 placement. The normal player/NPC/follower draw paths consume
these values automatically, including horizontal flips and the fishing pose.
Custom render pipelines can use the same geometry without reproducing the
pose rules:
```lua
local geometry = sprite:getPoseGeometry(facing, walkPhase, stepFlip)
-- geometry.quad, .x/.y/.width/.height, .anchorX/.anchorY, .mirror
local originX, originY = sprite:getScreenOrigin(px, py, camX, camY)
```
`getFrameGeometry(frame)` is the corresponding accessor for a specific
zero-based sheet frame. Both accessors return fresh tables and share the
renderers frame selection and mirror conventions.
## Battle sprite scaling
The enemy's front pic draws at 1x and the player's back pic at 2x, the way
@@ -140,6 +211,90 @@ default** (1x front, 2x back).
ball-to-pic grow multiplies your scale through each stage, so a rescaled
mon still grows into place from the ball, grounded the whole way.
## Durable tool storage and runtime checkpoints
`mod.save` remains the right place for state that should travel with the next
normal Pokémon SAVE. Tools that need independently written, larger data-only
records can use `mod.storage`; the engine scopes every logical key by game
version, opaque playthrough identity, and mod id, and routes it through the same
standard or portable persistence backend as saves:
```lua
local context, code, message = mod.storage:context(game)
local ok, code, message = mod.storage:write(game, "history/quick/q0001", {
format = 1, createdAt = os.time(), payload = { money = 3000 },
})
local value, code, message = mod.storage:read(game, "history/quick/q0001")
local keys, code, message = mod.storage:list(game, "history/quick")
local deleted, code, message = mod.storage:delete(game, "history/quick/q0001")
```
`context` returns `{ engineVersion, gameVersion, playthroughId }`. The engine
version is compatibility metadata; physical launcher-slot and path identity stays
private.
Values must be tables containing serializable data only. Keys are conservative
slash-separated segments (letters, digits, `_`, `-`); paths and filesystem
handles are never exposed. Writes are staged and decode-verified, reads recover
from a valid staged/backup generation, and methods return structured errors for
normal data or I/O failures. The playthrough identity is allocated lazily on the
first storage/checkpoint call, so an unused API changes no save bytes.
`mod.checkpoints` captures and reconstructs engine-owned semantic runtime state:
```lua
local capability = mod.checkpoints:inspect(game)
if capability.canCapture then
local checkpoint, code, message = mod.checkpoints:capture(game)
-- Store the detached data-only checkpoint through mod.storage.
end
local ok, code, message = mod.checkpoints:restore(game, checkpoint)
```
Checkpoint format 1 supports settled overworld control and proven battle
player-decision safe points. Battle checkpoints are limited to ordinary
single-player wild/trainer origins with no suspended script; link, Safari,
ghost, demo, scripted, animation, message, queue, and forced-action phases fail
closed. New checkpoints preserve gameplay RNG, while legacy overworld records
without RNG remain loadable. Capture excludes global options and runtime
objects. Restore validates format, game/playthrough identity, content,
coordinates, battle relationships, continuation, and RNG before mutation;
preserves current options; suppresses normal map-entry/save-load/intro side
effects; verifies a recapture; and rolls back runtime plus RNG in memory if
reconstruction fails. Callers that need crash recovery should durably capture
their own recovery checkpoint before restore.
Checkpoint ownership follows the persistence model rather than mod identity:
- canonical `game.save` progress, including every mod's `save.modData` /
`mod.save` bucket and data-only fields added to saved Pokémon, rewinds;
- global and per-mod options remain at their current values;
- independently written `mod.storage` records do not rewind; and
- mod-owned runtime objects, references, and caches are never serialized.
Successful restore emits `checkpoint.restored` only after reconstruction and
differential recapture have committed. Mods that cache rewound progress or hold
references to reconstructed runtime objects can re-read their own public state
and rebuild at that point:
```lua
mod.events:on("checkpoint.restored", function(ev)
-- ev.kind is "overworld" or "battle"; ev.game is fully reconstructed.
cachedQuestStage = mod.save:get("quest_stage", 0)
rebuildRuntimeFor(ev.game, ev.kind)
end)
```
The event is not emitted for validation failure, failed reconstruction, or a
successful rollback. Its payload contains no checkpoint data or other mod's
private state. A mod that deliberately stores progress-coupled truth in
`mod.storage` must version and reconcile that relationship itself; the engine
cannot distinguish it safely from independent history, configuration, or cache
data.
See RFC 0003, RFC 0004, and RFC 0005 for exact contracts and error codes.
## Developer console
Boot with developer mode on to unlock the in-game console and hot-reload
@@ -221,10 +376,76 @@ the finished `worldCanvas` and `uiCanvas` with their SGB `zones` / `worldZones`,
`worldActive`, the frame metrics (`ww`, `wh`, `pw`, `ph`, `ox`, `oy`, `vpw`,
`vph`, `scale`, `Sx`, `Sy`, `dpiX`, `dpiY`), `renderer:blitCanvas(...)` for a
palette-correct blit of either canvas into an arbitrary screen rect, and the
`secondScreen` bridge (`available()` / `push(imageData, w, h)` / `setEnabled`)
for driving a second physical display. This is what lets a mod lay the two
passes out as two stacked Game Boy screens, or push one onto a second screen,
without the engine knowing the layout.
`secondScreen` bridge (`available()` / `push(imageData, w, h)` / `pollTouch()` /
`setEnabled`) for driving a second physical display. `pollTouch()` returns the
oldest queued event as `"action,x,y"` in submitted-frame coordinates, or `nil`.
This is what lets a mod lay the two passes out as two stacked Game Boy screens,
or push one onto a second screen, without the engine knowing the layout.
`screen.render_visible` receives `(next, state)` while the main screen is being
composed. Return `false` to omit that state from drawing, opacity selection and
palette-zone ownership. The state remains on the stack and keeps its normal
update and input ownership, so a mod can mirror a native menu on another
display without reimplementing it. The default is `true`. Treat the wrapper as
a pure predicate: the renderer may ask it more than once per frame.
Scrollable list states expose `state.kind` for use with this hook. Generic
lists fall back to their title; PC lists use stable, localization-independent
identifiers: `pc_box_withdraw`, `pc_box_deposit`, `pc_box_release`,
`pc_box_change`, `pc_item_withdraw`, `pc_item_deposit`, and `pc_item_toss`.
`battle.bottom_ui_visible` and `battle.status_hud_visible` independently
control the battle text/menu layer and the HP/status panels. Both receive
`(next, state)` and default to `true`, so vanilla rendering is unchanged.
Pushed text boxes also pass through `battle.bottom_ui_visible`; a wrapper that
only owns battle presentation should return `false` only for its active battle
or text-box state.
`core.logic_speed` receives `(next, game)` once per `Game:logicSpeed()` call
(once per frame). Vanilla behavior resolves the per-category GAME SPEED
option (`GameSpeed.CATEGORIES`: overworld/battle/menu) for whichever
category `Game.speedCategoryInStack` says is active right now. A mod may
call `next(game)` and return its result to pass that resolution through, or
return a different number outright to override it for that frame (a bot mod
forcing 1X for one route segment, say, regardless of the category or saved
option). The result is clamped to the nearest valid `GameSpeed.LEVELS` entry
regardless of what a subscriber returns, so a bad value (0, negative, `nil`)
cannot destabilize the fixed-step accumulator. This hook runs *after* link
play's 1X lock and the `--speed`/equivalent run-argument override, both of
which stay unconditional and are never visible to a subscriber.
Developer mode also arms the mod loader's dev tripwire, which flags mods
that reach outside their permission set.
## Process-lifecycle hooks
These exist so a platform-specific launcher integration (a native shell
that embeds this engine and wraps its window in platform UI) can live
entirely in a mod instead of hand-patching `main.lua`, which every other
engine change also touches.
`core.update` receives `(next, game, dt)` once per frame from
`love.update`. Vanilla behavior is `game:update(dt)`, unconditionally. A
mod may skip calling `next(game, dt)` to pause the simulation for that
frame (e.g. while a native settings sheet is on top), and may run
additional per-frame polling before or after that call regardless of
whether it calls `next` -- useful for one-shot flags that must be observed
every frame even while paused.
`core.quit_to_launcher` receives `(next)` once from `love.quit()`. `next()`
returns the engine's own decision for whether closing the window should
return to the Lua launcher instead of exiting; a mod may return `false`
outright, without ever calling `next`, to veto that and let the process
really quit -- for a platform host that owns its own "return to launcher"
UI and would otherwise get looped straight back into the game it just
quit.
A manifest may also declare `force_enable_env`, an environment variable
name that re-enables the mod regardless of a saved disable in
`options.mods` when that variable is set to `"1"`. This is for a mod that
cannot function disabled on the one build where its env var is set (a
platform-bridge mod bundled only with that build's launcher, for example).
Neither hook needs a `Runtime.wantsHook` guard before calling it: `Hooks:call`
already falls straight through to the vanilla function when no mod has
wrapped the name, at negligible cost.
File diff suppressed because it is too large Load Diff
+42 -633
View File
@@ -1,634 +1,43 @@
# New features (deliberate additions beyond the original)
# New Features
Features intentionally added beyond the original Pokémon Red, Blue, and Yellow games:
* **Survey zoom** with connected-map rendering and configurable void fill
* **Perspective tilt mode** for an HD-2D-style overworld
* **Multiple color modes**, including original, SGB, advanced GBC, monochrome, and classic green
* **Optional GBC screen effects**, including pixel grids, shadows, glare, and transparency
* **Performance presets** and configurable FPS limits
* **Peer-to-peer link play** for trades and battles between Red, Blue, and Yellow
* **Persistent custom options** stored separately from game saves
* **Optional widescreen battle layout**
* **Mobile touch controls** with editable layouts, vibration, and orientation settings
* **Translation and custom font support**
* **Built-in save editor** for parties, boxes, items, events, maps, and Pokédex data
* **Tiled map editing tools** for mod authors
* **Pokédex diploma and printer image exports**
* **Community mod browser**
* **Soft reset button combination**
* **Keyboard and controller rebinding**
* **Mod profiles** with separate mod settings and save slots
* **Improved launcher and save editor UI**, including background downloads and update checks
* **Direct-launch options** for shortcuts, Steam entries, and handheld frontends
* **Custom boot branding**
## Pokémon Gold (Gen 2)
A fourth game the launcher can import and play, built from pret/pokegold the same way Red/Blue/Yellow are built from pokered. Port extras beyond the cartridge:
* **Separate Gold save file** beside the Gen 1 ones
* **COLOR, zoom, tilt, GBC FX, and quick save/load** on the same keys as Gen 1
* **UI that stays fixed while the overworld zooms**
* **Border-block surrounds** for maps smaller than the screen
* **Gold-specific launcher options**
* **Optional widescreen battle layout**
* **Skippable trade animation** with B or START
* **QUIT and EXIT GAME** from the menus
* **Mod manager** with Gen 1 mod adapters, per-game targeting, and `modkit gen2check`
* **Followers** for mods, plus Gen 2-only registries and hooks
* **On-screen touch pad** and controller SELECT for registered items
Actual approximations, and missing original behavior are documented separately in `docs/known-differences.md`.
Intentional enhancements this port adds on top of faithful Pokémon Red, Blue,
and Yellow behavior. They have no Game Boy equivalent and are kept by design.
Genuine divergences from the original (things still missing, wrong, or
approximated) live in docs/known-differences.md; faithfully-ported behavior is
in docs/behavior-porting-notes.md.
## Survey zoom
The mouse wheel (or `-`/`=`), the Options **ZOOM** row, or hotkey `4`
zooms the overworld between 1 pixel per world pixel (full survey) and 2×
the window fit scale (close-up), in crisp integer steps. This has no Game
Boy equivalent:
- Connected maps render their full bodies, and their NPCs appear as
visual-only "ghosts", they wander but have no sight lines, triggers,
dialogue, or collision until the map is actually entered.
- Menus, text boxes, and battles draw at normal scale on top of the
zoomed world. Zoom input is ignored while a script, menu, or battle is
active; the zoom offset is persisted as `save.options.zoom` (default
`0` = FIT) and survives New Game via `options.lua`.
- Hotkey `4` ticks through every integer zoom level (survey → FIT →
close-up → wrap). The Options row shows `FIT` / `OUTn` / `INn`.
- Beyond the border ring the void fill repeats indefinitely (see VOID
FILL below); interiors keep their own border block. Each visible map
area is colorized with its own SGB palette (the original recolored the
whole screen per map).
- Neighbor maps load two connection hops out so corner-adjacent maps
don't pop in and out, and ghost NPCs share instances with the real ones
so their wander positions persist across seamless connection crossings
(a warp or fresh map entry still respawns everything at its script
position, like the original's per-entry sprite init).
## VOID FILL
The Options **VOID FILL** row picks what paints the infinite beyond-edge
space on OVERWORLD-tileset maps during survey zoom:
- **TREES** (default): solid tree wall block `$0F`.
- **WATER**: animated water tile `$14` (same hshift cycle as on-map water).
- **BLACK**: solid black.
Other tilesets are unchanged (house/cave borders stay as authored).
Persisted as `save.options.voidFill`.
## Tilt mode
The `3` key (and the Options menu TILT row) cycles a visual-only perspective
tilt of the overworld through **OFF → 15° → 35° → 50° → OFF** for an HD-2D /
diorama look. Like survey zoom this is purely presentational and has no
Game Boy equivalent:
- The entire map tilts as one rigid ground plane, paths, grass, water,
floors, and every background-tile structure (buildings, trees, fences,
signs; in Gen 1 these are baked into the tile layer, not sprites), so
rows above the player recede and rows below come toward the viewer. Only
things that actually *stand* on the ground draw as upright billboards,
unscaled and pixel-identical to flat mode: the player, NPCs, item balls,
and the standing FX attached to them (emote bubbles, the fishing rod,
the FLY bird). The Poké Center heal-machine overlay stays on the ground
plane with the machine tiles (it is OAM glued to a BG graphic, not a
standing sprite). An earlier revision tried
billboarding buildings/trees/signs too (cutting them out of the ground
per hand-curated per-tileset tables); that chased an endless tail of
special cases, dense tree canopy, fences fused into grass, building
facades with their own baked-in fake perspective, because Gen 1's art
was never drawn with a clean seam between ground and standing scenery. It
wasn't merged; tilting everything but the characters as one plane is the
simpler, shipped tradeoff (buildings recede/foreshorten with the ground
like a photo of a diorama, rather than standing fully upright next to
a full-height character).
- Cycling tweens the angle between levels over ~0.25s rather than snapping;
with tilt fully off the world pass drops back onto the flat blit path, so
flat rendering stays pixel-identical to tilt-off and off costs nothing.
- Tilt input is gated exactly like survey zoom, honored only while
free-roaming, ignored while a script, menu, or battle is active, and it
composes with survey zoom (the zoom scale feeds the projection). The tilt
level is persisted in `save.options.tilt` (default OFF).
- It applies everywhere the overworld draws, interiors and caves included.
Menus, text boxes, and battles render flat on top, unaffected, and the
infinite beyond-the-border-ring fill stays flat by design.
- Collision, movement, sight lines, triggers, encounters, and scripts are
untouched; nothing about the tilt reaches gameplay.
## Colors mode
The `2` key (and the Options menu COLORS row) cycles the display mode
through **OG RED → SGB → ADVANCED → OG → OG INV → SGB INV → CLASSIC → OG RED**
(on Blue the first slot labels **OG BLUE**; on Yellow, **OG YELLOW**).
The first three are the real colorizations; the rest are DMG-shade novelties:
- **OG RED** / **OG BLUE**: the Game Boy Color boot-ROM look for that cart --
one global BG palette + one OBJ palette, every map, no per-map variation
(Red/Blue ship no CGB code, so on a GBC the boot ROM colors them globally).
The player/NPCs keep the boot-ROM OBJ color over the terrain via the OBP
bake + post-zone redraw (`PaletteFX.GBC_BG` / `GBC_OBJ`, or Blue's blue/pink
pair).
- **OG YELLOW** (Yellow playthrough, same `ogred` save id): Pokemon Yellow's
authentic GBC look from `CGBBasePalettes` (`data/palettes_yellow.lua`,
sourced from pret/pokeyellow). Per-map / per-species colors, not a single
boot-ROM ramp -- Yellow was CGB-enhanced.
- **SGB** (default): the per-map Super Game Boy region palettes
(`data/sgb/sgb_palettes.asm`). Sprites tint with the region palette, as on
real SGB. (This is the mode formerly mislabeled "GBC".)
- **ADVANCED**: pokered-gbc SuperPalettes -- real per-tile GBC coloring plus
per-species mon colors (`data/palettes_gbc.lua`). (Formerly labeled
"RED++"; it is the richest colorization rather than anything Red-specific.)
- **OG**: force the four DMG grays (colorization off).
- **OG INV**: inverted DMG grays.
- **SGB INV**: each SGB zone palette with shade order reversed.
- **CLASSIC**: original Game Boy pea-soup greens
(`#9BBC0F` / `#8BAC0F` / `#306230` / `#0F380F`).
The shade-remap transform is applied centrally in `PaletteFX.sendColors`, so
it covers overworld, menus, battles, and tilt upright billboards. OG RED's
global BG palette is supplied by `OverworldState:overworldBgColors` (per-map
override in the overworld pass). Persisted as `save.options.colors`; the
`gbc` / `gbc_inv` / `redpp` save ids are kept for back-compat under the new
labels.
## GBC FX
The `5` key (and the Options menu GBC FX row) cycles a "played on real
unlit-GBC hardware" post-process through **OFF → 1 → 2 → 3 → 4**. The
levels are a cumulative ladder:
- **1**: reflective-screen backing transparency.
- **2**: + LCD pixel grid.
- **3**: + pixel drop shadows.
- **4**: + sunlight glare and rainbow shimmer with a drifting light.
It runs as a final present pass after world + UI composite in
`Renderer:endFrame`, inspired by the Pixel Transparency RetroArch shader
([github.com/mattakins/Pixel_Transparency](https://github.com/mattakins/Pixel_Transparency)).
Default OFF; persisted as `save.options.gbcfx`.
Mobile GPUs often compile the pass but present a black frame, so Android and
iOS hide the row entirely, pin the level to OFF, and rewrite a level already
persisted in `options.lua` (issue #136). `POKEPORT_GBCFX` overrides that
decision either way, same tri-state as `POKEPORT_TOUCH`: `=0` refuses the
effect, `=1` forces it available. The Anbernic handheld pack exports `0` from
its launcher because the device reports `"Linux"` while its GPU is in the
phone class (see [Anbernic RG34XXSP](anbernic-rg34xxsp.md)).
## Performance tier (low-end devices)
The Options **PERFORMANCE** row scales the port's optional presentation
extras down for weaker hardware. The extras it governs are the three
heaviest things the port adds on top of the original -- the whole-screen 3D
**TILT** (transforms the entire map as a ground plane), the **GBC FX**
post-process shader (a fullscreen pass), and survey **ZOOM** (zooming out
renders the connected neighbor maps, a lot of extra overdraw) -- plus a hard
FPS ceiling. None of this touches game logic, which is fixed-step off `dt`
(`src/core/FixedStep.lua`), so every tier plays identically; they differ
only in how much eye-candy the renderer is allowed to do.
| Tier | TILT | GBC FX | Survey ZOOM | Extra FPS ceiling |
| ------------ | ---- | ------ | ----------- | ----------------- |
| **HIGH** | on | on | on | none |
| **BALANCED** | off | off | on | none |
| **LOW** | off | off | off | 60 |
| **AUTO** | picks a default from the device (below) |||
- **AUTO** (the default) reads the device once at boot: ARM Linux handhelds
(e.g. the RG34XXSP) resolve to **LOW**, phones/tablets and very-low-core
desktops to **BALANCED**, and everything else -- a normal desktop, and
every existing `options.lua` that predates this option -- to **HIGH**,
so the common case is unchanged. See `src/core/Performance.detect`.
- AUTO only chooses the *default*; all four tiers are selectable, so a
wrong guess is one row away from being overridden.
- The clamps are applied **live** against your stored options and never
rewrite them (`Game:applyOptions`), so a lower tier hides your TILT / GBC
FX / ZOOM without forgetting them -- raising the tier restores exactly
what you had. (This is why the TILT / GBC FX / ZOOM rows still show your
saved choice on a clamped tier: it's your preference, waiting for a tier
that can afford it.)
- Persisted as `save.options.performance` (`auto` | `high` | `balanced` |
`low`); unit-tested in `tests/engine/performance_tiers.lua`.
## Peer-to-peer link play (lua-enet)
Trades and link battles connect two copies of the game directly over
lua-enet (ENet ships inside LÖVE, nothing to install, no server to run)
on a reliable-ordered channel, replacing the original standalone Python
room-code relay (`tools/relay_server.py`, deleted). HOST A GAME shows the
host's LAN address (UDP 7777; `POKEPORT_LINK_PORT` overrides); JOIN A
GAME enters it. Closing performs a graceful ENet disconnect so the final
confirm/bye always lands; a vanished peer exits with "The link was
broken." Internet play needs a forwarded UDP port or a VPN (deliberate
tradeoff vs. the relay). Headless tests drive the protocol over an
in-memory loopback (`Net.loopbackPair`); under LÖVE the same test file
also exercises real UDP pairing.
Red, Blue, and Yellow copies link with each other, as the real cable
does. The compatibility fingerprint hashes only data a link mode can
actually read, so Yellow's Dragonair/Dragonite catch-rate retunes (the
only R/B/Y link-surface difference) no longer read as different games
(issue #511). Moving the fingerprint is a link parity change: builds
from before this fix will refuse to pair with builds after it.
## Fair play in link and online matches
A link session is decided by the battle and nothing else, so for its
duration:
- **Game speed is pinned to normal.** The GAME SPEED option and
`POKEPORT_SPEED` are ignored from the moment LINK PLAY opens until it
closes, and apply again after. Fast-forward otherwise runs one peer's
queue faster than the peer it is locked to and drains a tournament shot
clock faster than the opponent racing it.
- **Online play runs vanilla, except for your language.** Picking ONLINE
MATCH or TOURNAMENT with mods enabled offers to switch the gameplay ones
off and relaunch (mods merge at boot, so a restart is the only way). The
restart is confirmed, not silent. They stay listed as disabled, ready to
switch back on. A mod that declares itself a translation and provably
writes nothing but text stays on: the two games hash the same link
surface, so a Spanish install and an English one can battle and trade,
each reading the game in its own language and naming the other player's
party out of its own text.
- **Only a meaningful split ends a match.** The per-turn state signature
both peers exchange is split three ways: `actives` and `bench` carry
species, HP, status, stat stages, PP and the rest of the party, and a
divergence there ends the match as a draw. `volatile` carries per-turn
flags both sides recompute anyway - a divergence there is logged and
reported to mods, and play continues.
The relay logs which component diverged on which turn, so a desync report
names something specific.
## Custom boot text
The boot sequence replaces the Nintendo / GAME FREAK identifiers with
"bois club" / "bryanthaboi", a deliberate branding customization. The
rest of the boot beats (copyright splash, "presents" shooting-star, the
Nidorino-vs-Gengar attract scene) mirror the original.
## Custom Options
Options persist in a standalone `options.lua` (separate from the game
progress `save.lua`), so audio/display/battle preferences survive New Game
and aren't wiped when a save slot is cleared. Changing a row in the Options
menu or cycling hotkeys `2`/`3`/`4`/`5` writes immediately; an in-game save also
flushes the live options. Old saves that still embed an `options` table are
migrated once into `options.lua` on load.
- Music / SFX volume
- PIKACHU VOL (0-7, Yellow only): trims Pikachu's PCM voice clips under the
SFX level, so the follower's constant chatter, the title-screen cry and
every in-battle "Pika!" can be pulled down (or muted at 0) without
quieting the rest of the sound effects. The row is hidden on Red/Blue,
which have no voice clips.
- Music Filter
- OG GLITCHES on / off (Gen 1 quirks vs. modern-clean battle rules)
- BATTLE LAYOUT (OG / WIDE); see "Widescreen battle layout" below
- COLORS (OG RED / SGB / ADVANCED / OG / OG INV / SGB INV / CLASSIC), also
hotkey `2` (OG RED = GBC boot-ROM look; ADVANCED uses pokered-gbc
SuperPalettes + per-species mon colors)
- TILT (OFF / 15 / 35 / 50), also hotkey `3` while free-roaming
- ZOOM (FIT / OUTn / INn), also hotkey `4` while free-roaming; wheel and
`-`/`=` step one level and save
- VOID FILL (TREES / WATER / BLACK) for OVERWORLD beyond-edge space
- GBC FX (OFF / 1 / 2 / 3 / 4), also hotkey `5`
- MAX FPS (30 / 40 / 50 / 60 / 75 / 90 / 100 / 120 / 144 / 160, default 60),
a hard render frame-rate cap (`save.options.fpsCap`).
## Battle transition cascade + white battle letterbox
Into-battle wipes still run the original eight styles inside the classic
160×144 letterbox. On wide/tall windows (survey zoom), matching black 8×8
blocks cascade outward from that square into the surrounding world so the
void outside the OG wipe fills in lockstep. Once the battle state is up,
letterbox voids around the battle canvas fill **white** instead of black
so the whole window reads as one continuous battle screen.
## Widescreen battle layout
Options **BATTLE LAYOUT** picks the battle screen's composition: **OG**
(the default: the original 160×144 arrangement, unchanged) or **WIDE**,
which gives battles a 304×144 native-pixel surface and a Gen 3-style
arrangement on it:
- the foe's status box upper left, the foe's picture upper right;
- the player's picture lower left, the player's status box lower right,
with a longer HP bar and the numeric HP under it;
- a full-width message window;
- a split "What will X do?" prompt / 2×2 command window;
- a 2×2 move menu, navigated with all four directions, with a PP and type
panel attached to its right.
Only the composition changes. Pictures, palettes, HP-bar colors, font
pages, window borders, sounds, animations, timing and every battle rule
stay the engine's, so a COLORS mode or an asset mod still owns the look.
Each side's picture keeps its original pixels and placement math and is
composited into its own region of the wider battlefield -- nothing is
scaled or squeezed -- and animations, which are authored in the original
160-pixel space, shift as one rigid group onto whichever side they play
on. The whole screen is drawn at the window's integer fit scale for the
wider surface, so a 304-pixel screen is drawn a step smaller than a
160-pixel one in the same window.
The wide surface is live only while the battle itself is the screen on
top: a party menu, the bag or a nickname prompt is a 160×144 screen and
brings the classic surface back with it.
## On-screen touch controls (mobile)
On Android/iOS the game draws a translucent d-pad (bottom-left), A/B
buttons (bottom-right, Game Boy diagonal), and +/- START/SELECT (bottom
center) over the frame, using Xelu's CC0 controller prompts
(`assets/touch/`). Real buttons, not gestures: press lands the frame the
finger does, sliding on the d-pad changes direction without lifting, and
multi-touch chords (e.g. hold a direction + tap B) work. The overlay only
appears while no controller is being used: the first gamepad button or
stick push hides it, the next screen touch brings it back, and unplugging
the last controller restores it immediately. Layout re-derives from the
window size on rotation. Desktop testing: `POKEPORT_TOUCH=1 love .` forces
the overlay on and lets the mouse act as a finger (`=0` forces it off).
The launcher's **Touch Controls** button opens a drag editor: move each
button freely, resize the whole pad with **-/+** (60% to 160%), **Disable**
to hide the overlay permanently (for controllers / emulation handhelds --
distinct from the temporary gamepad auto-hide), **Reset** for defaults,
**Done** to save into `options.lua` as normalized window fractions so a
different screen keeps the relative placement.
Portrait and landscape are edited and saved separately (#633): the editor
follows whichever orientation is on screen, and **Reset** only clears that
one, so a layout that works held upright does not have to double as the
one used sideways. An `options.lua` from before this split keeps its single
layout in both orientations until one of them is edited. In-game, Options →
**TOUCH PAD** toggles the same on/off flag without leaving a play session.
## Haptic feedback (mobile)
Options → **VIBRATION** (also in the launcher's gear menu) buzzes the device
the instant an on-screen control takes a button (#806). A glass pad has no
edges under a thumb, so the pulse is what tells you the press landed:
sliding the d-pad from one direction to the next buzzes again, a second
finger landing on a button that is already held does not, and releasing
never does.
Four levels: **OFF**, **LIGHT** (the default), **MEDIUM**, **HEAVY**.
"Intensity" is really a pulse length -- the platform call takes a duration
and nothing else -- so LIGHT is a 12 ms tick, MEDIUM 25 ms, HEAVY 45 ms.
Stepping the row fires one sample pulse at the level you land on, so the
three can be compared without leaving the menu. On iOS the system
vibration has one fixed length, so all three levels feel the same there and
the row is effectively on/off. The setting lives in `options.lua` and the
row only appears where the on-screen pad can (Android/iOS, or desktop with
`POKEPORT_TOUCH=1`, where it does nothing since desktop LOVE has no
vibrator).
## Screen orientation lock (Android)
Options → **ORIENTATION** (also in the launcher's gear menu) locks the
screen to **PORTRAIT**, **LANDSCAPE** (either landscape, following the
device), or **REVERSE LANDSCAPE**, or leaves it on **AUTO** (#592). AUTO
allows every orientation but defers to the system: with auto-rotate turned
off in Android's quick settings, the game stays put instead of following
the sensor (#716). Changes apply immediately -- the screen rotates as the
row is stepped -- and persist in `options.lua`. Android only: iOS follows
the app's fixed orientation list, and desktop windows rotate nothing.
## Translation support
Every string the player can read is now reachable from a mod, so a
translation is an ordinary content mod rather than a fork.
Two things had to change. Text layout stopped counting bytes: the dialogue
box measures a line in glyphs (charmap sequences), so a 3-byte character
costs one column, a cut never lands inside a character, and a page with a
non-default `advance` re-measures instead of overflowing. That also fixed
25 vanilla English lines that were wrapping early because `é` in POKéMON
and POKéDEX costs two bytes ("I study POKéMON as" is 19 bytes and 18
glyphs, and the box was breaking it).
Second, the text the engine writes itself - battle messages, item results,
menu labels, the link-play screens - moved behind `src/core/Strings.lua`
and the new `strings` registry. Extracted script text was already
overridable through `text`; this covers the other half. Entries are keyed
by the English source, so a translation that has not reached a string yet
keeps rendering in English and a half-finished translation stays playable.
Authors generate the whole thing:
```sh
python3 tools/modkit.py translation francais --language "Francais"
```
That scaffolds a mod with every translatable string as an empty catalog,
plus a glyph-page and charmap stub, a naming-grid stub, and a
`francais-worksheet/` directory holding the English to translate from
(deliberately outside the mod: extracted text is ROM content and must not
be packed). `--refresh` re-harvests after an engine update, keeping
existing translations and parking orphaned keys rather than dropping them.
A translation can also skip glyph pages entirely: scaffolding with
`--pixel-font` (or registering `mod.content.font:register("ttf", {})` in
an existing mod) renders text through a bundled TTF covering Latin with
diacritics, Cyrillic, kana and CJK, while box borders and `<PK>`-style
macro glyphs keep their tiles. The font is "Plain Pixel Font" by Douglas
Vautour (Burpy Fresh), licensed under CC-BY 4.0 (5x11 base characters,
11x11 double-width; see `assets/fonts/plainpixel/README.md`). Options on
the registry entry: `file` for a mod-shipped TTF, `size` (the font's
design em; Plain Pixel rasterizes cleanly only at multiples of 15),
`spacing` added to every advance, `yOffset` for vertical alignment
against the 8px cell grid, `bold`, which double-prints at a 1px
offset for fonts whose strokes read too light, and `tiles`, the
characters that keep their ROM tile instead of coming from the TTF.
`tiles` matters for a CJK translation. Sizing the font so a kana fills
the 8px cell leaves Latin narrower than the tile font it replaces, which
pulls the numeric columns out of line: the party menu's `:L12` stops
sitting over `34/ 34`. Naming `"0123456789/:"` keeps those on the
vanilla tiles, so numbers render exactly as they do in English while
kana still come from the font. It takes a string of characters, or a
list when a multi-character charmap sequence is meant.
See the wiki's Translations guide.
## Save editor (bundled, reachable from the launcher)
The save editor ships inside every build instead of being a developer-only
script, and the launcher's SAVE SLOT card grows an **Edit** label next to
Delete on every slot that actually holds a save. Edit suspends the
launcher, opens that slot's file in the editor, and **Close** hands the
process back to the launcher with the slot list re-read (a rename, a badge
or a dex change shows up on the row immediately). Unsaved edits arm a
confirm first, so leaving cannot lose work. `love . --editor` still opens
it standalone, where Close quits instead; `--save <path>` points it at any
file, and a save can be dragged onto the window.
The editor now wears the launcher's visual language - the same navy radial
field, 16px translucent cards, tri-colour version rail and green/yellow/red
semantics - so the two windows read as one app. Six tabs:
- **Party**: the roster with sprites, HP bars and level chips on the left,
and the mon inspector permanently docked on the right instead of floating
over the list. Species, level, DVs and moves all round-trip through the
Gen 1 formulas, so the inspector can never show illegal stats.
- **Boxes**: the 12 PC boxes as a 5x4 grid with a fill meter per box and a
party dock, so deposit and withdraw live in one place. Empty slots are
clickable and create a mon there.
- **Items**: money, a searchable item picker (replacing the arrows that
cycled one id at a time through ~250 items), the configurable bag (20 slots
by default), PC storage
with no slot cap, and the eight badges as toggle chips. The picker, the bag
and PC storage all scroll under the mouse wheel, so the whole catalog is
reachable one-handed without typing a query.
- **Events**: flags, defeated trainers, taken items and per-map object
toggles, with a real filter field and a two-column paged grid.
- **Map**: any map rendered with the game's own renderer, warps followable,
and the player / lastHeal / lastOutdoor spawn points settable by clicking
a cell. Setting lastOutdoor on a map the game would not accept as an
outdoor source is refused with the reason.
- **Dex**: seen / owned completion meters and a four-column grid; owning
implies seen and un-seeing clears owned, exactly as the game requires.
Two rules run through all of it. Every mutation goes through one funnel
that sets the dirty flag and writes the status line together, so nothing
changes silently and no branch can quietly no-op - "Party is full", "Bag is
full", "click a cell first" all say so. And every destructive verb (Remove,
Release, Clear all, Wipe dex) arms on the first click and commits on the
second, relabelling itself to `Confirm?` in between.
A validation pill in the tab rail mirrors what the running game would
quarantine on load; clicking it jumps to the tab holding the first problem.
## Tiled map editing (mod authoring)
`tools/tiled_export.py` turns the imported ROM cache into a Tiled workspace,
so maps can be edited in a real map editor and exported back out as a mod.
It has its own document: docs/tiled-map-editing.md.
## Pokédex diploma (both versions)
The Celadon Mansion 3F game designer shows the dex-completion diploma
once 150 species are owned. On Yellow, the graphic artist next to him
then offers to print it, saving the certificate as a PNG under `prints/`
in the save directory, and Bill's PC gains Yellow's PRINT BOX item which
exports the current box list the same way.
## Pokédex printing (Yellow)
Yellow's Game Boy Printer PRNT option in the Pokédex side menu is stood in
for by an image export: choosing PRNT renders the mon's entry page (sprite,
kind, number, height/weight, dex text) to a PNG at 4x scale under
`prints/` in the save directory, then reports the filename in a dialog.
No printer hardware or link cable emulation involved; the file is the
printout.
## Find Mods (community mod indexes)
A FIND MODS tab sits beside MODS in the launcher and browses a published
mod index: a metadata-only feed listing mods that live in their authors'
own repositories. No index ships with the launcher and none is ever added
automatically, so the tab opens on an "Add an index" prompt until you name
one; paste an index URL or its `owner/repo` and it is remembered in
`options.lua`. More than one index can be added, and the listings merge.
A feed author can publish per-mod release stats by adding three optional
fields to an entry -- `downloads` (total across every release), and
`first_release` / `last_release` (ISO days) -- which the listing shows in
the same gold line the MODS tab uses. When a feed does not carry them,
the row fetches the mod's own GitHub releases instead -- the same cached
`ModUpdate` fetch the MODS tab uses, one entry per frame -- so the stats
appear for any mod with a `github` field regardless of feed maintenance.
The fields are additive: feeds that carry them stay readable by every
build that predates them, and feeds that do not render exactly as before.
## Soft reset (all versions)
Holding A, B, START and SELECT together restarts the game the way flicking
a Game Boy's power switch did, dropping straight back to the title screen.
It works from anywhere, including mid-battle, which the QUIT entry on the
start menu cannot do: the original combo is how stationary and gift
Pokemon get their stats rerolled without sitting through a full relaunch.
Unsaved progress is discarded, exactly as on hardware.
As on the original, the four buttons have to stay held for 16 straight
polls (better than a quarter of a second) and any direction in the mix
cancels it, so it is hard to hit by accident -- including on the on-screen
touch controls, where it would take four fingers held on four separate
controls.
## Controls rebinding (CONTROLS screen)
OPTIONS -> CONTROLS lists every Game Boy button with its current keyboard
key and controller button side by side (Z/A). Press A on a row, then press
and release the key or pad button you want; the rebind commits on the
release. If that input already belongs to another row, the two rows swap,
so no button is ever stranded without an input and no input ever serves
two buttons. Holding a second key or pad button while the first is still
down backs out of the capture without touching a keyboard; Escape still
cancels too. SELECT clears one row back to its default, and START resets
every binding after a confirmation.
Controllers a system has no mapping for (common on Linux handhelds and
off-brand pads) report bare button numbers rather than names. Those are
rebindable on the same screen and show up as JOY1, JOY2 and so on in the
controller column. Recognized controllers are read only through their
named buttons, so a rebind on those is never shadowed by the factory
layout underneath it.
## Mod profiles (#593)
The mod manager's PROFILES tab holds named setups. A profile remembers which
mods are on, every mod's own options, and which save slot each game version
plays, so swapping profiles swaps the whole playthrough and not just the mod
list. The setup that existed before profiles shipped becomes PROFILE 1 the
first time the manager opens.
EXPORT.. writes the selected profile to `profiles/<NAME>.g1rmodlist` in the
save directory; drop a `.g1rmodlist` someone shared into that folder and
IMPORT.. adds it. Imported profiles never overwrite an existing one (a name
clash gets a number). Mods the shared profile names but that are not installed
are reported when the profile is applied; installing them is still a manual
trip through the mods list or Find Mods.
## Windows: no console windows on launcher actions
Checking for updates, browsing a mod index, adding a mod repo, installing a
mod and picking a ROM all run a host tool (curl, PowerShell) in a child
process. On Windows those children used to each open their own console
window, so a session could end up buried under half a dozen of them. The
game now claims one console for itself at boot and hides it; the children
inherit that invisible console and nothing pops up. Nothing else changes:
file pickers are ordinary desktop dialogs and still appear normally, and a
run started from a terminal (`lovec.exe`, what `scripts\run.ps1` prefers)
keeps its terminal and its printed output. Set `POKEPORT_CONSOLE=1` to opt
out.
## A faster, higher-contrast launcher
The launcher and the save editor were rebuilt on one small immediate-mode UI
kit (`src/ui/kit/`), replacing the vendored FlexLove layout engine. The
visible result is that the launcher is quick: building and drawing a frame
went from about 9 ms to under 1 ms on the same machine and the same data, at
every window size, so the window keeps up with the pointer instead of
trailing it. Measure it yourself with `POKEPORT_LAUNCHER_PROF=200 love .`.
**Nothing blocks the window any more.** Fetching a mod index, checking a mod
for updates, listing versions, downloading an install and pulling thumbnails
all run on background threads. Opening FIND MODS on a cold cache used to
freeze the launcher for as long as the server took -- often minutes, with no
indication anything was happening. Mod indexes are now also fetched at boot,
so the tab is usually already populated by the time you reach it.
**Anything you wait on says so.** Every operation that takes time raises a
loading panel with a spinner or a progress bar that cannot be clicked around
or dismissed, so a half-finished install can never be interrupted by a stray
click. Work that only affects one row (a mod's update check) shows a small
spinner on that row instead and leaves the rest of the list usable.
**Lists page instead of scrolling.** Mods, Find Mods, save slots, settings,
release notes and the version list all show a fixed number of rows with a
pager underneath, and the number of rows comes from the window height -- a
tall window shows more, a phone shows fewer. A long list costs exactly what a
short one does. The mouse wheel turns pages.
**Updates live in the top right.** The in-app updater moved next to the
settings gear and pulses when an update is waiting, instead of sitting in a
banner at the bottom of the page that you had to scroll to notice. Checking
for updates from there shows a loader like everything else.
**A quit button.** An X sits to the right of the settings gear and closes
the app cleanly, the same shutdown path as the window's close button. Mostly
for platforms where reaching the window chrome is awkward (Android, Steam
Deck, fullscreen desktops).
**The look.** Black background, white outlines, no gradients or glows, and
buttons that are solid colour-coded keys: green commits, blue navigates, red
destroys, yellow wants attention. The three game tabs keep their red, blue
and gold cartridge colours. Everything is about a third larger than before.
The save editor follows the same theme, and adding an item there is now a
searchable pop-up like adding a Pokemon, rather than a cramped list wedged
into the tab.
**Reset rebinds.** Input rebinds are additive, so there was no in-game way to
undo one. A RESET REBINDS row in Settings, and a matching button under Touch
Controls on each game tab, restore the stock keyboard, gamepad and touch
layout. Both ask twice.
## Launch options: boot straight into a game
`love . --game red` skips the launcher and starts that game; `--slot <id or
number>` picks the save slot to load, and `--launcher` forces the launcher
anyway. `POKEPORT_GAME` / `POKEPORT_SLOT` do the same for shortcuts that can
only pass environment variables. This is for one-click entries: a desktop
shortcut per game, a Steam entry, or a handheld frontend. Asking for a game
whose ROM has not been imported opens the launcher on that game's tab rather
than failing.
+784
View File
@@ -0,0 +1,784 @@
# Preparing your mod for Gen 2 (Gold)
You have a mod that works on Red, Blue or Yellow, and you want it to work on
Gold. This is the migration guide: what breaks, what the engine papers over
for you, what it refuses to paper over, and the order to do the work in.
`docs/mod-api-gen2-compat.md` is the reference for *what Gold serves*. This
document is the procedure for *getting your mod there*. Read that one when you
need to know whether a registry or a hook exists; read this one first.
## What actually breaks, and why
Gold is not a skin over the Gen 1 engine. It is a second engine living beside
the first one: `src/core/Game2.lua` owns the boot, `src/world/gen2/World.lua`
is the overworld, `src/battle/gen2/Battle.lua` is the battle, and
`src/script/gen2/Vm.lua` runs the cart's own bytecode instead of a Lua row
list. A Gold boot never loads `src/core/Game.lua`,
`src/world/OverworldController.lua` or `src/battle/BattleState.lua` at all.
The mod API on top is deliberately one API -- the same registry names, the
same hook names, the same event names, the same `mod.*` facade -- so a mod
that stays on that surface mostly moves across unchanged. What does not move
is everything underneath it.
The failure that motivated all of this is quiet, which is what makes it worth
a whole document. A mod with `engine_internals` writes
`local Game = require("src.core.Game")` and patches a method on it. Under Gold
that require used to succeed: the file is on disk, `require` finds it, hands
back a perfectly good module table, and your patch lands on it. Nothing ever
instantiates that table, so the patch runs zero times and the only symptom is
that your mod does nothing. No error, no warning, no crash to bisect. Two
things fixed that. First, a mod is not loaded on a Gold boot unless it says it
is for Gold, so the default outcome is "not running" rather than "running
wrong". Second, when it does say so, a require made from your own file is
answered by an adapter (`src/mods/Gen2Compat.lua`) that presents the Gen 1 API
over Gold's internals, and a member the adapter cannot honestly back reads nil
instead of reading plausibly-wrong.
## Step 1: run the checker before you change anything
`modkit gen2check` reads your manifest, statically scans every `.lua` the
package carries, and cross-references what it finds against the adapter's own
coverage table. Run it first, because it tells you the size of the job in a
few seconds.
```sh
python3 tools/modkit.py gen2check <id-or-path> [<id-or-path>...]
```
Real output, against a follower mod written for Yellow:
```
-- PokePCFollowers_VoxelMerge: api 1, profile content, no games declared, permissions engine_internals, 0 dependencies, game_version unset
MK400 ERROR manifest.json: no Gen 2 game in "games" (and no gen2compat), so a Gen 2 boot skips this mod; the rest of this report is what it would hit once it claims one
MK404 ERROR main.lua:575: BattleState.newWild has no Gen 2 backing: Gold has no factory that returns an unpushed battle, and World:startBattle constructs and pushes in one call. A mod that wraps newWild to rewrite the species must be pointed at the encounter.species hook, which Gold raises with the same name and shape (World:rollEncounter); this reads nil
MK404 ERROR main.lua:576: BattleState.newWild has no Gen 2 backing: ... ; nothing on a Gen 2 boot reads this write
MK409 WARN main.lua:13: allow-lists a Gen 1 version string, which excludes this mod from a Gen 2 game by construction; test for the capability the code needs instead of the version
MK409 WARN main.lua:424: ... (same, a second allow-list)
MK409 WARN main.lua:565: ... (and a third)
modkit: unresolved: 1 site: requires whose result is neither bound to a name nor indexed here, so where the module goes is not followed (main.lua:221)
modkit: unresolved: 5 debug upvalue calls whose target function this scan could not tie to an engine module, so the local they reach could not be resolved (main.lua:279, main.lua:285, main.lua:288, main.lua:321 and 1 more)
modkit: src.world.PikachuFollower.onMapEntered closes over 'shouldSpawn' on a Gen 2 boot, so the upvalue surgery at main.lua:325 lands as it does on Gen 1
FAIL PokePCFollowers_VoxelMerge on gen 2: will not work (3 errors, 3 warnings)
```
Three kinds of line, and the difference matters:
- **`MK4xx ERROR` / `MK4xx WARN`** are findings with a file and a line. Errors
set the exit code; warnings do not unless you pass `--strict`.
- **`modkit:` notes** are things the tool derived rather than found, or could
not decide at all. They never change the exit code. The `shouldSpawn` note
above is the tool resolving that member through the adapter on a Gen 2 boot,
enumerating the function's real upvalues, and confirming the surgery lands;
the `unresolved:` notes are the tool naming, with file and line, every reach
it saw and could not follow.
- **The verdict**: `will load`, `will load but degrade`, or `will not work`.
The rule ladder:
| rule | what it means |
| --- | --- |
| `MK400` | the manifest claims no Gen 2 game, so a Gen 2 boot skips the mod |
| `MK401` | a dependency claims no Gen 2 game, which takes you down with it |
| `MK402` | you require a Gen 1-only module the adapter does not serve |
| `MK403` | a Gen 2 boot runs a `gen2/` sibling of the module instead |
| `MK404` | a member you touch has no Gen 2 backing (the adapter's own reason is quoted) |
| `MK405` | a member you touch degrades and says so once |
| `MK406` | the signature moved under an alias |
| `MK407` | `debug` upvalue surgery the Gen 2 arm cannot take: the member is not a function there, or the function does not close over that local |
| `MK408` | upvalue surgery the scan could not resolve either way |
| `MK409` | a version allow-list, or a Gen 1 screen id |
| `MK410` | the entry chunk reads a member of a game that is not up yet |
Flags: `--strict` promotes warnings to failures, `--notes` prints the adapter's
note for every *backed* member you touch (worth reading once per mod, because
several backed members are backed with a caveat), `--json` emits one document
for the whole batch, `--quiet` drops everything except the findings -- no
header, no notes, no verdict line, so a clean mod prints nothing at all and the
exit code is the whole answer. Exit code is 0 clean, 1 on a fatal finding, 2 on
usage.
Name several mods in one invocation and they are read as one install set, so a
mod and its dependencies can answer each other's `MK401`.
**What the checker cannot see, and now says so.** It is a static scan, not a
run. It follows more than it used to -- a require made through your own
`tryRequire`-style wrapper, `local ok, M = pcall(require, "...")`, an inline
`require("src.world.Map").waterTiles(...)`, a bracket index `M["member"]`, a
local hop `local F = M` -- so reaches that used to be invisible now produce
real findings, and a mod that passed before can fail now.
Two places where it used to answer confidently and wrongly now do not.
`local A, B = require("src.world.Map")` is read as binding `A`, which is what
Lua does; it used to take the name nearest the `=` and pin the module on `B`,
so every reach off `A` went unchecked and every reach off `B` was checked
against a module that was never there. And a helper of your own is only read as
upvalue surgery when the scan can see it forward its own `(function, name)`
pair into the `debug` call; a helper that merely mentions `upvalue`, or that
finds the slot by walking `debug.getupvalue`, no longer has its call sites
read as naming an engine local, because they do not.
What it still cannot follow it names instead of ignoring. Every unfollowed
reach comes back as an `unresolved:` note carrying a file and a line. The scan
side raises one for:
- a require name built at runtime, whether handed in whole or concatenated
(`require("src.world." .. name)` is as unfollowable as `require(name)`);
- an engine module name handed to a call the scan does not follow;
- an engine module name spelled in a literal with no require attached;
- a require whose result is neither bound to a name nor indexed on the spot;
- a require in a multiple assignment whose value it cannot pair to a name;
- a name bound to a *member* of a module rather than the module;
- an engine module indexed with a computed key;
- `rawget` or `rawset` on a bound module: that goes straight to the table the
require shim hands back, so on a Gen 2 boot it reads or writes the
Gen2Compat facade and not the module behind it;
- an engine module read as a value rather than indexed, so where it goes from
there (a table field, a call argument, a metatable's `__index`) is not
followed;
- a `debug` upvalue call whose target function could not be tied to a module;
- a call through one of your own upvalue helpers that the scan could not
confirm carries an upvalue name through to the `debug` call.
Four more come from the coverage side rather than the scan: a dependency that
is not installed beside your mod, a required name that is neither an adapter
nor a module in this checkout, a module with no coverage row at all, and a Gen
1 member the coverage table does not classify.
The practical consequence is worth stating plainly: an empty finding list
*plus* no `unresolved:` notes now means the scan followed everything it saw,
and an empty finding list on its own does not.
It is still silent on any member the adapter's coverage table does not record:
the table lists 481 members across the 15 served modules, which is a large
majority of what real mods touch and is not the whole Gen 1 API. A clean
`gen2check` means "nothing known-broken was found", not "this works". Boot it.
## Step 2: declare which games the mod is for
Nothing moves on disk. A mod is installed once, into `mods/<id>/`, and that one
directory serves every game. There is no `mods/gen1/`, no `mods/gen2/`, and no
per-generation copy: targeting is something the manifest *declares*, not
something the filesystem encodes.
```json
{
"id": "my_mod",
"name": "My Mod",
"version": "1.0.0",
"entry": "main.lua",
"api": 2,
"games": ["gen1", "gen2"]
}
```
`games` is an optional array. Each entry is one of:
| token | means |
| --- | --- |
| `"red"`, `"blue"`, `"yellow"`, `"gold"` | that one game (a version id from `GameVersion.ORDER`) |
| `"gen1"`, `"gen2"` | every game of that generation (case-insensitive; `"gen 2"` also parses) |
| `"all"` | every game this engine has |
`src/mods/ModTargets.lua` is the one place those tokens are resolved, and it
derives the list from `GameVersion.ORDER` rather than restating it, so a game
added later needs no edit there. The scaffold writes the key for you:
```sh
python3 tools/modkit.py scaffold my_mod --games gen1,gen2
```
**Omitting `games` keeps the old meaning exactly.** No `games` key means Gen 1
only, plus Gen 2 if the legacy `"gen2compat": true` flag is set. Every manifest
written before the key existed means precisely what it always meant.
`gen2compat` is still accepted and is purely additive: it *adds* the Gen 2
games to whatever `games` says, so no manifest can lose a game it already ran
on. `Manifest.validate` (`src/mods/Manifest.lua:210-224`) resolves the two
into one ORDER-sorted `manifest.games` array and derives `manifest.gen2compat`
from it, which is why `"games": ["gen2"]` is honoured by the loader's gate
today with no other change.
An unknown token warns and is dropped under `api` 1 and refuses the manifest
under `api` 2 (the normal `violation()` rule). A `games` array that names no
game this engine knows falls back to the default rather than orphaning the mod.
A non-array `games` is a hard error.
### What you are claiming
Adding a game to `games` is you saying *I have run this there*. It is not a
request for best-effort support and the loader does not treat it as one: a mod
that claims a game is loaded on that boot in full, with its registrations, its
subscriptions and its entry chunk, exactly like a mod written for it. If it is
half-working, the player sees a broken mod, not a partially-supported one. That
is the whole reason the key exists rather than being inferred.
**Every token is enforced, per game.** `Loader:_gateGeneration`
(`src/mods/Loader.lua:447`) gates on `ModTargets.supports(manifest, version,
generation)` -- the same call both mod surfaces make -- so `"games": ["blue"]`
really does not load on Red, and the skip line is the launcher's line, `For
Blue, not Red`. `"games": ["gold"]` alone no longer loads on Red either: it
names one game, and that game is Gold. A manifest with no `games` and no
`gen2compat` still covers every Gen 1 game, so nothing written before the key
existed changes behavior; what changed is that a version-id token is now a
statement the boot keeps rather than a label the UIs draw. If you want a mod
everywhere, say so: `["gen1", "gen2"]` or `["all"]`.
**Dependencies are contagious.** A mod whose hard dependency does not run here
is left out too, carrying the dependency's own wording (`depends on X, which
does not run here (For Blue, not Red)`). It is reported as a skip rather than a
failure and neither mod lands on the boot error list, but the mod does not run.
Every hard dependency in the chain has to cover the same games; `MK401` is the
checker's version of this question for the Gen 2 half of it.
**The player can overrule you, in one direction only.** The in-game mod
manager offers `TRY HERE ANYWAY` on the detail pane for any mod that does not
claim *this* game (`src/mods/ManagerState.lua:386`), which now includes a Gen 1
boot: a Blue-only mod is genuinely skipped on Red, so that row is the only way
to run it there. The choice is **per game**: `options.modsGen2[id]` is a
`{ [version] = true }` table, so forcing a mod onto Red does not force it onto
Gold. A stored legacy `options.modsGen2[id] = true` from before the key was
per-game reads as "the Gen 2 games", which is the only set it could ever have
affected, and it is expanded in place the next time the player answers. A
forced mod loads normally and keeps a note saying its author never verified it
here; the launcher shows it as `Forced onto Gold by you (untested)`. If the
override cannot be persisted the manager says `COULD NOT SAVE` rather than
promising a restart that would change nothing.
### What the player sees
All three surfaces read the same derivation -- the two UIs and the loader --
so they cannot disagree about your mod. The launcher's mod panel carries a
`Show for:` chip row (All games / Red / Gold / ...) and a per-mod tag from
`ModTargets.chip` -- `GEN 1`, `GEN 1+2`, `RED/GOLD`, `BLUE` -- greyed out when
the mod does not run on the selected game, with the line `Not for this game`
(`src/import/LauncherView.lua:320`) and the detail from `ModTargets.detail`,
`For Gen 1, not Gold`. The in-game manager shows the same thing as
`ENABLED (NOT THIS GAME)` with the skipped glyph, plus an inert `FOR GEN 1+2`
row on the detail screen. The launcher's dependency verdict asks the same
question of your dependencies: a mod whose hard dependency does not run on the
selected game reads `Needs <id> (not for Gold)` rather than `Ready`.
### One limit worth knowing
**Per-game enable flags are still a preview.** The overlay
`options.modsByVersion[version][id]` exists and every surface goes through
`SaveData.modEnabled` / `SaveData.setModEnabled`, but
`SaveData.PER_VERSION_MODS` is `false`
(`src/core/SaveData.lua:489`). While it is false, `SaveData.modScope` answers
nil for every caller, so the launcher panel, the in-game manager *and* the
loader all read and write the one shared `options.mods` flag and the overlay is
not consulted for enablement anywhere. That matters because the overlay is
plantable from an imported `.g1rmodlist`: keeping every reader on the same
scope as every writer is what stops a stored per-game flag from showing a mod
set no boot would honour. Nothing about this affects a mod author; it affects
what a player can currently express.
Targeting is a different question from enablement and *is* enforced per game,
as above. The two do not share a switch.
## Step 3: prefer the API over the modules
Before doing any adapter work, check whether you need the modules at all. In
new code, take the live game from `mod.game` and the world from `mod.world`.
Both resolve per generation inside the loader (`src/mods/Loader.lua:1021`):
`mod.game` is `src/core/Game.lua`'s singleton under Gen 1 and the `Game2`
*instance* Gold injected under Gen 2, read on every touch rather than cached;
`mod.world` is `src/world/WorldAPI.lua` or `src/world/gen2/WorldAPI.lua` behind
one method set. Neither needs `engine_internals`. The `game.ready` payload and
every `ui.*` hook's first argument carry the same live game.
Anything you can express as a registry write, a hook or an event subscription
is generation-agnostic already and needs nothing from this document. The
adapter exists for the code that was written before Gold did, and for the small
number of things the API genuinely does not reach.
## Step 4: the adapter, module by module
On a Gen 2 boot with mods present, `require` is interposed
(`Loader:_installDevShim`, `src/mods/Loader.lua:184`) and a require *made from
a mod's own chunk* for one of fifteen Gen 1 names is answered by
`src/mods/Gen2Compat.lua`. Engine code is unaffected: the shim compares the
caller's chunk name against the engine tree, so `src/render/PaletteFX.lua`
requiring `src.core.Game` still gets the real Gen 1 module on both generations.
This is not a dev-mode feature; it installs on any Gold boot that has mods.
| the name you require | kind | what you get | backed / warned / absent |
| --- | --- | --- | --- |
| `src.core.Game` | facade | a live proxy onto the `Game2` instance | 70 / 9 / 12 |
| `src.world.OverworldController` | facade | over `src/world/gen2/World.lua` | 56 / 5 / 68 |
| `src.world.Map` | alias | `src/world/gen2/Map.lua` | 28 / 2 / 9 |
| `src.world.NPC` | alias | `src/world/gen2/Npc.lua` | 27 / 0 / 1 |
| `src.pokemon.Boxes` | facade | over `src/core/gen2/Boxes.lua` | 22 / 0 / 0 |
| `src.battle.BattleState` | facade | over `src/ui/gen2/BattleState.lua` | 16 / 2 / 39 |
| `src.ui.PartyMenu` | facade | over `src/ui/gen2/PartyMenu.lua` | 15 / 2 / 16 |
| `src.world.WorldAPI` | alias | `src/world/gen2/WorldAPI.lua` | 12 / 2 / 0 |
| `src.world.PikachuFollower` | alias | `src/world/gen2/Follower.lua` | 10 / 0 / 11 |
| `src.script.ScriptRunner` | facade | over `src/script/gen2/Vm.lua` | 10 / 7 / 1 |
| `src.ui.OptionsMenu` | facade | over `src/ui/gen2/OptionsMenu.lua` | 8 / 0 / 1 |
| `src.world.FieldDefaults` | facade | the `playerSprites` answer and named refusals | 5 / 2 / 3 |
| `src.world.Collision` | facade | `DELTA` / `target` / `occupied` / `canMove` | 4 / 1 / 0 |
| `src.ui.StartMenu` | facade | over `src/ui/gen2/StartMenu.lua` | 4 / 0 / 0 |
| `src.ui.BoxMenu` | alias | `src/ui/gen2/PcMenu.lua` | 1 / 0 / 0 |
**Alias means the adapter *is* the Gen 2 module.** Your monkey-patch, your
`rawset` sentinel and your `==` idempotency check all land on the table Gold
actually runs, and `getmetatable(npc) == NPC` is true. Five names are aliases
because nothing less would work: mods set their own trailer's metatable to
`src.world.NPC`, a mod is handed `world.map` rather than building one, the
loader builds every `mod.world` out of `src.world.WorldAPI` so a copy would
give two, `src.world.PikachuFollower` is reached with `debug.setupvalue` on a
file-local, and `Screens` caches `src.ui.BoxMenu` for `"Gen2PcMenu"` so a
`.new` patch has to land there.
Note that `src.ui.BoxMenu` points at `src/ui/gen2/PcMenu.lua`, not at
`src/ui/gen2/BoxMenu.lua`. Gen 1's `BoxMenu` is Bill's PC *top menu*, whose
Gold counterpart is `PcMenu`; Gold's `BoxMenu` is the withdraw/deposit *list*
that Gen 1 builds inline.
**Facade means a translating wrapper.** `.overworld` resolves `Game2.world`,
`writeOptions` resolves `Game2:persistOptions`, `game.data.sprites` resolves
`data.gen2Sprites`, `NPC.new(data, mapId, objDef)` is sniffed apart from
`NPC.new(mapId, objDef, spriteDef)` and the movement vocabulary is translated
with it. The four UI facades (`PartyMenu`, `StartMenu`, `OptionsMenu`,
`BattleState`) are write-through: reads fall to the Gen 2 class and **writes go
to the Gen 2 class**, so `PartyMenu.update = wrapper` still patches the live
class Gold pushes. Your write also *reads back as your own value* -- after
`PartyMenu.new = wrapper`, `PartyMenu.new` is `wrapper` and nothing else, so
`rawequal` holds and an idempotency check works. That is what makes the ordinary
capture-and-chain idiom safe: a wrapper that calls the value it captured reaches
Gold's real constructor rather than re-entering the facade's own override.
Writing `nil` clears the member instead of re-exposing the override underneath.
The `src.world.OverworldController` facade is a facade over the live `World`,
not over a class, so seven of its fields (`map`, `player`, `npcs`, `entities`,
`ghosts`, `npcPool`, `camera`) read **and write** through to the running world:
Gen 1's module *is* the singleton, so a write has to land somewhere real. A
write made before a world exists is dropped with a warning rather than
shadowing the world it would have applied to.
### backed, warned, absent
The adapter publishes what it covers, and the checker consumes that same table
rather than a copy of it. Exactly three statuses, and a member listed as both
resolves to the weaker one:
- **`backed`** -- present, and it does the Gen 1 job on Gold. Read the note
anyway where there is one: several backed members are backed with a caveat
(`Boxes.COUNT` is 14 on Gold and not 12; `BattleState.say` ignores
`sayAuto`'s delay because Gold's messages always auto-advance;
`Collision.DELTA` is Gold's live table, so adding a key mutates Gold's own
movement).
- **`warned`** -- present, answers nil or degrades, and names itself once in
the log with your mod attributed. `Game.renderer`, `Game.load`,
`Game.step`, `game.data.field`, `game.data.constants`,
`ScriptRunner.resume` / `.update` / `.parallel`, `PartyMenu.tmhm` and
`OverworldController.neighbors` / `.npcByIndex` are here. `neighbors` is the
shape of the whole category: Gold's rows are `{ id, ox, oy, image }` where
Gen 1's are `{ map = mapDef, ox, oy }`, so the field warns and answers nil
rather than handing back a list whose `nb.map` is nil on every row.
- **`absent`** -- deliberately not on the table. It reads nil, which is the
honest failure. `BattleState.newWild`, `OverworldController.rollEncounter`,
`Map.warpPadOrHoleAt`, `PikachuFollower.shouldSpawn` and 157 others are
here. (`shouldSpawn` is absent as a *module member* on both generations: it
is a file-local, reached through `setShouldSpawn` or the upvalue of that
name, and the coverage table says so rather than implying a field exists.)
"Absent" means *not served*, not *wrong*. Every one of them was left off for a
stated reason, and the reason is in the coverage note. `BattleState.newWild` is
the clearest case: Gold has no factory that returns an unpushed battle, because
`World:startBattle` constructs and pushes in one call, so a `newWild` taking a
species and a level would be a lie about what Gold's battle screen is. The
route for the thing you were actually doing (rewriting the species of a wild
encounter) is the `encounter.species` hook, which Gold raises under the same
name with the same shape.
A member the table does not record is not a guarantee of anything. What it does
depends on the adapter: an alias hands you the Gen 2 module's own member,
whatever that is; a write-through facade falls to the Gen 2 class; the
`src.core.Game` facade names it in the log and reads nil; the
`src.world.OverworldController` facade reads nil silently. The checker is
silent about it too.
### Reading the coverage yourself
The table is queryable, and it is the same query the checker makes:
```lua
local Gen2Compat = require("src.mods.Gen2Compat")
Gen2Compat.modules() -- the 15 served names, sorted
Gen2Compat.serves("src.world.Map") -- true
Gen2Compat.memberStatus("src.battle.BattleState", "newWild") -- "absent"
local c = Gen2Compat.coverage("src.world.Map")
-- { module, kind = "facade"|"alias", target, members = { [name] = status },
-- notes = { [name-or-topic] = "one line" } }
```
`Gen2Compat.COVERAGE_VERSION` is 1 and `Gen2Compat.STATUS` carries the three
status strings. `notes` keys are documentation topics, not a member list:
dotted paths (`save.money`), field names (`warpAt`), hook names
(`hook ui.pc.items`) and bare topics (`identity`, `iteration`, `rawset`) all
appear there. `members` is the authoritative set.
To dump the lot for one module:
```sh
luajit -e 'package.path="./?.lua;"..package.path
local G=require("src.mods.Gen2Compat")
local c=G.coverage("src.world.OverworldController")
for m,s in pairs(c.members) do print(s,m) end
for k,v in pairs(c.notes) do print("note",k,v) end'
```
## The patterns no adapter can fix
Five shapes come up in nearly every real Gen 1 mod, and none of them can be
fixed on the engine side without lying to you. Each one has a route that works
on both generations.
### 1. A hardcoded version allow-list
```lua
local v = GameVersion.get()
if v ~= "red" and v ~= "blue" and v ~= "yellow" then return false end
```
This excludes you from Gold by construction, and it does so *after* everything
else in your mod has been made to work, which is why it produces the most
confusing possible outcome: the adapter resolves, your patches land, and the
feature still never appears. `MK409` catches it.
**Instead**, test for the thing the branch actually depends on. If it is there
because a member might be missing, test the member:
```lua
local Follower = require("src.world.PikachuFollower")
if Follower.setShouldSpawn then ... end -- present on Gold, absent on Gen 1
```
If it is there because a piece of per-cart content might be missing, test the
content -- `mod.find` and the merged data tables answer that in both games.
Version tests stay legitimate for genuinely per-cart *content*, which is what
Yellow's starter rename is; they are never right as a gate on a whole feature.
### 2. String-matching a screen id
```lua
if id == "BoxMenu" then ... end
```
Gold's builtin screens are registered under `Gen2`-prefixed ids, so this
matches nothing there. `Screens.GEN2_IDS` in `src/ui/Screens.lua` is the full
list, 51 ids: `Gen2BoxMenu`, `Gen2PartyMenu`, `Gen2NamingScreen`,
`Gen2Credits` and 47 more. `MK409` catches this exact line: it keys off the
string literal itself, not off a screen-shaped word elsewhere on the line, so
`if id == "BoxMenu" then` is flagged where it used to slip through. The price
of that is deliberate breadth -- any literal equal to a Gen 1 screen id with a
`Gen2` twin is warned about, wherever it appears -- so the message states what
is true of the literal rather than guessing what the surrounding code meant.
It is a warn, and reading past a false one costs you nothing.
**Instead**, either match both ids, or stop matching ids and take the seam the
screen offers. Most screens a mod wants to decorate raise a hook whose name is
shared across both generations -- `ui.start_menu.items`, `ui.options.rows`,
`ui.party.submenu`, `ui.pc.items`, `ui.naming.grid`, `ui.list_menu` -- and a
hook subscription needs no id at all. Where you genuinely must key off the id:
```lua
local BOX_IDS = { BoxMenu = true, Gen2PcMenu = true }
if BOX_IDS[id] then ... end
```
Watch the pairing. `ui.pc.items` has the same name on both sides but a
different menu behind it: Gen 1 raises it over the WHICH-PC list, Gold over
Bill's PC's own rows. And Gen 1's `BoxMenu` pairs with `Gen2PcMenu`, not with
`Gen2BoxMenu`.
### 3. `debug.setupvalue` on an engine local
```lua
local idx = findUpvalue(PikachuFollower.update, "shouldSpawn")
debug.setupvalue(PikachuFollower.update, idx, myPredicate)
```
This only ever worked because the Gen 1 file happened to hold that predicate in
a file-local of that name. Nothing about the engine promises it, and on the Gen
2 side the local has to exist under the same name and hold the same thing for
the surgery to land. Today it does: `src/world/gen2/Follower.lua:23` declares
`local shouldSpawn` for exactly this reason, so follower mods reaching for it
work unchanged on Gold. That is a deliberate courtesy, not a contract.
`MK407` fires in the two cases where the surgery cannot land: when a Gen 2 boot
resolves the member to something that is not a function (so `debug.setupvalue`
raises), and when the function it does resolve to does not close over that
name, in which case the message quotes the upvalues it *does* close over. The
check resolves the member through the adapter exactly as the loader does and
enumerates the resolved function's real upvalues, so a local that merely
appears somewhere in the Gen 2 file is never mistaken for one -- that used to
be the check, and it blessed surgery that landed on nothing. `MK408` fires when
the scan could not resolve the member either way, which is what you get when
`luajit` is not on `PATH`: the check degrades to an honest warn, never to a
reassuring note.
**Instead**, use the named seam when there is one, and fall back only when
there is not:
```lua
if Follower.setShouldSpawn then
Follower.setShouldSpawn(myPredicate) -- Gen 2, and any future Gen 1 arm
else
patchUpvalue(Follower.update, "shouldSpawn", myPredicate) -- Gen 1 today
end
```
`Follower.setShouldSpawn` writes the same cell `debug.setupvalue` reaches, so
the two cannot disagree. Note the presence test is doing real work:
`src/world/PikachuFollower.lua` has no `setShouldSpawn`, so this is not a
rename you can apply blindly. Note also that the predicate is called
`(game, world)` on Gold where Gen 1 passes `(game, ow)` -- the same object under
a different name, so a predicate reading `ow.player` or `ow.map` is unchanged.
### 4. Capturing state off `src.core.Game` at file scope
```lua
local Game = require("src.core.Game")
local save = Game.save -- nil forever
local party = Game.save.party -- error at load
```
The module require itself is fine and is meant to be: the Gen 2 `src.core.Game`
is a proxy that reads the live `Game2` instance on *every* touch, precisely so
that a mod capturing it at file scope, before a save or a world exists, keeps
working once they do. What does not survive is capturing a *field* off it at
file scope, which snapshots nil. This is true on Gen 1 as well; Gold just makes
it bite more often because the entry chunk runs earlier relative to the world.
`MK410` catches the file-scope read of a member the Gen 1 module only ever
writes as `self.<name>`.
**Instead**, read through the facade at the moment you need the value, or take
the live game from the `game.ready` payload:
```lua
local Game = require("src.core.Game")
mod.events:on("game.ready", function(ev)
local game = ev.game -- the real Game2 instance
local party = Game.save.party -- read now, not at file scope
end)
```
Three further properties of the proxy that a Gen 1 mod can trip over, all
recorded in the coverage notes:
- **Identity.** The proxy can never compare equal to the `Game2` instance the
`game.ready` payload carries. Lua 5.1 fires `__eq` only when both operands
share a metatable, so `Game == ev.game` is false on Gold. Do not use it as
an idempotency check.
- **Iteration.** `pairs`, `next` and `rawget` see an *empty* table, because the
proxy holds nothing of its own. Enumerate the `game.ready` payload instead.
- **`rawset`.** `rawset(Game, k, v)` lands on the proxy, reads back correctly
through the same facade, and is completely invisible to the engine. That
read-back is what hides it. Use a plain assignment, which writes through to
the live instance.
The save layout moved too, and those fields are absent rather than aliased so
that a wrong read is loud rather than silent: `save.money` is
`save.player.money`, `save.player.map` / `.x` / `.y` / `.facing` are
`save.position.*`, and `save.player.rival` is `save.rival.name`. `save` itself
is a straight pass-through on purpose.
### 5. Monkey-patching a class, and the two ways it goes wrong
Patching a shared class method is *supported*, and this is worth stating
plainly because it is the thing most authors expect to have to rewrite. The
four UI facades write through: `__newindex` forwards to the Gen 2 class, so
```lua
local PartyMenu = require("src.ui.PartyMenu")
local origUpdate = PartyMenu.update
function PartyMenu.update(self, dt) ... return origUpdate(self, dt) end
```
lands on the class Gold actually pushes. Aliases are the class, so the same
holds there.
Two variants do not work, and neither can be made to.
**Patching a member the Gen 2 class does not have.** The write succeeds, reads
back as your own function, and nothing ever calls it. `BattleState.newWild =
wrapper` is the canonical case: the assignment is taken, and no Gold code path
reads that name. This is the one place the read-back works against you, which
is why `MK404` reports the write site separately from the read site.
**Patching a field on a live instance.** `menu.onSwitch = fn` writes a field
Gen 2 never reads -- Gold takes it as `onChoose` at construction. Same for
`menu.swapFrom` (renamed `switchFrom`) and for `StartMenu`'s `tx` / `ty` / `tw`
/ `th` / `anchor` / `maxVisible`, which do not exist on Gold at all because the
box is fixed at `Chrome.box(10, 0, 10, h)`. A write to any of them is inert.
Pass what you need to `.new` instead: `PartyMenu.new(game, { onSwitch = f })`
with no `battle`, `pickOnly` or `forceSwitch` opens the plain list and calls
`onSwitch(mon, menu)` on A, which is the Gen 1 behavior the facade reproduces.
A close relative worth calling out because it errors rather than no-ops:
`map.warpAt` is a name collision, not a rename. Gen 1's is a *table* keyed by
cell; Gold's `Map:warpAt` is a *method* of the same name. `map.warpAt[cell]`
and `pairs(map.warpAt)` both raise, which is loud but points at your mod.
Enumerate `map.warps`, which Gold carries as an ordered array.
## A worked migration
Here is one real one, start to finish. The mod is a follower pack written for
Red/Blue/Yellow. `gen2check` reports `MK400` on the manifest, `MK404` twice on
`BattleState.newWild` and `MK409` on a version allow-list, plus a note
confirming its `shouldSpawn` surgery lands.
**Before.** Three separate problems in about twenty lines.
```lua
local BattleState = require("src.battle.BattleState")
local PikachuFollower = require("src.world.PikachuFollower")
local GameVersion = require("src.core.GameVersion")
return function(mod)
-- (1) rewrite the starter encounter's species
local origNewWild = BattleState.newWild
BattleState.newWild = function(game, species, level, ...)
if species == "PIKACHU" and level == 5 then species = "CHARMANDER" end
return origNewWild(game, species, level, ...)
end
-- (2) decide whether a follower spawns
local newShouldSpawn = function(game, ow)
local v = GameVersion.get()
if v ~= "red" and v ~= "blue" and v ~= "yellow" then return false end
return packSize(game) > 0
end
-- (3) install it
patchUpvalue(PikachuFollower.update, "shouldSpawn", newShouldSpawn)
patchUpvalue(PikachuFollower.onMapEntered, "shouldSpawn", newShouldSpawn)
end
```
On Gold: (1) assigns onto a name nothing reads, so the species rewrite never
happens. (2) returns false for every Gold boot, so no follower ever spawns.
(3) actually works, and works on a predicate that has already decided to do
nothing. Two silent failures and one correct mechanism pointed at them.
**After.** The manifest gains `"games": ["gen1", "gen2"]`, and:
```lua
local PikachuFollower = require("src.world.PikachuFollower")
return function(mod)
-- (1) the species of a wild encounter is a hook on both generations
mod.hooks:wrap("encounter.species", function(next, enc, ctx)
local rolled = next(enc, ctx)
if rolled and rolled.species == "PIKACHU" and rolled.level == 5 then
rolled.species = "CHARMANDER"
end
return rolled
end)
-- (2) no cart check: whether there is a pack to walk is the whole question
local newShouldSpawn = function(game, ow)
return packSize(game) > 0
end
-- (3) the named seam where there is one, the upvalue where there is not
if PikachuFollower.setShouldSpawn then
PikachuFollower.setShouldSpawn(newShouldSpawn)
else
patchUpvalue(PikachuFollower.update, "shouldSpawn", newShouldSpawn)
patchUpvalue(PikachuFollower.onMapEntered, "shouldSpawn", newShouldSpawn)
end
end
```
`gen2check` now reports clean, and the mod is shorter than it was on Gen 1
alone. That is the usual shape of this work: two of the three fixes replace
engine surgery with an API that existed the whole time, and only the third
needs a generation branch.
The one change that is *not* a simplification is the hook's contract. A wrapper
takes `(next, ...)` and must call `next` with the arguments it was handed, where
the monkey-patch could do as it liked with them. `encounter.species` transforms
a rolled `{ species, level }` and gets a `ctx` beside it: Gen 1 fills in
`mapId`, `terrain` and `rng`, and Gold adds `daytime`, `environment`, `kind`
(`"wild"` / `"contest"` / `"script"` / `"sweet_scent"`), `tables` and `data`.
So the same
subscription serves both games, and a Gold-only refinement is a field test
rather than a second hook. That is the trade: a narrower seam that both engines
raise, in exchange for not owning a function neither engine promised you.
## Testing
**Headless, without a Gold cache.** The SDK harness takes the generation
directly, and everything after that is the production path -- same loader, same
validate, same topological sort, same merge:
```lua
local run = T.sdk.loadMod("mods/my_mod", { generation = 2 })
T.eq(run.mod and run.mod.state, "loaded",
"runs on gen 2: " .. tostring(run.mod and run.mod.skipReason))
T.eq(#run.errors, 0, "and loads with no boot errors")
run.release()
```
**Assert the state, not just the error count.** A gate skip is deliberately not
an error: `Loader:_skip` sets `mod.state` and `mod.skipReason` and stays off
`loader.errors`, because neither the mod nor its dependency has a bug. So
`T.eq(#run.errors, 0)` on its own passes for a mod that never ran a line, which
is the one result you were testing to rule out. `run.mod.state` is `"loaded"`
when the entry chunk ran and `"wrong_generation"` when the gate or the
dependency contagion took it, with `run.mod.skipReason` carrying the sentence
the manager would show. Keep the error assertion too: it is what catches a
registry with no Gen 2 home and a require the adapter does not serve, both of
which *do* land on `loader.errors`.
**On a real Gold boot.** Nothing above substitutes for running it. Import Gold
in the launcher, enable your mod, and play the part your mod touches. Be
precise about where the adapter talks to you, because the two channels are not
the same:
- **The log** carries the adapter's own warnings, each attributed to the mod
holding the facade (`[my_mod] Game.renderer has no Gen 2 backing: ...`), so a
member that degraded tells you which one and why. `Gen2Compat.warnOnce` goes
to `Logger.warn` and nowhere else -- these do **not** appear in the manager.
- **The manager's error feed** (`loader.errors`) is a shorter list: a mod that
failed validation, a duplicate mod id, a registry with no Gen 2 target, a
cross-validation problem, and the one adapter-adjacent case, a require for a
Gen 1 module the adapter does not serve. A skipped mod is not on it, and
neither is a degraded member.
So: read the log for coverage problems, and the manager for load problems.
`POKEPORT_IDENTITY=<name>` sandboxes the save directory if you want a clean
profile to test in, and `POKEPORT_DEV=1` adds the console and `F5` hot reload.
## What this guide does not promise
- **Coverage is partial and will stay partial.** 15 Gen 1 modules are served
out of a much larger engine, and within those 15 the coverage table records
288 backed members against 32 warned and 161 absent. The absent ones are not
a backlog; most are absent because there is no honest Gen 2 answer, and each
one carries its reason. The counts move as the adapter learns something: a
member that turns out to answer nil is demoted from backed to warned or
absent rather than left flattering the table.
- **Absent is not broken, it is not-served.** A nil read is the designed
outcome. If you would rather have an error, test for the member before you
use it.
- **The checker is a static scan.** It cannot follow a require built at
runtime, cannot tie every `debug` call to a module, and says nothing at all
about a member the coverage table does not record. What it *can* do is admit
each of those individually, with a file and a line, as an `unresolved:` note.
Read the notes as part of the report: a clean finding list with notes under
it means "nothing known-broken was found in the part I could follow", and
only a clean finding list with no notes means the scan followed everything.
- **A backed member can still surprise you.** `backed` means the adapter took
responsibility for the Gen 1 call shape, not that Gold behaves identically.
Run `gen2check --notes` once and read the caveats on the members you touch.
- **The adapter is not a compatibility layer for new code.** It exists so mods
written before Gold existed keep working. If you are writing something now,
`mod.game`, `mod.world`, the registries and the hooks mean the same thing in
both games and need none of this.
-96
View File
@@ -1,96 +0,0 @@
# RFC 0001 — Port Yellow's `IsSurfingPikachuInParty` surf sprite
## Status
Proposed. Engine: `Player.lua`, `FieldDefaults.lua`,
`OverworldController.lua`, `RomExtractor.lua`, `PaletteFX.lua`. Tools:
`build_rom_data.py`, `extract/sprites.py`, `make_rom_manifest.py`,
`make_yellow_manifest.py`. Tests: `parity_surfing_pikachu_sprite.lua`,
`mod_world_tests.lua`.
**Regeneration required.** The manifest and sprite sheet update by
re-running `make_yellow_manifest.py` against a `pret/pokeyellow`
checkout, then re-importing the Yellow ROM.
## Motivation
Yellow's `IsSurfingPikachuInParty` + `LoadSurfingPlayerSpriteGraphics2`
(`home/map_objects.asm`, `home/overworld.asm`) swap the player's
overworld sheet to `SurfingPikachuSprite` (`gfx/sprites/
surfing_pikachu.2bpp`, a 16×96 walk sheet — not the minigame sheets)
when the party mon that knows SURF is a Pikachu. The recomp misses this
in two places:
1. **Extraction.** `SurfingPikachuSprite` is not in
`SpriteSheetPointerTable` — loaded by its own `ld de,` like
`RedBikeSprite`. The extractor never sees it, and the symbol is not
in the Yellow manifest.
2. **Engine rule.** `field.playerSprites.surf` is one static
(`SPRITE_SEEL`), cached at boot. No seam for "swap when the SURF-mon
is a Pikachu."
## The decision it extends
No prior D-number. Extends the surf-field-move port in
`docs/behavior-porting-notes.md` (the `IsSurfingAllowed` exact port)
with the player-sprite swap vanilla runs alongside it.
## The exact API delta
Backward-compatible, additive-only.
### `field.playerSprites.surfPikachu`
New optional key alongside `walk`/`surf`/`bike`/`fly`, defaults to
`SPRITE_SURFING_PIKACHU`. Guarded in `Player.new` so before extraction
lands the ride keeps the Seel — no plain on-water Pikachu.
### `Player.surfPikachuSprite`
`Player.new` builds a second `SpriteRenderer` when the field resolves.
`pose()` picks it when `surfing and surfingPikachu`.
### `Player.surfingPikachu` (runtime)
Runtime-only boolean (not persisted); re-derived so a party change
between save and load is honored.
### `OverworldState:syncSurfingPikachu()`
Sets `player.surfingPikachu` from `partyKnows("SURF")`. Called at every
surf-state toggle: trySurf, dismount, flyTo, beginTeleportOut,
warpToHealPoint, forced-surf tile, setMap boot-restore.
### Importer — `SPRITE_SURFING_PIKACHU`
`make_yellow_manifest.py` adds `SurfingPikachuSprite` to
`YELLOW_EXTRA_SYMBOLS`. `make_rom_manifest.py`'s `sprite_metadata()`
gains a `surfPikachu` entry (guarded, so Red/Blue unchanged).
`RomExtractor.extractSprites` + `build_rom_data.py` + `extract/sprites.py`
each gain a parallel extract mirroring `RedBikeSprite`.
### `PaletteFX.spriteObp`
`SurfingPikachuSprite` joins `RedBikeSprite` in the no-bracket-index
special case, wearing the player's OBP palette so it colors in GBC mode.
## Migration note for existing mods
**Nothing.** `surf` still defaults to `SPRITE_SEEL`; `surfPikachu`
only resolves on a Yellow import after regeneration. No manifest or
`mod.save` shape changes. An eligibility hook that swaps a rental
SURF-mon still drives the sprite pick via `partyKnows`.
## Parity tests
- **No-mod** (`mod_world_tests.lua`): `surf == "SPRITE_SEEL"`,
`surfPikachu == "SPRITE_SURFING_PIKACHU"` seeded at boot. The 19229-check
`world & maps v2` suite stays green.
- **Mod-API** (`parity_surfing_pikachu_sprite.lua`): `syncSurfingPikachu`
+ `Player:pose` across four party shapes (12/12). The existing
`parity_cinnabar_east_surf.lua` (24/24) stays green.
## Deprecation etiquette
Nothing deprecated. Additive: a new `field.playerSprites` key, a new
runtime flag, a new engine method, a new sprite id.
+114
View File
@@ -0,0 +1,114 @@
# RFC 0003 — Add a reusable multiplayer session layer
## Status
Proposed. Engine: `Session.lua`, `Net.lua`, `LinkState.lua`,
`Tournament.lua`. Tests: `link_session.lua`.
## Motivation
Link play and tournaments currently own transport lifecycle details and
temporarily remove and reinsert packets in `Net.inbox` when a handshake or
battle starts. That makes packet ownership fragile and gives a future
shared-world mode no stable host/guest-aware boundary to reuse.
The engine needs one small layer that preserves today's wire protocol while
owning received-packet order and terminal cleanup. Pokémon, battle, tournament,
save, and overworld rules remain outside that layer.
## The decision it extends
Extends the existing split between `Net` (backend setup, framing, and relay
controls), `Handshake`/`Protocol` (mode payloads), and the states that
interpret those payloads. It does not replace any of those components.
## The exact API delta
Backward-compatible and internal-only.
### `Session.new(transport, options)`
Wraps one successfully configured Net-compatible transport. `options.role`
is exactly `"host"` or `"guest"`; `options.kind` is a non-empty
local label such as `"link"` or `"tournament"`. Role and kind are
immutable session metadata selected locally and are never inferred from peer
packets.
The facade forwards the narrow fields current consumers need:
`paired`, `code`, `address`, `target`,
`error`, and `closed`.
It forwards valid outbound tables unchanged through `send(message)`.
### Receive and lifecycle methods
- `update()` pumps the transport, validates decoded inbound values, and
appends accepted messages to a private FIFO.
- `pollOne()` removes the oldest queued message.
- `poll()` removes every queued message in order.
- `take(type)` removes the first queued message with that type without
disturbing any other message.
- `hasPending()` reports whether the FIFO is non-empty.
- `getRole()`, `getKind()`, `getStatus()`, and
`getFailure()` expose local metadata and lifecycle.
- `close()` closes the underlying transport once and is safe to repeat.
Statuses are `connecting`, `paired`, `draining`, `closed`,
and `failed`. A transport close or failure becomes `draining` while
accepted packets remain queued. The terminal `closed`/`error`
compatibility projection appears only after that FIFO drains, so a last packet
travelling with a disconnect remains observable.
An inbound value is structurally valid only when it is a table with a string
`type`. Invalid decoded values end the session with a protocol failure.
Unknown but structurally valid types remain queued for the owning mode; the
session does not contain a packet allowlist.
## Authority direction
A later `WorldSession` may compose this facade. In that mode the host
will own the world snapshot, map state, NPC state, event results, and shared
progression. A guest will bring a trainer identity plus their Pokémon party,
inventory, and other explicitly selected profile snapshot.
Guest profile data and commands will be untrusted input. The host must validate
them and must authorize every world mutation before rebroadcasting the result.
The concrete snapshot schema, command vocabulary, conflict rules, and
persistence policy require a separate RFC and are not introduced here.
## Compatibility and security
No packet envelope, message name, payload shape, framing rule, relay protocol,
save schema, or engine protocol version changes. Existing valid outbound
messages encode exactly as before, and existing link and tournament screens
keep their current player-facing behavior.
The layer does not authenticate players or encrypt traffic. Existing LAN and
relay access assumptions remain unchanged; knowledge of a join address or code
still grants the same access it grants today. Authentication, reconnect
identity, rate limits, and abuse controls remain future protocol decisions.
## Migration note for players, mods, and peers
**Nothing.** `LinkState` and `Tournament` adopt the facade
internally. Existing peers receive the same messages, mods gain no new API, and
players do not migrate saves or settings.
## Parity tests
- **ROM-free facade:** constructor validation, immutable role/kind, unchanged
send shape, FIFO ordering, typed retrieval, unknown typed packets, draining,
terminal failure latching, protected transport calls, and decoded-value
rejection.
- **Existing modes:** source guards prohibit direct inbox mutation; headless
module loads and the complete engine tier cover both migrated states.
- **ROM-backed link play:** run the existing link driver when generated ROM
data is available; the normal quick suite remains the required baseline.
## Deprecation etiquette and non-goals
Nothing deprecated. This RFC adds an internal facade and removes no transport
method.
It does not add shared-world packets, co-op screens, a remote actor, save
transfer, server persistence, matchmaking, reconnect, or a protocol-version
bump. Those changes require the world-specific layer and its own review.
+124
View File
@@ -0,0 +1,124 @@
# RFC 0003 — Playthrough-scoped mod storage
## Status
Proposed. Engine: `SaveData.lua`, `SaveSerializer.lua`, `Storage.lua`,
`Loader.lua`. Tests: `playthrough_identity.lua`, `storage.lua`, the existing
save-slot and mod-save suites.
## Motivation
`mod.save` intentionally lives inside the normal progress record. That is the
right home for quest state, but not for independent tool data such as replay
captures, checkpoint histories, or recovery records: writing it would require a
normal Pokémon SAVE, and storing copies of progress beneath `save.modData` would
recursively embed the save that contains them.
Mods also cannot safely infer which launcher slot or portable filesystem backs
the active playthrough. Direct filesystem access would expose private paths and
make isolation dependent on engine implementation details.
## The decision it extends
Extends the per-mod persistence contract documented in `docs/modding.md` and the
wiki's Save Model. `mod.save` and `mod.options` keep their existing behavior.
## The exact API delta
Backward-compatible, additive-only. `Loader:_api` binds a new `mod.storage`
facade to the calling mod id. Mods receive logical keys and decoded values, never
filesystem handles or physical paths.
### Lazy opaque playthrough identity
`SaveData.ensurePlaythroughId(save[, fs]) -> id | nil` allocates an opaque
32-hex-character identity without consuming gameplay RNG. It is called only when
`mod.storage` or `mod.checkpoints` first needs a scope; New Game, ordinary SAVE,
and ordinary load remain byte-compatible when no caller uses either API.
The id is stored in `save.meta.playthroughId` after allocation. Until the next
ordinary SAVE writes it into progress, a mapping in `options.lua` keeps legacy
saves stable by game version and active launcher slot (or the legacy flat-save
scope). A newly created playthrough never adopts the previous playthrough's
mapping for that slot.
`SaveData.persistenceFs([fs])` is engine-only routing used by the storage
implementation. It follows the same standard/portable backend as progress and
honors injected test filesystems; it is not exposed on the mod object.
### `mod.storage:context(game)`
Returns:
```lua
{ engineVersion = "0.9.0", gameVersion = "red", playthroughId = "..." }
```
or `nil, code, message`. `engineVersion` is warning-grade compatibility metadata;
the context intentionally omits launcher slot ids and paths.
### `mod.storage:write(game, key, value)`
Accepts a data-only table and returns `true`, or
`false, code, message`. Keys are nonempty slash-separated segments containing
letters, digits, underscore, or dash. Empty segments, leading/trailing slash,
`.`/`..`, and other characters are rejected.
The engine encodes deterministically, stages and decodes a `.tmp` witness,
preserves the previous valid generation, writes and decodes the main record,
then rolls the verified bytes to `.bak`. A failed stage or replacement leaves a
verified prior generation readable.
### `mod.storage:read(game, key)`
Returns a freshly decoded table, or `nil, code, message`. It tries main, staged,
then backup data. A valid staged/backup value is returned and promoted
best-effort; corrupt bytes are never executed.
### `mod.storage:list(game[, prefix])`
Returns sorted logical keys beneath a valid prefix, an exact key when the prefix
names one, or `nil, code, message`. Physical witness filenames are hidden.
### `mod.storage:delete(game, key)`
Deletes only that key's main, backup, and staged witnesses. Returns `true`, or
`false, code, message`.
### Scope and errors
Physical records are scoped as:
`persistence root / mod_storage / game version / playthrough id / mod id`
Stable error codes are `not_in_playthrough`, `storage_unavailable`,
`invalid_key`, `encode_failed`, `write_failed`, `verify_failed`, and
`not_found`. Ordinary data and I/O failures are return values, not callback-
terminating errors.
The restricted serializer's recursive writer runs outside LuaJIT traces. A
1,000-process GC stress regression found compiled recursion could intermittently
drop a newly inserted nested identity entry and produce undecodable bytes; save
encoding is infrequent and I/O-bound, so interpreter execution is the safe
boundary.
## Migration note for existing mods
**Nothing.** No API is removed, no manifest field changes, and no storage path or
playthrough id is created unless a mod invokes `mod.storage` or
`mod.checkpoints`. Existing save bytes remain unchanged on the no-caller path.
## Parity tests
- **No-mod:** New Game plus ordinary save/load creates no identity or storage
file; the existing save-slot and mod-save suites remain green.
- **Engine identity:** lazy allocation, save/load preservation, stable legacy
mapping, fresh-playthrough replacement, and version/slot isolation.
- **Public Mod API:** two real API-2 entry chunks prove data-only roundtrip,
deterministic listing, key rejection, mod/game/playthrough isolation,
corrupt-main recovery, failure retention, exact delete, and no-mod no-write.
## Deprecation etiquette
Nothing deprecated. The additions are one bound public facade and engine-private
persistence/identity helpers.
+138
View File
@@ -0,0 +1,138 @@
# RFC 0004 — Stable runtime checkpoints for mods
## Status
Proposed. Engine: `Checkpoint.lua`, `Game.lua`, `OverworldController.lua`,
`Loader.lua`. Tests: `checkpoints.lua`, existing world and engine suites.
## Motivation
Mods can observe world events and request semantic actions, but no supported API
can capture canonical progress at a proven-safe runtime boundary or reconstruct
the overworld without replaying map-entry scripts. Reaching into the state stack,
controller, ScriptRunner, or save restore internals would bind distributable mods
to private objects and can duplicate story side effects.
The engine is the only component that can authoritatively decide whether the
runtime is settled and rebuild its controller objects. A generic checkpoint seam
lets tools store data-only records while keeping those responsibilities private.
## The decision it extends
Extends the public world/tool surfaces in `docs/modding.md`. It does not change
`mod.world`, normal CONTINUE, vanilla SAVE, or save lifecycle hooks/events.
## The exact API delta
Backward-compatible, additive-only. `Loader:_api` binds `mod.checkpoints`; mods
never receive `Game`, StateStack, controller, coroutine, renderer, or filesystem
internals inside a checkpoint.
### `mod.checkpoints:inspect(game)`
Returns a capability record. Stable overworld control returns:
```lua
{ canCapture = true, canRestore = true, kind = "overworld" }
```
A refusal returns the same booleans as `false` plus `kind`, `reason`, and a
player-readable `message`. Format-1 supports only an overworld whose controller
is topmost, player movement has settled on a tile, and no transition, foreground
or parallel ScriptRunner, queued script, scripted move, engagement, emote,
teleport, field animation, or similar partial controller mutation is active.
Refusal reasons are `not_in_playthrough`, `not_overworld`, `screen_busy`,
`transition_busy`, `script_busy`, `animation_busy`, and `movement_busy`.
Identity allocation is lazy and happens only after an active topmost overworld
has been established.
### `mod.checkpoints:capture(game)`
Returns a detached data-only format-1 checkpoint, or
`nil, code, message`:
```lua
{
format = 1,
kind = "overworld",
identity = {
engineVersion = "...", gameVersion = "red", playthroughId = "...",
},
save = { -- canonical dynamic progress, excluding global options },
runtime = { overworld = {
map = "PALLET_TOWN", x = 5, y = 6,
facing = "down", surfing = false,
} },
}
```
`engineVersion` is metadata for caller compatibility warnings; the engine does
not reject patch/minor mismatches on restore. Capture deep-copies through the restricted serializer before and after
`OverworldController:captureSave` synchronizes live map, tile, facing, and surf
state. It excludes `save.options`, functions, userdata, threads, metatables as
behavior, controller instances, and static content registries. Failure code
`capture_failed` covers non-data progress and synchronization errors.
### `mod.checkpoints:restore(game, checkpoint)`
Returns `true`, or `false, code, message`. Before mutation it requires the current
runtime to be capturable and validates a detached copy of the complete record:
format, kind, internal identity consistency, current game/playthrough identity,
map availability, integral in-bounds tile, facing, surfing, and synchronized save
position.
Validation codes are `invalid_checkpoint`, `invalid_content`, `unsupported_format`,
`unsupported_runtime_kind`, `wrong_game`, `wrong_playthrough`, `invalid_map`, and
`invalid_position`, in addition to the capability refusal reasons.
The canonical save validator runs against the detached record. Unlike ordinary
CONTINUE, a checkpoint never accepts a quarantine, remap, reclaim, clamp, or
repair: any such content change returns `invalid_content` before live mutation.
The engine captures an in-memory rollback checkpoint, preserves current global
options, then reconstructs semantic overworld state through
`Game:restoreCheckpointSave`. Checkpoint entry suppresses normal map exit/entry
events, `onEnter` scripts, forced-movement/current checks, and last-map rewrites;
it does not emit normal `save.loading`/`save.loaded` lifecycle events. After
reconstruction, the engine recaptures and byte-compares normalized data. A failed
apply rolls back and returns `restore_failed`; failure of that rollback returns
`rollback_failed`. Only after a successful comparison does the engine emit
`checkpoint.restored` with `{ game = game, kind = "overworld" }`. Validation
failure, failed apply, and successful rollback emit nothing.
Durable recovery remains a caller responsibility: in-memory rollback handles a
runtime exception, not process termination.
## Runtime boundary and future kinds
This RFC's original Level A contract intentionally rejects battles, menus,
transitions, animations, and suspended/queued scripts. RFC 0005 subsequently
adds a separately inventoried `battle` kind with deterministic RNG and
differential reconstruction tests; it does not broaden script or arbitrary-frame
support implied here.
## Migration note for existing mods
**Nothing required.** No existing hook, save, controller, or world action changes
when `mod.checkpoints` is unused. The reconstruction path is called only by a
successful public restore after validation. Mods whose runtime caches derive from
rewound `game.save` or `mod.save` state may optionally subscribe to
`checkpoint.restored` and rebuild from their own public state.
## Parity tests
- **No-mod:** the complete ROM-free engine suite and existing world behavior stay
green; ordinary New Game/save/load allocates no checkpoint identity.
- **Public Mod API:** a real API-2 entry chunk proves stable inspection and every
unsafe refusal, detached data-only capture, exact map/tile/facing/surf sync,
`A -> mutate B -> restore A -> recapture A2` equality across representative
progress, settings preservation, compatibility rejection without mutation,
map-side-effect suppression, injected reconstruction rollback, mod-owned
metadata and `mod.save` rewind, independent `mod.storage`/options preservation,
and success-only runtime-cache reconciliation through `checkpoint.restored`.
## Deprecation etiquette
Nothing deprecated. This adds one public facade and a checkpoint-only semantic
reconstruction route.
@@ -0,0 +1,140 @@
# RFC 0005 — Persistent battle safe-point checkpoints
## Status
Proposed. Extends RFC 0004. Engine: `BattleCheckpoint.lua`, `Checkpoint.lua`,
`Game.lua`, `BattleState.lua`, and `OverworldController.lua`. Tests:
`battle_checkpoint_*.lua`, `checkpoints.lua`, and the existing no-mod suites.
## Motivation
RFC 0004 lets a tool capture and reconstruct settled overworld progress without
private engine access. A battle is a different runtime: its queue can hold Lua
functions and UI factories, its controller contains renderer objects and live
references, completion is currently an `onFinish` closure, and scripted battles
resume a suspended `ScriptRunner` coroutine. Copying the controller would create
a record that is neither data-only nor process-independent.
The engine can instead expose a narrow semantic safe point. This gives all mods
the strongest persistent battle checkpoint the current architecture can prove,
without claiming mid-animation or suspended-script support.
## API delta
No new facade is added. The existing additive `mod.checkpoints` API gains a
second format-1 runtime kind.
### Capability
`mod.checkpoints:inspect(game)` returns this only when an ordinary single-player
wild or trainer battle is settled at the player command menu:
```lua
{ canCapture = true, canRestore = true, kind = "battle" }
```
The action/message queue, waits, UI, animations, HP/status presentation, and
faint processing must be settled. The player must actually control the menu.
The underlying overworld must have no running/queued script or scripted move,
and the battle must carry an engine-owned semantic continuation descriptor.
Additional refusal codes are `battle_phase_busy`, `battle_origin_unsupported`,
`battle_variant_unsupported`, and `link_battle_unsupported`. Link, Safari,
ghost, old-man/demo, fishing, static-object, script-suspended, and mod-created
closure continuations remain rejected.
### Capture
A battle checkpoint remains detached and data-only:
```lua
{
format = 1,
kind = "battle",
identity = { engineVersion = "...", gameVersion = "red",
playthroughId = "..." },
save = { -- canonical dynamic progress, excluding global options },
runtime = {
overworld = { map = "ROUTE_1", x = 7, y = 8,
facing = "left", surfing = false },
battle = { -- normalized semantic model and continuation },
},
rng = { love = "..." },
}
```
The model carries player/enemy roster indices, dynamic enemy Pokémon, turn and
escape state, HP/PP/status/stages/volatiles, participants, level-up tracking,
trainer AI state, battle ruleset identity, side/field extension data, and
normalized pointer relationships such as multi-turn move slots and Mimic
restoration entries. Definitions, sprites, canvases, queues, callbacks, and
controller objects are reconstructed or excluded.
Callback-bearing battle extension tokens fail with `battle_extension_unsafe`;
invalid live reference relationships fail with `battle_state_invalid`. Nothing
is silently stripped.
New overworld checkpoints also carry the LÖVE gameplay RNG state. Legacy
format-1 overworld checkpoints without `rng` remain loadable and leave the
current stream untouched.
### Restore
Battle restore validates the detached save, map, content references, ruleset,
roster indices, move references, continuation identity, and RNG before live
mutation. The engine then:
1. reconstructs the saved overworld return point without entry side effects;
2. creates a fresh `BattleState` from current content registries;
3. applies the normalized battle model and rebuilds object-reference relations;
4. binds an engine-owned wild/trainer completion continuation;
5. installs the battle directly at the settled menu without replaying its intro;
6. restores the RNG after reconstruction has finished; and
7. recaptures and compares the complete checkpoint; and
8. emits `checkpoint.restored` with `{ game = game, kind = "battle" }` after the
comparison succeeds.
The pre-operation checkpoint is the transaction rollback. A failed post-install
RNG restore is covered: both battle runtime and RNG are reconstructed back to
their original values. Validation failure, failed reconstruction, and successful
rollback emit no checkpoint lifecycle event.
## Continuation decision
Ordinary random wild battles resume through `OverworldState:afterBattle`.
Ordinary trainer battles use a descriptor containing map id, stable NPC id,
trainer class/party, and optional header event; a win reapplies the same defeated
flag, event, reward, and `afterBattle` path. Reconstructed overworld input and
NPC freeze state are normalized instead of reviving the old closure.
`Commands.start_battle` is deliberately unsupported: its completion closure
mutates script context and resumes a coroutine whose program counter and Lua
stack cannot be serialized. Existing script rejection remains the correct safe
contract until a separate semantic ScriptRunner checkpoint RFC exists.
## Migration note
**Existing mods require no changes.** The facade and format number are unchanged;
the new kind, RNG field, and success-only lifecycle event are additive.
Overworld-only callers may continue to filter `capability.kind`. Mods with derived
runtime caches may rebuild them from restored public state when the event fires.
No-mod behavior is unchanged when checkpoints are unused.
## Verification
- settled/unsafe boundary and every variant refusal;
- data-only wild and trainer capture, including callback-bearing extension
rejection;
- process-independent controller and continuation reconstruction;
- exact differential recapture for wild and trainer states;
- HP, PP, status/stages/volatiles, AI layer, participants, enemy roster,
multi-turn move references, and Mimic restore pointers;
- exact damage, critical, accuracy, random AI, escape, next encounter, and next
raw RNG result after reload;
- corrupt content/continuation rejection before mutation;
- injected post-install failure with full runtime and RNG rollback;
- mod-added Pokémon metadata and `mod.save` rewind while independent
`mod.storage` and options remain current;
- exactly one post-verification `checkpoint.restored` event and none on failure;
- legacy overworld checkpoint compatibility;
- complete ROM-free engine and public mod-API suites.
+107
View File
@@ -0,0 +1,107 @@
# RFC 0006 — Generic process-lifecycle hooks for platform launcher integrations
## Status
Proposed. Engine: `PlatformHooks.lua` (new), `main.lua`, `Manifest.lua`,
`Loader.lua`. Tests: `tests/modkit/cases/platform_lifecycle_hooks.lua`,
`tests/mod_loader_tests.lua`, `tests/mod_manifest_tests.lua`.
## Motivation
A platform-specific launcher wrapper -- a native shell that embeds this
engine and owns its own UI around the game window (a mobile app shell,
say, presenting its own settings/import/save screens and only handing
control to the LÖVE window once play starts) needs three things no
current hook covers:
1. Pause the simulation while its own UI is on top of the game window.
2. Live-reload options it wrote from outside any Lua UI.
3. Veto `main.lua`'s "closing the window returns to the Lua launcher"
behavior when the platform shell owns that job itself -- without this,
a shell that re-fronts its own launcher UI on quit gets looped straight
back into `HostShell.restart()`'s in-process reboot instead.
Implementing this by hand-patching `main.lua`'s `love.update`/`love.quit`
directly ties every such integration to editing the one file every other
engine change also touches, guaranteeing merge conflicts for any second
platform integration (or any unrelated engine PR landing around the same
time). No existing hook covers "should the per-frame simulation step run"
or "should closing the window return to the Lua launcher."
## The decision it extends
No prior D-number. Extends the hook-contract section of `docs/modding.md`
alongside `input.step`, `render.hud`, `screen.render_visible`, etc.
## The exact API delta
Backward-compatible, additive-only.
### `core.update`
New hook, `(game, dt) -> nil` through the public wrapper signature
`(next, game, dt)`, called once per frame from `love.update` via
`src/core/PlatformHooks.lua`'s `PlatformHooks.update(game, dt)`. Vanilla
behavior (used when no mod claims the hook) is `game:update(dt)`,
unconditionally -- identical to `love.update`'s behavior before this hook
existed. A subscriber may skip calling `next(game, dt)` to pause the
simulation for that frame, or do additional per-frame work before/after
calling it regardless of whether it calls `next`.
### `core.quit_to_launcher`
New hook, `() -> boolean` through the public wrapper signature `(next)`,
called once from `love.quit()` via
`PlatformHooks.quitToLauncher(vanilla)`. `vanilla` is the pre-existing
non-platform-specific decision (`Game and not Importer and not
quitToLauncher and not scripted and not launchedIntoGame`). A subscriber
may return `false` outright to veto returning to the Lua launcher (without
ever calling `next`, so the vanilla condition is never evaluated), or call
`next()` and return its result to pass the vanilla decision through
unchanged.
Neither hook is guarded by `Runtime.wantsHook` -- both fire unconditionally
every call, matching the existing `input.step` precedent
(`src/core/Game.lua`), since `Hooks:call` already fast-paths to a bare
`vanilla(...)` call when no mod has wrapped the name.
### `Manifest.force_enable_env`
New optional manifest field, a bare env-var name. `Loader:load` re-enables
a mod carrying this field whenever that variable is set to `"1"`,
regardless of a saved disable in `options.mods`. This exists for exactly
the mod class this RFC is for: a platform-bridge mod that ships only with
one build and cannot function disabled there, but must still behave like
every other mod (a manifest opt-in, not an engine special case) on every
build that doesn't set its variable.
## Migration note for existing mods
**Nothing.** With no subscriber, `love.update` still calls `Game:update(dt)`
unconditionally every frame and `love.quit()`'s restart-to-launcher
decision is exactly the pre-existing condition -- bit-identical to today's
behavior on every platform where no mod wraps either hook. A manifest with
no `force_enable_env` field behaves exactly as before.
## Parity tests
- **No-mod:** `core.update`'s vanilla runs exactly once per call with the
hook chain empty; `core.quit_to_launcher`'s vanilla return value passes
through unchanged. Both hooks are picked up automatically by the
catalog-driven no-mod gate (`tests/engine/gate_hooks.lua`, which scans
for `Runtime.call("...")` call sites), so neither needs a dedicated
no-mod test file.
- **Mod-API:** `tests/modkit/cases/platform_lifecycle_hooks.lua` proves,
through a fixture mod loaded via the public loader (not the engine's
internals), that a subscriber can skip the vanilla update call (pause),
run extra per-frame polling regardless of pause state, and veto the
quit-to-launcher decision without the vanilla condition ever running.
- `tests/mod_loader_tests.lua` and `tests/mod_manifest_tests.lua` cover
`force_enable_env`: a matching env var re-enables a mod saved as
disabled, and an unset one leaves the saved disable alone.
## Deprecation etiquette
Nothing deprecated. These are two additive hooks and one additive manifest
field; `main.lua`'s only footprint is one `require` and two call sites
into `src/core/PlatformHooks.lua`.

Some files were not shown because too many files have changed in this diff Show More