Files
gen1recomp/src/render/MonAnim.lua
T
bryanthaboi ca4d3d283c Add Pokemon Crystal as a sixth supported version
Crystal boots from a user-supplied ROM, imports a full cache and is playable:
copyright, the Crystal intro movie, the animated title, gender select, Oak,
and out into Johto. 122 of the cart's 169 script specials are implemented.

Import and data
- tools/make_crystal_manifest.py derives the manifest by importing
  make_gold_manifest as a library, with three additive keyword seams. Gold and
  Silver still regenerate byte-identical, which is the standing requirement for
  touching that generator.
- crystal_symbol_deltas.py and crystal_movie_symbols.py carry the symbol delta:
  Crystal renames the credits mons, splits the trainer card, Pokegear and
  pack-pal blocks by gender, and replaces the intro and title outright.
- Crystal-only manifest keys: engineFlagOrder (162 flags to Gold's 93, so the
  badge block sits one higher) and unownCharmap (the main charmap parser stops
  at the first newcharmap so the two cannot contaminate each other).

Extractor
- RomExtractorGen2 becomes three-edition. Crystal corrections: PAL_MAP_BANK
  0x13, a flat PICS_FIX pic bank, audio bank 0x5e, the mapSongs id-100 hole,
  seven NPC trades, a TradeTexts stride of 8, the five Crystal tileset anim
  steps with per-row degrade, and the column-major trainer card portraits.
- New: animated front sprites (frames, bitmasks, play and idle scripts), the
  Battle Tower roster, Kris assets, Mobile System GB art, and the Crystal
  intro and title via src/import/CrystalMovie.lua.

Engine
- GameVersion gains engine(id) and fixes(id). Gold and Silver keep their
  original bugs where the bug is not hardware dependent; Crystal gets the fixes
  Crystal shipped: Lucky Number boxes 10-14, surfing onto an NPC, and the
  Reflect and Light Screen defence overflow.
- Crystal story: Suicune and Eusine, Celebi behind the GS Ball flag, the Ruins
  of Alph chambers, Buena, the Move Tutor, the Poke Seer, and the Battle Tower
  including the wInBattleTowerBattle badge-boost guard.
- Kris and the gender flag, animated fronts in battle and the summary screen,
  and mon caught data.

Verification
- Every extracted asset is pixel-compared against pret's own source PNGs.
- Gold caches are byte-identical before and after, file for file.
- New Crystal suites plus a T2 Gen 2 tier; the full suite passes.
2026-08-23 12:10:28 -04:00

165 lines
5.0 KiB
Lua

