#!/usr/bin/env luajit package.path = "./?.lua;./?/init.lua;" .. package.path require("tests.game3_cache").mountOrSkip("game3_learn_move_test", "scripts/text.lua") 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 Summary = { open = false, opens = 0, last = nil } function Summary.openMenu(party, idx, opts) Summary.open = true Summary.opens = Summary.opens + 1 Summary.last = { party = party, idx = idx, opts = opts or {} } end function Summary.isOpen() return Summary.open end function Summary.pick(slotIdx) local o = Summary.last and Summary.last.opts or {} Summary.open = false if o.onSelectMove then o.onSelectMove(slotIdx) end end package.loaded["src.ui.game3.summary_menu"] = Summary 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 Summary.open = false Summary.opens = 0 Summary.last = nil 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, ctx) Ui.askForget(labels, cb, ctx) end, headless = false, battleText = true, } local yesNoLayouts = {} 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, battleText = hooks.battleText, onDone = function(learned) result = learned end, }) for _ = 1, 200 do if result ~= nil then break end if Summary.open then local ans = table.remove(answers, 1) if ans == "cancel" then Summary.pick(nil) else Summary.pick(ans) end elseif 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 "" yesNoLayouts[#yesNoLayouts + 1] = { kind = Choice.kind, style = Choice.style, left = Choice.left, top = Choice.top, labels = Choice.options and table.concat(Choice.options, "/"), } 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) local Pokemon = require("src.core.game3.pokemon") local names = { [52] = "EMBER", [10] = "SCRATCH", [45] = "GROWL", [15] = "CUT", [33] = "TACKLE", [39] = "TAIL WHIP" } local function canon(s) if type(s) ~= "string" then return s end for id, want in pairs(names) do local got = Pokemon.moveName(id) or ("MOVE " .. id) if got ~= want then local i = s:find(got, 1, true) while i do s = s:sub(1, i - 1) .. want .. s:sub(i + #got) i = s:find(got, i + #want, true) end end end return s end local shownCanon = {} for i, t in ipairs(Message.shown) do shownCanon[i] = canon(t) end for i, t in ipairs(prompts) do prompts[i] = canon(t) end return { ok = ok, err = err, result = result, mon = mon, shown = shownCanon, prompts = prompts, active = LearnMove.busy(), open = Message.open, openedWhileTyping = openedWhileTyping, layouts = yesNoLayouts, summaryOpens = Summary.opens, summaryLast = Summary.last, } end local function count_exact(shown, text) local n = 0 for _, t in ipairs(shown) do if (t:gsub("\\p$", "")) == text then n = n + 1 end end return n 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") check(idx_of(r.shown, "CHARMANDER learned\nEMBER!") ~= nil, "PKMNLEARNEDMOVE text") print("[test] 2. four moves: NO delete, YES stop -> did not learn (ROM strings)") r = run({ 10, 45, 33, 39 }, { false, true }) check(r.ok, "no error " .. tostring(r.err)) local ord, miss = in_order(r.shown, { "CHARMANDER is trying to\nlearn EMBER.", "But, CHARMANDER can't learn\nmore than four moves.", "Delete a move to make\nroom for EMBER?", "Stop learning\nEMBER?", "CHARMANDER did not learn\nEMBER.", }) check(ord, "TRYTOLEARNMOVE1/2/3, STOPLEARNINGMOVE, DIDNOTLEARNMOVE order (missing " .. tostring(miss) .. ")") check(idx_of(r.shown, "Should a move be deleted") == nil and idx_of(r.shown, "Stop trying") == nil, "field party-menu wording not used in battle") check(r.result == false, "onDone(false)") check(not r.active, "LearnMove idle") check(r.prompts[1] == "Delete a move to make\nroom for EMBER?", "delete prompt text stays up under Yes/No") check(r.prompts[2] == "Stop learning\nEMBER?", "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") local lay = r.layouts[1] or {} check(lay.kind == "yesno" and lay.style == "battle" and lay.left == 24 and lay.top == 9, "Yes/No content at tiles (24,9) 5x4, frame 23..29 x 8..13") check(lay.labels == "Yes/No", "gText_BattleYesNoChoice labels") check(r.summaryOpens == 0, "summary never opened on the stop path") print("[test] 3. four moves: YES delete -> summary select move, forget slot 1 -> Poof chain") r = run({ 10, 45, 33, 39 }, { true, 0 }) check(r.ok, "no error " .. tostring(r.err)) check(r.summaryOpens == 1, "summary move-select opened once") local so = r.summaryLast and r.summaryLast.opts or {} check(so.mode == "select_move" and so.moveToLearn == 52, "summary in select_move mode with EMBER as fifth move") check(r.summaryLast and r.summaryLast.party and r.summaryLast.party[1] == r.mon, "summary shows the learning mon") for _, l in ipairs(r.layouts) do check(l.kind ~= "multi", "no Choice.multi forget list") end ord, miss = in_order(r.shown, { "Delete a move to make\nroom for EMBER?", "1, 2, and… … … Poof!", "CHARMANDER forgot\nSCRATCH.", "And…", "CHARMANDER learned\nEMBER!", }) check(ord, "123POOF, PKMNFORGOTMOVE, ANDELLIPSIS, PKMNLEARNEDMOVE 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 picked: HM text then summary reopens, pick slot 2") r = run({ 15, 45, 33, 39 }, { true, 0, 1 }) check(r.ok, "no error " .. tostring(r.err)) ord, miss = in_order(r.shown, { "Delete a move to make", "HM moves can't be\nforgotten now.", "CHARMANDER forgot\nGROWL.", "CHARMANDER learned\nEMBER!" }) check(ord, "message order (missing " .. tostring(miss) .. ")") check(r.summaryOpens == 2, "summary reopened after HM refusal without re-asking") check(count_exact(r.shown, "Delete a move to make\nroom for EMBER?") == 1, "no second delete prompt") check(r.result == true, "onDone(true)") check(r.mon.moves[1] == 15 and r.mon.moves[2] == 52, "CUT kept, EMBER in slot 2") print("[test] 5. summary 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)") ord, miss = in_order(r.shown, { "Delete a move to make", "Stop learning\nEMBER?", "CHARMANDER did not learn\nEMBER." }) check(ord, "cancel -> stop -> did not learn (missing " .. tostring(miss) .. ")") print("[test] 5b. NO to Stop learning re-asks from TRYTOLEARNMOVE1") r = run({ 10, 45, 33, 39 }, { false, false, false, true }) check(r.ok, "no error " .. tostring(r.err)) check(count_exact(r.shown, "CHARMANDER is trying to\nlearn EMBER.") == 2, "TRYTOLEARNMOVE1 printed twice") check(count_exact(r.shown, "But, CHARMANDER can't learn\nmore than four moves.") == 2, "TRYTOLEARNMOVE2 printed twice") check(count_exact(r.shown, "Delete a move to make\nroom for EMBER?") == 2, "TRYTOLEARNMOVE3 printed twice") check(r.result == false, "onDone(false)") 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") print("[test] 8. real summary select_move: HM pick shows the HM notice and stays open") package.loaded["src.ui.game3.summary_menu"] = nil local okS, RealSummary = pcall(require, "src.ui.game3.summary_menu") check(okS and RealSummary and RealSummary.openMenu, "summary_menu loads") if okS and RealSummary then local function inputFor(key) return { wasPressed = function(_, k) return k == key end, isDown = function() return false end } end local picked = "unset" local hmMon = { species = 4, level = 19, moves = { 15, 45, 33, 39 }, pp = { 30, 40, 35, 30 } } local okOpen, errOpen = pcall(RealSummary.openMenu, { hmMon }, 1, { mode = "select_move", moveToLearn = 52, onSelectMove = function(slot) picked = slot end, }) check(okOpen, "openMenu ok " .. tostring(errOpen)) RealSummary.handleInput(inputFor("a")) check(RealSummary.isOpen() and picked == "unset", "HM slot not selectable") check(RealSummary._hmNotice == true, "HM moves can't be forgotten notice raised") RealSummary.handleInput(inputFor("down")) check(RealSummary._hmNotice == false, "cursor move clears the notice") RealSummary.handleInput(inputFor("a")) check(picked == 1 and not RealSummary.isOpen(), "non-HM slot 2 returns index 1") end if failed > 0 then print(string.format("[FAIL] %d check(s) failed", failed)) os.exit(1) end print("[PASS] game3 learn move battle contract")