mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
321 lines
13 KiB
Lua
321 lines
13 KiB
Lua
#!/usr/bin/env luajit
|
|
-- pokefirered/src/daycare.c:370
|
|
|
|
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
|
|
|
|
local function eq(a, b, msg)
|
|
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
|
|
end
|
|
|
|
local function finish()
|
|
if failed > 0 then
|
|
print("[test] FAILED " .. failed)
|
|
os.exit(1)
|
|
end
|
|
print("[test] all passed")
|
|
os.exit(0)
|
|
end
|
|
|
|
package.loaded["src.core.game3.audio"] = setmetatable({}, {
|
|
__index = function() return function() end end,
|
|
})
|
|
|
|
local store = { flags = {}, vars = {} }
|
|
local session = {
|
|
store = store, map = "FR_ROUTE_5_POKEMON_DAY_CARE", party = {},
|
|
name = "RED", trainerId = 4242, vars = {}, flags = {},
|
|
}
|
|
package.loaded["src.core.game3.runtime"] = {
|
|
getSession = function() return session end,
|
|
isActive = function() return true end,
|
|
}
|
|
|
|
local Daycare = require("src.core.game3.daycare")
|
|
local Natives = require("src.core.game3.scripting.natives")
|
|
local Std = require("src.core.game3.scripting.stdscripts")
|
|
local Flags = require("src.core.game3.scripting.flags")
|
|
local NativesDaycare = require("src.core.game3.scripting.natives_daycare")
|
|
local StepEvents = require("src.core.game3.step_events")
|
|
|
|
local VAR_0x8004 = 0x8004
|
|
local VAR_0x8005 = 0x8005
|
|
|
|
local function newCtx()
|
|
return { specialVars = {}, stringVars = { [1] = "", [2] = "", [3] = "" } }
|
|
end
|
|
|
|
local function getVar(ctx, id) return tonumber(Flags.getVar(store, ctx, id)) or 0 end
|
|
local function setVar(ctx, id, v) Flags.setVar(store, ctx, id, v) end
|
|
|
|
print("[test] 1. the specials layer and the model are one source of truth")
|
|
eq(NativesDaycare.stateOf, Daycare.stateOf, "natives_daycare.stateOf is the model's")
|
|
eq(NativesDaycare.route5Of, Daycare.route5Of, "natives_daycare.route5Of is the model's")
|
|
eq(NativesDaycare.cost, Daycare.cost, "natives_daycare.cost is the model's")
|
|
eq(Daycare.DAYCARE_MON_COUNT, 2, "the Four Island day care holds two")
|
|
|
|
local Cache = require("tests.game3_cache")
|
|
local cacheRoot = Cache.mount("meta.json")
|
|
if not cacheRoot then
|
|
print("[skip] game3_daycare_model_test needs species data: " .. tostring(Cache.reason))
|
|
finish()
|
|
end
|
|
print("[info] FireRed cache at " .. cacheRoot)
|
|
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
Pokemon.install(nil)
|
|
local Experience = require("src.core.game3.battle.experience")
|
|
local SummaryData = require("src.core.game3.summary_data")
|
|
local Party = require("src.core.game3.party")
|
|
|
|
local function makeMon(species, level)
|
|
local scratch = { party = {}, name = "RED", trainerId = 4242 }
|
|
local ok, _, mon = Party.giveMon(scratch, species, level, "")
|
|
check(ok, "built a level " .. level .. " species " .. species)
|
|
return mon
|
|
end
|
|
|
|
local function stepsForLevels(mon, toLevel)
|
|
local growth = Experience.growthRate(mon)
|
|
return SummaryData.expForLevel(growth, toLevel)
|
|
- SummaryData.expForLevel(growth, tonumber(mon.level) or 1)
|
|
end
|
|
|
|
local function walk(n)
|
|
for _ = 1, n do StepEvents.onStepTaken(session, nil) end
|
|
end
|
|
|
|
local function deepCopy(value, seen)
|
|
if type(value) ~= "table" then return value end
|
|
seen = seen or {}
|
|
if seen[value] then return seen[value] end
|
|
local out = {}
|
|
seen[value] = out
|
|
for k, v in pairs(value) do out[deepCopy(k, seen)] = deepCopy(v, seen) end
|
|
return out
|
|
end
|
|
|
|
print("[test] 2. the two day cares are separate records")
|
|
session.party = { makeMon(19, 5), makeMon(16, 5), makeMon(21, 5) }
|
|
session.modData = nil
|
|
session.daycare = nil
|
|
session.route5Daycare = nil
|
|
local ctx = newCtx()
|
|
setVar(ctx, VAR_0x8004, 0)
|
|
Natives.special(ctx, Std.SPECIAL.PutMonInRoute5Daycare, nil)
|
|
ctx = newCtx()
|
|
setVar(ctx, VAR_0x8004, 0)
|
|
Natives.special(ctx, Std.SPECIAL.StoreSelectedPokemonInDaycare, nil)
|
|
local r5 = Daycare.route5Of(session)
|
|
local dc = Daycare.stateOf(session)
|
|
eq(r5.mon and r5.mon.species, 19, "the Route 5 slot holds the RATTATA")
|
|
eq(Daycare.mon(dc, 1) and Daycare.mon(dc, 1).species, 16,
|
|
"the Four Island slot 1 holds the PIDGEY")
|
|
eq(Daycare.count(dc), 1, "one mon in the Four Island day care")
|
|
eq(#session.party, 1, "the party kept only the third mon")
|
|
local _, state = Natives.special(newCtx(), Std.SPECIAL.GetDaycareState, nil)
|
|
eq(state, 2, "GetDaycareState is DAYCARE_ONE_MON")
|
|
|
|
print("[test] 3. every field step feeds both day cares one experience point")
|
|
local r5Before, dcBefore = r5.steps, dc.steps[1]
|
|
local partyExpBefore = session.party[1].exp
|
|
walk(40)
|
|
eq(r5.steps - r5Before, 40, "the Route 5 mon banked 40 steps")
|
|
eq(dc.steps[1] - dcBefore, 40, "the Four Island slot 1 banked 40 steps")
|
|
eq(dc.steps[2], 0, "the empty second slot banks nothing")
|
|
eq(dc.stepCounter, 40 % 256, "the shared step counter advanced with them")
|
|
eq(session.party[1].exp, partyExpBefore, "a mon in the party gains nothing from walking")
|
|
|
|
print("[test] 4. the level menu and the cost follow the banked steps")
|
|
local stored = r5.mon
|
|
local want = stepsForLevels(stored, 8)
|
|
walk(want - r5.steps)
|
|
eq(r5.steps, want, "walked exactly to the level 8 threshold")
|
|
eq(Daycare.levelAfterSteps(stored, r5.steps), 8, "GetLevelAfterDaycareSteps reads level 8")
|
|
ctx = newCtx()
|
|
local _, gained = Natives.special(ctx, Std.SPECIAL.GetNumLevelsGainedForRoute5DaycareMon, nil)
|
|
eq(gained, 3, "three levels gained")
|
|
eq(ctx.stringVars[2], "3", "STR_VAR_2 carries the level count")
|
|
ctx = newCtx()
|
|
Natives.special(ctx, Std.SPECIAL.GetCostToWithdrawRoute5DaycareMon, nil)
|
|
eq(getVar(ctx, VAR_0x8005), 400, "the cost is 100 + 100 * 3")
|
|
eq(Daycare.cost(stored, 0), 100, "no levels gained costs the flat 100")
|
|
eq(Daycare.cost(stored, stepsForLevels(stored, 6)), 200, "one level costs 200")
|
|
|
|
print("[test] 5. the stay survives a save and reload")
|
|
local Schema = require("src.core.game3.save_schema_firered")
|
|
local saved = deepCopy(Schema.toSaveTable(session))
|
|
local loaded = Schema.fromSaveTable(saved)
|
|
package.loaded["src.core.game3.runtime"].getSession = function() return loaded end
|
|
local prevSession = session
|
|
session = loaded
|
|
session.store = store
|
|
local loadedR5 = Daycare.route5Of(session)
|
|
local loadedDc = Daycare.stateOf(session)
|
|
eq(loadedR5.mon and loadedR5.mon.species, 19, "the Route 5 mon came back from the save")
|
|
eq(loadedR5.steps, want, "with its banked steps")
|
|
eq(loadedDc.steps[1], want, "and the Four Island slot kept its own count")
|
|
check(loadedR5.mon ~= prevSession.route5Daycare.mon, "the reload really rebuilt the record")
|
|
walk(10)
|
|
eq(Daycare.route5Of(session).steps, want + 10, "steps keep accruing after the reload")
|
|
|
|
print("[test] 6. taking the mon back applies the levels and the moves it learned")
|
|
local learnLevel, learnMove
|
|
for _, entry in ipairs(Pokemon.learnset(19)) do
|
|
local lv = entry[1] or entry.level or 0
|
|
if lv > 5 and lv <= 8 then
|
|
learnLevel, learnMove = lv, tonumber(entry[2] or entry.move)
|
|
break
|
|
end
|
|
end
|
|
check(learnMove ~= nil, "RATTATA learns something between level 6 and 8")
|
|
ctx = newCtx()
|
|
local _, taken = Natives.special(ctx, Std.SPECIAL.TakePokemonFromRoute5Daycare, nil)
|
|
eq(taken, 19, "TakePokemonFromRoute5Daycare returns the species")
|
|
local back = session.party[#session.party]
|
|
eq(back.species, 19, "the mon is back in the party")
|
|
eq(back.level, 8, "three levels higher")
|
|
eq(back.hp, back.maxHp, "at full HP, as BoxMonToMon leaves it")
|
|
eq(Daycare.route5Of(session).mon, nil, "the Route 5 slot is empty again")
|
|
local knows = false
|
|
for _, mv in ipairs(back.moves or {}) do if mv == learnMove then knows = true end end
|
|
check(knows, "it knows the move it learned at level " .. tostring(learnLevel))
|
|
|
|
print("[test] 7. a full moveset drops the first move for the one learned away")
|
|
local crammed = makeMon(19, 5)
|
|
local pool = {}
|
|
for _, entry in ipairs(Pokemon.learnset(19)) do
|
|
local mv = tonumber(entry[2] or entry.move)
|
|
local dup = (mv == learnMove)
|
|
for _, have in ipairs(pool) do if have == mv then dup = true end end
|
|
if mv and mv > 0 and not dup and #pool < 4 then pool[#pool + 1] = mv end
|
|
end
|
|
eq(#pool, 4, "four distinct level-up moves to cram in")
|
|
crammed.moves = { pool[1], pool[2], pool[3], pool[4] }
|
|
crammed.pp = { 10, 10, 10, 10 }
|
|
crammed.maxPp = { 10, 10, 10, 10 }
|
|
session.party[#session.party + 1] = crammed
|
|
ctx = newCtx()
|
|
setVar(ctx, VAR_0x8004, #session.party - 1)
|
|
Natives.special(ctx, Std.SPECIAL.PutMonInRoute5Daycare, nil)
|
|
r5 = Daycare.route5Of(session)
|
|
eq(r5.mon, crammed, "the crammed mon went in")
|
|
walk(stepsForLevels(crammed, learnLevel))
|
|
ctx = newCtx()
|
|
Natives.special(ctx, Std.SPECIAL.TakePokemonFromRoute5Daycare, nil)
|
|
eq(crammed.level, learnLevel, "it came back at the level it learned the move")
|
|
eq(#crammed.moves, 4, "still four moves")
|
|
eq(crammed.moves[4], learnMove, "the new move landed in the last slot")
|
|
eq(crammed.moves[1], pool[2], "and the first move it knew was deleted")
|
|
|
|
print("[test] 8. the Four Island slots shift up when the first one leaves")
|
|
session.daycare = nil
|
|
session.modData[Daycare.SAVE_KEY].daycare = nil
|
|
dc = Daycare.stateOf(session)
|
|
local first, second = makeMon(19, 5), makeMon(16, 5)
|
|
session.party = { first, second }
|
|
for slot = 1, 2 do
|
|
ctx = newCtx()
|
|
setVar(ctx, VAR_0x8004, 0)
|
|
Natives.special(ctx, Std.SPECIAL.StoreSelectedPokemonInDaycare, nil)
|
|
end
|
|
eq(Daycare.count(dc), 2, "both mons are boarded")
|
|
eq(#session.party, 0, "and the party is empty")
|
|
walk(30)
|
|
eq(dc.steps[1], 30, "slot 1 banked its steps")
|
|
eq(dc.steps[2], 30, "slot 2 banked its own")
|
|
local _, two = Natives.special(newCtx(), Std.SPECIAL.GetDaycareState, nil)
|
|
eq(two, 3, "GetDaycareState is DAYCARE_TWO_MONS")
|
|
ctx = newCtx()
|
|
setVar(ctx, VAR_0x8004, 0)
|
|
local _, outSpecies = Natives.special(ctx, Std.SPECIAL.TakePokemonFromDaycare, nil)
|
|
eq(outSpecies, 19, "TakePokemonFromDaycare returned the first mon")
|
|
eq(Daycare.mon(dc, 1), second, "ShiftDaycareSlots moved the second mon into slot 1")
|
|
eq(Daycare.mon(dc, 2), nil, "and emptied slot 2")
|
|
eq(dc.steps[1], 30, "the shifted mon kept its banked steps")
|
|
eq(dc.steps[2], 0, "slot 2's counter is cleared")
|
|
|
|
print("[test] 9. a full party refuses the withdrawal at the special seam")
|
|
session.party = {}
|
|
for i = 1, 6 do session.party[i] = makeMon(21, 5) end
|
|
local slot6 = session.party[6]
|
|
ctx = newCtx()
|
|
setVar(ctx, VAR_0x8004, 0)
|
|
local _, refused = Natives.special(ctx, Std.SPECIAL.TakePokemonFromDaycare, nil)
|
|
-- data/maps/FourIsland_PokemonDayCare/scripts.inc:86-88
|
|
eq(refused, 0, "a full party refuses the withdrawal (SPECIES_NONE)")
|
|
eq(#session.party, 6, "the party is still six mons")
|
|
eq(session.party[6], slot6, "slot 6 still holds its own mon")
|
|
eq(Daycare.mon(dc, 1), second, "and the mon stays in the daycare")
|
|
-- src/daycare.c:525
|
|
eq(Daycare.take(session, 1), 16, "the model still withdraws the mon")
|
|
eq(#session.party, 6, "the party never grows past six")
|
|
eq(session.party[6], second, "pret's gPlayerParty[PARTY_SIZE - 1] write holds the mon")
|
|
|
|
print("[test] 9b. Route 5 refuses the withdrawal on a full party too")
|
|
-- data/scripts/day_care.inc:79-81
|
|
r5 = Daycare.route5Of(session)
|
|
r5.mon = makeMon(19, 5)
|
|
session.party = {}
|
|
for i = 1, 6 do session.party[i] = makeMon(21, 5) end
|
|
local r5Slot = session.party[6]
|
|
ctx = newCtx()
|
|
setVar(ctx, VAR_0x8004, 0)
|
|
local _, r5Refused = Natives.special(ctx, Std.SPECIAL.TakePokemonFromRoute5Daycare, nil)
|
|
eq(r5Refused, 0, "a full party refuses the Route 5 withdrawal (SPECIES_NONE)")
|
|
eq(session.party[6], r5Slot, "slot 6 is untouched")
|
|
check(r5.mon ~= nil, "and the mon stays with the day-care man")
|
|
r5.mon = nil
|
|
|
|
print("[test] 10. ChooseSendDaycareMon picks the mon through the party seam")
|
|
local asked
|
|
local adapters = {
|
|
chooseParty = function(opts, done)
|
|
asked = opts
|
|
done(2)
|
|
end,
|
|
setStringVar = function() end,
|
|
}
|
|
ctx = newCtx()
|
|
setVar(ctx, VAR_0x8004, 0)
|
|
Natives.special(ctx, Std.SPECIAL.ChooseSendDaycareMon, adapters)
|
|
-- pokefirered/src/party_menu.c:6318 InitPartyMenu(PARTY_MENU_TYPE_DAYCARE, ...)
|
|
eq(asked and asked.menuType, 6, "the party opened as PARTY_MENU_TYPE_DAYCARE")
|
|
eq(getVar(ctx, VAR_0x8004), 2, "VAR_0x8004 carries the slot the player chose")
|
|
adapters.chooseParty = function(opts, done)
|
|
asked = opts
|
|
done(nil)
|
|
end
|
|
ctx = newCtx()
|
|
setVar(ctx, VAR_0x8004, 0)
|
|
Natives.special(ctx, Std.SPECIAL.ChooseSendDaycareMon, adapters)
|
|
-- pokefirered/data/maps/FourIsland_PokemonDayCare/scripts.inc:25 goto_if_ge PARTY_SIZE
|
|
check(getVar(ctx, VAR_0x8004) >= 6, "backing out returns a slot the script reads as a cancel")
|
|
|
|
print("[test] 11. GetDaycareMonNicknames fills the three string vars")
|
|
session.daycare = nil
|
|
session.modData[Daycare.SAVE_KEY].daycare = nil
|
|
dc = Daycare.stateOf(session)
|
|
session.party = { makeMon(19, 5), makeMon(16, 5) }
|
|
for _ = 1, 2 do
|
|
ctx = newCtx()
|
|
setVar(ctx, VAR_0x8004, 0)
|
|
Natives.special(ctx, Std.SPECIAL.StoreSelectedPokemonInDaycare, nil)
|
|
end
|
|
ctx = newCtx()
|
|
Natives.special(ctx, Std.SPECIAL.GetDaycareMonNicknames, nil)
|
|
eq(ctx.stringVars[1], Pokemon.name(19), "STR_VAR_1 is the first slot's nickname")
|
|
eq(ctx.stringVars[2], Pokemon.name(16), "STR_VAR_2 is the second slot's nickname")
|
|
eq(ctx.stringVars[3], "RED", "STR_VAR_3 is the first mon's OT name")
|
|
|
|
finish()
|