mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
257 lines
10 KiB
Lua
257 lines
10 KiB
Lua
-- CountStep (engine/overworld/events.asm) -- the per-step event chain.
|
|
-- ROM-free: `luajit tests/gen2_steps_test.lua`.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gen2 steps")
|
|
local check, eq = S.check, S.eq
|
|
|
|
local StepEvents = require("src.world.gen2.StepEvents")
|
|
local Breeding = require("src.core.gen2.Breeding")
|
|
local Happiness = require("src.core.gen2.Happiness")
|
|
local Phone = require("src.core.gen2.Phone")
|
|
|
|
local function mon(opts)
|
|
opts = opts or {}
|
|
return {
|
|
species = opts.species or "CHIKORITA",
|
|
name = opts.name or "CHIKORITA",
|
|
hp = opts.hp or 20,
|
|
maxHp = opts.maxHp or 20,
|
|
status = opts.status,
|
|
happiness = opts.happiness or 70,
|
|
level = opts.level or 5,
|
|
}
|
|
end
|
|
|
|
local function newSave(opts)
|
|
opts = opts or {}
|
|
return { party = opts.party or { mon() }, phone = {} }
|
|
end
|
|
|
|
-- ---- DoPoisonStep ---------------------------------------------------------
|
|
-- One HP off every poisoned mon that is still standing. A mon that runs out
|
|
-- has its status CLEARED on the way down, so a party wiped by poison walks into
|
|
-- the Center with nothing left to cure.
|
|
do
|
|
local party = {
|
|
mon({ hp = 5, status = "psn" }),
|
|
mon({ hp = 1, status = "psn" }),
|
|
mon({ hp = 9 }),
|
|
mon({ hp = 0, status = "psn" }),
|
|
}
|
|
local event = StepEvents.poisonStep(party)
|
|
eq(party[1].hp, 4, "a poisoned mon loses one HP")
|
|
eq(party[2].hp, 0, "and one on its last point drops")
|
|
check(party[2].status == nil, "with its status cleared as it goes")
|
|
eq(party[3].hp, 9, "an unpoisoned mon is untouched")
|
|
eq(party[4].hp, 0, "and an already-fainted one is skipped, not decremented")
|
|
eq(event.kind, "poisonFaint",
|
|
"one faint anywhere outranks the rest (wPoisonStepFlagSum and %10 first)")
|
|
eq(event.fainted[1], 2, "and names the slot that dropped")
|
|
check(event.blocks, "the faint arm sets carry, so no wild roll that step")
|
|
end
|
|
|
|
do
|
|
local party = { mon({ hp = 5, status = "psn" }) }
|
|
local event = StepEvents.poisonStep(party)
|
|
eq(event.kind, "poisonHurt", "damage with no faint is the .PlayPoisonSFX arm")
|
|
check(not event.blocks,
|
|
"which ends `xor a`: the step still counts and the grass still rolls")
|
|
check(StepEvents.poisonStep({}) == nil, "an empty party is no event at all")
|
|
check(StepEvents.poisonStep({ mon() }) == nil, "and a clean party is none")
|
|
end
|
|
|
|
-- CheckPlayerPartyForFitMon. An egg is not a fit mon.
|
|
do
|
|
check(StepEvents.whitedOut({ mon({ hp = 0 }) }), "no HP left is a whiteout")
|
|
check(not StepEvents.whitedOut({ mon({ hp = 1 }) }), "one point is not")
|
|
check(StepEvents.whitedOut({ mon({ hp = 0 }), { isEgg = true, hp = 0 } }),
|
|
"and an egg does not count as a fighter")
|
|
end
|
|
|
|
-- ---- DoRepelStep ----------------------------------------------------------
|
|
-- `dec a / ret nz`: the wear-off lands on the step that reaches zero, and that
|
|
-- step is not counted.
|
|
do
|
|
local save = newSave()
|
|
save.repelSteps = 2
|
|
check(not StepEvents.repelStep(save), "a repel with steps left is quiet")
|
|
eq(save.repelSteps, 1, "and ticks down")
|
|
check(StepEvents.repelStep(save), "the step that reaches zero wears off")
|
|
check(not StepEvents.repelStep(save), "and it only fires once")
|
|
end
|
|
|
|
-- ---- DoBikeStep -----------------------------------------------------------
|
|
do
|
|
local save = newSave()
|
|
save.bikeShopCall = true
|
|
save.bikeStep = 0
|
|
check(not StepEvents.bikeStep(save, { playerState = "normal" }),
|
|
"on foot the counter does not even move")
|
|
eq(save.bikeStep, 0, "literally does not move")
|
|
check(not StepEvents.bikeStep(save, { playerState = "bike",
|
|
phoneService = false }), "and a map with no service is refused")
|
|
save.bikeStep = StepEvents.BIKE_CALL_STEPS - 2
|
|
check(not StepEvents.bikeStep(save, { playerState = "bike" }),
|
|
"1022 steps is not yet 1024")
|
|
check(StepEvents.bikeStep(save, { playerState = "bike" }),
|
|
"the 1024th queues the call (`cp HIGH(1024)` on the counter's high byte)")
|
|
eq(Phone.specialCallVar(save), Phone.SPECIALCALL.SPECIALCALL_BIKESHOP,
|
|
"as SPECIALCALL_BIKESHOP")
|
|
check(save.bikeShopCall == false, "and clears the flag that asked for it")
|
|
|
|
-- "If a call has already been queued, don't overwrite that call."
|
|
local busy = newSave()
|
|
busy.bikeShopCall = true
|
|
busy.bikeStep = StepEvents.BIKE_CALL_STEPS
|
|
Phone.queueSpecialCall(busy, Phone.SPECIALCALL.SPECIALCALL_SSTICKET)
|
|
check(not StepEvents.bikeStep(busy, { playerState = "bike" }),
|
|
"a call already queued is not overwritten")
|
|
eq(Phone.specialCallVar(busy), Phone.SPECIALCALL.SPECIALCALL_SSTICKET,
|
|
"the S.S. Ticket call survives")
|
|
|
|
-- The counter saturates rather than wrapping (`cp 255` on both bytes).
|
|
local full = newSave()
|
|
full.bikeShopCall = true
|
|
full.bikeStep = StepEvents.BIKE_STEP_MAX
|
|
StepEvents.bikeStep(full, { playerState = "bike" })
|
|
eq(full.bikeStep, StepEvents.BIKE_STEP_MAX, "and stops at $ffff")
|
|
|
|
-- The flag the Goldenrod clerk actually writes: `setflag
|
|
-- ENGINE_BIKE_SHOP_CALL_ENABLED` lands on save.engineFlags, and DoBikeStep
|
|
-- reads it there rather than waiting for a field nothing sets.
|
|
local Bike = require("src.world.gen2.Bike")
|
|
local clerk = newSave()
|
|
clerk.engineFlags = {}
|
|
clerk.bikeStep = StepEvents.BIKE_CALL_STEPS
|
|
check(not StepEvents.bikeStep(clerk, { playerState = "bike" }),
|
|
"with the clerk's flag clear there is no call to make")
|
|
clerk.engineFlags[Bike.ENGINE_BIKE_SHOP_CALL_ENABLED] = true
|
|
check(StepEvents.bikeStep(clerk, { playerState = "bike" }),
|
|
"and once the clerk sets it, the call is queued")
|
|
check(clerk.engineFlags[Bike.ENGINE_BIKE_SHOP_CALL_ENABLED] == nil,
|
|
"`res STATUSFLAGS2_BIKE_SHOP_CALL_F`: one call, ever")
|
|
end
|
|
|
|
-- ---- CountStep, the whole block ------------------------------------------
|
|
do
|
|
local save = newSave()
|
|
local event, counted = StepEvents.count(save, {})
|
|
check(event == nil, "an ordinary footfall produces no player event")
|
|
check(counted, "and is counted")
|
|
eq(save.stepCount, 1, "wStepCount moved")
|
|
eq(save.poisonStepCount, 1, "and so did wPoisonStepCount")
|
|
end
|
|
|
|
-- Neither a special call nor a repel wearing off counts the step.
|
|
do
|
|
local save = newSave()
|
|
Phone.queueSpecialCall(save, Phone.SPECIALCALL.SPECIALCALL_SSTICKET)
|
|
local event, counted = StepEvents.count(save, { phone = {} })
|
|
eq(event.kind, "phoneCall", "a queued special call is the first thing checked")
|
|
check(not counted, "and the step is NOT counted")
|
|
eq(save.stepCount or 0, 0, "wStepCount is untouched")
|
|
check(event.blocks, "it queues a player event, so no wild roll")
|
|
end
|
|
|
|
do
|
|
local save = newSave()
|
|
save.repelSteps = 1
|
|
local event, counted = StepEvents.count(save, {})
|
|
eq(event.kind, "repel", "a repel wearing off is the second")
|
|
check(not counted, "and that step is not counted either")
|
|
eq(save.stepCount or 0, 0, "so the last repel step never ticks an egg")
|
|
end
|
|
|
|
-- StepHappiness is the wStepCount WRAP, and its own toggle halves it again:
|
|
-- 512 footfalls a point, not 256.
|
|
do
|
|
local save = newSave({ party = { mon({ happiness = 100 }) } })
|
|
for _ = 1, 256 do StepEvents.count(save, {}) end
|
|
eq(save.stepCount, 0, "256 steps wrap wStepCount")
|
|
eq(save.party[1].happiness, 100,
|
|
"and the first wrap only flips StepHappiness' own toggle")
|
|
for _ = 1, 256 do StepEvents.count(save, {}) end
|
|
eq(save.party[1].happiness, 101, "the SECOND wrap is what pays the point")
|
|
eq(Happiness.stepsToGain(save), 512, "and the next one is 512 away again")
|
|
end
|
|
|
|
-- DoEggStep is 128 steps offset from the happiness wrap, and a hatch skips the
|
|
-- rest of the block.
|
|
do
|
|
local egg = { isEgg = true, species = "TOGEPI", eggSteps = 1 }
|
|
local save = newSave({ party = { mon(), egg } })
|
|
local event
|
|
for _ = 1, Breeding.EGG_STEP_PHASE do
|
|
event = StepEvents.count(save, {})
|
|
end
|
|
eq(save.stepCount, Breeding.EGG_STEP_PHASE, "the tick lands at $80")
|
|
eq(egg.eggSteps, 0, "which spends the last cycle")
|
|
eq(event.kind, "hatch", "and answers PLAYEREVENT_HATCH")
|
|
check(event.blocks, "which stops the step: no wild battle on a hatch")
|
|
end
|
|
|
|
do
|
|
-- Nothing happens on any other step of the cycle.
|
|
local egg = { isEgg = true, species = "TOGEPI", eggSteps = 2 }
|
|
local save = newSave({ party = { egg } })
|
|
for _ = 1, Breeding.EGG_STEP_PHASE - 1 do StepEvents.count(save, {}) end
|
|
eq(egg.eggSteps, 2, "127 steps do not touch the counter")
|
|
end
|
|
|
|
-- Poison fires every fourth step, and the counter is reset rather than masked.
|
|
do
|
|
local poisoned = mon({ hp = 10, status = "psn" })
|
|
local save = newSave({ party = { poisoned } })
|
|
for _ = 1, 3 do StepEvents.count(save, {}) end
|
|
eq(poisoned.hp, 10, "three steps do no damage")
|
|
local event = StepEvents.count(save, {})
|
|
eq(poisoned.hp, 9, "the fourth does one point")
|
|
eq(event.kind, "poisonHurt", "and reports it")
|
|
eq(save.poisonStepCount, 0, "with the counter reset, not wrapped")
|
|
for _ = 1, 4 do StepEvents.count(save, {}) end
|
|
eq(poisoned.hp, 8, "and again four steps later")
|
|
end
|
|
|
|
-- The whiteout answer rides on the event so the caller does not have to ask.
|
|
do
|
|
local dying = mon({ hp = 1, status = "psn" })
|
|
local save = newSave({ party = { dying } })
|
|
local event
|
|
for _ = 1, 4 do event = StepEvents.count(save, {}) end
|
|
eq(event.kind, "poisonFaint", "the last point is a faint")
|
|
check(event.whiteout, "and with nothing else standing, a whiteout")
|
|
end
|
|
|
|
do
|
|
local dying = mon({ hp = 1, status = "psn" })
|
|
local save = newSave({ party = { dying, mon({ hp = 5 }) } })
|
|
local event
|
|
for _ = 1, 4 do event = StepEvents.count(save, {}) end
|
|
eq(event.kind, "poisonFaint", "a faint with a healthy mon behind it")
|
|
check(not event.whiteout, "is not a whiteout")
|
|
end
|
|
|
|
-- "Don't count steps in link communication rooms."
|
|
do
|
|
local save = newSave()
|
|
local event, counted = StepEvents.count(save, { linkMode = true })
|
|
check(event == nil and not counted, "a link room counts nothing")
|
|
eq(save.stepCount or 0, 0, "and moves no counter")
|
|
end
|
|
|
|
-- The battle writes "poison"/"toxic" (Battle.STATUS_EFFECTS); both spellings
|
|
-- must register as PSN out here or bad poison never hurts on the overworld.
|
|
do
|
|
local party = {
|
|
mon({ hp = 5, status = "toxic" }),
|
|
mon({ hp = 5, status = "poison" }),
|
|
}
|
|
local event = StepEvents.poisonStep(party)
|
|
eq(party[1].hp, 4, "a badly poisoned mon loses one HP on the step")
|
|
eq(party[2].hp, 4, "alongside the plain poison spelling")
|
|
eq(event.kind, "poisonHurt", "and the hurt arm fires for both")
|
|
end
|
|
|
|
S.finish()
|