mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 06:55:29 +02:00
440 lines
17 KiB
Lua
440 lines
17 KiB
Lua
-- Crystal's intro movie and title screen extraction (pokecrystal
|
|
-- engine/movie/intro.asm:1678-1777, engine/movie/title.asm:364-374).
|
|
-- Called from RomExtractorGen2's extractIntro/extractTitle stages when
|
|
-- edition == "crystal"; reads through the passed-in extractor and never
|
|
-- mutates it.
|
|
|
|
local ImageWriter = require("src.import.ImageWriter")
|
|
|
|
local CrystalMovie = {}
|
|
|
|
local TILE_BYTES = 16
|
|
local SHEET_TILES = 16
|
|
|
|
-- vBGMap tilemaps and attrmaps are one full 32x32 map
|
|
-- (engine/movie/intro.asm:1611-1628 decompresses $40 tiles = 1024 bytes).
|
|
local MAP_BYTES = 1024
|
|
|
|
local function padTiles(raw, count)
|
|
local length = count * TILE_BYTES
|
|
while #raw > length do table.remove(raw) end
|
|
while #raw < length do raw[#raw + 1] = 0 end
|
|
return raw
|
|
end
|
|
|
|
-- Lay `count` tiles of `source` (from tile `from`, 0-based) over `target`
|
|
-- starting at tile `at` (0-based).
|
|
local function overlayTiles(target, source, at, from, count)
|
|
for offset = 1, count * TILE_BYTES do
|
|
target[at * TILE_BYTES + offset] = source[from * TILE_BYTES + offset] or 0
|
|
end
|
|
return target
|
|
end
|
|
|
|
local function blankTiles(count)
|
|
local out = {}
|
|
for index = 1, count * TILE_BYTES do out[index] = 0 end
|
|
return out
|
|
end
|
|
|
|
-- Sheets are 16 tiles per row so tile id N resolves to (N % 16, N / 16),
|
|
-- the same layout data/sprite_anims/oam.asm:120-136 assumes.
|
|
local function writeSheet(self, raw, count, relative)
|
|
padTiles(raw, count)
|
|
local rows = math.ceil(count / SHEET_TILES)
|
|
padTiles(raw, rows * SHEET_TILES)
|
|
self:write2bpp(raw, SHEET_TILES * 8, rows * 8, relative, true)
|
|
return "assets/generated/" .. relative
|
|
end
|
|
|
|
local function readMap(self, label)
|
|
local bytes = self:decompressLz3Symbol(label)
|
|
while #bytes > MAP_BYTES do table.remove(bytes) end
|
|
while #bytes < MAP_BYTES do bytes[#bytes + 1] = 0 end
|
|
return bytes
|
|
end
|
|
|
|
-- 16 palettes: 8 BG then 8 OBJ (engine/movie/intro.asm:123-130 copies
|
|
-- `16 palettes` over wBGPals1, which wOBPals1 follows).
|
|
local function readPalettes(self, label)
|
|
local symbol = self:symbol(label)
|
|
local flat = self:colors(symbol.bank, symbol.address, 64)
|
|
local bg, obj = {}, {}
|
|
for pal = 0, 7 do
|
|
local a, b = {}, {}
|
|
for slot = 1, 4 do
|
|
a[slot] = flat[pal * 4 + slot]
|
|
b[slot] = flat[(pal + 8) * 4 + slot]
|
|
end
|
|
bg[pal + 1], obj[pal + 1] = a, b
|
|
end
|
|
return { bg = bg, obj = obj }
|
|
end
|
|
|
|
local function readColors(self, label, count)
|
|
local symbol = self:symbol(label)
|
|
return self:colors(symbol.bank, symbol.address, count)
|
|
end
|
|
|
|
function CrystalMovie.extractIntro(self)
|
|
self:beginStage("Intro movie")
|
|
local out = {
|
|
generation = 2,
|
|
layout = "crystal",
|
|
source = "ROM:CrystalIntro (engine/movie/intro.asm)",
|
|
}
|
|
|
|
local unowns = padTiles(self:decompressLz3Symbol("IntroUnownsGFX"), 128)
|
|
local grassSym = self:symbol("IntroGrass4GFX")
|
|
local grass4 = self.rom:bytes(grassSym.bank, grassSym.address, TILE_BYTES)
|
|
|
|
local unownsPath = writeSheet(self, unowns, 128, "intro/unowns_tiles.png")
|
|
local pulsePath = writeSheet(self,
|
|
self:decompressLz3Symbol("IntroPulseGFX"), 16, "intro/pulse_sprites.png")
|
|
local backgroundPath = writeSheet(self,
|
|
self:decompressLz3Symbol("IntroBackgroundGFX"), 128,
|
|
"intro/background_tiles.png")
|
|
local runPath = writeSheet(self,
|
|
self:decompressLz3Symbol("IntroSuicuneRunGFX"), 192,
|
|
"intro/suicune_run_sprites.png")
|
|
local pichuPath = writeSheet(self,
|
|
self:decompressLz3Symbol("IntroPichuWooperGFX"), 128,
|
|
"intro/pichu_wooper_sprites.png")
|
|
self:tick("Intro movie", 1, 4)
|
|
|
|
-- IntroScene15: jump BG at vTiles2 plus IntroGrass4GFX at vTiles1 tile 0,
|
|
-- i.e. BG id $80 (engine/movie/intro.asm:744-753).
|
|
local jump = blankTiles(256)
|
|
overlayTiles(jump, self:decompressLz3Symbol("IntroSuicuneJumpGFX"), 0, 0, 128)
|
|
overlayTiles(jump, grass4, 0x80, 0, 1)
|
|
local jumpPath = writeSheet(self, jump, 256, "intro/suicune_jump_tiles.png")
|
|
|
|
-- The same grass tile is the whole of the SUICUNE_AWAY object's sheet
|
|
-- (data/sprite_anims/oam.asm:136 base $80; engine/movie/intro.asm:750-753).
|
|
local unownBack = blankTiles(144)
|
|
overlayTiles(unownBack, self:decompressLz3Symbol("IntroUnownBackGFX"), 0, 0, 48)
|
|
overlayTiles(unownBack, grass4, 0x80, 0, 1)
|
|
local unownBackPath = writeSheet(self, unownBack, 144,
|
|
"intro/unown_back_sprites.png")
|
|
|
|
-- IntroScene17 loads 255 tiles from vTiles1, so BG id $80 is close tile 0
|
|
-- and ids wrap through $00-$7e (engine/movie/intro.asm:825-828).
|
|
local closeRaw = padTiles(self:decompressLz3Symbol("IntroSuicuneCloseGFX"), 255)
|
|
local close = blankTiles(256)
|
|
for id = 0, 255 do
|
|
local from = (id + 128) % 256
|
|
if from < 255 then overlayTiles(close, closeRaw, id, from, 1) end
|
|
end
|
|
local closePath = writeSheet(self, close, 256, "intro/suicune_close_tiles.png")
|
|
|
|
-- IntroScene19: suicune_back at vTiles2, the Unown ring at vTiles1, and
|
|
-- grass over vTiles1 tile $7f = BG id $ff (engine/movie/intro.asm:890-901).
|
|
local back = blankTiles(256)
|
|
overlayTiles(back, self:decompressLz3Symbol("IntroSuicuneBackGFX"), 0, 0, 128)
|
|
overlayTiles(back, unowns, 128, 0, 128)
|
|
overlayTiles(back, grass4, 0xff, 0, 1)
|
|
local backPath = writeSheet(self, back, 256, "intro/suicune_back_tiles.png")
|
|
|
|
local crystalUnownsPath = writeSheet(self,
|
|
self:decompressLz3Symbol("IntroCrystalUnownsGFX"), 32,
|
|
"intro/crystal_unowns_tiles.png")
|
|
|
|
-- Intro_RustleGrass swaps 4 tiles at vTiles2 tile $09 through grass1/2/3
|
|
-- (engine/movie/intro.asm:1521-1547); one 16-tile row, frame f at f*4.
|
|
local grassStrip = blankTiles(16)
|
|
for frame, label in ipairs({ "IntroGrass1GFX", "IntroGrass2GFX",
|
|
"IntroGrass3GFX" }) do
|
|
local sym = self:symbol(label)
|
|
overlayTiles(grassStrip,
|
|
self.rom:bytes(sym.bank, sym.address, 4 * TILE_BYTES), (frame - 1) * 4,
|
|
0, 4)
|
|
end
|
|
out.grassFrames = writeSheet(self, grassStrip, 16, "intro/grass_anim.png")
|
|
self:tick("Intro movie", 2, 4)
|
|
|
|
local unownPals = readPalettes(self, "IntroUnownsPalette")
|
|
local backgroundPals = readPalettes(self, "IntroBackgroundPalette")
|
|
local suicunePals = readPalettes(self, "IntroSuicunePalette")
|
|
local closePals = readPalettes(self, "IntroSuicuneClosePalette")
|
|
local crystalUnownsPals = readPalettes(self, "IntroCrystalUnownsPalette")
|
|
|
|
local function act(tiles, sprites, tilemapLabel, attrmapLabel, palettes)
|
|
return {
|
|
tiles = tiles,
|
|
sprites = sprites,
|
|
tilemap = readMap(self, tilemapLabel),
|
|
attrmap = readMap(self, attrmapLabel),
|
|
palettes = palettes,
|
|
}
|
|
end
|
|
|
|
out.acts = {
|
|
unownA = act(unownsPath, pulsePath,
|
|
"IntroUnownATilemap", "IntroUnownAAttrmap", unownPals),
|
|
unownHI = act(unownsPath, pulsePath,
|
|
"IntroUnownHITilemap", "IntroUnownHIAttrmap", unownPals),
|
|
unowns = act(unownsPath, pulsePath,
|
|
"IntroUnownsTilemap", "IntroUnownsAttrmap", unownPals),
|
|
background = act(backgroundPath, runPath,
|
|
"IntroBackgroundTilemap", "IntroBackgroundAttrmap", backgroundPals),
|
|
suicuneJump = act(jumpPath, unownBackPath,
|
|
"IntroSuicuneJumpTilemap", "IntroSuicuneJumpAttrmap", suicunePals),
|
|
suicuneClose = act(closePath, nil,
|
|
"IntroSuicuneCloseTilemap", "IntroSuicuneCloseAttrmap", closePals),
|
|
suicuneBack = act(backPath, unownBackPath,
|
|
"IntroSuicuneBackTilemap", "IntroSuicuneBackAttrmap", suicunePals),
|
|
crystalUnowns = act(crystalUnownsPath, nil,
|
|
"IntroCrystalUnownsTilemap", "IntroCrystalUnownsAttrmap",
|
|
crystalUnownsPals),
|
|
}
|
|
-- IntroScene7/10 load pichu_wooper into VRAM bank 1
|
|
-- (engine/movie/intro.asm:340-348); OAM_BANK1 picks this sheet.
|
|
out.acts.background.sprites1 = pichuPath
|
|
self:tick("Intro movie", 3, 4)
|
|
|
|
out.fades = {
|
|
toWhite = {},
|
|
unownAppear = readColors(self, "Intro_Scene20_AppearUnown.pal1", 4),
|
|
unownAppear2 = readColors(self, "Intro_Scene20_AppearUnown.pal2", 4),
|
|
wordFast = readColors(self, "Intro_FadeUnownWordPals.FastFadePalettes", 16),
|
|
wordSlow = readColors(self, "Intro_FadeUnownWordPals.SlowFadePalettes", 16),
|
|
}
|
|
-- Intro_Scene24_ApplyPaletteFade steps 8 rows of one palette each
|
|
-- (engine/movie/intro.asm:1155-1189).
|
|
local fade = readColors(self, "Intro_Scene24_ApplyPaletteFade.FadePals", 32)
|
|
for row = 0, 7 do
|
|
local pal = {}
|
|
for slot = 1, 4 do pal[slot] = fade[row * 4 + slot] end
|
|
out.fades.toWhite[row + 1] = pal
|
|
end
|
|
|
|
self:write("intro", out)
|
|
self:tick("Intro movie", 4, 4)
|
|
return out
|
|
end
|
|
|
|
--------------------------------------------------------------------------
|
|
|
|
local function shadeOf(r)
|
|
if r > 0.9 then return 0 end
|
|
if r > 0.5 then return 1 end
|
|
if r > 0.2 then return 2 end
|
|
return 3
|
|
end
|
|
|
|
local function tilesFrom2bpp(raw)
|
|
local tiles = {}
|
|
for offset = 1, #raw - (#raw % TILE_BYTES), TILE_BYTES do
|
|
local one = {}
|
|
for index = offset, offset + TILE_BYTES - 1 do one[#one + 1] = raw[index] end
|
|
tiles[#tiles + 1] = ImageWriter.decode2bpp(one, 8, 8, true)
|
|
end
|
|
return tiles
|
|
end
|
|
|
|
local function blitTile(target, tile, tx, ty)
|
|
if not tile then return end
|
|
for y = 0, 7 do
|
|
for x = 0, 7 do
|
|
local r, g, b, a = tile:getPixel(x, y)
|
|
if a ~= 0 then target:setPixel(tx + x, ty + y, r, g, b, a) end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function colorize(image, palFor)
|
|
local w, h = image:getDimensions()
|
|
local out = ImageWriter.blank(w, h, 0, 0, 0, 0)
|
|
for y = 0, h - 1 do
|
|
for x = 0, w - 1 do
|
|
local r, _, _, a = image:getPixel(x, y)
|
|
if a ~= 0 then
|
|
local pal = palFor(x, y)
|
|
local c = pal[shadeOf(r) + 1] or pal[4] or { 0, 0, 0 }
|
|
out:setPixel(x, y, c[1] / 255, c[2] / 255, c[3] / 255, 1)
|
|
end
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- engine/movie/splash.asm:20-30 sets SCGB_GAMEFREAK_LOGO before Copyright, so
|
|
-- the card runs on gfx/sgb/predef.pal:79 PREDEFPAL_GAMEFREAK_LOGO_BG.
|
|
local COPYRIGHT_TILES = 29
|
|
local COPYRIGHT_BG = {
|
|
{ 0, 0, 0 }, { 66, 90, 90 }, { 173, 173, 173 }, { 255, 255, 255 },
|
|
}
|
|
|
|
-- data/copyright.asm at hlcoord 2, 7 (engine/menus/intro_menu.asm:1315-1326).
|
|
local COPYRIGHT_LINES = {
|
|
{ 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
|
|
0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c },
|
|
{ 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
|
|
0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x7a, 0x7b, 0x7c },
|
|
{ 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
|
|
0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c },
|
|
}
|
|
|
|
local function extractCopyright(self)
|
|
local symbol = self:symbol("CopyrightGFX")
|
|
local raw = self.rom:bytes(symbol.bank, symbol.address,
|
|
COPYRIGHT_TILES * TILE_BYTES)
|
|
self:write2bpp(raw, COPYRIGHT_TILES * 8, 8, "title/copyright.png")
|
|
|
|
local painted = {}
|
|
for index, tile in ipairs(tilesFrom2bpp(raw)) do
|
|
painted[index] = colorize(tile, function() return COPYRIGHT_BG end)
|
|
end
|
|
local backdrop = COPYRIGHT_BG[1]
|
|
local splash = ImageWriter.blank(160, 144,
|
|
backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255, 1)
|
|
for row, line in ipairs(COPYRIGHT_LINES) do
|
|
for column, id in ipairs(line) do
|
|
blitTile(splash, painted[id - 0x60 + 1], (1 + column) * 8, (6 + row) * 8)
|
|
end
|
|
end
|
|
self:save(splash, "title/copyright_splash.png")
|
|
end
|
|
|
|
function CrystalMovie.extractTitle(self)
|
|
self:beginStage("Title screen")
|
|
|
|
-- TitleLogoGFX decompresses to vTiles1, so BG id $80 is logo tile 0 and
|
|
-- ids $00-$1b wrap on past it (engine/movie/title.asm:91-94).
|
|
local logoTiles = tilesFrom2bpp(self:decompressLz3Symbol("TitleLogoGFX"))
|
|
-- TitleSuicuneGFX fills vTiles4-vTiles5: bank-1 ids $80-$ff then $00-$7f
|
|
-- (engine/movie/title.asm:24-27).
|
|
local suicuneTiles = tilesFrom2bpp(self:decompressLz3Symbol("TitleSuicuneGFX"))
|
|
local gemTiles = tilesFrom2bpp(self:decompressLz3Symbol("TitleCrystalGFX"))
|
|
local pals = readPalettes(self, "TitleScreenPalettes")
|
|
self:tick("Title screen", 1, 5)
|
|
|
|
-- Attribute regions from _TitleScreen's ByteFills
|
|
-- (engine/movie/title.asm:39-85): logo gradient rows, the version strip,
|
|
-- pal 7 on the window copyright line, pal 0 everywhere else.
|
|
local function bgPalAt(col, row)
|
|
if row == 9 and col >= 5 and col <= 15 then return 1 end
|
|
if row >= 3 and row <= 4 then return 2 end
|
|
if row == 5 then return 3 end
|
|
if row == 6 then return 4 end
|
|
if row == 7 then return 5 end
|
|
if row >= 8 and row <= 9 then return 6 end
|
|
if row == 17 then return 7 end
|
|
return 0
|
|
end
|
|
|
|
local shade = ImageWriter.blank(160, 144, 0, 0, 0, 0)
|
|
-- DrawTitleGraphic lays 7 rows of 20 running tiles from d=$80
|
|
-- (engine/movie/title.asm:107-112,275-302).
|
|
for row = 0, 6 do
|
|
for col = 0, 19 do
|
|
blitTile(shade, logoTiles[row * 20 + col + 1], col * 8, (row + 3) * 8)
|
|
end
|
|
end
|
|
-- Copyright line: 13 tiles from d=$c on the window's row 0, shown at
|
|
-- hWY = $88 (engine/movie/title.asm:114-119; engine/menus/intro_menu.asm:1121-1122).
|
|
for index = 0, 12 do
|
|
blitTile(shade, logoTiles[128 + 12 + index + 1], (3 + index) * 8, 17 * 8)
|
|
end
|
|
|
|
local colored = colorize(shade, function(x, y)
|
|
return pals.bg[bgPalAt(math.floor(x / 8), math.floor(y / 8)) + 1]
|
|
end)
|
|
self:save(colored, "title/crystal_screen.png")
|
|
self:save(shade, "title/crystal_screen_gray.png")
|
|
|
|
local logo = ImageWriter.blank(160, 56, 0, 0, 0, 0)
|
|
ImageWriter.blit(logo, colored, 0, 0, 0, 24, 160, 56)
|
|
self:save(logo, "title/crystal_logo.png")
|
|
local wordmark = ImageWriter.blank(88, 8, 0, 0, 0, 0)
|
|
ImageWriter.blit(wordmark, colored, 0, 0, 40, 72, 88, 8)
|
|
self:save(wordmark, "title/crystal_wordmark.png")
|
|
self:tick("Title screen", 2, 5)
|
|
|
|
-- LoadSuicuneFrame: 6 rows of 8 tiles at hlcoord 6,12, row stride 16;
|
|
-- frame bases $80/$88/$00/$08 (engine/movie/title.asm:245-273).
|
|
local suicunePaths, suicuneGrayPaths = {}, {}
|
|
for index, base in ipairs({ 0x00, 0x08, 0x80, 0x88 }) do
|
|
local frame = ImageWriter.blank(64, 48, 0, 0, 0, 0)
|
|
for row = 0, 5 do
|
|
for col = 0, 7 do
|
|
blitTile(frame, suicuneTiles[base + row * 16 + col + 1],
|
|
col * 8, row * 8)
|
|
end
|
|
end
|
|
local tinted = colorize(frame, function() return pals.bg[1] end)
|
|
local rel = ("title/crystal_suicune_%d.png"):format(index)
|
|
self:save(tinted, rel)
|
|
suicunePaths[index] = "assets/generated/" .. rel
|
|
local grayRel = ("title/crystal_suicune_%d_gray.png"):format(index)
|
|
self:save(frame, grayRel)
|
|
suicuneGrayPaths[index] = "assets/generated/" .. grayRel
|
|
if index == 1 then self:save(tinted, "title/crystal_suicune.png") end
|
|
end
|
|
self:tick("Title screen", 3, 5)
|
|
|
|
-- InitializeBackground: five 48x16 strips of six 8x16 OBJs, consecutive
|
|
-- tile pairs, OBJ pal 0, OAM_PRIO (engine/movie/title.asm:304-338).
|
|
local gem = ImageWriter.blank(48, 80, 0, 0, 0, 0)
|
|
for strip = 0, 4 do
|
|
for slot = 0, 5 do
|
|
local tile = strip * 12 + slot * 2
|
|
blitTile(gem, gemTiles[tile + 1], slot * 8, strip * 16)
|
|
blitTile(gem, gemTiles[tile + 2], slot * 8, strip * 16 + 8)
|
|
end
|
|
end
|
|
local gemTinted = colorize(gem, function() return pals.obj[1] end)
|
|
self:save(gemTinted, "title/crystal_gem.png")
|
|
self:save(gem, "title/crystal_gem_gray.png")
|
|
extractCopyright(self)
|
|
self:tick("Title screen", 4, 5)
|
|
|
|
local function unit(color) return { color[1] / 255, color[2] / 255, color[3] / 255 } end
|
|
|
|
local data = {
|
|
generation = 2,
|
|
layout = "crystal_title",
|
|
source = "ROM:TitleSuicuneGFX + TitleLogoGFX + TitleCrystalGFX"
|
|
.. " + TitleScreenPalettes + CopyrightGFX",
|
|
screen = "assets/generated/title/crystal_screen.png",
|
|
screenGray = "assets/generated/title/crystal_screen_gray.png",
|
|
image = "assets/generated/title/crystal_logo.png",
|
|
wordmark = "assets/generated/title/crystal_wordmark.png",
|
|
suicune = "assets/generated/title/crystal_suicune.png",
|
|
suicuneFrames = suicunePaths,
|
|
suicuneFramesGray = suicuneGrayPaths,
|
|
-- hlcoord 6, 12 (engine/movie/title.asm:252) in screen pixels.
|
|
suicuneX = 48,
|
|
suicuneY = 96,
|
|
-- SuicuneFrameIterator advances every 8 frames (engine/movie/title.asm:217-243).
|
|
suicuneEvery = 8,
|
|
gem = "assets/generated/title/crystal_gem.png",
|
|
gemGray = "assets/generated/title/crystal_gem_gray.png",
|
|
copyright = "assets/generated/title/copyright.png",
|
|
copyrightSplash = "assets/generated/title/copyright_splash.png",
|
|
copyrightBackdrop = unit(COPYRIGHT_BG[1]),
|
|
copyrightInk = unit(COPYRIGHT_BG[4]),
|
|
-- Strip 0 starts at OAM (64, -$22) and stops at OAM y 22, i.e. screen
|
|
-- (56, 6) (engine/movie/title.asm:306-311,340-362).
|
|
gemX = 56,
|
|
gemY = 6,
|
|
gemFromY = -50,
|
|
gemStep = 2,
|
|
-- TitleScreenEntrance: hSCX from +112 to 0 by 4 with alternating-line
|
|
-- signage over the logo's 80 lines; the copyright window sits at
|
|
-- hWY = $88 only after it lands (engine/menus/intro_menu.asm:1078-1111).
|
|
entrance = { scx = 112, step = 4, lines = 80, hideBelow = 136 },
|
|
entranceSfx = "Sfx_TitleScreenEntrance",
|
|
-- TitleScreenTimer (engine/menus/intro_menu.asm:1125-1136).
|
|
timeoutFrames = 73 * 60 + 36,
|
|
sky = unit(pals.bg[1][1]),
|
|
below = unit(pals.bg[1][1]),
|
|
palettes = { bg = pals.bg, obj = pals.obj },
|
|
}
|
|
self:write("title", data)
|
|
self:tick("Title screen", 5, 5)
|
|
return data
|
|
end
|
|
|
|
return CrystalMovie
|