mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
299 lines
11 KiB
Lua
299 lines
11 KiB
Lua
#!/usr/bin/env luajit
|
|
-- Comprehensive unit and integration test for:
|
|
-- 1. StepEvents Engine (lockstep queue, lethal poison, whiteout flush, repel, happiness, egg cycles, vs seeker charge)
|
|
-- 2. VS Seeker Engine (battery charge only with the item in the bag, map gate, uncharged use)
|
|
-- 3. TM Case & Berry Pouch Sub-Containers & Bag state persistence
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
local Game3Cache = require("tests.game3_cache")
|
|
if not Game3Cache.bundle() then print("[skip] step_events_queue: " .. tostring(Game3Cache.reason)) return end
|
|
|
|
local failed = 0
|
|
local function check(cond, msg)
|
|
if cond then
|
|
print("[ok] " .. msg)
|
|
else
|
|
failed = failed + 1
|
|
print("[FAIL] " .. msg)
|
|
end
|
|
end
|
|
|
|
-- Mock Love2D globals if not present
|
|
if not _G.love then
|
|
_G.love = {
|
|
graphics = {
|
|
setColor = function() end,
|
|
rectangle = function() end,
|
|
circle = function() end,
|
|
arc = function() end,
|
|
draw = function() end,
|
|
newImage = function() return { setFilter = function() end } end,
|
|
newQuad = function() return {} end,
|
|
},
|
|
timer = { getTime = function() return 0 end },
|
|
filesystem = { read = function() return nil end },
|
|
}
|
|
end
|
|
|
|
local StepEvents = require("src.core.game3.step_events")
|
|
local VsSeeker = require("src.core.game3.vs_seeker")
|
|
local ItemsData = require("src.core.game3.items_data")
|
|
local Bag = require("src.core.game3.bag")
|
|
local ItemUse = require("src.core.game3.item_use")
|
|
local TmCase = require("src.ui.game3.tm_case")
|
|
local BerryPouch = require("src.ui.game3.berry_pouch")
|
|
local BagMenu = require("src.ui.game3.bag_menu")
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
|
|
print("=== 1. StepEvents Engine & Lockstep Queue Tests ===")
|
|
|
|
StepEvents.flush()
|
|
check(not StepEvents.busy(), "Initial StepEvents is not busy")
|
|
|
|
local eventOrder = {}
|
|
StepEvents.queueEvent(function() table.insert(eventOrder, "first") end)
|
|
StepEvents.queueEvent(function() table.insert(eventOrder, "second") end)
|
|
check(StepEvents.busy(), "StepEvents is busy with 2 queued events")
|
|
|
|
StepEvents.update(0.1)
|
|
check(#eventOrder == 1 and eventOrder[1] == "first", "First event executed")
|
|
check(StepEvents.busy(), "StepEvents still busy with remaining event")
|
|
|
|
StepEvents.update(0.1)
|
|
check(#eventOrder == 2 and eventOrder[2] == "second", "Second event executed")
|
|
check(not StepEvents.busy(), "Queue drained, StepEvents is no longer busy")
|
|
|
|
print("=== 2. VS Seeker Battery Management across Locomotion Modes ===")
|
|
|
|
local session = {
|
|
repelSteps = 10,
|
|
bag = Bag.new(),
|
|
party = {
|
|
{ species = 25, nickname = "PIKACHU", hp = 20, maxHp = 20, friendship = 70 },
|
|
},
|
|
}
|
|
|
|
for i = 1, 5 do
|
|
StepEvents.onStep(session, {})
|
|
end
|
|
check(VsSeeker.getBattery(session) == 0, "VS Seeker does not charge while it is not in the bag")
|
|
session.repelSteps = 10
|
|
Bag.add(session.bag, ItemsData.ITEM_VS_SEEKER, 1)
|
|
|
|
-- Step counting across locomotion modes
|
|
for i = 1, 50 do
|
|
StepEvents.onStep(session, { running = false, biking = false, surfing = false })
|
|
end
|
|
check(VsSeeker.getBattery(session) == 50, "VS Seeker charged 50 steps via walking")
|
|
|
|
for i = 1, 30 do
|
|
StepEvents.onStep(session, { running = true, biking = false, surfing = false })
|
|
end
|
|
check(VsSeeker.getBattery(session) == 80, "VS Seeker charged +30 steps via running (total 80)")
|
|
|
|
for i = 1, 20 do
|
|
StepEvents.onStep(session, { running = false, biking = true, surfing = false })
|
|
end
|
|
check(VsSeeker.getBattery(session) == 100, "VS Seeker reached 100 charge via biking")
|
|
|
|
-- Charging capped at 100
|
|
StepEvents.onStep(session, { running = false, biking = false, surfing = true })
|
|
check(VsSeeker.getBattery(session) == 100, "VS Seeker charge capped at 100")
|
|
|
|
print("=== 3. Repel Countdown & Single Wear-off Mechanics ===")
|
|
|
|
check(session.repelSteps == 0, "Repel wore off after 10 steps (started at 10)")
|
|
-- Verify repel queued wear off event
|
|
check(StepEvents.busy(), "Repel wear-off event queued")
|
|
local handledRepel = false
|
|
local mockHud = {
|
|
openMessage = function(_game, msg, opts)
|
|
if msg == require("src.core.game3.rom_text").box("Text_RepelWoreOff") then
|
|
handledRepel = true
|
|
end
|
|
if opts and opts.done then opts.done() end
|
|
end
|
|
}
|
|
package.loaded["src.ui.game3.hud"] = mockHud
|
|
StepEvents.update(0.1)
|
|
check(handledRepel, "Repel displayed ROM Text_RepelWoreOff")
|
|
|
|
print("=== 4. Happiness & Egg Cycles Mechanics ===")
|
|
|
|
local eggMon = { species = 175, nickname = "EGG", isEgg = true, eggCycles = 2 }
|
|
local partyMon = { species = 25, nickname = "PIKA", hp = 30, maxHp = 30, friendship = 100 }
|
|
session.party = { partyMon, eggMon }
|
|
StepEvents.flush()
|
|
|
|
-- Step 128 times for friendship
|
|
for i = 1, 128 do
|
|
StepEvents.onStep(session, {})
|
|
end
|
|
check(partyMon.friendship == 101, "Party mon gained +1 friendship after 128 steps")
|
|
|
|
-- Step another 128 times (total 256 steps) for egg cycle decrement
|
|
for i = 1, 128 do
|
|
StepEvents.onStep(session, {})
|
|
end
|
|
check(eggMon.eggCycles == 1, "Egg decremented 1 egg cycle after 256 steps")
|
|
|
|
print("=== 5. Gen 3 Lethal Poison & Whiteout Queue Flush ===")
|
|
|
|
StepEvents.flush()
|
|
local poisonedMon1 = { species = 1, nickname = "BULBA", hp = 2, maxHp = 20, status = "PSN" }
|
|
local poisonedMon2 = { species = 4, nickname = "CHAR", hp = 1, maxHp = 20, status = "PSN" }
|
|
session.party = { poisonedMon1, poisonedMon2 }
|
|
|
|
-- 4 steps trigger 1 HP poison damage
|
|
for i = 1, 4 do
|
|
StepEvents.onStep(session, {})
|
|
end
|
|
check(poisonedMon1.hp == 1, "Poisoned mon 1 took 1 HP damage (now 1 HP)")
|
|
check(poisonedMon2.hp == 0, "Poisoned mon 2 dropped to 0 HP and fainted")
|
|
|
|
-- Process queued poison faint dialogue
|
|
local faintedMsg = false
|
|
mockHud.openMessage = function(_game, msg, opts)
|
|
if msg:find("CHAR fainted") or msg:find("fainted") then
|
|
faintedMsg = true
|
|
end
|
|
if opts and opts.done then opts.done() end
|
|
end
|
|
StepEvents.update(0.1)
|
|
check(faintedMsg, "Lethal poison faint event processed")
|
|
StepEvents.update(0.1)
|
|
check(not StepEvents.busy(), "Party still standing: the whiteout check releases the queue")
|
|
|
|
-- Next 4 steps drop last Pokémon to 0 HP -> Whiteout
|
|
for i = 1, 4 do
|
|
StepEvents.onStep(session, {})
|
|
end
|
|
check(poisonedMon1.hp == 0, "Poisoned mon 1 dropped to 0 HP (entire party fainted)")
|
|
|
|
session.money = 1000
|
|
local messages = {}
|
|
mockHud.openMessage = function(_game, msg, opts)
|
|
messages[#messages + 1] = msg
|
|
if opts and opts.done then opts.done() end
|
|
end
|
|
local respawned, locked = false, false
|
|
package.loaded["src.core.game3.audio"] = { fadeOutBgm = function() end, playCry = function() end }
|
|
package.loaded["src.ui.game3.fade"] = { MODE = { TO_BLACK = 1 }, begin = function(_, _, cb) if cb then cb() end end }
|
|
package.loaded["src.core.game3.field"] = {
|
|
lock = function() locked = true end,
|
|
respawnAtHeal = function() respawned = true end,
|
|
}
|
|
for _ = 1, 4 do StepEvents.update(0.1) end
|
|
check(messages[1] and messages[1]:find("BULBA fainted…", 1, true) ~= nil, "Last faint message shown before the wipe check")
|
|
check(messages[2] and messages[2]:find("panicked and lost ¥8…", 1, true) ~= nil, "Whiteout text carries the money loss")
|
|
check(session.money == 992, "Money loss applied on field whiteout (" .. tostring(session.money) .. ")")
|
|
check(locked and respawned, "Field whiteout locks the player and respawns at the heal point")
|
|
check(not StepEvents.busy(), "StepEvents queue completely empty after whiteout")
|
|
package.loaded["src.core.game3.audio"] = nil
|
|
package.loaded["src.ui.game3.fade"] = nil
|
|
package.loaded["src.core.game3.field"] = nil
|
|
|
|
print("=== 6. VS Seeker Map Gate & Uncharged Use ===")
|
|
|
|
local mapType = 8
|
|
package.loaded["src.core.game3.map"] = {
|
|
current = "FR_ROUTE_8",
|
|
currentDef = function() return { mapType = mapType } end,
|
|
}
|
|
session.map = "FR_ROUTE_8"
|
|
session.name = "RED"
|
|
session.bag = Bag.new()
|
|
Bag.add(session.bag, ItemsData.ITEM_VS_SEEKER, 1)
|
|
VsSeeker.setBattery(session, 100)
|
|
|
|
local okIndoor, kindIndoor, textIndoor = ItemUse.useField(session, session.bag, ItemsData.ITEM_VS_SEEKER)
|
|
check(not okIndoor and kindIndoor == "vs_seeker", "VS Seeker refused on an indoor map type")
|
|
check(textIndoor == "OAK: RED!\nThis isn't the time to use that!", "refusal uses the OAK forbids text")
|
|
check(VsSeeker.getBattery(session) == 100, "Battery preserved at 100 on refusal")
|
|
|
|
mapType = 3
|
|
local okRoute, kindRoute = ItemUse.useField(session, session.bag, ItemsData.ITEM_VS_SEEKER)
|
|
check(okRoute and kindRoute == "vs_seeker", "VS Seeker accepted on a ROUTE map type")
|
|
check(VsSeeker.mapAllowed("FR_ROUTE_8", 1) and VsSeeker.mapAllowed("FR_ROUTE_8", 2), "TOWN and CITY map types allowed")
|
|
check(not VsSeeker.mapAllowed("FR_ROUTE_8", 4), "UNDERGROUND map type refused")
|
|
check(not VsSeeker.mapAllowed("ViridianForest", 3), "Viridian Forest excluded by name")
|
|
|
|
VsSeeker.setBattery(session, 40)
|
|
local notChargedMsg
|
|
mockHud.openMessage = function(_game, msg, opts)
|
|
notChargedMsg = msg
|
|
if opts and opts.done then opts.done() end
|
|
end
|
|
local okUse, code = VsSeeker.use(session, nil)
|
|
check(not okUse and code == VsSeeker.NOT_CHARGED, "uncharged VS Seeker reports NOT_CHARGED")
|
|
check(notChargedMsg and notChargedMsg:find("charge the battery: 60", 1, true) ~= nil, "uncharged text prints 100 - charge")
|
|
check(VsSeeker.getBattery(session) == 40, "uncharged use does not drain the battery")
|
|
package.loaded["src.core.game3.map"] = nil
|
|
|
|
print("=== 7. TM Case & Berry Pouch Sub-Containers & Bag State Persistence ===")
|
|
|
|
local bag = Bag.new()
|
|
Bag.add(bag, 289, 1) -- TM01
|
|
Bag.add(bag, 290, 2) -- TM02
|
|
Bag.add(bag, 139, 5) -- ORAN BERRY
|
|
Bag.add(bag, 142, 3) -- SITRUS BERRY
|
|
|
|
session.bag = bag
|
|
session.party = {
|
|
{ species = 1, nickname = "BULBASAUR", hp = 20, maxHp = 20, moves = { 33, 45 } },
|
|
}
|
|
|
|
-- TM Case listing
|
|
TmCase.show(session, bag)
|
|
check(TmCase.isOpen(), "TmCase opened")
|
|
local tmList = TmCase.list()
|
|
check(#tmList == 2, "TmCase has 2 TMs listed")
|
|
TmCase.close()
|
|
check(not TmCase.isOpen(), "TmCase closed cleanly")
|
|
|
|
-- Berry Pouch listing
|
|
BerryPouch.show(session, bag)
|
|
check(BerryPouch.isOpen(), "BerryPouch opened")
|
|
local berryList = BerryPouch.list()
|
|
check(#berryList == 2, "BerryPouch has 2 Berries listed")
|
|
BerryPouch.close()
|
|
check(not BerryPouch.isOpen(), "BerryPouch closed cleanly")
|
|
|
|
-- BagMenu sub-container transition & state preservation
|
|
BagMenu.show(session, { bag = bag })
|
|
BagMenu.settle()
|
|
BagMenu.pocketIdx = 2 -- KEY_ITEMS
|
|
BagMenu.cursor = 1
|
|
BagMenu.scroll = 0
|
|
|
|
-- Simulate selecting TM Case from Key Items
|
|
BagMenu.mode = "action"
|
|
BagMenu.actionCursor = 1 -- USE
|
|
local mockRow = { id = ItemsData.ITEM_TM_CASE, name = "TM CASE" }
|
|
-- Mock rows in BagMenu
|
|
BagMenu.list = function() return { mockRow } end
|
|
|
|
local mockInput = {
|
|
wasPressed = function(self, key)
|
|
return key == "a"
|
|
end
|
|
}
|
|
BagMenu.handleInput(mockInput)
|
|
BagMenu.settle()
|
|
check(TmCase.isOpen(), "Using TM CASE from BagMenu opens TmCase UI")
|
|
|
|
-- Close TM Case and verify BagMenu state restored
|
|
TmCase.close()
|
|
check(BagMenu.pocketIdx == 2, "BagMenu pocketIdx preserved (2 = KEY_ITEMS)")
|
|
check(BagMenu.cursor == 1, "BagMenu cursor preserved")
|
|
check(BagMenu.mode == "list", "BagMenu mode returned to list")
|
|
BagMenu.close()
|
|
|
|
print(string.format("\n=========================================="))
|
|
if failed == 0 then
|
|
print("ALL TESTS PASSED SUCCESSFULLY!")
|
|
else
|
|
print(string.format("TESTS FAILED WITH %d ERRORS", failed))
|
|
os.exit(1)
|
|
end
|