mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-16 00:02:23 +02:00
362 lines
14 KiB
Lua
362 lines
14 KiB
Lua
-- The Magnet Train ride (pokegold engine/events/magnet_train.asm).
|
|
--
|
|
-- `special MagnetTrain` is a self-contained cutscene: it takes the whole frame
|
|
-- loop away from the overworld, redraws the background out of the train
|
|
-- station tileset, and runs a seven-entry jumptable until it sets
|
|
-- JUMPTABLE_EXIT. Everything below is that routine with no love calls in it,
|
|
-- so the timing, the scroll and the frameset can be asserted headless; the
|
|
-- screen that draws it is src/ui/gen2/MagnetTrainRide.lua.
|
|
--
|
|
-- The illusion is one 32x18 background and three horizontal SCX bands:
|
|
--
|
|
-- scanlines 0-46 wMagnetTrainOffset * 2 bushes, always moving
|
|
-- scanlines 47-94 wMagnetTrainPosition the train body
|
|
-- scanlines 95-143 wMagnetTrainOffset * 2 bushes again
|
|
--
|
|
-- MagnetTrain_UpdateLYOverrides writes those three runs into
|
|
-- wLYOverridesBackup every frame and then advances the offset, so the scenery
|
|
-- never stops even while the jumptable is parked on a .WaitScene. The train
|
|
-- band is what the jumptable actually moves, and the player sprite rides it
|
|
-- through wGlobalAnimXOffset, which is why the two stay locked together.
|
|
--
|
|
-- Everything here is 8-bit and wraps, exactly as the ASM's `add` does: the
|
|
-- forward trip runs wMagnetTrainPosition from 96 down past 0 to -96, and it is
|
|
-- the byte wrap that keeps SCX legal on the way.
|
|
|
|
local MagnetTrain = {}
|
|
MagnetTrain.__index = MagnetTrain
|
|
|
|
-- constants/gfx_constants.asm
|
|
local TILE_WIDTH = 8
|
|
local SCREEN_WIDTH, SCREEN_HEIGHT = 20, 18
|
|
local TILEMAP_WIDTH = 32
|
|
local SCREEN_HEIGHT_PX = 144
|
|
|
|
-- PAL_BG_* (constants/gfx_constants.asm) as 1-based palette slots, the way
|
|
-- src/world/gen2/Palettes.lua indexes a bgSet.
|
|
MagnetTrain.PAL_BG_GRAY = 1
|
|
MagnetTrain.PAL_BG_GREEN = 3
|
|
MagnetTrain.PAL_BG_YELLOW = 5
|
|
|
|
-- SetMagnetTrainPals paints the attribute map in four ByteFills: four rows of
|
|
-- green, ten of gray, four more of green, and then six tiles of yellow at
|
|
-- (7, 8) for the window the player is framed in.
|
|
local BUSH_ROWS_TOP = 4 -- hlbgcoord 0, 0 / bc = 4 * TILEMAP_WIDTH
|
|
local TRAIN_ROWS = 10 -- hlbgcoord 0, 4 / bc = 10 * TILEMAP_WIDTH
|
|
local WINDOW_ROW = 8 -- hlbgcoord 7, 8 / bc = 6
|
|
local WINDOW_COL, WINDOW_WIDTH = 7, 6
|
|
|
|
-- DrawMagnetTrain lays MagnetTrainTilemap over BG rows 6-9.
|
|
local FG_ROW = 6
|
|
local FG_ROWS = 4
|
|
|
|
-- Every value in this file is a hardware byte.
|
|
local function b(value) return value % 256 end
|
|
MagnetTrain.byte = b
|
|
|
|
--------------------------------------------------------------------------
|
|
-- The player in the window
|
|
--------------------------------------------------------------------------
|
|
|
|
-- data/sprite_anims/framesets.asm .Frameset_MagnetTrainRed: two OAM sets on an
|
|
-- eight frame beat, the fourth of them mirrored, then `oamrestart`. The
|
|
-- object's own sequence is SPRITE_ANIM_FUNC_NULL (data/sprite_anims/
|
|
-- objects.asm), so nothing ever moves the struct: the only motion the player
|
|
-- has is wGlobalAnimXOffset, which the two MoveTrain states advance.
|
|
local FRAMESET = {
|
|
{ oamset = 1, duration = 8, xflip = false },
|
|
{ oamset = 2, duration = 8, xflip = false },
|
|
{ oamset = 1, duration = 8, xflip = false },
|
|
{ oamset = 2, duration = 8, xflip = true },
|
|
"restart",
|
|
}
|
|
|
|
-- data/sprite_anims/oam.asm: SPRITE_ANIM_OAMSET_MAGNET_TRAIN_RED_1 and _2 are
|
|
-- vtile $00 and $04 over the same .OAMData_MagnetTrainRed 2x2 block. Those
|
|
-- two vtiles are the two four-tile requests MagnetTrain_LoadGFX_PlayMusic
|
|
-- makes: ChrisSpriteGFX at vTiles0 $00, and ChrisSpriteGFX + 12 tiles at
|
|
-- vTiles0 $04. A walking overworld sprite is six 16x16 frames, so those are
|
|
-- sheet frame 0 (standing down) and sheet frame 3 (the down walk step).
|
|
local OAMSET_VTILE = { 0x00, 0x04 }
|
|
MagnetTrain.SHEET_FRAME = { [0x00] = 0, [0x04] = 3 }
|
|
|
|
-- .OAMData_MagnetTrainRed, `dbsprite x tile, y tile, x px, y px, vtile, attr`.
|
|
-- Every entry carries OAM_PRIO, so on the cart the four tiles sit BEHIND
|
|
-- background colours 1-3 and only show through the window's colour 0.
|
|
local OAM_DATA = {
|
|
{ x = b(-1 * TILE_WIDTH), y = b(-1 * TILE_WIDTH), tile = 0x00 },
|
|
{ x = b(0 * TILE_WIDTH), y = b(-1 * TILE_WIDTH), tile = 0x01 },
|
|
{ x = b(-1 * TILE_WIDTH), y = b(0 * TILE_WIDTH), tile = 0x02 },
|
|
{ x = b(0 * TILE_WIDTH), y = b(0 * TILE_WIDTH), tile = 0x03 },
|
|
}
|
|
|
|
-- AddOrSubtractX: a mirrored object flips around its own 8-pixel cell.
|
|
local function mirror(value, flip)
|
|
if not flip then return value end
|
|
return b(-(value + TILE_WIDTH))
|
|
end
|
|
|
|
--------------------------------------------------------------------------
|
|
-- The ride
|
|
--------------------------------------------------------------------------
|
|
|
|
-- `toGoldenrod` is the wScriptVar the script left behind: Goldenrod's officer
|
|
-- writes `setval FALSE` and Saffron's writes `setval TRUE`, and MagnetTrain
|
|
-- reads it as "and a / jr nz, .ToGoldenrod".
|
|
--
|
|
-- opts.bgTiles is MagnetTrainBGTiles (a 2x18 tilemap) and opts.fgTilemap is
|
|
-- MagnetTrainTilemap (20x4); both come from the extracted cache and either may
|
|
-- be missing, in which case :tilemap() answers nil and the ride still runs.
|
|
function MagnetTrain.new(opts)
|
|
opts = opts or {}
|
|
local self = setmetatable({}, MagnetTrain)
|
|
self.toGoldenrod = opts.toGoldenrod and true or false
|
|
if self.toGoldenrod then
|
|
-- .ToGoldenrod: `ld a, -1` / `lb bc, -8 tiles, -12 tiles` /
|
|
-- `lb de, (11 tiles) + (11 tiles + 4), 12 tiles`.
|
|
self.direction = b(-1)
|
|
self.holdPosition = b(-8 * TILE_WIDTH) -- b
|
|
self.initPosition = b(-12 * TILE_WIDTH) -- c
|
|
self.finalPosition = b(12 * TILE_WIDTH) -- e
|
|
self.playerSpriteInitX =
|
|
b((11 * TILE_WIDTH) + (11 * TILE_WIDTH + 4)) -- d
|
|
else
|
|
-- forwards: `ld a, 1` / `lb bc, 8 tiles, 12 tiles` /
|
|
-- `lb de, (11 tiles) - (11 tiles + 4), -12 tiles`.
|
|
self.direction = 1
|
|
self.holdPosition = b(8 * TILE_WIDTH)
|
|
self.initPosition = b(12 * TILE_WIDTH)
|
|
self.finalPosition = b(-12 * TILE_WIDTH)
|
|
self.playerSpriteInitX = b((11 * TILE_WIDTH) - (11 * TILE_WIDTH + 4))
|
|
end
|
|
|
|
-- MagnetTrain_LoadGFX_PlayMusic's tail writes wJumptableIndex and the three
|
|
-- bytes after it, so the wait counter starts life holding the init position.
|
|
-- State 0 overwrites it before any .WaitScene reads it.
|
|
self.index = 0
|
|
self.offset = self.initPosition
|
|
self.position = self.initPosition
|
|
self.waitCounter = self.initPosition
|
|
self.exited = false
|
|
self.globalX = 0
|
|
|
|
-- The sprite struct does not exist until .InitPlayerSpriteAnim runs.
|
|
self.spriteX, self.spriteY = nil, nil
|
|
self.frame, self.frameDuration = -1, 0
|
|
self.oamFrame = nil
|
|
|
|
self.bgTiles = opts.bgTiles
|
|
self.fgTilemap = opts.fgTilemap
|
|
self:updateLYOverrides(true)
|
|
return self
|
|
end
|
|
|
|
function MagnetTrain:done() return self.exited end
|
|
|
|
-- MagnetTrain's .loop, one pass: the exit bit, PlaySpriteAnimations, the
|
|
-- jumptable, then the LY overrides. Returns the sfx label the frame played,
|
|
-- which is only ever SFX_TRAIN_ARRIVED on the last one.
|
|
function MagnetTrain:update()
|
|
if self.exited then return nil end
|
|
self:stepSpriteFrame()
|
|
local sfx = self:runJumptable()
|
|
self:updateLYOverrides()
|
|
return sfx
|
|
end
|
|
|
|
-- MagnetTrain_Jumptable.Next
|
|
function MagnetTrain:next()
|
|
self.index = self.index + 1
|
|
end
|
|
|
|
-- .WaitScene: zero means "advance", anything else counts down. A counter of
|
|
-- 128 therefore holds for 129 frames, the last of which is the one that reads
|
|
-- zero and moves on.
|
|
function MagnetTrain:waitScene()
|
|
if self.waitCounter == 0 then
|
|
self:next()
|
|
return
|
|
end
|
|
self.waitCounter = self.waitCounter - 1
|
|
end
|
|
|
|
function MagnetTrain:runJumptable()
|
|
local index = self.index
|
|
if index == 0 then
|
|
-- .InitPlayerSpriteAnim: InitSpriteAnimStruct at d = (8 + 2) * 8 + 5,
|
|
-- e = wMagnetTrainPlayerSpriteInitX, then SPRITEANIMSTRUCT_TILE_ID = 0.
|
|
self.spriteY = b((8 + 2) * TILE_WIDTH + 5)
|
|
self.spriteX = self.playerSpriteInitX
|
|
self.frame, self.frameDuration, self.oamFrame = -1, 0, nil
|
|
self:next()
|
|
self.waitCounter = 128
|
|
elseif index == 1 or index == 3 or index == 5 then
|
|
self:waitScene()
|
|
elseif index == 2 then
|
|
-- .MoveTrain1: one pixel a frame until the train reaches its hold
|
|
-- position, then park for another 128 frames.
|
|
if self.position == self.holdPosition then
|
|
self:next()
|
|
self.waitCounter = 128
|
|
return nil
|
|
end
|
|
self.position = b(self.position - self.direction)
|
|
self.globalX = b(self.globalX + self.direction)
|
|
elseif index == 4 then
|
|
-- .MoveTrain2: the same, at double speed, until it leaves the screen.
|
|
if self.position == self.finalPosition then
|
|
self:next()
|
|
return nil
|
|
end
|
|
self.position = b(self.position - 2 * self.direction)
|
|
self.globalX = b(self.globalX + 2 * self.direction)
|
|
elseif index >= 6 then
|
|
-- .TrainArrived: JUMPTABLE_EXIT and SFX_TRAIN_ARRIVED, and the loop reads
|
|
-- the exit bit at the top of the next pass.
|
|
self.exited = true
|
|
return "Sfx_TrainArrived"
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- MagnetTrain_UpdateLYOverrides. The three runs are 6*8-1, 6*8 and 6*8+1
|
|
-- entries, which is 144 scanlines exactly; hSCX takes the first band's value
|
|
-- because line 0 is drawn before the LCD interrupt has fired. The offset
|
|
-- advances by two per frame (`add d` twice) AFTER the overrides are written.
|
|
--
|
|
-- `initial` builds the first frame's table without advancing, matching
|
|
-- MagnetTrain_InitLYOverrides, which ByteFills the whole array with the init
|
|
-- position before the loop starts.
|
|
function MagnetTrain:updateLYOverrides(initial)
|
|
local ly = self.ly or {}
|
|
if initial then
|
|
for line = 1, SCREEN_HEIGHT_PX do ly[line] = self.initPosition end
|
|
self.ly = ly
|
|
self.scx = self.initPosition
|
|
return ly
|
|
end
|
|
local scx = b(self.offset * 2)
|
|
self.scx = scx
|
|
local line = 1
|
|
for _ = 1, 6 * TILE_WIDTH - 1 do ly[line] = scx; line = line + 1 end
|
|
for _ = 1, 6 * TILE_WIDTH do ly[line] = self.position; line = line + 1 end
|
|
for _ = 1, 6 * TILE_WIDTH + 1 do ly[line] = scx; line = line + 1 end
|
|
self.ly = ly
|
|
self.offset = b(self.offset + 2 * self.direction)
|
|
return ly
|
|
end
|
|
|
|
-- The SCX each of the three bands is scrolled by this frame, as
|
|
-- { first scanline, last scanline (inclusive), scx }. A band is a run of
|
|
-- equal LY overrides, so this is the same information the table holds and the
|
|
-- shape a renderer wants.
|
|
function MagnetTrain:bands()
|
|
local ly = self.ly
|
|
if not ly then return {} end
|
|
local out = {}
|
|
local start, value = 0, ly[1]
|
|
for line = 1, SCREEN_HEIGHT_PX do
|
|
if ly[line] ~= value then
|
|
out[#out + 1] = { start, line - 2, value }
|
|
start, value = line - 1, ly[line]
|
|
end
|
|
end
|
|
out[#out + 1] = { start, SCREEN_HEIGHT_PX - 1, value }
|
|
return out
|
|
end
|
|
|
|
--------------------------------------------------------------------------
|
|
-- The background
|
|
--------------------------------------------------------------------------
|
|
|
|
-- DrawMagnetTrain. Rows 0-17 are MagnetTrainBGTiles' two-tile pair for that
|
|
-- row repeated across all 32 columns (`.FillAlt`, TILEMAP_WIDTH / 2 times),
|
|
-- and then MagnetTrainTilemap's four 20-tile lines are laid over rows 6-9.
|
|
--
|
|
-- Answers nil when the cache carries no tilemaps, which is what a cache built
|
|
-- before the extractor learned about them looks like.
|
|
function MagnetTrain:tilemap()
|
|
local bg = self.bgTiles
|
|
if not (bg and #bg >= SCREEN_HEIGHT * 2) then return nil end
|
|
local rows = {}
|
|
for row = 0, SCREEN_HEIGHT - 1 do
|
|
local even, odd = bg[row * 2 + 1], bg[row * 2 + 2]
|
|
local line = {}
|
|
for col = 0, TILEMAP_WIDTH - 1 do
|
|
line[col + 1] = (col % 2 == 0) and even or odd
|
|
end
|
|
rows[row + 1] = line
|
|
end
|
|
local fg = self.fgTilemap
|
|
if fg and #fg >= SCREEN_WIDTH * FG_ROWS then
|
|
for line = 0, FG_ROWS - 1 do
|
|
local row = rows[FG_ROW + line + 1]
|
|
for col = 0, SCREEN_WIDTH - 1 do
|
|
row[col + 1] = fg[line * SCREEN_WIDTH + col + 1]
|
|
end
|
|
end
|
|
end
|
|
return rows
|
|
end
|
|
|
|
-- SetMagnetTrainPals, read back as "which palette does this cell use".
|
|
-- `col` and `row` are 0-based BG map coordinates.
|
|
function MagnetTrain.paletteSlot(col, row)
|
|
if row == WINDOW_ROW and col >= WINDOW_COL
|
|
and col < WINDOW_COL + WINDOW_WIDTH then
|
|
return MagnetTrain.PAL_BG_YELLOW
|
|
end
|
|
if row < BUSH_ROWS_TOP then return MagnetTrain.PAL_BG_GREEN end
|
|
if row < BUSH_ROWS_TOP + TRAIN_ROWS then return MagnetTrain.PAL_BG_GRAY end
|
|
return MagnetTrain.PAL_BG_GREEN
|
|
end
|
|
|
|
--------------------------------------------------------------------------
|
|
-- The sprite
|
|
--------------------------------------------------------------------------
|
|
|
|
-- GetSpriteAnimFrame, cut down to one frameset that never waits, ends or
|
|
-- changes sequence. A frame with duration 8 is therefore shown nine times:
|
|
-- the pass that sets the duration, then eight that decrement it.
|
|
function MagnetTrain:stepSpriteFrame()
|
|
if not self.spriteX then return end
|
|
if self.frameDuration ~= 0 then
|
|
self.frameDuration = self.frameDuration - 1
|
|
return
|
|
end
|
|
self.frame = self.frame + 1
|
|
local entry = FRAMESET[self.frame + 1]
|
|
if entry == "restart" then
|
|
self.frame = 0
|
|
entry = FRAMESET[1]
|
|
end
|
|
self.frameDuration = entry.duration
|
|
self.oamFrame = entry
|
|
end
|
|
|
|
-- The four OAM entries the player is drawn as this frame, each
|
|
-- { x, y, tile, xflip } in SCREEN pixels (the hardware's byte minus the 8 and
|
|
-- 16 pixel OAM origins). `tile` is the vtile the OAM set resolves to, which
|
|
-- MagnetTrain.SHEET_FRAME turns into a 16x16 frame of the walking sheet.
|
|
--
|
|
-- Empty before .InitPlayerSpriteAnim has run.
|
|
function MagnetTrain:playerOam()
|
|
local entry = self.oamFrame
|
|
if not (entry and self.spriteX) then return {} end
|
|
local vtile = OAMSET_VTILE[entry.oamset]
|
|
local out = {}
|
|
for _, sprite in ipairs(OAM_DATA) do
|
|
local x = b(self.spriteX + self.globalX + mirror(sprite.x, entry.xflip))
|
|
local y = b(self.spriteY + sprite.y)
|
|
out[#out + 1] = {
|
|
x = x - 8,
|
|
y = y - 16,
|
|
tile = vtile + sprite.tile,
|
|
xflip = entry.xflip,
|
|
}
|
|
end
|
|
return out
|
|
end
|
|
|
|
return MagnetTrain
|