mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Merge pull request #586 from andimiller/surfing-pikachu-fix
feat(yellow): port SurfingPikachu behaviour
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
@@ -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)")
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user