Merge pull request #586 from andimiller/surfing-pikachu-fix

feat(yellow): port SurfingPikachu behaviour
This commit is contained in:
bryanthaboi
2026-08-02 00:58:32 -04:00
committed by GitHub
13 changed files with 437 additions and 15 deletions
+96
View File
@@ -0,0 +1,96 @@
# 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.
+19
View File
@@ -495,6 +495,25 @@ function RomExtractor:extractSprites()
image = "assets/generated/sprites/red_bike.png",
frames = bikeFrames, walker = bikeFrames >= 6,
}
-- Yellow-only: the surfing-Pikachu overworld sheet, loaded outside
-- SpriteSheetPointerTable (LoadSurfingPlayerSpriteGraphics2) -- same
-- extra-extract shape as RedBikeSprite. (RFC 0001)
local surfPika = self.manifest.sprites.surfPikachu
if surfPika and self.symbols[surfPika.label] then
local spSymbol = self:symbol(surfPika.label)
self:write2bpp(
self.rom:bytes(spSymbol.bank, spSymbol.address,
surfPika.imageWidth * surfPika.imageHeight / 4),
surfPika.imageWidth, surfPika.imageHeight,
"sprites/" .. surfPika.imageBase .. ".png", true)
local spFrames = surfPika.imageHeight / 16
out.SPRITE_SURFING_PIKACHU = {
id = "SPRITE_SURFING_PIKACHU",
source = ("ROM:%s"):format(surfPika.label),
image = "assets/generated/sprites/" .. surfPika.imageBase .. ".png",
frames = spFrames, walker = spFrames >= 6,
}
end
self:write("sprites", out)
self:tick("Overworld sprites", #order + 1, #order + 1)
return out
+7 -4
View File
@@ -646,10 +646,13 @@ function PaletteFX.spriteObp(spriteDef, seed)
local src = spriteDef and (spriteDef.paletteSource or spriteDef.source)
if not (w and src) then return nil end
local idx = tonumber(src:match("%[(%d+)%]"))
-- RedBikeSprite loads outside SpriteSheetPointerTable
-- (LoadBikePlayerSpriteGraphics), so its source carries no bracketed
-- index; it wears the player's own palette, same as SPRITE_RED
if not idx and src:find("RedBikeSprite", 1, true) then idx = 0 end
-- RedBikeSprite and SurfingPikachuSprite load outside
-- SpriteSheetPointerTable, so their source has no bracketed index;
-- they wear the player's OBP palette (spriteAssignment[0]).
if not idx and (src:find("RedBikeSprite", 1, true)
or src:find("SurfingPikachuSprite", 1, true)) then
idx = 0
end
local group = idx and w.spriteAssignment[idx]
if group == nil then return nil end
if group == "random" then
+6
View File
@@ -77,6 +77,12 @@ local SAFARI = {
local PLAYER_SPRITES = {
walk = "SPRITE_RED", surf = "SPRITE_SEEL",
bike = "SPRITE_RED_BIKE", fly = "SPRITE_BIRD",
-- Yellow's IsSurfingPikachuInParty: when the SURF-mon is a Pikachu,
-- the player rides this sheet. GFX loads via
-- LoadSurfingPlayerSpriteGraphics2, not SpriteSheetPointerTable, so
-- it needs a manifest extract (RFC 0001). Guarded in Player.new so
-- before extraction lands the ride keeps the Seel.
surfPikachu = "SPRITE_SURFING_PIKACHU",
}
-- The player's own trainer art: RedPicBack (the battle back pic, up until
+25
View File
@@ -389,6 +389,10 @@ function OverworldState:setMap(mapId, x, y, facing, opts)
and not self.map:isWalkableCell(x, y)
and self.map:isWaterCell(x, y)
end
-- re-derive from the live party: a reloaded save with the SURF-Pikachu
-- since deposited should not render the Pikachu sheet.
-- ponytail: re-derived rather than persisted.
self:syncSurfingPikachu()
end
-- crossConnection re-arms this after setMap; clear so a warp/reload
-- cannot leave a stale deferred PlayMapMusic pending
@@ -1489,6 +1493,21 @@ function OverworldState:partyKnows(moveId)
return partyKnowsVanilla(moveId)
end
-- IsSurfingPikachuInParty (home/map_objects.asm): when the SURF-mon
-- is a Pikachu, pose() renders the Pikachu surf sprite. Called at
-- every surf-state change so a reloaded save picks the right sheet
-- after a party change. No-op when not surfing.
function OverworldState:syncSurfingPikachu()
local p = self.player
if not p then return end
if not p.surfing then
p.surfingPikachu = false
return
end
local mon = self:partyKnows("SURF")
p.surfingPikachu = mon ~= nil and mon.species == "PIKACHU" or false
end
-- The rejection loop shared by the Good and Super Rods
-- (item_effects.asm ItemUseGoodRod .RandomLoop / ReadSuperRodData): an
-- odd random byte is no bite; otherwise a 2-bit pick rerolls until it
@@ -1585,6 +1604,7 @@ function OverworldState:flyTo(mapId)
Game.save.onBike = false
Game.save.forcedBike = nil -- HandleFlyWarpOrDungeonWarp res BIT_ALWAYS_ON_BIKE
self.player.surfing = false
self:syncSurfingPikachu()
-- the bird carries the player off westward before the warp
-- (engine/overworld/player_animations.asm LoadBirdSpriteGraphics)
self.flyAnim = { frames = 48 }
@@ -1612,6 +1632,7 @@ function OverworldState:beginTeleportOut(onDone)
end
require("src.core.Sound").play(Game.data, "Teleport_Exit1")
self.player.surfing = false
self:syncSurfingPikachu()
self.player.inputLocked = true
-- rising spin: the mirror of the arrival spin-drop set in startWarpTo, so
-- spinRise lifts the sprite (Player:pose) while spinFrames counts down
@@ -2275,6 +2296,7 @@ function OverworldState:trySurf(fx, fy, onClose)
Game.stack:push(TextBox.new(Game, text, function()
if onClose then onClose() end
p.surfing = true
self:syncSurfingPikachu()
require("src.core.Music").setSurfing(Game.data, true)
Game.stack:push(require("src.render.Transition").whiteFlash(Game, nil,
function() self:stepForwardOrCrossEdge(p.facing) end))
@@ -3196,6 +3218,7 @@ function OverworldState:onStepComplete()
-- dismounting a surf: landing on a walkable cell ends it
if p.surfing and self.map:isWalkableCell(p.cellX, p.cellY) then
p.surfing = false
self:syncSurfingPikachu()
require("src.core.Music").setSurfing(Game.data, false)
end
@@ -3497,6 +3520,7 @@ function OverworldState:checkForcedMovement()
end
elseif tile.mode == "surf" then
p.surfing = true
self:syncSurfingPikachu()
require("src.core.Music").setSurfing(Game.data, true)
end
return false
@@ -3775,6 +3799,7 @@ end
function OverworldState:warpToHealPoint(onDone, opts)
local heal = self:healPoint()
self.player.surfing = false
self:syncSurfingPikachu()
-- HandleFlyWarpOrDungeonWarp + DisplayPlayerBlackedOutText both clear
-- BIT_ALWAYS_ON_BIKE (home/overworld.asm / home/text_script.asm)
Game.save.forcedBike = nil
+8
View File
@@ -41,11 +41,18 @@ function Player.new(data, cx, cy, facing)
-- LoadSurfingPlayerSpriteGraphics, home/overworld.asm)
local walkId = FieldDefaults.fieldValue(data, "playerSprites", "walk")
local surfId = FieldDefaults.fieldValue(data, "playerSprites", "surf")
local surfPikaId = FieldDefaults.fieldValue(data, "playerSprites", "surfPikachu")
local bikeId = FieldDefaults.fieldValue(data, "playerSprites", "bike")
self.sprite = SpriteRenderer.new(data.sprites[walkId], "player")
if surfId and data.sprites[surfId] then
self.surfSprite = SpriteRenderer.new(data.sprites[surfId], "player")
end
-- Yellow's surfing-Pikachu ride (Yellow LoadSurfingPlayerSpriteGraphics2,
-- paired with field.playerSprites.surfPikachu). rotated in at pose()
-- when the SURF-mon is a Pikachu.
if surfPikaId and data.sprites[surfPikaId] then
self.surfPikachuSprite = SpriteRenderer.new(data.sprites[surfPikaId], "player")
end
if bikeId and data.sprites[bikeId] then
self.bikeSprite = SpriteRenderer.new(data.sprites[bikeId], "player")
end
@@ -288,6 +295,7 @@ function Player:pose()
-- RodResponse (engine/items/item_effects.asm) zeroes wWalkBikeSurfState
-- across FishingAnim, so casting from the water shows the on-foot sheet
local sprite = (self.fishing and self.sprite)
or (self.surfing and self.surfingPikachu and self.surfPikachuSprite)
or (self.surfing and self.surfSprite)
or (self.onBike and self.bikeSprite) or self.sprite
return sprite, self.px, py, facing, phase, flip, hopping
+4
View File
@@ -1011,6 +1011,10 @@ do
"the boot path seeded field.palettes")
check(Data.field.playerSprites.walk == "SPRITE_RED",
"the boot path seeded field.playerSprites")
check(Data.field.playerSprites.surf == "SPRITE_SEEL",
"the surf sprite defaults to the Seel")
check(Data.field.playerSprites.surfPikachu == "SPRITE_SURFING_PIKACHU",
"the surfing-Pikachu sprite defaults to SPRITE_SURFING_PIKACHU (RFC 0001; Yellow ride)")
check(Data.field.badgeGates.ROUTE_22_GATE.passedFlag == "PASSED_ROUTE22_GATE",
"the boot path filled the gaps in a stamped key")
check(Data.constants.world.stepFrames == 16,
+128
View File
@@ -0,0 +1,128 @@
-- Parity port: Yellow's IsSurfingPikachuInParty (home/map_objects.asm).
-- When the party mon that knows SURF is a Pikachu, the player's surf sprite
-- swaps from the default (the Seel) to a Pikachu overworld sheet. This
-- mirrors vanilla Yellow, which repoints wSpritePlayerStatePtr at the
-- surfing-Pikachu sheet during the ride.
--
-- Covers the engine change that adds field.playerSprites.surfPikachu,
-- Player.surfPikachuSprite, OverworldState:syncSurfingPikachu, and the
-- pose() sprite-pick switch. No sprite bytes are read -- the assertions
-- compare the chosen SpriteRenderer's backing def, so the test is ROM-free
-- against the fixture dataset and runs in CI without an import.
--
-- Self-contained; run via:
-- luajit tests/parity_surfing_pikachu_sprite.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
local Data = require("src.core.Data")
if not (Data.maps and Data.maps.CINNABAR_ISLAND) then Data:load() end
local Game = require("src.core.Game")
local Input = require("src.core.Input")
local Renderer = require("src.render.Renderer")
local SaveData = require("src.core.SaveData")
local StateStack = require("src.core.StateStack")
local OW = require("src.world.OverworldController")
local S = require("tests.harness").suite("parity surfing-pikachu sprite")
local check, eq = S.check, S.eq
-- field default: the new key is seeded alongside the existing surf sprite,
-- so a mod-free boot gets the Pikachu overworld sheet by default
check(Data.field.playerSprites.surfPikachu == "SPRITE_SURFING_PIKACHU",
"field.playerSprites.surfPikachu defaults to SPRITE_SURFING_PIKACHU (RFC 0001; Yellow ride)")
check(Data.field.playerSprites.surf == "SPRITE_SEEL",
"the default surf sprite is still the Seel (no behavior change)")
Game.data = Data
Game.input = Input; Input:init()
Game.renderer = Renderer; Renderer:init()
Game.stack = StateStack
StateStack:init()
Game.overworld = OW
local function mkMon(species, ...)
local moves = {}
for _, id in ipairs({ ... }) do
table.insert(moves, { id = id, pp = 10, ppUp = 0 })
end
return {
species = species, level = 30, hp = 50, maxHp = 50,
status = 0, moves = moves,
}
end
local function freshOw(party)
Game.save = SaveData.newGame()
Game.save.party = party
Game.save.inventory = { SOULBADGE = true }
while Game.stack:top() do Game.stack:pop() end
Game.stack:push(OW, "CINNABAR_ISLAND", 19, 8, "right")
local ow = Game.stack:top()
-- the mount itself goes through trySurf (TextBox + whiteFlash + step),
-- so arm the surf state directly and let syncSurfingPikachu derive
-- the sprite pick -- the unit under test.
ow.player.surfing = true
ow:syncSurfingPikachu()
-- the real SPRITE_SURFING_PIKACHU art only exists after an Yellow
-- import + the RFC 0001 extractor path; inject a sentinel surf sprite
-- so pose()'s selection rule is testable in the fixture dataset.
ow.player.surfPikachuSprite = { def = { id = "SPRITE_TEST_SURF_PIKA" } }
return ow
end
-- the sprite def backing a Player:surfSprite / surfPikachuSprite, so the
-- assertion can compare identities without drawing
local function surfSpriteId(player)
local sprite, _px, _py = player:pose()
return sprite and sprite.def and sprite.def.id or nil
end
-- -------------------------------------------------- Pikachu knows SURF
do
local ow = freshOw({ mkMon("PIKACHU", "SURF"), mkMon("SQUIRTLE") })
check(ow.player.surfing, "player is now surfing")
check(ow.player.surfingPikachu == true,
"surfingPikachu set when a Pikachu knows SURF")
eq(surfSpriteId(ow.player), "SPRITE_TEST_SURF_PIKA",
"pose() draws the surf-pikachu sprite, not the Seel (when set)")
-- dismount flips it off again: set surfing false and re-sync, the way
-- the dismount paths in OverworldController do
ow.player.surfing = false
ow:syncSurfingPikachu()
check(ow.player.surfingPikachu == false,
"syncSurfingPikachu clears the flag when dismounted")
end
-- -------------------------------------------------- no Pikachu, no swap
do
local ow = freshOw({ mkMon("SQUIRTLE", "SURF") })
check(ow.player.surfing, "player is surfing")
check(ow.player.surfingPikachu == false,
"no Pikachu in the SURF-mon: surfingPikachu stays false")
eq(surfSpriteId(ow.player), "SPRITE_SEEL",
"pose() keeps the default Seel surf sprite")
end
-- -------------------------------------------------- SURF-knower is the
-- Pikachu's species, not a slot position: a Pikachu without SURF behind a
-- Squirtle that has SURF must NOT trigger the swap
do
local ow = freshOw({ mkMon("SQUIRTLE", "SURF"), mkMon("PIKACHU", "THUNDER_SHOCK") })
check(ow.player.surfingPikachu == false,
"Pikachu present but not the SURF-mon: no swap")
eq(surfSpriteId(ow.player), "SPRITE_SEEL",
"pose() keeps the Seel when the SURF-mon is not a Pikachu")
end
-- -------------------------------------------------- not surfing: no swap
do
local ow = freshOw({ mkMon("PIKACHU", "SURF") })
ow.player.surfing = false
ow:syncSurfingPikachu()
check(ow.player.surfingPikachu == false,
"syncSurfingPikachu is a no-op swap when not surfing")
end
S.finish()
+21
View File
@@ -461,6 +461,27 @@ def extract_sprites(rom, symbols, manifest, out_dir, assets_dir):
"walker": bike_frames >= 6,
}
# Yellow-only: the surfing-Pikachu overworld ride sheet, loaded
# outside SpriteSheetPointerTable (LoadSurfingPlayerSpriteGraphics2) --
# same extra extract as RedBikeSprite. (RFC 0001)
surf = manifest["sprites"].get("surfPikachu")
if surf and _has_symbol(symbols, surf["label"]):
surf_symbol = _symbol(symbols, surf["label"])
surf_length = surf["imageWidth"] * surf["imageHeight"] // 4
_write_2bpp_png(
rom.bytes(surf_symbol.bank, surf_symbol.address, surf_length),
surf["imageWidth"], surf["imageHeight"],
os.path.join(assets_dir, "sprites", surf["imageBase"] + ".png"),
transparent_color0=True)
surf_frames = surf["imageHeight"] // 16
out["SPRITE_SURFING_PIKACHU"] = {
"id": "SPRITE_SURFING_PIKACHU",
"source": f"ROM:{surf['label']}",
"image": f"assets/generated/sprites/{surf['imageBase']}.png",
"frames": surf_frames,
"walker": surf_frames >= 6,
}
util.write_lua(
os.path.join(out_dir, "sprites.lua"), out,
header="Source: canonical Pokemon Red ROM (overworld sprite sheets)")
+17
View File
@@ -83,6 +83,23 @@ def extract(pokered, out_dir, assets_dir, sprite_order):
"walker": frames >= 6,
}
# Yellow-only: the surfing-Pikachu overworld ride sheet, loaded
# outside SpriteSheetPointerTable (LoadSurfingPlayerSpriteGraphics2) --
# same extra extract as RedBikeSprite. (RFC 0001)
surf_src = files.get("SurfingPikachuSprite")
if surf_src:
png_src = os.path.join(pokered, re.sub(r"\.2bpp$", ".png", surf_src))
dst = os.path.join(assets_dir, "sprites", "surfing_pikachu.png")
size = gfx.convert_png(png_src, dst, transparent_color0=True)
frames = size[1] // 16
out["SPRITE_SURFING_PIKACHU"] = {
"id": "SPRITE_SURFING_PIKACHU",
"source": "gfx/sprites.asm SurfingPikachuSprite (LoadSurfingPlayerSpriteGraphics2)",
"image": "assets/generated/sprites/surfing_pikachu.png",
"frames": frames,
"walker": frames >= 6,
}
util.write_lua(os.path.join(out_dir, "sprites.lua"), out,
header="Sources: data/sprites/sprites.asm, gfx/sprites/*.png\n"
"Frame order (walker): stand D/U/L, walk D/U/L; right = flipped left.")
+15 -1
View File
@@ -280,7 +280,7 @@ def sprite_metadata(pokered, order):
bike_png = re.sub(r"\.2bpp$", ".png", bike_source)
with Image.open(os.path.join(pokered, bike_png)) as image:
bike_width, bike_height = image.size
return {
out_manifest = {
"order": out,
"bike": {
"label": "RedBikeSprite",
@@ -289,6 +289,20 @@ def sprite_metadata(pokered, order):
"imageHeight": bike_height,
},
}
# Yellow-only: the surfing-Pikachu overworld ride sheet, parallel
# to the bike entry; absent in Red/Blue. (RFC 0001)
surf_source = files.get("SurfingPikachuSprite")
if surf_source:
surf_png = re.sub(r"\.2bpp$", ".png", surf_source)
with Image.open(os.path.join(pokered, surf_png)) as image:
surf_width, surf_height = image.size
out_manifest["surfPikachu"] = {
"label": "SurfingPikachuSprite",
"imageBase": "surfing_pikachu",
"imageWidth": surf_width,
"imageHeight": surf_height,
}
return out_manifest
def text_metadata(pokered):
+4
View File
@@ -103,6 +103,10 @@ YELLOW_EXTRA_SYMBOLS = (
"Pic_e5b7d", "Pic_e5ddd", "GFX_e6020", "Pic_e6340", "Pic_e6587",
"Pic_e67d6", "GFX_e6e6f", "GFX_e718f", "GFX_e74af", "Pic_e77cf",
"Pic_f0abf", "Pic_f0cf4",
# Yellow-only overworld player surf sprite, loaded outside
# SpriteSheetPointerTable (LoadSurfingPlayerSpriteGraphics2) --
# needs its own extract like RedBikeSprite. (RFC 0001)
"SurfingPikachuSprite",
)
# Yellow-only dialogue whose bank labels carry no leading underscore, so
+87 -10
View File
@@ -19934,7 +19934,13 @@
"imageWidth": 16,
"label": "GamblerAsleepSprite"
}
]
],
"surfPikachu": {
"imageBase": "surfing_pikachu",
"imageHeight": 96,
"imageWidth": 16,
"label": "SurfingPikachuSprite"
}
},
"symbols": {
"AbraPicBack": [
@@ -21369,6 +21375,22 @@
28,
16822
],
"FanClubChairPrintText1": [
43,
22070
],
"FanClubChairPrintText2": [
43,
22210
],
"FanClubChairPrintText3": [
43,
22243
],
"FanClubChairPrintText4": [
43,
22257
],
"FarfetchdPicBack": [
10,
28976
@@ -25293,6 +25315,18 @@
24,
22540
],
"SSAnneKitchenCook7EelsAuBarbecueText": [
38,
21363
],
"SSAnneKitchenCook7PrimeBeefSteakText": [
38,
21414
],
"SSAnneKitchenCook7SalmonDuSaladText": [
38,
21298
],
"SSAnneKitchenCook7Text": [
24,
22545
@@ -26069,10 +26103,18 @@
21,
25898
],
"SilphCo9FNurseDontGiveUpText": [
38,
30668
],
"SilphCo9FNurseText": [
23,
22467
],
"SilphCo9FNurseThankYouText": [
38,
30683
],
"SilphCo9FNurseYouLookTiredText": [
38,
30622
@@ -26237,10 +26279,18 @@
32,
25380
],
"SurfingPikachuSprite": [
63,
28143
],
"SwimmerPic": [
19,
20787
],
"TMNotebookText": [
39,
27310
],
"TamerPic": [
19,
23374
@@ -26753,6 +26803,10 @@
6,
21103
],
"ViridianCityFisherYouCanHaveThisText": [
45,
18396
],
"ViridianCityGambler1Text": [
6,
21055
@@ -26801,6 +26855,14 @@
6,
21043
],
"ViridianCityYoungster2CaterpieAndWeedleDescriptionText": [
45,
18125
],
"ViridianCityYoungster2OkThenText": [
45,
18111
],
"ViridianCityYoungster2Text": [
6,
21067
@@ -39174,7 +39236,30 @@
]
},
"labels": [
"FanClubChairPrintText1",
"FanClubChairPrintText2",
"FanClubChairPrintText3",
"FanClubChairPrintText4",
"MelanieBulbasaurText",
"MelanieOddishText",
"MelanieSandshrewText",
"MelanieText1",
"MelanieText2",
"MelanieText3",
"MelanieText4",
"MelanieText5",
"SSAnneKitchenCook7EelsAuBarbecueText",
"SSAnneKitchenCook7PrimeBeefSteakText",
"SSAnneKitchenCook7SalmonDuSaladText",
"SilphCo2FSilphWorkerFPleaseTakeThisText",
"SilphCo9FNurseDontGiveUpText",
"SilphCo9FNurseThankYouText",
"SilphCo9FNurseYouLookTiredText",
"TMNotebookText",
"TeachingHMsText",
"ViridianCityFisherYouCanHaveThisText",
"ViridianCityYoungster2CaterpieAndWeedleDescriptionText",
"ViridianCityYoungster2OkThenText",
"_AIBattleUseItemText",
"_AIBattleWithdrawText",
"_AbandonLearningText",
@@ -41860,15 +41945,7 @@
"_YeahText",
"_YourNameIsText",
"_ZapdosDexEntry",
"_ZubatDexEntry",
"MelanieText1",
"MelanieText2",
"MelanieText3",
"MelanieText4",
"MelanieText5",
"MelanieBulbasaurText",
"MelanieOddishText",
"MelanieSandshrewText"
"_ZubatDexEntry"
],
"pointers": {
"AgathasRoom": {