Files
gen1recomp/src/ui/gen2/Diploma.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

196 lines
7.2 KiB
Lua

-- The #DEX-completion diploma (engine/events/diploma.asm PlaceDiplomaOnScreen,
-- reached through `special Diploma` after Celadon Mansion 3F's game designer
-- checks VAR_DEXCAUGHT == 251). Only page 1 is ever shown in play --
-- PrintDiplomaPage2 is the Game Boy Printer's second sheet
-- (engine/printer/printer.asm:420), built with hBGMapMode zeroed and undone by
-- SafeLoadTempTilemapToTilemap, so it never reaches the screen on the cart
-- either -- so this transcribes PlaceDiplomaOnScreen alone, not diploma2.asm.
--
-- THE CERTIFICATE IS A TILEMAP, NOT A TEXT BOX. PlaceDiplomaOnScreen
-- decompresses DiplomaGFX into vTiles2 and then CopyBytes' DiplomaPage1Tilemap
-- (a whole SCREEN_AREA of tile ids) straight over the background before a
-- single string is placed, so the border, the seal and the ribbon are cart
-- art. Both come out of the cache as `data.gen2Diploma`
-- (RomExtractorGen2:extractDiploma); a cache that predates that stage falls
-- back to the plain Chrome.box frame below, which is a placeholder and not
-- the real seal.
--
-- Positions below are the literal hlcoord operands PlaceDiplomaOnScreen
-- calls PlaceString with, not a layout guessed from a screenshot:
--
-- hlcoord 2, 5 "PLAYER" (.Player, "PLAYER@")
-- hlcoord 15, 5 .EmptyString ("@") -- a bare terminator, nothing to draw
-- hlcoord 9, 5 wPlayerName, dropped in over the row .Player/.EmptyString
-- bracket
-- hlcoord 2, 8 .Certification, five `next`-joined lines that PlaceString
-- walks one row down at column 2 apiece: rows 8-12
--
-- COLOUR. _CGB_Diploma (engine/gfx/cgb_layouts.asm) loads all eight
-- DiplomaPalettes sets and then WipeAttrmap zeroes the attrmap, so every tile
-- on the screen -- art and text alike -- draws through set 0. The strings go
-- down with Chrome.printThrough rather than Chrome.print for that reason: a
-- black print over the art would be the one thing on screen not going through
-- the palette.
--
-- WaitPressAorB_BlinkCursor just parks on A or B; there is no menu here.
local Chrome = require("src.ui.gen2.Chrome")
local GbcPalette = require("src.render.GbcPalette")
local Strings = require("src.core.Strings")
local Diploma = {}
Diploma.__index = Diploma
Diploma.isOpaque = true
function Diploma:wantsFillScale() return true end
local CERTIFICATION = {
Strings.source("This certifies"),
Strings.source("that you have"),
Strings.source("completed the"),
Strings.source("new #DEX."),
Strings.source("Congratulations!"),
}
-- opts: playerName, gfx, onClose()
function Diploma.new(game, opts)
opts = opts or {}
local self = setmetatable({}, Diploma)
self.game = game
local save = game and game.save
self.playerName = opts.playerName
or (save and save.player and save.player.name) or "?"
self.onClose = opts.onClose
self.gfx = opts.gfx or ((game and game.data) or {}).gen2Diploma
self.images = {}
self.done = false
return self
end
function Diploma:finish()
if self.done then return end
self.done = true
if self.onClose then self.onClose() end
end
function Diploma:update(_dt)
if self.done then return end
local input = self.game and self.game.input
if not input then return end
if input:wasPressed("a") or input:wasPressed("b") then
self:finish()
end
end
-- Set 0 of DiplomaPalettes, the one WipeAttrmap leaves the whole screen on.
function Diploma:palette()
local palettes = self.gfx and self.gfx.palettes
return palettes and palettes[1] or nil
end
function Diploma:image(path)
if not path then return nil end
local cached = self.images[path]
if cached == nil then
local Assets = require("src.render.Assets")
local ok, image = pcall(Assets.image, path)
cached = ok and image or false
if cached then cached:setFilter("nearest", "nearest") end
self.images[path] = cached
end
return cached or nil
end
-- The page as one batch of 8x8 tiles. Built once, because the tilemap is
-- copied once and nothing on this screen ever moves; the palette is applied at
-- draw time instead of baked in, so a COLOR mode change needs no rebuild.
function Diploma:batch()
if self.tilemap ~= nil then return self.tilemap or nil end
local gfx = self.gfx
local image = gfx and gfx.page1 and self:image(gfx.image)
if not image then
self.tilemap = false
return nil
end
local across = gfx.sheetTiles or 16
local width = gfx.width or Chrome.SCREEN_W
local height = gfx.height or Chrome.SCREEN_H
local batch = love.graphics.newSpriteBatch(image, width * height)
local quads = {}
for index = 0, width * height - 1 do
local tile = gfx.page1[index + 1] or 0
local quad = quads[tile]
if not quad then
quad = love.graphics.newQuad(tile % across * 8,
math.floor(tile / across) * 8, 8, 8, image:getDimensions())
quads[tile] = quad
end
batch:add(quad, index % width * 8, math.floor(index / width) * 8)
end
self.tilemap = batch
return batch
end
function Diploma:drawPanel()
local palette = self:palette()
local batch = self:batch()
if batch then
-- ClearTilemap leaves the screen on colour 0 of the loaded set, and the
-- page covers all of it, but the fill is what a letterboxed fill-scale
-- draw shows outside the 160x144 page.
local paper = GbcPalette.color(palette, 1)
love.graphics.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
love.graphics.rectangle("fill", 0, 0,
Chrome.SCREEN_W * 8, Chrome.SCREEN_H * 8)
love.graphics.setColor(1, 1, 1, 1)
local function blit() love.graphics.draw(batch, 0, 0) end
-- A palette-less cache would send the shader four black entries, so the
-- sheet's own grey shades are the fallback rather than a black page.
if palette then GbcPalette.with(palette, blit) else blit() end
love.graphics.setColor(1, 1, 1, 1)
else
-- No gfx/diploma in the cache: a placeholder frame, not the real seal.
Chrome.clear()
Chrome.box(0, 0, Chrome.SCREEN_W, Chrome.SCREEN_H)
end
Chrome.printThrough(Strings("PLAYER"), 2, 5, palette)
Chrome.printThrough(self.playerName, 9, 5, palette)
for i, line in ipairs(CERTIFICATION) do
Chrome.printThrough(Strings(line), 2, 7 + i, palette)
end
end
function Diploma:draw()
self:drawPanel()
end
-- PlaceDiplomaOnScreen opens on ClearBGPalettes / ClearTilemap
-- (engine/events/diploma.asm:13-14), so the map is gone and the diploma's own
-- paper is the whole screen. Colour 1 of the page palette is that paper (a
-- pale green), which is why the surround cannot be the generic white the
-- fallback paints: the page would sit in a white field instead of running to
-- the window edge.
function Diploma:drawsWidescreen() return true end
function Diploma:drawWidescreen(winW, winH)
local G = love.graphics
-- Nil palette degrades to the DMG ramp's colour 1, i.e. white, matching the
-- Chrome.clear() the panel falls back to when the cache has no gfx/diploma.
local paper = GbcPalette.color(self:palette(), 1)
G.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
G.rectangle("fill", 0, 0, winW, winH)
G.setColor(1, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
G.push()
G.translate(ox, oy)
G.scale(scale, scale)
self:drawPanel()
G.pop()
end
return Diploma