mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
bazinga
This commit is contained in:
+5
-2
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Package entry for Game3 runtime modules.
|
||||
return require("src.core.game3.init")
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user