mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 15:01:11 +02:00
ca4d3d283c
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.
335 lines
13 KiB
Lua
335 lines
13 KiB
Lua
-- Crystal's player gender: the InitGender screen, the byte it writes, and the
|
|
-- eight places that read it back.
|
|
-- luajit tests/gen2_crystal_gender_test.lua
|
|
--
|
|
-- Gold and Silver have no Kris to extract, so every assertion comes in a
|
|
-- Crystal half and a Gold half: Gold must still boot straight into Oak with no
|
|
-- gender beat in the speech and Chris on the field.
|
|
-- The cache half SKIPs when no crystal cache is present.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gen2 crystal gender")
|
|
local check, eq = S.check, S.eq
|
|
|
|
love = require("tests.love_stub")
|
|
|
|
local FieldMoves = require("src.world.gen2.FieldMoves")
|
|
local GameVersion = require("src.core.GameVersion")
|
|
local GenderSelect = require("src.ui.gen2.GenderSelect")
|
|
local NamePick = require("src.ui.gen2.NamePick")
|
|
local NamingScreen = require("src.ui.gen2.NamingScreen")
|
|
local OakSpeech = require("src.ui.gen2.OakSpeech")
|
|
local Pokegear = require("src.ui.gen2.Pokegear")
|
|
local Save = require("src.core.gen2.Save")
|
|
local Screens = require("src.ui.Screens")
|
|
local TrainerCard = require("src.ui.gen2.TrainerCard")
|
|
|
|
local priorVersion = GameVersion.get()
|
|
|
|
-- ------------------------------------------------- the two sprite tables
|
|
|
|
-- data/sprites/player_sprites.asm
|
|
eq(FieldMoves.stateSprite("normal", "male"), "SPRITE_CHRIS", "Chris on foot")
|
|
eq(FieldMoves.stateSprite("bike", "male"), "SPRITE_CHRIS_BIKE", "Chris biking")
|
|
eq(FieldMoves.stateSprite("normal", "female"), "SPRITE_KRIS", "Kris on foot")
|
|
eq(FieldMoves.stateSprite("bike", "female"), "SPRITE_KRIS_BIKE", "Kris biking")
|
|
eq(FieldMoves.stateSprite("surf", "female"), "SPRITE_SURF",
|
|
"both share the Lapras")
|
|
eq(FieldMoves.stateSprite("surf_pika", "female"), "SPRITE_SURFING_PIKACHU",
|
|
"and the surfing PIKACHU")
|
|
eq(FieldMoves.stateSprite("normal", nil), "SPRITE_CHRIS",
|
|
"no recorded gender is PLAYERGENDER_MALE")
|
|
eq(FieldMoves.stateSprite("skate", "female"), "SPRITE_KRIS",
|
|
"a state with no row falls back to PLAYER_NORMAL's")
|
|
eq(FieldMoves.playerSprite("female"), "SPRITE_KRIS", "playerSprite is the same")
|
|
check(FieldMoves.isFemale("female"), "female reads as PLAYERGENDER_FEMALE_F")
|
|
check(not FieldMoves.isFemale("male"), "male does not")
|
|
check(not FieldMoves.isFemale(nil), "and neither does an absent byte")
|
|
|
|
check(FieldMoves.hasGenderChoice({ SPRITE_KRIS = {} }),
|
|
"a cache with SPRITE_KRIS offers the choice")
|
|
check(not FieldMoves.hasGenderChoice({ SPRITE_CHRIS = {} }),
|
|
"a Gold cache does not")
|
|
check(not FieldMoves.hasGenderChoice(nil), "and neither does no cache at all")
|
|
|
|
-- ------------------------------------------------- the save field (CT-5)
|
|
|
|
eq(Save.defaultPlayerName("crystal", "male"), "CHRIS", "MalePlayerNameArray[0]")
|
|
eq(Save.defaultPlayerName("crystal", "female"), "KRIS",
|
|
"FemalePlayerNameArray[0]")
|
|
eq(Save.defaultPlayerName("gold", "female"), "GOLD",
|
|
"Gold has no female array to fall into")
|
|
eq(Save.defaultPlayerName("silver", "female"), "SILVER", "nor does Silver")
|
|
eq(Save.defaultPlayerName("crystal"), "CHRIS", "and no gender is Chris")
|
|
|
|
check(Save.isFemale({ player = { gender = "female" } }), "isFemale on a save")
|
|
check(not Save.isFemale({ player = { gender = "male" } }), "and not on Chris")
|
|
check(not Save.isFemale({}), "nor on a save with no player block")
|
|
|
|
GameVersion.set("crystal")
|
|
local kris = Save.newGame({ gender = "female" })
|
|
eq(kris.player.gender, "female", "newGame records the answer")
|
|
eq(kris.player.name, "KRIS", "and lays down .Kris as the default name")
|
|
local chris = Save.newGame({})
|
|
eq(chris.player.gender, "male", "InitCrystalData zeroes the byte")
|
|
eq(chris.player.name, "CHRIS", "so a fresh save is Chris")
|
|
eq(Save.newGame({ gender = "female", playerName = "ZZ" }).player.name, "ZZ",
|
|
"an explicit name still wins")
|
|
|
|
local normalized = Save.normalize({ version = "crystal",
|
|
player = { gender = "female" } })
|
|
eq(normalized.player.name, "KRIS", "normalize fills the gendered default")
|
|
eq(Save.normalize({ version = "crystal", player = {} }).player.name, "CHRIS",
|
|
"and the male one with no byte")
|
|
|
|
-- ------------------------------------------------- the name presets
|
|
|
|
GameVersion.set("crystal")
|
|
eq(NamePick.presetsFor("male")[1], "CHRIS", "ChrisNameMenuHeader row 1")
|
|
eq(NamePick.presetsFor("male")[4], "JON", "and row 4")
|
|
eq(NamePick.presetsFor("female")[1], "KRIS", "KrisNameMenuHeader row 1")
|
|
eq(NamePick.presetsFor("female")[4], "JODI", "and row 4")
|
|
eq(NamePick.presetsFor()[1], "CHRIS", "no gender is the male header")
|
|
GameVersion.set("gold")
|
|
eq(NamePick.presetsFor("female")[1], "GOLD",
|
|
"Gold keeps PlayerNameArray whatever is asked for")
|
|
GameVersion.set("silver")
|
|
eq(NamePick.presetsFor("female")[1], "SILVER", "and so does Silver")
|
|
GameVersion.set("crystal")
|
|
|
|
eq(NamingScreen.playerSprite("female"), "SPRITE_KRIS",
|
|
"GetPlayerIcon's female sheet")
|
|
eq(NamingScreen.playerSprite("male"), "SPRITE_CHRIS", "and its male one")
|
|
|
|
-- ------------------------------------------------- the screen itself
|
|
|
|
check(Screens.GEN2_IDS ~= nil, "the registry lists the Gen 2 screens")
|
|
local registered = {}
|
|
for _, id in ipairs(Screens.GEN2_IDS) do registered[id] = true end
|
|
check(registered.Gen2GenderSelect, "Gen2GenderSelect has an id")
|
|
eq(Screens.get({ data = {} }, "Gen2GenderSelect"), GenderSelect,
|
|
"and resolves to the builtin")
|
|
|
|
eq(GenderSelect.OPTIONS[1].label, "Boy", ".MenuData item 1")
|
|
eq(GenderSelect.OPTIONS[1].gender, "male", "writes wPlayerGender 0")
|
|
eq(GenderSelect.OPTIONS[2].label, "Girl", ".MenuData item 2")
|
|
eq(GenderSelect.OPTIONS[2].gender, "female", "writes wPlayerGender 1")
|
|
|
|
-- A joypad that answers one press then goes quiet, the way the harness fakes
|
|
-- input for every other screen suite here.
|
|
local function fakeInput(button)
|
|
local pressed = button
|
|
return {
|
|
wasPressed = function(_, name)
|
|
if pressed and name == pressed then
|
|
pressed = nil
|
|
return true
|
|
end
|
|
return false
|
|
end,
|
|
}
|
|
end
|
|
|
|
local function screenGame(save, button)
|
|
return { save = save, data = {}, input = fakeInput(button) }
|
|
end
|
|
|
|
local save = Save.newGame({})
|
|
local answered
|
|
local game = screenGame(save, "a")
|
|
local screen = GenderSelect.new(game, { onDone = function(g) answered = g end })
|
|
eq(screen.cursor, 1, "`db 1 ; default option` opens on Boy")
|
|
screen:update(0)
|
|
eq(save.player.gender, "male", "A on Boy writes PLAYERGENDER_MALE")
|
|
eq(answered, nil, "and the DelayFrames 10 has not run out yet")
|
|
for _ = 1, 10 do screen:update(0) end
|
|
eq(answered, "male", "onDone fires once the ten frames are gone")
|
|
|
|
save = Save.newGame({})
|
|
game = screenGame(save, "down")
|
|
screen = GenderSelect.new(game, { onDone = function() end })
|
|
screen:update(0)
|
|
eq(screen.cursor, 2, "down walks to Girl")
|
|
game.input = fakeInput("down")
|
|
screen:update(0)
|
|
eq(screen.cursor, 1, "STATICMENU_WRAP wraps at the bottom")
|
|
game.input = fakeInput("up")
|
|
screen:update(0)
|
|
eq(screen.cursor, 2, "and at the top")
|
|
game.input = fakeInput("b")
|
|
screen:update(0)
|
|
eq(save.player.gender, "male", "STATICMENU_DISABLE_B: B answers nothing")
|
|
game.input = fakeInput("a")
|
|
screen:update(0)
|
|
eq(save.player.gender, "female", "A on Girl writes PLAYERGENDER_FEMALE")
|
|
|
|
check(type(screen.text) == "string" and screen.text:find("boy"),
|
|
"the prompt is _AreYouABoyOrAreYouAGirlText")
|
|
|
|
-- ------------------------------------------------- the beat in Oak's speech
|
|
|
|
local function speechFor(sprites)
|
|
return { game = { data = { gen2Sprites = sprites } } }
|
|
end
|
|
|
|
local crystalSteps = OakSpeech.defaultSteps(speechFor({ SPRITE_KRIS = {} }))
|
|
eq(crystalSteps[1].id, "gender_select",
|
|
"PlayerProfileSetup's InitGender runs before OakSpeech")
|
|
eq(crystalSteps[1].kind, "gender", "as its own step kind")
|
|
eq(crystalSteps[2].id, "init_clock", "and the clock follows it")
|
|
|
|
local goldSteps = OakSpeech.defaultSteps(speechFor({ SPRITE_CHRIS = {} }))
|
|
eq(goldSteps[1].id, "init_clock", "Gold opens on the clock, as it always did")
|
|
for _, step in ipairs(goldSteps) do
|
|
check(step.kind ~= "gender", "and never grows a gender beat")
|
|
end
|
|
eq(OakSpeech.defaultSteps()[1].id, "init_clock",
|
|
"a bare call (no speech) is the Gold list")
|
|
eq(#goldSteps + 1, #crystalSteps, "the Crystal list is the Gold list plus one")
|
|
|
|
-- ------------------------------------------------- the trainer card
|
|
|
|
local CARD_GFX = {
|
|
card = "card.png", cardFemale = "card_f.png", cardTilesWide = 16,
|
|
leaderClasses = { "FALKNER" },
|
|
-- the male attrmap the extractor writes: portrait on $0, corner on $1
|
|
paletteZones = { { 14, 1, 5, 7, 1 }, { 18, 1, 1, 1, 2 } },
|
|
}
|
|
|
|
local function cardFor(gender)
|
|
return TrainerCard.new({ data = {} }, {
|
|
save = { player = { gender = gender, badges = {}, kantoBadges = {} } },
|
|
menuGfx = { trainerCard = CARD_GFX },
|
|
palettes = { trainers = { PLAYER = { { 1, 1, 1 }, { 2, 2, 2 } },
|
|
FALKNER = { { 3, 3, 3 }, { 4, 4, 4 } } } },
|
|
})
|
|
end
|
|
|
|
local male = cardFor("male")
|
|
check(not male.female, "Chris keeps ChrisCardPic")
|
|
eq(male.zoneDefault, 2, "and the border wears Falkner's row")
|
|
eq(male.zone[1 * 20 + 14], 1, "with the portrait on the player's")
|
|
|
|
local female = cardFor("female")
|
|
check(female.female, "Kris takes KrisCardPic")
|
|
eq(female.zoneDefault, 1, "the border swaps to Chris's row")
|
|
eq(female.zone[1 * 20 + 14], 2, "the portrait to Falkner's, which is Kris's")
|
|
eq(female.zone[1 * 20 + 18], 1, "the top-right corner follows the border")
|
|
eq(female.zone[14 * 20 + 14], 2, "and Clair borrows Kris's palette")
|
|
eq(female.zone[14 * 20 + 10], nil, "Pryce's box is untouched")
|
|
|
|
local noFemaleArt = TrainerCard.new({ data = {} }, {
|
|
save = { player = { gender = "female" } },
|
|
menuGfx = { trainerCard = { card = "card.png", paletteZones = {} } },
|
|
})
|
|
check(not noFemaleArt.female,
|
|
"a Gold cache with no cardFemale draws the card it has")
|
|
|
|
-- ------------------------------------------------- the Pokegear
|
|
|
|
local GEAR_GFX = {
|
|
tiles = "gear.png",
|
|
palettes = { { { 1, 1, 1 } } },
|
|
palettesFemale = { { { 9, 9, 9 } } },
|
|
}
|
|
|
|
local function gearFor(gender)
|
|
return Pokegear.new({ data = {} }, {
|
|
save = { player = { gender = gender }, pokegearFlags = {} },
|
|
menuGfx = { pokegear = GEAR_GFX },
|
|
})
|
|
end
|
|
|
|
eq(gearFor("male"):pals(), GEAR_GFX.palettes, "MalePokegearPals for Chris")
|
|
eq(gearFor("female"):pals(), GEAR_GFX.palettesFemale,
|
|
"FemalePokegearPals for Kris")
|
|
local noFemalePals = Pokegear.new({ data = {} }, {
|
|
save = { player = { gender = "female" }, pokegearFlags = {} },
|
|
menuGfx = { pokegear = { tiles = "gear.png", palettes = GEAR_GFX.palettes } },
|
|
})
|
|
eq(noFemalePals:pals(), GEAR_GFX.palettes,
|
|
"a Gold cache has one set and uses it")
|
|
|
|
-- ------------------------------------------------- the magnet train
|
|
|
|
local MagnetTrainRide = require("src.ui.gen2.MagnetTrainRide")
|
|
local SPRITES = { SPRITE_CHRIS = { image = "chris.png" },
|
|
SPRITE_KRIS = { image = "kris.png" } }
|
|
eq(MagnetTrainRide.playerSpriteDef({ data = { gen2Sprites = SPRITES },
|
|
gender = "female" }), SPRITES.SPRITE_KRIS, "Kris rides MAGNET_TRAIN_BLUE")
|
|
eq(MagnetTrainRide.playerSpriteDef({ data = { gen2Sprites = SPRITES },
|
|
gender = "male" }), SPRITES.SPRITE_CHRIS, "Chris rides MAGNET_TRAIN_RED")
|
|
eq(MagnetTrainRide.playerSpriteDef({
|
|
data = { gen2Sprites = { SPRITE_CHRIS = SPRITES.SPRITE_CHRIS } },
|
|
gender = "female" }), SPRITES.SPRITE_CHRIS,
|
|
"a Gold cache has only the one sheet")
|
|
|
|
-- ------------------------------------------------------------ cache-gated
|
|
|
|
local cache = os.getenv("CRYSTAL_CACHE")
|
|
if not cache then
|
|
local home = os.getenv("HOME") or ""
|
|
cache = home .. "/Library/Application Support/LOVE/crystal-dev/crystal"
|
|
end
|
|
|
|
local function loadLua(rel)
|
|
local chunk = loadfile(cache .. "/" .. rel)
|
|
if not chunk then return nil end
|
|
local ok, value = pcall(chunk)
|
|
return ok and value or nil
|
|
end
|
|
|
|
local function exists(rel)
|
|
local f = io.open(cache .. "/" .. rel, "rb")
|
|
if not f then return false end
|
|
f:close()
|
|
return true
|
|
end
|
|
|
|
local sprites = loadLua("data/generated/sprites.lua")
|
|
if not sprites then
|
|
check(true, "crystal cache absent : SKIP")
|
|
GameVersion.set(priorVersion)
|
|
S.finish()
|
|
return
|
|
end
|
|
|
|
check(sprites.SPRITE_KRIS ~= nil, "the cache carries SPRITE_KRIS")
|
|
check(sprites.SPRITE_KRIS_BIKE ~= nil, "and SPRITE_KRIS_BIKE")
|
|
eq(sprites.SPRITE_KRIS.palette, "PAL_OW_BLUE", "Kris is PAL_NPC_BLUE")
|
|
eq(sprites.SPRITE_CHRIS.palette, "PAL_OW_RED", "Chris is PAL_NPC_RED")
|
|
check(FieldMoves.hasGenderChoice(sprites),
|
|
"so a real Crystal cache offers the choice")
|
|
|
|
for _, rel in ipairs({
|
|
"assets/generated/intro/chris.png",
|
|
"assets/generated/intro/kris.png",
|
|
"assets/generated/trainer_card/card.png",
|
|
"assets/generated/trainer_card/card_f.png",
|
|
"assets/generated/sprites/kris.png",
|
|
"assets/generated/sprites/kris_bike.png",
|
|
}) do
|
|
check(exists(rel), "the cache carries " .. rel)
|
|
end
|
|
|
|
local menuGfx = loadLua("data/generated/menu_gfx.lua") or {}
|
|
check(menuGfx.trainerCard and menuGfx.trainerCard.cardFemale ~= nil,
|
|
"menu_gfx.trainerCard.cardFemale is extracted")
|
|
check(menuGfx.pokegear and menuGfx.pokegear.palettesFemale ~= nil,
|
|
"menu_gfx.pokegear.palettesFemale is extracted")
|
|
check(menuGfx.pack and menuGfx.pack.palettesFemale ~= nil,
|
|
"menu_gfx.pack.palettesFemale is extracted")
|
|
|
|
local palettes = loadLua("data/generated/palettes.lua") or {}
|
|
check(palettes.trainers and palettes.trainers.PLAYER ~= nil,
|
|
"PlayerPalette is row 0")
|
|
check(palettes.trainers and palettes.trainers.FALKNER ~= nil,
|
|
"and KrisPalette is Falkner's")
|
|
|
|
local romText = loadLua("data/generated/rom_text.lua") or {}
|
|
check(romText._AreYouABoyOrAreYouAGirlText ~= nil,
|
|
"the prompt is in the extracted text")
|
|
|
|
GameVersion.set(priorVersion)
|
|
S.finish()
|