Files
gen1recomp/tests/parity_surfing_pikachu_sprite.lua
T
Andi Miller 1fe35335a5 feat(yellow): port SurfingPikachu behaviour
Yellow's IsSurfingPikachuInParty swaps the player's overworld sheet to
a Pikachu-on-a-surfboard when the party mon that knows SURF is a
Pikachu. The recomp was missing both halves of this: the sheet was
never extracted, and the engine had no seam for the swap.

Extraction: SurfingPikachuSprite (gfx/sprites/surfing_pikachu.2bpp)
loads outside SpriteSheetPointerTable via
LoadSurfingPlayerSpriteGraphics2, the same bypass RedBikeSprite uses.
Added the symbol to the Yellow manifest and a parallel extract in
RomExtractor / build_rom_data / extract/sprites, minting
SPRITE_SURFING_PIKACHU. PaletteFX colors it (player OBP palette, same
as the bike).

Engine: new field.playerSprites.surfPikachu (default
SPRITE_SURFING_PIKACHU, guarded so before extraction the ride keeps
the Seel). Player.new caches surfPikachuSprite; pose() picks it when
surfing and the SURF-mon is a Pikachu. New
OverworldState:syncSurfingPikachu derives the flag from partyKnows at
every surf-state toggle (mount, dismount, fly, teleport, blackout,
forced-surf tile, boot-restore). Runtime-only, re-derived at load so a
party change between save and load is honored.

Lane B: RFC at docs/rfcs/0001-surfing-pikachu-sprite.md.
Backward-compatible — existing mods see no change (surf still defaults
to SPRITE_SEEL; surfPikachu only resolves on a Yellow import after
regeneration). Parity tests in tests/parity_surfing_pikachu_sprite.lua
(12/12) and tests/mod_world_tests.lua (19229/19229 with the new
boot-seed checks). tests/parity_cinnabar_east_surf.lua (24/24)
unchanged.

Regeneration required: re-run make_yellow_manifest.py against a
pret/pokeyellow checkout, then re-import the Yellow ROM.
2026-08-01 15:50:22 +01:00

128 lines
5.1 KiB
Lua

-- 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()