-- Mom's savings, special BankOfMom (engine/events/mom.asm). ROM-free: -- luajit tests/gen2_bank_of_mom_test.lua package.path = "./?.lua;./?/init.lua;" .. package.path local S = require("tests.harness").suite("gen2 bank of mom") local check, eq = S.check, S.eq local Vm = require("src.script.gen2.Vm") local Events = require("src.world.gen2.Events") local Specials = require("src.script.gen2.Specials") local Save = require("src.core.gen2.Save") check(Specials.STUBS.BankOfMom == nil, "BankOfMom is a HANDLER: the six-digit keypad it needed now exists") check(Specials.HANDLERS.BankOfMom ~= nil, "and special dispatch resolves to it") -- A minimal Gold save: just the two accounts BankOfMom reads and writes. local function record(money, saved, active, savingMoney) return { player = { money = money or 0 }, mom = { savedMoney = saved or 0, active = active or false, savingMoney = savingMoney or false }, } end -- Wires H.BankOfMom's whole `hooks(vm)` surface: `money`/`setMoney` over the -- two accounts (YOUR_MONEY 0, MOMS_MONEY 1), `bankOfMomAmount` answering off -- a fixed queue the way a real keypad answers one amount per open, and -- `playSfxNamed` recorded so the transaction jingle can be checked for. local function bankVm(rec, amounts) local sfx = {} local amountIndex = 0 local vm = Vm.new({}, {}, Events.new(), { specials = { save = function() return rec end, money = function(account) return account == 1 and rec.mom.savedMoney or rec.player.money end, setMoney = function(account, value) if account == 1 then rec.mom.savedMoney = value else rec.player.money = value end end, bankOfMomAmount = function(_kind, _saved, _held, done) amountIndex = amountIndex + 1 done(amounts and amounts[amountIndex]) end, playSfxNamed = function(name, fallback) sfx[#sfx + 1] = { name = name, fallback = fallback } end, }, }) vm.showTextFn = function() end vm.sfx = sfx return vm end -- Drives the coroutine to completion, feeding a `yesorno` off `yesAnswers` and -- a `menu` off `menuAnswers` (both 1-based queues, consumed in the order the -- handler asks), and collects every `text` yield along the way -- the same -- shape tests/gen2_vm_test.lua's driveMoveDeletion/driveNameRater use, with -- a `menu` arm added for BankOfMom's GET/SAVE/CHANGE/CANCEL screen. local function drive(vm, yesAnswers, menuAnswers) local texts, menus = {}, {} local yi, mi = 0, 0 local resumeArg = nil local ok, req = coroutine.resume(vm.co, resumeArg) while true do if not ok then error(req) end if req and req.kind == "text" then texts[#texts + 1] = req.text end if req and req.kind == "menu" then menus[#menus + 1] = req.header end if coroutine.status(vm.co) == "dead" then break end resumeArg = nil if req and req.kind == "yesorno" then yi = yi + 1 resumeArg = yesAnswers[yi] elseif req and req.kind == "menu" then mi = mi + 1 resumeArg = menuAnswers[mi] end ok, req = coroutine.resume(vm.co, resumeArg) end return texts, menus end local function run(vm, yesAnswers, menuAnswers) vm.co = coroutine.create(function() Specials.HANDLERS.BankOfMom(vm) end) return drive(vm, yesAnswers or {}, menuAnswers or {}) end -- ---- .CheckIfBankInitialized / .InitializeBank ----------------------------- -- The very first visit skips IsThisAboutYourMoney and jumps straight to -- "shall I save your money?" do local rec = record(3000, 0, false, false) local vm = bankVm(rec) local texts = run(vm, { true }) eq(#texts, 3, "leaving1, leaving2 (accepted), leaving3") check(texts[1]:find("cute", 1, true) ~= nil, "MomLeavingText1") check(texts[2]:find("take care", 1, true) ~= nil, "MomLeavingText2") check(texts[3]:find("careful", 1, true) ~= nil, "MomLeavingText3") check(rec.mom.active, "MOM_ACTIVE_F is set either way") check(rec.mom.savingMoney, "and MOM_SAVING_SOME_MONEY_F, since the answer was yes") end do local rec = record(3000, 0, false, false) local vm = bankVm(rec) local texts = run(vm, { false }) eq(#texts, 2, "leaving1 and leaving3 only -- MomLeavingText2 is skipped") check(texts[2]:find("careful", 1, true) ~= nil, "still MomLeavingText3") check(rec.mom.active, "the bank is still marked initialized") check(not rec.mom.savingMoney, "but nothing is being saved") end -- ---- .IsThisAboutYourMoney -------------------------------------------------- do local rec = record(1000, 500, true, true) local texts = run(bankVm(rec), { false }) eq(#texts, 2, "the room-tidy line, then the fallback close") check(texts[2]:find("Just do what", 1, true) ~= nil, "declining reads as MomJustDoWhatYouCanText -- DSTChecks does not apply " .. "in this port (no wStartHour offset to nudge)") eq(rec.player.money, 1000, "and nothing about either account moved") eq(rec.mom.savedMoney, 500, "") end -- ---- .AccessBankOfMom: CANCEL and B both fall through to the same close ---- do local rec = record(1000, 500, true, true) local texts, menus = run(bankVm(rec), { true }, { 4 }) check(#menus == 1, "the GET/SAVE/CHANGE/CANCEL menu opened once") eq(menus[1].items[1], "GET", "GET is withdraw") eq(menus[1].items[2], "SAVE", "SAVE is deposit") eq(menus[1].items[3], "CHANGE", "CHANGE is the savings toggle") eq(menus[1].items[4], "CANCEL", "") check(texts[#texts]:find("Just do what", 1, true) ~= nil, "CANCEL falls to MomJustDoWhatYouCanText") end do local rec = record(1000, 500, true, true) local texts = run(bankVm(rec), { true }, { 0 }) check(texts[#texts]:find("Just do what", 1, true) ~= nil, "so does B (menu answers 0, the way Script_verticalmenu's carry does)") end -- ---- .StoreMoney (SAVE / deposit) ------------------------------------------ do -- First offer is more than the wallet holds: retried in place, no state -- change, then a good amount goes through. local rec = record(1000, 500, true, true) local vm = bankVm(rec, { 2000, 300 }) local texts = run(vm, { true }, { 2 }) check(texts[4]:find("don't have", 1, true) ~= nil, "MomInsufficientFundsInWalletText for the first, too-big offer") check(texts[#texts]:find("safe", 1, true) ~= nil, "MomStoredMoneyText closes it") eq(rec.player.money, 700, "the wallet paid out the SECOND, valid amount") eq(rec.mom.savedMoney, 800, "and the savings account received it") eq(#vm.sfx, 1, "the transaction jingle played once") eq(vm.sfx[1].name, "Sfx_Transaction", "by its pokegold label") end -- Depositing enough to overflow Mom's account clamps it to the cap and -- reports "no room" -- but the clamp already landed, exactly the way -- GiveMoney commits before it reports carry (engine/events/money.asm). The -- wallet is never charged for a deposit that could not fully land. do local rec = record(999999, 999990, true, true) local vm = bankVm(rec, { 20, 0 }) local texts = run(vm, { true }, { 2 }) check(texts[4]:find("can't save", 1, true) ~= nil, "MomNotEnoughRoomInBankText") eq(rec.mom.savedMoney, 999999, "clamped to MAX_MONEY regardless") eq(rec.player.money, 999999, "the wallet was never touched by the failed leg") check(texts[#texts]:find("Just do what", 1, true) ~= nil, "entering 0 on the retry cancels out to the same close as B") end -- A zero entry or a flat B (bankOfMomAmount answering nil) both read as -- CancelDeposit -- no retry, straight to the close. do local rec = record(1000, 500, true, true) local texts = run(bankVm(rec, { 0 }), { true }, { 2 }) check(texts[#texts]:find("Just do what", 1, true) ~= nil, "amount 0 cancels") eq(rec.player.money, 1000, "untouched") end do local rec = record(1000, 500, true, true) local texts = run(bankVm(rec, { nil }), { true }, { 2 }) check(texts[#texts]:find("Just do what", 1, true) ~= nil, "B (nil) cancels too") end -- ---- .TakeMoney (GET / withdraw) ------------------------------------------- do local rec = record(200, 900, true, true) local vm = bankVm(rec, { 1000, 400 }) local texts = run(vm, { true }, { 1 }) check(texts[4]:find("haven't saved", 1, true) ~= nil, "MomHaventSavedThatMuchText for the first offer, more than is saved") check(texts[#texts]:find("don't", 1, true) ~= nil or texts[#texts]:find("give up", 1, true) ~= nil, "MomTakenMoneyText closes it") eq(rec.player.money, 600, "the wallet received the withdrawal") eq(rec.mom.savedMoney, 500, "and the savings account paid it out") eq(vm.sfx[1].name, "Sfx_Transaction", "the same jingle as a deposit") end -- The mirror of the deposit overflow: too much withdrawn to fit the wallet -- clamps the WALLET to the cap and leaves the savings account untouched. do local rec = record(999990, 999999, true, true) local vm = bankVm(rec, { 20 }) local texts = run(vm, { true }, { 1 }) check(texts[4]:find("can't take", 1, true) ~= nil, "MomNotEnoughRoomInWalletText") eq(rec.player.money, 999999, "the wallet was clamped to MAX_MONEY") eq(rec.mom.savedMoney, 999999, "and Mom's account never paid out the failed leg") end -- ---- .StopOrStartSavingMoney (CHANGE) -------------------------------------- do local rec = record(1000, 500, true, false) local texts = run(bankVm(rec), { true, true }, { 3 }) check(texts[#texts]:find("Trust me", 1, true) ~= nil, "MomStartSavingMoneyText") check(rec.mom.savingMoney, "and MOM_SAVING_SOME_MONEY_F is set") end do local rec = record(1000, 500, true, true) local texts = run(bankVm(rec), { true, false }, { 3 }) check(texts[#texts]:find("Just do what", 1, true) ~= nil, "declining still closes on MomJustDoWhatYouCanText, with no line of its own") check(not rec.mom.savingMoney, "but the flag comes off") check(rec.mom.active, "MOM_ACTIVE_F stays set -- the bank itself is not undone") end -- ---- Save.lua: the fields BankOfMom reads and writes ----------------------- do local fresh = Save.newGame({}) eq(fresh.mom.active, false, "a new save has never talked to the bank") eq(fresh.mom.savingMoney, false, "and nothing is being saved yet") eq(fresh.mom.savedMoney, 0, "wMomsMoney starts empty") -- An older save's `mom` table predates `active`/`savingMoney` entirely. local old = { mom = { name = "MOM" } } Save.normalize(old) eq(old.mom.active, false, "normalize fills in the unset bank state") eq(old.mom.savingMoney, false, "") eq(old.mom.savedMoney, 0, "and the balance, clamped the way player.money is") local over = { mom = { savedMoney = 5000000 } } Save.normalize(over) eq(over.mom.savedMoney, Save.MAX_MONEY, "an out-of-range balance is clamped, not trusted") end -- ---- World:whiteOut -- HalveMoney (engine/events/whiteout.asm) ------------ do local World = require("src.world.gen2.World") local healed, warped = false, false local fakeSelf = { game = { save = { player = { money = 5001 } } }, showText = function(_self, _text, onDone) onDone() end, healParty = function() healed = true end, warpToSpawn = function() warped = true end, } World.whiteOut(fakeSelf) eq(fakeSelf.game.save.player.money, 2500, "halved and floored") check(healed, "HealParty still runs") check(warped, "and WarpToSpawnPoint still runs") end S.finish()