mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
23617f43b4
CLOSES #2290, CLOSES #2338, CLOSES #2186, CLOSES #2305, CLOSES #2332, CLOSES #2320, CLOSES #2344, CLOSES #2291, CLOSES #2335, CLOSES #2340, CLOSES #2385, CLOSES #2352, CLOSES #2378, CLOSES #2403, CLOSES #2404, CLOSES #2405, CLOSES #2367, CLOSES #2037, CLOSES #2177, CLOSES #2368, CLOSES #2382, CLOSES #1951
1081 lines
42 KiB
Lua
1081 lines
42 KiB
Lua
-- The Pokecenter PC and the player's item PC.
|
|
--
|
|
-- luajit tests/gen2_pc_screens_test.lua (GOLD_CACHE for the map runs)
|
|
--
|
|
-- Three layers, matching how the cart reaches them:
|
|
--
|
|
-- * World:interact's CheckFacingTileForStdScript arm
|
|
-- (engine/events/std_collision.asm): an A press on a COLL_PC tile runs
|
|
-- PCScript out of std_scripts, which is the ONLY way any Pokecenter PC
|
|
-- opens -- no Pokecenter map carries a PC bg event.
|
|
-- * CenterPcMenu (engine/events/pokecenter_pc.asm PokemonCenterPC): the
|
|
-- whose-PC list and its .ChooseWhichPCListToUse gating, the party gate,
|
|
-- and the rows it opens.
|
|
-- * ItemPcMenu (_PlayersPC + the PlayerWithdraw/Deposit/TossItemMenu rows):
|
|
-- items moving bag <-> save.pcItems for real, with the cart's caps and
|
|
-- refusals.
|
|
--
|
|
-- With a GOLD_CACHE the whole chain runs on the real Cherrygrove Pokecenter
|
|
-- and the real bedroom map: a real Map, a real Vm over the extracted scripts,
|
|
-- and World:openPc pushing the real screens.
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
love = love or {}
|
|
love.graphics = love.graphics or {
|
|
getColor = function() return 1, 1, 1, 1 end,
|
|
setColor = function() end,
|
|
rectangle = function() end,
|
|
print = function() end,
|
|
printf = function() end,
|
|
draw = function() end,
|
|
newQuad = function() return {} end,
|
|
newImage = function() return nil end,
|
|
getShader = function() return nil end,
|
|
setShader = function() end,
|
|
newShader = function() error("no shaders in this harness") end,
|
|
getDimensions = function() return 160, 144 end,
|
|
push = function() end, pop = function() end,
|
|
translate = function() end, scale = function() end,
|
|
circle = function() end, clear = function() end,
|
|
setLineWidth = function() end,
|
|
}
|
|
love.math = love.math or {
|
|
random = function(a, b)
|
|
if b then return a end
|
|
return a and 1 or 0.5
|
|
end,
|
|
}
|
|
love.filesystem = love.filesystem or {
|
|
load = function() return nil end,
|
|
getInfo = function() return nil end,
|
|
read = function() return nil end,
|
|
write = function() return true end,
|
|
remove = function() return true end,
|
|
}
|
|
love.timer = love.timer or { getTime = function() return 0 end }
|
|
|
|
require("src.core.Logger").warn = function() end
|
|
|
|
local S = require("tests.harness").suite("gen2 pc screens")
|
|
local check, eq = S.check, S.eq
|
|
|
|
local Bag = require("src.inventory.Bag")
|
|
local CenterPcMenu = require("src.ui.gen2.CenterPcMenu")
|
|
local ItemPcMenu = require("src.ui.gen2.ItemPcMenu")
|
|
local PcMenu = require("src.ui.gen2.PcMenu")
|
|
local Map = require("src.world.gen2.Map")
|
|
local Save = require("src.core.gen2.Save")
|
|
local Screens = require("src.ui.Screens")
|
|
local Strings = require("src.core.Strings")
|
|
local Vm = require("src.script.gen2.Vm")
|
|
local World = require("src.world.gen2.World")
|
|
|
|
local function newInput()
|
|
local input = { pressed = {} }
|
|
function input:press(...)
|
|
for _, button in ipairs({ ... }) do self.pressed[button] = true end
|
|
end
|
|
function input:wasPressed(button)
|
|
if self.pressed[button] then
|
|
self.pressed[button] = nil
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
function input:isDown() return false end
|
|
return input
|
|
end
|
|
|
|
local ITEMS = {
|
|
POTION = { id = "POTION", name = "POTION", pocket = "ITEM", index = 2,
|
|
canToss = true },
|
|
ANTIDOTE = { id = "ANTIDOTE", name = "ANTIDOTE", pocket = "ITEM", index = 9,
|
|
canToss = true },
|
|
BICYCLE = { id = "BICYCLE", name = "BICYCLE", pocket = "KEY_ITEM", index = 6,
|
|
canToss = false },
|
|
HM_CUT = { id = "HM_CUT", name = "HM01", pocket = "TM_HM", index = 0xf3,
|
|
canToss = false },
|
|
TM_HEADBUTT = { id = "TM_HEADBUTT", name = "TM02", pocket = "TM_HM",
|
|
index = 0xea, canToss = true, teaches = "HEADBUTT" },
|
|
}
|
|
|
|
local function newGame(save, items)
|
|
local input = newInput()
|
|
return {
|
|
input = input,
|
|
save = save,
|
|
data = { audio = {}, pokemon = {}, items = items or ITEMS },
|
|
stack = { _items = {},
|
|
push = function(self, s) self._items[#self._items + 1] = s end,
|
|
pop = function(self) return table.remove(self._items) end,
|
|
top = function(self) return self._items[#self._items] end,
|
|
},
|
|
}, input
|
|
end
|
|
|
|
local function newSave(partySize)
|
|
local save = Save.newGame({ playerName = "GOLD", trainerId = 1234 })
|
|
for i = 1, partySize or 1 do
|
|
save.party[i] = { species = "CYNDAQUIL", nickname = "MON" .. i,
|
|
hp = 20, maxHp = 20, level = 10 }
|
|
end
|
|
return save
|
|
end
|
|
|
|
-- Drive a screen: press the buttons one at a time, updating after each.
|
|
-- ../pokecrystal/home/print_text.asm:1
|
|
local function settle(screen)
|
|
for _ = 1, 600 do
|
|
if not (screen.typer and not screen.typer:done()) then break end
|
|
screen:update(0)
|
|
end
|
|
end
|
|
|
|
local function press(screen, input, ...)
|
|
for _, button in ipairs({ ... }) do
|
|
settle(screen)
|
|
input:press(button)
|
|
screen:update(0)
|
|
end
|
|
end
|
|
|
|
local function entryIds(entries)
|
|
local ids = {}
|
|
for _, entry in ipairs(entries) do ids[#ids + 1] = entry.id end
|
|
return table.concat(ids, ",")
|
|
end
|
|
|
|
-- ------------------------------------------------- the ids resolve
|
|
|
|
Screens.invalidate()
|
|
local sg = { data = {} }
|
|
eq(Screens.get(sg, "Gen2CenterPcMenu"), CenterPcMenu,
|
|
"Gen2CenterPcMenu resolves to its builtin")
|
|
eq(Screens.get(sg, "Gen2ItemPcMenu"), ItemPcMenu,
|
|
"Gen2ItemPcMenu resolves to its builtin")
|
|
|
|
-- Built-in PC rows are finite UI labels. The player's name remains raw and
|
|
-- is interpolated only after the surrounding label has been translated.
|
|
do
|
|
local Chrome = require("src.ui.gen2.Chrome")
|
|
local priorPrint = Chrome.print
|
|
local printed = {}
|
|
Chrome.print = function(text) printed[#printed + 1] = text end
|
|
Strings.load({ strings = {
|
|
["BILL's PC"] = "PC DE LEO",
|
|
["%s's PC"] = "PC DE %s",
|
|
["TURN OFF"] = "ETEINDRE",
|
|
["WITHDRAW <PK><MN>"] = "RETIRER <PK><MN>",
|
|
["WITHDRAW ITEM"] = "RETIRER OBJET",
|
|
["LOG OFF"] = "DECONNEXION",
|
|
} })
|
|
|
|
local save = newSave(1)
|
|
local game = newGame(save)
|
|
local center = CenterPcMenu.new(game, { save = save })
|
|
center.message = nil
|
|
-- drawPanel only draws the entry rows once booted (dev's PC boot-sequence
|
|
-- gate); this check is about label translation, not the boot animation.
|
|
center.booted = true
|
|
center:drawPanel()
|
|
local pc = PcMenu.new(game, { save = save, bills = true })
|
|
pc:drawPanel()
|
|
local items = ItemPcMenu.new(game, { save = save })
|
|
items:drawPanel()
|
|
local all = table.concat(printed, "|")
|
|
check(all:find("PC DE LEO", 1, true) ~= nil, "BILL's PC is translated")
|
|
check(all:find("PC DE GOLD", 1, true) ~= nil,
|
|
"the player name stays raw inside a translated PC label")
|
|
check(all:find("RETIRER <PK><MN>", 1, true) ~= nil,
|
|
"the storage PC action is translated")
|
|
check(all:find("RETIRER OBJET", 1, true) ~= nil,
|
|
"the item PC action is translated")
|
|
check(all:find("DECONNEXION", 1, true) ~= nil,
|
|
"the item PC exit is translated")
|
|
|
|
Strings.load({})
|
|
Chrome.print = priorPrint
|
|
end
|
|
|
|
-- PC messages are whole catalog keys, including the dynamic item-name and
|
|
-- quantity templates. Names supplied by registries remain raw arguments.
|
|
do
|
|
Strings.load({ strings = {
|
|
["{PLAYER} turned on\nthe PC."] = "{PLAYER} ACTIVE\nPC-FR.",
|
|
["BILL's PC\naccessed.\n\n#MON Storage\nSystem opened."] =
|
|
"PC LEO OUVERT.\n\nSTOCKAGE OUVERT.",
|
|
["What do you want\nto do?"] = "QUE FAIRE?",
|
|
["Withdrew %d\n%s(S)."] = "PRIS %d\n%s.",
|
|
["Toss out how many\n%s(S)?"] = "JETER COMBIEN\n%s?",
|
|
POTION = "INTERDIT",
|
|
} })
|
|
local save = newSave(1)
|
|
save.pcItems.POTION = 2
|
|
local game = newGame(save)
|
|
|
|
local center = CenterPcMenu.new(game, { save = save })
|
|
eq(center.message.pages[1][1], "{PLAYER} ACTIVE",
|
|
"the Center PC boot message is translated as a whole key")
|
|
center.message = nil
|
|
center.index = 1
|
|
center:choose()
|
|
eq(center.message.pages[1][1], "PC LEO OUVERT.",
|
|
"the Center PC access flow is translated")
|
|
|
|
local itemPc = ItemPcMenu.new(game, { save = save, items = ITEMS })
|
|
itemPc:withdraw({ id = "POTION", name = "POTION" }, 1)
|
|
eq(itemPc.message.pages[1][1], "PRIS 1",
|
|
"the item PC quantity template is translated")
|
|
eq(itemPc.message.pages[1][2], "POTION.",
|
|
"an item name remains a raw template argument")
|
|
itemPc.message = nil
|
|
itemPc.rows = { { id = "POTION", name = "POTION", count = 1 } }
|
|
itemPc.listIndex = 1
|
|
itemPc:chooseToss()
|
|
eq(itemPc.qtyState.prompt[1], "JETER COMBIEN",
|
|
"the item PC toss prompt is translated")
|
|
eq(itemPc.qtyState.prompt[2], "POTION?",
|
|
"the toss prompt also preserves the item name argument")
|
|
Strings.load({})
|
|
end
|
|
|
|
-- A mod may intentionally reuse a built-in source label. Only rows marked by
|
|
-- the engine are translated; collision labels injected through the hook stay
|
|
-- byte-for-byte mod content.
|
|
do
|
|
local Chrome = require("src.ui.gen2.Chrome")
|
|
local Hooks = require("src.mods.Hooks")
|
|
local Runtime = require("src.mods.Runtime")
|
|
local priorEvents, priorHooks, priorErrors =
|
|
Runtime.events, Runtime.hooks, Runtime.errors
|
|
local priorPrint = Chrome.print
|
|
local printed = {}
|
|
Chrome.print = function(text) printed[#printed + 1] = text end
|
|
Strings.load({ strings = {
|
|
["WITHDRAW <PK><MN>"] = "RETIRER MON",
|
|
["WITHDRAW ITEM"] = "RETIRER OBJET",
|
|
} })
|
|
|
|
local function hookedWith(label)
|
|
local hooks = Hooks.new()
|
|
hooks:wrap("ui.pc.items", function(nextFn, game, entries)
|
|
entries = nextFn(game, entries)
|
|
entries[#entries + 1] = { id = "collision", label = label }
|
|
return entries
|
|
end)
|
|
Runtime.install(priorEvents, hooks, priorErrors or {})
|
|
end
|
|
|
|
local save = newSave(1)
|
|
local game = newGame(save)
|
|
hookedWith("WITHDRAW <PK><MN>")
|
|
PcMenu.new(game, { save = save, bills = true }):drawPanel()
|
|
local all = table.concat(printed, "|")
|
|
check(all:find("RETIRER MON", 1, true) ~= nil,
|
|
"PcMenu translates its marked built-in row")
|
|
check(all:find("WITHDRAW <PK><MN>", 1, true) ~= nil,
|
|
"PcMenu leaves a colliding hook label raw")
|
|
|
|
printed = {}
|
|
hookedWith("WITHDRAW ITEM")
|
|
ItemPcMenu.new(game, { save = save }):drawPanel()
|
|
all = table.concat(printed, "|")
|
|
check(all:find("RETIRER OBJET", 1, true) ~= nil,
|
|
"ItemPcMenu translates its marked built-in row")
|
|
check(all:find("WITHDRAW ITEM", 1, true) ~= nil,
|
|
"ItemPcMenu leaves a colliding hook label raw")
|
|
|
|
Strings.load({})
|
|
Chrome.print = priorPrint
|
|
Runtime.install(priorEvents, priorHooks, priorErrors)
|
|
end
|
|
|
|
-- ------------------------------------------------- the whose-PC gating
|
|
|
|
-- PCPC_BEFORE_POKEDEX: three rows.
|
|
do
|
|
local save = newSave(1)
|
|
local game, input = newGame(save)
|
|
local pc = CenterPcMenu.new(game, { save = save, onClose = function() end })
|
|
check(pc.message ~= nil, "the PC boots with the turn-on line")
|
|
press(pc, input, "a")
|
|
eq(pc.message, nil, "which one A clears")
|
|
eq(entryIds(pc.entries), "bills,players,turnoff",
|
|
"no #DEX: BILL's PC / <PLAYER>'s PC / TURN OFF")
|
|
eq(pc.entries[2].label, "GOLD's PC", "the item PC row carries the name")
|
|
end
|
|
|
|
-- PCPC_BEFORE_HOF: PROF.OAK's PC appears with CheckReceivedDex.
|
|
do
|
|
local save = newSave(1)
|
|
save.engineFlags = { [CenterPcMenu.ENGINE_POKEDEX] = true }
|
|
local game = newGame(save)
|
|
local pc = CenterPcMenu.new(game, { save = save })
|
|
eq(entryIds(pc.entries), "bills,players,oaks,turnoff",
|
|
"with the #DEX the OAK row slots in above TURN OFF")
|
|
end
|
|
|
|
-- PCPC_POSTGAME: HALL OF FAME appears once wHallOfFameCount is non-zero.
|
|
do
|
|
local save = newSave(1)
|
|
save.engineFlags = { [CenterPcMenu.ENGINE_POKEDEX] = true }
|
|
save.hallOfFame.count = 1
|
|
local game = newGame(save)
|
|
local pc = CenterPcMenu.new(game, { save = save })
|
|
eq(entryIds(pc.entries), "bills,players,oaks,hof,turnoff",
|
|
"a champion sees all five rows")
|
|
end
|
|
|
|
-- PC_CheckPartyForPokemon: no party, no PC.
|
|
do
|
|
local save = newSave(0)
|
|
local game, input = newGame(save)
|
|
local closed = false
|
|
local pc = CenterPcMenu.new(game, { save = save,
|
|
onClose = function() closed = true end })
|
|
check(pc.message ~= nil, "an empty party gets the Bzzzzt! refusal")
|
|
-- data/text/common_2.asm:725 _PokecenterPCCantUseText ends in `cont`.
|
|
local refusal = pc.message.pages
|
|
eq(#refusal, 2, "the refusal is two pages, not one three-line page")
|
|
eq(#refusal[1], 2, "page 1 fills the two-row box")
|
|
eq(#refusal[2], 2, "page 2 too")
|
|
eq(refusal[2][1], refusal[1][2], "the scroll keeps 'have a #MON to' on top")
|
|
eq(refusal[2][2], "use this!", "and lands the cont line under it")
|
|
press(pc, input, "a")
|
|
check(pc.message ~= nil, "the cont waits for its own A press")
|
|
press(pc, input, "a")
|
|
check(closed, "and the PC never opens")
|
|
eq(#game.stack._items, 0, "nothing was pushed")
|
|
end
|
|
|
|
-- ------------------------------------------------- the rows open the screens
|
|
|
|
-- <PLAYER>'s PC opens the item PC (PLAYERSPC_NORMAL).
|
|
do
|
|
local save = newSave(1)
|
|
local game, input = newGame(save)
|
|
local pc = CenterPcMenu.new(game, { save = save })
|
|
press(pc, input, "a") -- boot line
|
|
press(pc, input, "down", "a") -- <PLAYER>'s PC
|
|
check(pc.message ~= nil, "PokecenterPlayersPCText comes up first")
|
|
press(pc, input, "a", "a") -- both pages
|
|
local top = game.stack:top()
|
|
check(top ~= nil, "then the item PC is pushed")
|
|
eq(top and top.screenId, "Gen2ItemPcMenu", "as Gen2ItemPcMenu")
|
|
eq(entryIds(top.entries), "withdraw,deposit,toss,mailbox,logoff",
|
|
"with _PlayersPC's PLAYERSPC_NORMAL rows, LOG OFF last")
|
|
end
|
|
|
|
-- BILL's PC opens the storage system with _BillsPC's own five rows.
|
|
do
|
|
local save = newSave(1)
|
|
local game, input = newGame(save)
|
|
local pc = CenterPcMenu.new(game, { save = save })
|
|
press(pc, input, "a") -- boot line
|
|
press(pc, input, "a") -- BILL's PC (row 1)
|
|
press(pc, input, "a", "a") -- PokecenterBillsPCText, both pages
|
|
local top = game.stack:top()
|
|
eq(top and top.screenId, "Gen2PcMenu", "BILL's PC pushes the storage menu")
|
|
eq(entryIds(top.entries), "withdraw,deposit,changebox,move,seeya",
|
|
"with no MAIL BOX row: that lives on the item PC")
|
|
end
|
|
|
|
-- PROF.OAK's PC: the yes/no, the counts, the rating, the link-closed line.
|
|
do
|
|
local save = newSave(1)
|
|
save.engineFlags = { [CenterPcMenu.ENGINE_POKEDEX] = true }
|
|
save.pokedex = { seen = { A = true, B = true, C = true },
|
|
caught = { A = true, B = true } }
|
|
local game, input = newGame(save)
|
|
local pc = CenterPcMenu.new(game, { save = save })
|
|
press(pc, input, "a") -- boot line
|
|
press(pc, input, "down", "down", "a") -- PROF.OAK's PC
|
|
press(pc, input, "a", "a") -- PokecenterOaksPCText, both pages
|
|
check(pc.confirm ~= nil, "OakPCText1 asks for the yes/no")
|
|
press(pc, input, "a") -- YES
|
|
check(pc.message ~= nil, "and the rating flow starts")
|
|
local sawCounts, sawRating = false, false
|
|
for _ = 1, 12 do
|
|
if not pc.message then break end
|
|
local page = pc.message.pages[pc.message.page]
|
|
for _, line in ipairs(page) do
|
|
if line == "3 #MON seen" then sawCounts = true end
|
|
if line == "Look for #MON" then sawRating = true end
|
|
end
|
|
press(pc, input, "a")
|
|
end
|
|
check(sawCounts, "the counts page names 3 seen")
|
|
check(sawRating, "2 owned lands on OakRating01")
|
|
check(pc.closed == false, "the OAK flow drops back to the menu, not out")
|
|
end
|
|
|
|
-- Two rows to the box (constants/text_constants.asm:32 TEXTBOX_INNERY); the
|
|
-- ratings' third lines are `cont` (data/text/common_2.asm:841).
|
|
do
|
|
local function ratingPages(caught)
|
|
local save = newSave(1)
|
|
save.engineFlags = { [CenterPcMenu.ENGINE_POKEDEX] = true }
|
|
local dex = {}
|
|
for i = 1, caught do dex["SPECIES" .. i] = true end
|
|
save.pokedex = { seen = dex, caught = dex }
|
|
local game, input = newGame(save)
|
|
local pc = CenterPcMenu.new(game, { save = save })
|
|
press(pc, input, "a")
|
|
pc:oakRate()
|
|
return pc.message.pages
|
|
end
|
|
|
|
local function indexOf(pages, first)
|
|
for i, page in ipairs(pages) do
|
|
if page[1] == first then return i end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- One caught count per band (data/events/pokedex_ratings.asm).
|
|
local tall = {}
|
|
for _, caught in ipairs({ 5, 15, 25, 40, 55, 70, 85, 100, 115, 130, 145,
|
|
160, 175, 190, 205, 220, 235, 245, 251 }) do
|
|
for _, page in ipairs(ratingPages(caught)) do
|
|
if #page ~= 2 then
|
|
tall[#tall + 1] = caught .. ":" .. table.concat(page, "/")
|
|
end
|
|
end
|
|
end
|
|
eq(table.concat(tall, " "), "",
|
|
"every band pages exactly two lines, the box's two rows")
|
|
|
|
local pages = ratingPages(205)
|
|
eq(table.concat(pages[1], "/"), "Current #DEX/completion level:",
|
|
"_OakPCText2 opens the flow")
|
|
eq(table.concat(pages[2], "/"), "205 #MON seen/205 #MON owned",
|
|
"then the two counts")
|
|
eq(table.concat(pages[3], "/"), "PROF.OAK's/Rating:",
|
|
"and the para clears the box for the rating header")
|
|
|
|
local at = indexOf(pages, "Wow! You've hit")
|
|
check(at ~= nil, "205 owned lands on _OakRating15")
|
|
eq(pages[at][2], "200! Your #DEX", "page 1 is text + line")
|
|
eq(pages[at + 1][1], "200! Your #DEX", "the cont scroll keeps line 2 on top")
|
|
eq(pages[at + 1][2], "is looking great!", "with the cont line under it")
|
|
eq(pages[at + 2], nil, "and that is the last page")
|
|
|
|
pages = ratingPages(25)
|
|
at = indexOf(pages, "You're getting")
|
|
check(at ~= nil, "25 owned lands on _OakRating03")
|
|
eq(table.concat(pages[at], "/"), "You're getting/good at this.",
|
|
"page 1 is text + line")
|
|
eq(table.concat(pages[at + 1], "/"), "But you have a/long way to go.",
|
|
"and the para page shares no line with it")
|
|
end
|
|
|
|
-- TURN OFF: the Link closed line, then the shutdown.
|
|
do
|
|
local save = newSave(1)
|
|
local game, input = newGame(save)
|
|
local closed = false
|
|
local pc = CenterPcMenu.new(game, { save = save,
|
|
onClose = function() closed = true end })
|
|
press(pc, input, "a") -- boot line
|
|
press(pc, input, "up", "a") -- TURN OFF (wraps to the last row)
|
|
check(pc.message ~= nil, "TurnOffPC prints the Link closed line")
|
|
press(pc, input, "a")
|
|
check(closed, "and carries into .shutdown")
|
|
end
|
|
|
|
-- ------------------------------------------------- the item PC, for real
|
|
|
|
-- DEPOSIT: bag -> save.pcItems through the PACK chooser.
|
|
do
|
|
local save = newSave(1)
|
|
local game, input = newGame(save)
|
|
check(Bag.add(save, "POTION", 5, game.data), "five POTIONs in the bag")
|
|
local pc = ItemPcMenu.new(game, { save = save, items = ITEMS })
|
|
eq(pc.message, nil, "PLAYERSPC_NORMAL boots straight to the menu")
|
|
press(pc, input, "down", "a") -- DEPOSIT ITEM
|
|
eq(pc.phase, "deposit", "DEPOSIT opens the PACK as a chooser")
|
|
check(pc.pack ~= nil, "held by the screen, the way the mart sells")
|
|
press(pc, input, "a") -- choose POTION
|
|
check(pc.qtyState ~= nil, "the quantity selector comes up")
|
|
press(pc, input, "up") -- 2
|
|
press(pc, input, "a") -- deposit x2
|
|
eq(save.pcItems.POTION, 2, "two POTIONs land in the PC")
|
|
eq(save.inventory.POTION, 3, "and leave the bag")
|
|
check(pc.message ~= nil, "with _PlayersPCDepositItemsText up")
|
|
eq(pc.message.pages[1][1], "Deposited 2", "naming the count")
|
|
press(pc, input, "a") -- clear it
|
|
press(pc, input, "b") -- close the PACK
|
|
eq(pc.phase, "menu", "B drops back to the item PC menu")
|
|
end
|
|
|
|
-- The same DepositSellPack chooser holds a TM: it must reach the quantity
|
|
-- selector like everything tossable, never the teach party (issue #1243).
|
|
do
|
|
local save = newSave(1)
|
|
local game, input = newGame(save)
|
|
Bag.add(save, "TM_HEADBUTT", 1, game.data)
|
|
local pc = ItemPcMenu.new(game, { save = save, items = ITEMS })
|
|
press(pc, input, "down", "a") -- DEPOSIT ITEM
|
|
press(pc, input, "right", "right", "right") -- ITEM -> BALL -> KEY_ITEM -> TM_HM
|
|
press(pc, input, "a") -- choose the TM
|
|
check(pc.qtyState ~= nil, "a TM asks how many to deposit")
|
|
eq(#game.stack._items, 0, "with no teach screen pushed over the pack")
|
|
press(pc, input, "a") -- deposit x1
|
|
eq(save.pcItems.TM_HEADBUTT, 1, "the TM lands in the PC")
|
|
eq(save.inventory.TM_HEADBUTT, nil, "and leaves the bag")
|
|
end
|
|
|
|
-- .DepositItem: CANT_TOSS only skips .AskQuantity. A KEY ITEM deposits x1,
|
|
-- and .Submenu withdraws it back the same way (issue #1486).
|
|
do
|
|
local save = newSave(1)
|
|
local game, input = newGame(save)
|
|
Bag.add(save, "BICYCLE", 1, game.data)
|
|
local pc = ItemPcMenu.new(game, { save = save, items = ITEMS })
|
|
press(pc, input, "down", "a") -- DEPOSIT ITEM
|
|
press(pc, input, "right", "right") -- ITEM -> BALL -> KEY_ITEM pocket
|
|
press(pc, input, "a") -- choose the BICYCLE
|
|
eq(pc.qtyState, nil, "no quantity selector for a KEY ITEM")
|
|
eq(save.pcItems.BICYCLE, 1, "the BICYCLE lands in the PC")
|
|
eq(save.inventory.BICYCLE, nil, "and leaves the bag")
|
|
eq(pc.message.pages[1][1], "Deposited 1", "_PlayersPCDepositItemsText")
|
|
press(pc, input, "a") -- clear it
|
|
press(pc, input, "b") -- close the PACK
|
|
press(pc, input, "up", "a") -- WITHDRAW ITEM
|
|
eq(pc.phase, "withdraw", "the PC item list opens on the KEY ITEM")
|
|
press(pc, input, "a") -- the BICYCLE row
|
|
eq(pc.qtyState, nil, "no quantity selector on the way back either")
|
|
eq(save.inventory.BICYCLE, 1, "the BICYCLE is back in the bag")
|
|
eq(save.pcItems.BICYCLE, nil, "and out of the PC")
|
|
end
|
|
|
|
-- The TM_HM pocket's HMs are CANT_TOSS too, and deposit prompt-free x1.
|
|
do
|
|
local save = newSave(1)
|
|
local game, input = newGame(save)
|
|
Bag.add(save, "HM_CUT", 1, game.data)
|
|
local pc = ItemPcMenu.new(game, { save = save, items = ITEMS })
|
|
press(pc, input, "down", "a") -- DEPOSIT ITEM
|
|
press(pc, input, "right", "right", "right") -- ITEM -> BALL -> KEY_ITEM -> TM_HM
|
|
press(pc, input, "a") -- choose HM01
|
|
eq(pc.qtyState, nil, "no quantity selector for an HM")
|
|
eq(save.pcItems.HM_CUT, 1, "HM01 lands in the PC")
|
|
eq(save.inventory.HM_CUT, nil, "and leaves the bag")
|
|
end
|
|
|
|
-- PlaceMenuItemQuantity: the PC list draws no xNN for a CANT_TOSS row.
|
|
do
|
|
local save = newSave(1)
|
|
local game = newGame(save)
|
|
save.pcItems = { POTION = 3, HM_CUT = 1 }
|
|
local pc = ItemPcMenu.new(game, { save = save, items = ITEMS })
|
|
local Chrome = require("src.ui.gen2.Chrome")
|
|
local TIMES = "\xc3\x97"
|
|
local saved = { print = Chrome.print, box = Chrome.box,
|
|
cursor = Chrome.cursor }
|
|
local printed = {}
|
|
Chrome.print = function(text, x, y)
|
|
printed[#printed + 1] = { text = text, x = x, y = y }
|
|
end
|
|
Chrome.box = function() end
|
|
Chrome.cursor = function() end
|
|
pc.phase = "withdraw"
|
|
pc:rebuild()
|
|
local drew, err = pcall(function() pc:drawList() end)
|
|
Chrome.print, Chrome.box, Chrome.cursor = saved.print, saved.box, saved.cursor
|
|
check(drew, "the PC list draws: " .. tostring(err))
|
|
local rowY = {}
|
|
for i, row in ipairs(pc.rows) do rowY[row.id] = i * 2 end
|
|
local counts = {}
|
|
for _, p in ipairs(printed) do
|
|
if p.x == 14 then counts[p.y] = p.text end
|
|
end
|
|
eq(counts[rowY.POTION + 1], TIMES .. " 3", "the POTION stack keeps its xNN")
|
|
eq(counts[rowY.HM_CUT + 1], nil,
|
|
"and the HM row draws none (PlaceMenuItemQuantity .done)")
|
|
local names = {}
|
|
for _, p in ipairs(printed) do names[p.text] = p.x .. "," .. p.y end
|
|
eq(names.POTION, "5," .. rowY.POTION, "the name column is menu_coords 4 + 1")
|
|
eq(names.CANCEL, "5," .. (#pc.rows + 1) * 2, "and CANCEL shares it")
|
|
end
|
|
|
|
-- (engine/events/pokecenter_pc.asm:641) and .a_1 (:628) leaves the row A
|
|
-- picked hollow, the way engine/menus/scrolling_menu.asm:86 does everywhere
|
|
-- (engine/items/buy_sell_toss.asm:205), menu_coords 15, 9.
|
|
do
|
|
local save = newSave(1)
|
|
local game = newGame(save)
|
|
save.pcItems = { POTION = 3 }
|
|
local pc = ItemPcMenu.new(game, { save = save, items = ITEMS })
|
|
local Chrome = require("src.ui.gen2.Chrome")
|
|
local saved = { print = Chrome.print, box = Chrome.box,
|
|
cursor = Chrome.cursor, clear = Chrome.clear }
|
|
local boxes, cursors
|
|
Chrome.print = function() end
|
|
Chrome.box = function(x, y, w, h) boxes[#boxes + 1] = { x, y, w, h } end
|
|
Chrome.cursor = function(x, y, hollow)
|
|
cursors[#cursors + 1] = { x = x, y = y, hollow = hollow or false }
|
|
end
|
|
Chrome.clear = function() end
|
|
pc.phase = "withdraw"
|
|
pc:rebuild()
|
|
|
|
local function shot()
|
|
boxes, cursors = {}, {}
|
|
pc:drawPanel()
|
|
return boxes, cursors
|
|
end
|
|
|
|
local _, idle = shot()
|
|
eq(idle[1] and idle[1].x, 4, "the PC list cursor sits in menu_coords 4")
|
|
eq(idle[1] and idle[1].hollow, false, "solid while the list has the joypad")
|
|
|
|
pc.qtyState = { qty = 1, max = 3, prompt = { "How many?" } }
|
|
local qtyBoxes, qtyCursors = shot()
|
|
eq(qtyCursors[1] and qtyCursors[1].hollow, true,
|
|
"hollow once A opened the quantity prompt")
|
|
local qtyBox
|
|
for _, b in ipairs(qtyBoxes) do
|
|
if b[1] == 15 and b[2] == 9 then qtyBox = b end
|
|
end
|
|
eq(qtyBox and qtyBox[3], 5, "TossItem_MenuHeader is five columns wide")
|
|
eq(qtyBox and qtyBox[4], 3, "and three rows tall, bottom-right of the list")
|
|
local strayBox = false
|
|
for _, b in ipairs(qtyBoxes) do
|
|
if b[1] == 7 and b[2] == 15 then strayBox = true end
|
|
end
|
|
eq(strayBox, false, "and nothing sits at BuyItem_MenuHeader's 7, 15")
|
|
|
|
pc.qtyState = nil
|
|
pc.confirm = { prompt = { "Throw away?" }, choice = 1 }
|
|
local _, confirmCursors = shot()
|
|
eq(confirmCursors[1] and confirmCursors[1].hollow, true,
|
|
"hollow under the yes/no too")
|
|
|
|
pc.confirm = nil
|
|
pc.message = { pages = { { "Withdrew POTION." } }, page = 1 }
|
|
local _, messageCursors = shot()
|
|
eq(messageCursors[1] and messageCursors[1].hollow, true,
|
|
"and under the text box A ends on")
|
|
|
|
pc.message = nil
|
|
local _, backCursors = shot()
|
|
eq(backCursors[1] and backCursors[1].hollow, false,
|
|
"filling back in when the list gets the joypad back")
|
|
|
|
Chrome.print, Chrome.box = saved.print, saved.box
|
|
Chrome.cursor, Chrome.clear = saved.cursor, saved.clear
|
|
end
|
|
|
|
-- An empty bag never opens the PACK (.CheckItemsInBag).
|
|
do
|
|
local save = newSave(1)
|
|
local game, input = newGame(save)
|
|
local pc = ItemPcMenu.new(game, { save = save, items = ITEMS })
|
|
press(pc, input, "down", "a")
|
|
eq(pc.phase, "menu", "DEPOSIT refuses with nothing to deposit")
|
|
eq(pc.message.pages[1][1], "No items here!", "with _PlayersPCNoItemsText")
|
|
end
|
|
|
|
-- WITHDRAW: save.pcItems -> bag, with the no-room refusal.
|
|
do
|
|
local save = newSave(1)
|
|
local game, input = newGame(save)
|
|
save.pcItems = { POTION = 2 }
|
|
local pc = ItemPcMenu.new(game, { save = save, items = ITEMS })
|
|
press(pc, input, "a") -- WITHDRAW ITEM
|
|
eq(pc.phase, "withdraw", "the PC item list opens")
|
|
eq(pc.rows[1] and pc.rows[1].id, "POTION", "with the POTION stack on it")
|
|
press(pc, input, "a") -- choose it
|
|
check(pc.qtyState ~= nil, "stackable: the selector asks how many")
|
|
press(pc, input, "a") -- x1
|
|
eq(save.inventory.POTION, 1, "one POTION reaches the bag")
|
|
eq(save.pcItems.POTION, 1, "one stays behind")
|
|
eq(pc.message.pages[1][1], "Withdrew 1", "_PlayersPCWithdrewItemsText")
|
|
press(pc, input, "a")
|
|
|
|
-- Fill the ITEM pocket: ReceiveItem answers no-carry and the stack stays.
|
|
Bag.remove(save, "POTION", 1)
|
|
for i = 1, Bag.capacity(game.data, "ITEM") do
|
|
check(Bag.add(save, "FILL_" .. i, 1, game.data), "filler " .. i .. " fits")
|
|
end
|
|
press(pc, input, "a", "a") -- choose the POTION stack again, x1
|
|
eq(pc.message.pages[1][1], "There's no room",
|
|
"a full pocket is _PlayersPCNoRoomWithdrawText")
|
|
eq(save.pcItems.POTION, 1, "and the stack never left the PC")
|
|
press(pc, input, "a", "b") -- clear, back to menu
|
|
end
|
|
|
|
-- TOSS: the quantity, the yes/no, the discard -- and the KEY ITEM refusal.
|
|
do
|
|
local save = newSave(1)
|
|
local game, input = newGame(save)
|
|
save.pcItems = { POTION = 3, HM_CUT = 1 }
|
|
local pc = ItemPcMenu.new(game, { save = save, items = ITEMS })
|
|
press(pc, input, "down", "down", "a") -- TOSS ITEM
|
|
eq(pc.phase, "toss", "the toss list opens")
|
|
press(pc, input, "a") -- POTION (index 2 sorts it first)
|
|
check(pc.qtyState ~= nil, "TossItemFromPC asks how many")
|
|
press(pc, input, "up", "up", "a") -- x3
|
|
check(pc.confirm ~= nil, "then .ItemsThrowAwayText asks yes/no")
|
|
eq(pc.confirm.prompt[1], "Throw away 3", "naming the count")
|
|
press(pc, input, "a") -- YES
|
|
eq(save.pcItems.POTION, nil, "the stack is discarded")
|
|
eq(pc.message.pages[1][1], "Discarded", "_ItemsDiscardedText")
|
|
press(pc, input, "a")
|
|
|
|
press(pc, input, "a") -- the HM is the only row left
|
|
eq(pc.qtyState, nil, "an HM never reaches the selector")
|
|
eq(pc.message.pages[1][1], "That's too impor-",
|
|
".CantToss: _ItemsTooImportantText")
|
|
press(pc, input, "a", "b")
|
|
end
|
|
|
|
-- The PC's fifty stacks (ReceiveItem over wNumPCItems).
|
|
do
|
|
local save = newSave(1)
|
|
local game, input = newGame(save)
|
|
Bag.add(save, "POTION", 1, game.data)
|
|
save.pcItems = {}
|
|
for i = 1, ItemPcMenu.PC_ITEM_CAPACITY do save.pcItems["S" .. i] = 1 end
|
|
local pc = ItemPcMenu.new(game, { save = save, items = ITEMS })
|
|
press(pc, input, "down", "a") -- DEPOSIT ITEM
|
|
press(pc, input, "a") -- choose POTION
|
|
press(pc, input, "a") -- x1
|
|
eq(pc.message.pages[1][1], "There's no room to",
|
|
"the fifty-first stack is _PlayersPCNoRoomDepositText")
|
|
eq(save.inventory.POTION, 1, "and the POTION stays in the bag")
|
|
end
|
|
|
|
-- PLAYERSPC_HOUSE: the boot line, DECORATION, TURN OFF, and the answer out.
|
|
do
|
|
local save = newSave(1)
|
|
local game, input = newGame(save)
|
|
local answered = nil
|
|
local pc = ItemPcMenu.new(game, { save = save, items = ITEMS, house = true,
|
|
onClose = function(changed) answered = changed end })
|
|
check(pc.message ~= nil, "_PlayersHousePC opens on PlayersPCTurnOnText")
|
|
press(pc, input, "a")
|
|
eq(entryIds(pc.entries), "withdraw,deposit,toss,mailbox,decoration,turnoff",
|
|
"PLAYERSPC_HOUSE carries DECORATION and ends on TURN OFF")
|
|
-- The DECORATION row pushes the decoration menu and carries `changed` out.
|
|
press(pc, input, "up", "up", "a")
|
|
local top = game.stack:top()
|
|
eq(top and top.screenId, "Gen2DecorationMenu", "DECORATION opens the menu")
|
|
top.onDone(true)
|
|
eq(pc.changedDecorations, true, "a moved decoration is remembered")
|
|
press(pc, input, "b")
|
|
eq(answered, true, "and answered out, for PlayersHousePCScript's iftrue")
|
|
end
|
|
|
|
-- ------------------------------------------------- the A press, wired
|
|
|
|
-- World:interact's CheckFacingTileForStdScript arm, on a stub map: COLL_PC
|
|
-- runs PCScript, COLL_RADIO runs Radio1Script, floor runs nothing.
|
|
do
|
|
local started = nil
|
|
local world = setmetatable({
|
|
map = {
|
|
def = { bgEvents = {} },
|
|
cellCollision = function(_, x, _) return x == 1 and 0x93
|
|
or (x == 3 and 0x94) or 0x00 end,
|
|
inBounds = function() return true end,
|
|
},
|
|
player = { facing = "up", cellX = 1, cellY = 1, moving = false },
|
|
npcs = {},
|
|
events = { get = function() return false end },
|
|
stdScripts = { scripts = {
|
|
PCScript = { key = "s:pc" },
|
|
Radio1Script = { key = "s:radio" },
|
|
} },
|
|
vm = { start = function(_, key) started = key return true end,
|
|
running = function() return false end },
|
|
}, { __index = World })
|
|
check(world:interact(), "A on a COLL_PC tile is claimed")
|
|
eq(started, "s:pc", "and runs PCScript")
|
|
world.player.cellX = 3
|
|
world:interact()
|
|
eq(started, "s:radio", "COLL_RADIO runs Radio1Script")
|
|
started = nil
|
|
world.player.cellX = 5
|
|
check(not world:interact(), "a plain floor tile claims nothing")
|
|
eq(started, nil, "and starts nothing")
|
|
end
|
|
|
|
-- ------------------------------------------------- the real thing
|
|
|
|
local cache = os.getenv("GOLD_CACHE")
|
|
local function loadCache(name)
|
|
local chunk = cache
|
|
and loadfile(cache .. "/data/generated/" .. name .. ".lua")
|
|
return chunk and chunk()
|
|
end
|
|
|
|
local mapsData = loadCache("maps")
|
|
local tilesetsData = loadCache("tilesets")
|
|
local scriptsData = loadCache("scripts")
|
|
local textData = loadCache("text")
|
|
local stdScriptsData = loadCache("std_scripts")
|
|
local constsData = loadCache("constants")
|
|
local itemsData = loadCache("items")
|
|
|
|
local haveCache = mapsData and tilesetsData and scriptsData and stdScriptsData
|
|
and constsData and (constsData.specialOrder ~= nil)
|
|
|
|
if not haveCache then
|
|
check(true, "no GOLD_CACHE: real-map PC checks (SKIP)")
|
|
else
|
|
local function realWorld(mapId, save)
|
|
local def = mapsData[mapId]
|
|
if not def then return nil end
|
|
local game = newGame(save, itemsData or ITEMS)
|
|
local world = World.new(game)
|
|
game.world = world
|
|
world.map = Map.new(def, tilesetsData[def.tileset] or {})
|
|
world.maps = mapsData
|
|
world.stdScripts = stdScriptsData
|
|
world.text = textData
|
|
world.constants = constsData
|
|
world.pollTimeOfDay = function() end
|
|
world.vm = Vm.new(scriptsData, textData, world.events, {
|
|
specialOrder = constsData.specialOrder,
|
|
specials = world:specialHooks(),
|
|
openPc = function() world:openPc() end,
|
|
})
|
|
return world, game
|
|
end
|
|
|
|
-- The Cherrygrove Pokecenter: find the PC by its collision, stand under it,
|
|
-- press A, and the whole chain runs -- PCScript out of std_scripts, the
|
|
-- PokemonCenterPC special, World:openPc, the whose-PC menu.
|
|
do
|
|
local save = newSave(1)
|
|
local world, game = realWorld("CHERRYGROVE_POKECENTER_1F", save)
|
|
check(world ~= nil, "the cache carries CHERRYGROVE_POKECENTER_1F")
|
|
if world then
|
|
local pcX, pcY
|
|
for cy = 0, world.map.heightCells - 1 do
|
|
for cx = 0, world.map.widthCells - 1 do
|
|
if world.map:cellCollision(cx, cy) == 0x93 then pcX, pcY = cx, cy end
|
|
end
|
|
end
|
|
check(pcX ~= nil, "and its PC is a COLL_PC tile")
|
|
if pcX then
|
|
world.player = { facing = "up", cellX = pcX, cellY = pcY + 1,
|
|
moving = false }
|
|
check(world:interact(), "the A press at the PC is claimed")
|
|
for _ = 1, 8 do
|
|
if not world.vm:running() then break end
|
|
world.vm:update()
|
|
end
|
|
local top = game.stack:top()
|
|
eq(top and top.screenId, "Gen2CenterPcMenu",
|
|
"and the whose-PC menu is on the stack")
|
|
if top and top.screenId == "Gen2CenterPcMenu" then
|
|
local input = game.input
|
|
press(top, input, "a") -- boot line
|
|
eq(entryIds(top.entries), "bills,players,turnoff",
|
|
"a fresh save sees the BEFORE_POKEDEX list")
|
|
-- Deposit a POTION through <PLAYER>'s PC, for real.
|
|
Bag.add(save, "POTION", 2, game.data)
|
|
press(top, input, "down", "a", "a", "a")
|
|
local itemPc = game.stack:top()
|
|
eq(itemPc and itemPc.screenId, "Gen2ItemPcMenu",
|
|
"<PLAYER>'s PC opens the item PC")
|
|
if itemPc and itemPc.screenId == "Gen2ItemPcMenu" then
|
|
press(itemPc, input, "down", "a") -- DEPOSIT ITEM
|
|
press(itemPc, input, "a") -- the POTION row
|
|
press(itemPc, input, "a") -- x1
|
|
eq(save.pcItems.POTION, 1, "the POTION lands in save.pcItems")
|
|
eq(save.inventory.POTION, 1, "and leaves the bag")
|
|
press(itemPc, input, "a", "b", "b") -- message, PACK, log off
|
|
eq(game.stack:top(), top, "LOG OFF drops back to the whose-PC menu")
|
|
end
|
|
press(top, input, "b") -- shutdown
|
|
eq(game.stack:top(), nil, "and B logs the whole PC off")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- The bedroom: the real PLAYERS_HOUSE_2F bg event, the blocking
|
|
-- PlayersHousePC special, the item PC -- and the FALSE answered back to
|
|
-- PlayersHousePCScript when no decoration moved.
|
|
do
|
|
local save = newSave(1)
|
|
local world, game = realWorld("PLAYERS_HOUSE_2F", save)
|
|
check(world ~= nil, "the cache carries PLAYERS_HOUSE_2F")
|
|
if world then
|
|
local pcEvent
|
|
for _, ev in ipairs(world.map.def.bgEvents or {}) do
|
|
if ev.kind == 1 then pcEvent = ev end -- BGEVENT_UP: the PC
|
|
end
|
|
check(pcEvent ~= nil, "the bedroom PC is its BGEVENT_UP event")
|
|
if pcEvent then
|
|
world.player = { facing = "up", cellX = pcEvent.x, cellY = pcEvent.y + 1,
|
|
moving = false }
|
|
check(world:interact(), "the A press at the bedroom PC is claimed")
|
|
for _ = 1, 4 do
|
|
if game.stack:top() then break end
|
|
world.vm:update()
|
|
end
|
|
local top = game.stack:top()
|
|
eq(top and top.screenId, "Gen2ItemPcMenu",
|
|
"and the bedroom PC is the ITEM PC, not the storage system")
|
|
if top and top.screenId == "Gen2ItemPcMenu" then
|
|
eq(top.house, true, "in its PLAYERSPC_HOUSE shape")
|
|
check(top.message ~= nil, "with the turn-on line up")
|
|
local input = game.input
|
|
press(top, input, "a", "b") -- boot line, then TURN OFF via B
|
|
eq(game.stack:top(), nil, "closing it pops the screen")
|
|
for _ = 1, 6 do
|
|
if not world.vm:running() then break end
|
|
world.vm:update()
|
|
end
|
|
check(not world.vm:running(),
|
|
"and PlayersHousePCScript runs to its end off the FALSE answer")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------- MOVE POKéMON W/O MAIL, in full
|
|
--
|
|
-- The regression this block exists for: A on a mon used to move it instantly
|
|
-- to "the next box with room", with the confirmation string computed and then
|
|
-- thrown away -- so from the player's chair the PC ATE the mon. Nothing on
|
|
-- screen named a destination and nothing was printed, which is the shape of
|
|
-- the bug report ("Move Pokemon w/o mail deletes the Pokemon when you select
|
|
-- it"). These checks pin the cart's own flow instead
|
|
-- (_MovePKMNWithoutMail, engine/pokemon/bills_pc.asm:480): a submenu, a
|
|
-- destination the player picks, and the mon accounted for at every step.
|
|
do
|
|
local BoxMenu = require("src.ui.gen2.BoxMenu")
|
|
local Boxes = require("src.core.gen2.Boxes")
|
|
|
|
local function census(save)
|
|
local n = #(save.party or {})
|
|
for i = 1, Boxes.NUM_BOXES do n = n + Boxes.count(save, i) end
|
|
return n
|
|
end
|
|
|
|
local save = newSave(3)
|
|
local box = Boxes.box(save, 1)
|
|
box[1] = { species = "PIDGEY", nickname = "BOXED1", hp = 10, maxHp = 10,
|
|
level = 3 }
|
|
box[2] = { species = "RATTATA", nickname = "BOXED2", hp = 10, maxHp = 10,
|
|
level = 3 }
|
|
local game, input = newGame(save)
|
|
local menu = BoxMenu.new(game, { save = save, mode = "move",
|
|
onClose = function() end })
|
|
eq(census(save), 5, "five mons before anything is moved")
|
|
eq(menu:prompt(), "Choose a <PK><MN>.", "PCString_ChooseaPKMN opens the list")
|
|
|
|
-- 1. A opens .MoveMonWOMailSubmenu rather than moving anything.
|
|
press(menu, input, "a")
|
|
eq(menu.phase, "submenu", "A on a mon opens the submenu")
|
|
eq(menu:prompt(), "What's up?", "under PCString_WhatsUp")
|
|
eq(#Boxes.box(save, 1), 2, "and the box is untouched by opening it")
|
|
|
|
-- CANCEL and B both come back with the mon exactly where it was.
|
|
press(menu, input, "b")
|
|
eq(menu.phase, nil, "B closes the submenu")
|
|
eq(#Boxes.box(save, 1), 2, "still two in BOX1")
|
|
|
|
-- 2. MOVE asks where, and the mon is STILL in its box while it asks.
|
|
press(menu, input, "a", "a")
|
|
eq(menu.phase, "insert", "MOVE opens the insert cursor")
|
|
eq(menu:prompt(), "Move to where?", "under PCString_MoveToWhere")
|
|
eq(#Boxes.box(save, 1), 2, "nothing has left the box yet")
|
|
eq(census(save), 5, "and nothing has left the save")
|
|
|
|
-- B there is .b_button_2: the backed-up position, nothing moved.
|
|
press(menu, input, "b")
|
|
eq(menu.phase, nil, "B backs out of the insert cursor")
|
|
eq(#Boxes.box(save, 1), 2, "with the mon still in BOX1")
|
|
eq(census(save), 5, "and the census unchanged")
|
|
|
|
-- 3. right walks to BOX2 and A inserts there -- the destination the player
|
|
-- chose, named on screen, with the cart's own confirmation.
|
|
press(menu, input, "a", "a", "right")
|
|
eq(menu.boxIndex, 2, "right walks the insert cursor to the next box")
|
|
eq(menu:title(), "BOX2", "and the header names it")
|
|
press(menu, input, "a")
|
|
eq(census(save), 5, "the mon still exists")
|
|
eq(#Boxes.box(save, 1), 1, "one left BOX1")
|
|
eq(Boxes.box(save, 2)[1].nickname, "BOXED1", "and it is in BOX2")
|
|
eq(menu.message, "Saving\xe2\x80\xa6 Leave ON!",
|
|
"MovePKMNWithoutMail_InsertMon's own line says so")
|
|
|
|
-- 4. left twice reaches the PARTY (wBillsPC_LoadedBox 0), which the old
|
|
-- screen could not show at all.
|
|
press(menu, input, "a", "left", "left")
|
|
eq(menu.boxIndex, 0, "left wraps through BOX1 to the party")
|
|
eq(menu:title(), "PARTY <PK><MN>", "BillsPC_BoxName's .party arm")
|
|
eq(#menu:list(), 3, "and the list is the party")
|
|
|
|
-- A party mon moves into a box, and the party shrinks by exactly one.
|
|
press(menu, input, "a", "a", "right")
|
|
eq(menu.boxIndex, 1, "right from the party is BOX1")
|
|
press(menu, input, "a")
|
|
eq(#save.party, 2, "the party is one shorter")
|
|
eq(census(save), 5, "and the mon is still in the save")
|
|
eq(Boxes.box(save, 1)[1].nickname, "MON1", "sitting where the cursor was")
|
|
|
|
-- BillsPC_CheckMail_PreventBlackout: a party of two may not send one away.
|
|
press(menu, input, "a", "left")
|
|
eq(menu.boxIndex, 0, "back on the party")
|
|
press(menu, input, "a", "a")
|
|
eq(menu.phase, nil, "the MOVE is refused outright")
|
|
eq(menu.message, "It's your last <PK><MN>!", "with PCString_ItsYourLastPKMN")
|
|
eq(#save.party, 2, "and the party is untouched")
|
|
|
|
-- .MoveMonWOMailSubmenu has no RELEASE row, so SELECT on this screen must
|
|
-- not open the withdraw list's release question either.
|
|
press(menu, input, "a")
|
|
press(menu, input, "select")
|
|
eq(#game.stack._items, 0, "SELECT on the move screen opens nothing")
|
|
eq(census(save), 5, "and releases nothing")
|
|
end
|
|
|
|
-- Box-screen labels and complete prompts are translated at draw/use time;
|
|
-- box names and nicknames remain save content even when they collide with a
|
|
-- catalog key.
|
|
do
|
|
local BoxMenu = require("src.ui.gen2.BoxMenu")
|
|
local Boxes = require("src.core.gen2.Boxes")
|
|
local Strings = require("src.core.Strings")
|
|
Strings.load({ strings = {
|
|
["PARTY <PK><MN>"] = "EQUIPE <PK><MN>",
|
|
["Choose a <PK><MN>."] = "Choisissez un <PK><MN>.",
|
|
["What's up?"] = "Que faire ?",
|
|
["Release <PK><MN>?"] = "Relacher le <PK><MN> ?",
|
|
["BOX CUSTOM"] = "NE PAS TRADUIRE",
|
|
} })
|
|
local save = newSave(3)
|
|
save.boxNames = save.boxNames or {}
|
|
save.boxNames[1] = "BOX CUSTOM"
|
|
Boxes.box(save, 1)[1] = { species = "PIDGEY", nickname = "BOX CUSTOM",
|
|
hp = 10, maxHp = 10, level = 3 }
|
|
local game = newGame(save)
|
|
local move = BoxMenu.new(game, { save = save, mode = "move" })
|
|
eq(move:prompt(), "Choisissez un <PK><MN>.",
|
|
"the complete choose-mon prompt passes through Strings")
|
|
move.phase = "submenu"
|
|
eq(move:prompt(), "Que faire ?", "the complete submenu prompt is translated")
|
|
move.boxIndex = 0
|
|
eq(move:title(), "EQUIPE <PK><MN>", "the built-in PARTY title is translated")
|
|
move.boxIndex = 1
|
|
eq(move:title(), "BOX CUSTOM", "a user-defined box name remains raw")
|
|
|
|
local withdraw = BoxMenu.new(game, { save = save, mode = "withdraw" })
|
|
withdraw:askRelease()
|
|
eq(withdraw.message, "Relacher le <PK><MN> ?",
|
|
"the complete release prompt passes through Strings")
|
|
Strings.load({})
|
|
end
|
|
|
|
S.finish()
|