mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-26 15:31:15 +02:00
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.
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
-- 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
|
||||
|
||||
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")
|
||||
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",
|
||||
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",
|
||||
-- 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
|
||||
+26
-10
@@ -405,6 +405,7 @@ end
|
||||
local CART_COLOR = {
|
||||
red = PAL.railRed, blue = PAL.railBlue, yellow = PAL.railGold,
|
||||
gold = PAL.railAmber, silver = PAL.railSilver,
|
||||
crystal = PAL.railCrystal,
|
||||
}
|
||||
local function cartColor(version)
|
||||
return CART_COLOR[version] or PAL.green
|
||||
@@ -1062,6 +1063,8 @@ local GAME_TABS = {
|
||||
label = "Gold" },
|
||||
{ id = "silver", key = "tab-silver", letter = "S", color = PAL.railSilver,
|
||||
label = "Silver" },
|
||||
{ id = "crystal", key = "tab-crystal", letter = "C",
|
||||
color = PAL.railCrystal, label = "Crystal" },
|
||||
}
|
||||
|
||||
local HEADER_TABS = {
|
||||
@@ -3555,24 +3558,37 @@ end
|
||||
-- also what a controller reaches after the tab row.
|
||||
local function buildGameModal(imp, m)
|
||||
local pad = math.floor(18 * m.s)
|
||||
local gap = math.floor(8 * m.s)
|
||||
local w = math.floor(360 * m.s)
|
||||
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
+ #GAME_TABS * (m.btnH + gap) + m.btnH + pad
|
||||
local px, py, pw = modalPanel(m, w, h)
|
||||
local headH = Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
local avail = m.H - 2 * m.pad
|
||||
local cols, gap, btnH = 1, math.floor(8 * m.s), m.btnH
|
||||
local function rows() return math.ceil(#GAME_TABS / cols) + 1 end
|
||||
local function total() return 2 * pad + headH + rows() * btnH
|
||||
+ (rows() - 1) * gap end
|
||||
if total() > avail then cols = 2 end
|
||||
if total() > avail then gap = math.max(2, math.floor(3 * m.s)) end
|
||||
if total() > avail then
|
||||
btnH = math.max(Kit.tapMin(),
|
||||
btnH - math.ceil((total() - avail) / rows()))
|
||||
end
|
||||
local nrows = rows() - 1
|
||||
local w = math.floor((cols > 1 and 440 or 360) * m.s)
|
||||
local px, py, pw = modalPanel(m, w, total())
|
||||
local cy = py + pad
|
||||
Kit.text("button", Strings("Choose game"), px + pad, cy, PAL.heading)
|
||||
cy = cy + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
cy = cy + headH
|
||||
local chrome = headerChrome(imp)
|
||||
for _, g in ipairs(GAME_TABS) do
|
||||
btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "gamepop-" .. g.id,
|
||||
local colW = math.floor((pw - 2 * pad - (cols - 1) * gap) / cols)
|
||||
for i, g in ipairs(GAME_TABS) do
|
||||
local bx = px + pad + ((i - 1) % cols) * (colW + gap)
|
||||
local by = cy + math.floor((i - 1) / cols) * (btnH + gap)
|
||||
btn(imp, bx, by, colW, btnH, "gamepop-" .. g.id,
|
||||
Strings(g.label), {
|
||||
face = "tab", font = "small", letter = g.letter, color = g.color,
|
||||
active = imp.tab == g.id,
|
||||
action = chrome.tab[g.id] })
|
||||
cy = cy + m.btnH + gap
|
||||
end
|
||||
btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "gamepop-close",
|
||||
cy = cy + nrows * (btnH + gap)
|
||||
btn(imp, px + pad, cy, pw - 2 * pad, btnH, "gamepop-close",
|
||||
Strings("Close"), { font = "small",
|
||||
action = function() imp._gamePopup = nil end })
|
||||
end
|
||||
|
||||
+1011
-111
File diff suppressed because it is too large
Load Diff
@@ -155,6 +155,53 @@ local VERSION_REQUIRED_FILES_OVERRIDE = {
|
||||
-- PCMailGFX (engine/pokemon/bills_pc.asm:2170-2173)
|
||||
"assets/generated/pc/mail_item.png",
|
||||
},
|
||||
crystal = {
|
||||
"data/generated/constants.lua",
|
||||
"data/generated/maps.lua",
|
||||
"data/generated/roofs.lua",
|
||||
"data/generated/sprites.lua",
|
||||
"data/generated/scripts.lua",
|
||||
"data/generated/text.lua",
|
||||
"data/generated/rom_text.lua",
|
||||
"data/generated/pokemon.lua",
|
||||
"data/generated/encounters.lua",
|
||||
"data/generated/tilesets.lua",
|
||||
"data/generated/landmarks.lua",
|
||||
"data/generated/audio.lua",
|
||||
"data/generated/marts.lua",
|
||||
"data/generated/oak_speech.lua",
|
||||
"data/generated/title.lua",
|
||||
"data/generated/intro.lua",
|
||||
"assets/generated/fonts/font.png",
|
||||
"assets/generated/fonts/frames.png",
|
||||
"assets/generated/title/crystal_logo.png",
|
||||
"assets/generated/title/crystal_wordmark.png",
|
||||
"assets/generated/title/crystal_suicune.png",
|
||||
"assets/generated/splash/ditto.png",
|
||||
"assets/generated/intro/chris.png",
|
||||
"assets/generated/intro/kris.png",
|
||||
"assets/generated/intro/suicune_run_sprites.png",
|
||||
"assets/generated/intro/unowns_tiles.png",
|
||||
"assets/generated/intro/oak.png",
|
||||
"assets/generated/tilesets/johto.png",
|
||||
"assets/generated/tilesets/roofs/new_bark.png",
|
||||
"assets/generated/sprites/chris.png",
|
||||
"assets/generated/sprites/kris.png",
|
||||
"assets/generated/battle/front/chikorita.png",
|
||||
"assets/generated/battle/front/wooper.png",
|
||||
"assets/generated/battle/front/pikachu.png",
|
||||
"assets/generated/battle/trainers/falkner.png",
|
||||
"assets/generated/battle/hud/balls.png",
|
||||
"assets/generated/audio/programs.bin",
|
||||
"assets/generated/slots/gold_slots_1.png",
|
||||
"assets/generated/card_flip/card_flip_1.png",
|
||||
"assets/generated/pc/mail_item.png",
|
||||
"assets/generated/trainer_card/card_f.png",
|
||||
"data/generated/mobile_gfx.lua",
|
||||
"assets/generated/battle/player_back_female.png",
|
||||
"assets/generated/battle/trainers/kris.png",
|
||||
"assets/generated/battle/trainers/chris.png",
|
||||
},
|
||||
}
|
||||
-- Same Gen 2 extract, so a Silver cache is complete when the same files exist.
|
||||
VERSION_REQUIRED_FILES_OVERRIDE.silver = VERSION_REQUIRED_FILES_OVERRIDE.gold
|
||||
@@ -605,6 +652,27 @@ local function isAcceptedRomSize(n)
|
||||
return n == ROM_BYTES_GEN1 or n == ROM_BYTES_GEN2
|
||||
end
|
||||
|
||||
local function cartLabels(generation)
|
||||
local out = {}
|
||||
for _, id in ipairs(GameVersion.ORDER) do
|
||||
if generation == nil or GameVersion.generation(id) == generation then
|
||||
out[#out + 1] = GameVersion.info(id).label
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function cartsSlashed(generation)
|
||||
return table.concat(cartLabels(generation), "/")
|
||||
end
|
||||
|
||||
local function cartsProse()
|
||||
local names = cartLabels(nil)
|
||||
local last = table.remove(names)
|
||||
if #names == 0 then return last end
|
||||
return table.concat(names, ", ") .. ", or " .. last
|
||||
end
|
||||
|
||||
local function savesInboxDir(version)
|
||||
return SAVES_INBOX_DIR .. "/" .. tostring(version)
|
||||
end
|
||||
@@ -1849,18 +1917,18 @@ function RomImporter:startData(data, displayName)
|
||||
return
|
||||
end
|
||||
if not isAcceptedRomSize(#data) then
|
||||
self:setError(("Expected a 1 MiB Game Boy ROM (Red/Blue/Yellow) or a "
|
||||
.. "2 MiB Game Boy Color ROM (Gold/Silver); this file is %.2f MiB.")
|
||||
:format(#data / 1024 / 1024))
|
||||
self:setError(("Expected a 1 MiB Game Boy ROM (%s) or a "
|
||||
.. "2 MiB Game Boy Color ROM (%s); this file is %.2f MiB.")
|
||||
:format(cartsSlashed(1), cartsSlashed(2), #data / 1024 / 1024))
|
||||
return
|
||||
end
|
||||
local actualHash = sha1(data)
|
||||
local version = GameVersion.forSha1(actualHash)
|
||||
if not version then
|
||||
self:setError(("Unsupported ROM (SHA-1 %s). This needs a clean US Pokemon "
|
||||
.. "Red, Blue, Yellow, Gold, or Silver dump; patched, trimmed or "
|
||||
.. "%s dump; patched, trimmed or "
|
||||
.. "\"fixed\" dumps "
|
||||
.. "(tagged [b] or [BF]) never verify."):format(actualHash))
|
||||
.. "(tagged [b] or [BF]) never verify."):format(actualHash, cartsProse()))
|
||||
return
|
||||
end
|
||||
local info = GameVersion.info(version)
|
||||
@@ -2965,8 +3033,10 @@ function RomImporter:resumeAfterOverlay()
|
||||
end
|
||||
|
||||
function RomImporter:_cycleTab(delta)
|
||||
local order = { "red", "blue", "yellow", "gold", "silver",
|
||||
"mods", "find", "skins", "bug" }
|
||||
local order = { "mods", "find", "skins", "bug" }
|
||||
for i = #GameVersion.ORDER, 1, -1 do
|
||||
table.insert(order, 1, GameVersion.ORDER[i])
|
||||
end
|
||||
local idx = 1
|
||||
for i, id in ipairs(order) do
|
||||
if id == self.tab then idx = i; break end
|
||||
|
||||
Reference in New Issue
Block a user