Files
gen1recomp/tests/gen2_crystal_gender_test.lua

446 lines
17 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 InitClock = require("src.ui.gen2.InitClock")
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 Strings = require("src.core.Strings")
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
-- pokecrystal/engine/menus/init_gender.asm:29-34
local function openMenu(screen)
for _ = 1, 600 do
if screen:menuOpen() then break end
screen:update(0)
end
return screen
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(screen.chosen, nil, "A while the prompt types answers nothing")
openMenu(screen)
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 })
openMenu(screen)
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 ROM prompt remains the source of truth while the two finite menu rows
-- go through the translation catalog.
do
local Chrome = require("src.ui.gen2.Chrome")
local priorPrint = Chrome.print
local printed = {}
Chrome.print = function(text) printed[#printed + 1] = text end
Strings.load({ strings = { Boy = "Garçon", Girl = "Fille" } })
local translated = GenderSelect.new({ data = { text = {
_AreYouABoyOrAreYouAGirlText = "ROM prompt stays intact",
} } })
eq(translated.text, "ROM prompt stays intact",
"the extracted RomText prompt is not replaced by a catalog fallback")
translated.text = ""
openMenu(translated)
translated:drawPanel()
check(table.concat(printed, "|"):find("Garçon", 1, true) ~= nil,
"Boy is translated when drawn")
check(table.concat(printed, "|"):find("Fille", 1, true) ~= nil,
"Girl is translated when drawn")
Strings.load({})
Chrome.print = priorPrint
end
-- home/text.asm:566
do
local Chrome = require("src.ui.gen2.Chrome")
local priorPrint = Chrome.print
local printed, rows = {}, {}
Chrome.print = function(text, _tx, ty)
printed[#printed + 1] = text
rows[text] = ty
end
local done = GenderSelect.new({ data = { text = {
_AreYouABoyOrAreYouAGirlText = "Are you a boy?\nOr are you a girl?{DONE}",
} } })
eq(done.text, "Are you a boy?\nOr are you a girl?",
"the {DONE} terminator is stripped from the prompt")
for _ = 1, 300 do
if done.typer:done() then break end
done:update(0)
end
done:drawPanel()
check(not table.concat(printed, "|"):find("DONE", 1, true),
"and never reaches the tile grid")
-- ../pokecrystal/home/text.asm:473
eq(rows["Are you a boy?"], 14, "the prompt's first line sits on row 14")
eq(rows["Or are you a girl?"], 16, "and `line` drops the second to row 16")
local prompt = GenderSelect.new({ data = { text = {
_AreYouABoyOrAreYouAGirlText = "Are you a boy?{PROMPT}",
} } })
eq(prompt.text, "Are you a boy?", "nor is {PROMPT}")
-- ../pokecrystal/home/print_text.asm:5
local slow = GenderSelect.new({
save = { options = { textSpeed = "SLOW" } },
data = { text = { _AreYouABoyOrAreYouAGirlText = "Are you a boy?" } },
})
check(slow.typer ~= nil and not slow.typer:done(),
"the prompt opens with nothing shown")
for _ = 1, 15 do slow:update(0) end
eq(slow.typer.shown, 3, "SLOW is one character every five frames")
printed = {}
slow:drawPanel()
check(table.concat(printed, "|"):find("Are", 1, true) ~= nil
and not table.concat(printed, "|"):find("boy", 1, true),
"and only the typed part reaches the tile grid")
local fast = GenderSelect.new({
save = { options = { textSpeed = "FAST" } },
data = { text = { _AreYouABoyOrAreYouAGirlText = "Are you a boy?" } },
})
for _ = 1, 15 do fast:update(0) end
eq(fast.typer.shown, 14, "FAST is one a frame, and the line is 14 long")
Chrome.print = priorPrint
end
-- ------------------------------------------------- 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")
-- ../pokecrystal/engine/menus/intro_menu.asm:875
do
local G = love.graphics
local priorScissor, clips = G.setScissor, {}
G.setScissor = function(x, y, w, h) clips[#clips + 1] = { x, y, w, h } end
for _, screen in ipairs({
setmetatable({}, OakSpeech),
GenderSelect.new({ data = {} }),
InitClock.new({}, { save = {} }),
}) do
clips = {}
screen:draw()
eq(#clips, 1, "draw scissors once")
eq(clips[1][1], 0, "at the panel origin")
eq(clips[1][2], 0, "at the panel origin")
eq(clips[1][3], 160, "over the LCD's own width")
eq(clips[1][4], 144, "and its height")
end
G.setScissor = priorScissor
end
-- ------------------------------------------------- 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()