so many bugs i cannot even breathe

This commit is contained in:
bryanthaboi
2026-07-30 07:41:41 -04:00
parent e63fa70700
commit 0f581e2f69
78 changed files with 7729 additions and 523 deletions
+17
View File
@@ -128,6 +128,23 @@ FieldDefaults.FIELD = {
-- the one-shot flag the gate's pass text is gated on; a gate a mod adds
-- gets "PASSED_<mapId>" instead of this pre-v2 spelling
badgeGates = { ROUTE_22_GATE = { passedFlag = "PASSED_ROUTE22_GATE" } },
-- the Rocket Hideout lift gates the floor callbacks stamp shut
-- (scripts/RocketHideoutB1F.asm / RocketHideoutB4F.asm
-- ...DoorCallbackScript); seeds caches imported before the manifest
-- carried them, which left both gates standing open (#372)
cardKeyDoors = {
closedDoors = {
ROCKET_HIDEOUT_B1F = {
{ block = 0x54, bx = 12, by = 8, open = 0x0e,
event = "EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_4" },
},
ROCKET_HIDEOUT_B4F = {
{ block = 0x2d, bx = 12, by = 5, open = 0x0e,
events = { "EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_0",
"EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_1" } },
},
},
},
-- VermilionGymSetDoorTile opens the motorized door once both locks are hit
hiddenExtras = {
-- PrintTrashText bins (#188); seeds stale caches missing the key
+166 -144
View File
@@ -65,6 +65,16 @@ local ROD_OAM = {
right = { dx = 16, dy = 4, tile = 1, flip = true }, -- dbsprite 11, 10, 0, 0, $fe, XFLIP
}
-- field.darkMaps (home/overworld.asm's dark-map check): the floors that run
-- with wMapPalOffset = 6 until FLASH
local function isDarkMap(mapId)
local darkDef = Game.data.field.darkMaps
for _, m in ipairs(darkDef and darkDef.maps or {}) do
if m == mapId then return true end
end
return false
end
-- object_event spawn filter (toggleable_objects, items taken, beaten
-- static encounters), shared by the current map's real NPCs and the
-- visual-only ghosts on connected neighbor maps
@@ -188,8 +198,50 @@ function OverworldState:enter(mapId, x, y, facing)
-- survives save/load: a loaded game may start inside a building whose
-- exit mat is a LAST_MAP warp
self.lastOutdoor = Game.save.lastOutdoor
self.justWarped = false
self:setMap(mapId, x, y, facing, { via = "boot" })
-- boot/load: derive the flag from the tile the save left us standing on,
-- like MapEntryAfterBattle's IsPlayerStandingOnWarp, so a game saved on a
-- door mat can still walk straight back out (issue #378)
self:refreshStandingOnWarp()
end
-- Silph Co card key doors + Rocket Hideout elevator gates: the .blk
-- layouts ship with the doorways open; each floor's map script stamps
-- the closed door block on load until its unlock event is set
-- (scripts/SilphCo2F.asm SilphCo2FGateCallbackScript et al., closed
-- blocks $54/$5f/$20; scripts/RocketHideoutB1F.asm +
-- RocketHideoutB4F.asm ...DoorCallbackScript, closed blocks $54/$2d over
-- the lift doorway). A door opens on its single `event`, or on `events`
-- when every listed flag must be set (Rocket Hideout B4F's lift gate
-- needs both guard trainers beaten -- CheckBothEventsSet). The callbacks
-- run whenever BIT_CUR_MAP_LOADED_1 is set, which is map load AND the end
-- of a battle on that map (home/trainers.asm EndTrainerBattle), so the
-- gate opens with SFX_GO_INSIDE the moment the last guard falls (#372).
function OverworldState:stampClosedDoors()
local closedDoors = FieldDefaults.fieldValue(Game.data, "cardKeyDoors",
"closedDoors")
local floorDoors = self.map and closedDoors and closedDoors[self.map.id]
if not floorDoors then return end
local stamped, unlocked = false, false
for _, door in ipairs(floorDoors) do
local open
if door.events then
open = true
for _, ev in ipairs(door.events) do
if not Game.save.flags[ev] then open = false break end
end
else
open = Game.save.flags[door.event]
end
local want = open and door.open or door.block
if self.map:blockAt(door.bx, door.by) ~= want then
self.map:setBlock(door.bx, door.by, want)
stamped = true
if open then unlocked = true end
end
end
if stamped then self.map.renderer:rebuild() end
if unlocked then require("src.core.Sound").play(Game.data, "Go_Inside") end
end
function OverworldState:setMap(mapId, x, y, facing, opts)
@@ -222,6 +274,13 @@ function OverworldState:setMap(mapId, x, y, facing, opts)
self.tileAnimOverride.tileset.animation = self.tileAnimOverride.animation
self.tileAnimOverride = nil
end
-- ADVANCED bakes colour into the tileset atlas, so the dark-cave shift has
-- to be armed before that atlas is built for this map (#383); self.dark is
-- settled below, once the map record is in hand.
if PaletteFX.setDarkWorld(isDarkMap(mapId) and not Game.save.flashLit)
and PaletteFX.usesGbcPack() then
MapLoader.invalidateAll()
end
self.map = MapLoader.load(Game.data, mapId)
-- STRENGTH deactivates on every real map load (home/overworld.asm
-- EnterMap -> ResetUsingStrengthOutOfBattleBit clears BIT_STRENGTH_ACTIVE
@@ -241,38 +300,7 @@ function OverworldState:setMap(mapId, x, y, facing, opts)
self.map.renderer:rebuild()
self.cutBlocks[mapId] = nil
end
-- Silph Co card key doors + Rocket Hideout elevator gates: the .blk
-- layouts ship with the doorways open; each floor's map script stamps
-- the closed door block on load until its unlock event is set
-- (scripts/SilphCo2F.asm SilphCo2FGateCallbackScript et al., closed
-- blocks $54/$5f/$20; scripts/RocketHideoutB1F.asm +
-- RocketHideoutB4F.asm ...DoorCallbackScript, closed blocks $54/$2d over
-- the lift doorway). A door opens on its single `event`, or on `events`
-- when every listed flag must be set (Rocket Hideout B4F's lift gate
-- needs both guard trainers beaten -- CheckBothEventsSet).
local closedDoors = FieldDefaults.fieldValue(Game.data, "cardKeyDoors",
"closedDoors")
local floorDoors = closedDoors and closedDoors[mapId]
if floorDoors then
local stamped = false
for _, door in ipairs(floorDoors) do
local open
if door.events then
open = true
for _, ev in ipairs(door.events) do
if not Game.save.flags[ev] then open = false break end
end
else
open = Game.save.flags[door.event]
end
local want = open and door.open or door.block
if self.map:blockAt(door.bx, door.by) ~= want then
self.map:setBlock(door.bx, door.by, want)
stamped = true
end
end
if stamped then self.map.renderer:rebuild() end
end
self:stampClosedDoors()
-- forced dismount only where riding is disallowed (IsBikeRidingAllowed,
-- home/overworld.asm: bike_riding_tilesets.asm tilesets plus the
-- ROUTE_23/INDIGO_PLATEAU map exceptions)
@@ -294,18 +322,11 @@ function OverworldState:setMap(mapId, x, y, facing, opts)
-- Rock Tunnel darkness (wMapPalOffset, home/overworld.asm): dark
-- until FLASH is used; the light persists between the tunnel floors
-- and resets once outside
local darkDef = Game.data.field.darkMaps
self.dark = false
if darkDef then
local isDark = false
for _, m in ipairs(darkDef.maps) do
if m == mapId then isDark = true break end
end
if isDark then
self.dark = not Game.save.flashLit
else
Game.save.flashLit = nil
end
if isDarkMap(mapId) then
self:setDark(not Game.save.flashLit)
else
Game.save.flashLit = nil
self:setDark(false)
end
if Game.data.field.flyWarps[mapId] then
Game.save.visited = Game.save.visited or {}
@@ -576,17 +597,19 @@ function OverworldState:sgbWorldZones()
return zones
end
-- Whether the dark-map shade shift (PaletteFX.DARK_BGP, armed in drawWorld)
-- can actually reach this frame's world pass. It cannot in RED++: that mode
-- bakes real per-tile colour into the tileset atlas and sgbWorldZones returns
-- an EMPTY zone list above, so the world blits with no shade-remap shader at
-- all and there is no palette left to permute. Only then does fxDark
-- composite the darkness by hand (#322).
function OverworldState:darkNeedsOverlay()
if not self.dark then return false end
local renderer = self.map and self.map.renderer
return PaletteFX.usesGbcPack() and renderer ~= nil
and renderer.gbcAtlas ~= nil
-- wMapPalOffset, the one piece of state both halves of the darkness read:
-- drawWorld arms PaletteFX.DARK_BGP off self.dark for the shade-remapped
-- modes, and PaletteFX.setDarkWorld feeds the bakes ADVANCED does instead of
-- shading (tileset atlas, sprite sheets) plus their cache keys. A bake cannot
-- be re-shaded in place, so a change there rebuilds every resident map --
-- every dark floor, not just this one, since FLASH lights them all (#383).
function OverworldState:setDark(on)
on = on and true or false
self.dark = on
if PaletteFX.setDarkWorld(on) and PaletteFX.usesGbcPack() and self.map then
MapLoader.invalidateAll()
self:reloadMap(self.map.id, "dark")
end
end
function OverworldState:npcByIndex(index)
@@ -795,6 +818,15 @@ function OverworldState:update(dt)
if ca.onDone then ca.onDone() end
end
end
-- fishing pose tail: the rod is already gone, the pose holds for the
-- frames the original spends unwinding the item menu (#384)
if self.fishPose then
self.fishPose = self.fishPose - 1
if self.fishPose <= 0 then
self.fishPose = nil
self.player.fishing = nil
end
end
-- Yellow's companion hopping up onto the Poke Center counter owns the
-- world for its arc, the same way the heal machine below does (#417)
if self.pikaHop then
@@ -961,19 +993,33 @@ function OverworldState:dirHeld()
or input:isDown("left") or input:isDown("right")
end
-- The warp cell the player WARPED IN on is inert until they physically step
-- off it: standing on it, or bonking a wall/edge from it, must not re-fire a
-- warp (CheckWarpsNoCollision's arrival-disable; see setMap where
-- warpEntryCell/justWarped are set and onStepComplete where they clear).
-- Both stand-still warp triggers -- the map-edge exit (checkEdgeExit) and the
-- blocked-step collision warp (handleInput) -- must consult this, or a corner
-- staircase whose warp tile sits on the map edge (Red's-house (7,1)) bounces
-- floors every input frame (issue #230).
function OverworldState:onWarpArrivalCell()
if self.justWarped then return true end
local entry = self.warpEntryCell
return entry ~= nil and self.player.cellX == entry.x
and self.player.cellY == entry.y
-- BIT_STANDING_ON_WARP (wMovementFlags): the warp under the player's feet may
-- only fire from a collision -- the blocked-step warp (handleInput) and the
-- map-edge exit (checkEdgeExit) -- while this flag is set. pokered clears it
-- on every completed step, sets it again when that step lands on a warp
-- square, then clears it once more when the square is a warp-activating tile
-- that is not also a door tile (CheckWarpsNoCollisionLoop ->
-- IsPlayerStandingOnDoorTileOrWarpTile, engine/overworld/player_state.asm);
-- onStepComplete maintains it. ClearVariablesOnEnterMap does not clear
-- wMovementFlags, so the flag rides through the warp itself: a house door
-- tile ($1B) leaves it set, so you land on the interior mat still able to
-- walk back out on that same tile (issue #378), while a staircase tile
-- ($1A/$1C) clears it and cannot bounce you between floors (issue #230).
function OverworldState:canCollisionWarp()
return self.standingOnWarp == true
end
-- Re-derive the flag from the tile under the player, the way a completed step
-- does (and the way MapEntryAfterBattle's IsPlayerStandingOnWarp does after a
-- battle): a door tile keeps it, a stair/ladder warp tile clears it.
function OverworldState:refreshStandingOnWarp()
local p = self.player
self.standingOnWarp = false
if self.map:warpAtCell(p.cellX, p.cellY)
and not (self.map:isWarpTileCell(p.cellX, p.cellY)
and not self.map:isDoorTileCell(p.cellX, p.cellY)) then
self.standingOnWarp = true
end
end
function OverworldState:handleInput()
@@ -1017,9 +1063,9 @@ function OverworldState:handleInput()
local result, why = self.player:tryMove(dir, self.map, self.entities)
-- a collision while standing on a warp square fires the warp when the
-- extra check passes (CheckWarpsCollision: route-gate doorways, dock
-- entrances, ...) -- but never on the inert cell we just warped in on
-- (issue #230), which the completed-step path guards the same way.
if result == "blocked" and not self:onWarpArrivalCell() then
-- entrances, ...), and only while BIT_STANDING_ON_WARP is set (issue
-- #230), which the map-edge path guards the same way.
if result == "blocked" and self:canCollisionWarp() then
local w = Warp.onCollision(self.map, Game.data.field.warpCarpets,
self.player.cellX, self.player.cellY, dir)
if w then
@@ -1175,11 +1221,11 @@ function OverworldState:checkEdgeExit(dir)
local w = Warp.onEdge(self.map, p.cellX, p.cellY, dir)
if w then
-- ...but not while still standing on the warp cell we just arrived on
-- (issue #230): fall through so pushing into the edge bonks (SFX +
-- walk-in-place) instead of instantly re-warping. A real step onto an
-- exit-carpet edge cleared warpEntryCell first, so those still fire.
if self:onWarpArrivalCell() then return false end
-- ...but only with BIT_STANDING_ON_WARP set: a staircase tile clears it,
-- so pushing into the edge beside one bonks (SFX + walk-in-place) instead
-- of bouncing floors (issue #230), while the door mat you warped in on
-- keeps it and exits on that same tile (issue #378).
if not self:canCollisionWarp() then return false end
self:takeWarp(w.def)
return true
end
@@ -1405,6 +1451,7 @@ function OverworldState:goFishing(rod)
-- the bobber waits a beat before the verdict (the original's
-- FishingInit dot animation); the rod pose draws in the meantime
self.fishing = { facing = self.player.facing }
self.player.fishing = true
Game.stack:push(TextBox.new(Game, ". . .", function()
-- FishingAnim (engine/overworld/player_animations.asm) holds
-- BIT_LEDGE_OR_FISHING -- the rod OAM and the fishing pose -- through
@@ -1412,12 +1459,20 @@ function OverworldState:goFishing(rod)
-- must NOT vanish with the dots box (#321).
if not enc then
Game.stack:push(TextBox.new(Game, Strings("Not even a nibble!"), function()
-- the rod OAM goes out with the verdict box (res BIT_LEDGE_OR_FISHING
-- straight after PrintText) but the player keeps the patched tiles
-- until the overworld reloads them a few frames later
-- (RestoreScreenTilesAndReloadTilePatterns, home/palettes.asm ->
-- ReloadMapSpriteTilePatterns, home/reload_sprites.asm) -- #384
self.fishing = nil
self.fishPose = 10
end))
return
end
Game.stack:push(TextBox.new(Game, Strings("Oh!\nIt's a bite!"), function()
-- the bite goes straight into battle, which reloads the sprite tiles
self.fishing = nil
self.player.fishing = nil
local BattleState = require("src.battle.BattleState")
local battle = BattleState.newWild(Game, enc.species, enc.level, { hooked = true })
if Game.save.safari and Map.inRegion(self.map.def, "SAFARI", "SAFARI_ZONE") then
@@ -2098,25 +2153,26 @@ end
-- Gen 1 has no confirmation prompt: using SURF gets straight on
-- (_SurfingGotOnText, item_effects.asm .surf). Called from the party
-- menu's SURF action (via useSurfFieldMove) once the facing tile has been
-- confirmed to be water -- there is no overworld A-press hook.
function OverworldState:trySurf(fx, fy)
-- confirmed to be water -- there is no overworld A-press hook. onClose is
-- that menu's own close, called when the got-on text ends (see below).
function OverworldState:trySurf(fx, fy, onClose)
local mon = self:partyKnows("SURF")
if not mon then return end
local name = mon.nickname or Game.data.pokemon[mon.species].name
local p = self.player
local text = (Game.data.text._SurfingGotOnText or Strings("{PLAYER} got on\n{RAM:wNameBuffer}!"))
:gsub("{RAM:wNameBuffer}", name)
-- GBPalWhiteOutWithDelay3 runs while the got-on text is still up
-- (start_sub_menus.asm .surf), so the blink reads as a text flash
-- instead of a flashbang on the empty map (#320). The flash sits
-- under the textbox on the stack: only the top state updates, so it
-- holds its frames until the text closes. surfing (and the sprite
-- swap) only applies when the step happens -- no paddling on land.
Game.stack:push(require("src.render.Transition").whiteFlash(Game))
-- UseItem prints the got-on text with the party menu still on screen and
-- GBPalWhiteOutWithDelay3 + .goBackToMap only run after it
-- (start_sub_menus.asm .surf), so the text reads over the menu and the
-- blink is the menu closing, not a flashbang on the empty map (#320,
-- #385). The mount rides the blink, so nothing paddles on land.
Game.stack:push(TextBox.new(Game, text, function()
if onClose then onClose() end
p.surfing = true
require("src.core.Music").setSurfing(Game.data, true)
self:stepForwardOrCrossEdge(p.facing)
Game.stack:push(require("src.render.Transition").whiteFlash(Game, nil,
function() self:stepForwardOrCrossEdge(p.facing) end))
end))
end
@@ -3050,18 +3106,16 @@ function OverworldState:onStepComplete()
entry = nil
end
-- The arrival disable is POSITIONAL: warpEntryCell above is the whole
-- test. justWarped only records that an arrival happened (it still
-- backs onWarpArrivalCell's bonk guard for issue #230), so consuming a
-- completed step with it swallowed the warp under the player's feet,
-- which is why a second ladder one cell from the first did nothing
-- (Seafoam B3F has warp tiles on (25,3) and (25,4)) -- issue #265.
-- pokered has no such counter: every completed step runs
-- CheckWarpsNoCollision (home/overworld.asm), and BIT_STANDING_ON_WARP,
-- the flag the bonk path needs, is only set by
-- CheckWarpsNoCollisionLoop itself or by IsPlayerStandingOnWarp from
-- MapEntryAfterBattle, never on a plain warp arrival -- which is
-- exactly what warpEntryCell reproduces.
self.justWarped = false
-- test. Consuming a completed step with a one-shot "just warped" counter
-- instead swallowed the warp under the player's feet, which is why a
-- second ladder one cell from the first did nothing (Seafoam B3F has warp
-- tiles on (25,3) and (25,4)) -- issue #265. pokered has no such counter:
-- every completed step runs CheckWarpsNoCollision (home/overworld.asm).
-- That same step is where BIT_STANDING_ON_WARP is maintained: cleared
-- before the check (home/overworld.asm:324), set again while standing on a
-- warp square, then cleared once more when the square is a warp-activating
-- tile that is not a door tile (IsPlayerStandingOnDoorTileOrWarpTile).
self:refreshStandingOnWarp()
if entry then
-- still standing on the warp we arrived through; do not re-trigger it
else
@@ -3499,6 +3553,10 @@ function OverworldState:afterBattle(result, battle)
{ save = Game.save, healTarget = self:healPoint() })
self:warpToHealPoint(evolutions)
else
-- EndTrainerBattle sets BIT_CUR_MAP_LOADED_1 (home/trainers.asm), which
-- re-runs the floor's door callback: beating the last Rocket Hideout guard
-- opens the lift gate without leaving the map (#372)
self:stampClosedDoors()
-- throwing the last SAFARI BALL ends the game
if Game.save.safari and Game.save.safari.balls <= 0 then
self:safariGameOver(Strings("PA: You're out of\nSAFARI BALLs!"))
@@ -3632,12 +3690,12 @@ function OverworldState:startWarpTo(mapId, x, y, facing, onDone, opts)
self.arriveWarp = nil
Game.stack:push(Transition.new(Game, function()
self:setMap(mapId, x, y, facing or "down", opts)
self.justWarped = true
-- The warp we land ON stays inert until we physically step off it, so a
-- warp whose destination cell is itself a warp cannot bounce us straight
-- back (elevator cars, stacked stair/door mats). This generalizes the
-- one-step justWarped guard, which only skipped the very next frame's
-- check and so let a mon walked back onto the pad re-trigger it.
-- The warp we land ON stays inert for the completed-step check until we
-- physically step off it, so a warp whose destination cell is itself a
-- warp cannot bounce us straight back (elevator cars, stacked stair/door
-- mats). BIT_STANDING_ON_WARP is deliberately NOT touched here:
-- ClearVariablesOnEnterMap leaves wMovementFlags alone, so the flag the
-- departing tile set rides through the warp (issue #378).
self.warpEntryCell = { x = x, y = y }
-- Fly/Teleport/Dig/Escape-Rope landings poof the player back in
-- (player_animations.asm EnterMapAnim). Blackouts and ordinary
@@ -3663,8 +3721,8 @@ function OverworldState:startWarpTo(mapId, x, y, facing, onDone, opts)
-- PlayerStepOutFromDoor (engine/overworld/auto_movement.asm): any
-- warp that lands on a door tile auto-steps south once, indoor or
-- outdoor. Auto-walk leaves the mat, so the arrival disable
-- (warpEntryCell / justWarped) is unnecessary -- and would let you
-- stand on the door without re-entering if you hold back into it.
-- (warpEntryCell) is unnecessary -- and would let you stand on the
-- door without re-entering if you hold back into it.
-- The walk-out is a simulated d-pad press (wSimulatedJoypadStates),
-- not a forced move, so it obeys collision: on a landing with a
-- solid cell south of the door (the mansion stair landings back
@@ -3673,7 +3731,6 @@ function OverworldState:startWarpTo(mapId, x, y, facing, onDone, opts)
if self.map:isDoorTileCell(self.player.cellX, self.player.cellY) then
if Collision.canMove(self.map, self.entities, self.player, "down") then
self.warpEntryCell = nil
self.justWarped = false
self:scriptMove(self.player, "down", 1)
else
self.player.facing = "down"
@@ -4116,23 +4173,6 @@ function OverworldState:drawWorld()
end
end
-- Rock Tunnel darkness. The original never cuts a window of light around
-- the player: it shifts the BG palette for the WHOLE screen (wMapPalOffset
-- = 6 -> home/fade.asm LoadGBPal -> FadePal2 `dc 3,3,3,2`) and FLASH shifts
-- it back (#322). PaletteFX.DARK_BGP does that for every shade-remapped
-- mode, armed at the top of drawWorld. RED++ is the one mode with no
-- palette left to shift -- TileRenderer bakes true colour into the tileset
-- atlas and sgbWorldZones hands the blit an EMPTY zone list, so no shader
-- runs over the world at all -- so there the darkness is composited instead:
-- a flat veil over the whole world view (surveying still cannot peek past
-- it) at the 85/255 brightness FadePal2 leaves DMG white on.
local function fxDark()
if not self:darkNeedsOverlay() then return end
love.graphics.setColor(0, 0, 0, 1 - 85 / 255)
love.graphics.rectangle("fill", 0, 0, vw, vh)
love.graphics.setColor(1, 1, 1, 1)
end
-- the FLY bird sweeping off with the player
local function fxBird()
if not self.flyAnim then return end
@@ -4210,7 +4250,7 @@ function OverworldState:drawWorld()
return PaletteFX.pal(Game.data, self:paletteNameFor(map or self.map))
end,
fx = { heal = fxHeal, dust = fxDust, cutTree = fxCutTree,
emote = fxEmote, dark = fxDark, bird = fxBird, rod = fxRod },
emote = fxEmote, bird = fxBird, rod = fxRod },
}
-- Draw every active field FX into the finished scene. `project(wx, wy)`
-- maps a world point to canvas pixels (nil when it is behind the
@@ -4261,17 +4301,6 @@ function OverworldState:drawWorld()
if self.fishing then
at(fxRod, self.player.px + 8, self.player.py + 16)
end
-- Rock Tunnel darkness is a screen-space veil, not a ground object:
-- draw it flat over the finished scene like the tilt path. It fills
-- the view in world-pixel units, so it only needs the scale, and it is
-- only needed at all in the mode whose palette cannot carry the
-- darkening itself (see fxDark).
if self:darkNeedsOverlay() then
love.graphics.push()
love.graphics.scale(scale, scale)
fxDark()
love.graphics.pop()
end
end
override = Pipelines.drawWorld(pipelineId, ctx)
-- world post-processes (a miniature-diorama blur, a colour grade) fold
@@ -4334,7 +4363,6 @@ function OverworldState:drawWorld()
fxDust()
fxCutTree()
fxEmote()
fxDark()
fxBird()
fxRod()
else
@@ -4423,12 +4451,6 @@ function OverworldState:drawWorld()
self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false, fxRod)
end
-- Rock Tunnel darkness is a screen-space veil, not a ground object --
-- draw it flat into the upright canvas so it darkens the final
-- composited scene uniformly. It no-ops unless this mode needs the
-- composited fallback (see fxDark).
fxDark()
Game.renderer:endUprightPass()
end
+29 -1
View File
@@ -42,6 +42,20 @@ function Player.new(data, cx, cy, facing)
local ok, img = pcall(love.graphics.newImage, fx.shadow.path)
self.shadowImg = ok and img or nil
end
-- FishingAnim (engine/overworld/player_animations.asm) patches tiles
-- $02/$06/$0a -- the bottom tile row of each standing frame -- with
-- RedFishingTiles before it parks the rod OAM, so the rod stroke meets a
-- pair of hands instead of ending in mid air (#384)
if fx then
local function posePath(name)
local def = fx[name]
return def and def.path or nil
end
local pose = { down = posePath("redFishFront"), up = posePath("redFishBack") }
pose.left = posePath("redFishSide")
pose.right = pose.left -- the side pose mirrors like the sprite (OAM_XFLIP)
if pose.down or pose.up or pose.left then self.fishTiles = pose end
end
self.cellX, self.cellY = cx, cy
self.px, self.py = cx * 16, cy * 16
self.facing = facing or "down"
@@ -231,7 +245,10 @@ function Player:pose()
py = py - math.floor((total - self.spinFrames) * 24 / total)
end
end
local sprite = (self.surfing and self.surfSprite)
-- 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.surfSprite)
or (self.onBike and self.bikeSprite) or self.sprite
return sprite, self.px, py, facing, phase, flip, hopping
end
@@ -263,6 +280,17 @@ function Player:draw(camX, camY)
love.graphics.draw(self.shadowImg, sx + 16, sy + 16, 0, -1, -1)
end
end
-- Fishing pose: the standing frame with its bottom tile row swapped for
-- RedFishingTiles, which is where the hands and the near half of the rod
-- live; the far half is the rod OAM OverworldState draws (FishingRodOAM,
-- engine/overworld/player_animations.asm) -- #384
local fishTile = self.fishing and self.fishTiles and self.fishTiles[facing]
if fishTile then
sprite:draw(px, py, camX, camY, facing, 0, false, true)
sprite:drawTile(fishTile, math.floor(px - camX),
math.floor(py - camY) - 4 + 8, facing == "right")
return
end
sprite:draw(px, py, camX, camY, facing, phase, flip)
end