This commit is contained in:
bryanthaboi
2026-09-13 01:06:04 -04:00
parent 65badd1a4b
commit acdcbd8d4d
33 changed files with 2387 additions and 193 deletions
+5 -2
View File
@@ -105,6 +105,9 @@ function Game3:load(opts)
local continueOk = self:_hasContinueSave()
self.boot = Boot.new()
Boot.setHasContinue(self.boot, continueOk)
if continueOk then
Boot.setContinueInfo(self.boot, Boot.continueInfoFromSave(rawSave))
end
self.phase = "boot"
self.session = nil
@@ -245,11 +248,11 @@ function Game3:draw()
Boot.draw(self.boot)
love.graphics.setCanvas()
love.graphics.pop()
local scale, ox, oy = Display.fit(w, h)
local scale, ox, oy, _, _, scaleY = Display.fit(w, h)
love.graphics.setColor(0.02, 0.04, 0.08, 1)
love.graphics.rectangle("fill", 0, 0, w, h)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(canvas, ox, oy, 0, scale, scale)
love.graphics.draw(canvas, ox, oy, 0, scale, scaleY)
else
Boot.draw(self.boot)
end
-2
View File
@@ -1,2 +0,0 @@
-- Package entry for Game3 runtime modules.
return require("src.core.game3.init")
+8 -3
View File
@@ -16,14 +16,19 @@ local Healthbox = {}
Healthbox.ENEMY_CENTER = { x = 44, y = 30 }
Healthbox.PLAYER_CENTER = { x = 158, y = 88 }
local function c5(v)
return math.floor(v * 255 / 31 + 0.5) / 255
end
-- Cream fill matches healthbox pal index 2 (text bg).
local CREAM = { 248 / 255, 248 / 255, 216 / 255, 1 }
-- pokefirered/src/battle_interface.c:2204
local CREAM = { c5(31), c5(31), c5(27), 1 }
-- Healthbox OBJ pal text colors (gBattleInterface_Healthbox_Pal).
-- Nick: fg=1 shadow=3; gender uses DYNAMIC_COLOR_2/1 (pal 11 / 10).
local HB_TEXT = {
fg = { 64 / 255, 64 / 255, 64 / 255, 1 },
shadow = { 216 / 255, 208 / 255, 176 / 255, 1 },
fg = { c5(8), c5(8), c5(8), 1 },
shadow = { c5(27), c5(26), c5(22), 1 },
}
local HB_MALE = {
fg = { 65 / 255, 205 / 255, 255 / 255, 1 },
+44 -64
View File
@@ -380,12 +380,13 @@ end
local function choice_hooks()
return {
pushMsg = function(text) Ui.push(text) end,
askYesNo = function(cb) Ui.askYesNo(cb) end,
pushMsg = function(text, cb) Ui.push(text, cb) end,
askYesNo = function(a, b) Ui.askYesNo(a, b) end,
askForget = function(labels, cb) Ui.askForget(labels, cb) end,
headless = Battle._headless,
}
end
Battle._choiceHooksForTests = choice_hooks
local function begin_evo_or_end()
local st = Battle._st
@@ -539,79 +540,58 @@ local function handle_enemy_faint(opts)
local song = Audio.role(role) or ((st and st.wild) and 311 or 310)
Audio.playSong(song)
local pname = st.playerName or "PLAYER"
local function push_defeated()
local trName = (st.trainerClassName and st.trainerClassName ~= "")
and (st.trainerClassName .. " " .. (st.trainerName or ""))
or (st.trainerName or "TRAINER")
-- pokefirered/data/battle_scripts_1.s:2912
Ui.push(string.format("%s defeated\n%s!", pname, trName))
end
local function push_lose_text_and_money()
local Trainers = require("src.core.game3.scripting.trainers")
local dialogs = Trainers.dialogs(st.trainerId)
local defeatSpeech = st.defeatText or (dialogs and dialogs.defeat)
-- pokefirered/data/battle_scripts_1.s:2915
if defeatSpeech and defeatSpeech ~= "" then
Ui.push(defeatSpeech)
end
local Prize = require("src.core.game3.battle.prize")
local Runtime = package.loaded["src.core.game3.runtime"]
local session = Runtime and Runtime.getSession and Runtime.getSession()
if session then
local info = Trainers.info(st.trainerId)
local lastLevel = info and tonumber(info.lastLevel)
if not lastLevel or lastLevel < 1 then
lastLevel = st.enemy and st.enemy.mon and tonumber(st.enemy.mon.level) or 1
end
local gained = Prize.awardTrainerWin(session, st.trainerId, {
lastLevel = lastLevel,
double = st.double or false,
moneyMultiplier = st.moneyMultiplier or 1,
})
if gained > 0 then
Ui.push(Prize.moneyMessage(session.name or pname, gained))
end
end
end
if not st.wild and st.trainerId and not Battle._headless then
push_defeated()
SwitchSeq.beginTrainerSlideIn(st, {
headless = false,
onDone = function()
local Trainers = require("src.core.game3.scripting.trainers")
local dialogs = Trainers.dialogs(st.trainerId)
local defeatSpeech = st.defeatText or (dialogs and dialogs.defeat)
if defeatSpeech and defeatSpeech ~= "" then
Ui.push(defeatSpeech)
end
local trName = (st.trainerClassName and st.trainerClassName ~= "")
and (st.trainerClassName .. " " .. (st.trainerName or ""))
or (st.trainerName or "TRAINER")
local pname = st.playerName or "PLAYER"
Ui.push(string.format("%s defeated\n%s!", pname, trName))
local Prize = require("src.core.game3.battle.prize")
local Runtime = package.loaded["src.core.game3.runtime"]
local session = Runtime and Runtime.getSession and Runtime.getSession()
if session then
local info = Trainers.info(st.trainerId)
local lastLevel = info and tonumber(info.lastLevel)
if not lastLevel or lastLevel < 1 then
lastLevel = st.enemy and st.enemy.mon and tonumber(st.enemy.mon.level) or 1
end
local gained = Prize.awardTrainerWin(session, st.trainerId, {
lastLevel = lastLevel,
double = st.double or false,
moneyMultiplier = st.moneyMultiplier or 1,
})
if gained > 0 then
Ui.push(Prize.moneyMessage(session.name or pname, gained))
end
end
push_lose_text_and_money()
begin_evo_or_end()
end,
})
Battle._phase = "switching"
else
if not st.wild and st.trainerId then
local Trainers = require("src.core.game3.scripting.trainers")
local dialogs = Trainers.dialogs(st.trainerId)
local defeatSpeech = st.defeatText or (dialogs and dialogs.defeat)
if defeatSpeech and defeatSpeech ~= "" then
Ui.push(defeatSpeech)
end
local trName = (st.trainerClassName and st.trainerClassName ~= "")
and (st.trainerClassName .. " " .. (st.trainerName or ""))
or (st.trainerName or "TRAINER")
local pname = st.playerName or "PLAYER"
Ui.push(string.format("%s defeated\n%s!", pname, trName))
local Prize = require("src.core.game3.battle.prize")
local Runtime = package.loaded["src.core.game3.runtime"]
local session = Runtime and Runtime.getSession and Runtime.getSession()
if session then
local info = Trainers.info(st.trainerId)
local lastLevel = info and tonumber(info.lastLevel)
if not lastLevel or lastLevel < 1 then
lastLevel = st.enemy and st.enemy.mon and tonumber(st.enemy.mon.level) or 1
end
local gained = Prize.awardTrainerWin(session, st.trainerId, {
lastLevel = lastLevel,
double = st.double or false,
moneyMultiplier = st.moneyMultiplier or 1,
})
if gained > 0 then
Ui.push(Prize.moneyMessage(session.name or pname, gained))
end
end
else
Ui.push("You won the battle!")
push_defeated()
push_lose_text_and_money()
end
-- pokefirered/src/battle_main.c:3764
begin_evo_or_end()
end
else
-8
View File
@@ -237,14 +237,6 @@ function LearnMove.begin(opts)
say(LearnMove._name .. " learned\n" .. LearnMove._moveName .. "!")
finish(ok)
else
-- Two contexts call LearnMove.begin:
-- * Battle (Ui.push): pushMsg ignores the callback → pump() watches
-- Ui.dialogPending() and calls finish() once the message is dismissed.
-- * Party menu / TM use (PartyMenu.showMessage): pushMsg DOES call cb
-- when the player presses A → finish() fires inline via the callback.
-- Both are handled: set _pending="done" for the pump() path, and also
-- pass a callback to say() for the direct path. The guard prevents
-- double-finish if both somehow fire.
LearnMove._pendingResult = ok
LearnMove._pending = "done"
say(LearnMove._name .. " learned\n" .. LearnMove._moveName .. "!", function()
+69 -11
View File
@@ -32,6 +32,7 @@ Ui._menuIndex = 1
Ui._moveIndex = 1
Ui._st = nil
Ui._pendingCommand = nil
Ui._pendingYesNo = nil
Ui._session = nil
-- pret sBattlerCoords (singles) — CreateSprite CENTER before pic y_offset
@@ -70,6 +71,7 @@ function Ui.reset(opts)
Ui._moveIndex = 1
Ui._st = nil
Ui._pendingCommand = nil
Ui._pendingYesNo = nil
if not Ui._headless then
pcall(BattleChrome.install, nil)
end
@@ -84,10 +86,13 @@ function Ui.bindSession(session)
Ui._session = session
end
function Ui.push(text)
if not text or text == "" then return end
function Ui.push(text, cb)
if not text or text == "" then
if cb then cb() end
return
end
Ui._log[#Ui._log + 1] = text
Ui._queue[#Ui._queue + 1] = text
Ui._queue[#Ui._queue + 1] = cb and { text = text, cb = cb } or text
end
function Ui.busy()
@@ -108,7 +113,9 @@ function Ui.dialogPending()
end
--- YES/NO during award/learn (Choice.yesNo). cb(true|false)
function Ui.askYesNo(cb)
function Ui.askYesNo(a, b)
local cb = (type(a) == "function") and a or b
local prompt = (type(a) == "string") and a or nil
if Ui._headless then
if cb then cb(false) end
return
@@ -117,9 +124,37 @@ function Ui.askYesNo(cb)
if cb then cb(false) end
return
end
if prompt and prompt ~= "" and Message and Message.show then
Ui._log[#Ui._log + 1] = prompt
Message.show(prompt, { frame = "battle", battle = true, stay = true })
-- pokefirered/data/battle_scripts_1.s:3127
Ui._pendingYesNo = cb or false
return
end
Choice.yesNo(cb, { left = 22, top = 8 })
end
local function open_pending_yesno()
if Ui._pendingYesNo == nil then return false end
if not (Message and Message.isOpen and Message.isOpen()) then
Ui._pendingYesNo = nil
return false
end
if not (Message.isWaiting and Message.isWaiting()) then return true end
local cb = Ui._pendingYesNo
Ui._pendingYesNo = nil
-- pokefirered/data/battle_scripts_1.s:3129
Choice.yesNo(function(yes)
if Message.isOpen() and Message.close then Message.close() end
if cb then cb(yes) end
end, { left = 22, top = 8 })
return true
end
function Ui.yesNoPending()
return Ui._pendingYesNo ~= nil
end
--- Multi-choice forget list. cb(0-based index) or cb(-1)/cb(127) on cancel.
function Ui.askForget(labels, cb)
if Ui._headless then
@@ -231,11 +266,20 @@ function Ui.takeCommand()
return c
end
local function pop_queue()
local item = table.remove(Ui._queue, 1)
if type(item) == "table" then return item.text, item.cb end
return item, nil
end
local function show_next()
if Ui._showing then return end
if #Ui._queue == 0 then return end
local text = table.remove(Ui._queue, 1)
if Ui._headless then return end
local text, cb = pop_queue()
if Ui._headless then
if cb then cb() end
return
end
if Message and Message.show then
Ui._showing = true
Message.show(text, {
@@ -243,17 +287,24 @@ local function show_next()
battle = true,
done = function()
Ui._showing = false
if cb then cb() end
end,
})
elseif cb then
cb()
end
end
function Ui.pump()
if Ui._headless then
while #Ui._queue > 0 do table.remove(Ui._queue, 1) end
while #Ui._queue > 0 do
local _, cb = pop_queue()
if cb then cb() end
end
Ui._showing = false
return true
end
if open_pending_yesno() then return false end
if Message and Message.isOpen and Message.isOpen() then
return false
end
@@ -411,8 +462,8 @@ local function draw_mon_sprite(battler, base, back)
love.graphics.setColor(shade, shade, shade, a)
end
local hFlip = (pres and pres.hFlip) and true or false
local sx = (hFlip and -1 or 1) * ((pres and pres.sx) or scale)
local sy = (pres and pres.sy) or scale
local sx = (hFlip and -1 or 1) * scale * ((pres and pres.sx) or 1)
local sy = scale * ((pres and pres.sy) or 1)
love.graphics.draw(entry.image, cx, cy, 0, sx, sy, 32, 32)
else
-- Placeholder silhouette so lunge/shake is visible before full pic extract.
@@ -489,7 +540,7 @@ local function draw_move_menu(st)
end
local function draw_trainer_sprites(stage)
local function draw_enemy_trainer(stage)
if not stage or not stage.trainer then return end
local TrainerPic = require("src.core.game3.trainer_pic")
local te = stage.trainer.enemy
@@ -503,6 +554,11 @@ local function draw_trainer_sprites(stage)
end
end
end
end
local function draw_player_trainer(stage)
if not stage or not stage.trainer then return end
local TrainerPic = require("src.core.game3.trainer_pic")
local tp = stage.trainer.player
if tp and tp.visible then
local entry = TrainerPic.back(tp.gender or 0)
@@ -691,12 +747,14 @@ function Ui.draw(w, h)
-- 3. In front of Enemy / Behind Player / Mid-field (Z: 101 .. 199)
-- 4. Player Mon (Z: 200)
-- 5. In front of Player & Global Foreground (Z: 201 .. 999)
draw_trainer_sprites(stage)
draw_enemy_trainer(stage)
Anim.drawParticles(0, 99)
if st then
draw_mon_sprite(st.enemy, ENEMY_MON, false)
end
Anim.drawParticles(101, 199)
-- pokefirered/src/battle_anim_mons.c:1908
draw_player_trainer(stage)
if st then
draw_mon_sprite(st.player, PLAYER_MON, true)
end
+29 -15
View File
@@ -30,7 +30,7 @@ function Display.ensureCanvas(which)
return existing
end
end
local ok, canvas = pcall(love.graphics.newCanvas, Display.W, Display.H)
local ok, canvas = pcall(love.graphics.newCanvas, Display.W, Display.H, { dpiscale = 1 })
if not ok or not canvas then
log("FAILED canvas create")
return nil
@@ -63,28 +63,42 @@ function Display.fit(winW, winH)
end
end
local dpiX, dpiY = 1, 1
if gw > 0 and gh > 0 and love.graphics.getPixelDimensions then
local fw, fh = love.graphics.getPixelDimensions()
if fw and fw > 0 then dpiX = fw / gw end
if fh and fh > 0 then dpiY = fh / gh end
elseif love and love.graphics and love.graphics.getDPIScale then
local d = tonumber(love.graphics.getDPIScale())
if d and d > 1e-6 then dpiX, dpiY = d, d end
end
local isPortrait = safeH > safeW
local scale, ox, oy, pw, ph
local k, ox, oy, pw, ph, scaleX, scaleY
if isPortrait then
-- On mobile portrait, scale to fit the available safe width cleanly.
scale = safeW / Display.W
pw = Display.W * scale
ph = Display.H * scale
ox = safeX + math.floor((safeW - pw) * 0.5)
k = math.max(1, math.floor(safeW * dpiX / Display.W + 1e-9))
scaleX, scaleY = k / dpiX, k / dpiY
pw = Display.W * scaleX
ph = Display.H * scaleY
ox = safeX + (safeW - pw) * 0.5
-- In portrait, center the screen in the upper deck area (above the touch controls deck).
local topDeckH = safeH * 0.48
oy = safeY + math.max(4, math.floor((topDeckH - ph) * 0.5))
oy = safeY + math.max(4, (topDeckH - ph) * 0.5)
else
-- Landscape / desktop: fit cleanly within safe area
scale = math.max(1, math.floor(math.min(safeW / Display.W, safeH / Display.H)))
pw = Display.W * scale
ph = Display.H * scale
ox = safeX + math.floor((safeW - pw) * 0.5)
oy = safeY + math.floor((safeH - ph) * 0.5)
k = math.max(1, math.floor(math.min(safeW * dpiX / Display.W, safeH * dpiY / Display.H) + 1e-9))
scaleX, scaleY = k / dpiX, k / dpiY
pw = Display.W * scaleX
ph = Display.H * scaleY
ox = safeX + (safeW - pw) * 0.5
oy = safeY + (safeH - ph) * 0.5
end
ox = math.floor(ox * dpiX + 1e-9) / dpiX
oy = math.floor(oy * dpiY + 1e-9) / dpiY
return scale, ox, oy, pw, ph
return scaleX, ox, oy, pw, ph, scaleY
end
local function beginOn(canvas)
@@ -194,9 +208,9 @@ function Display.present(game, winW, winH)
-- Void bars + blit our frame (game3 letterbox, not Gen2 Playfield).
love.graphics.setColor(0.02, 0.04, 0.08, 1)
love.graphics.rectangle("fill", 0, 0, winW, winH)
local scale, ox, oy = Display.fit(winW, winH)
local scale, ox, oy, _, _, scaleY = Display.fit(winW, winH)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(canvas, ox, oy, 0, scale, scale)
love.graphics.draw(canvas, ox, oy, 0, scale, scaleY)
return true
end
+5
View File
@@ -65,6 +65,9 @@ function Schema.newGame(opts)
local Rng = require("src.core.game3.rng")
session.trainerId = Rng.seedNewGame({ seed = opts.rngSeed })
Rng.captureToSession(session)
local Storage = require("src.core.game3.storage")
session.storage = Storage.new()
session.storage.items[1] = { id = 13, qty = 1 } -- pokefirered/src/player_pc.c:100
Options.ensure(session)
-- Plan naming: text_speed / l_equals_a aliases mirror Options fields.
session.options.text_speed = session.options.textSpeed
@@ -104,6 +107,7 @@ function Schema.toSaveTable(session)
playTime = session.playtime or session.playTime or { hours = 0, minutes = 0, seconds = 0 },
options = session.options,
pc = session.pc,
storage = session.storage and require("src.core.game3.storage").serialize(session.storage) or nil,
registeredItem = session.registeredItem,
move_overlay = session.move_overlay or {},
trainerId = session.trainerId,
@@ -144,6 +148,7 @@ function Schema.fromSaveTable(save)
playtime = save.playTime or save.playtime or { hours = 0, minutes = 0, seconds = 0 },
options = save.options,
pc = save.pc or { items = {} },
storage = type(save.storage) == "table" and require("src.core.game3.storage").deserialize(save.storage) or nil,
registeredItem = save.registeredItem,
move_overlay = save.move_overlay or {},
trainerId = save.trainerId,
+4 -1
View File
@@ -584,7 +584,7 @@ function Adapters.host(mod, game, world)
end
finish()
end,
openPc = function(done)
openPc = function(done, pcOpts)
local function finish()
local okMsg, Message = pcall(require, "src.ui.game3.message")
if okMsg and Message and Message.close then Message.close() end
@@ -600,9 +600,12 @@ function Adapters.host(mod, game, world)
if okHud and Hud and Hud.clearWaitButton then Hud.clearWaitButton() end
a.log("[game3] openPc via game3 PcMenu")
local PcMenu = require("src.ui.game3.pc_menu")
local bedroom = type(pcOpts) == "table" and pcOpts.bedroom == true
PcMenu.show({
session = Runtime.getSession(),
onClose = finish,
startMode = bedroom and "player_pc" or nil,
closeOnExit = bedroom,
})
return
end
+7
View File
@@ -31,6 +31,13 @@ Natives.ALLOW = {
if not (adapters and adapters.openPc) then return false end
return yield_host(ctx, adapters, adapters.openPc)
end,
-- pokefirered/src/player_pc.c:151
["special:" .. Std.SPECIAL.BedroomPC] = function(ctx, adapters)
if not (adapters and adapters.openPc) then return false end
return yield_host(ctx, adapters, function(done)
adapters.openPc(done, { bedroom = true })
end)
end,
["special:" .. Std.SPECIAL.AnimatePcTurnOn] = function() return false end,
["special:" .. Std.SPECIAL.AnimatePcTurnOff] = function() return false end,
["special:" .. Std.SPECIAL.CreatePCMenu] = function(ctx, adapters)
+1
View File
@@ -22,6 +22,7 @@ Std.SPECIAL = {
ShowRegionMap = 0xAF,
AnimatePcTurnOn = 0xD6,
AnimatePcTurnOff = 0xD7,
BedroomPC = 0xF9, -- pokefirered/data/specials.inc:260
PlayerPC = 0xFA,
CreatePCMenu = 0x106,
EnterHallOfFame = 0x110, -- 272 (special HallOfFame / GameClear)
+189 -74
View File
@@ -2,7 +2,7 @@
-- Oak onboarding lives in oak_speech.lua (pret oak_speech.c task chain).
local Display = require("src.core.game3.display")
local FrlgFont = require("src.ui.game3.frlg_font")
local Window = require("src.ui.game3.window")
local Audio = require("src.core.game3.audio")
local OakSpeech = require("src.ui.game3.oak_speech")
local IntroGuide = require("src.ui.game3.intro_guide")
@@ -16,6 +16,7 @@ Boot.PHASE = {
INTRO = "intro",
COPYRIGHT = "copyright",
TITLE = "title",
TITLE_CRY = "title_cry",
MENU = "menu",
CONTROLS = "controls",
PIKACHU = "pikachu",
@@ -137,11 +138,62 @@ function Boot.setHasContinue(state, yes)
state.menuIndex = 1
end
function Boot.setContinueInfo(state, info)
state.continueInfo = info
end
function Boot.continueInfoFromSave(save)
if type(save) ~= "table" then return nil end
local Flags = require("src.core.game3.scripting.flags")
local store = { flags = type(save.flags) == "table" and save.flags or {} }
local pt = type(save.playTime) == "table" and save.playTime
or type(save.playtime) == "table" and save.playtime or {}
local dex = type(save.dex) == "table" and save.dex or {}
local caught = dex.caught or dex.owned or {}
local counted, n = {}, 0
for sp, on in pairs(caught) do
local id = tonumber(sp)
if id and on and on ~= 0 and not counted[id] and (dex.national or id <= 151) then
counted[id] = true
n = n + 1
end
end
local name = tostring(save.name or save.playerName or "")
return {
name = name:sub(1, 7),
gender = tonumber(save.gender) or 0,
hours = tonumber(pt.hours) or 0,
minutes = tonumber(pt.minutes) or 0,
hasDex = Flags.getFlag(store, nil, Flags.IDS.SYS_POKEDEX_GET) == true,
dexCount = n,
badges = Flags.countBadges(store),
}
end
local function menuItems(state)
if state.hasContinue then
return { "CONTINUE", "NEW GAME", "OPTION" }
return { "CONTINUE", "NEW GAME" }
end
return { "NEW GAME", "OPTION" }
return { "NEW GAME" }
end
Boot.menuItems = menuItems
local function beginNewGame(state)
NamingChrome.install()
state.phase = Boot.PHASE.CONTROLS
state.guide = IntroGuide.beginControls(state.assets)
IntroGuide.start(state.guide)
state.timer = 0
Audio.playSong(323)
return nil
end
local function beginMenuFade(state, color, from, to, after)
state.fadeColor = color
state.fadeT = from
state.fadeTarget = to
state.fadeThen = after
end
local function enterTitle(state)
@@ -199,13 +251,12 @@ function Boot.update(state, input, dt)
if state.phase == Boot.PHASE.TITLE then
TitleScreen.update(state, dt)
if a() then
leaveTitle(state)
state.phase = Boot.PHASE.MENU
state.timer = 0
Audio.playSe(5)
end
-- Idle timeout: cycle back to intro after 30s
if state.timer > 30.0 then
state.phase = Boot.PHASE.TITLE_CRY
state.cryFrames = 0
state.white = 0
state.whiteFading = false
Audio.playCry(6, 0) -- pokefirered/src/title_screen.c:715
elseif state.timer >= 2700 / 60 then -- pokefirered/src/title_screen.c:435
leaveTitle(state)
state.phase = Boot.PHASE.INTRO
state.introMovie = IntroMovie.new(state.assets)
@@ -214,31 +265,72 @@ function Boot.update(state, input, dt)
return nil
end
if state.phase == Boot.PHASE.MENU then
local items = menuItems(state)
if up() then
state.menuIndex = state.menuIndex - 1
if state.menuIndex < 1 then state.menuIndex = #items end
Audio.playSe(5)
elseif down() then
state.menuIndex = state.menuIndex + 1
if state.menuIndex > #items then state.menuIndex = 1 end
Audio.playSe(5)
elseif a() then
local choice = items[state.menuIndex]
Audio.playSe(5)
if choice == "CONTINUE" then
return { action = "continue" }
elseif choice == "NEW GAME" then
NamingChrome.install()
state.phase = Boot.PHASE.CONTROLS
state.guide = IntroGuide.beginControls(state.assets)
IntroGuide.start(state.guide)
state.timer = 0
Audio.playSong(323)
elseif choice == "OPTION" then
print("[game3/boot] OPTION selected (stub)")
if state.phase == Boot.PHASE.TITLE_CRY then
TitleScreen.update(state, dt)
if not state.whiteFading then
if state.cryFrames < 90 then -- pokefirered/src/title_screen.c:722
state.cryFrames = state.cryFrames + 1
else
state.whiteFading = true
Audio.fadeOutBgm(4) -- pokefirered/src/title_screen.c:728
end
elseif state.white < 16 then
state.white = math.min(16, state.white + 2) -- pokefirered/src/palette.c:162
else
leaveTitle(state)
state.whiteFading = false
state.timer = 0
if not state.hasContinue then -- pokefirered/src/main_menu.c:317
return beginNewGame(state)
end
state.phase = Boot.PHASE.MENU
state.menuIndex = 1
beginMenuFade(state, "white", 16, 0, nil) -- pokefirered/src/main_menu.c:398
end
return nil
end
if state.phase == Boot.PHASE.MENU then
local t, target = state.fadeT or 0, state.fadeTarget or 0
if t ~= target then
if t < target then
state.fadeT = math.min(target, t + 2)
else
state.fadeT = math.max(target, t - 2)
end
return nil
end
local pending = state.fadeThen
if pending then
state.fadeThen = nil
if pending == "continue" then
state.fadeT, state.fadeTarget = 0, 0
return { action = "continue" }
elseif pending == "new_game" then
state.fadeT, state.fadeTarget = 0, 0
return beginNewGame(state)
elseif pending == "title" then
state.fadeT, state.fadeTarget = 0, 0
state.phase = Boot.PHASE.TITLE
state.timer = 0
enterTitle(state)
Audio.playSong(278) -- pokefirered/src/title_screen.c:389
end
return nil
end
local items = menuItems(state)
local pressed = function(k) return input and input.wasPressed and input:wasPressed(k) end
if pressed("a") then -- pokefirered/src/main_menu.c:570
Audio.playSe(5)
local choice = items[state.menuIndex]
beginMenuFade(state, "black", 0, 16, choice == "CONTINUE" and "continue" or "new_game")
elseif pressed("b") then -- pokefirered/src/main_menu.c:577
Audio.playSe(5)
beginMenuFade(state, "black", 0, 16, "title")
elseif up() and state.menuIndex > 1 then
state.menuIndex = state.menuIndex - 1
elseif down() and state.menuIndex < #items then
state.menuIndex = state.menuIndex + 1
end
return nil
end
@@ -271,15 +363,58 @@ function Boot.update(state, input, dt)
return nil
end
local function drawText(str, x, y, kind)
kind = kind or "NORMAL"
local colors = FrlgFont.COLOR and FrlgFont.COLOR[kind] or nil
if FrlgFont.draw then
FrlgFont.draw(str, x, y, { colors = colors, maxWidth = 220 })
else
love.graphics.setColor(1, 1, 1, 1)
love.graphics.print(str, x, y)
local MENU_BG = { 139 / 255, 148 / 255, 255 / 255 }
local MENU_TEXT = { 98 / 255, 98 / 255, 98 / 255, 1 }
local MENU_SHADOW = { 213 / 255, 213 / 255, 205 / 255, 1 }
local MENU_FILL = { 1, 1, 1, 1 }
local ACCENT_MALE = { 4 / 31, 16 / 31, 31 / 31, 1 }
local ACCENT_FEMALE = { 31 / 31, 3 / 31, 21 / 31, 1 }
local WIN0V = { { 0x02, 0x5E }, { 0x62, 0x7E } }
local function darkenOutside(W, H, x0, y0, x1, y1)
love.graphics.setColor(0, 0, 0, 7 / 16) -- pokefirered/src/main_menu.c:231
love.graphics.rectangle("fill", 0, 0, W, y0)
love.graphics.rectangle("fill", 0, y1, W, H - y1)
love.graphics.rectangle("fill", 0, y0, x0, y1 - y0)
love.graphics.rectangle("fill", x1, y0, W - x1, y1 - y0)
end
local function drawMainMenu(state, W, H)
love.graphics.clear(MENU_BG[1], MENU_BG[2], MENU_BG[3], 1) -- pokefirered/src/main_menu.c:199
local info = state.continueInfo or {}
local head = { fg = MENU_TEXT, shadow = MENU_SHADOW, bg = MENU_FILL }
local stat = {
fg = (info.gender == 1) and ACCENT_FEMALE or ACCENT_MALE, -- pokefirered/src/main_menu.c:342
shadow = MENU_SHADOW,
bg = MENU_FILL,
}
Window.stdFrame(Window.template(3, 1, 24, 10)) -- pokefirered/src/main_menu.c:84
Window.stdFrame(Window.template(3, 13, 24, 2)) -- pokefirered/src/main_menu.c:93
local x, y = 24, 8
Window.printPx("CONTINUE", x + 2, y + 2, { colors = head })
Window.printPx("PLAYER", x + 2, y + 18, { colors = stat }) -- pokefirered/src/main_menu.c:623
Window.printPx(info.name or "", x + 62, y + 18, { colors = stat })
Window.printPx("TIME", x + 2, y + 34, { colors = stat }) -- pokefirered/src/main_menu.c:636
Window.printPx(string.format("%d:%02d", info.hours or 0, info.minutes or 0), x + 62, y + 34, { colors = stat })
if info.hasDex then -- pokefirered/src/main_menu.c:648
Window.printPx("POKéDEX", x + 2, y + 50, { colors = stat })
Window.printPx(tostring(info.dexCount or 0), x + 62, y + 50, { colors = stat })
end
Window.printPx("BADGES", x + 2, y + 66, { colors = stat }) -- pokefirered/src/main_menu.c:672
Window.printPx(tostring(info.badges or 0), x + 62, y + 66, { colors = stat })
Window.printPx("NEW GAME", 24 + 2, 104 + 2, { colors = head })
local rows = WIN0V[state.menuIndex] or WIN0V[1] -- pokefirered/src/main_menu.c:565
darkenOutside(W, H, 18, rows[1], 222, rows[2])
local t = state.fadeT or 0
if t > 0 then
if state.fadeColor == "white" then
love.graphics.setColor(1, 1, 1, t / 16)
else
love.graphics.setColor(0, 0, 0, t / 16)
end
love.graphics.rectangle("fill", 0, 0, W, H)
end
love.graphics.setColor(1, 1, 1, 1)
end
function Boot.draw(state)
@@ -296,39 +431,19 @@ function Boot.draw(state)
return
end
if state.phase == Boot.PHASE.TITLE_CRY then
TitleScreen.draw(state)
local white = state.white or 0
if white > 0 then
love.graphics.setColor(1, 1, 1, white / 16) -- pokefirered/src/title_screen.c:726
love.graphics.rectangle("fill", 0, 0, W, H)
love.graphics.setColor(1, 1, 1, 1)
end
return
end
if state.phase == Boot.PHASE.MENU then
-- Static title underlay (no particles) + dim
if state.titleBorder then
love.graphics.setColor(255 / 255, 255 / 255, 139 / 255, 1)
love.graphics.rectangle("fill", 0, 0, W, H)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(state.titleBorder, 0, 0)
if state.titleMon then love.graphics.draw(state.titleMon, 0, 0) end
if state.copyrightLayer then love.graphics.draw(state.copyrightLayer, 0, 0) end
if state.titleLogo then love.graphics.draw(state.titleLogo, 0, 0) end
elseif state.titleScreen then
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(state.titleScreen, 0, 0)
else
love.graphics.setColor(0.1, 0.2, 0.4, 1)
love.graphics.rectangle("fill", 0, 0, W, H)
end
love.graphics.setColor(0, 0, 0, 0.55)
love.graphics.rectangle("fill", 0, 0, W, H)
local items = menuItems(state)
local boxX, boxY = 56, 48
love.graphics.setColor(0.95, 0.95, 0.95, 1)
love.graphics.rectangle("fill", boxX, boxY, 128, 16 + #items * 18)
love.graphics.setColor(0.2, 0.35, 0.7, 1)
love.graphics.rectangle("line", boxX, boxY, 128, 16 + #items * 18)
for i, label in ipairs(items) do
local y = boxY + 8 + (i - 1) * 18
love.graphics.setColor(0.1, 0.1, 0.15, 1)
if i == state.menuIndex then
drawText(">", boxX + 8, y)
end
drawText(label, boxX + 24, y)
end
drawMainMenu(state, W, H)
return
end
+2 -2
View File
@@ -55,8 +55,8 @@ function MonPic.draw()
if not MonPic.active then return end
local tx = MonPic.left or 10
local ty = MonPic.top or 3
-- pret CreateWindowFromRect(x, y, 8, 8) then sprite at 8*x+40, 8*y+40.
Window.stdFrame(Window.template(tx, ty, 8, 8))
-- pokefirered/src/script_menu.c:1193
Window.stdFrame(Window.template(tx + 1, ty + 1, 8, 8))
local cx = tx * Display.TILE + 40
local cy = ty * Display.TILE + 40
if MonPic._img then
+24 -10
View File
@@ -49,9 +49,15 @@ function PcMenu.show(opts)
PcMenu.open = true
PcMenu._session = opts.session
PcMenu._onClose = opts.onClose
PcMenu.mode = "root"
PcMenu._closeOnExit = opts.closeOnExit == true
PcMenu.cursor = 1
PcMenu._status = "Which PC would you like to access?"
if opts.startMode == "player_pc" then
PcMenu.mode = "player_pc"
PcMenu._status = "What would you like to do?" -- pokefirered/src/player_pc.c:160
else
PcMenu.mode = "root"
PcMenu._status = "Which PC would you like to access?"
end
Storage.ensure(PcMenu._session)
se(2) -- SE_PC_ON / SE_PC_LOGIN
Stack.push("pc_menu", PcMenu, { hideBelow = false })
@@ -261,10 +267,14 @@ function PcMenu.handleInput(input)
elseif input:wasPressed("a") then
local choice = playerOptions[PcMenu.cursor]
if choice.id == "logoff" then
PcMenu.mode = "root"
PcMenu.cursor = 2
PcMenu._status = "Which PC would you like to access?"
se(5)
if PcMenu._closeOnExit then
PcMenu.close() -- pokefirered/src/player_pc.c:257
else
PcMenu.mode = "root"
PcMenu.cursor = 2
PcMenu._status = "Which PC would you like to access?"
se(5)
end
elseif choice.id == "withdraw" then
local storage = Storage.ensure(PcMenu._session)
if #storage.items < 1 then
@@ -301,10 +311,14 @@ function PcMenu.handleInput(input)
end
end
elseif input:wasPressed("b") then
PcMenu.mode = "root"
PcMenu.cursor = 2
PcMenu._status = "Which PC would you like to access?"
se(5)
if PcMenu._closeOnExit then
PcMenu.close()
else
PcMenu.mode = "root"
PcMenu.cursor = 2
PcMenu._status = "Which PC would you like to access?"
se(5)
end
end
return
end
+86
View File
@@ -0,0 +1,86 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp"
return function(game)
local fails = 0
local function result(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then fails = fails + 1 end
end
for _ = 1, 600 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
game:_handleBootAction({ action = "new_game", name = "RED" })
U.wait(180)
local Runtime = require("src.core.game3.runtime")
local Party = require("src.core.game3.party")
local session = Runtime.getSession()
session.party = {}
Party.giveMon(session, 4, 5)
local BattleBridge = require("src.core.game3.battle_bridge")
local species = tonumber(os.getenv("U10_SPECIES") or "") or 95
local ok, err = BattleBridge.startWild(Runtime._mod, game, { species = species, level = 3 }, { fade = false })
result(ok == true, "u10 wild battle started " .. tostring(err or ""))
if not ok then love.event.quit(1) return end
local Anim = require("src.core.game3.battle.anim")
local TrainerPic = require("src.core.game3.trainer_pic")
local Pokemon = require("src.core.game3.pokemon")
local function waitEnemyOx(target)
for _ = 1, 900 do
local p = Anim.present("enemy")
if p and p.visible ~= false and (p.ox or 0) >= target then return true end
U.wait(1)
end
return false
end
local function recordOrder()
local monEntry = Pokemon.frontPic(species)
local backEntry = TrainerPic.back(Anim.stage().trainer.player.gender or 0)
local monImg = monEntry and monEntry.image
local backImg = backEntry and backEntry.image
local order = {}
local realDraw = love.graphics.draw
love.graphics.draw = function(img, ...)
if img == monImg then order[#order + 1] = "mon" end
if img == backImg then order[#order + 1] = "back" end
return realDraw(img, ...)
end
U.wait(2)
love.graphics.draw = realDraw
local iMon, iBack
for i, t in ipairs(order) do
if t == "mon" and not iMon then iMon = i end
if t == "back" and not iBack then iBack = i end
end
return iMon, iBack
end
local reached = waitEnemyOx(-48)
result(reached, "u10 enemy mon reached crossing ox -48")
if reached then
local tp = Anim.stage().trainer.player
result(tp.visible == true, "u10 player trainer back pic visible at crossing")
local iMon, iBack = recordOrder()
result(iMon ~= nil and iBack ~= nil and iMon < iBack, "u10 enemy mon drawn under trainer back pic")
U.shot(game, DIR .. "/u10_01_cap_over_mon_crossing.png")
end
reached = waitEnemyOx(-32)
if reached then
U.shot(game, DIR .. "/u10_02_cap_brim_over_mon_tail.png")
end
reached = waitEnemyOx(0)
result(reached, "u10 intro slide finished")
if reached then
U.shot(game, DIR .. "/u10_03_slide_settled.png")
end
love.event.quit(fails == 0 and 0 or 1)
end
+106
View File
@@ -0,0 +1,106 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp"
local failures = 0
local function result(ok, label)
if ok then
print("PASS " .. label)
else
failures = failures + 1
print("FAIL " .. label)
end
return ok
end
local function mods()
return package.loaded["src.ui.game3.pc_menu"], package.loaded["src.ui.game3.message"],
package.loaded["src.core.game3.scripting.space"]
end
local function waitFor(pred, n)
for _ = 1, n do
if pred() then return true end
U.wait(1)
end
return pred()
end
local function finish()
if failures == 0 then
print("PASS u1_bedroom_pc")
love.event.quit(0)
else
print("FAIL u1_bedroom_pc failures=" .. failures)
love.event.quit(1)
end
end
return function(game)
U.wait(30)
game:_handleBootAction({ action = "new_game", name = "RED" })
U.wait(120)
local Map = require("src.core.game3.map")
Map.load(nil, game, "FR_PLAYERS_HOUSE_2F", { x = 1, y = 2, facing = "up" })
game.session.x, game.session.y, game.session.facing = 1, 2, "up"
U.wait(90)
local st = game.session.storage
result(st and st.items[1] and st.items[1].id == 13 and st.items[1].qty == 1, "new game PC holds POTION x1")
U.tap(game, "a")
local booted = waitFor(function()
local _, Message = mods()
return Message and Message.isOpen and Message.isOpen()
end, 120)
U.wait(20)
if result(booted, "booted up the PC message") then
U.shot(game, DIR .. "/u1_01_booted_pc_msg.png")
end
local PcMenu
local opened = false
for _ = 1, 6 do
U.tap(game, "a")
opened = waitFor(function()
PcMenu = mods()
return PcMenu and PcMenu.isOpen and PcMenu.isOpen()
end, 60)
if opened then break end
end
U.wait(10)
local Natives = package.loaded["src.core.game3.scripting.natives"]
result(not (Natives and Natives._logged and Natives._logged["special:249"]), "special 0xF9 not skipped")
if not result(opened and PcMenu.mode == "player_pc", "bedroom PC opens straight to player PC menu") then
return finish()
end
U.shot(game, DIR .. "/u1_02_player_pc_menu.png")
U.tap(game, "a")
U.wait(15)
if result(PcMenu.mode == "withdraw_item", "WITHDRAW ITEM lists PC items") then
U.shot(game, DIR .. "/u1_03_withdraw_list_potion.png")
end
U.tap(game, "a")
U.wait(15)
local Bag = require("src.core.game3.bag")
local got = Bag.has(game.session.bag, 13, 1)
if result(PcMenu.mode == "msg" and got, "POTION withdrawn into bag") then
U.shot(game, DIR .. "/u1_04_withdrew_potion.png")
end
U.tap(game, "a")
U.wait(15)
U.tap(game, "b")
local closedOk = waitFor(function()
local _, Message, Space = mods()
local running = Space and Space.vm and Space.vm.isRunning and Space.vm:isRunning()
return not PcMenu.isOpen() and not running and not (Message and Message.isOpen and Message.isOpen())
end, 120)
U.wait(20)
if result(closedOk, "B turns the PC off and returns to the field") then
U.shot(game, DIR .. "/u1_05_pc_off_field.png")
end
finish()
end
@@ -0,0 +1,64 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp"
local failures = 0
local function result(ok, label)
if ok then
print("PASS " .. label)
else
failures = failures + 1
print("FAIL " .. label)
end
return ok
end
local function finish()
if failures == 0 then
print("PASS u2_monpic_centered")
love.event.quit(0)
else
print("FAIL u2_monpic_centered failures=" .. failures)
love.event.quit(1)
end
end
return function(game)
U.wait(30)
game:_handleBootAction({ action = "new_game", name = "RED" })
U.wait(120)
local Map = require("src.core.game3.map")
Map.load(nil, game, "FR_OAKS_LAB", { x = 8, y = 5, facing = "up" })
game.session.x, game.session.y, game.session.facing = 8, 5, "up"
U.wait(90)
local Chrome = require("src.ui.game3.chrome")
local MonPic = require("src.ui.game3.mon_pic")
local frame
local origStdFrame = Chrome.stdFrame
Chrome.stdFrame = function(tx, ty, tw, th)
if MonPic.active and tw == 8 and th == 8 then
frame = { tx, ty, tw, th }
end
return origStdFrame(tx, ty, tw, th)
end
MonPic.show(1, 10, 3)
U.wait(20)
Chrome.stdFrame = origStdFrame
result(MonPic.active and MonPic._img ~= nil, "showmonpic 10,3 opened with a front pic")
if not result(frame ~= nil, "pic window frame drawn") then
return finish()
end
result(frame[1] == 11 and frame[2] == 4,
("window content at tiles (11,4), got (%s,%s)"):format(tostring(frame[1]), tostring(frame[2])))
local picCx, picCy = MonPic.left * 8 + 40, MonPic.top * 8 + 40
local frameCx, frameCy = (frame[1] + 4) * 8, (frame[2] + 4) * 8
if result(picCx == frameCx and picCy == frameCy,
("pic center (%d,%d) == window center (%d,%d)"):format(picCx, picCy, frameCx, frameCy)) then
U.shot(game, DIR .. "/u2_01_lab_starter_pic_centered.png")
end
MonPic.hide()
finish()
end
+120
View File
@@ -0,0 +1,120 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp"
local function fail(label)
print("FAIL " .. label)
love.event.quit(1)
while true do coroutine.yield() end
end
local function px8(img, x, y)
local r, g, b, a = img:getPixel(x, y)
return math.floor(r * 255 + 0.5), math.floor(g * 255 + 0.5), math.floor(b * 255 + 0.5), math.floor(a * 255 + 0.5)
end
local function render(fn)
local canvas = love.graphics.newCanvas(240, 160)
canvas:setFilter("nearest", "nearest")
love.graphics.push("all")
love.graphics.setCanvas(canvas)
love.graphics.origin()
love.graphics.clear(0, 0, 0, 0)
love.graphics.setColor(1, 1, 1, 1)
fn()
love.graphics.pop()
return canvas:newImageData()
end
local function key(r, g, b) return r .. "," .. g .. "," .. b end
local function check_region(label, raw, full, tlX, tlY, x0, x1, y0, y1)
local counts = {}
for y = 0, 63 do
for x = 0, 127 do
local sx, sy = tlX + x, tlY + y
if sx >= 0 and sx < 240 and sy >= 0 and sy < 160 then
local r, g, b, a = px8(raw, sx, sy)
if a == 255 then
local k = key(r, g, b)
counts[k] = (counts[k] or 0) + 1
end
end
end
end
local cream, best = nil, -1
for k, n in pairs(counts) do
if n > best then cream, best = k, n end
end
print("U3 " .. label .. " baked cream", cream)
local erased, bad = 0, 0
for y = y0, y1 do
for x = x0, x1 do
local rr, rg, rb = px8(raw, tlX + x, tlY + y)
if key(rr, rg, rb) ~= cream then
local fr, fg, fb = px8(full, tlX + x, tlY + y)
local k = key(fr, fg, fb)
if k == cream then
erased = erased + 1
elseif k == key(rr, rg, rb) or k == "248,248,216" then
bad = bad + 1
print("U3 " .. label .. " ghost px", x, y, k)
end
end
end
end
print("U3 " .. label .. " erased", erased, "ghost", bad)
return erased > 0 and bad == 0
end
return function(game)
for _ = 1, 600 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
game:_handleBootAction({ action = "new_game", name = "RED" })
U.wait(180)
local Runtime = require("src.core.game3.runtime")
local Party = require("src.core.game3.party")
local session = Runtime.getSession()
if not session then fail("u3 session") end
session.party = {}
Party.giveMon(session, 4, 5)
local BattleBridge = require("src.core.game3.battle_bridge")
local ok, err = BattleBridge.startWild(Runtime._mod, game, { species = 16, level = 3 }, { fade = false })
if not ok then print("U3 start", err) fail("u3 battle start") end
local Battle = require("src.core.game3.battle")
local Ui = require("src.core.game3.battle.ui")
for i = 1, 1500 do
if Ui._mode == "menu" then break end
if i % 20 == 0 then U.tap(game, "a") else U.wait(1) end
end
if Ui._mode ~= "menu" then fail("u3 reached action menu") end
U.wait(30)
local st = Battle._st
local Anim = require("src.core.game3.battle.anim")
local mon = st and st.player and st.player.mon
if not mon then fail("u3 player mon") end
mon.hp = 7
Anim.syncDisplayFromState(st)
U.wait(20)
local BattleChrome = require("src.ui.game3.battle_chrome")
local Healthbox = require("src.core.game3.battle.healthbox")
local ptlX, ptlY = Healthbox.PLAYER_CENTER.x - 32, Healthbox.PLAYER_CENTER.y - 16
local etlX, etlY = Healthbox.ENEMY_CENTER.x - 32, Healthbox.ENEMY_CENTER.y - 16
local rawP = render(function() BattleChrome.drawPlayerBox(ptlX, ptlY) end)
local fullP = render(function() Healthbox.draw("player", st.player) end)
local rawE = render(function() BattleChrome.drawEnemyBox(etlX, etlY) end)
local fullE = render(function() Healthbox.draw("enemy", st.enemy) end)
local passP = check_region("player_slash", rawP, fullP, ptlX, ptlY, 64, 71, 24, 31)
local passE = check_region("enemy_lv", rawE, fullE, etlX, etlY, 56, 63, 10, 15)
if not U.shot(game, DIR .. "/u3_01_hp_7_no_ghost_slash_or_lv.png") then fail("u3 shot") end
local all = true
if passP then print("PASS u3 player healthbox no ghost slash at 1-digit hp") else print("FAIL u3 player healthbox no ghost slash at 1-digit hp") all = false end
if passE then print("PASS u3 enemy healthbox no ghost Lv") else print("FAIL u3 enemy healthbox no ghost Lv") all = false end
love.event.quit(all and 0 or 1)
while true do coroutine.yield() end
end
+85
View File
@@ -0,0 +1,85 @@
local U = require("tests.drivers.util")
local Boot = require("src.ui.game3.boot")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp"
return function(game)
local ok = true
local function expect(cond, label)
if cond then
print("PASS " .. label)
else
print("FAIL " .. label)
ok = false
end
end
local function waitFor(pred, limit)
for _ = 1, limit do
if pred() then return true end
U.wait(1)
end
return pred()
end
local function phase() return game.boot and game.boot.phase end
local n = 0
while phase() ~= "title" and n < 1200 do
U.tap(game, "start")
U.wait(30)
n = n + 30
end
expect(phase() == "title", "reached title")
if phase() ~= "title" then love.event.quit(1) return end
Boot.setHasContinue(game.boot, true)
if not game.boot.continueInfo then
Boot.setContinueInfo(game.boot, {
name = "RED", gender = 0, hours = 1, minutes = 23, hasDex = true, dexCount = 12, badges = 2,
})
end
U.wait(60)
U.shot(game, DIR .. "/u5_01_title.png")
U.tap(game, "start")
U.wait(2)
expect(phase() == "title_cry", "start on title plays cry, no menu")
U.shot(game, DIR .. "/u5_02_cry_title_still_up.png")
waitFor(function() return (game.boot.white or 0) >= 8 end, 200)
expect(phase() == "title_cry" and (game.boot.white or 0) >= 8, "fade to white after cry wait")
U.shot(game, DIR .. "/u5_03_white_fade.png")
waitFor(function() return phase() == "menu" end, 60)
expect(phase() == "menu", "separate main menu after white fade")
waitFor(function() return (game.boot.fadeT or 0) == 0 end, 30)
U.wait(2)
expect(not game.boot._titleActive, "title art torn down under menu")
U.shot(game, DIR .. "/u5_04_main_menu_continue.png")
U.tap(game, "down")
U.wait(3)
expect(game.boot.menuIndex == 2, "down moves to NEW GAME")
U.tap(game, "down")
U.wait(3)
expect(game.boot.menuIndex == 2, "down clamps at NEW GAME")
U.shot(game, DIR .. "/u5_05_main_menu_newgame_row.png")
U.tap(game, "b")
waitFor(function() return phase() == "title" end, 60)
expect(phase() == "title", "B returns to title")
U.wait(40)
U.shot(game, DIR .. "/u5_06_b_back_to_title.png")
Boot.setHasContinue(game.boot, false)
U.tap(game, "start")
local sawMenu = false
for _ = 1, 200 do
if phase() == "menu" then sawMenu = true end
if phase() ~= "title_cry" and phase() ~= "title" then break end
U.wait(1)
end
expect(not sawMenu and phase() == "controls", "no save goes straight to new game")
U.wait(40)
U.shot(game, DIR .. "/u5_07_nosave_new_game.png")
love.event.quit(ok and 0 or 1)
end
@@ -0,0 +1,83 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp"
return function(game)
local ok = true
local function expect(cond, label)
if cond then
print("PASS " .. label)
else
print("FAIL " .. label)
ok = false
end
end
local function loadShot(path)
local f = io.open(path, "rb")
if not f then return nil end
local bytes = f:read("*a")
f:close()
local okData, fd = pcall(love.filesystem.newFileData, bytes, "shot.png")
if not okData then return nil end
local okImg, img = pcall(love.image.newImageData, fd)
if not okImg then return nil end
return img
end
local function blockStats(img, ox, oy, k)
local iw, ih = img:getDimensions()
local bad, total = 0, 0
for by = 0, 159 do
for bx = 0, 239 do
local x0, y0 = ox + bx * k, oy + by * k
if x0 + k <= iw and y0 + k <= ih then
total = total + 1
local r0, g0, b0 = img:getPixel(x0, y0)
local uniform = true
for yy = y0, y0 + k - 1 do
for xx = x0, x0 + k - 1 do
local r, g, b = img:getPixel(xx, yy)
if r ~= r0 or g ~= g0 or b ~= b0 then uniform = false break end
end
if not uniform then break end
end
if not uniform then bad = bad + 1 end
end
end
end
return bad, total
end
U.wait(30)
local start = { map = "FR_PALLET_TOWN", x = 8, y = 10, facing = "down" }
game:_handleBootAction({ action = "new_game", name = "RED", rivalName = "BLUE", gender = 0, start = start })
U.wait(120)
local Display = require("src.core.game3.display")
local sizes = {
{ 1360, 860, 5, "u6_01_landscape_1360x860_5x" },
{ 1024, 768, 4, "u6_02_landscape_1024x768_4x" },
{ 900, 1100, 3, "u6_03_portrait_900x1100_3x" },
{ 700, 1000, 2, "u6_04_portrait_700x1000_2x" },
}
for _, sz in ipairs(sizes) do
love.window.updateMode(sz[1], sz[2], { resizable = true })
U.wait(20)
local w, h = love.graphics.getDimensions()
local scale, ox, oy, _, _, scaleY = Display.fit(w, h)
print("U6 size", w, h, "scale", scale, scaleY, "ox", ox, "oy", oy)
expect(scale == sz[3] and scaleY == sz[3], sz[4] .. " whole scale " .. sz[3])
expect(ox == math.floor(ox) and oy == math.floor(oy), sz[4] .. " whole-pixel offsets")
local path = DIR .. "/" .. sz[4] .. ".png"
U.shot(game, path)
local img = loadShot(path)
expect(img ~= nil, sz[4] .. " shot readable")
if img then
local bad, total = blockStats(img, ox, oy, sz[3])
print("U6 blocks", sz[4], "nonuniform", bad, "of", total)
expect(total == 240 * 160 and bad == 0, sz[4] .. " every texel is a uniform " .. sz[3] .. "x" .. sz[3] .. " block")
end
end
love.event.quit(ok and 0 or 1)
end
+104
View File
@@ -0,0 +1,104 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp"
return function(game)
local fails = 0
local function check(cond, label)
if cond then
print("PASS " .. label)
else
fails = fails + 1
print("FAIL " .. label)
end
end
U.wait(30)
game:_handleBootAction({ action = "new_game", name = "RED", rivalName = "BLUE", gender = 0 })
U.wait(120)
local Runtime = require("src.core.game3.runtime")
local session = Runtime.getSession()
local Party = require("src.core.game3.party")
session.party = {}
Party.giveMon(session, 6, 60)
local BattleBridge = require("src.core.game3.battle_bridge")
local Battle = require("src.core.game3.battle")
local Ui = require("src.core.game3.battle.ui")
local Anim = require("src.core.game3.battle.anim")
local function onScreen()
if not Ui._showing then return "" end
local log = Ui.log() or {}
return log[#log - #(Ui._queue or {})] or ""
end
local function anyLine(needle)
for _, t in ipairs(Ui.log() or {}) do
if t:find(needle, 1, true) then return true end
end
return false
end
local function advanceUntil(pred, limit)
for f = 1, limit do
U.wait(1)
if pred() then return true end
if f % 20 == 0 then U.tap(game, "a") end
end
return false
end
local function trainerSprite()
local ok, s = pcall(Anim.stage)
local t = ok and s and s.trainer and s.trainer.enemy
return t and t.visible, t and t.ox
end
local ok = BattleBridge.startWild(Runtime._mod, game, { species = 16, level = 2 }, {})
check(ok, "u7 wild battle started")
local sawExp = advanceUntil(function() return onScreen():find("EXP. Points", 1, true) ~= nil end, 3000)
check(sawExp, "u7 wild EXP line reached")
if sawExp then
check(#(Ui._queue or {}) == 0, "u7 wild EXP line is the last queued text")
U.wait(150)
U.shot(game, DIR .. "/u7_01_wild_exp_is_last_text.png")
end
local sawWon = false
local ended = advanceUntil(function()
if anyLine("won the battle") then sawWon = true end
return not Battle.isActive()
end, 3000)
check(ended, "u7 wild battle ended")
check(not sawWon, "u7 wild win shows no 'You won the battle!'")
U.wait(90)
U.shot(game, DIR .. "/u7_02_wild_back_on_overworld.png")
U.wait(30)
ok = BattleBridge.start(Runtime._mod, game, { species = 7, level = 5, trainerId = 326 }, { trainerId = 326 })
check(ok, "u7 trainer battle started")
local sawDefeated = advanceUntil(function() return onScreen():find("defeated\n", 1, true) ~= nil end, 4000)
check(sawDefeated, "u7 trainer defeated line reached")
local sawLose
if sawDefeated then
check(not anyLine("wrong"), "u7 defeated line printed before lose text")
local vis, ox = trainerSprite()
check(not (vis and ox == 0), "u7 trainer not slid in while defeated line shows")
U.wait(150)
U.shot(game, DIR .. "/u7_03_trainer_defeated_before_slide_in.png")
sawLose = advanceUntil(function() return onScreen():find("wrong", 1, true) ~= nil end, 2000)
check(sawLose, "u7 trainer lose text follows defeated line")
if sawLose then
U.wait(150)
local vis2, ox2 = trainerSprite()
check(vis2 and ox2 == 0, "u7 trainer slid in for lose text")
U.shot(game, DIR .. "/u7_04_trainer_lose_text_after_slide_in.png")
end
end
local tEnded = advanceUntil(function() return not Battle.isActive() end, 3000)
check(tEnded, "u7 trainer battle ended")
for i, t in ipairs(Ui.log() or {}) do print("U7 LOG", i, (t:gsub("\n", " / "))) end
if fails == 0 then
print("PASS u7 win text")
love.event.quit(0)
else
print("FAIL u7 win text")
love.event.quit(1)
end
end
+144
View File
@@ -0,0 +1,144 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp"
return function(game)
local fails = 0
local function check(cond, label)
if cond then
print("PASS " .. label)
else
fails = fails + 1
print("FAIL " .. label)
end
return cond
end
local function finish()
if fails == 0 then
print("PASS u8 learn move four-move paths")
love.event.quit(0)
else
print("FAIL u8 learn move four-move paths")
love.event.quit(1)
end
end
U.wait(30)
local Schema = require("src.core.game3.save_schema_firered")
local Party = require("src.core.game3.party")
local Experience = require("src.core.game3.battle.experience")
local SummaryData = require("src.core.game3.summary_data")
local Runtime = require("src.core.game3.runtime")
local BattleBridge = require("src.core.game3.battle_bridge")
local Battle = require("src.core.game3.battle")
local Ui = require("src.core.game3.battle.ui")
local LearnMove = require("src.core.game3.battle.learn_move")
local Message = require("src.ui.game3.message")
local Choice = require("src.ui.game3.choice")
local session = Schema.newGame({ name = "RED", rivalName = "BLUE", gender = 0 })
local function fresh_charmander()
session.party = {}
Party.giveMon(session, 4, 6)
local mon = session.party[1]
Experience.syncExpToLevel(mon)
mon.exp = SummaryData.expForLevel(Experience.growthRate(mon), 7) - 3
mon.moves = { 10, 45, 33, 39 }
mon.pp = { 35, 40, 35, 30 }
mon.maxPp = { 35, 40, 35, 30 }
return mon
end
local function page()
return (Message.isOpen() and Message.currentPage and Message.currentPage()) or ""
end
local function anyLine(needle)
for _, t in ipairs(Ui.log() or {}) do
if t:find(needle, 1, true) then return true end
end
return false
end
local function advanceUntil(pred, limit)
for f = 1, limit do
U.wait(1)
if pred() then return true end
if f % 6 == 0 and not Choice.active and not Message._stay then U.tap(game, "a") end
end
return false
end
local function promptUp(needle)
return function()
return Choice.active and Choice.kind == "yesno" and Message.isWaiting()
and page():find(needle, 1, true) ~= nil
end
end
local function start_wild()
return BattleBridge.startWild(Runtime._mod, game, { species = 16, level = 3 }, { fade = false })
end
fresh_charmander()
session.map = "FR_ROUTE_1"
session.x = 10
session.y = 20
session.flags = session.flags or {}
game:_enterField(session, "new_game")
U.wait(90)
check(start_wild(), "u8 stop-path battle started")
if not check(advanceUntil(promptUp("Should a move be deleted"), 6000), "u8 delete prompt shows with YES/NO") then
U.shot(game, DIR .. "/u8_98_softlock_before_delete_prompt.png")
return finish()
end
check(anyLine("wants to learn") and anyLine("four moves"), "u8 wants-to-learn and four-moves lines shown first")
U.wait(20)
U.shot(game, DIR .. "/u8_01_delete_prompt_yesno.png")
U.tap(game, "down")
U.wait(4)
U.tap(game, "a")
if not check(advanceUntil(promptUp("Stop trying"), 600), "u8 NO opens stop-trying prompt") then
return finish()
end
U.wait(20)
U.shot(game, DIR .. "/u8_02_stop_trying_prompt.png")
U.tap(game, "a")
local sawDidNot = advanceUntil(function()
return Message.isWaiting() and page():find("did not learn", 1, true) ~= nil
end, 600)
if check(sawDidNot, "u8 YES to stop prints did-not-learn") then
U.wait(10)
U.shot(game, DIR .. "/u8_03_did_not_learn.png")
end
check(advanceUntil(function() return not Battle.isActive() end, 3000), "u8 stop-path battle ended")
local m1 = session.party[1]
check(m1 and table.concat(m1.moves, ",") == "10,45,33,39" and m1.level == 7, "u8 stop path kept moves at LV. 7")
check(not LearnMove.busy(), "u8 LearnMove idle after stop path")
U.wait(60)
fresh_charmander()
check(start_wild(), "u8 forget-path battle started")
if not check(advanceUntil(promptUp("Should a move be deleted"), 6000), "u8 forget path reaches delete prompt") then
return finish()
end
U.wait(10)
U.tap(game, "a")
if not check(advanceUntil(function() return Choice.active and Choice.kind == "multi" end, 600), "u8 YES opens forget list") then
return finish()
end
U.wait(20)
U.shot(game, DIR .. "/u8_04_forget_list.png")
U.tap(game, "a")
local sawLearned = advanceUntil(function()
return Message.isWaiting() and page():find("learned", 1, true) ~= nil and anyLine("Poof")
end, 900)
if check(sawLearned, "u8 Poof chain reaches learned EMBER") then
check(anyLine("forgot how to"), "u8 forgot-how-to line shown")
U.wait(10)
U.shot(game, DIR .. "/u8_05_learned_ember.png")
end
check(advanceUntil(function() return not Battle.isActive() end, 3000), "u8 forget-path battle ended")
local m2 = session.party[1]
check(m2 and m2.moves[1] == 52 and m2.level == 7, "u8 EMBER replaced SCRATCH at LV. 7")
U.wait(60)
U.shot(game, DIR .. "/u8_06_back_on_route1.png")
finish()
end
+119
View File
@@ -0,0 +1,119 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp"
return function(game)
local fails = 0
local function result(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then fails = fails + 1 end
end
for _ = 1, 600 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
game:_handleBootAction({ action = "new_game", name = "RED" })
U.wait(180)
local Runtime = require("src.core.game3.runtime")
local Party = require("src.core.game3.party")
local BattleBridge = require("src.core.game3.battle_bridge")
local Battle = require("src.core.game3.battle")
local Anim = require("src.core.game3.battle.anim")
local Ui = require("src.core.game3.battle.ui")
local Pokemon = require("src.core.game3.pokemon")
local session = Runtime.getSession()
session.party = {}
Party.giveMon(session, 4, 5)
local function waitEmerge(side, maxFrames)
for _ = 1, maxFrames do
local p = Anim.present(side)
local s = Anim.stage()
if p and p.visible ~= false and (p.scale or 1) < 0.35 and s.ball and s.ball.visible
and (s.ball.frame or 0) >= 1 then
return true
end
if Ui.dialogPending and Ui.dialogPending() then
U.tap(game, "a")
else
U.wait(1)
end
end
return false
end
local function drawnScale(side)
local b = Ui._st and Ui._st[side]
local sp = b and (b.species or (b.mon and Pokemon.speciesOf(b.mon)))
local entry = side == "player" and Pokemon.backPic(sp) or Pokemon.frontPic(sp)
local img = entry and entry.image
local got
local realDraw = love.graphics.draw
love.graphics.draw = function(i, x, y, r, sx, sy, ...)
if i == img and not got then got = { sx = sx, sy = sy } end
return realDraw(i, x, y, r, sx, sy, ...)
end
U.wait(1)
love.graphics.draw = realDraw
return got
end
local function waitScale(side, target, maxFrames)
for _ = 1, maxFrames do
local p = Anim.present(side)
if p and p.visible ~= false and (p.scale or 1) >= target
and (target < 1 or (p.oy or 0) == 0) then return true end
U.wait(1)
end
return false
end
local function waitFull(side, maxFrames)
return waitScale(side, 1, maxFrames)
end
local ok, err = BattleBridge.startWild(Runtime._mod, game, { species = 16, level = 3 }, { fade = false })
result(ok == true, "u9 wild battle started " .. tostring(err or ""))
if not ok then love.event.quit(1) return end
local reached = waitEmerge("player", 1500)
result(reached, "u9 player send-out emerge started")
if reached then
local want = Anim.present("player").scale
local got = drawnScale("player")
result(got ~= nil and math.abs(got.sy) < 0.5 and math.abs(got.sy) >= want - 1e-6,
string.format("u9 player pic drawn scaled on open tick sy=%s pres=%.2f", tostring(got and got.sy), want))
if waitScale("player", 0.55, 30) then
U.shot(game, DIR .. "/u9_01_player_emerge_half_size.png")
end
result(waitFull("player", 60), "u9 player emerge grew to full size")
U.shot(game, DIR .. "/u9_02_player_emerge_full.png")
end
Battle.abort("win")
U.wait(120)
for _ = 1, 30 do U.tap(game, "a") U.wait(4) end
local ok2, err2 = BattleBridge.start(Runtime._mod, game,
{ trainerId = 326, party = { { species = 7, level = 5 } } },
{ wild = false, trainerId = 326, fade = false })
result(ok2 == true, "u9 trainer battle started " .. tostring(err2 or ""))
if ok2 then
reached = waitEmerge("enemy", 2000)
result(reached, "u9 enemy send-out emerge started")
if reached then
local want = Anim.present("enemy").scale
local got = drawnScale("enemy")
result(got ~= nil and math.abs(got.sy) < 0.5 and math.abs(got.sy) >= want - 1e-6,
string.format("u9 enemy pic drawn scaled on open tick sy=%s pres=%.2f", tostring(got and got.sy), want))
if waitScale("enemy", 0.55, 30) then
U.shot(game, DIR .. "/u9_03_enemy_emerge_half_size.png")
end
result(waitFull("enemy", 60), "u9 enemy emerge grew to full size")
U.shot(game, DIR .. "/u9_04_enemy_emerge_full.png")
end
end
love.event.quit(fails == 0 and 0 or 1)
end
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local Ui = require("src.core.game3.battle.ui")
local Anim = require("src.core.game3.battle.anim")
local BattleBg = require("src.core.game3.battle.bg")
local Healthbox = require("src.core.game3.battle.healthbox")
local BattleChrome = require("src.ui.game3.battle_chrome")
local TrainerPic = require("src.core.game3.trainer_pic")
local Pokemon = require("src.core.game3.pokemon")
local IMG = { enemyMon = { tag = "enemyMon" }, playerMon = { tag = "playerMon" } }
local calls = {}
local gfx = setmetatable({}, { __index = function() return function() end end })
gfx.draw = function(img, x, y, r, sx, sy)
if img and img.tag then calls[img.tag] = { sx = sx, sy = sy } end
end
gfx.newQuad = function() return {} end
_G.love = { graphics = gfx }
BattleBg.draw = function() return true end
Healthbox.draw = function() end
BattleChrome.drawPanel = function() end
BattleChrome.drawPartyBar = function() end
BattleChrome.manifest = function() return {} end
Anim.drawParticles = function() end
TrainerPic.front = function() return nil end
TrainerPic.back = function() return nil end
Pokemon.frontPic = function() return { image = IMG.enemyMon } end
Pokemon.backPic = function() return { image = IMG.playerMon } end
local function near(a, b) return a and math.abs(a - b) < 1e-6 end
local function drawWith(setup)
Ui.reset({ headless = false })
Anim.reset({ headless = false })
Ui._st = { enemy = { species = 16 }, player = { species = 4 } }
Anim.present("player").visible = true
Anim.present("enemy").visible = true
setup(Anim.present("player"), Anim.present("enemy"))
calls = {}
Ui.draw(240, 160)
return calls.playerMon, calls.enemyMon
end
print("[test] emerge scale reaches the drawn pic")
local p, e = drawWith(function(pp, pe)
pp.scale = 0.23
pe.scale = 0.16
end)
check(p and near(p.sx, 0.23) and near(p.sy, 0.23), "player pic drawn at pres.scale 0.23")
check(e and near(e.sx, 0.16) and near(e.sy, 0.16), "enemy pic drawn at pres.scale 0.16")
print("[test] default present draws at 1.0")
p, e = drawWith(function() end)
check(p and near(p.sx, 1) and near(p.sy, 1), "player pic drawn at 1.0")
check(e and near(e.sx, 1) and near(e.sy, 1), "enemy pic drawn at 1.0")
print("[test] deform sx/sy composes with scale")
p = drawWith(function(pp)
pp.scale = 0.5
pp.sx = 0.8
pp.sy = 1.2
end)
check(p and near(p.sx, 0.4) and near(p.sy, 0.6), "sx/sy deform multiplies scale")
p = drawWith(function(pp)
pp.sx = 0.8
pp.sy = 1.2
end)
check(p and near(p.sx, 0.8) and near(p.sy, 1.2), "sx/sy deform unchanged at scale 1")
print("[test] hFlip keeps scale magnitude")
p = drawWith(function(pp)
pp.scale = 0.5
pp.hFlip = true
end)
check(p and near(p.sx, -0.5) and near(p.sy, 0.5), "hFlip gives -scale on x")
print(failed == 0 and "[ok] all" or ("[FAIL] " .. failed .. " failed"))
os.exit(failed == 0 and 0 or 1)
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local Battle = require("src.core.game3.battle")
local Ui = require("src.core.game3.battle.ui")
local function party()
return { { species = 6, level = 60, hp = 200, maxHp = 200, moves = { 53 }, pp = { 15 }, maxPp = { 15 } } }
end
local function run(opts)
if Battle.isActive() then Battle.abort("win") end
local ok = Battle.start(opts)
local res = Battle.runToEnd()
local log = {}
for i, t in ipairs(Ui.log() or {}) do log[i] = t end
return ok, res, log
end
local function find(log, needle)
for i, t in ipairs(log) do
if t:find(needle, 1, true) then return i end
end
return nil
end
print("[test] 1. wild win prints nothing after EXP")
local ok, res, log = run({ wild = true, headless = true, playerParty = party(), foe = { species = 16, level = 2 } })
check(ok and res == "win", "wild battle won")
check(find(log, "won the battle") == nil, "no 'You won the battle!' line")
check(log[#log] ~= nil and log[#log]:find("EXP. Points", 1, true) ~= nil, "last wild line is the EXP line")
print("[test] 2. trainer win: defeated before lose text")
ok, res, log = run({ wild = false, headless = true, trainerId = 326, playerParty = party(), foe = { species = 7, level = 5, trainerId = 326 } })
check(ok and res == "win", "trainer battle won")
local iDef = find(log, "defeated\n")
local iLose = find(log, "I picked the wrong")
check(iDef ~= nil, "defeated line present")
check(iLose ~= nil, "lose text present")
check(iDef and iLose and iDef < iLose, "defeated line precedes lose text")
check(find(log, "won the battle") == nil, "no 'You won the battle!' in trainer log")
if failed > 0 then
print(string.format("[FAIL] %d check(s) failed", failed))
os.exit(1)
end
print("[PASS] game3 battle win text")
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env luajit
-- pokefirered/src/player_pc.c:151 BedroomPC, pokefirered/src/player_pc.c:100 gNewGamePCItems
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
package.loaded["src.ui.game3.stack"] = {
push = function() end,
pop = function() end,
busy = function() return false end,
drawOrder = function() return {} end,
}
package.loaded["src.core.game3.audio"] = {
playSe = function() end,
playCry = function() end,
}
print("[test] 1. BedroomPC special id + handler")
local Std = require("src.core.game3.scripting.stdscripts")
check(Std.SPECIAL.BedroomPC == 249, "BedroomPC = 249 (got " .. tostring(Std.SPECIAL.BedroomPC) .. ")")
local Natives = require("src.core.game3.scripting.natives")
check(Natives.ALLOW["special:249"] ~= nil, "special:249 handler registered")
print("[test] 2. special 249 yields into openPc with bedroom opts")
local gotOpts, gotDone
local ctx = { mode = "bytecode", status = "running" }
local adapters = {
openPc = function(done, opts)
gotDone = done
gotOpts = opts
end,
log = function() end,
}
local yielded = Natives.special(ctx, 249, adapters)
check(yielded == true, "special 249 yields")
check(type(gotOpts) == "table" and gotOpts.bedroom == true, "openPc got bedroom=true")
check(ctx.nativePoll and ctx.nativePoll() == false, "native wait pending while PC open")
if gotDone then gotDone() end
check(ctx.nativePoll and ctx.nativePoll() == true, "native wait finishes on close")
print("[test] 3. host openPc routes bedroom into player_pc with closeOnExit")
do
local shown
local realPc = package.loaded["src.ui.game3.pc_menu"]
local realRt = package.loaded["src.core.game3.runtime"]
package.loaded["src.ui.game3.pc_menu"] = { show = function(o) shown = o end }
package.loaded["src.core.game3.runtime"] = {
isActive = function() return true end,
getSession = function() return {} end,
}
local okA, Adapters = pcall(require, "src.core.game3.scripting.adapters")
local okH, host = false, nil
if okA then
okH, host = pcall(Adapters.host, nil, {}, nil)
end
check(okH and host and host.openPc, "Adapters.host builds openPc")
if okH and host and host.openPc then
host.openPc(function() end, { bedroom = true })
check(shown and shown.startMode == "player_pc" and shown.closeOnExit == true,
"bedroom -> startMode player_pc, closeOnExit")
shown = nil
host.openPc(function() end)
check(shown and shown.startMode == nil and not shown.closeOnExit, "no opts -> root hub")
end
package.loaded["src.ui.game3.pc_menu"] = realPc
package.loaded["src.core.game3.runtime"] = realRt
end
print("[test] 4. PcMenu bedroom mode: starts in player_pc, B and LOG OFF close")
local PcMenu = require("src.ui.game3.pc_menu")
local function input(btn)
return { wasPressed = function(_, b) return b == btn end }
end
local closed = 0
PcMenu.show({ session = {}, onClose = function() closed = closed + 1 end,
startMode = "player_pc", closeOnExit = true })
check(PcMenu.mode == "player_pc", "starts in player_pc (got " .. tostring(PcMenu.mode) .. ")")
check(PcMenu._status == "What would you like to do?", "status What would you like to do?")
PcMenu.handleInput(input("b"))
check(not PcMenu.isOpen() and closed == 1, "B closes bedroom PC and fires onClose")
PcMenu.show({ session = {}, onClose = function() closed = closed + 1 end,
startMode = "player_pc", closeOnExit = true })
PcMenu.handleInput(input("up"))
check(PcMenu.cursor == 4, "cursor on LOG OFF")
PcMenu.handleInput(input("a"))
check(not PcMenu.isOpen() and closed == 2, "LOG OFF closes bedroom PC")
PcMenu.show({ session = {}, onClose = function() closed = closed + 1 end })
check(PcMenu.mode == "root", "default show opens root hub")
PcMenu.cursor = 2
PcMenu.handleInput(input("a"))
check(PcMenu.mode == "player_pc", "hub -> player_pc")
PcMenu.handleInput(input("b"))
check(PcMenu.isOpen() and PcMenu.mode == "root" and closed == 2, "hub player_pc B returns to root")
PcMenu.close()
print("[test] 5. New game seeds PC POTION; withdraw moves it to bag; save round-trip")
local Schema = require("src.core.game3.save_schema_firered")
local Bag = require("src.core.game3.bag")
local Storage = require("src.core.game3.storage")
local session = Schema.newGame({ rngSeed = 1 })
local it = session.storage and session.storage.items and session.storage.items[1]
check(it and it.id == 13 and it.qty == 1, "newGame storage.items[1] = POTION x1")
check(session.storage and #session.storage.items == 1, "exactly one PC item")
local saved = Schema.toSaveTable(session)
check(type(saved.storage) == "table" and saved.storage.items[1] and saved.storage.items[1].id == 13,
"toSaveTable writes storage")
local restored = Schema.fromSaveTable(saved)
check(restored.storage and restored.storage.items[1] and restored.storage.items[1].id == 13,
"fromSaveTable restores storage")
local legacy = Schema.fromSaveTable({ map = "FR_PALLET_TOWN", x = 1, y = 1 })
check(legacy.storage == nil, "legacy save without storage gets no seeded POTION")
check(Storage.withdrawItem(session, 1, 1) == true, "withdrawItem POTION ok")
check(Bag.has(session.bag, 13, 1), "bag has POTION")
check(#session.storage.items == 0, "PC storage empty after withdraw")
if failed == 0 then
print("\nAll game3 bedroom PC tests passed.")
os.exit(0)
else
print("\n" .. failed .. " bedroom PC test(s) failed.")
os.exit(1)
end
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local win = { w = 1024, h = 768, pw = 1024, ph = 768 }
local canvasCalls = {}
local fakeCanvas = {}
fakeCanvas.__index = fakeCanvas
function fakeCanvas:getWidth() return self.w end
function fakeCanvas:getHeight() return self.h end
function fakeCanvas:setFilter() end
_G.love = {
graphics = {
getDimensions = function() return win.w, win.h end,
getPixelDimensions = function() return win.pw, win.ph end,
getDPIScale = function() return win.ph / win.h end,
newCanvas = function(w, h, settings)
canvasCalls[#canvasCalls + 1] = { w = w, h = h, settings = settings }
return setmetatable({ w = w, h = h }, fakeCanvas)
end,
},
}
package.loaded["src.core.SafeArea"] = {
windowRect = function() return 0, 0, win.w, win.h end,
}
local Display = require("src.core.game3.display")
local function isWhole(v)
return math.abs(v - math.floor(v + 0.5)) < 1e-6
end
local function case(w, h, dpi, expectK)
win.w, win.h = w, h
win.pw, win.ph = math.floor(w * dpi + 0.5), math.floor(h * dpi + 0.5)
local scale, ox, oy, pw, ph, scaleY = Display.fit(w, h)
scaleY = scaleY or scale
local dx, dy = win.pw / w, win.ph / h
local label = ("%dx%d dpi %g"):format(w, h, dpi)
check(isWhole(scale * dx) and isWhole(scaleY * dy),
label .. " whole physical pixels per texel (scale " .. tostring(scale) .. ")")
check(math.abs(scale * dx - scaleY * dy) < 1e-6, label .. " square texels")
check(isWhole(ox * dx) and isWhole(oy * dy),
label .. " offsets on physical pixels (" .. tostring(ox) .. "," .. tostring(oy) .. ")")
check(ox >= 0 and ox + pw <= w + 1e-6 and oy >= 0 and oy + ph <= h + 1e-6,
label .. " frame inside window")
if expectK then
check(math.abs(scale * dx - expectK) < 1e-6,
label .. " scale is " .. expectK .. "x (got " .. tostring(scale * dx) .. ")")
end
end
case(1024, 768, 1, 4)
case(1360, 860, 1, 5)
case(1100, 1000, 1, 4)
case(900, 1100, 1, 3)
case(700, 1000, 1, 2)
case(1080, 1920, 1, 4)
case(390, 844, 3, 4)
case(392, 870, 2.755, 4)
win.w, win.h, win.pw, win.ph = 1360, 860, 1360, 860
local s, ox, oy = Display.fit(1360, 860)
check(s == 5 and ox == 80 and oy == 30, "desktop 1360x860 unchanged (5x at 80,30)")
win.w, win.h, win.pw, win.ph = 1100, 1000, 1100, 1000
s, ox, oy = Display.fit(1100, 1000)
check(s == 4 and ox == 70 and oy == 180, "desktop 1100x1000 unchanged (4x at 70,180)")
Display._canvas = nil
Display.ensureCanvas("main")
local last = canvasCalls[#canvasCalls]
check(last and last.w == 240 and last.h == 160, "canvas is 240x160")
check(last and last.settings and last.settings.dpiscale == 1, "canvas created at dpiscale 1")
print(("game3_display_fit_test: %s (%d failed)"):format(failed == 0 and "PASS" or "FAIL", failed))
os.exit(failed == 0 and 0 or 1)
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function find_upvalue(fn, name, seen)
seen = seen or {}
if type(fn) ~= "function" or seen[fn] then return nil end
seen[fn] = true
local i = 1
while true do
local n, v = debug.getupvalue(fn, i)
if not n then break end
if n == name and type(v) == "function" then return v end
if type(v) == "function" then
local r = find_upvalue(v, name, seen)
if r then return r end
end
i = i + 1
end
return nil
end
local bake_rgb
do
local ok, Extract = pcall(require, "src.import.gba.battle_chrome_extract")
if ok and type(Extract) == "table" then
for _, v in pairs(Extract) do
bake_rgb = find_upvalue(v, "bgr555_to_rgb8")
if bake_rgb then break end
end
end
end
if not bake_rgb then
bake_rgb = function(c)
local r5, g5, b5 = c % 32, math.floor(c / 32) % 32, math.floor(c / 1024) % 32
return math.floor(r5 * 255 / 31 + 0.5), math.floor(g5 * 255 / 31 + 0.5), math.floor(b5 * 255 / 31 + 0.5)
end
end
check(bake_rgb ~= nil, "bake conversion available")
local function bgr(r, g, b) return r + g * 32 + b * 1024 end
local function rgb8(r5, g5, b5)
local r, g, b = bake_rgb(bgr(r5, g5, b5))
return { r, g, b }
end
local PAL2 = rgb8(31, 31, 27)
local PAL1 = rgb8(8, 8, 8)
local PAL3 = rgb8(27, 26, 22)
check(PAL2[1] == 255 and PAL2[2] == 255 and PAL2[3] == 222, "bake cream is 255,255,222")
check(PAL3[1] == 222 and PAL3[2] == 214 and PAL3[3] == 181, "bake shadow is 222,214,181")
local function to8(c)
return { math.floor(c[1] * 255 + 0.5), math.floor(c[2] * 255 + 0.5), math.floor(c[3] * 255 + 0.5) }
end
local function same(a, b)
return a[1] == b[1] and a[2] == b[2] and a[3] == b[3]
end
local function fmt(c) return string.format("%d,%d,%d", c[1], c[2], c[3]) end
local log = {}
local phase = "pre"
local current = { 1, 1, 1, 1 }
_G.love = {
graphics = {
setColor = function(r, g, b, a)
if type(r) == "table" then current = { r[1], r[2], r[3], r[4] or 1 }
else current = { r, g, b, a or 1 } end
end,
rectangle = function(mode, x, y, w, h)
log[#log + 1] = { phase = phase, color = current, x = x, y = y, w = w, h = h }
end,
draw = function() end,
},
}
local textColors = {}
package.loaded["src.ui.game3.battle_chrome"] = {
drawPlayerBox = function() phase = "erase" end,
drawEnemyBox = function() phase = "erase" end,
drawHpBar = function() phase = "post" end,
drawExpBar = function() end,
}
package.loaded["src.ui.game3.frlg_font"] = {
CHAR_LV_2 = 1, CHAR_MALE = 2, CHAR_FEMALE = 3,
draw = function(_, _, _, opts) textColors[#textColors + 1] = opts and opts.colors end,
drawGlyph = function(_, _, _, opts) textColors[#textColors + 1] = opts and opts.colors end,
measure = function(s) return #tostring(s) * 5 end,
advance = function() return 6 end,
}
package.loaded["src.core.game3.battle.state"] = {
displayName = function(b) return b.name end,
}
package.loaded["src.core.game3.battle.anim"] = {
stage = function() return { healthbox = { player = { visible = true, ox = 0 }, enemy = { visible = true, ox = 0 } } } end,
displayHpRatio = function(_, b) return b.mon.hp / b.mon.maxHp, b.mon.hp, b.mon.maxHp end,
displayExpRatio = function() return 0 end,
present = function() return {} end,
}
package.loaded["src.ui.game3.summary_chrome"] = { drawStatusIcon = function() end }
package.loaded["src.core.game3.summary_data"] = { statusAilment = function() return 0 end }
package.loaded["src.core.game3.pokemon"] = { gender = function() return nil end }
local Healthbox = require("src.core.game3.battle.healthbox")
local function run(side, name)
log, textColors, phase = {}, {}, "pre"
Healthbox.draw(side, {
name = name,
mon = { species = 4, level = 5, hp = 7, maxHp = 19, gender = "X" },
})
local erased = {}
for _, e in ipairs(log) do
if e.phase == "erase" then erased[#erased + 1] = e end
end
return erased
end
for _, side in ipairs({ "player", "enemy" }) do
print("[test] " .. side .. " placeholder erase uses bake cream")
local erased = run(side, side == "player" and "ABC" or "PIDGEY")
check(#erased > 0, side .. " erase painted pixels (" .. #erased .. ")")
local bad
for _, e in ipairs(erased) do
if not same(to8(e.color), PAL2) then bad = e break end
end
check(bad == nil, side .. " erase color == " .. fmt(PAL2) .. (bad and (" got " .. fmt(to8(bad.color))) or ""))
print("[test] " .. side .. " text colors use bake ink/shadow")
local c = textColors[1]
check(c and same(to8(c.fg), PAL1), side .. " text fg == " .. fmt(PAL1) .. (c and (" got " .. fmt(to8(c.fg))) or ""))
check(c and same(to8(c.shadow), PAL3), side .. " text shadow == " .. fmt(PAL3) .. (c and (" got " .. fmt(to8(c.shadow))) or ""))
end
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")
+212
View File
@@ -0,0 +1,212 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local Message = { open = false, _stay = false, _done = nil, _waiting = false, shown = {} }
function Message.show(text, opts)
opts = type(opts) == "table" and opts or {}
Message.open = true
Message._stay = opts.stay and true or false
Message._done = opts.done
Message._waiting = false
Message.shown[#Message.shown + 1] = tostring(text)
return Message
end
function Message.showStay(text, opts)
opts = opts or {}
opts.stay = true
return Message.show(text, opts)
end
function Message.isOpen() return Message.open end
function Message.isWaiting() return Message.open and Message._waiting end
function Message.isTyping() return Message.open and not Message._waiting end
function Message.tick()
if Message.open then Message._waiting = true end
end
function Message.skipReveal()
if Message.open then Message._waiting = true end
end
function Message.close()
local done = Message._done
Message.open = false
Message._stay = false
Message._waiting = false
Message._done = nil
if done then done() end
end
package.loaded["src.ui.game3.message"] = Message
local Battle = require("src.core.game3.battle")
local Ui = require("src.core.game3.battle.ui")
local LearnMove = require("src.core.game3.battle.learn_move")
local Choice = require("src.ui.game3.choice")
local function idx_of(shown, needle, from)
for i = from or 1, #shown do
if shown[i]:find(needle, 1, true) then return i end
end
return nil
end
local function in_order(shown, needles)
local at = 0
for _, n in ipairs(needles) do
local i = idx_of(shown, n, at + 1)
if not i then return false, n end
at = i
end
return true
end
local function run(moves, answers)
Ui.reset({ headless = false })
LearnMove.reset()
Choice.active = false
Message.open = false
Message._done = nil
Message.shown = {}
Battle._headless = false
local hooks = Battle._choiceHooksForTests and Battle._choiceHooksForTests() or {
pushMsg = function(text, cb) Ui.push(text, cb) end,
askYesNo = function(a, b) Ui.askYesNo(a, b) end,
askForget = function(labels, cb) Ui.askForget(labels, cb) end,
headless = false,
}
local mon = { species = 4, level = 19, moves = moves, pp = { 35, 40, 35, 30 }, maxPp = { 35, 40, 35, 30 } }
local result
local prompts = {}
local openedWhileTyping = false
local ok, err = pcall(function()
LearnMove.begin({
mon = mon,
moveId = 52,
displayName = "CHARMANDER",
pushMsg = hooks.pushMsg,
askYesNo = hooks.askYesNo,
askForget = hooks.askForget,
headless = hooks.headless,
onDone = function(learned) result = learned end,
})
for _ = 1, 200 do
if result ~= nil then break end
if Choice.active then
if Message.open and not Message._waiting then openedWhileTyping = true end
prompts[#prompts + 1] = Message.open and Message.shown[#Message.shown] or ""
local ans = table.remove(answers, 1)
if ans == "cancel" then Choice.cancel() else Choice.autoPick(ans) end
elseif Message.open and Message._waiting and not Message._stay then
Message.close()
else
Ui.pump()
LearnMove.pump()
Message.tick()
end
end
end)
return {
ok = ok, err = err, result = result, mon = mon, shown = Message.shown,
prompts = prompts, active = LearnMove.busy(), open = Message.open,
openedWhileTyping = openedWhileTyping,
}
end
print("[test] 1. free slot: learned message dismissed finishes")
local r = run({ 10, 45 }, {})
check(r.ok, "no error " .. tostring(r.err))
check(r.result == true, "onDone(true)")
check(r.mon.moves[3] == 52, "EMBER in slot 3")
check(not r.active, "LearnMove idle")
print("[test] 2. four moves: NO delete, YES stop -> did not learn")
r = run({ 10, 45, 33, 39 }, { false, true })
check(r.ok, "no error " .. tostring(r.err))
local ord, miss = in_order(r.shown, { "wants to learn", "already", "Should a move be deleted", "Stop trying", "did not learn" })
check(ord, "message order (missing " .. tostring(miss) .. ")")
check(r.result == false, "onDone(false)")
check(not r.active, "LearnMove idle")
check(r.prompts[1] and r.prompts[1]:find("Should a move be deleted", 1, true) ~= nil, "delete prompt text stays up under YES/NO")
check(r.prompts[2] and r.prompts[2]:find("Stop trying", 1, true) ~= nil, "stop prompt text stays up under YES/NO")
check(not r.openedWhileTyping, "YES/NO never opens while the prompt is still typing")
check(not r.open, "no message left open")
check(r.mon.moves[1] == 10 and r.mon.moves[4] == 39, "moves unchanged")
print("[test] 3. four moves: YES delete, forget slot 1 -> Poof chain")
r = run({ 10, 45, 33, 39 }, { true, 0 })
check(r.ok, "no error " .. tostring(r.err))
ord, miss = in_order(r.shown, { "wants to learn", "Should a move be deleted", "Poof", "forgot how to", "And...", "learned" })
check(ord, "message order (missing " .. tostring(miss) .. ")")
check(r.result == true, "onDone(true)")
check(r.mon.moves[1] == 52, "EMBER replaced slot 1")
check(not r.active, "LearnMove idle")
print("[test] 4. HM refusal then stop")
r = run({ 15, 45, 33, 39 }, { true, 0, false, true })
check(r.ok, "no error " .. tostring(r.err))
ord, miss = in_order(r.shown, { "Should a move be deleted", "HM moves can't", "Should a move be deleted", "Stop trying", "did not learn" })
check(ord, "message order (missing " .. tostring(miss) .. ")")
check(r.result == false, "onDone(false)")
check(r.mon.moves[1] == 15, "CUT kept")
print("[test] 5. forget list B-cancel returns to stop prompt")
r = run({ 10, 45, 33, 39 }, { true, "cancel", true })
check(r.ok, "no error " .. tostring(r.err))
check(r.result == false, "onDone(false)")
check(idx_of(r.shown, "did not learn") ~= nil, "did not learn shown")
print("[test] 6. Ui.askYesNo(cb) single-arg form still works")
Ui.reset({ headless = false })
Message.open = false
Message.shown = {}
local got
Ui.askYesNo(function(yes) got = yes end)
check(Choice.active and not Message.open, "choice open without a prompt message")
Choice.autoPick(true)
check(got == true, "cb(true)")
print("[test] 6b. Ui.askYesNo(prompt, cb) waits for the prompt to finish printing")
Ui.reset({ headless = false })
Message.open = false
Message.shown = {}
Choice.active = false
got = nil
Ui.askYesNo("Should a move be deleted?", function(yes) got = yes end)
check(Message.open and Message._stay and not Message._waiting, "prompt shown as a typing stay message")
check(not Choice.active, "YES/NO not open while typing")
check(Ui.pump() == false and not Choice.active, "pump while typing keeps YES/NO closed")
Message.tick()
check(Ui.pump() == false and Choice.active and Choice.kind == "yesno", "YES/NO opens once the text is waiting")
Choice.autoPick(false)
check(got == false and not Message.open, "cb(false) and prompt closed")
print("[test] 7. Ui.push cb fires on dismiss and in headless pump")
Ui.reset({ headless = false })
local fired = 0
Ui.push("hello", function() fired = fired + 1 end)
Ui.push("plain")
check(Ui.log()[1] == "hello" and Ui.log()[2] == "plain", "log stays strings")
Ui.pump()
check(fired == 0 and Message.open, "cb waits for dismiss")
Message.close()
check(fired == 1, "cb fired on dismiss")
Ui.reset({ headless = true })
Ui.push("x", function() fired = fired + 1 end)
Ui.pump()
check(fired == 2, "headless pump fires cb")
Ui.push("", function() fired = fired + 1 end)
check(fired == 3, "empty text fires cb immediately")
if failed > 0 then
print(string.format("[FAIL] %d check(s) failed", failed))
os.exit(1)
end
print("[PASS] game3 learn move battle contract")
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
package.loaded["src.ui.game3.intro_guide"] = {
beginControls = function() return { kind = "controls" } end,
beginPikachu = function() return { kind = "pikachu" } end,
start = function() end,
update = function() return false end,
draw = function() end,
}
package.loaded["src.ui.game3.naming_chrome"] = {
install = function() return true end,
ready = function() return true end,
get = function() return nil end,
}
local Audio = require("src.core.game3.audio")
local calls = {}
Audio.playCry = function(sp) calls[#calls + 1] = "cry:" .. tostring(sp) return true end
Audio.fadeOutBgm = function(s) calls[#calls + 1] = "fadeout:" .. tostring(s) return true end
Audio.playSong = function(id) calls[#calls + 1] = "song:" .. tostring(id) return true end
Audio.playSe = function(id) calls[#calls + 1] = "se:" .. tostring(id) return true end
local Boot = require("src.ui.game3.boot")
local FrlgFont = require("src.ui.game3.frlg_font")
local Chrome = require("src.ui.game3.chrome")
local function keys(...)
local set = {}
for _, k in ipairs({ ... }) do set[k] = true end
return { wasPressed = function(_, k) return set[k] == true end }
end
local function step(state, input, n)
local action
for _ = 1, (n or 1) do
action = Boot.update(state, input, 1 / 60) or action
end
return action
end
local function atTitle(hasContinue)
local boot = Boot.new()
Boot.setHasContinue(boot, hasContinue)
step(boot, keys("a"))
step(boot, nil)
for _ = 1, 120 do
if boot.phase == Boot.PHASE.TITLE then break end
step(boot, nil)
end
return boot
end
local function throughCry(boot)
for _ = 1, 300 do
if boot.phase ~= Boot.PHASE.TITLE_CRY then break end
step(boot, nil)
end
end
print("[test] 1. START on the title plays the cry, not the menu")
local boot = atTitle(true)
check(boot.phase == Boot.PHASE.TITLE, "reached TITLE")
calls = {}
step(boot, keys("start"))
check(boot.phase == "title_cry", "START -> title_cry (got " .. tostring(boot.phase) .. ")")
check(calls[1] == "cry:6", "Charizard cry played")
step(boot, nil, 89)
check(boot.phase == "title_cry", "still title_cry after 89 frames")
check((boot.white or 0) == 0, "no white yet during the 90-frame wait")
step(boot, nil, 2)
local sawFade = false
for _, c in ipairs(calls) do if c == "fadeout:4" then sawFade = true end end
check(sawFade, "FadeOutBGM(4) after the wait")
throughCry(boot)
check(boot.phase == Boot.PHASE.MENU, "with a save -> MENU after the white fade")
check(boot._titleActive == false, "title torn down")
check(boot.fadeT == 16 and boot.fadeColor == "white", "menu starts fading in from white")
print("[test] 2. Menu rows and cursor clamp")
local items = Boot.menuItems(boot)
check(#items == 2 and items[1] == "CONTINUE" and items[2] == "NEW GAME", "CONTINUE / NEW GAME only")
local hasOption = false
for _, it in ipairs(items) do if it == "OPTION" then hasOption = true end end
check(not hasOption, "no OPTION row")
step(boot, nil, 10)
check(boot.fadeT == 0, "fade-in finished")
step(boot, keys("up"))
check(boot.menuIndex == 1, "up at top stays at 1")
step(boot, keys("down"))
step(boot, nil)
step(boot, keys("down"))
check(boot.menuIndex == 2, "down at bottom stays at 2")
print("[test] 3. Menu draw never paints the title art")
local sentinels = { titleBorder = {}, titleMon = {}, titleLogo = {}, copyrightLayer = {}, titleScreen = {} }
for k, v in pairs(sentinels) do boot[k] = v end
local drewTitle, clearColor = false, nil
local origFont, origFrame = FrlgFont.draw, Chrome.stdFrame
FrlgFont.draw = function() return 0 end
Chrome.stdFrame = function() end
_G.love = {
graphics = {
clear = function(r, g, b) clearColor = { r, g, b } end,
setColor = function() end,
rectangle = function() end,
draw = function(img)
for _, v in pairs(sentinels) do if img == v then drewTitle = true end end
end,
},
}
Boot.draw(boot)
_G.love = nil
FrlgFont.draw, Chrome.stdFrame = origFont, origFrame
check(not drewTitle, "no title BG image drawn under the menu")
check(clearColor and math.abs(clearColor[1] * 255 - 139) < 1 and math.abs(clearColor[3] * 255 - 255) < 1,
"backdrop is main_menu bg.pal[0]")
print("[test] 4. B returns to the title after a black fade")
calls = {}
step(boot, keys("b"))
check(calls[1] == "se:5", "SE_SELECT on B")
check(boot.phase == Boot.PHASE.MENU and boot.fadeColor == "black", "fading to black")
step(boot, nil, 12)
check(boot.phase == Boot.PHASE.TITLE, "back on TITLE (got " .. tostring(boot.phase) .. ")")
check(boot._titleActive == true, "title re-entered")
print("[test] 5. No save goes straight to the new game")
local fresh = atTitle(false)
step(fresh, keys("start"))
local sawMenu = false
for _ = 1, 300 do
if fresh.phase == Boot.PHASE.MENU then sawMenu = true end
if fresh.phase ~= Boot.PHASE.TITLE_CRY then break end
step(fresh, nil)
end
check(not sawMenu, "no menu frame without a save")
check(fresh.phase == Boot.PHASE.CONTROLS, "CONTROLS after the white fade (got " .. tostring(fresh.phase) .. ")")
print("[test] 6. A on CONTINUE fades to black, then continues")
local cont = atTitle(true)
step(cont, keys("start"))
throughCry(cont)
step(cont, nil, 10)
local action = step(cont, keys("a"))
check(action == nil and cont.fadeColor == "black", "A starts a black fade first")
action = step(cont, nil, 12)
check(action and action.action == "continue", "continue action after the fade")
print("[test] 7. Continue stats from a save")
local info = Boot.continueInfoFromSave({
name = "BRYANTHABOI",
gender = 1,
playTime = { hours = 12, minutes = 5 },
flags = { ["2089"] = true, ["2080"] = true, ["2081"] = true },
dex = { caught = { [1] = true, [4] = true, [200] = true }, national = false },
})
check(info.name == "BRYANTH", "name clipped to 7")
check(info.hours == 12 and info.minutes == 5, "play time")
check(info.hasDex == true, "FLAG_SYS_POKEDEX_GET read")
check(info.badges == 2, "badge count 2 (got " .. tostring(info.badges) .. ")")
check(info.dexCount == 2, "kanto caught count 2 (got " .. tostring(info.dexCount) .. ")")
print("[test] 8. Title idle restart at 2700 frames")
local idle = atTitle(false)
step(idle, nil, 1790)
check(idle.phase == Boot.PHASE.TITLE, "still TITLE at 30s")
step(idle, nil, 920)
check(idle.phase == Boot.PHASE.INTRO, "restarts intro by 45s")
if failed == 0 then
print("\nAll game3 main menu tests passed.")
os.exit(0)
else
print("\n" .. failed .. " main menu test(s) failed.")
os.exit(1)
end
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local frames = {}
local draws = {}
love = love or {}
love.graphics = love.graphics or {}
love.graphics.setColor = function() end
love.graphics.rectangle = function() end
love.graphics.draw = function(img, x, y)
draws[#draws + 1] = { img = img, x = x, y = y }
end
package.loaded["src.ui.game3.chrome"] = {
stdFrame = function(tx, ty, tw, th)
frames[#frames + 1] = { tx, ty, tw, th }
end,
}
package.loaded["src.ui.game3.frlg_font"] = { COLOR = {} }
local MonPic = require("src.ui.game3.mon_pic")
local function run(x, y, ex, ey, epx, epy)
frames, draws = {}, {}
MonPic.active = true
MonPic.left, MonPic.top = x, y
MonPic._img = {}
MonPic._w, MonPic._h = 64, 64
MonPic.draw()
local f = frames[1]
check(f ~= nil, ("showmonpic %d,%d draws a frame"):format(x, y))
if not f then return end
check(f[1] == ex and f[2] == ey and f[3] == 8 and f[4] == 8,
("frame content at (%d,%d) 8x8, got (%s,%s)"):format(ex, ey, tostring(f[1]), tostring(f[2])))
local d = draws[1]
check(d ~= nil and d.x == epx and d.y == epy,
("pic top-left at (%d,%d), got (%s,%s)"):format(epx, epy, tostring(d and d.x), tostring(d and d.y)))
if d then
local fcx, fcy = (f[1] + 4) * 8, (f[2] + 4) * 8
check(d.x + 32 == fcx and d.y + 32 == fcy,
("pic center (%d,%d) matches frame center (%d,%d)"):format(d.x + 32, d.y + 32, fcx, fcy))
end
end
print("[test] showmonpic window centered on its pic")
run(10, 3, 11, 4, 88, 32)
run(0, 0, 1, 1, 8, 8)
if failed > 0 then
print(string.format("[FAIL] %d test(s) failed", failed))
os.exit(1)
else
print("[test] all passed")
end
+1 -1
View File
@@ -6,7 +6,7 @@
-- 4. Page transitions (INFO -> SKILLS -> MOVES -> MOVE_DETAIL)
-- 5. Modal Stack integration with PartyMenu
local Game3 = require("src.core.game3")
local Game3 = require("src.core.game3.init")
local PartyMenu = require("src.ui.game3.party_menu")
local SummaryMenu = require("src.ui.game3.summary_menu")
local SummaryData = require("src.core.game3.summary_data")
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local Ui = require("src.core.game3.battle.ui")
local Anim = require("src.core.game3.battle.anim")
local BattleBg = require("src.core.game3.battle.bg")
local Healthbox = require("src.core.game3.battle.healthbox")
local BattleChrome = require("src.ui.game3.battle_chrome")
local TrainerPic = require("src.core.game3.trainer_pic")
local Pokemon = require("src.core.game3.pokemon")
local IMG = {
enemyMon = { tag = "enemyMon" },
playerMon = { tag = "playerMon" },
trainerBack = { tag = "trainerBack" },
trainerFront = { tag = "trainerFront" },
}
local drawn = {}
local gfx = setmetatable({}, { __index = function() return function() end end })
gfx.draw = function(img) drawn[#drawn + 1] = img and img.tag end
gfx.newQuad = function() return {} end
_G.love = { graphics = gfx }
BattleBg.draw = function() return true end
Healthbox.draw = function() end
BattleChrome.drawPanel = function() end
BattleChrome.drawPartyBar = function() end
BattleChrome.manifest = function() return {} end
Anim.drawParticles = function() end
TrainerPic.front = function() return { image = IMG.trainerFront } end
TrainerPic.back = function() return { image = IMG.trainerBack } end
Pokemon.frontPic = function() return { image = IMG.enemyMon } end
Pokemon.backPic = function() return { image = IMG.playerMon } end
local function indexOf(tag)
for i, t in ipairs(drawn) do
if t == tag then return i end
end
return nil
end
print("[test] wild intro: enemy mon passes under player trainer back pic")
Ui.reset({ headless = false })
Anim.reset({ headless = false })
Ui._st = { enemy = { species = 95 }, player = { species = 4 } }
Anim.present("enemy").visible = true
Anim.present("enemy").ox = -48
local stage = Anim.stage()
stage.trainer.player.visible = true
stage.trainer.player.ox = 48
drawn = {}
Ui.draw(240, 160)
local iMon, iBack = indexOf("enemyMon"), indexOf("trainerBack")
check(iMon ~= nil and iBack ~= nil, "both enemy mon and trainer back drawn")
check(iMon and iBack and iMon < iBack, "enemy mon drawn before player trainer back")
print("[test] trainer intro: enemy trainer front under player trainer back")
Ui.reset({ headless = false })
Anim.reset({ headless = false })
Ui._st = { enemy = { species = 7 }, player = { species = 4 } }
stage = Anim.stage()
stage.trainer.enemy.visible = true
stage.trainer.enemy.picId = 106
stage.trainer.player.visible = true
drawn = {}
Ui.draw(240, 160)
local iFront = indexOf("trainerFront")
iBack = indexOf("trainerBack")
check(iFront and iBack and iFront < iBack, "enemy trainer front drawn before player trainer back")
print("[test] send-out: player mon still above enemy mon")
Ui.reset({ headless = false })
Anim.reset({ headless = false })
Ui._st = { enemy = { species = 95 }, player = { species = 4 } }
Anim.present("enemy").visible = true
Anim.present("player").visible = true
drawn = {}
Ui.draw(240, 160)
iMon = indexOf("enemyMon")
local iPlayer = indexOf("playerMon")
check(iMon and iPlayer and iMon < iPlayer, "enemy mon drawn before player mon")
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")