CLOSES #415, CLOSES #484, CLOSES #488, CLOSES #492, CLOSES #497, CLOSES #541, CLOSES #559, CLOSES #562, CLOSES #563, CLOSES #564, CLOSES #565, CLOSES #566, CLOSES #567, CLOSES #568, CLOSES #569, CLOSES #570, CLOSES #571, CLOSES #572

This commit is contained in:
bryanthaboi
2026-08-01 08:10:20 -04:00
parent a33f3b1ceb
commit 9326b07583
70 changed files with 6791 additions and 208 deletions
+6 -1
View File
@@ -89,6 +89,11 @@ local PLAYER_SPRITES = {
local PLAYER_PICS = {
back = "assets/generated/battle/redb.png",
demoBack = "assets/generated/battle/oldmanb.png",
-- LoadPlayerBackPic keys the demo pic off wBattleType, not off "is a
-- demo": BATTLE_TYPE_PIKACHU (Yellow's Pallet catch scene) puts PROF.OAK
-- in the player's place with his own back pic, not the old man's (#557).
-- Red/Blue never reach it -- only the Yellow script names that thrower.
oakBack = "assets/generated/battle/profoakb.png",
front = "assets/generated/trainer_card/red.png",
}
@@ -185,7 +190,7 @@ FieldDefaults.CONSTANTS = {
neighborHops = 2, -- connection hops drawn around the current map
stepFrames = 16, -- 1px per frame, 16 frames per tile
bikeStepFrames = 8, -- the bicycle doubles walking speed
turnFrames = 2, -- the extra OverworldLoop pass after a turn
turnFrames = 4, -- tap window before a turn commits to a step
},
-- cumulative slot thresholds out of 256 (engine/battle/wild_encounters.asm)
encounterBuckets = { 51, 102, 141, 166, 191, 216, 229, 242, 253, 256 },
+34 -7
View File
@@ -1155,6 +1155,13 @@ function OverworldState:handleInput()
end
end
-- .noDirectionButtonsPressed (home/overworld.asm) is the only place that
-- sets wCheckFor180DegreeTurn, so reaching this line -- a poll that found
-- no direction held -- is what re-arms the next turn in place. The early
-- returns above (mid-step, A, START) skip it exactly as the original's
-- jumps to .moveAhead and .displayDialogue do (#415).
self.player.turnArmed = true
-- Cycling Road's downhill pull: with no d-pad held the bike rolls
-- south (home/overworld.asm JoypadOverworld's simulated PAD_DOWN).
-- The mask there is PAD_CTRL_PAD | PAD_B | PAD_A, so HOLDING A or B
@@ -1891,9 +1898,20 @@ function OverworldState:tryHiddenObject(fx, fy)
end
end
-- bench guys (data/events/bench_guys.asm)
-- Bench guys (data/events/bench_guys.asm). A hidden_event's fourth byte
-- is wHiddenEventFunctionArgument, not a facing gate -- pokered's own
-- macro comment says the SPRITE_FACING_* values parked there "do not
-- actually prevent the player from interacting with them in any
-- direction" (data/events/hidden_events.asm). The facing that does decide
-- a bench guy is PrintBenchGuyText's own test against BenchGuyTextPointers,
-- SPRITE_FACING_LEFT for all twelve seats, which the manifest carries as
-- `textFacing`. Gating on the hidden_event byte instead silenced the four
-- seats that store SPRITE_FACING_UP (Vermilion, Saffron, Fuchsia,
-- Cinnabar): (0,4) is the bench wall cell and can only ever be faced from
-- the right (#488).
for _, h in ipairs(extras.benchGuys[self.map.id] or {}) do
if h.x == fx and h.y == fy and (not h.facing or h.facing == facing) then
local want = h.textFacing or h.facing
if h.x == fx and h.y == fy and (not want or want == facing) then
local text = OverworldState.benchGuyText(Game.data, save, h.text)
if text then
Game.stack:push(TextBox.new(Game, text))
@@ -3829,7 +3847,15 @@ end
-- dev-mode hot reload). The neighbors go too: their strips render the
-- same tileset. When the active map is the one that changed, the player
-- is clamped back in bounds, the NPC pool is reused so runtime handles
-- survive, and the tile-pair table is re-read.
-- survive, and the tile-pair table is re-read. keepMusic: a reload is not a
-- map entry. Its counterpart ReloadMapData (home/reload_tiles.asm) only
-- re-reads the map view and the tileset tile patterns after the Pokedex /
-- start menu / PC clobbered VRAM; map music starts from LoadMapData alone
-- (home/overworld.asm, gated on BIT_NO_MAP_MUSIC). Whatever is playing
-- belongs to the state on top, so a COLORS cycle during a battle
-- (PaletteFX.setMode reloads the live map to rebuild its baked atlas) must
-- not drop the route theme over the battle song (#484). The out-of-bounds
-- fallback below is a real map change and keeps its map music.
function OverworldState:reloadMap(mapId, reason)
MapLoader.invalidate(mapId)
for _, nb in ipairs(self.neighbors or {}) do MapLoader.invalidate(nb.map.id) end
@@ -3837,7 +3863,8 @@ function OverworldState:reloadMap(mapId, reason)
local p = self.player
local x, y, facing = p.cellX, p.cellY, p.facing
Collision.load(Game.data)
self:setMap(mapId, x, y, facing, { seamless = true, via = "reload" })
self:setMap(mapId, x, y, facing,
{ seamless = true, via = "reload", keepMusic = true })
if not self.map:inBounds(x, y) then
local heal = self:healPoint()
Logger.warn("map %s reloaded out from under the player; sending to %s",
@@ -4568,9 +4595,9 @@ function OverworldState:drawUI()
-- TalkToPikachu's picture box (engine/pikachu/pikachu_pic_animation.asm
-- PlacePikapicTextBoxBorder: TextBoxBorder at (6,5) with b,c = 5,5, so a
-- 7x7 box holding the 5x5 pic at (7,6) -- PikaAnimTilemap_1). The
-- per-emotion frame gfx (gfx/pikachu/unknown_*) are not extracted, so the
-- front pic stands in for every frame of the script; PikachuFollower
-- .picLift lifts it on the runs that draw the alternate pose, and the
-- script's base frame is ripped as pikachu/pikapic_N.png (#561) but the
-- pikaframe overlays on top of it are not, so PikachuFollower
-- .picLift lifts the base on the runs that draw the alternate pose, and the
-- script's own duration times the beat (#407, #424). Palette zone
-- PAL_PIKACHU_PORTRAIT covers (7,6)-(11,10) via sgbPalettes above.
if self.emote and self.emote.pikaPic then
+31 -8
View File
@@ -518,9 +518,12 @@ end
-- pikachu_emotions.asm): pick a scripted emotion, then play its bubble
-- and voiced PCM clip, and raise the framed Pikachu picture the original
-- puts over the map (pikaemotion_pikapic -> pikachu_pic_animation.asm
-- PlacePikapicTextBoxBorder), drawn by OverworldController:drawUI. The
-- per-emotion animation frames (gfx/pikachu/unknown_*) are not extracted,
-- so the front pic stands in for all twenty of them (#407).
-- PlacePikapicTextBoxBorder), drawn by OverworldController:drawUI. Each
-- script's BASE 5x5 frame is ripped as pikachu/pikapic_N.png (#561); the
-- pikaframe overlays it alternates with are a second full-body pose out of
-- the same blob and are still unported, so picLift below stands in for
-- their motion and the battle front pic covers caches built before the
-- rip (#407).
-- ---------------------------------------------------------------------
-- PikachuEmotionTable, reduced to each entry's bubble + pikaemotion_pcm
@@ -715,9 +718,17 @@ function PikachuFollower.talk(game, ow, npc, done)
-- 40x40 front pic is the size of PikaAnimTilemap_1's 5x5 base frame;
-- Sprites.path keeps a mod's replacement skin in play.
local Sprites = require("src.pokemon.Sprites")
local pic = Sprites.path(game.data, "PIKACHU", "front",
{ kind = "overworld" })
local anim = PIKAPIC[PIKAPIC_SCRIPT[emotion] or emotion] or PIKAPIC[1]
-- The chosen script's own base frame (its first pikapic_loadgfx, ripped as
-- pikachu/pikapic_N.png). Red/Blue have no such art and Yellow caches
-- built before the rip do not carry it, so both fall back to the battle
-- front pic that stood in for every script before (#561).
local script = PIKAPIC_SCRIPT[emotion] or emotion
local pic = "assets/generated/pikachu/pikapic_" .. script .. ".png"
if not require("src.render.Assets").exists(pic) then
pic = Sprites.path(game.data, "PIKACHU", "front",
{ kind = "overworld" })
end
local anim = PIKAPIC[script] or PIKAPIC[1]
local hold = anim.dur * PIKAPIC_TICK
ow.emote = {
npc = npc, frames = hold, bubble = bi or false, pikaPic = pic,
@@ -790,10 +801,22 @@ function PikachuFollower.onBillEnteredMachine(game, ow)
if not (GameVersion.isYellow() and ow.pikachuBillsScene) then return end
local npc = findFollower(ow)
if not npc then return end
-- BillsHouseScript3 (pokeyellow scripts/BillsHouse.asm:100-115). The two
-- movement tables are named the wrong way round in the cartridge source:
-- hl is seeded with ..._EnterCellSeparatorDown, then
-- and a ; cp SPRITE_FACING_DOWN (SPRITE_FACING_DOWN is 0)
-- jr nz, .applyPikachuMovement
-- keeps that seeded table when the player is NOT facing down, and only the
-- fallthrough -- facing down -- swaps in ..._EnterCellSeparatorNotDown.
-- So facing down takes the long way round the cell separator and every
-- other facing walks straight up. Following the label names instead of the
-- branch inverts the scene (#455).
local steps = ow.player.facing == "down"
and { { "up", 3 } }
or { { "up", 1 }, { "left", 1 }, { "up", 2 }, { "right", 1 } }
and { { "up", 1 }, { "left", 1 }, { "up", 2 }, { "right", 1 } }
or { { "up", 3 } }
movePikachu(ow, npc, steps, function()
-- PIKAMOVEMENT_LOOK_UP closes the detour table before the bubble
npc.facing = "up"
billsHouseEmotion(game, ow, npc, "QUESTION_BUBBLE")
end)
end
+43 -7
View File
@@ -15,11 +15,21 @@ local STEP_FRAMES = 16
-- a turn in place blocks movement for the one extra OverworldLoop pass the
-- original spends after a direction change: .handleDirectionButtonPress ends
-- `jp OverworldLoop` (home/overworld.asm), and OverworldLoop burns two
-- DelayFrame calls before the next JoypadOverworld, so the sample that can
-- commit to a step lands exactly 2 fixed steps after the turn -- the same
-- 2-frames-per-iteration cadence that makes STEP_FRAMES 16 above
-- (wWalkCounter = 8, 2px per AdvancePlayerSprite) (#415)
local TURN_FRAMES = 2
-- DelayFrame calls before the next JoypadOverworld, so the step can only
-- commit at the following poll -- the same 2-frames-per-iteration cadence
-- that makes STEP_FRAMES 16 above (wWalkCounter = 8, 2px per
-- AdvancePlayerSprite).
-- Those polls sit on a 2-frame grid, so the hardware samples a press 0 or 1
-- frames after the d-pad physically goes down and the release deadline lands
-- 2 or 3 frames after that. We sample on the frame the button goes down
-- with none of that poll latency, so a flat 2 handed every tap the tightest
-- case the original could produce; 4 covers the grid instead of
-- undercutting it (#415).
local TURN_FRAMES = 4
-- The on-screen d-pad cannot produce a 60ms tap: a finger press and release
-- run well past it even before the OS batches the touch events, so the
-- overlay gets a longer window than a physical pad (#415).
local TOUCH_TURN_FRAMES = 8
function Player.new(data, cx, cy, facing)
local self = setmetatable({}, Player)
@@ -67,6 +77,11 @@ function Player.new(data, cx, cy, facing)
self.progress = 0
self.stepFlip = false
self.turnTimer = 0
-- wCheckFor180DegreeTurn (home/overworld.asm): the original only lets a
-- turn in place happen on a poll whose previous pass found no direction
-- held. It starts armed, tryMove spends it, and OverworldState:handleInput
-- re-arms it from a standstill.
self.turnArmed = true
self.inputLocked = false
return self
end
@@ -75,14 +90,35 @@ function Player:position()
return self.cellX, self.cellY
end
-- How long a fresh turn holds the step off for; see TURN_FRAMES. The
-- overlay is detected per source rather than by whether the touch controls
-- are on screen, so a phone with a controller attached still gets the
-- physical pad's window (Input:isTouchDown, src/core/Input.lua).
function Player:turnWindow()
local frames = self.turnFrames or TURN_FRAMES
local input = require("src.core.Game").input
if input and input.isTouchDown and input:isTouchDown(self.facing) then
return math.max(frames, TOUCH_TURN_FRAMES)
end
return frames
end
-- Attempt to start a step; returns "moved"|"turned"|"blocked"|nil.
function Player:tryMove(dir, map, entities)
if self.moving or self.inputLocked then return nil end
if self.facing ~= dir then
self.facing = dir
self.turnTimer = self.turnFrames or TURN_FRAMES
self.bumpFrames = nil -- turning to a new facing ends any wall-bonk cycle
return "turned"
-- .handleDirectionButtonPress only reaches the turn while
-- wCheckFor180DegreeTurn is still set, and .noDirectionButtonsPressed is
-- the one place that sets it (home/overworld.asm), so a facing change
-- made without the d-pad ever coming up steps straight away rather than
-- paying the turn delay at every corner (#415)
if self.turnArmed then
self.turnArmed = false
self.turnTimer = self:turnWindow()
return "turned"
end
end
if self.turnTimer > 0 then return nil end
local ok, why = Collision.canMove(map, entities, self, dir)