Files
gen1recomp/tests/game3_step_events_queue_test.lua
T
bryanthaboi 661d75ef1c junk
2026-09-18 16:49:48 -04:00

280 lines
9.7 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 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:find("Repel's effect wore off") 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 retail 'Repel's effect wore off...' message without prompt")
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")
-- 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)")
local whiteoutTriggered = false
session.onWhiteout = function()
whiteoutTriggered = true
end
StepEvents.update(0.1)
check(whiteoutTriggered, "Whiteout triggered and StepEvents queue cleanly flushed")
check(not StepEvents.busy(), "StepEvents queue completely empty after whiteout")
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