-- ../pokecrystal/engine/gfx/pic_animation.asm:544 ConvertAndApplyBitmask
-- ../pokecrystal/engine/gfx/pic_animation.asm:356 PokeAnim_DoAnimScript
local MonAnim = {}
MonAnim.__index = MonAnim
-- .Sizes db 4, 5, 7: bitmask bytes for a 5x5, 6x6 and 7x7 pic.
-- ../pokecrystal/engine/gfx/pic_animation.asm:534
MonAnim.BITMASK_BYTES = { [5] = 4, [6] = 5, [7] = 7 }
-- ../pokecrystal/macros/scripts/pic_anims.asm:13-27
MonAnim.END = 0xff
MonAnim.SETREPEAT = 0xfe
MonAnim.DOREPEAT = 0xfd
-- .NextBit walks the row first, so bit i is row i % height, column i / height.
-- ../pokecrystal/engine/gfx/pic_animation.asm:746
function MonAnim.tileMap(data, frame)
local tiles = data and data.tiles
if not tiles then return nil end
local count = tiles * tiles
local out = {}
for i = 1, count do out[i] = i - 1 end
if not frame or frame <= 0 then return out end
local row = data.frames and data.frames[frame]
local mask = row and data.bitmasks and data.bitmasks[row.bitmask]
if not (row and mask) then return nil end
local cursor = 1
for i = 0, count - 1 do
local byte = mask[math.floor(i / 8) + 1] or 0
if math.floor(byte / 2 ^ (i % 8)) % 2 == 1 then
local id = row.tiles[cursor]
if id == nil then return nil end
out[i + 1] = id
cursor = cursor + 1
end
end
return out
end
-- PokeAnim_GetDuration: a * (1 + [wPokeAnimSpeed] / 16), truncated to 8 bits.
-- ../pokecrystal/engine/gfx/pic_animation.asm:413
function MonAnim.duration(param, speed)
param = param % 256
local scaled = math.floor(param * (speed or 0) / 16) % 256
return (scaled + param) % 256
end
-- The PokeAnims programs less their cry commands; "wait" is SetWait's 18.
-- ../pokecrystal/engine/gfx/pic_animation.asm:69-77, :150-164
local SCENES = {
battle = { "setup", "play" },
battleSlow = { "setup2", "play" },
menu = { "setup", "play", "wait", "idle", "play" },
}
MonAnim.SCENE_WAIT = 18
function MonAnim.scenes() return SCENES end
function MonAnim.new(data, scene)
local steps = SCENES[scene or "battle"]
if not (data and steps and data.play and #data.play > 0) then return nil end
return setmetatable({
data = data,
steps = steps,
step = 1,
frame = 0,
speed = 0,
script = nil,
pc = 1,
repeatTimer = 0,
waiting = false,
waitCounter = 0,
sceneWait = nil,
done = false,
}, MonAnim)
end
function MonAnim:finished() return self.done end
-- PokeAnim_GetFrame's `and a / ret z`: command 0 is the base picture.
-- ../pokecrystal/engine/gfx/pic_animation.asm:431-435
function MonAnim:currentFrame() return self.frame end
function MonAnim:beginScript(rows, speed)
self.script = rows or {}
self.speed = speed
self.pc = 1
self.repeatTimer = 0
self.waiting = false
self.waitCounter = 0
end
-- One tick of PokeAnim_DoAnimScript; true once the script has hit endanim.
-- ../pokecrystal/engine/gfx/pic_animation.asm:370-411
function MonAnim:runScript()
if self.waiting then
self.waitCounter = (self.waitCounter - 1) % 256
if self.waitCounter == 0 then self.waiting = false end
return false
end
for _ = 1, 256 do
local row = self.script[self.pc]
self.pc = self.pc + 1
if row == nil then return true end
local command = row[1]
if command == MonAnim.END then
return true
elseif command == MonAnim.SETREPEAT then
self.repeatTimer = row[2]
elseif command == MonAnim.DOREPEAT then
-- .DoRepeat returns on both `ret z` (:397-406).
if self.repeatTimer == 0 then return false end
self.repeatTimer = self.repeatTimer - 1
if self.repeatTimer == 0 then return false end
self.pc = row[2] + 1
else
self.frame = command
self.waiting = true
self.waitCounter = MonAnim.duration(row[2], self.speed)
-- StartWaitAnim falls through into .WaitAnim (:383-388), so the frame
-- it places is the frame the counter first ticks on.
self.waitCounter = (self.waitCounter - 1) % 256
if self.waitCounter == 0 then self.waiting = false end
return false
end
end
return true
end
-- One iteration of AnimateFrontpic's .loop: one scene command per frame.
-- ../pokecrystal/engine/gfx/pic_animation.asm:79-89
function MonAnim:update()
if self.done then return end
local step = self.steps[self.step]
if step == nil then
-- PokeAnim_Finish's DeinitFrames puts the base picture back (:224-228).
self.frame = 0
self.done = true
return
end
if step == "setup" or step == "setup2" or step == "idle" then
local rows = (step == "idle") and self.data.idle or self.data.play
self:beginScript(rows, step == "setup2" and 4 or 0)
self.step = self.step + 1
return
end
if step == "wait" then
self.sceneWait = (self.sceneWait or MonAnim.SCENE_WAIT) - 1
if self.sceneWait <= 0 then
self.sceneWait = nil
self.step = self.step + 1
end
return
end
if self:runScript() then
-- PokeAnim_Play redraws the base picture as the script ends (:196-205).
self.frame = 0
self.step = self.step + 1
end
end
return MonAnim