mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
squashy squash
This commit is contained in:
@@ -647,6 +647,68 @@ check(Transition.new({ data = retimed }).frames == 30,
|
||||
check(Transition.new({ data = { transitions = {} } }).frames == 12,
|
||||
"an unregistered id falls back to the built-in 12 frames")
|
||||
|
||||
-- issue #121: with the survey-zoom world pass active, the warp fade must
|
||||
-- darken the full window composite (via Renderer.worldFadeAlpha), not just
|
||||
-- paint a 160x144 rect on the UI letterbox.
|
||||
do
|
||||
local rects = {}
|
||||
local color = { 1, 1, 1, 1 }
|
||||
local savedRect, savedColor = love.graphics.rectangle, love.graphics.setColor
|
||||
love.graphics.rectangle = function(mode, x, y, w, h)
|
||||
rects[#rects + 1] = {
|
||||
mode = mode, x = x, y = y, w = w, h = h,
|
||||
a = color[4], r = color[1],
|
||||
}
|
||||
end
|
||||
love.graphics.setColor = function(r, g, b, a)
|
||||
color[1], color[2], color[3], color[4] = r, g, b, a or 1
|
||||
end
|
||||
|
||||
Renderer:init()
|
||||
local fade = Transition.new({ renderer = Renderer, stack = { pop = noop } })
|
||||
fade.t = 6 -- mid fade-out (12 frames)
|
||||
fade.phase = "out"
|
||||
|
||||
Renderer:beginFrame(true)
|
||||
Renderer:beginWorldPass()
|
||||
Renderer:endWorldPass()
|
||||
check(Renderer.worldActive == true, "world pass stays marked active until endFrame")
|
||||
fade:draw()
|
||||
check(Renderer.worldFadeAlpha == 0.5,
|
||||
"warp fade hands mid-out alpha to the world composite overlay")
|
||||
check(#rects == 0,
|
||||
"warp fade does not paint the 160x144 UI letterbox while the world pass is up")
|
||||
|
||||
resetLog()
|
||||
rects = {}
|
||||
Renderer:endFrame(nil, fullWorldZones())
|
||||
local fadeRect
|
||||
for _, r in ipairs(rects) do
|
||||
-- endFrame's letterbox clear is also a full-window black fill (a == 1);
|
||||
-- the warp overlay is the half-alpha one Transition requested
|
||||
if r.mode == "fill" and r.x == 0 and r.y == 0
|
||||
and r.w == 640 and r.h == 576 and r.r == 0 and r.a == 0.5 then
|
||||
fadeRect = r
|
||||
end
|
||||
end
|
||||
check(fadeRect ~= nil,
|
||||
"endFrame paints the warp fade over the full window at the fade alpha")
|
||||
check(Renderer.worldActive == false, "endFrame clears worldActive")
|
||||
|
||||
-- without a world pass (opaque UI states), keep the classic UI rect
|
||||
Renderer:beginFrame(false)
|
||||
fade.t = 6
|
||||
fade.phase = "out"
|
||||
rects = {}
|
||||
fade:draw()
|
||||
check(Renderer.worldFadeAlpha == nil,
|
||||
"no world pass: warp fade does not set a world overlay")
|
||||
check(#rects == 1 and rects[1].w == 160 and rects[1].h == 144,
|
||||
"no world pass: warp fade still fills the 160x144 UI canvas")
|
||||
|
||||
love.graphics.rectangle, love.graphics.setColor = savedRect, savedColor
|
||||
end
|
||||
|
||||
-- ------- the transition.style hook
|
||||
|
||||
local stack = { pop = function() end }
|
||||
|
||||
+51
-4
@@ -55,9 +55,27 @@ local function newStack()
|
||||
return stack, items
|
||||
end
|
||||
|
||||
-- snapshot / restore car warps so tests that seed or ride do not leak
|
||||
-- across cases (Silph's ROM default is UNUSED_MAP_ED)
|
||||
local function snapshotWarps(mapId)
|
||||
local snap = {}
|
||||
for i, w in ipairs(Data.maps[mapId].warps) do
|
||||
snap[i] = { destMap = w.destMap, destWarp = w.destWarp }
|
||||
end
|
||||
return snap
|
||||
end
|
||||
|
||||
local function restoreWarps(mapId, snap)
|
||||
for i, w in ipairs(Data.maps[mapId].warps) do
|
||||
w.destMap, w.destWarp = snap[i].destMap, snap[i].destWarp
|
||||
end
|
||||
end
|
||||
|
||||
-- drives one elevator map's onEnter and returns the pushed state (either a
|
||||
-- ListMenu or, for the keyGated Rocket Hideout without the key, a TextBox)
|
||||
local function openElevator(mapId, inventory)
|
||||
-- ListMenu or, for the keyGated Rocket Hideout without the key, a TextBox).
|
||||
-- fromMapId is the floor the player entered from (OverworldState:setMap
|
||||
-- passes it), used to seed a cancel-safe walk-out destination.
|
||||
local function openElevator(mapId, inventory, fromMapId)
|
||||
local script = mapScripts.get(mapId)
|
||||
check(script ~= nil, mapId .. " script registered")
|
||||
check(script and script.onEnter ~= nil, mapId .. " has onEnter")
|
||||
@@ -98,7 +116,7 @@ local function openElevator(mapId, inventory)
|
||||
stack = stack,
|
||||
}
|
||||
sfxCalls = {}
|
||||
script.onEnter(game, ow)
|
||||
script.onEnter(game, ow, fromMapId)
|
||||
return items[#items], warpCalls, stack, ow
|
||||
end
|
||||
|
||||
@@ -130,7 +148,14 @@ end
|
||||
-- Silph Co elevator: 11 floors, the double-digit sort/label regression
|
||||
-- ===================================================================
|
||||
do
|
||||
local menu, warpCalls, stack, ow = openElevator("SILPH_CO_ELEVATOR")
|
||||
local silphSnap = snapshotWarps("SILPH_CO_ELEVATOR")
|
||||
-- ROM default (UNUSED_MAP_ED) must still be what we start from so the
|
||||
-- cancel-then-exit regression below is real
|
||||
eq(silphSnap[1].destMap, "UNUSED_MAP_ED",
|
||||
"Silph Co ROM car warps still default to UNUSED_MAP_ED")
|
||||
|
||||
local menu, warpCalls, stack, ow =
|
||||
openElevator("SILPH_CO_ELEVATOR", nil, "SILPH_CO_5F")
|
||||
check(menu ~= nil and getmetatable(menu) == ListMenu, "SILPH_CO_ELEVATOR opens a ListMenu")
|
||||
if menu then
|
||||
eq(#menu.items, 11, "Silph Co elevator lists all 11 floors")
|
||||
@@ -143,12 +168,32 @@ do
|
||||
check(menu.items[1].label ~= "SILPH CO 1F" and not menu.items[1].label:find("SILPH"),
|
||||
"Silph Co floor label is the short token, not the full map id")
|
||||
|
||||
-- onEnter seeds the car's exit to the floor we came from so a B-cancel
|
||||
-- cannot leave UNUSED_MAP_ED in place (#123 hard crash on walk-out)
|
||||
eq(ow.map.def.warps[1].destMap, "SILPH_CO_5F",
|
||||
"Silph onEnter seeds exit warps to the entry floor")
|
||||
check(Data.maps[ow.map.def.warps[1].destMap] ~= nil,
|
||||
"seeded Silph exit map exists in Data.maps")
|
||||
|
||||
-- Cancel: pokered's DisplayElevatorFloorMenu does `ret c` on B --
|
||||
-- no warp at all.
|
||||
clear(warpCalls)
|
||||
menu.onCancel()
|
||||
eq(#warpCalls, 0, "Cancel does not warp (bare ret c, no floors[1] fallback)")
|
||||
|
||||
-- #123: after B-cancel, walking out of the car must resolve without
|
||||
-- asserting on the ROM placeholder UNUSED_MAP_ED
|
||||
clear(warpCalls)
|
||||
local okExit, errExit = pcall(function()
|
||||
ow:takeWarp(ow.map.def.warps[1])
|
||||
end)
|
||||
check(okExit, "cancel then exit does not crash: " .. tostring(errExit))
|
||||
eq(#warpCalls, 1, "cancel then exit takes the seeded entry-floor warp")
|
||||
if warpCalls[1] then
|
||||
eq(warpCalls[1].map, "SILPH_CO_5F",
|
||||
"cancel then exit returns to the floor the player entered from")
|
||||
end
|
||||
|
||||
-- Choose a mid-list floor (5F): pokered never warps on the spot --
|
||||
-- DisplayElevatorFloorMenu sets BIT_CUR_MAP_USED_ELEVATOR and the
|
||||
-- map script runs ShakeElevator: a 12-frame lead-in (the script's
|
||||
@@ -157,6 +202,7 @@ do
|
||||
-- cycle, then SFX_SAFARI_ZONE_PA, and only then the floor warp.
|
||||
clear(warpCalls)
|
||||
sfxCalls = {}
|
||||
ow.walkSteps = {}
|
||||
local chosen = menu.items[5] -- "5F"
|
||||
menu.onChoose(chosen, menu)
|
||||
eq(#warpCalls, 0, "choosing a floor does not warp on the spot (the shake runs first)")
|
||||
@@ -184,6 +230,7 @@ do
|
||||
eq(warpCalls[1].y, chosen.value.y, "walk-out lands on the chosen floor's y")
|
||||
end
|
||||
end
|
||||
restoreWarps("SILPH_CO_ELEVATOR", silphSnap)
|
||||
end
|
||||
|
||||
-- ===================================================================
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
-- Parity test, Bill's House PC + Route25ToggleBillsScript (#120).
|
||||
--
|
||||
-- asm sources:
|
||||
-- engine/events/hidden_events/bills_house_pc.asm (BillsHousePC /
|
||||
-- BillsHousePokemonList: after EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING
|
||||
-- the PC opens EEVEE/FLAREON/JOLTEON/VAPOREON; before that the
|
||||
-- teleporter monitor text or cell-separator cutscene)
|
||||
-- scripts/Route25.asm (Route25ToggleBillsScript: first Route 25 load
|
||||
-- after EVENT_MET_BILL_2 + EVENT_GOT_SS_TICKET sets
|
||||
-- EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING, hides BILL1 / nugget-bridge
|
||||
-- guy, shows BILL2; mid-quest leave resets the separator flag)
|
||||
--
|
||||
-- Self-contained: run via `luajit tests/parity_bills_pc.lua`; also
|
||||
-- dofile'd by tests/run_tests.lua's aggregator.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
local Data = require("src.core.Data")
|
||||
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
||||
local S = require("tests.harness").suite("parity bills pc")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local OW = require("src.world.OverworldController")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local story = require("data.scripts.story")
|
||||
|
||||
local function getUpvalue(fn, name)
|
||||
local i = 1
|
||||
while true do
|
||||
local n, v = debug.getupvalue(fn, i)
|
||||
if not n then return nil end
|
||||
if n == name then return v end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
local function setUpvalue(fn, name, val)
|
||||
local i = 1
|
||||
while true do
|
||||
local n = debug.getupvalue(fn, i)
|
||||
if not n then return false end
|
||||
if n == name then debug.setupvalue(fn, i, val); return true end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
local pushed = {}
|
||||
local stackStub = {
|
||||
push = function(_, item)
|
||||
pushed[#pushed + 1] = item
|
||||
end,
|
||||
}
|
||||
local textBoxStub = {
|
||||
new = function(_, text, onDone)
|
||||
local box = { kind = "text", text = text, onDone = onDone }
|
||||
if onDone then onDone() end
|
||||
return box
|
||||
end,
|
||||
}
|
||||
local menuStub = {
|
||||
new = function(_, items, opts)
|
||||
return { kind = "menu", items = items, opts = opts or {} }
|
||||
end,
|
||||
}
|
||||
local screensStub = {
|
||||
push = function(_, id, species)
|
||||
pushed[#pushed + 1] = { kind = "screen", id = id, species = species }
|
||||
end,
|
||||
}
|
||||
|
||||
local fakeGame = {
|
||||
data = Data,
|
||||
save = SaveData.newGame(),
|
||||
stack = stackStub,
|
||||
}
|
||||
check(setUpvalue(OW.billsHousePC, "TextBox", textBoxStub), "TextBox upvalue")
|
||||
check(setUpvalue(OW.billsHousePC, "Game", fakeGame), "Game upvalue for PC")
|
||||
-- Screens is only referenced from billsHousePokemonList
|
||||
check(setUpvalue(OW.billsHousePokemonList, "Screens", screensStub),
|
||||
"Screens upvalue on pokemon list")
|
||||
check(setUpvalue(OW.billsHousePokemonList, "Game", fakeGame),
|
||||
"Game upvalue on pokemon list")
|
||||
check(setUpvalue(OW.billsHousePokemonList, "TextBox", textBoxStub),
|
||||
"TextBox upvalue on pokemon list")
|
||||
|
||||
-- Menu is required inside billsHousePokemonList; stub via package.loaded
|
||||
local realMenu = package.loaded["src.ui.Menu"]
|
||||
package.loaded["src.ui.Menu"] = menuStub
|
||||
|
||||
local fakeSelf = setmetatable({
|
||||
queueScript = function() end,
|
||||
billsHouseBillExits = function() end,
|
||||
}, { __index = OW })
|
||||
|
||||
local function resetFlags()
|
||||
fakeGame.save = SaveData.newGame()
|
||||
setUpvalue(OW.billsHousePC, "Game", fakeGame)
|
||||
pushed = {}
|
||||
end
|
||||
|
||||
local function lastPush()
|
||||
return pushed[#pushed]
|
||||
end
|
||||
|
||||
local function runPC()
|
||||
pushed = {}
|
||||
fakeSelf:billsHousePC()
|
||||
end
|
||||
|
||||
-- === 1) default: teleporter monitor text ===
|
||||
resetFlags()
|
||||
runPC()
|
||||
eq(lastPush() and lastPush().kind, "text", "default PC is a text box")
|
||||
check(tostring(lastPush().text):find("TELEPORTER", 1, true)
|
||||
or tostring(lastPush().text):find("displayed on the", 1, true),
|
||||
"default PC shows monitor text")
|
||||
|
||||
-- === 2) Bill in machine, separator not used yet: initiated text ===
|
||||
resetFlags()
|
||||
fakeGame.save.flags.EVENT_BILL_SAID_USE_CELL_SEPARATOR = true
|
||||
local musicStopped = false
|
||||
local realMusic = package.loaded["src.core.Music"]
|
||||
package.loaded["src.core.Music"] = {
|
||||
stop = function() musicStopped = true end,
|
||||
playMap = function() end,
|
||||
}
|
||||
local realSound = package.loaded["src.core.Sound"]
|
||||
package.loaded["src.core.Sound"] = {
|
||||
play = function() end,
|
||||
playCry = function() end,
|
||||
}
|
||||
runPC()
|
||||
check(musicStopped, "cell separator stops map music")
|
||||
check(tostring(lastPush().text):find("Cell", 1, true)
|
||||
or tostring(lastPush().text):find("TELEPORTER", 1, true),
|
||||
"separator path prints initiated text")
|
||||
check(fakeGame.save.flags.EVENT_USED_CELL_SEPARATOR_ON_BILL,
|
||||
"separator path sets EVENT_USED_CELL_SEPARATOR_ON_BILL")
|
||||
|
||||
-- === 3) after separator, before leaving: monitor text again ===
|
||||
resetFlags()
|
||||
fakeGame.save.flags.EVENT_BILL_SAID_USE_CELL_SEPARATOR = true
|
||||
fakeGame.save.flags.EVENT_USED_CELL_SEPARATOR_ON_BILL = true
|
||||
fakeGame.save.flags.EVENT_MET_BILL = true
|
||||
runPC()
|
||||
check(tostring(lastPush().text):find("TELEPORTER", 1, true)
|
||||
or tostring(lastPush().text):find("displayed on the", 1, true),
|
||||
"post-separator pre-leave PC shows monitor text")
|
||||
|
||||
-- === 4) after leaving: Eevee collection menu ===
|
||||
resetFlags()
|
||||
fakeGame.save.flags.EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING = true
|
||||
runPC()
|
||||
local menu
|
||||
for _, p in ipairs(pushed) do
|
||||
if p.kind == "menu" then menu = p break end
|
||||
end
|
||||
check(menu ~= nil, "post-leave PC opens a menu")
|
||||
local labels = {}
|
||||
for _, item in ipairs(menu.items) do labels[#labels + 1] = item.label end
|
||||
eq(table.concat(labels, ","), "EEVEE,FLAREON,JOLTEON,VAPOREON,CANCEL",
|
||||
"Eevee collection lists the four mons + CANCEL")
|
||||
|
||||
-- selecting EEVEE marks seen and opens DexEntryMenu without closing list
|
||||
fakeGame.save.pokedex = { seen = {}, owned = {} }
|
||||
menu.items[1].onSelect()
|
||||
check(fakeGame.save.pokedex.seen.EEVEE, "viewing marks EEVEE seen")
|
||||
local dexPush
|
||||
for i = #pushed, 1, -1 do
|
||||
if pushed[i].kind == "screen" then dexPush = pushed[i] break end
|
||||
end
|
||||
eq(dexPush and dexPush.id, "DexEntryMenu", "selection opens DexEntryMenu")
|
||||
eq(dexPush and dexPush.species, "EEVEE", "DexEntryMenu gets EEVEE")
|
||||
check(menu.items[1].keepOpen, "dex pick keeps the list open")
|
||||
|
||||
-- === 5) Route25ToggleBillsScript ===
|
||||
local toggles = {}
|
||||
local Commands = require("src.script.Commands")
|
||||
local realHide, realShow = Commands.hide_object, Commands.show_object
|
||||
Commands.hide_object = function(_, mapId, name)
|
||||
toggles[mapId .. ":" .. name] = false
|
||||
end
|
||||
Commands.show_object = function(_, mapId, name)
|
||||
toggles[mapId .. ":" .. name] = true
|
||||
end
|
||||
|
||||
local function runRoute25(flags)
|
||||
toggles = {}
|
||||
local save = SaveData.newGame()
|
||||
for k, v in pairs(flags) do save.flags[k] = v end
|
||||
story.ROUTE_25.onEnter({ save = save }, {})
|
||||
return save.flags, toggles
|
||||
end
|
||||
|
||||
local f, t = runRoute25({})
|
||||
check(not f.EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING,
|
||||
"no leave flag before meeting Bill")
|
||||
check(t["BILLS_HOUSE:BILLSHOUSE_BILL_POKEMON"] == true,
|
||||
"mid-quest leave restores monster Bill")
|
||||
check(f.EVENT_BILL_SAID_USE_CELL_SEPARATOR == nil,
|
||||
"mid-quest leave clears separator arming flag")
|
||||
|
||||
f, t = runRoute25({ EVENT_MET_BILL_2 = true })
|
||||
check(not f.EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING,
|
||||
"MET_BILL_2 without ticket does not arm leave flag")
|
||||
|
||||
f, t = runRoute25({
|
||||
EVENT_MET_BILL_2 = true,
|
||||
EVENT_GOT_SS_TICKET = true,
|
||||
})
|
||||
check(f.EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING,
|
||||
"ticket + MET_BILL_2 arms EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING")
|
||||
eq(t["BILLS_HOUSE:BILLSHOUSE_BILL1"], false, "hides SS-Ticket Bill")
|
||||
eq(t["BILLS_HOUSE:BILLSHOUSE_BILL2"], true, "shows rare-POKéMON Bill")
|
||||
eq(t["ROUTE_24:ROUTE24_COOLTRAINER_M1"], false, "hides nugget-bridge guy")
|
||||
|
||||
f, t = runRoute25({
|
||||
EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING = true,
|
||||
EVENT_MET_BILL_2 = true,
|
||||
EVENT_GOT_SS_TICKET = true,
|
||||
})
|
||||
eq(next(t), nil, "already-left Route 25 enter is a no-op")
|
||||
|
||||
Commands.hide_object = realHide
|
||||
Commands.show_object = realShow
|
||||
package.loaded["src.ui.Menu"] = realMenu
|
||||
if realMusic ~= nil then package.loaded["src.core.Music"] = realMusic end
|
||||
if realSound ~= nil then package.loaded["src.core.Sound"] = realSound end
|
||||
|
||||
S.finish()
|
||||
@@ -0,0 +1,116 @@
|
||||
-- Regression (#125): SURF from Cinnabar's east coast into Route 20 water.
|
||||
--
|
||||
-- Cinnabar's easternmost cells are walkable land (tile $39); the water is
|
||||
-- on ROUTE_20 across the east connection. pokered loads that strip into
|
||||
-- the border, so IsNextTileShoreOrWater sees shore tile $32. The port
|
||||
-- must read the connection landing the same way -- an inBounds-only
|
||||
-- water check returns "no_water" and blocks the mount (and the reverse
|
||||
-- party-menu dismount back onto the coast).
|
||||
--
|
||||
-- Self-contained; run via `luajit tests/parity_cinnabar_east_surf.lua`.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local Data = require("src.core.Data")
|
||||
if not (Data.maps and Data.maps.CINNABAR_ISLAND) then Data:load() end
|
||||
|
||||
local Game = require("src.core.Game")
|
||||
local Input = require("src.core.Input")
|
||||
local Map = require("src.world.Map")
|
||||
local MapLoader = require("src.world.MapLoader")
|
||||
local Renderer = require("src.render.Renderer")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
local OW = require("src.world.OverworldController")
|
||||
local S = require("tests.harness").suite("parity cinnabar east surf")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local function mkMon(species, ...)
|
||||
local moves = {}
|
||||
for _, id in ipairs({ ... }) do
|
||||
table.insert(moves, { id = id, pp = 10, ppUp = 0 })
|
||||
end
|
||||
return {
|
||||
species = species, level = 30, hp = 50, maxHp = 50,
|
||||
status = 0, moves = moves, nickname = nil,
|
||||
}
|
||||
end
|
||||
|
||||
local cin = MapLoader.load(Data, "CINNABAR_ISLAND")
|
||||
local r20 = MapLoader.load(Data, "ROUTE_20")
|
||||
local r20ts = Data.tilesets[r20.def.tileset]
|
||||
|
||||
-- ground truth: east edge is land, Route 20 west landing is shore/water
|
||||
check(cin:isWalkableCell(19, 8), "Cinnabar (19,8) is walkable coast")
|
||||
check(not cin:isWaterCell(19, 8), "Cinnabar (19,8) is not water")
|
||||
check(not cin:inBounds(20, 8), "facing east from (19,8) is off-map")
|
||||
check(r20:isWaterCell(0, 8), "ROUTE_20 (0,8) is water/shore")
|
||||
check(Map.defIsWaterCell(r20.def, r20ts, 0, 8),
|
||||
"defIsWaterCell agrees on ROUTE_20 (0,8)")
|
||||
check(Map.defIsWalkableCell(cin.def, Data.tilesets[cin.def.tileset], 19, 8),
|
||||
"defIsWalkableCell agrees on Cinnabar coast")
|
||||
|
||||
Game.data = Data
|
||||
Game.input = Input; Input:init()
|
||||
Game.renderer = Renderer; Renderer:init()
|
||||
Game.stack = StateStack
|
||||
StateStack:init()
|
||||
Game.save = SaveData.newGame()
|
||||
Game.save.party = { mkMon("SQUIRTLE", "SURF") }
|
||||
Game.save.inventory = { SOULBADGE = true }
|
||||
Game.overworld = OW
|
||||
|
||||
while Game.stack:top() do Game.stack:pop() end
|
||||
Game.stack:push(OW, "CINNABAR_ISLAND", 19, 8, "right")
|
||||
local ow = Game.stack:top()
|
||||
eq(ow.map.id, "CINNABAR_ISLAND", "start on Cinnabar east coast")
|
||||
eq(ow.player.cellX, 19, "coast x")
|
||||
eq(ow.player.cellY, 8, "coast y")
|
||||
eq(ow.player.facing, "right", "facing east toward Route 20")
|
||||
|
||||
check(ow:facingIsShoreOrWater(),
|
||||
"facingIsShoreOrWater reads Route 20 shore across the seam")
|
||||
eq(ow:useSurfFieldMove(), "ok",
|
||||
"useSurfFieldMove ok facing connection water (#125)")
|
||||
|
||||
-- reverse: surfing on Route 20 west edge, facing Cinnabar land
|
||||
while Game.stack:top() do Game.stack:pop() end
|
||||
Game.stack:push(OW, "ROUTE_20", 0, 8, "left")
|
||||
ow = Game.stack:top()
|
||||
ow.player.surfing = true
|
||||
eq(ow.map.id, "ROUTE_20", "on Route 20 west water")
|
||||
check(ow.map:isWaterCell(0, 8), "standing cell is water")
|
||||
check(ow:facingIsLandDismount(),
|
||||
"facingIsLandDismount sees Cinnabar coast across the seam")
|
||||
eq(ow:useSurfFieldMove(), "dismount",
|
||||
"party-menu SURF dismounts onto Cinnabar east coast")
|
||||
|
||||
-- facing open water while surfing still refuses
|
||||
ow.player.facing = "right"
|
||||
eq(ow:useSurfFieldMove(), "no_place",
|
||||
"surfing + facing open water: no place (unchanged)")
|
||||
|
||||
-- mount step crosses the seam onto Route 20 (surfing already armed)
|
||||
while Game.stack:top() do Game.stack:pop() end
|
||||
Game.stack:push(OW, "CINNABAR_ISLAND", 19, 8, "right")
|
||||
ow = Game.stack:top()
|
||||
ow.player.surfing = true
|
||||
check(ow:stepForwardOrCrossEdge("right"),
|
||||
"stepForwardOrCrossEdge crosses onto Route 20 while surfing")
|
||||
eq(ow.map.id, "ROUTE_20", "map swapped to Route 20 after edge surf step")
|
||||
-- crossConnection parks one cell before the seam and walks in (same as
|
||||
-- a live edge press); the landing target is Route 20 (0,8)
|
||||
eq(ow.player.targetX, 0, "step target is Route 20 west column")
|
||||
eq(ow.player.targetY, 8, "same Y across offset-0 east connection")
|
||||
check(ow.player.moving, "seam step is in progress")
|
||||
|
||||
-- in-map Pallet south shore still works (no false connection match)
|
||||
while Game.stack:top() do Game.stack:pop() end
|
||||
Game.stack:push(OW, "PALLET_TOWN", 4, 13, "down")
|
||||
ow = Game.stack:top()
|
||||
ow.player.surfing = false
|
||||
eq(ow:useSurfFieldMove(), "ok", "in-map water mount still ok")
|
||||
ow.player.facing = "up"
|
||||
eq(ow:useSurfFieldMove(), "no_water", "in-map land still no_water")
|
||||
|
||||
S.finish()
|
||||
@@ -0,0 +1,184 @@
|
||||
-- Parity test for Route 5 Day Care (issue #118).
|
||||
-- Self-contained: `luajit tests/parity_daycare.lua`; also dofile'd by
|
||||
-- tests/run_tests.lua's parity_* aggregator.
|
||||
--
|
||||
-- Covers scripts/Daycare.asm retrieve flow ported in
|
||||
-- data/scripts/story2.lua M.DAYCARE:
|
||||
-- * name substitution in grown / got-back text
|
||||
-- * fee = ¥100 + ¥100 per level gained
|
||||
-- * declining retrieve must not collapse the fee to ¥100 on re-talk
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
local Data = require("src.core.Data")
|
||||
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
||||
local S = require("tests.harness").suite("parity daycare")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local realTextBox = package.loaded["src.render.TextBox"]
|
||||
local shownTexts = {}
|
||||
local choiceAnswer = true
|
||||
package.loaded["src.render.TextBox"] = {
|
||||
new = function(game, text, onDone, opts)
|
||||
table.insert(shownTexts, text)
|
||||
if opts and opts.choice then
|
||||
opts.choice(choiceAnswer)
|
||||
elseif onDone then
|
||||
onDone()
|
||||
end
|
||||
return { text = text }
|
||||
end,
|
||||
}
|
||||
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local Growth = require("src.pokemon.Growth")
|
||||
local story2 = require("data.scripts.story2")
|
||||
|
||||
check(story2.DAYCARE ~= nil, "DAYCARE registered")
|
||||
check(story2.DAYCARE.talk.TEXT_DAYCARE_GENTLEMAN ~= nil,
|
||||
"DAYCARE gentleman talk handler registered")
|
||||
|
||||
local talkGentleman = story2.DAYCARE.talk.TEXT_DAYCARE_GENTLEMAN
|
||||
|
||||
local function newGame()
|
||||
local save = SaveData.newGame()
|
||||
save.money = 10000
|
||||
local game = { data = Data, save = save, stack = { push = function() end } }
|
||||
return game
|
||||
end
|
||||
|
||||
local function talk(game)
|
||||
shownTexts = {}
|
||||
local doneCalled = false
|
||||
talkGentleman(game, {}, nil, function() doneCalled = true end)
|
||||
return doneCalled
|
||||
end
|
||||
|
||||
local function boardAtLevel(game, species, level, nickname)
|
||||
local mon = Pokemon.new(Data, species, level)
|
||||
if nickname then mon.nickname = nickname end
|
||||
game.save.daycare = { mon = mon, steps = 0 }
|
||||
return mon
|
||||
end
|
||||
|
||||
-- Exp needed to gain `gained` levels from `level` (exclusive of current).
|
||||
local function stepsForLevels(species, level, gained)
|
||||
local def = Data.pokemon[species]
|
||||
local target = Growth.expForLevel(def.growthRate, level + gained)
|
||||
local current = Growth.expForLevel(def.growthRate, level)
|
||||
return target - current
|
||||
end
|
||||
|
||||
-- === 1) grown text names the mon; fee matches levels grown ===
|
||||
do
|
||||
local game = newGame()
|
||||
local mon = boardAtLevel(game, "RATTATA", 5, "SCRAPPY")
|
||||
local gained = 3
|
||||
game.save.daycare.steps = stepsForLevels("RATTATA", 5, gained)
|
||||
choiceAnswer = false -- decline retrieve
|
||||
check(talk(game), "grown+decline talk completes")
|
||||
local grown
|
||||
for _, s in ipairs(shownTexts) do
|
||||
if s:find("grown a lot", 1, true) then grown = s break end
|
||||
end
|
||||
check(grown ~= nil, "shows MonHasGrownText")
|
||||
check(grown:find("SCRAPPY", 1, true), "grown text substitutes wNameBuffer")
|
||||
check(grown:find(tostring(gained), 1, true), "grown text shows levels grown")
|
||||
check(not grown:find("{RAM:", 1, true), "grown text has no leftover RAM tokens")
|
||||
local owe
|
||||
for _, s in ipairs(shownTexts) do
|
||||
if s:find("owe me", 1, true) then owe = s break end
|
||||
end
|
||||
check(owe ~= nil, "shows OweMoneyText")
|
||||
eq(owe:match("¥(%d+)"), tostring(100 + gained * 100),
|
||||
"fee is ¥100 + ¥100 per level gained")
|
||||
eq(game.save.daycare.mon.level, 5,
|
||||
"decline leaves deposit level untouched (BoxLevel baseline)")
|
||||
eq(game.save.daycare.steps, 0, "pending steps folded into exp on talk")
|
||||
end
|
||||
|
||||
-- === 2) re-talk after decline keeps the correct fee (issue #118) ===
|
||||
do
|
||||
local game = newGame()
|
||||
boardAtLevel(game, "RATTATA", 5, "SCRAPPY")
|
||||
local gained = 3
|
||||
game.save.daycare.steps = stepsForLevels("RATTATA", 5, gained)
|
||||
choiceAnswer = false
|
||||
talk(game)
|
||||
choiceAnswer = false
|
||||
check(talk(game), "second decline talk completes")
|
||||
local owe
|
||||
for _, s in ipairs(shownTexts) do
|
||||
if s:find("owe me", 1, true) then owe = s break end
|
||||
end
|
||||
check(owe ~= nil, "re-talk still shows OweMoneyText")
|
||||
eq(owe:match("¥(%d+)"), tostring(100 + gained * 100),
|
||||
"re-talk after decline keeps fee (not ¥100)")
|
||||
eq(game.save.daycare.mon.level, 5, "re-talk still leaves deposit level")
|
||||
end
|
||||
|
||||
-- === 3) paid retrieve raises level, names the mon, clears daycare ===
|
||||
do
|
||||
local game = newGame()
|
||||
boardAtLevel(game, "RATTATA", 5, "SCRAPPY")
|
||||
local gained = 2
|
||||
game.save.daycare.steps = stepsForLevels("RATTATA", 5, gained)
|
||||
local fee = 100 + gained * 100
|
||||
local moneyBefore = game.save.money
|
||||
choiceAnswer = true
|
||||
check(talk(game), "retrieve talk completes")
|
||||
eq(game.save.daycare, nil, "daycare cleared after paid retrieve")
|
||||
eq(#game.save.party, 1, "mon returned to party")
|
||||
eq(game.save.party[1].nickname, "SCRAPPY", "retrieved nickname preserved")
|
||||
eq(game.save.party[1].level, 5 + gained, "level applied only on retrieve")
|
||||
eq(game.save.money, moneyBefore - fee, "money deducted by correct fee")
|
||||
local got
|
||||
for _, s in ipairs(shownTexts) do
|
||||
if s:find("got", 1, true) and s:find("back", 1, true) then got = s break end
|
||||
end
|
||||
check(got ~= nil, "shows GotMonBackText")
|
||||
check(got:find("SCRAPPY", 1, true), "got-back text substitutes wDayCareMonName")
|
||||
check(not got:find("{RAM:", 1, true), "got-back text has no leftover RAM tokens")
|
||||
end
|
||||
|
||||
-- === 4) no levels gained: NeedsMoreTime still names the mon; fee ¥100 ===
|
||||
do
|
||||
local game = newGame()
|
||||
boardAtLevel(game, "RATTATA", 10, "PIP")
|
||||
game.save.daycare.steps = 0
|
||||
choiceAnswer = false
|
||||
check(talk(game), "no-growth talk completes")
|
||||
local needs
|
||||
for _, s in ipairs(shownTexts) do
|
||||
if s:find("Back already", 1, true) then needs = s break end
|
||||
end
|
||||
check(needs ~= nil, "shows MonNeedsMoreTimeText")
|
||||
check(needs and needs:find("PIP", 1, true), "needs-more-time names the mon")
|
||||
local owe
|
||||
for _, s in ipairs(shownTexts) do
|
||||
if s:find("owe me", 1, true) then owe = s break end
|
||||
end
|
||||
eq(owe and owe:match("¥(%d+)"), "100", "no growth still costs base ¥100")
|
||||
end
|
||||
|
||||
-- === 5) depositLevel survives a corrupted mon.level from older buggy talks ===
|
||||
do
|
||||
local game = newGame()
|
||||
local mon = boardAtLevel(game, "RATTATA", 5, "SCRAPPY")
|
||||
local gained = 4
|
||||
game.save.daycare.steps = stepsForLevels("RATTATA", 5, gained)
|
||||
game.save.daycare.depositLevel = 5
|
||||
mon.level = 5 + gained -- simulate pre-#118 raise-on-talk corruption
|
||||
choiceAnswer = false
|
||||
check(talk(game), "depositLevel baseline talk completes")
|
||||
local owe
|
||||
for _, s in ipairs(shownTexts) do
|
||||
if s:find("owe me", 1, true) then owe = s break end
|
||||
end
|
||||
eq(owe and owe:match("¥(%d+)"), tostring(100 + gained * 100),
|
||||
"depositLevel keeps fee correct even if mon.level was raised early")
|
||||
eq(game.save.daycare.mon.level, 5, "decline restores mon.level to depositLevel")
|
||||
end
|
||||
|
||||
package.loaded["src.render.TextBox"] = realTextBox
|
||||
S.finish()
|
||||
@@ -0,0 +1,199 @@
|
||||
-- Parity test: Vermilion City S.S. Anne sailor (#122).
|
||||
--
|
||||
-- pokered VermilionCityDefaultScript (scripts/VermilionCity.asm):
|
||||
-- stepping onto SSAnneTicketCheckCoords (18,30) facing down always
|
||||
-- DisplayTextID's the sailor. With a ticket and the ship still docked,
|
||||
-- FlashedTicket plays and the player may continue; without a ticket, or
|
||||
-- after EVENT_SS_ANNE_LEFT, they are walked back up.
|
||||
--
|
||||
-- The port used to early-return when the ticket was in the bag, skipping
|
||||
-- the flash dialog entirely.
|
||||
--
|
||||
-- Self-contained; run via `luajit tests/parity_ss_anne_guard.lua`.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
local S = require("tests.harness").suite("parity ss anne guard")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
package.loaded["src.render.TextBox"] = {
|
||||
new = function(_, text, done) return { text = text, done = done } end,
|
||||
}
|
||||
|
||||
local musicCalls = {}
|
||||
package.loaded["src.core.Music"] = {
|
||||
stop = function() musicCalls[#musicCalls + 1] = { "stop" } end,
|
||||
play = function(_, song)
|
||||
musicCalls[#musicCalls + 1] = { "play", song }
|
||||
end,
|
||||
playOnce = function(_, song)
|
||||
musicCalls[#musicCalls + 1] = { "playOnce", song }
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
local soundCalls = {}
|
||||
package.loaded["src.core.Sound"] = {
|
||||
play = function(_, id) soundCalls[#soundCalls + 1] = id end,
|
||||
}
|
||||
|
||||
local story = dofile("data/scripts/story.lua")
|
||||
local story3 = dofile("data/scripts/story3.lua")
|
||||
local Flags = require("src.script.Flags")
|
||||
|
||||
local function gameWith(opts)
|
||||
local pushed, moved = {}, {}
|
||||
local game = {
|
||||
save = {
|
||||
inventory = opts.inventory or {},
|
||||
flags = opts.flags or {},
|
||||
},
|
||||
data = {
|
||||
text = {
|
||||
_VermilionCitySailor1DoYouHaveATicketText =
|
||||
"Welcome to S.S.\nANNE!\fExcuse me, do you\nhave a ticket?",
|
||||
_VermilionCitySailor1FlashedTicketText =
|
||||
"PLAYER flashed\nthe S.S.TICKET!",
|
||||
_VermilionCitySailor1YouNeedATicketText =
|
||||
"You need a ticket\nto get aboard.",
|
||||
_VermilionCitySailor1ShipSetSailText = "The ship set sail.",
|
||||
_SSAnneCaptainsRoomRubCaptainsBackText = "Rub-rub...",
|
||||
_SSAnneCaptainsRoomCaptainIFeelMuchBetterText = "I feel better!",
|
||||
_SSAnneCaptainsRoomCaptainReceivedHM01Text = "Got HM01!",
|
||||
_SSAnneCaptainsRoomCaptainNotSickAnymoreText = "Not sick.",
|
||||
},
|
||||
},
|
||||
stack = { push = function(_, box) pushed[#pushed + 1] = box end },
|
||||
_pushed = pushed,
|
||||
_moved = moved,
|
||||
}
|
||||
return game, pushed, moved
|
||||
end
|
||||
|
||||
local function owWith(moved)
|
||||
return {
|
||||
player = { facing = "down", cellY = 2 },
|
||||
scriptMove = function(_, _, dir, n) moved[#moved + 1] = { dir, n } end,
|
||||
queueScript = function(self, rows) self._queued = rows end,
|
||||
startDustAnim = function() end,
|
||||
map = {
|
||||
setBlock = function() end,
|
||||
renderer = { rebuild = function() end },
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
local city = story.VERMILION_CITY
|
||||
check(city and city.onStep, "VERMILION_CITY has the sailor coord trigger")
|
||||
|
||||
-- With ticket, ship still docked: flash dialog, do NOT walk back.
|
||||
do
|
||||
local game, pushed, moved = gameWith({ inventory = { S_S_TICKET = 1 } })
|
||||
local ow = owWith(moved)
|
||||
check(city.onStep(game, ow, 18, 30), "ticket: walk-past trigger consumes the step")
|
||||
check(#pushed == 1, "ticket: auto dialog is shown")
|
||||
check(pushed[1].text:find("flashed", 1, true)
|
||||
or pushed[1].text:find("ticket", 1, true),
|
||||
"ticket: flash / ticket-check dialog text")
|
||||
check(#moved == 0, "ticket: player is not walked back")
|
||||
end
|
||||
|
||||
-- Without ticket: dialog + walk back.
|
||||
do
|
||||
local game, pushed, moved = gameWith({})
|
||||
local ow = owWith(moved)
|
||||
check(city.onStep(game, ow, 18, 30), "no ticket: trigger fires")
|
||||
check(#pushed == 1, "no ticket: dialog shown")
|
||||
if pushed[1] and pushed[1].done then pushed[1].done() end
|
||||
check(#moved == 1 and moved[1][1] == "up",
|
||||
"no ticket: walked back up after dialog")
|
||||
end
|
||||
|
||||
-- After the ship leaves: ShipSetSail + walk back, even with a ticket.
|
||||
do
|
||||
local game, pushed, moved = gameWith({
|
||||
inventory = { S_S_TICKET = 1 },
|
||||
flags = { EVENT_SS_ANNE_LEFT = true },
|
||||
})
|
||||
local ow = owWith(moved)
|
||||
check(city.onStep(game, ow, 18, 30), "ship left: trigger still fires")
|
||||
eq(pushed[1] and pushed[1].text, "The ship set sail.",
|
||||
"ship left: dialog updates to ShipSetSail")
|
||||
if pushed[1] and pushed[1].done then pushed[1].done() end
|
||||
check(#moved == 1 and moved[1][1] == "up",
|
||||
"ship left: walked back up")
|
||||
end
|
||||
|
||||
-- Off-tile / wrong facing: no trigger.
|
||||
do
|
||||
local game, pushed = gameWith({ inventory = { S_S_TICKET = 1 } })
|
||||
local ow = owWith({})
|
||||
eq(city.onStep(game, ow, 18, 29), false, "wrong Y: no trigger")
|
||||
ow.player.facing = "up"
|
||||
eq(city.onStep(game, ow, 18, 30), false, "facing up: no trigger")
|
||||
check(#pushed == 0, "no spurious dialog off the check")
|
||||
end
|
||||
|
||||
-- Sailor talk after ship left: ShipSetSail branch (row script).
|
||||
do
|
||||
local ScriptRunner = require("src.script.ScriptRunner")
|
||||
local game = gameWith({ flags = { EVENT_SS_ANNE_LEFT = true } })
|
||||
local rows = city.talk.TEXT_VERMILIONCITY_SAILOR1
|
||||
local runner = ScriptRunner.new(game, nil)
|
||||
runner:run(rows, {})
|
||||
local guard = 0
|
||||
while runner:isRunning() and guard < 200 do
|
||||
guard = guard + 1
|
||||
if game.stack and game._pushed then
|
||||
local box = game._pushed[#game._pushed]
|
||||
if box and box.done then box.done() end
|
||||
end
|
||||
runner:update()
|
||||
end
|
||||
local last = game._pushed[#game._pushed]
|
||||
eq(last and last.text, "The ship set sail.",
|
||||
"talk after ship left shows ShipSetSail")
|
||||
end
|
||||
|
||||
-- Departure cutscene: Music_Surfing + EVENT_SS_ANNE_LEFT via Flags.set.
|
||||
do
|
||||
for i = #musicCalls, 1, -1 do musicCalls[i] = nil end
|
||||
local game, _, moved = gameWith({ flags = { EVENT_GOT_HM01 = true } })
|
||||
local ow = owWith(moved)
|
||||
story3.VERMILION_DOCK.onEnter(game, ow)
|
||||
check(Flags.get(game.save, "EVENT_SS_ANNE_LEFT"),
|
||||
"departure sets EVENT_SS_ANNE_LEFT")
|
||||
local sawSurf = false
|
||||
for _, c in ipairs(musicCalls) do
|
||||
if c[1] == "play" and c[2] == "Music_Surfing" then sawSurf = true end
|
||||
end
|
||||
check(sawSurf, "departure plays Music_Surfing")
|
||||
check(ow._queued ~= nil, "departure queues the sail-away script")
|
||||
local kept, horn = false, false
|
||||
for _, row in ipairs(ow._queued or {}) do
|
||||
if row[1] == "play_music" and row[2] == "Music_Surfing"
|
||||
and row[3] and row[3].keep then
|
||||
kept = true
|
||||
end
|
||||
if row[1] == "play_sound" and row[2] == "SS_Anne_Horn" then
|
||||
horn = true
|
||||
end
|
||||
end
|
||||
check(kept, "departure keeps Music_Surfing across the city warp")
|
||||
check(horn, "departure queues SS_Anne_Horn")
|
||||
end
|
||||
|
||||
-- Captain rub jingle: play_once Music_PkmnHealed sits after the rub text.
|
||||
do
|
||||
local rows = story.SS_ANNE_CAPTAINS_ROOM.talk.TEXT_SSANNECAPTAINSROOM_CAPTAIN
|
||||
local found = false
|
||||
for i, row in ipairs(rows) do
|
||||
if row[1] == "play_once" and row[2] == "Music_PkmnHealed" then
|
||||
found = true
|
||||
check(rows[i - 1] and rows[i - 1][2] == "_SSAnneCaptainsRoomRubCaptainsBackText",
|
||||
"play_once follows the rub-back text")
|
||||
end
|
||||
end
|
||||
check(found, "captain script plays Music_PkmnHealed after the rub")
|
||||
end
|
||||
|
||||
S.finish()
|
||||
@@ -1895,6 +1895,87 @@ do
|
||||
eq(#StateStack.states, depth0, "mart menu unwound cleanly")
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------- UI text layout (#115/#116/#119)
|
||||
-- Nested function so LuaJIT's 200-local main-chunk limit is not hit.
|
||||
;(function()
|
||||
local BoxMenu = require("src.ui.BoxMenu")
|
||||
local box = BoxMenu.new(Game)
|
||||
check(box.tw >= 14, "Bill's PC menu is wide enough for CHANGE BOX")
|
||||
local labelTiles = 2 + #"CHANGE BOX" -- cursor gap + label
|
||||
check(box.tx + labelTiles <= box.tx + box.tw - 1,
|
||||
"CHANGE BOX fits inside the Bill's PC border")
|
||||
|
||||
-- ¥ is one glyph (charmap 0xF0) but two UTF-8 bytes; right-align must
|
||||
-- use Font.width, not #string.
|
||||
eq(Font.width("¥300"), 4 * 8, "Font.width counts yen as one glyph")
|
||||
check(Font.width("¥300") < #"¥300" * 8,
|
||||
"byte-length right-align would shift yen prices left")
|
||||
|
||||
-- Sell list: name + quantity column only (no ¥ price on the row).
|
||||
Game.save.inventory = { GREAT_BALL = 5, HYPER_POTION = 99 }
|
||||
local sellShop = require("src.ui.ShopMenu").new(Game, { "POTION" }, function() end)
|
||||
StateStack:push(sellShop)
|
||||
Input.pressed = { down = true }; StateStack:update(1 / 60); Input.pressed = {}
|
||||
Input.pressed = { a = true }; StateStack:update(1 / 60); Input.pressed = {}
|
||||
local sellList = StateStack:top()
|
||||
local foundHyper
|
||||
for _, it in ipairs(sellList.items or {}) do
|
||||
if it.value == "HYPER_POTION" then
|
||||
foundHyper = it
|
||||
local nameEnd = 16 + Font.width(it.label)
|
||||
local rightX = 160 - 8 - Font.width(it.right)
|
||||
check(nameEnd <= rightX,
|
||||
"sell HYPER POTION name does not overlap quantity")
|
||||
check(not tostring(it.right):find("¥", 1, true),
|
||||
"sell list keeps prices out of the right column")
|
||||
check(tostring(it.label):find("x", 1, true) == nil,
|
||||
"sell list does not glue quantity into the name")
|
||||
end
|
||||
end
|
||||
check(foundHyper, "sell list includes HYPER POTION")
|
||||
StateStack:pop() -- sell list
|
||||
StateStack:pop() -- shop
|
||||
Game.save.inventory = {}
|
||||
|
||||
-- Status screen page 2: next-level line stays left of the line-box edge.
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local mon = Pokemon.new(Data, "RAICHU", 27)
|
||||
local SummaryMenu = require("src.ui.SummaryMenu")
|
||||
local HudTiles = require("src.render.HudTiles")
|
||||
local drawn = {}
|
||||
local savedDraw, savedTile = Font.draw, HudTiles.tile
|
||||
Font.draw = function(text, x, y)
|
||||
drawn[#drawn + 1] = { text = tostring(text), x = x, y = y }
|
||||
return Font.width(text)
|
||||
end
|
||||
HudTiles.tile = function(code, x, y)
|
||||
drawn[#drawn + 1] = { tile = code, x = x, y = y }
|
||||
end
|
||||
local summary = SummaryMenu.new(Game, mon)
|
||||
summary.page = 2
|
||||
summary:draw()
|
||||
Font.draw = savedDraw
|
||||
HudTiles.tile = savedTile
|
||||
local nextExp, lvTile, lvNum
|
||||
for _, d in ipairs(drawn) do
|
||||
if d.text == "LEVEL UP" then
|
||||
eq(d.y, 40, "LEVEL UP sits on tile row 5")
|
||||
elseif type(d.text) == "string" and d.text:match("^%s*%d+$") and d.y == 48 and d.x == 56 then
|
||||
nextExp = d
|
||||
elseif d.tile == 0x6E and d.y == 48 then
|
||||
lvTile = d
|
||||
elseif d.text == "28" and d.y == 48 then
|
||||
lvNum = d
|
||||
end
|
||||
end
|
||||
check(nextExp, "next-exp prints at (7,6)")
|
||||
check(lvTile and lvTile.x == 128, "next level uses the <LV> tile at col 16")
|
||||
check(lvNum and lvNum.x == 136, "next level digits follow <LV>")
|
||||
local edge = 19 * 8 -- DrawLineBox vertical at col 19
|
||||
check(lvNum.x + Font.width(lvNum.text) <= edge,
|
||||
"next level digits stay left of the status line-box")
|
||||
end)()
|
||||
|
||||
|
||||
|
||||
-- ================= BUGS.md batch: battle-victory-music =================
|
||||
|
||||
Reference in New Issue
Block a user