mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-26 07:21:22 +02:00
Spider Car Unleashed
CLOSES #1483, CLOSES #1610, CLOSES #1615, CLOSES #1646, CLOSES #1649, CLOSES #1651, CLOSES #1653, CLOSES #1656, CLOSES #1683, CLOSES #1685, CLOSES #1686, CLOSES #1687, CLOSES #1688, CLOSES #1689, CLOSES #1690, CLOSES #1693, CLOSES #1694, CLOSES #1695, CLOSES #1696, CLOSES #1702, CLOSES #1704, CLOSES #1705, CLOSES #1706, CLOSES #1707, CLOSES #1708, CLOSES #1710, CLOSES #1711, CLOSES #1712, CLOSES #1713, CLOSES #1716, CLOSES #1717, CLOSES #1718, CLOSES #1719, CLOSES #1720, CLOSES #1721, CLOSES #1725, CLOSES #1732, CLOSES #1745, CLOSES #1748, CLOSES #1749, CLOSES #1751, CLOSES #1754
This commit is contained in:
@@ -42,6 +42,23 @@ function NPC.new(data, mapId, objDef)
|
||||
return self
|
||||
end
|
||||
|
||||
-- LoadMapHeader .loadSpriteData zeroes the sprite state data and re-seeds
|
||||
-- MAPY/MAPX from the map header -- home/overworld.asm:2133
|
||||
function NPC:resetToSpawn()
|
||||
local def = self.def
|
||||
self.cellX, self.cellY = def.x, def.y
|
||||
self.px, self.py = self.cellX * 16, self.cellY * 16
|
||||
self.facing = FACING_FROM_RANGE[def.range] or "down"
|
||||
self.targetX, self.targetY = nil, nil
|
||||
self.moving = false
|
||||
self.marching = false
|
||||
self.hopStep = nil
|
||||
self.progress = 0
|
||||
self.animClock = 0
|
||||
self.stepFlip = false
|
||||
self.timer = love.math.random(30, 120)
|
||||
end
|
||||
|
||||
function NPC:facePlayer(player)
|
||||
local dx = player.cellX - self.cellX
|
||||
local dy = player.cellY - self.cellY
|
||||
|
||||
@@ -81,6 +81,24 @@ local HEAL_BALL_XY = {
|
||||
-- swaps the two middle shades of the monitor/ball art in place
|
||||
local HEAL_FLASH_MAP = { [0] = 0, [1] = 2, [2] = 1, [3] = 3 }
|
||||
|
||||
-- scripts/VermilionDock.asm:39 VermilionDockSSAnneLeavesScript: her hull is
|
||||
-- the four blocks (5..8, 1..2) of VermilionDock.blk
|
||||
local SS_ANNE_BLOCK = { x = 5, y = 1, w = 4, h = 2 }
|
||||
-- scripts/VermilionDock.asm:164 VermilionDock_SyncScrollWithLY splits rSCX at
|
||||
-- LY $50, so her top 16px -- the player's own cell row -- never scrolls
|
||||
local SS_ANNE_KEEP_PX = 16
|
||||
-- scripts/VermilionDock.asm:79 `ld e, $8`: eight 16px columns, and each
|
||||
-- .delay_between_drifts pass is eight frames per pixel
|
||||
local SS_ANNE_SAIL_PX, SS_ANNE_PX_FRAMES = 128, 8
|
||||
-- scripts/VermilionDock.asm:182 VermilionDock_EraseSSAnne: block 1 is the
|
||||
-- shoreline row she sat in, block 13 the open water below it
|
||||
local SS_ANNE_WATER = { 1, 13 }
|
||||
-- scripts/VermilionDock.asm:142 VermilionDock_EmitSmokePuff: a puff per
|
||||
-- column off the front smokestack, drifting east 2px per drift step
|
||||
local SS_ANNE_SMOKE = { dx = 64, dy = 20, drift = 2, every = 16, count = 5 }
|
||||
-- scripts/VermilionDock.asm:65 `ldh [rOBP1], a` with a = 0: the puff is white
|
||||
local SS_ANNE_SMOKE_MAP = { [0] = 0, [1] = 0, [2] = 0, [3] = 0 }
|
||||
|
||||
-- Fishing rod placement (FishingRodOAM, engine/overworld/player_animations
|
||||
-- .asm). Those dbsprite rows are raw shadow-OAM bytes like HEAL_BALL_XY
|
||||
-- above (screen = tile*8 + pixel - 8/16), measured against the player
|
||||
@@ -320,6 +338,7 @@ function OverworldState:setMap(mapId, x, y, facing, opts)
|
||||
self.parallelQueue = {}
|
||||
end
|
||||
self.marchers = {}
|
||||
self.shipAnim = nil
|
||||
local queue = self.pendingScripts
|
||||
if queue then
|
||||
for i = #queue, 1, -1 do
|
||||
@@ -413,12 +432,14 @@ function OverworldState:setMap(mapId, x, y, facing, opts)
|
||||
-- (home/overworld.asm), so an NPC who walked up to the player stands
|
||||
-- on her spawn cell again the next time that map loads (#1028). Only
|
||||
-- the save-side spawn flags survive, and those live in Game.save, not
|
||||
-- here. Warps rebuild the whole pool from scratch.
|
||||
-- here. Warps rebuild the whole pool from scratch; a seam crossing arms
|
||||
-- the re-seed and applyPendingSpawnResets lands it off camera (#1755).
|
||||
if not (opts and opts.seamless and self.npcPool) then
|
||||
self.npcPool = {}
|
||||
elseif fromMapId ~= mapId then
|
||||
for _, obj in ipairs(self.map.def.objects or {}) do
|
||||
self.npcPool[mapId .. "_obj_" .. obj.index] = nil
|
||||
local npc = self.npcPool[mapId .. "_obj_" .. obj.index]
|
||||
if npc then npc.pendingSpawnReset = true end
|
||||
end
|
||||
end
|
||||
self.npcs = {}
|
||||
@@ -624,6 +645,41 @@ function OverworldState:rebuildNeighbors()
|
||||
end
|
||||
end
|
||||
|
||||
-- the deferred half of the seam re-seed armed in setMap (#1755): the cart's
|
||||
-- connected map has no sprites to be seen snapping -- home/overworld.asm:2133
|
||||
function OverworldState:applyPendingSpawnResets()
|
||||
local pool = self.npcPool
|
||||
if not (pool and self.camera) then return end
|
||||
local due
|
||||
for _, npc in pairs(pool) do
|
||||
if npc.pendingSpawnReset then
|
||||
due = due or {}
|
||||
due[npc] = true
|
||||
end
|
||||
end
|
||||
if not due then return end
|
||||
local cam = self.camera
|
||||
local vw, vh = Game.renderer:worldViewSize()
|
||||
local function onCamera(px, py)
|
||||
return px + 16 > cam.x - 16 and px < cam.x + vw + 16
|
||||
and py + 16 > cam.y - 16 and py < cam.y + vh + 16
|
||||
end
|
||||
for _, mv in ipairs(self.scriptMoves or {}) do due[mv.entity] = nil end
|
||||
for entity in pairs(self.marchers or {}) do due[entity] = nil end
|
||||
for _, npc in ipairs(self.npcs or {}) do
|
||||
if due[npc] and onCamera(npc.px, npc.py) then due[npc] = nil end
|
||||
end
|
||||
for _, g in ipairs(self.ghosts or {}) do
|
||||
if due[g.npc] and onCamera(g.npc.px + g.ox, g.npc.py + g.oy) then
|
||||
due[g.npc] = nil
|
||||
end
|
||||
end
|
||||
for npc in pairs(due) do
|
||||
npc.pendingSpawnReset = nil
|
||||
npc:resetToSpawn()
|
||||
end
|
||||
end
|
||||
|
||||
-- SGB overworld palette (engine/gfx/palettes.asm SetPal_Overworld):
|
||||
-- towns use their own palette, routes PAL_ROUTE, interiors the town or
|
||||
-- route they are in (wLastMap = our lastOutdoor), with tileset and
|
||||
@@ -1068,6 +1124,27 @@ function OverworldState:update(dt)
|
||||
if da.onDone then da.onDone() end
|
||||
end
|
||||
end
|
||||
-- scripts/VermilionDock.asm:80 .shift_columns_up
|
||||
if self.shipAnim and not self.shipAnim.gone then
|
||||
local sa = self.shipAnim
|
||||
sa.frames = sa.frames + 1
|
||||
if sa.frames >= SS_ANNE_PX_FRAMES then
|
||||
sa.frames = 0
|
||||
sa.off = sa.off + 1
|
||||
if sa.off % SS_ANNE_SMOKE.every == 1
|
||||
and #sa.puffs < SS_ANNE_SMOKE.count then
|
||||
sa.puffs[#sa.puffs + 1] = { x = sa.px - sa.off + SS_ANNE_SMOKE.dx,
|
||||
y = sa.py + SS_ANNE_SMOKE.dy }
|
||||
end
|
||||
for _, p in ipairs(sa.puffs) do p.x = p.x + SS_ANNE_SMOKE.drift end
|
||||
if sa.off >= SS_ANNE_SAIL_PX then
|
||||
sa.gone, sa.puffs = true, {}
|
||||
local done = sa.onDone
|
||||
sa.onDone = nil
|
||||
if done then done() end
|
||||
end
|
||||
end
|
||||
end
|
||||
if self.cutAnim then
|
||||
local ca = self.cutAnim
|
||||
ca.frames = ca.frames - 1
|
||||
@@ -1227,6 +1304,7 @@ function OverworldState:update(dt)
|
||||
-- escort then walks an extra tile before PlayerEntryMovementRLE, and
|
||||
-- the player lands on desk Oak.
|
||||
local scripted = self.runner:isRunning() or #self.scriptMoves > 0
|
||||
or (self.hopLand or 0) > 0
|
||||
or self.engaging or self.emote or self.teleportOut
|
||||
or self.flyAnim or self.flyArrive
|
||||
if not scripted and not self.transitioning then
|
||||
@@ -1236,6 +1314,7 @@ function OverworldState:update(dt)
|
||||
-- direction handling (JoypadOverworld runs the map script first) --
|
||||
-- the player can never start another step after being spotted.
|
||||
scripted = self.runner:isRunning() or #self.scriptMoves > 0
|
||||
or (self.hopLand or 0) > 0
|
||||
or self.engaging or self.emote or self.teleportOut
|
||||
or self.flyAnim or self.flyArrive
|
||||
end
|
||||
@@ -1244,6 +1323,7 @@ function OverworldState:update(dt)
|
||||
if not scripted and not self.transitioning and Game.stack:top() == self then
|
||||
self:handleInput()
|
||||
end
|
||||
if (self.hopLand or 0) > 0 then self.hopLand = self.hopLand - 1 end
|
||||
|
||||
local stepped = self.player:update()
|
||||
-- the warp-arrival cell goes stale the instant the player's real cell
|
||||
@@ -1271,6 +1351,7 @@ function OverworldState:update(dt)
|
||||
|
||||
self.camera:follow(self.player.px, self.player.py,
|
||||
Game.renderer:worldViewSize())
|
||||
self:applyPendingSpawnResets()
|
||||
|
||||
-- pan_camera offset rides on top of the follow; the ramp resumes its
|
||||
-- runner when it lands
|
||||
@@ -1509,6 +1590,76 @@ function OverworldState:startDustAnim(cx, cy, onDone)
|
||||
self.dustAnim = { x = cx, y = cy, frames = 32, onDone = onDone }
|
||||
end
|
||||
|
||||
-- scripts/VermilionDock.asm:39 VermilionDockSSAnneLeavesScript: snapshot her
|
||||
-- hull tiles, flood her box with water, and slide the snapshot west from there
|
||||
function OverworldState:startSsAnneDeparture(onDone)
|
||||
local map = self.map
|
||||
local tx0, ty0 = SS_ANNE_BLOCK.x * 4, SS_ANNE_BLOCK.y * 4
|
||||
local tiles = {}
|
||||
for row = 1, SS_ANNE_BLOCK.h * 4 do
|
||||
local r = {}
|
||||
for col = 1, SS_ANNE_BLOCK.w * 4 do
|
||||
r[col] = map:tileAt(tx0 + col - 1, ty0 + row - 1)
|
||||
end
|
||||
tiles[row] = r
|
||||
end
|
||||
for bx = SS_ANNE_BLOCK.x, SS_ANNE_BLOCK.x + SS_ANNE_BLOCK.w - 1 do
|
||||
for by = SS_ANNE_BLOCK.y, SS_ANNE_BLOCK.y + SS_ANNE_BLOCK.h - 1 do
|
||||
map:setBlock(bx, by, SS_ANNE_WATER[by - SS_ANNE_BLOCK.y + 1] or 13)
|
||||
end
|
||||
end
|
||||
map.renderer:rebuild()
|
||||
self.shipAnim = { px = tx0 * 8, py = ty0 * 8, tiles = tiles,
|
||||
off = 0, frames = 0, puffs = {}, onDone = onDone }
|
||||
end
|
||||
|
||||
-- scripts/VermilionDock.asm:164: the rSCX split keeps her top 16px (the
|
||||
-- shoreline row and the gangway under the player) put while the rest sails
|
||||
function OverworldState:drawShipAnim(camX, camY)
|
||||
local sa = self.shipAnim
|
||||
if not sa then return end
|
||||
local renderer = self.map.renderer
|
||||
local img, quads = renderer.image, renderer.quads
|
||||
local ox, oy = -math.floor(camX), -math.floor(camY)
|
||||
local keep = SS_ANNE_KEEP_PX / 8
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
for row = 1, #sa.tiles do
|
||||
if row <= keep or not sa.gone then
|
||||
local slide = row > keep and sa.off or 0
|
||||
local wy = sa.py + (row - 1) * 8 + oy
|
||||
for col = 1, #sa.tiles[row] do
|
||||
local quad = quads[sa.tiles[row][col]]
|
||||
if quad then
|
||||
love.graphics.draw(img, quad, sa.px + (col - 1) * 8 - slide + ox, wy)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if #sa.puffs == 0 then return end
|
||||
local fxDef = Game.data.field.overworldFx
|
||||
local smoke = fxDef and fxDef.smoke
|
||||
if not smoke then return end
|
||||
if self.smokeImg == nil then
|
||||
local ok, image = pcall(love.graphics.newImage, smoke.path)
|
||||
self.smokeImg = ok and image or false
|
||||
end
|
||||
if not self.smokeImg then return end
|
||||
local shader = PaletteFX.shader()
|
||||
if shader then
|
||||
PaletteFX.sendColors(shader,
|
||||
PaletteFX.permute(PaletteFX.GRAYS, SS_ANNE_SMOKE_MAP))
|
||||
love.graphics.setShader(shader)
|
||||
end
|
||||
for _, p in ipairs(sa.puffs) do
|
||||
for i = 0, 1 do
|
||||
for j = 0, 1 do
|
||||
love.graphics.draw(self.smokeImg, p.x + i * 8 + ox, p.y + j * 8 + oy)
|
||||
end
|
||||
end
|
||||
end
|
||||
if shader then love.graphics.setShader() end
|
||||
end
|
||||
|
||||
-- Ledge hops (data/tilesets/ledge_tiles.asm): standing tile + ledge tile
|
||||
-- in front + matching input direction -> jump two cells.
|
||||
function OverworldState:checkLedgeHop(dir)
|
||||
@@ -1542,17 +1693,22 @@ function OverworldState:checkLedgeHop(dir)
|
||||
return false
|
||||
end
|
||||
require("src.core.Sound").play(Game.data, "Ledge")
|
||||
local hop = (p.stepFramesCur or p.stepFrames or 16) * 2
|
||||
p.ledgeHop = true -- BIT_LEDGE_OR_FISHING, no bike speedup mid-hop
|
||||
local hop = p:stepLength() * 2
|
||||
p.hopFrames, p.hopTotal = hop, hop -- jump arc (cosmetic)
|
||||
self:scriptMove(p, dir, 1, function() self:checkEdgeExit(dir) end)
|
||||
self:scriptMove(p, dir, 1, function()
|
||||
self:checkEdgeExit(dir)
|
||||
self:finishLedgeHop()
|
||||
end)
|
||||
return true
|
||||
end
|
||||
if not Collision.occupied(self.entities, lx, ly, p)
|
||||
and self.map:isWalkableCell(lx, ly) then
|
||||
require("src.core.Sound").play(Game.data, "Ledge")
|
||||
local hop = (p.stepFramesCur or p.stepFrames or 16) * 2
|
||||
p.ledgeHop = true -- BIT_LEDGE_OR_FISHING, no bike speedup mid-hop
|
||||
local hop = p:stepLength() * 2
|
||||
p.hopFrames, p.hopTotal = hop, hop -- jump arc (cosmetic)
|
||||
self:scriptMove(p, dir, 2)
|
||||
self:scriptMove(p, dir, 2, function() self:finishLedgeHop() end)
|
||||
return true
|
||||
end
|
||||
end
|
||||
@@ -1560,6 +1716,13 @@ function OverworldState:checkLedgeHop(dir)
|
||||
return false
|
||||
end
|
||||
|
||||
-- _HandleMidJump .finishedJump lands with UpdateSprites + Delay3 before it
|
||||
-- clears the joypad bytes -- engine/overworld/player_animations.asm:509
|
||||
function OverworldState:finishLedgeHop()
|
||||
self.player.ledgeHop = nil
|
||||
self.hopLand = 3
|
||||
end
|
||||
|
||||
-- walking off the map edge: connection crossing or edge warp (exit mats)
|
||||
function OverworldState:checkEdgeExit(dir)
|
||||
local p = self.player
|
||||
@@ -1665,9 +1828,7 @@ function OverworldState:crossConnection(dir, conn)
|
||||
-- fresh walk-cycle clock so the seam step always shows leg frames
|
||||
-- (mid-cycle stand phase would otherwise look like a slide)
|
||||
p.animClock = 0
|
||||
p.stepFramesCur = Game.save.onBike
|
||||
and (FieldDefaults.world(Game.data, "bikeStepFrames") or 8)
|
||||
or (FieldDefaults.world(Game.data, "stepFrames") or 16)
|
||||
p.stepFramesCur = p:stepLength()
|
||||
require("src.core.FixedStep"):discardCatchup()
|
||||
return true
|
||||
end
|
||||
@@ -2413,16 +2574,12 @@ function OverworldState:trashCanSwitch(canIndex)
|
||||
local adj = tc.adjacent[puz.first]
|
||||
local masked = require("bit").band(love.math.random(0, 255), #adj)
|
||||
puz.second = masked == 0 and 0 or adj[masked]
|
||||
-- VermilionGymTrashSuccessText1's text_asm tail plays SFX_SWITCH only
|
||||
-- after the text has printed (text_far ...; text_asm;
|
||||
-- WaitForSoundToFinish; PlaySound SFX_SWITCH; WaitForSoundToFinish),
|
||||
-- and DisplayTextID's WaitForTextScrollButtonPress then holds the box
|
||||
-- until the player dismisses it -- so the beep belongs on close, not
|
||||
-- open.
|
||||
-- engine/events/hidden_events/vermilion_gym_trash.asm:130 (text_asm tail:
|
||||
-- SFX_SWITCH once the text has printed, before the button wait) (#1702)
|
||||
Game.stack:push(TextBox.new(Game,
|
||||
t._VermilionGymTrashSuccessText1
|
||||
or Strings("Hey! There's a\nswitch under the\ntrash!\fThe 1st electric\nlock opened!"),
|
||||
function() require("src.core.Sound").play(Game.data, "Switch") end))
|
||||
nil, TextBox.soundOpts(Game, "Switch")))
|
||||
return
|
||||
end
|
||||
-- .trySecondLock
|
||||
@@ -2433,25 +2590,28 @@ function OverworldState:trashCanSwitch(canIndex)
|
||||
-- the clear floor block opens the doors (VermilionGymSetDoorTile)
|
||||
local door = FieldDefaults.fieldValue(Game.data, "hiddenExtras",
|
||||
"trashCans", "doorBlock")
|
||||
self:replaceBlock(door.bx, door.by, door.block)
|
||||
-- SuccessText3's text_asm tail plays SFX_GO_INSIDE after the text
|
||||
-- prints, so the beep fires as the box closes, not as it opens.
|
||||
-- engine/events/hidden_events/vermilion_gym_trash.asm:153 beeps as the
|
||||
-- text finishes; scripts/VermilionGym.asm:30 beeps on the swap (#1702)
|
||||
Game.stack:push(TextBox.new(Game,
|
||||
t._VermilionGymTrashSuccessText3
|
||||
or Strings("The 2nd electric\nlock opened!\fThe motorized door\nopened!"),
|
||||
function() require("src.core.Sound").play(Game.data, "Go_Inside") end))
|
||||
function()
|
||||
require("src.core.Sound").play(Game.data, "Go_Inside")
|
||||
self:replaceBlock(door.bx, door.by, door.block)
|
||||
end,
|
||||
TextBox.soundOpts(Game, "Go_Inside")))
|
||||
else
|
||||
-- wrong can: ResetEvent EVENT_1ST_LOCK_OPENED and immediately
|
||||
-- re-roll the first switch (Random & $e)
|
||||
save.flags.EVENT_1ST_LOCK_OPENED = nil
|
||||
puz.first = love.math.random(0, 7) * 2
|
||||
puz.second = nil
|
||||
-- VermilionGymTrashFailText's text_asm tail plays SFX_DENIED after the
|
||||
-- text prints, so the beep fires as the box closes, not as it opens.
|
||||
-- engine/events/hidden_events/vermilion_gym_trash.asm:162 (text_asm tail:
|
||||
-- SFX_DENIED once the text has printed, before the button wait) (#1702)
|
||||
Game.stack:push(TextBox.new(Game,
|
||||
t._VermilionGymTrashFailText
|
||||
or Strings("Nope! There's\nonly trash here.\fHey! The electric\nlocks were reset!"),
|
||||
function() require("src.core.Sound").play(Game.data, "Denied") end))
|
||||
nil, TextBox.soundOpts(Game, "Denied")))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2899,10 +3059,10 @@ function OverworldState:talkTo(npc)
|
||||
if entry then
|
||||
if entry.mart then
|
||||
npc:facePlayer(self.player)
|
||||
Game.stack:push(TextBox.new(Game, romText(Game.data, "_PokemartGreetingText", "Hi there!\nMay I help you?"), function()
|
||||
Screens.push(Game, "ShopMenu", entry.mart)
|
||||
unfreeze()
|
||||
end))
|
||||
-- the greeting stays in the box under the menu, so ShopMenu owns it
|
||||
-- now -- home/text_script.asm:143
|
||||
Screens.push(Game, "ShopMenu", entry.mart)
|
||||
unfreeze()
|
||||
return
|
||||
end
|
||||
if entry.nurse then
|
||||
@@ -3365,22 +3525,18 @@ function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText
|
||||
or (header and header.won and Game.data.text[header.won])
|
||||
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local stingPlayed = false
|
||||
-- home/trainers.asm:109 prints, :123 engages (BIT_SEEN_BY_TRAINER =
|
||||
-- self.engaging), then home/text_script.asm:96 waits for A (#764, #1683)
|
||||
local function playMeetSting()
|
||||
if stingPlayed or self.engaging then return end
|
||||
stingPlayed = true
|
||||
local theme = meetTrainerTheme(d.trainerClass)
|
||||
if theme then require("src.core.Music").play(Game.data, theme) end
|
||||
end
|
||||
local function startBattle(options)
|
||||
self.cancelledTrainerSight = nil
|
||||
-- TalkToTrainer (home/trainers.asm:88) prints the before-battle text
|
||||
-- FIRST and only then runs `call EngageMapTrainer` / `jp
|
||||
-- StartTrainerBattle`, so a trainer challenged on foot gets the sting
|
||||
-- over the battle transition rather than under the dialogue. Its
|
||||
-- `bit BIT_SEEN_BY_TRAINER, [hl] / ret nz` guard is self.engaging
|
||||
-- here: TrainerEngage (engine/overworld/trainer_sight.asm:224) already
|
||||
-- started the sting before the "!" bubble on the sight path, so it
|
||||
-- must not restart. Script-driven challenges (gyms.lua leaders,
|
||||
-- scripts/SilphCo11F.asm:269 Giovanni, scripts/FightingDojo.asm:122)
|
||||
-- all `call EngageMapTrainer` too, and reach this same path (#764).
|
||||
if not self.engaging then
|
||||
local theme = meetTrainerTheme(d.trainerClass)
|
||||
if theme then require("src.core.Music").play(Game.data, theme) end
|
||||
end
|
||||
playMeetSting()
|
||||
local battle = BattleState.newTrainer(Game, d.trainerClass, d.trainerParty,
|
||||
options)
|
||||
battle.checkpointOrigin = {
|
||||
@@ -3451,7 +3607,8 @@ function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText
|
||||
if skipBattleText then
|
||||
prepareBattle()
|
||||
else
|
||||
Game.stack:push(TextBox.new(Game, battleText, prepareBattle))
|
||||
Game.stack:push(TextBox.new(Game, battleText, prepareBattle,
|
||||
{ auto = { wait = true, delay = 0, sound = playMeetSting } }))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4395,18 +4552,11 @@ function OverworldState:afterBattle(result, battle)
|
||||
Logger.info("battle over: %s (lead %s %d/%d)", tostring(result),
|
||||
lead and lead.species or "-", lead and lead.hp or 0,
|
||||
lead and lead.stats.hp or 0)
|
||||
local Evolution = require("src.pokemon.Evolution")
|
||||
local function evolutions()
|
||||
-- Only mons that gained a level this battle (EXP.ALL included).
|
||||
-- Scanning the whole party re-offered B-cancelled evolutions forever (#213).
|
||||
Evolution.checkParty(Game, nil, battle and battle.leveledUp)
|
||||
end
|
||||
if result == "lose" then
|
||||
local oaksLabRival = battle and battle.oppClass == "OPP_RIVAL1"
|
||||
and self.map and self.map.id == "OAKS_LAB"
|
||||
if oaksLabRival then
|
||||
-- stay in the lab; OaksLabRivalEndBattleScript heals and continues
|
||||
evolutions()
|
||||
return
|
||||
end
|
||||
-- blackout: revive the party at the last heal point; half the
|
||||
@@ -4419,7 +4569,7 @@ function OverworldState:afterBattle(result, battle)
|
||||
/ (FieldDefaults.world(Game.data, "blackoutMoneyDivisor") or 2))
|
||||
Runtime.emit("world.blacked_out",
|
||||
{ save = Game.save, healTarget = self:healPoint() })
|
||||
self:warpToHealPoint(evolutions)
|
||||
self:warpToHealPoint()
|
||||
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
|
||||
@@ -4429,7 +4579,6 @@ function OverworldState:afterBattle(result, battle)
|
||||
if Game.save.safari and Game.save.safari.balls <= 0 then
|
||||
self:safariGameOver(Strings("PA: You're out of\nSAFARI BALLs!"))
|
||||
end
|
||||
evolutions()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4858,6 +5007,9 @@ function OverworldState:updateScriptMoves()
|
||||
e.facing = mv.dir
|
||||
local tx, ty = Collision.target(e.cellX, e.cellY, mv.dir)
|
||||
e.targetX, e.targetY = tx, ty
|
||||
-- a simulated d-pad press runs at the CURRENT walk/bike speed, not
|
||||
-- whatever the last real step left behind -- home/overworld.asm:276
|
||||
if e.stepLength then e.stepFramesCur = e:stepLength() end
|
||||
e.moving = true
|
||||
e.progress = 0
|
||||
mv.remaining = mv.remaining - 1
|
||||
@@ -5018,6 +5170,7 @@ function OverworldState:drawWorld()
|
||||
for _, nb in ipairs(self.neighbors) do
|
||||
nb.map.renderer:drawMapOnly(cam.x - nb.ox, bgY - nb.oy, vw, vh)
|
||||
end
|
||||
self:drawShipAnim(cam.x, bgY)
|
||||
end
|
||||
-- per-billboard SGB palette source; only needed (and only paid for) when
|
||||
-- tilting. nil headless / on stale palettes -> billboards go uncolorized.
|
||||
@@ -5374,6 +5527,7 @@ function OverworldState:drawWorld()
|
||||
for _, nb in ipairs(self.neighbors) do
|
||||
nb.map.renderer:drawMapOnly(cam.x - nb.ox, bgY - nb.oy, vw, vh)
|
||||
end
|
||||
self:drawShipAnim(cam.x, bgY)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+21
-16
@@ -110,6 +110,26 @@ function Player:turnWindow()
|
||||
return frames
|
||||
end
|
||||
|
||||
-- the bicycle doubles walking speed (8 frames per step); movement.speed
|
||||
-- lets a mod multiply or replace that (running shoes, dash, etc.)
|
||||
-- DoBikeSpeedup is skipped mid-hop -- home/overworld.asm:283
|
||||
function Player:stepLength()
|
||||
local Game = require("src.core.Game")
|
||||
local save = Game.save
|
||||
local onBike = (save and save.onBike and not self.ledgeHop) or false
|
||||
local frames = onBike and self.bikeStepFrames or self.stepFrames or STEP_FRAMES
|
||||
if Runtime.wantsHook("movement.speed") then
|
||||
frames = Runtime.call("movement.speed", function(f) return f end, frames, {
|
||||
onBike = onBike,
|
||||
surfing = self.surfing and true or false,
|
||||
player = self,
|
||||
input = Game.input,
|
||||
save = save,
|
||||
})
|
||||
end
|
||||
return math.max(1, math.floor(tonumber(frames) or STEP_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
|
||||
@@ -145,22 +165,7 @@ function Player:tryMove(dir, map, entities)
|
||||
self.moving = true
|
||||
self.bumpFrames = nil -- a real step supersedes any in-place bonk
|
||||
self.progress = 0
|
||||
-- the bicycle doubles walking speed (8 frames per step); movement.speed
|
||||
-- lets a mod multiply or replace that (running shoes, dash, etc.)
|
||||
local Game = require("src.core.Game")
|
||||
local save = Game.save
|
||||
local frames = (save and save.onBike) and self.bikeStepFrames
|
||||
or self.stepFrames or STEP_FRAMES
|
||||
if Runtime.wantsHook("movement.speed") then
|
||||
frames = Runtime.call("movement.speed", function(f) return f end, frames, {
|
||||
onBike = save and save.onBike or false,
|
||||
surfing = self.surfing and true or false,
|
||||
player = self,
|
||||
input = Game.input,
|
||||
save = save,
|
||||
})
|
||||
end
|
||||
self.stepFramesCur = math.max(1, math.floor(tonumber(frames) or STEP_FRAMES))
|
||||
self.stepFramesCur = self:stepLength()
|
||||
return "moved"
|
||||
end
|
||||
|
||||
|
||||
@@ -102,8 +102,8 @@ FieldMoves.TEXT = {
|
||||
.. "\fA #MON may be\nable to pass it."),
|
||||
ASK_WHIRLPOOL = Strings.source("A whirlpool is in\nthe way."
|
||||
.. "\fWant to use\nWHIRLPOOL?"),
|
||||
-- Not a cart line: the stand-in destination prompt World:askFlyPoint uses
|
||||
-- until the POKeGEAR's MAP card grows _FlyMap's cursor mode.
|
||||
-- Not a cart line: the prompt World:askFlyPoint falls back to when there is
|
||||
-- no screen at all to push -- a headless probe, never a real run.
|
||||
ASK_FLY_TO = Strings.source("Fly to %s?"),
|
||||
}
|
||||
|
||||
@@ -183,6 +183,10 @@ function FieldMoves.bindEngineFlags(order)
|
||||
-- The flag IS wPlayerGender's bit 0, so World routes it to the gender byte
|
||||
-- (data/events/engine_flags.asm:131, constants/engine_flags.asm:121).
|
||||
FieldMoves.FEMALE_FLAG = byName[FieldMoves.FEMALE_FLAG_NAME]
|
||||
-- Crystal's ENGINE_MOBILE_SYSTEM (constants/engine_flags.asm:25) has no Gold
|
||||
-- row, so every id from BUG_CONTEST_TIMER up shifts one.
|
||||
FieldMoves.BUG_CONTEST_FLAG = byName["ENGINE_BUG_CONTEST_TIMER"] or 16
|
||||
FieldMoves.BIKE_SHOP_CALL_FLAG = byName["ENGINE_BIKE_SHOP_CALL_ENABLED"] or 19
|
||||
return flags
|
||||
end
|
||||
|
||||
|
||||
+22
-1
@@ -31,6 +31,8 @@ local MOVE = {
|
||||
STANDING_LEFT = 8,
|
||||
STANDING_RIGHT = 9,
|
||||
SPINRANDOM_FAST = 10,
|
||||
-- data/sprites/map_objects.asm:181-187
|
||||
POKEMON = 0x16,
|
||||
SPINCOUNTERCLOCKWISE = 0x1e,
|
||||
SPINCLOCKWISE = 0x1f,
|
||||
-- The three rows whose palette-flags byte is `STRENGTH_BOULDER | BIG_OBJECT`
|
||||
@@ -123,6 +125,11 @@ local SPIN_NEXT = {
|
||||
-- each quarter, no Random anywhere in the loop.
|
||||
local SPIN_TURN_FRAMES = 16
|
||||
|
||||
-- SetFacingBounce steps OBJECT_STEP_FRAME once a frame and reads bit 3, so a
|
||||
-- mon object holds each icon frame for eight -- map_object_action.asm:184-201
|
||||
local BOUNCE_PERIOD = 16
|
||||
local BOUNCE_HALF = 8
|
||||
|
||||
local function rand(a, b)
|
||||
if love and love.math and love.math.random then
|
||||
return love.math.random(a, b)
|
||||
@@ -252,6 +259,8 @@ function NPC.new(mapId, objDef, spriteDef)
|
||||
bigObject = BIG_OBJECT[movement] == true,
|
||||
bigFacing = NPC.bigFacing(movement, spriteDef and spriteDef.id),
|
||||
fixedFacing = FIXED_FACING_MOVE[movement] or nil,
|
||||
bouncing = movement == MOVE.POKEMON or nil,
|
||||
bounceStep = 0,
|
||||
timer = rand(30, 120),
|
||||
sprite = SpriteRenderer.new(spriteDef, string.format("%s_obj_%d", mapId, objDef.index or 0)),
|
||||
-- The sheet is grayscale and carries no alpha; PAL_OW_* crossed with the
|
||||
@@ -501,6 +510,14 @@ function NPC:walkPhase()
|
||||
return (p >= frames / 4 and p < frames * 3 / 4) and 1 or 0
|
||||
end
|
||||
|
||||
-- OBJECT_ACTION_BOUNCE's two columns, SetFacingBounce and
|
||||
-- SetFacingFreezeBounce -- engine/overworld/map_object_action.asm:184-201
|
||||
function NPC:bounceFrame()
|
||||
if not self.bouncing then return nil end
|
||||
if self.frozen then return 0 end
|
||||
return ((self.bounceStep or 0) >= BOUNCE_HALF) and 1 or 0
|
||||
end
|
||||
|
||||
-- Gen 1's seven-value entity pose (src/world/NPC.lua:124), on the class so a
|
||||
-- mod poses the object it is FOLLOWING, not only one it built itself.
|
||||
function NPC:pose()
|
||||
@@ -522,6 +539,9 @@ function NPC:update(map, entities)
|
||||
self.spawnLatched = true
|
||||
self.inGrass = NPC.grassAt(map, self.cellX, self.cellY)
|
||||
end
|
||||
if self.bouncing and not self.frozen then
|
||||
self.bounceStep = ((self.bounceStep or 0) + 1) % BOUNCE_PERIOD
|
||||
end
|
||||
-- The teleport step type owns the object outright (it replaces
|
||||
-- STEP_TYPE_FROM_MOVEMENT until its last beat), so it runs above the frozen
|
||||
-- gate the way the walk interpolation does.
|
||||
@@ -763,7 +783,8 @@ function NPC:draw(ox, oy, scale)
|
||||
else
|
||||
self.sprite:draw(
|
||||
self.px, self.py + yOffset, 0, 0,
|
||||
self.facing, self:walkPhase(), self.stepFlip)
|
||||
self.facing, self:walkPhase(), self.stepFlip,
|
||||
false, false, self:bounceFrame())
|
||||
end
|
||||
G.pop()
|
||||
end
|
||||
|
||||
+84
-12
@@ -23,6 +23,19 @@ local JUMP_Y = {
|
||||
-11, -10, -9, -8, -6, -4, 0, 0,
|
||||
}
|
||||
|
||||
-- FacingFish*'s loose rod OAM, offset from the sprite's top-left, and which
|
||||
-- 8x8 of the sheet's rod row it draws (data/sprites/facings.asm:122-152).
|
||||
local ROD_OAM = {
|
||||
down = { dx = 0, dy = 16, tile = 0 },
|
||||
up = { dx = 0, dy = -8, tile = 0 },
|
||||
left = { dx = -8, dy = 5, tile = 1, flip = true },
|
||||
right = { dx = 16, dy = 5, tile = 1 },
|
||||
}
|
||||
|
||||
-- The sheet row LoadFishingGFX lays over each standing frame's bottom tiles
|
||||
-- (engine/events/fishing_gfx.asm:2-20).
|
||||
local FISH_ROW = { down = 0, up = 1, left = 2, right = 2 }
|
||||
|
||||
function Player.new(cx, cy, facing, spriteDef)
|
||||
local self = setmetatable({
|
||||
cellX = cx, cellY = cy,
|
||||
@@ -138,6 +151,17 @@ function Player:scriptStep(dir)
|
||||
return true
|
||||
end
|
||||
|
||||
-- CounterclockwiseSpinAction's .facings, seeded from the current direction by
|
||||
-- Movement_step_dig -- map_object_action.asm:96-152, movement.asm:113-116
|
||||
local SPIN_FACINGS = { "down", "right", "up", "left" }
|
||||
local SPIN_START = { down = 0, right = 1, up = 2, left = 3 }
|
||||
|
||||
function Player:scriptSpin(frames)
|
||||
if not frames or frames <= 0 then return end
|
||||
self.spinFrames = frames
|
||||
self.spinTimer = (SPIN_START[self.facing] or 0) * 4
|
||||
end
|
||||
|
||||
-- Gen 1's name for the cell being faced (src/world/Player.lua), so a mod that
|
||||
-- wraps World:interact asks one question of either generation.
|
||||
function Player:facingCell()
|
||||
@@ -158,6 +182,11 @@ function Player:update()
|
||||
if self.turnTimer > 0 then
|
||||
self.turnTimer = self.turnTimer - 1
|
||||
end
|
||||
if self.spinFrames then
|
||||
self.spinTimer = (self.spinTimer or 0) + 1
|
||||
self.spinFrames = self.spinFrames - 1
|
||||
if self.spinFrames <= 0 then self.spinFrames = nil end
|
||||
end
|
||||
if not self.moving then
|
||||
-- Re-arm turn-in-place once a poll finds no held direction (caller
|
||||
-- clears this while a dir is held; we only set it from idle).
|
||||
@@ -170,17 +199,27 @@ function Player:update()
|
||||
-- and the facing-delta math walked only half of it, leaving the sprite a
|
||||
-- cell behind where the grid said the player was.
|
||||
local frames = self.stepFrames or STEP_FRAMES
|
||||
local adv = math.floor(self.progress * 16 / frames)
|
||||
local dx = (self.targetX or self.cellX) - self.cellX
|
||||
local dy = (self.targetY or self.cellY) - self.cellY
|
||||
self.px = self.cellX * 16 + dx * adv
|
||||
self.py = self.cellY * 16 + dy * adv
|
||||
-- engine/overworld/map_objects.asm:331 -- AddStepVector moves the object
|
||||
-- every frame, so the span is the whole move, not one cell scaled by dx.
|
||||
local span = math.max(math.abs(dx), math.abs(dy), 1)
|
||||
local adv = math.floor(self.progress * 16 * span / frames)
|
||||
self.px = self.cellX * 16 + (dx / span) * adv
|
||||
self.py = self.cellY * 16 + (dy / span) * adv
|
||||
if self.jumping then
|
||||
-- engine/overworld/map_objects.asm:1815
|
||||
local idx = math.floor((self.progress - 1) / 2) + 1
|
||||
-- engine/overworld/map_objects.asm:1796 -- one table entry per cart frame,
|
||||
-- tweened across our doubled step (#1713)
|
||||
local t = (self.progress - 1) * (#JUMP_Y - 1)
|
||||
/ math.max(frames - 1, 1) + 1
|
||||
local idx = math.floor(t)
|
||||
if idx < 1 then idx = 1 end
|
||||
if idx > #JUMP_Y then idx = #JUMP_Y end
|
||||
self.spriteYOffset = JUMP_Y[idx]
|
||||
if idx >= #JUMP_Y then
|
||||
self.spriteYOffset = JUMP_Y[#JUMP_Y]
|
||||
else
|
||||
self.spriteYOffset = math.floor(
|
||||
JUMP_Y[idx] + (JUMP_Y[idx + 1] - JUMP_Y[idx]) * (t - idx) + 0.5)
|
||||
end
|
||||
end
|
||||
if self.progress >= frames then
|
||||
self.cellX, self.cellY = self.targetX, self.targetY
|
||||
@@ -195,12 +234,36 @@ function Player:update()
|
||||
return false
|
||||
end
|
||||
|
||||
-- FacingFishDown/Up/Left/Right: the standing frame's bottom tile row swapped
|
||||
-- for the fishing sheet, plus the loose rod tile -- facings.asm:122-152 (#1708)
|
||||
function Player:drawFishing(yOffset)
|
||||
local sprite = self.sprite
|
||||
local py = self.py + yOffset
|
||||
local facing = self.facing
|
||||
sprite:draw(self.px, py, 0, 0, facing, 0, false, true)
|
||||
if not self.fishQuads then
|
||||
self.fishQuads = { pose = {}, rod = {} }
|
||||
for i = 0, 2 do
|
||||
self.fishQuads.pose[i] = love.graphics.newQuad(0, i * 8, 16, 8, 16, 32)
|
||||
end
|
||||
for i = 0, 1 do
|
||||
self.fishQuads.rod[i] = love.graphics.newQuad(i * 8, 24, 8, 8, 16, 32)
|
||||
end
|
||||
end
|
||||
local sx, sy = sprite:getScreenOrigin(self.px, py, 0, 0)
|
||||
sprite:drawTile(self.fishSheet, sx,
|
||||
sy + math.max(0, sprite.frameHeight - 8), facing == "right",
|
||||
self.fishQuads.pose[FISH_ROW[facing] or 0])
|
||||
local oam = ROD_OAM[facing] or ROD_OAM.down
|
||||
sprite:drawTile(self.fishSheet, sx + oam.dx, sy + oam.dy, oam.flip,
|
||||
self.fishQuads.rod[oam.tile])
|
||||
end
|
||||
|
||||
function Player:draw(ox, oy, scale)
|
||||
local G = love.graphics
|
||||
-- OBJECT_SPRITE_Y_OFFSET: added to the OBJ's y as it is written to OAM, so
|
||||
-- it moves the sprite without moving the player off the tile they are
|
||||
-- standing on. StepFunction_GotBite's `xor 1` rod bob and the fly take-off
|
||||
-- lift both ride this one byte.
|
||||
-- standing on. StepFunction_GotBite's `xor 1` rod bob rides this one byte.
|
||||
local yOffset = self.spriteYOffset or 0
|
||||
if self.jumping then
|
||||
-- engine/overworld/map_objects.asm:1995
|
||||
@@ -217,9 +280,18 @@ function Player:draw(ox, oy, scale)
|
||||
G.scale(scale, scale)
|
||||
-- Chris is PAL_OW_RED; World:applyPalettes keeps the SpriteRenderer's
|
||||
-- OBJ palette current.
|
||||
self.sprite:draw(
|
||||
self.px, self.py + yOffset, 0, 0,
|
||||
self.facing, self:walkPhase(), self.stepFlip)
|
||||
if self.fishing and self.fishSheet then
|
||||
self:drawFishing(yOffset)
|
||||
else
|
||||
local facing, phase = self.facing, self:walkPhase()
|
||||
-- OBJECT_ACTION_SPIN (map_object_action.asm:96-152), for step_dig.
|
||||
if self.spinFrames then
|
||||
facing = SPIN_FACINGS[math.floor(self.spinTimer / 4) % 4 + 1]
|
||||
phase = 0
|
||||
end
|
||||
self.sprite:draw(
|
||||
self.px, self.py + yOffset, 0, 0, facing, phase, self.stepFlip)
|
||||
end
|
||||
G.pop()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
--
|
||||
-- Everything here is love-free and takes its state as arguments so the whole
|
||||
-- chain is testable without a world.
|
||||
local Bike = require("src.world.gen2.Bike")
|
||||
local FieldMoves = require("src.world.gen2.FieldMoves")
|
||||
local Breeding = require("src.core.gen2.Breeding")
|
||||
local Happiness = require("src.core.gen2.Happiness")
|
||||
local Phone = require("src.core.gen2.Phone")
|
||||
@@ -118,7 +118,7 @@ end
|
||||
local function bikeShopCallEnabled(save)
|
||||
local flags = save.engineFlags
|
||||
if type(flags) == "table" then
|
||||
local set = flags[Bike.ENGINE_BIKE_SHOP_CALL_ENABLED]
|
||||
local set = flags[FieldMoves.BIKE_SHOP_CALL_FLAG]
|
||||
if set ~= nil then return set == true end
|
||||
end
|
||||
return save.bikeShopCall == true
|
||||
@@ -137,7 +137,7 @@ function StepEvents.bikeStep(save, opts)
|
||||
Phone.queueSpecialCall(save, Phone.SPECIALCALL.SPECIALCALL_BIKESHOP)
|
||||
-- `res STATUSFLAGS2_BIKE_SHOP_CALL_F`: one call, ever.
|
||||
if type(save.engineFlags) == "table" then
|
||||
save.engineFlags[Bike.ENGINE_BIKE_SHOP_CALL_ENABLED] = nil
|
||||
save.engineFlags[FieldMoves.BIKE_SHOP_CALL_FLAG] = nil
|
||||
end
|
||||
save.bikeShopCall = false
|
||||
return true
|
||||
|
||||
+290
-65
@@ -56,6 +56,7 @@ local Roamers = require("src.core.gen2.Roamers")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local Screens = require("src.ui.Screens")
|
||||
local Sound = require("src.core.Sound")
|
||||
local SpriteRenderer = require("src.render.SpriteRenderer")
|
||||
local StepEvents = require("src.world.gen2.StepEvents")
|
||||
local Tilt = require("src.render.Tilt")
|
||||
local Strings = require("src.core.Strings")
|
||||
@@ -85,6 +86,7 @@ local SFX = {
|
||||
EXIT_BUILDING = 35,
|
||||
JUMP_OVER_LEDGE = 0x16,
|
||||
BUMP = 0x24,
|
||||
FLY = 0x18,
|
||||
}
|
||||
local EMOTE_SHOCK = 0
|
||||
|
||||
@@ -757,6 +759,15 @@ function World:load()
|
||||
self.tilesets = tilesets
|
||||
self.roofs = self:dataTable("gen2Roofs", "data/generated/roofs.lua")
|
||||
self.sprites = self:dataTable("gen2Sprites", "data/generated/sprites.lua")
|
||||
-- A pre-#1748 cache stamps a SpriteMons row `frames = 1`, leaving
|
||||
-- OBJECT_ACTION_BOUNCE one frame -- engine/overworld/map_object_action.asm:184
|
||||
for _, def in pairs(self.sprites or {}) do
|
||||
if type(def) == "table" and (def.frames or 1) < 2
|
||||
and type(def.source) == "string"
|
||||
and def.source:find("^ROM:SpriteMons") then
|
||||
def.frames = 2
|
||||
end
|
||||
end
|
||||
-- A cache from before the palette stage existed simply has no palettes.lua;
|
||||
-- everything below falls back to the grayscale path rather than failing.
|
||||
self.palettes = self:dataTable("gen2Palettes", "data/generated/palettes.lua")
|
||||
@@ -800,6 +811,14 @@ function World:load()
|
||||
local okImg, img = pcall(Assets.image, emotes.grassRustle)
|
||||
if okImg then self.grassRustleImage = img end
|
||||
end
|
||||
-- LoadFishingGFX's two sheets
|
||||
-- (../pokecrystal/engine/events/fishing_gfx.asm:7-12)
|
||||
if emotes.fishing and pcall(Assets.image, emotes.fishing) then
|
||||
self.fishingSheet = emotes.fishing
|
||||
end
|
||||
if emotes.fishingFemale and pcall(Assets.image, emotes.fishingFemale) then
|
||||
self.fishingSheetFemale = emotes.fishingFemale
|
||||
end
|
||||
end
|
||||
-- The heal machine's two OBJ tiles and their CGB palette, for the
|
||||
-- Pokecenter light show (World:startHealMachineAnim). A cache from before
|
||||
@@ -1427,6 +1446,9 @@ function World:busy()
|
||||
-- and the waterfall climb are all applymovement / pause commands inside a
|
||||
-- queued script, so nothing else may run under them.
|
||||
or self.fieldMove ~= nil
|
||||
-- FlyFromAnim and FlyToAnim are blocking `callasm`s inside .FlyScript
|
||||
-- (engine/events/overworld.asm:599, :605).
|
||||
or self.flyAnim ~= nil
|
||||
end
|
||||
|
||||
-- CheckMenuOW (engine/overworld/events.asm:802) is the tail of OWPlayerInput,
|
||||
@@ -1434,12 +1456,15 @@ end
|
||||
-- away while wScriptRunning is non-zero (events.asm:238-243). Two more gates
|
||||
-- sit above it even with no script up: PlayerMovement answering
|
||||
-- PLAYERMOVEMENT_CONTINUE, i.e. the player is mid-step (events.asm:474-477),
|
||||
-- and CheckStandingOnIce carrying (events.asm:479-480). START and SELECT are
|
||||
-- read NOWHERE else in the overworld, so a press that arrives while any of
|
||||
-- those hold is not queued or deferred, it is never read at all.
|
||||
-- and CheckStandingOnIce carrying (events.asm:479-480). The frame a step is
|
||||
-- QUEUED answers PLAYERMOVEMENT_FINISH instead (player_movement.asm:455-461),
|
||||
-- which is zero, so the poll still runs there -- and on the Cycling Road's
|
||||
-- forced roll that landing frame is the only one there is (#1718).
|
||||
function World:acceptsMenuInput()
|
||||
if self.battleActive or self:busy() then return false end
|
||||
if self.player and self.player.moving then return false end
|
||||
if self.player and self.player.moving and not self.stepFinished then
|
||||
return false
|
||||
end
|
||||
-- The same latch pair World:step's slide uses: a latched direction on an ice
|
||||
-- tile is CheckStandingOnIce's carry.
|
||||
if self.turningDirection and Permissions.isIce(self:playerCollision()) then
|
||||
@@ -1695,7 +1720,7 @@ function World:engineFlag(flag)
|
||||
-- how the two would come apart -- the officer's `setflag` and the results
|
||||
-- script's `clearflag` are the only writers, and both go through the pair
|
||||
-- below.
|
||||
if flag == BugContest.ENGINE_BUG_CONTEST_TIMER and save then
|
||||
if flag == FieldMoves.BUG_CONTEST_FLAG and save then
|
||||
return BugContest.isActive(save)
|
||||
end
|
||||
-- Badges live in save.player.badges, not in the flag table: on the cart the
|
||||
@@ -1736,7 +1761,7 @@ end
|
||||
function World:setEngineFlag(flag, value)
|
||||
if flag == nil then return end
|
||||
local save = self.game and self.game.save
|
||||
if flag == BugContest.ENGINE_BUG_CONTEST_TIMER and save then
|
||||
if flag == FieldMoves.BUG_CONTEST_FLAG and save then
|
||||
-- Route35NationalParkGate_OkayToProceed sets the flag BEFORE `special
|
||||
-- GiveParkBalls`, so starting here and starting again there is the cart's
|
||||
-- own order and the second start is what puts the balls on the counter.
|
||||
@@ -2042,7 +2067,7 @@ function World:breedmonSpriteDef(species)
|
||||
local def = {
|
||||
id = "SPRITE_DAY_CARE_MON",
|
||||
image = entry.image,
|
||||
frames = 1,
|
||||
frames = 2,
|
||||
walker = false,
|
||||
spriteType = "POKEMON_SPRITE",
|
||||
palette = "PAL_OW_RED",
|
||||
@@ -2311,6 +2336,9 @@ end
|
||||
-- rang the ordinary item jingle while the item argument was thrown away. An
|
||||
-- item the cache cannot name takes the `cp TM_HM / jr z` fall-through, SFX.ITEM.
|
||||
function World:specialSound(itemIndex)
|
||||
-- The `waitsfx` above it (scripting.asm:445): SFX_READ_TEXT_2 ($08), which
|
||||
-- the box rings on its own press, outranks SFX_GET_TM ($9b) here (#1483).
|
||||
Sound.waitSfxDone()
|
||||
local id = itemIndex and self:itemIdByIndex(itemIndex)
|
||||
local items = self.game and self.game.data and self.game.data.items
|
||||
local def = id and items and items[id]
|
||||
@@ -3482,14 +3510,6 @@ function World:updateMapSetup()
|
||||
if ms.phase == "out" then
|
||||
ms.step = ms.step + 1
|
||||
self.fade, self.fadeLevel = "white", ms.step / FADE_STEPS
|
||||
-- FlyFromAnim carries the player up and off the map under the fade. The
|
||||
-- bird's own frames are not in the cache, but the lift is: it is the same
|
||||
-- OBJECT_SPRITE_Y_OFFSET sine the teleport step type walks
|
||||
-- (src/script/gen2/Movement.lua), stepped over the fade's four levels.
|
||||
if ms.lift and self.player then
|
||||
self.player.spriteYOffset = Movement.teleportYOffset(
|
||||
Movement.TELEPORT_RISE_HEIGHT + ms.step * FADE_STEPS)
|
||||
end
|
||||
if ms.step >= FADE_STEPS then
|
||||
-- setMap clears self.fade (a map load repaints everything), so the sheet
|
||||
-- has to be re-armed at full strength on the far side for the fade in to
|
||||
@@ -3501,16 +3521,12 @@ function World:updateMapSetup()
|
||||
return
|
||||
end
|
||||
ms.step = ms.step - 1
|
||||
-- FlyToAnim, the same curve read backwards: the player comes down onto the
|
||||
-- destination tile as the fade lets go of the screen.
|
||||
if ms.lift and self.player then
|
||||
self.player.spriteYOffset = Movement.teleportYOffset(
|
||||
Movement.TELEPORT_RISE_HEIGHT + ms.step * FADE_STEPS)
|
||||
end
|
||||
if ms.step <= 0 then
|
||||
self.fade, self.fadeLevel = nil, nil
|
||||
self.mapSetup = nil
|
||||
if ms.lift and self.player then self.player.spriteYOffset = 0 end
|
||||
-- `callasm FlyToAnim` is the command straight after `newloadmap
|
||||
-- MAPSETUP_TELEPORT` (engine/events/overworld.asm:604-605).
|
||||
if ms.flyIn then self:startFlyAnim("to", ms.flyIn) end
|
||||
return
|
||||
end
|
||||
self.fadeLevel = ms.step / FADE_STEPS
|
||||
@@ -3865,6 +3881,15 @@ function World:updateMovement()
|
||||
while st.i <= #st.bytes do
|
||||
local b = st.bytes[st.i]
|
||||
st.i = st.i + 1
|
||||
-- Movement_step_dig spins for the frames in the byte that follows
|
||||
-- -- engine/overworld/movement.asm:113-131 (#1716)
|
||||
if b == Movement.STEP_DIG then
|
||||
local duration = st.bytes[st.i] or 0
|
||||
st.i = st.i + 1
|
||||
if ent.scriptSpin then ent:scriptSpin(duration) end
|
||||
st.sleep = duration
|
||||
return
|
||||
end
|
||||
-- engine/overworld/movement.asm:163
|
||||
if b == 0x57 then
|
||||
local duration = st.bytes[st.i] or 0
|
||||
@@ -3890,6 +3915,11 @@ function World:updateMovement()
|
||||
elseif act.kind == "step" then
|
||||
local fromX, fromY = ent.cellX, ent.cellY
|
||||
if ent:scriptStep(act.dir) then
|
||||
-- TurningStep's OBJECT_ACTION_SPIN, for the length of the step
|
||||
-- (engine/overworld/movement.asm:693-699).
|
||||
if act.spin and ent.scriptSpin then
|
||||
ent:scriptSpin(ent.stepFrames or 16)
|
||||
end
|
||||
self:followStep(ent, fromX, fromY)
|
||||
end
|
||||
return
|
||||
@@ -4797,15 +4827,35 @@ end
|
||||
-- src/world/gen2/Bike.lua owns every decision; this is the world state those
|
||||
-- decisions read and the presentation they end in.
|
||||
|
||||
-- ../pokecrystal/constants/engine_flags.asm:25 ENGINE_MOBILE_SYSTEM shifts every
|
||||
-- later id up one: :36 ALWAYS_ON_BIKE against ../pokegold's :35.
|
||||
function World:engineFlagId(name, goldId)
|
||||
local order = self.constants and self.constants.engineFlagOrder
|
||||
if type(order) ~= "table" then return goldId end
|
||||
local ids = self.engineFlagIds
|
||||
if not ids then
|
||||
ids = {}
|
||||
for index, entry in pairs(order) do
|
||||
if type(index) == "number" and type(entry) == "string" then
|
||||
ids[entry] = index - 1
|
||||
end
|
||||
end
|
||||
self.engineFlagIds = ids
|
||||
end
|
||||
return ids[name] or goldId
|
||||
end
|
||||
|
||||
-- wBikeFlags' three bits are ENGINE_* ids like any other flag, so the map
|
||||
-- callbacks that set them (Route16AlwaysOnBikeCallback,
|
||||
-- Route17AlwaysOnBikeCallback) already land on save.engineFlags.
|
||||
function World:alwaysOnBike()
|
||||
return self:engineFlag(Bike.ENGINE_ALWAYS_ON_BIKE)
|
||||
return self:engineFlag(World.engineFlagId(
|
||||
self, "ENGINE_ALWAYS_ON_BIKE", Bike.ENGINE_ALWAYS_ON_BIKE))
|
||||
end
|
||||
|
||||
function World:downhill()
|
||||
return self:engineFlag(Bike.ENGINE_DOWNHILL)
|
||||
return self:engineFlag(World.engineFlagId(
|
||||
self, "ENGINE_DOWNHILL", Bike.ENGINE_DOWNHILL))
|
||||
end
|
||||
|
||||
-- GetPlayerTilePermission's operand: the collision under the player's feet.
|
||||
@@ -4963,6 +5013,11 @@ function World:beginFishing(outcome, wild)
|
||||
if self.player then
|
||||
self.player.fishing = true
|
||||
self.player.fishingState = self.fishing
|
||||
-- LoadFishingGFX reads wPlayerGender
|
||||
-- (../pokecrystal/engine/events/fishing_gfx.asm:8-12)
|
||||
self.player.fishSheet =
|
||||
(FieldMoves.isFemale(self:playerGender()) and self.fishingSheetFemale)
|
||||
or self.fishingSheet
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5345,6 +5400,9 @@ function World:fieldContext(mon)
|
||||
-- digFromMenu reads this rather than re-deriving the banked warp.
|
||||
canEscapeRope = self:escapeRopeTarget() ~= nil,
|
||||
playerState = self.playerState,
|
||||
-- SurfFunction.TrySurf and TrySurfOW both refuse while wBikeFlags'
|
||||
-- ALWAYS_ON_BIKE is set (engine/events/overworld.asm:343-345, :498-500).
|
||||
alwaysOnBike = self:alwaysOnBike(),
|
||||
strengthActive = self.strengthActive,
|
||||
-- FlashFunction tests wTimeOfDayPalset, not the map header, so a
|
||||
-- PALETTE_DARK map that FLASH has already lit refuses a second FLASH.
|
||||
@@ -5524,15 +5582,34 @@ function World:runCut(result)
|
||||
end
|
||||
|
||||
-- Script_UsedWhirlpool, which is Script_Cut with DisappearWhirlpool and
|
||||
-- PlayWhirlpoolSound (a bare SFX.SURF) in place of the snip.
|
||||
-- PlayWhirlpoolSound in place of the snip.
|
||||
function World:runWhirlpool(result)
|
||||
self:setNickname(result.mon)
|
||||
self:showText(Strings(result.text), function()
|
||||
self:replaceBlock(result.blockIndex, result.replacement)
|
||||
self:playSfx(SFX.SURF)
|
||||
self:playWhirlpoolSound()
|
||||
end)
|
||||
end
|
||||
|
||||
-- PlayWhirlpoolSound is WaitSFX, SFX_SURF, WaitSFX, never a bare PlaySFX
|
||||
-- -- engine/events/field_moves.asm:5-10 (#1717)
|
||||
function World:playWhirlpoolSound()
|
||||
self.fieldMove = { phase = "whirlpoolsfx", waiting = true, left = 180 }
|
||||
end
|
||||
|
||||
-- PlayerMovementPointers' .force_turn arm and the Script_ForcedMovement it
|
||||
-- calls -- events.asm:786-793, forced_movement.asm:1-51 (#1716)
|
||||
local FORCED_BACK = { up = "down", down = "up", left = "right", right = "left" }
|
||||
|
||||
function World:runForcedMovement()
|
||||
local p = self.player
|
||||
if not p or p.moving or self.moveState then return false end
|
||||
local back = FORCED_BACK[p.facing]
|
||||
if not back then return false end
|
||||
self:beginMovement(0, Movement.forcedMovementBytes(back))
|
||||
return true
|
||||
end
|
||||
|
||||
-- Script_UseFlash: the text plays SFX.FLASH from inside itself
|
||||
-- (UseFlashTextScript's text_asm), and BlindingFlash then sets
|
||||
-- STATUSFLAGS_FLASH_F and reloads the palettes. Setting the flag is all there
|
||||
@@ -5685,7 +5762,7 @@ function World:runFieldMove(result)
|
||||
elseif action == "waterfall" then
|
||||
self:runWaterfall(result)
|
||||
elseif action == "fly" then
|
||||
self:openFlyMap()
|
||||
self:openFlyMap(result.mon)
|
||||
elseif action == "headbutt" then
|
||||
self:runHeadbutt(result.facingX, result.facingY, result.mon)
|
||||
elseif action == "sweetscent" then
|
||||
@@ -5869,25 +5946,138 @@ function World:flyPoints()
|
||||
self.game and self.game.save, self.landmarks, self:region())
|
||||
end
|
||||
|
||||
-- .FlyScript: WarpToSpawnPoint, then `newloadmap MAPSETUP.TELEPORT` brings the
|
||||
-- map up with the player back in PLAYER_NORMAL. MapSetupScript_Teleport opens
|
||||
-- on FadeOutToWhite and falls through into _Warp, so flying is bracketed by the
|
||||
-- same pair of fades a door is, which is where the two fly animations ride:
|
||||
-- `lift` below hands them to World:updateMapSetup.
|
||||
function World:flyTo(spawnId)
|
||||
-- FlyFromAnim / FlyToAnim (engine/events/field_moves.asm:300, :334) and the two
|
||||
-- curves they run (engine/sprite_anims/functions.asm:1350, :1418).
|
||||
local FLY = {
|
||||
FROM_FRAMES = 128, TO_FRAMES = 64, HOVER = 0x40,
|
||||
AMP_MAX = 0x40, TO_AMP = 11 * 8, RISE = 84,
|
||||
}
|
||||
|
||||
-- FlyFunction_InitGFX's GetSpeciesIcon (engine/events/field_moves.asm:390):
|
||||
-- the icon of the mon in wCurPartyMon, on PAL_OW_RED like every other OW OBJ.
|
||||
function World:flyIconFor(mon)
|
||||
if type(mon) ~= "table" then return nil end
|
||||
local data = self.game and self.game.data
|
||||
local icons = data and data.gen2Icons
|
||||
local iconId = mon.isEgg and "ICON_EGG"
|
||||
or (icons and icons.species and mon.species
|
||||
and icons.species[mon.species])
|
||||
local entry = iconId and icons and icons.icons and icons.icons[iconId]
|
||||
if not (entry and entry.image) then return nil end
|
||||
local def = {
|
||||
id = "SPRITE_FLY_MON", image = entry.image, frames = 2, walker = false,
|
||||
spriteType = "POKEMON_SPRITE", palette = "PAL_OW_RED", paletteId = 0,
|
||||
species = mon.species, icon = iconId,
|
||||
}
|
||||
local ok, icon = pcall(SpriteRenderer.new, def, "gen2fly")
|
||||
if not (ok and icon) then return nil end
|
||||
local daytime = self.daytime or Palettes.daytimeFor(
|
||||
self.map and self.map.def, self:hour(), self.flashUsed)
|
||||
local colors = Palettes.spritePalette(self.palettes, daytime, def)
|
||||
if colors then
|
||||
icon:setObjPalette(colors, ("gen2:%s:0"):format(tostring(daytime)))
|
||||
end
|
||||
return icon
|
||||
end
|
||||
|
||||
-- False when there is no icon sheet (or no love at all): the caller then flies
|
||||
-- the way it always did rather than parking the world on an animation.
|
||||
function World:startFlyAnim(phase, mon, onDone)
|
||||
local icon = self:flyIconFor(mon)
|
||||
local p = self.player
|
||||
if not (icon and p) then return false end
|
||||
local landing = phase == "to"
|
||||
self.flyAnim = {
|
||||
phase = phase, icon = icon, onDone = onDone, t = 0,
|
||||
px = p.px, py = p.py, xoff = 0, wave = 0,
|
||||
left = landing and FLY.TO_FRAMES or FLY.FROM_FRAMES,
|
||||
hover = landing and 0 or FLY.HOVER,
|
||||
amp = landing and FLY.TO_AMP or 0,
|
||||
y = landing and -FLY.RISE or 0,
|
||||
}
|
||||
return true
|
||||
end
|
||||
|
||||
-- FlyFunction_FrameTimer (engine/events/field_moves.asm:409) over the two
|
||||
-- AnimSeq_Fly* curves; the wobble is Sprites_Cosine's d * cos(n * pi / 32).
|
||||
function World:stepFlyAnim()
|
||||
local fa = self.flyAnim
|
||||
if not fa then return end
|
||||
local left = fa.left
|
||||
if left <= 0 then
|
||||
local done = fa.onDone
|
||||
self.flyAnim = nil
|
||||
if done then done() end
|
||||
return
|
||||
end
|
||||
fa.left = left - 1
|
||||
if left >= 0x40 and left % 8 == 0 then
|
||||
self:playSfxNamed("Sfx_Fly", SFX.FLY)
|
||||
end
|
||||
fa.t = fa.t + 1
|
||||
local amp = fa.amp
|
||||
if fa.phase == "to" then
|
||||
if fa.y >= 0 then return end
|
||||
fa.y = fa.y + 2
|
||||
if amp > 0 then fa.amp = amp - 2 end
|
||||
else
|
||||
if fa.hover > 0 then
|
||||
fa.hover = fa.hover - 1
|
||||
return
|
||||
end
|
||||
if fa.y <= -FLY.RISE then return end
|
||||
fa.y = fa.y - 2
|
||||
if amp < FLY.AMP_MAX then fa.amp = amp + 8 end
|
||||
end
|
||||
fa.xoff = math.floor(amp * math.cos((fa.wave % 64) * math.pi / 32))
|
||||
fa.wave = fa.wave + 1
|
||||
end
|
||||
|
||||
-- .Frameset_RedWalk is two 8-frame icon beats, the fourth mirrored
|
||||
-- -- data/sprite_anims/framesets.asm:81-86
|
||||
function World:drawFlyAnim(s, billboard)
|
||||
local fa = self.flyAnim
|
||||
if not (fa and fa.icon) then return end
|
||||
local G = love.graphics
|
||||
local cam = self.camera
|
||||
local px = fa.px + fa.xoff
|
||||
local py = fa.py + fa.y
|
||||
local ox = math.floor((0 - cam.x) * s)
|
||||
local oy = math.floor((0 - cam.y) * s)
|
||||
local beat = math.floor(fa.t / 8) % 4
|
||||
local function body()
|
||||
G.setColor(1, 1, 1, 1)
|
||||
G.push()
|
||||
G.translate(ox, oy)
|
||||
G.scale(s, s)
|
||||
fa.icon:draw(px, py, 0, 0, "down", 0, false, false, beat == 3, beat % 2)
|
||||
G.pop()
|
||||
end
|
||||
if billboard then
|
||||
billboard(ox + (px + 8) * s, oy + (py + 16) * s, body)
|
||||
else
|
||||
body()
|
||||
end
|
||||
end
|
||||
|
||||
-- .FlyScript: FlyFromAnim, WarpToSpawnPoint, `newloadmap MAPSETUP_TELEPORT`,
|
||||
-- then FlyToAnim -- engine/events/overworld.asm:595-609
|
||||
function World:flyTo(spawnId, mon)
|
||||
local spawn = self.landmarks and self.landmarks.spawns
|
||||
and self.landmarks.spawns[spawnId]
|
||||
if not (spawn and spawn.map and self.maps and self.maps[spawn.map]) then
|
||||
return false
|
||||
end
|
||||
self:applyPlayerState(FieldMoves.PLAYER_NORMAL)
|
||||
local ok = self:runMapSetup(MAPSETUP.TELEPORT, function()
|
||||
return self:setMap(spawn.map, spawn.x, spawn.y, "down")
|
||||
end)
|
||||
-- FlyFromAnim / FlyToAnim ride the setup script's own two fades: the take-off
|
||||
-- lift under the fade out, the landing under the fade in.
|
||||
if self.mapSetup then self.mapSetup.lift = true end
|
||||
return ok
|
||||
local function warp()
|
||||
self:applyPlayerState(FieldMoves.PLAYER_NORMAL)
|
||||
local ok = self:runMapSetup(MAPSETUP.TELEPORT, function()
|
||||
return self:setMap(spawn.map, spawn.x, spawn.y, "down")
|
||||
end)
|
||||
if self.mapSetup then self.mapSetup.flyIn = mon end
|
||||
return ok
|
||||
end
|
||||
if self:startFlyAnim("from", mon, warp) then return true end
|
||||
return warp()
|
||||
end
|
||||
|
||||
-- _FlyMap: the town map with the cursor locked to visited flypoints, A takes
|
||||
@@ -5898,7 +6088,7 @@ end
|
||||
-- "Where?" plate over it instead of the card strip. A run with no love at all
|
||||
-- (a headless probe) has no screen to push, so the destinations are offered
|
||||
-- one at a time through the same yesorno box every other field move uses.
|
||||
function World:openFlyMap()
|
||||
function World:openFlyMap(mon)
|
||||
local points = self:flyPoints()
|
||||
if #points == 0 then return false end
|
||||
-- Loaded on demand and through pcall: a headless run has no love, and this
|
||||
@@ -5909,28 +6099,31 @@ function World:openFlyMap()
|
||||
save = self.game.save,
|
||||
currentLandmark = self:currentLandmarkId(),
|
||||
fly = points,
|
||||
-- TownMapMon draws wCurPartyMon's icon as the cursor
|
||||
-- (../pokecrystal/engine/pokegear/pokegear.asm:2708-2721).
|
||||
flyMon = mon,
|
||||
onFly = function(spawnId)
|
||||
self.game.stack:pop()
|
||||
self:flyTo(spawnId)
|
||||
self:flyTo(spawnId, mon)
|
||||
end,
|
||||
onClose = function() self.game.stack:pop() end,
|
||||
})
|
||||
return true
|
||||
end
|
||||
self:askFlyPoint(points, 1)
|
||||
self:askFlyPoint(points, 1, mon)
|
||||
return true
|
||||
end
|
||||
|
||||
function World:askFlyPoint(points, index)
|
||||
function World:askFlyPoint(points, index, mon)
|
||||
local row = points[index]
|
||||
if not row then return end
|
||||
local name = (row.name or row.landmark):gsub("\n", " ")
|
||||
self:showText(Strings(FieldMoves.TEXT.ASK_FLY_TO, name), function()
|
||||
self:askYesNo(function(yes)
|
||||
if yes then
|
||||
self:flyTo(row.spawn)
|
||||
self:flyTo(row.spawn, mon)
|
||||
else
|
||||
self:askFlyPoint(points, index + 1)
|
||||
self:askFlyPoint(points, index + 1, mon)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
@@ -5958,6 +6151,18 @@ function World:updateFieldMove()
|
||||
st.timer = st.timer - 1
|
||||
return
|
||||
end
|
||||
if st.phase == "whirlpoolsfx" then
|
||||
st.left = (st.left or 0) - 1
|
||||
if st.waiting then
|
||||
if Sound.sfxBusy() and st.left > 0 then return end
|
||||
st.waiting = nil
|
||||
self:playSfxNamed("Sfx_Surf", SFX.SURF)
|
||||
return
|
||||
end
|
||||
if Sound.sfxBusy() and st.left > 0 then return end
|
||||
self.fieldMove = nil
|
||||
return
|
||||
end
|
||||
if st.phase == "strength" then
|
||||
self.fieldMove = nil
|
||||
if st.text then self:showText(Strings(st.text)) end
|
||||
@@ -6056,6 +6261,9 @@ function World:startBattle(opts, onDone)
|
||||
-- wInBattleTowerBattle (../pokecrystal/engine/events/battle_tower/
|
||||
-- battle_tower.asm:220-223), which turns DoBadgeTypeBoosts off.
|
||||
battleTower = opts.battleTower,
|
||||
-- wTimeOfDay, for BattleCommand_TimeBasedHealContinue
|
||||
-- (engine/battle/effect_commands.asm:6401-6404).
|
||||
timeOfDay = self:timeOfDayId(),
|
||||
})
|
||||
self:playBattleMusic(opts)
|
||||
local function pushBattle()
|
||||
@@ -8639,8 +8847,10 @@ function World:setMap(mapId, cx, cy, facing, opts)
|
||||
-- MAPCALLBACK_NEWMAP runs, because the Cycling Road's callback is what sets
|
||||
-- them straight back again: leaving them set is how one visit to Route 17
|
||||
-- would keep the player glued to the bike for the rest of the game.
|
||||
self:setEngineFlag(Bike.ENGINE_ALWAYS_ON_BIKE, false)
|
||||
self:setEngineFlag(Bike.ENGINE_DOWNHILL, false)
|
||||
self:setEngineFlag(World.engineFlagId(
|
||||
self, "ENGINE_ALWAYS_ON_BIKE", Bike.ENGINE_ALWAYS_ON_BIKE), false)
|
||||
self:setEngineFlag(World.engineFlagId(
|
||||
self, "ENGINE_DOWNHILL", Bike.ENGINE_DOWNHILL), false)
|
||||
-- "Respawn in Pokemon Centers" (home/map.asm, LoadMapAttributes' .SetSpawn):
|
||||
-- walking from an OUTDOOR map into an INDOOR one whose tileset is
|
||||
-- TILESET_POKECENTER rewrites wLastSpawnMapGroup / wLastSpawnMapNumber, and
|
||||
@@ -9741,6 +9951,7 @@ end
|
||||
|
||||
function World:stepBody()
|
||||
if not self.map or not self.player then return end
|
||||
self.stepFinished = false
|
||||
self:pollTimeOfDay()
|
||||
-- ShakeScreen and the `musicfadeout` tail both run UNDER a script (the VM is
|
||||
-- parked on the earthquake's own waitFrames while the screen is still
|
||||
@@ -9821,6 +10032,7 @@ function World:stepBody()
|
||||
-- it ticks here above the input gate the same way the emote does; its
|
||||
-- last flash's onDone is what resumes the nurse.
|
||||
if self.healAnim then self:stepHealAnim() end
|
||||
if self.flyAnim then self:stepFlyAnim() end
|
||||
|
||||
-- HandleCmdQueue sits in the overworld loop, once a frame, above the input
|
||||
-- gate: it is what drops a boulder that is already sitting on a hole, and it
|
||||
@@ -9841,8 +10053,9 @@ function World:stepBody()
|
||||
|
||||
-- Freeze player input while a script / textbox / cutscene move is up.
|
||||
if self:busy() then
|
||||
-- Keep scripted entities animating mid-step.
|
||||
if self.player and self.player.moving then
|
||||
-- Keep scripted entities animating mid-step. A step_dig spin has the
|
||||
-- player standing still, so it has to tick here too.
|
||||
if self.player and (self.player.moving or self.player.spinFrames) then
|
||||
self:playerStepGrass()
|
||||
if self.player:update() then
|
||||
self.player.inGrass =
|
||||
@@ -9856,6 +10069,7 @@ function World:stepBody()
|
||||
local p = self.player
|
||||
self:playerStepGrass()
|
||||
local landed = p:update()
|
||||
self.stepFinished = landed
|
||||
-- CopyCoordsTileToLastCoordsTile -> SetTallGrassFlags, which is what a step
|
||||
-- ENDS on (engine/overworld/map_objects.asm:196-208, :247).
|
||||
if landed then p.inGrass = self:grassAt(p.cellX, p.cellY) end
|
||||
@@ -9917,6 +10131,11 @@ function World:stepBody()
|
||||
local dir = self.heldDir
|
||||
if not p.moving then
|
||||
local coll = self:playerCollision()
|
||||
-- .CheckTile tests CheckWhirlpoolTile above the nybble ladder
|
||||
-- -- engine/overworld/player_movement.asm:117-123 (#1716)
|
||||
if Permissions.isWhirlpool(coll) and self:runForcedMovement() then
|
||||
return
|
||||
end
|
||||
local current = Permissions.currentDirection(coll)
|
||||
or Permissions.doorForcedDirection(coll)
|
||||
if current then
|
||||
@@ -10030,18 +10249,21 @@ function World:drawPeople(s, billboard)
|
||||
local G = love.graphics
|
||||
local p = self.player
|
||||
local cam = self.camera
|
||||
local drawList = {
|
||||
{ kind = "player", py = p.py, ox = 0, oy = 0 },
|
||||
}
|
||||
for _, npc in ipairs(self.npcs) do
|
||||
drawList[#drawList + 1] = {
|
||||
kind = "npc", npc = npc, ox = 0, oy = 0, py = npc.py,
|
||||
}
|
||||
end
|
||||
for _, g in ipairs(self.ghosts) do
|
||||
drawList[#drawList + 1] = {
|
||||
kind = "npc", npc = g.npc, ox = g.ox, oy = g.oy, py = g.oy + g.npc.py,
|
||||
}
|
||||
-- .FlyScript hides the map's objects from HideSprites until its
|
||||
-- LoadWalkingSpritesGFX tail -- engine/events/overworld.asm:597, :608
|
||||
local drawList = {}
|
||||
if not self.flyAnim then
|
||||
drawList[1] = { kind = "player", py = p.py, ox = 0, oy = 0 }
|
||||
for _, npc in ipairs(self.npcs) do
|
||||
drawList[#drawList + 1] = {
|
||||
kind = "npc", npc = npc, ox = 0, oy = 0, py = npc.py,
|
||||
}
|
||||
end
|
||||
for _, g in ipairs(self.ghosts) do
|
||||
drawList[#drawList + 1] = {
|
||||
kind = "npc", npc = g.npc, ox = g.ox, oy = g.oy, py = g.oy + g.npc.py,
|
||||
}
|
||||
end
|
||||
end
|
||||
table.sort(drawList, function(a, b) return a.py < b.py end)
|
||||
|
||||
@@ -10076,6 +10298,7 @@ function World:drawPeople(s, billboard)
|
||||
|
||||
self:drawEmote(s, billboard)
|
||||
self:drawHealAnim(s, billboard)
|
||||
self:drawFlyAnim(s, billboard)
|
||||
end
|
||||
|
||||
-- Split out of drawPeople so World:drawPipeline composites the one copy the
|
||||
@@ -10143,11 +10366,12 @@ function World:drawPipeline(id, w, h, s)
|
||||
-- the art: nil, like Gen 1 returns in its true-colour modes.
|
||||
paletteFor = function() return nil end,
|
||||
spriteColors = function() return nil end,
|
||||
-- Gold's only standing effects; it has no dust/cutTree/bird/rod overlay,
|
||||
-- and Gen 1's `at` skips a nil body, so those keys are simply absent.
|
||||
-- Gold's only standing effects; it has no dust/cutTree/rod overlay, and
|
||||
-- Gen 1's `at` skips a nil body, so those keys are simply absent.
|
||||
fx = {
|
||||
emote = function() self:drawEmote(1, nil) end,
|
||||
heal = function() self:drawHealAnim(1, nil) end,
|
||||
bird = function() self:drawFlyAnim(1, nil) end,
|
||||
},
|
||||
}
|
||||
-- `project(wx, wy)` -> canvas pixels, nil behind the camera. s = 1 lays the
|
||||
@@ -10165,6 +10389,7 @@ function World:drawPipeline(id, w, h, s)
|
||||
end
|
||||
self:drawEmote(1, at)
|
||||
self:drawHealAnim(1, at)
|
||||
self:drawFlyAnim(1, at)
|
||||
end
|
||||
local override = Pipelines.drawWorld(id, ctx)
|
||||
-- world post-processes fold in here, so they never touch the text box on top
|
||||
|
||||
Reference in New Issue
Block a user