mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
fix(game3): battle animation timing, RNG parity, pokedex habitats, multichoice UI and overworld events
- Battle Animation Pipeline:
* Eliminate 90-frame audio polling stall in anim_vm end/waitsound opcodes,
allowing attack animations to transition instantly into target hit flicker
and HP bar drain.
* Correct visual task counting in AnimVm:visualCount to match pret gAnimVisualTaskCount.
* Signal bg task completion on restorebg (vm.args[7] = -1).
* Fix termination conditions on shake tasks (ShakeMon, ShakeMon2, ShakeBattleTerrain),
WaterSport droplet arcs, and StartSinAnimTimer.
* Handle RGB555 color args in start_blend_anim_sprite_color and route P.destroyTask
to AnimTasks.destroy.
- RNG System & Seeding Parity:
* Replace math.random in Party.giveMon with Rng.Random32() for personality values
and Rng.Random() for IV bitfield extraction (HP/Atk/Def/Spe/SpA/SpD) matching GBA format.
* Seed RNG on Title Screen A/Start press via Rng.seedNewGame() (matching SeedRngAndSetTrainerId)
and perturb RNG state on title menu inputs.
* Route NPC overworld idle/wander timers and wild encounter slot rolls through Rng.compat.
- Pokédex Habitat & Area Maps:
* Fix encounter tables extraction and map indexing to resolve Pokemon habitat
locations across Kanto/Sevii maps rather than showing Area Unknown.
* Render steady semi-transparent red overlays for Pokédex area map locations.
- Multichoice Menus & UI:
* Implement multichoice table extraction and data population for Viridian blackboard
status ailments and special dialog interactions.
* Improve choice box borders, pagination, and Town Map wall viewing.
- Overworld Events & Flags:
* Fix Route 1 entrance girl NPC spawning and script triggers after starter selection.
* Ensure consistent save reload state in Oak's Lab.
This commit is contained in:
@@ -135,6 +135,8 @@ end
|
||||
local function rng_fn(st, opts)
|
||||
if opts and opts.rng then return opts.rng end
|
||||
if st and type(st.rng) == "function" then return st.rng end
|
||||
local okR, Rng = pcall(require, "src.core.game3.rng")
|
||||
if okR and Rng and Rng.compat then return Rng.compat end
|
||||
return math.random
|
||||
end
|
||||
|
||||
@@ -145,6 +147,10 @@ local function roll(rng, lo, hi)
|
||||
if ok and type(v) == "number" then
|
||||
return lo + (math.floor(v) % (hi - lo + 1))
|
||||
end
|
||||
local okR, Rng = pcall(require, "src.core.game3.rng")
|
||||
if okR and Rng and Rng.compat then
|
||||
return Rng.compat(lo, hi)
|
||||
end
|
||||
return math.random(lo, hi)
|
||||
end
|
||||
|
||||
|
||||
@@ -131,6 +131,10 @@ AiSwitch.aiTypeCalc = ai_type_calc
|
||||
local function roll(rng, lo, hi)
|
||||
local ok, v = pcall(rng, lo, hi)
|
||||
if ok and type(v) == "number" then return v end
|
||||
local okR, Rng = pcall(require, "src.core.game3.rng")
|
||||
if okR and Rng and Rng.compat then
|
||||
return Rng.compat(lo, hi)
|
||||
end
|
||||
return math.random(lo, hi)
|
||||
end
|
||||
|
||||
|
||||
@@ -708,7 +708,11 @@ end
|
||||
|
||||
function P.destroyTask(t)
|
||||
local AnimTasks = package.loaded["src.core.game3.battle.anim_tasks"]
|
||||
if AnimTasks and AnimTasks._destroy then AnimTasks._destroy(t) else t.active = false end
|
||||
if AnimTasks and (AnimTasks.destroy or AnimTasks._destroy) then
|
||||
(AnimTasks.destroy or AnimTasks._destroy)(t)
|
||||
else
|
||||
t.active = false
|
||||
end
|
||||
end
|
||||
|
||||
function P.setBldAlpha(vm, eva, evb)
|
||||
|
||||
@@ -157,7 +157,7 @@ local function shake_terrain_step(t, vm)
|
||||
if y == -d[1] then y = 0 else y = -d[1] end
|
||||
d[3] = d[8]
|
||||
d[2] = d[2] - 1
|
||||
if d[2] == 0 then
|
||||
if d[2] <= 0 then
|
||||
shake_terrain_set(vm, 0, 0)
|
||||
K.destroy(t)
|
||||
return
|
||||
@@ -205,10 +205,17 @@ local function start_blend_anim_sprite_color(t, keys)
|
||||
local A, d = t._A, t.data
|
||||
t._keys = keys
|
||||
d[2] = A[1]
|
||||
d[3] = A[2]
|
||||
d[4] = A[3]
|
||||
d[5] = A[4]
|
||||
d[10] = A[2]
|
||||
if (A[2] or 0) > 16 then
|
||||
d[3] = 0
|
||||
d[4] = 16
|
||||
d[5] = A[2]
|
||||
d[10] = 0
|
||||
else
|
||||
d[3] = A[2]
|
||||
d[4] = A[3]
|
||||
d[5] = A[4]
|
||||
d[10] = A[2]
|
||||
end
|
||||
t._fn = blend_sprite_color_step2
|
||||
t._fn(t)
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@ local function shake_mon_step(t)
|
||||
if (p.oy or 0) == 0 then p.oy = d[5] else p.oy = 0 end
|
||||
d[3] = d[2]
|
||||
d[1] = d[1] - 1
|
||||
if d[1] == 0 then
|
||||
if d[1] <= 0 then
|
||||
p.ox = 0
|
||||
p.oy = 0
|
||||
K.destroy(t)
|
||||
@@ -64,7 +64,7 @@ local function shake_mon2_step(t)
|
||||
if (p.oy or 0) == d[5] then p.oy = -d[5] else p.oy = d[5] end
|
||||
d[3] = d[2]
|
||||
d[1] = d[1] - 1
|
||||
if d[1] == 0 then
|
||||
if d[1] <= 0 then
|
||||
p.ox = 0
|
||||
p.oy = 0
|
||||
K.destroy(t)
|
||||
|
||||
@@ -961,9 +961,9 @@ return function(K)
|
||||
|
||||
-- pokefirered/src/battle_anim_water.c:696
|
||||
local function runSinTimer(t, vm)
|
||||
vm.args[7] = P.band(vm.args[7] + 3, 0xFF)
|
||||
t.data[0] = t.data[0] - 1
|
||||
if t.data[0] == 0 then D(t) end
|
||||
vm.args[7] = P.band((vm.args[7] or 0) + 3, 0xFF)
|
||||
t.data[0] = (t.data[0] or 0) - 1
|
||||
if t.data[0] <= 0 then D(t) end
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:1831
|
||||
@@ -1251,6 +1251,7 @@ return function(K)
|
||||
if P.translateHArc(s) then
|
||||
s.x = s.x + s.ox
|
||||
s.y = s.y + s.oy
|
||||
s.ox, s.oy = 0, 0
|
||||
s.data[0] = 6
|
||||
s.data[2] = P.band(P.Random(), 0x1F) - 16 + s.x
|
||||
s.data[4] = P.band(P.Random(), 0x1F) - 16 + s.y
|
||||
@@ -1285,7 +1286,7 @@ return function(K)
|
||||
local st = d[0]
|
||||
if st == 0 then
|
||||
waterSportCreate(t, vm)
|
||||
if d[10] ~= 0 then d[0] = d[0] + 1 end
|
||||
if d[10] == 0 then d[0] = d[0] + 1 else d[0] = d[0] + 2 end
|
||||
elseif st == 1 then
|
||||
waterSportCreate(t, vm)
|
||||
d[1] = d[1] + 1
|
||||
@@ -1323,7 +1324,8 @@ return function(K)
|
||||
d[0] = d[0] + 1
|
||||
end
|
||||
elseif st == 6 then
|
||||
if d[8] == 0 then d[0] = d[0] + 1 end
|
||||
d[1] = (d[1] or 0) + 1
|
||||
if d[8] <= 0 or d[1] > 60 then d[0] = d[0] + 1 end
|
||||
else
|
||||
D(t)
|
||||
end
|
||||
@@ -1332,6 +1334,7 @@ return function(K)
|
||||
-- pokefirered/src/battle_anim_water.c:1343
|
||||
TK.WaterSport = function(t, vm)
|
||||
local d = t.data
|
||||
d[10] = vm.args[0] or 0
|
||||
d[3] = P.coord(vm, P.atk(vm), P.X_2)
|
||||
d[4] = P.coord(vm, P.atk(vm), P.Y_PIC_OFFSET)
|
||||
d[7] = (P.atk(vm) == "player") and 1 or -1
|
||||
|
||||
@@ -113,14 +113,6 @@ local function se12_panpot(pan)
|
||||
end
|
||||
AnimVm.se12PanpotControl = se12_panpot
|
||||
|
||||
local function se_playing()
|
||||
local ok, Audio = pcall(require, "src.core.game3.audio")
|
||||
if ok and Audio and Audio.isSePlaying then
|
||||
local ok2, v = pcall(Audio.isSePlaying)
|
||||
return ok2 and v or false
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function default_pal()
|
||||
local p = {}
|
||||
@@ -351,11 +343,6 @@ function AnimVm:visualCount()
|
||||
local t = AnimTasks._pool[i]
|
||||
if t.active and t._g4kind ~= "sound" and t._g4kind ~= "aux" and not t._uncounted then n = n + 1 end
|
||||
end
|
||||
AnimSprites.init()
|
||||
for i = 1, AnimSprites.MAX do
|
||||
local s = AnimSprites._pool[i]
|
||||
if s.active and s._g4counted then n = n + 1 end
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
@@ -1333,19 +1320,9 @@ end
|
||||
-- pokefirered/src/battle_anim.c:1526
|
||||
OPS.waitsound = function(vm)
|
||||
if vm:soundCount() ~= 0 then
|
||||
vm._soundWait = 0
|
||||
vm.framesToWait = 1
|
||||
return false
|
||||
elseif se_playing() then
|
||||
vm._soundWait = (vm._soundWait or 0) + 1
|
||||
if vm._soundWait > 90 then
|
||||
vm._soundWait = 0
|
||||
else
|
||||
vm.framesToWait = 1
|
||||
return false
|
||||
end
|
||||
end
|
||||
vm._soundWait = 0
|
||||
vm.framesToWait = 0
|
||||
return true
|
||||
end
|
||||
@@ -1378,19 +1355,10 @@ OPS["end"] = function(vm)
|
||||
vm._endWait = (vm._endWait or 0) + 1
|
||||
local capped = vm._endWait > WAIT_CAP
|
||||
if not capped and (vm:visualCount() ~= 0 or vm:soundCount() ~= 0 or next(vm._monbg) ~= nil) then
|
||||
vm._soundWait = 0
|
||||
vm.framesToWait = 1
|
||||
return false
|
||||
end
|
||||
if not capped and se_playing() then
|
||||
vm._soundWait = (vm._soundWait or 0) + 1
|
||||
if vm._soundWait <= 90 then
|
||||
vm.framesToWait = 1
|
||||
return false
|
||||
end
|
||||
end
|
||||
if capped then print("[battle.anim] end wait cap") end
|
||||
vm._soundWait = 0
|
||||
vm._endWait = 0
|
||||
finish(vm)
|
||||
return "end"
|
||||
@@ -1592,6 +1560,7 @@ end
|
||||
|
||||
-- pokefirered/src/battle_anim.c:1114
|
||||
OPS.restorebg = function(vm)
|
||||
vm.args[7] = -1
|
||||
start_bg_fade(vm, -1)
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -353,7 +353,16 @@ function Commands.tryFlee(st, adapter)
|
||||
local roll = adapter:rng()
|
||||
local r
|
||||
local ok, v = pcall(roll, 0, 255)
|
||||
if ok and type(v) == "number" then r = v else r = math.random(0, 255) end
|
||||
if ok and type(v) == "number" then
|
||||
r = v
|
||||
else
|
||||
local okR, Rng = pcall(require, "src.core.game3.rng")
|
||||
if okR and Rng and Rng.compat then
|
||||
r = Rng.compat(0, 255)
|
||||
else
|
||||
r = math.random(0, 255)
|
||||
end
|
||||
end
|
||||
if r < odds then
|
||||
adapter:say("Got away safely!")
|
||||
return true
|
||||
|
||||
@@ -105,6 +105,10 @@ local function roll_from(rng, lo, hi)
|
||||
local ok, v = pcall(rng, lo, hi)
|
||||
if ok and type(v) == "number" then return v end
|
||||
end
|
||||
local okR, Rng = pcall(require, "src.core.game3.rng")
|
||||
if okR and Rng and Rng.compat then
|
||||
return Rng.compat(lo, hi)
|
||||
end
|
||||
return math.random(lo, hi)
|
||||
end
|
||||
|
||||
|
||||
@@ -89,8 +89,14 @@ end
|
||||
Engine.hasFlag = has_flag
|
||||
|
||||
local function roll(adapter, lo, hi)
|
||||
local ok, v = pcall(adapter:rng(), lo, hi)
|
||||
if ok and type(v) == "number" then return v end
|
||||
if adapter and adapter.rng then
|
||||
local ok, v = pcall(adapter:rng(), lo, hi)
|
||||
if ok and type(v) == "number" then return v end
|
||||
end
|
||||
local okR, Rng = pcall(require, "src.core.game3.rng")
|
||||
if okR and Rng and Rng.compat then
|
||||
return Rng.compat(lo, hi)
|
||||
end
|
||||
return math.random(lo, hi)
|
||||
end
|
||||
Engine.roll = roll
|
||||
|
||||
@@ -129,6 +129,38 @@ local function foe_mon_from(foe)
|
||||
if gender ~= "M" and gender ~= "F" and gender ~= "U" then
|
||||
gender = Pokemon.gender and Pokemon.gender(species, personality) or "U"
|
||||
end
|
||||
local ivs = foe.ivs
|
||||
if ivs == nil then
|
||||
local iv1 = Rng.Random()
|
||||
local iv2 = Rng.Random()
|
||||
ivs = {
|
||||
hp = iv1 % 32,
|
||||
atk = math.floor(iv1 / 32) % 32,
|
||||
def = math.floor(iv1 / 1024) % 32,
|
||||
spe = iv2 % 32,
|
||||
spa = math.floor(iv2 / 32) % 32,
|
||||
spd = math.floor(iv2 / 1024) % 32,
|
||||
}
|
||||
end
|
||||
local item = foe.item
|
||||
if item == nil then
|
||||
local meta = Pokemon.speciesMeta and Pokemon.speciesMeta(species)
|
||||
if meta then
|
||||
local common = tonumber(meta.itemCommon) or 0
|
||||
local rare = tonumber(meta.itemRare) or 0
|
||||
if common ~= 0 or rare ~= 0 then
|
||||
local r = Rng.Random() % 100
|
||||
if common ~= 0 and rare ~= 0 then
|
||||
if r < 50 then item = common
|
||||
elseif r < 55 then item = rare end
|
||||
elseif common ~= 0 then
|
||||
if r < 50 then item = common end
|
||||
elseif rare ~= 0 then
|
||||
if r < 5 then item = rare end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local mon = {
|
||||
species = species,
|
||||
level = foe.level or 5,
|
||||
@@ -142,9 +174,9 @@ local function foe_mon_from(foe)
|
||||
spAtk = foe.spAtk or foe.spa,
|
||||
spDef = foe.spDef or foe.spd,
|
||||
speed = foe.speed or foe.spe,
|
||||
item = foe.item,
|
||||
item = item,
|
||||
gender = gender,
|
||||
ivs = foe.ivs,
|
||||
ivs = ivs,
|
||||
evs = foe.evs,
|
||||
personality = personality,
|
||||
nature = foe.nature or (Pokemon.natureId and Pokemon.natureId(personality)) or 0,
|
||||
@@ -1555,6 +1587,9 @@ function D.commandUpdate(input)
|
||||
if input then party_menu_input(PartyMenu, input) end
|
||||
return
|
||||
end
|
||||
if Ui._mode == "bag" or Ui._mode == "party" then
|
||||
Ui._mode = "menu"
|
||||
end
|
||||
if Ui.selectionPump() then
|
||||
local scmd = Ui.takeCommand()
|
||||
if scmd then D.onCommand(scmd) end
|
||||
@@ -2145,6 +2180,9 @@ function Battle.update(dt, game)
|
||||
if input then party_menu_input(PartyMenu, input) end
|
||||
return
|
||||
end
|
||||
if Ui._mode == "bag" or Ui._mode == "party" then
|
||||
Ui._mode = "menu"
|
||||
end
|
||||
if Ui.selectionPump() then
|
||||
local scmd = Ui.takeCommand()
|
||||
if scmd then begin_turn_with(scmd) end
|
||||
|
||||
@@ -40,6 +40,10 @@ local function roll(rng, lo, hi)
|
||||
local ok, v = pcall(rng, lo, hi)
|
||||
if ok and type(v) == "number" then return v end
|
||||
end
|
||||
local okR, Rng = pcall(require, "src.core.game3.rng")
|
||||
if okR and Rng and Rng.compat then
|
||||
return Rng.compat(lo, hi)
|
||||
end
|
||||
return math.random(lo, hi)
|
||||
end
|
||||
|
||||
@@ -74,6 +78,28 @@ function BattleItems.needsPartySelect(id)
|
||||
return false
|
||||
end
|
||||
|
||||
function BattleItems.canUseOn(st, itemId, partySlot, mon)
|
||||
if not mon or not itemId then return false, "It won't have any effect." end
|
||||
if mon.isEgg then return false, "An EGG can't be used on." end
|
||||
local hp = tonumber(mon.hp) or 0
|
||||
local maxHp = tonumber(mon.maxHp or mon.maxhp) or 1
|
||||
local mk = ItemsData.medicineKind(itemId)
|
||||
local use = ItemsData.fieldUseKind(itemId)
|
||||
if mk == "revive" or use == "revive" then
|
||||
if hp > 0 then return false, "It won't have any effect." end
|
||||
return true
|
||||
elseif hp <= 0 then
|
||||
return false, "It won't have any effect."
|
||||
elseif mk == "status" or use == "status" then
|
||||
local s = mon.status
|
||||
if not s or s == 0 or s == "" then return false, "It won't have any effect." end
|
||||
return true
|
||||
else
|
||||
if hp >= maxHp then return false, "It won't have any effect." end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function BattleItems.ballMultiplier(itemId, foeBattler, st, session)
|
||||
return Catching.ballMultiplier(itemId, foeBattler, st, session)
|
||||
end
|
||||
|
||||
@@ -125,6 +125,51 @@ function Rules.weather.effective(st, adapter)
|
||||
return kind
|
||||
end
|
||||
|
||||
local function fallback_rng(lo, hi)
|
||||
local okR, Rng = pcall(require, "src.core.game3.rng")
|
||||
if okR and Rng and Rng.compat then
|
||||
return Rng.compat(lo, hi)
|
||||
end
|
||||
return math.random(lo, hi)
|
||||
end
|
||||
|
||||
-- Partial trap (Gen3)
|
||||
Rules.partialTrap = {}
|
||||
|
||||
function Rules.partialTrap.chipAmount(maxHp)
|
||||
return math.max(1, math.floor((maxHp or 16) / Capabilities.partialTrapChipDenom))
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_script_commands.c:2490
|
||||
function Rules.partialTrap.rollTurns(rng)
|
||||
rng = rng or fallback_rng
|
||||
local ok, n = pcall(rng, 0, 3)
|
||||
if not (ok and type(n) == "number") then n = fallback_rng(0, 3) end
|
||||
return (math.floor(n) % 4) + 3
|
||||
end
|
||||
|
||||
function Rules.partialTrap.active()
|
||||
return Capabilities.gen3PartialTrap
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_message.c:1263
|
||||
function Rules.partialTrap.message(moveId)
|
||||
local name = partial_trap_name(moveId)
|
||||
return string.format("{DEFENDER} was trapped by %s!", name)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_message.c:1268
|
||||
function Rules.partialTrap.squeezeMessage(moveId)
|
||||
local name = partial_trap_name(moveId)
|
||||
return string.format("{DEFENDER} is hurt by %s!", name)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_message.c:1274
|
||||
function Rules.partialTrap.freedMessage(moveId)
|
||||
local name = partial_trap_name(moveId)
|
||||
return string.format("{DEFENDER} was freed from %s!", name)
|
||||
end
|
||||
|
||||
function Rules.weather.typeModifier(weather, moveTypeName)
|
||||
local kind = Rules.weather.kind(weather)
|
||||
local mods = {
|
||||
@@ -140,7 +185,7 @@ function Rules.weather.chipAmount(maxHp)
|
||||
return math.max(1, math.floor((maxHp or 16) / Capabilities.weatherChipDenom))
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_script_commands.c:588
|
||||
-- Critical hit (Gen3)
|
||||
Rules.crit = {}
|
||||
|
||||
Rules.crit.CHANCE = { [0] = 16, [1] = 8, [2] = 4, [3] = 3, [4] = 2 }
|
||||
@@ -180,11 +225,11 @@ end
|
||||
local function rollZeroTo(rng, den)
|
||||
if den <= 1 then return 0 end
|
||||
if type(rng) ~= "function" then
|
||||
return math.random(0, den - 1)
|
||||
return fallback_rng(0, den - 1)
|
||||
end
|
||||
local ok, a = pcall(rng, 0, den - 1)
|
||||
if ok and type(a) == "number" then return a % den end
|
||||
return math.random(0, den - 1)
|
||||
return fallback_rng(0, den - 1)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_script_commands.c:1199
|
||||
|
||||
@@ -324,6 +324,14 @@ function Ui.waitingForCommand()
|
||||
return Ui._mode == "menu" or Ui._mode == "moves" or Ui._mode == "bag" or Ui._mode == "target"
|
||||
end
|
||||
|
||||
local function restore_action_menu()
|
||||
Ui._mode = "menu"
|
||||
Ui._linger = false
|
||||
Ui._timed = nil
|
||||
Ui._showing = false
|
||||
if Message and Message.open then Message.open = false end
|
||||
end
|
||||
|
||||
local function open_battle_bag()
|
||||
local BagMenu = require("src.ui.game3.bag_menu")
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
@@ -332,7 +340,7 @@ local function open_battle_bag()
|
||||
local bag = session and session.bag
|
||||
if not bag then
|
||||
Ui.push("The BAG is empty.")
|
||||
Ui._mode = "menu"
|
||||
restore_action_menu()
|
||||
return
|
||||
end
|
||||
Ui._mode = "bag"
|
||||
@@ -341,7 +349,7 @@ local function open_battle_bag()
|
||||
battle = true,
|
||||
onBattleUse = function(itemId, partySlot)
|
||||
if itemId == nil then
|
||||
Ui._mode = "menu"
|
||||
restore_action_menu()
|
||||
return
|
||||
end
|
||||
Ui._pendingCommand = {
|
||||
@@ -354,7 +362,9 @@ local function open_battle_bag()
|
||||
Ui._mode = "none"
|
||||
end,
|
||||
onClose = function()
|
||||
-- close already notifies onBattleUse(nil) when cancelled
|
||||
if Ui._mode == "bag" then
|
||||
restore_action_menu()
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
@@ -422,7 +432,7 @@ local function open_battle_party_double()
|
||||
Ui.openPartyMenu(Ui._st, id, {
|
||||
onSelect = function(slot)
|
||||
if slot == nil then
|
||||
Ui._mode = "menu"
|
||||
restore_action_menu()
|
||||
return
|
||||
end
|
||||
Ui._pendingCommand = {
|
||||
@@ -434,7 +444,9 @@ local function open_battle_party_double()
|
||||
Ui._mode = "none"
|
||||
end,
|
||||
onClose = function()
|
||||
Ui._mode = "menu"
|
||||
if Ui._mode == "party" then
|
||||
restore_action_menu()
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
@@ -461,7 +473,7 @@ local function open_battle_party()
|
||||
validate = function(slot) return Commands.switchError(Ui._st, slot) end,
|
||||
onSelect = function(slot)
|
||||
if slot == nil or slot == activeSlot then
|
||||
Ui._mode = "menu"
|
||||
restore_action_menu()
|
||||
return
|
||||
end
|
||||
Ui._pendingCommand = {
|
||||
@@ -472,7 +484,9 @@ local function open_battle_party()
|
||||
Ui._mode = "none"
|
||||
end,
|
||||
onClose = function()
|
||||
Ui._mode = "menu"
|
||||
if Ui._mode == "party" then
|
||||
restore_action_menu()
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
@@ -183,9 +183,53 @@ local function table_for(mapId)
|
||||
if not mapId then return nil end
|
||||
local t = Encounters._tables[mapId]
|
||||
if t then return t end
|
||||
return Encounters._tables[tostring(mapId)]
|
||||
local s = tostring(mapId)
|
||||
t = Encounters._tables[s]
|
||||
if t then return t end
|
||||
|
||||
-- MapCatalog resolution (e.g. pret name or group:num)
|
||||
local ok, MapCatalog = pcall(require, "src.import.gba.map_catalog")
|
||||
if ok and MapCatalog then
|
||||
local res = MapCatalog.resolve(s)
|
||||
if res and Encounters._tables[res] then
|
||||
return Encounters._tables[res]
|
||||
end
|
||||
local slot = MapCatalog.slotKeyFor(s)
|
||||
if slot then
|
||||
local colonSlot = slot:gsub("_", ":")
|
||||
if Encounters._tables[colonSlot] then return Encounters._tables[colonSlot] end
|
||||
if Encounters._tables[slot] then return Encounters._tables[slot] end
|
||||
end
|
||||
end
|
||||
|
||||
-- Prefix stripping / addition
|
||||
if s:sub(1, 3) == "FR_" then
|
||||
t = Encounters._tables[s:sub(4)]
|
||||
if t then return t end
|
||||
else
|
||||
t = Encounters._tables["FR_" .. s]
|
||||
if t then return t end
|
||||
end
|
||||
|
||||
-- Route underscore normalization (ROUTE_22 <-> ROUTE22)
|
||||
local routeNum = s:match("ROUTE_?(%d+)")
|
||||
if routeNum then
|
||||
t = Encounters._tables["FR_ROUTE_" .. routeNum]
|
||||
or Encounters._tables["FR_ROUTE" .. routeNum]
|
||||
or Encounters._tables["ROUTE_" .. routeNum]
|
||||
or Encounters._tables["ROUTE" .. routeNum]
|
||||
if t then return t end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function Encounters.tableFor(mapId)
|
||||
Encounters.ensureLoaded()
|
||||
return table_for(mapId)
|
||||
end
|
||||
Encounters.table_for = Encounters.tableFor
|
||||
|
||||
local function roll_area(mapId, areaKey, weights, enterFromOther, fallbackRate)
|
||||
local t = table_for(mapId)
|
||||
local area = normalize_area(t and t[areaKey], fallbackRate)
|
||||
|
||||
@@ -146,7 +146,10 @@ function Gfx.drawUi()
|
||||
end
|
||||
|
||||
local okF, Fade = pcall(require, "src.ui.game3.fade")
|
||||
if okF and Fade.draw and not (okN and Naming.isOpen and Naming.isOpen()) then
|
||||
local isNamingOpen = okN and Naming.isOpen and Naming.isOpen()
|
||||
local okR, RegionMap = pcall(require, "src.ui.game3.region_map")
|
||||
local isRegionMapOpen = okR and RegionMap.isOpen and RegionMap.isOpen()
|
||||
if okF and Fade.draw and not isNamingOpen and not isRegionMapOpen then
|
||||
Fade.draw()
|
||||
end
|
||||
|
||||
|
||||
+123
-5
@@ -268,6 +268,109 @@ local function applyPerm(eo, mapId)
|
||||
end
|
||||
local face = ({ [7] = "up", [8] = "down", [9] = "left", [10] = "right" })[row.movementType]
|
||||
if face then eo.facing = face end
|
||||
if eo.def then eo.def.movementType = row.movementType end
|
||||
end
|
||||
if row.facing ~= nil then
|
||||
eo.facing = row.facing
|
||||
if eo.def then eo.def.facing = row.facing end
|
||||
end
|
||||
end
|
||||
|
||||
local function resolveContextualMapObjects(mapId)
|
||||
if mapId == "FR_OAKS_LAB" or mapId == "PalletTown_ProfessorOaksLab" then
|
||||
local Sp = Space()
|
||||
local store = Sp and Sp.store
|
||||
local Flags = package.loaded["src.core.game3.scripting.flags"]
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
||||
|
||||
local oakScene = 0
|
||||
if Flags and Flags.getVar and store then
|
||||
oakScene = Flags.getVar(store, nil, 0x4055)
|
||||
elseif session and session.vars then
|
||||
oakScene = tonumber(session.vars[0x4055]) or 0
|
||||
end
|
||||
|
||||
local starter = 0
|
||||
if Flags and Flags.getVar and store then
|
||||
starter = Flags.getVar(store, nil, 0x4031)
|
||||
elseif session and session.vars then
|
||||
starter = tonumber(session.vars[0x4031]) or 0
|
||||
end
|
||||
|
||||
if starter == 0 and session and session.party and session.party[1] then
|
||||
local sp = session.party[1].species
|
||||
if sp == 4 then starter = 2 -- Charmander -> Rival has Squirtle
|
||||
elseif sp == 7 then starter = 1 -- Squirtle -> Rival has Bulbasaur
|
||||
elseif sp == 1 then starter = 0 -- Bulbasaur -> Rival has Charmander
|
||||
end
|
||||
end
|
||||
|
||||
if oakScene == 1 or oakScene == 2 then
|
||||
rememberPerm(mapId, 4, { x = 6, y = 3, movementType = 8, facing = "down" })
|
||||
rememberPerm(mapId, 8, { x = 5, y = 4, movementType = 7, facing = "up" })
|
||||
elseif oakScene == 3 then
|
||||
rememberPerm(mapId, 4, { x = 6, y = 3, movementType = 8, facing = "down" })
|
||||
local rx, ry = 10, 5
|
||||
if starter == 1 then
|
||||
rx, ry = 8, 5
|
||||
elseif starter == 2 then
|
||||
rx, ry = 9, 5
|
||||
end
|
||||
rememberPerm(mapId, 8, { x = rx, y = ry, movementType = 7, facing = "up" })
|
||||
elseif (oakScene >= 4 and oakScene <= 6) or oakScene >= 8 then
|
||||
rememberPerm(mapId, 4, { x = 6, y = 3, movementType = 8, facing = "down" })
|
||||
end
|
||||
end
|
||||
if mapId == "FR_PALLET_TOWN" or mapId == "PalletTown" then
|
||||
local Sp = Space()
|
||||
local store = Sp and Sp.store
|
||||
local Flags = package.loaded["src.core.game3.scripting.flags"]
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
||||
|
||||
local signLadyScene = 0
|
||||
if Flags and Flags.getVar and store then
|
||||
signLadyScene = Flags.getVar(store, nil, 0x4070)
|
||||
elseif session and session.vars then
|
||||
signLadyScene = tonumber(session.vars[0x4070]) or 0
|
||||
end
|
||||
|
||||
local hasStarter = false
|
||||
if Flags and Flags.getFlag and store then
|
||||
hasStarter = Flags.getFlag(store, nil, 0x291) or Flags.getFlag(store, nil, 0x828)
|
||||
end
|
||||
if not hasStarter and session and session.flags then
|
||||
hasStarter = session.flags[0x291] == true or session.flags["0x291"] == true
|
||||
or session.flags[657] == true or session.flags["657"] == true
|
||||
or session.flags[0x828] == true or session.flags["0x828"] == true
|
||||
or session.flags[2088] == true or session.flags["2088"] == true
|
||||
end
|
||||
if not hasStarter and session and session.party and #session.party > 0 then
|
||||
hasStarter = true
|
||||
end
|
||||
|
||||
if signLadyScene == 0 then
|
||||
if hasStarter then
|
||||
rememberPerm(mapId, 1, { x = 12, y = 2, movementType = 8, facing = "down" })
|
||||
if Flags and store then
|
||||
Flags.setVar(store, nil, 0x4002, 1) -- VAR_TEMP_2 = 1 (SIGN_LADY_READY)
|
||||
Flags.setFlag(store, nil, 0x291, true)
|
||||
Flags.setFlag(store, nil, 0x83E, false) -- FLAG_OPENED_START_MENU = false until scene completes
|
||||
end
|
||||
if session then
|
||||
if session.vars then session.vars[0x4002] = 1 end
|
||||
if session.flags then
|
||||
session.flags[0x291] = true
|
||||
session.flags[657] = true
|
||||
session.flags[0x83E] = nil
|
||||
session.flags[2110] = nil
|
||||
end
|
||||
end
|
||||
else
|
||||
rememberPerm(mapId, 1, { x = 5, y = 15, movementType = 7, facing = "up" })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -305,6 +408,7 @@ function Objects.loadMap(game, mapId, mapDef)
|
||||
end
|
||||
end
|
||||
end
|
||||
resolveContextualMapObjects(mapId)
|
||||
local announce = ModRuntime.wants("world.npc_spawned")
|
||||
for _, def in ipairs(Objects._defs) do
|
||||
local eo = newEventObject(def)
|
||||
@@ -682,11 +786,13 @@ local function idleTick(eo, game, ctx)
|
||||
return
|
||||
end
|
||||
|
||||
local Rng = require("src.core.game3.rng")
|
||||
local dirs = dirsForRange(eo.range)
|
||||
if mv == "LOOK" or mv == "LOOK_AROUND" then
|
||||
local oldFacing = eo.facing
|
||||
eo.facing = dirs[math.random(#dirs)]
|
||||
eo.idleTimer = 48 + math.random(48)
|
||||
local pickIdx = Rng.compat(1, #dirs)
|
||||
eo.facing = dirs[pickIdx] or dirs[1]
|
||||
eo.idleTimer = 48 + Rng.compat(0, 47)
|
||||
if not ctx and eo.facing ~= oldFacing and eo.sight and eo.sight > 0 then
|
||||
local okTs, TrainerSight = pcall(require, "src.core.game3.trainer_sight")
|
||||
if okTs and TrainerSight and TrainerSight.check then
|
||||
@@ -697,13 +803,14 @@ local function idleTick(eo, game, ctx)
|
||||
end
|
||||
|
||||
if mv == "WALK" then
|
||||
local dir = dirs[math.random(#dirs)]
|
||||
local pickIdx = Rng.compat(1, #dirs)
|
||||
local dir = dirs[pickIdx] or dirs[1]
|
||||
local d = DELTA[dir]
|
||||
local tx, ty = eo.cellX + d[1], eo.cellY + d[2]
|
||||
local rx = (eo.radius and eo.radius.x) or 1
|
||||
local ry = (eo.radius and eo.radius.y) or 1
|
||||
if math.abs(tx - eo.homeX) > rx or math.abs(ty - eo.homeY) > ry then
|
||||
eo.idleTimer = 30 + math.random(30)
|
||||
eo.idleTimer = 30 + Rng.compat(0, 29)
|
||||
return
|
||||
end
|
||||
local ok
|
||||
@@ -723,7 +830,7 @@ local function idleTick(eo, game, ctx)
|
||||
else
|
||||
eo.facing = dir -- turn toward blocked anyway
|
||||
end
|
||||
eo.idleTimer = 40 + math.random(50)
|
||||
eo.idleTimer = 40 + Rng.compat(0, 49)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -936,6 +1043,17 @@ function Objects.setObjectXY(localId, x, y)
|
||||
if eo.def then eo.def.x, eo.def.y = eo.cellX, eo.cellY end
|
||||
end
|
||||
|
||||
function Objects.copyObjectXYToPerm(localId)
|
||||
local lid = tonumber(localId) or 0
|
||||
local eo = Objects._byId[lid]
|
||||
if not eo then return end
|
||||
local Sp = Space()
|
||||
local mapKey = (Sp and Sp.mapId) or Objects._mapId
|
||||
rememberPerm(mapKey, lid, { x = eo.cellX, y = eo.cellY })
|
||||
eo.homeX, eo.homeY = eo.cellX, eo.cellY
|
||||
if eo.def then eo.def.x, eo.def.y = eo.cellX, eo.cellY end
|
||||
end
|
||||
|
||||
function Objects.setMovementType(localId, mt)
|
||||
local lid = tonumber(localId) or 0
|
||||
local eo = Objects._byId[lid]
|
||||
|
||||
+12
-14
@@ -161,20 +161,18 @@ function Party.giveMon(session, species, level, nickname)
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
if not Pokemon._names then pcall(Pokemon.install, nil) end
|
||||
|
||||
local personality
|
||||
if love and love.math and love.math.random then
|
||||
personality = love.math.random(0, 0xFFFFFFFF)
|
||||
else
|
||||
personality = math.floor(math.random() * 0x100000000) % 0x100000000
|
||||
end
|
||||
local ivs = {}
|
||||
for _, k in ipairs({ "hp", "atk", "def", "spe", "spa", "spd" }) do
|
||||
if love and love.math and love.math.random then
|
||||
ivs[k] = love.math.random(0, 31)
|
||||
else
|
||||
ivs[k] = math.random(0, 31)
|
||||
end
|
||||
end
|
||||
local Rng = require("src.core.game3.rng")
|
||||
local personality = Rng.Random32()
|
||||
local iv1 = Rng.Random()
|
||||
local iv2 = Rng.Random()
|
||||
local ivs = {
|
||||
hp = iv1 % 32,
|
||||
atk = math.floor(iv1 / 32) % 32,
|
||||
def = math.floor(iv1 / 1024) % 32,
|
||||
spe = iv2 % 32,
|
||||
spa = math.floor(iv2 / 32) % 32,
|
||||
spd = math.floor(iv2 / 1024) % 32,
|
||||
}
|
||||
|
||||
local meta = Pokemon.speciesMeta and Pokemon.speciesMeta(species)
|
||||
local friendship = (meta and meta.friendship) or 70
|
||||
|
||||
+24
-13
@@ -507,11 +507,12 @@ local function finishStep(game)
|
||||
Field.tryCoordEvents(game, Player.cellX, Player.cellY)
|
||||
end
|
||||
|
||||
-- Wild encounters on grass when step completes (pret StandardWildEncounter).
|
||||
-- Wild encounters on grass/water when step completes (pret StandardWildEncounter).
|
||||
local onGrass = Collision.isGrass and Collision.isGrass(Player.cellX, Player.cellY)
|
||||
local onWater = Player.surfing and (Collision.isWater and Collision.isWater(Player.cellX, Player.cellY))
|
||||
local okE, Encounters = pcall(require, "src.core.game3.encounters")
|
||||
local triggeredBattle = false
|
||||
if onGrass and okE and Encounters and Encounters.onStep then
|
||||
if (onGrass or onWater) and okE and Encounters and Encounters.onStep then
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
local busy = (Battle and Battle.isActive and Battle.isActive()) or Field.locked
|
||||
local Space = package.loaded["src.core.game3.scripting.space"]
|
||||
@@ -525,23 +526,33 @@ local function finishStep(game)
|
||||
mapId = Map and Map.current
|
||||
end
|
||||
local enterFromOther = not (Encounters._prevGrass)
|
||||
local enc = Encounters.onStep(mapId, "land", { enterFromOther = enterFromOther })
|
||||
local terrain = onWater and "water" or "land"
|
||||
local enc = Encounters.onStep(mapId, terrain, { enterFromOther = enterFromOther })
|
||||
if enc then
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local BattleBridge = require("src.core.game3.battle_bridge")
|
||||
local mod = Runtime and Runtime._mod
|
||||
local g = game or (Runtime and Runtime._game)
|
||||
local okB, errB = BattleBridge.startWild(mod, g, enc, {})
|
||||
if not okB then
|
||||
print("[game3/encounters] startWild failed: " .. tostring(errB))
|
||||
else
|
||||
triggeredBattle = true
|
||||
-- Repel gating: pokefirered/src/wild_encounter.c:215
|
||||
local repelSteps = tonumber(session and (session.repelSteps or (session.vars and session.vars[0x4020]))) or 0
|
||||
local leadLevel = 1
|
||||
if session and session.party and session.party[1] then
|
||||
leadLevel = tonumber(session.party[1].level) or 1
|
||||
end
|
||||
local repelled = (repelSteps > 0) and (tonumber(enc.level) or 1) <= leadLevel
|
||||
if not repelled then
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local BattleBridge = require("src.core.game3.battle_bridge")
|
||||
local mod = Runtime and Runtime._mod
|
||||
local g = game or (Runtime and Runtime._game)
|
||||
local okB, errB = BattleBridge.startWild(mod, g, enc, {})
|
||||
if not okB then
|
||||
print("[game3/encounters] startWild failed: " .. tostring(errB))
|
||||
else
|
||||
triggeredBattle = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if okE and Encounters and Encounters.noteGrass then
|
||||
Encounters.noteGrass(onGrass)
|
||||
Encounters.noteGrass(onGrass or onWater)
|
||||
end
|
||||
if onGrass then
|
||||
local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects")
|
||||
|
||||
@@ -68,26 +68,90 @@ function PokedexData.init()
|
||||
return true
|
||||
end
|
||||
|
||||
local AREA_TO_MAP = {
|
||||
DEX_AREA_ONE_ISLAND = "one_island",
|
||||
DEX_AREA_KINDLE_ROAD = "one_island",
|
||||
DEX_AREA_TREASURE_BEACH = "one_island",
|
||||
DEX_AREA_MT_EMBER = "one_island",
|
||||
|
||||
DEX_AREA_TWO_ISLAND = "two_island",
|
||||
DEX_AREA_CAPE_BRINK = "two_island",
|
||||
|
||||
DEX_AREA_THREE_ISLAND = "three_island",
|
||||
DEX_AREA_BOND_BRIDGE = "three_island",
|
||||
DEX_AREA_THREE_ISLE_PATH = "three_island",
|
||||
DEX_AREA_BERRY_FOREST = "three_island",
|
||||
|
||||
DEX_AREA_FOUR_ISLAND = "four_island",
|
||||
DEX_AREA_ICEFALL_CAVE = "four_island",
|
||||
|
||||
DEX_AREA_FIVE_ISLAND = "five_island",
|
||||
DEX_AREA_RESORT_GORGEOUS = "five_island",
|
||||
DEX_AREA_WATER_LABYRINTH = "five_island",
|
||||
DEX_AREA_FIVE_ISLE_MEADOW = "five_island",
|
||||
DEX_AREA_MEMORIAL_PILLAR = "five_island",
|
||||
DEX_AREA_LOST_CAVE = "five_island",
|
||||
|
||||
DEX_AREA_SIX_ISLAND = "six_island",
|
||||
DEX_AREA_OUTCAST_ISLAND = "six_island",
|
||||
DEX_AREA_GREEN_PATH = "six_island",
|
||||
DEX_AREA_WATER_PATH = "six_island",
|
||||
DEX_AREA_RUIN_VALLEY = "six_island",
|
||||
DEX_AREA_DOTTED_HOLE = "six_island",
|
||||
DEX_AREA_PATTERN_BUSH = "six_island",
|
||||
DEX_AREA_ALTERING_CAVE = "six_island",
|
||||
|
||||
DEX_AREA_SEVEN_ISLAND = "seven_island",
|
||||
DEX_AREA_TRAINER_TOWER = "seven_island",
|
||||
DEX_AREA_CANYON_ENTRANCE = "seven_island",
|
||||
DEX_AREA_SEVAULT_CANYON = "seven_island",
|
||||
DEX_AREA_TANOBY_RUINS = "seven_island",
|
||||
DEX_AREA_TANOBY_CHAMBER = "seven_island",
|
||||
}
|
||||
|
||||
function PokedexData.getAreaMapKey(dexAreaKey)
|
||||
if not dexAreaKey then return "kanto" end
|
||||
return AREA_TO_MAP[dexAreaKey] or "kanto"
|
||||
end
|
||||
|
||||
--- Map wild encounter tables to species DEX_AREA locations
|
||||
function PokedexData._buildSpeciesWildAreas()
|
||||
PokedexData._speciesWildAreas = {}
|
||||
local encounters = load_lua("data/generated/gba/encounters.lua") or load_lua("data/generated/encounters.lua")
|
||||
local mapGroups = load_lua("src/import/gba/map_groups_firered.lua")
|
||||
local mapsecToArea = PokedexData._areaData and PokedexData._areaData.mapsecToArea or {}
|
||||
local markers = PokedexData._areaData and PokedexData._areaData.markers or {}
|
||||
local MapSectionsExtract = package.loaded["src.import.gba.map_sections_extract"]
|
||||
if not MapSectionsExtract then
|
||||
local okMs, ms = pcall(require, "src.import.gba.map_sections_extract")
|
||||
if okMs then MapSectionsExtract = ms end
|
||||
end
|
||||
|
||||
if encounters and mapGroups and mapGroups.groups then
|
||||
for key, header in pairs(encounters) do
|
||||
local gIdx, mIdx = header.mapGroup, header.mapNum
|
||||
if not gIdx or not mIdx then
|
||||
gIdx, mIdx = key:match("^(%d+):(%d+)$")
|
||||
gIdx, mIdx = tonumber(gIdx), tonumber(mIdx)
|
||||
local gStr, mStr = tostring(key):match("^(%d+):(%d+)$")
|
||||
if gStr and mStr then
|
||||
gIdx, mIdx = tonumber(gStr), tonumber(mStr)
|
||||
end
|
||||
end
|
||||
if gIdx and mIdx then
|
||||
local gTable = mapGroups.groups[gIdx + 1] or mapGroups.groups[gIdx]
|
||||
local mapInfo = gTable and (gTable[mIdx + 1] or gTable[mIdx])
|
||||
local mapSec = mapInfo and (mapInfo.mapSec or mapInfo.region_map_section or mapInfo.id)
|
||||
local dexArea = mapSec and mapsecToArea[mapSec]
|
||||
if dexArea then
|
||||
local gTable = mapGroups.groups[gIdx] or mapGroups.groups[gIdx + 1]
|
||||
local pretName = gTable and gTable.maps and (gTable.maps[mIdx + 1] or gTable.maps[mIdx])
|
||||
local secIdStr = nil
|
||||
if MapSectionsExtract and MapSectionsExtract.getInfo then
|
||||
local info = MapSectionsExtract.getInfo(nil, pretName)
|
||||
secIdStr = info and info.id
|
||||
end
|
||||
|
||||
local dexArea = secIdStr and mapsecToArea[secIdStr]
|
||||
if not dexArea and pretName then
|
||||
local norm = "DEX_AREA_" .. tostring(pretName):gsub("^FR_", ""):gsub("^SEVII_", ""):gsub("([a-z])([A-Z])", "%1_%2"):upper()
|
||||
if markers[norm] then dexArea = norm end
|
||||
end
|
||||
|
||||
if dexArea and (markers[dexArea] or mapsecToArea[secIdStr]) then
|
||||
local function addSpecies(sp)
|
||||
if not sp or sp == 0 then return end
|
||||
PokedexData._speciesWildAreas[sp] = PokedexData._speciesWildAreas[sp] or {}
|
||||
@@ -263,8 +327,9 @@ end
|
||||
function PokedexData.getWildAreasForSpecies(speciesId)
|
||||
PokedexData.init()
|
||||
local sp = tonumber(speciesId) or 1
|
||||
local dynamic = PokedexData._speciesWildAreas and PokedexData._speciesWildAreas[sp]
|
||||
if dynamic and #dynamic > 0 then return dynamic end
|
||||
return (PokedexData._areaData and PokedexData._areaData.speciesAreas and PokedexData._areaData.speciesAreas[sp])
|
||||
or (PokedexData._speciesWildAreas and PokedexData._speciesWildAreas[sp])
|
||||
or {}
|
||||
end
|
||||
|
||||
|
||||
@@ -765,6 +765,10 @@ function Adapters.host(mod, game, world)
|
||||
G3.setObjectXY(lid, row[2], row[3])
|
||||
elseif op == "setobjectmovementtype" then
|
||||
G3.setMovementType(lid, row[2])
|
||||
elseif op == "copyobjectxytoperm" then
|
||||
if G3.copyObjectXYToPerm then
|
||||
G3.copyObjectXYToPerm(lid)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
@@ -780,6 +784,10 @@ function Adapters.host(mod, game, world)
|
||||
npc.def.x, npc.def.y = x, y
|
||||
end
|
||||
end
|
||||
elseif op == "copyobjectxytoperm" then
|
||||
if npc.cellX and npc.cellY and npc.def then
|
||||
npc.def.x, npc.def.y = npc.cellX, npc.cellY
|
||||
end
|
||||
elseif op == "setobjectmovementtype" then
|
||||
-- Cosmetic on host; facing types 7–10 are FACE_*.
|
||||
local mt = tonumber(row[2]) or 0
|
||||
@@ -1118,23 +1126,35 @@ function Adapters.host(mod, game, world)
|
||||
local listId = 0
|
||||
local n = 3
|
||||
if row then
|
||||
-- pret: multichoice x, y, listId [, default]
|
||||
-- pret:
|
||||
-- multichoice left, top, listId, ignoreBPress
|
||||
-- multichoicedefault left, top, listId, default, ignoreBPress
|
||||
-- multichoicegrid left, top, listId, numColumns, ignoreBPress
|
||||
listId = tonumber(row.listId or row[3] or row[1]) or 0
|
||||
n = tonumber(row.count or row[4]) or n
|
||||
end
|
||||
local opts, layout = Multi.resolve(listId, n)
|
||||
layout = layout or {}
|
||||
local def = 0
|
||||
if row and row.op == "multichoicedefault" then
|
||||
def = tonumber(row.default or row[4] or row[5]) or 0
|
||||
end
|
||||
if layout and row then
|
||||
if row then
|
||||
-- pokefirered/src/script_menu.c:1195
|
||||
local x, y = tonumber(row.x or row[1]), tonumber(row.y or row[2])
|
||||
local x, y = tonumber(row.x or row.left or row[1]), tonumber(row.y or row.top or row[2])
|
||||
if x then layout.left = x + 1 end
|
||||
if y then layout.top = y + 1 end
|
||||
if row.op ~= "multichoicegrid" then
|
||||
if row.op == "multichoicegrid" then
|
||||
layout.cols = tonumber(row.cols or row.numColumns or row[4]) or 1
|
||||
layout.ignoreBPress = row.ignoreBPress or (row[5] and tonumber(row[5]) ~= 0) or false
|
||||
else
|
||||
-- pokefirered/src/script_menu.c:737
|
||||
layout.maxRight = 29
|
||||
if row.op == "multichoicedefault" then
|
||||
layout.ignoreBPress = row.ignoreBPress or (row[5] and tonumber(row[5]) ~= 0) or false
|
||||
else
|
||||
layout.ignoreBPress = row.ignoreBPress or (row[4] and tonumber(row[4]) ~= 0) or false
|
||||
end
|
||||
end
|
||||
end
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
@@ -1150,7 +1170,7 @@ function Adapters.host(mod, game, world)
|
||||
Choice.multi(opts, def, function(sel)
|
||||
if cb then cb(sel) end
|
||||
tick_vm()
|
||||
end)
|
||||
end, layout)
|
||||
Choice.autoPick(def)
|
||||
end,
|
||||
setMetatile = function(x, y, metatile, impassable)
|
||||
@@ -1185,10 +1205,17 @@ function Adapters.host(mod, game, world)
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local session = (Runtime and Runtime.getSession and Runtime.getSession()) or (resolveGame() and resolveGame().session)
|
||||
local RegionMap = require("src.ui.game3.region_map")
|
||||
local Fade = require("src.ui.game3.fade")
|
||||
local Message = require("src.ui.game3.message")
|
||||
if Message.isOpen and Message.isOpen() and Message.close then
|
||||
Message.close()
|
||||
end
|
||||
Fade.clear()
|
||||
a.log("[game3] showTownMap via RegionMap")
|
||||
RegionMap.show({
|
||||
session = session,
|
||||
onClose = function()
|
||||
Fade.clear()
|
||||
if done then done() end
|
||||
tick_vm()
|
||||
end,
|
||||
|
||||
@@ -9,10 +9,13 @@ local CollisionStd = {}
|
||||
CollisionStd.COLL_PC = 0x93
|
||||
CollisionStd.COLL_COUNTER = 0x90
|
||||
CollisionStd.COLL_BOOKSHELF = 0x91
|
||||
CollisionStd.COLL_TOWN_MAP = 0x95
|
||||
|
||||
-- Facing this collision runs the named game3 script (shared, not map-local).
|
||||
CollisionStd.SCRIPTS = {
|
||||
[0x93] = "EventScript_PC", -- MB_PC → COLL_PC
|
||||
[0x85] = "EventScript_WallTownMap", -- MB_TOWN_MAP
|
||||
[0x95] = "EventScript_WallTownMap", -- COLL_TOWN_MAP
|
||||
}
|
||||
|
||||
function CollisionStd.scriptFor(coll)
|
||||
|
||||
@@ -4,24 +4,7 @@
|
||||
|
||||
local Multichoice = {}
|
||||
|
||||
-- Common FRLG list ids used on Sevii / centers (approximate; extract overrides).
|
||||
Multichoice.LISTS = {
|
||||
-- Generic YES/NO style already uses Choice.yesNo; keep lists for multichoice ops.
|
||||
[0] = { labels = { "YES", "NO" }, left = 22, top = 8 },
|
||||
[1] = { labels = { "SEE YA!", "INFO" }, left = 20, top = 6 },
|
||||
[2] = { labels = { "ONE ISLAND", "TWO ISLAND", "THREE ISLAND", "EXIT" }, left = 14, top = 4 },
|
||||
[3] = { labels = { "FOUR ISLAND", "FIVE ISLAND", "SIX ISLAND", "SEVEN ISLAND", "EXIT" }, left = 12, top = 3 },
|
||||
[4] = { labels = { "TRADE CENTER", "COLOSSEUM", "EVOLUTION", "EXIT" }, left = 14, top = 5 },
|
||||
[5] = { labels = { "JOIN ROOM", "INFO", "EXIT" }, left = 18, top = 6 },
|
||||
[6] = { labels = { "POKéMON JUMP", "DODRIO BERRY", "EXIT" }, left = 16, top = 6 },
|
||||
[7] = { labels = { "YES", "NO" }, left = 22, top = 8 },
|
||||
[8] = { labels = { "NORMAL", "DIRECT", "EXIT" }, left = 18, top = 6 },
|
||||
[9] = { labels = { "MAKE A GROUP", "ACCEPT INVITE", "EXIT" }, left = 14, top = 6 },
|
||||
-- Ferry / island travel (Sevii)
|
||||
[10] = { labels = { "VERMILION", "ONE ISLAND", "EXIT" }, left = 16, top = 6 },
|
||||
[11] = { labels = { "ONE ISLAND", "TWO ISLAND", "THREE ISLAND", "EXIT" }, left = 14, top = 4 },
|
||||
[12] = { labels = { "GO ON", "INFO", "EXIT" }, left = 18, top = 6 },
|
||||
}
|
||||
Multichoice.LISTS = {}
|
||||
|
||||
--- Override/merge from extract cache if present.
|
||||
function Multichoice.loadExtract(tbl)
|
||||
@@ -43,8 +26,8 @@ function Multichoice.tryLoadCache()
|
||||
return true
|
||||
end
|
||||
-- CacheFS fallback for offline / test loads.
|
||||
local Extract = require("src.import.gba.extract_island1")
|
||||
local root = (Extract.CACHE_ROOT or "data/generated/gba") .. "/scripts/multichoice.lua"
|
||||
local okE, Extract = pcall(require, "src.import.gba.extract_island1")
|
||||
local root = ((okE and Extract and Extract.CACHE_ROOT) or "data/generated/gba") .. "/scripts/multichoice.lua"
|
||||
local chunk = loadfile(root)
|
||||
if chunk then
|
||||
local d = chunk()
|
||||
@@ -53,8 +36,13 @@ function Multichoice.tryLoadCache()
|
||||
return false
|
||||
end
|
||||
|
||||
-- Preload cache immediately
|
||||
Multichoice.tryLoadCache()
|
||||
|
||||
function Multichoice.resolve(listId, countHint)
|
||||
Multichoice.tryLoadCache()
|
||||
if not next(Multichoice.LISTS) then
|
||||
Multichoice.tryLoadCache()
|
||||
end
|
||||
local id = tonumber(listId) or 0
|
||||
local entry = Multichoice.LISTS[id]
|
||||
if entry and entry.labels and #entry.labels > 0 then
|
||||
|
||||
@@ -217,6 +217,18 @@ Natives.ALLOW = {
|
||||
if not (adapters and adapters.showTownMap) then return false end
|
||||
return yield_host(ctx, adapters, adapters.showTownMap)
|
||||
end,
|
||||
["special:" .. Std.SPECIAL.FieldShowRegionMap] = function(ctx, adapters)
|
||||
if not (adapters and adapters.showTownMap) then return false end
|
||||
return yield_host(ctx, adapters, adapters.showTownMap)
|
||||
end,
|
||||
["special:251"] = function(ctx, adapters)
|
||||
if not (adapters and adapters.showTownMap) then return false end
|
||||
return yield_host(ctx, adapters, adapters.showTownMap)
|
||||
end,
|
||||
["special:0xFB"] = function(ctx, adapters)
|
||||
if not (adapters and adapters.showTownMap) then return false end
|
||||
return yield_host(ctx, adapters, adapters.showTownMap)
|
||||
end,
|
||||
-- Shared intro/field primitives (fade / naming / cry)
|
||||
["special:" .. Std.SPECIAL.FadeScreen] = function(ctx, adapters)
|
||||
if not (adapters and adapters.fadeScreen) then return false end
|
||||
@@ -232,7 +244,7 @@ Natives.ALLOW = {
|
||||
end,
|
||||
-- pret EventScript_ChangePokemonNickname: fadescreen TO_BLACK → this → waitstate.
|
||||
-- Opens naming under the held black, fades in, writes nickname on confirm.
|
||||
["special:" .. Std.SPECIAL.ChangePokemonNickname] = function(ctx, adapters)
|
||||
["special:158"] = function(ctx, adapters)
|
||||
if not (adapters and adapters.openNaming) then return false end
|
||||
return yield_host(ctx, adapters, function(done)
|
||||
local slot = 0
|
||||
@@ -263,7 +275,10 @@ Natives.ALLOW = {
|
||||
end)
|
||||
end)
|
||||
end,
|
||||
["special:" .. Std.SPECIAL.BufferMonNickname] = function(ctx, adapters)
|
||||
["special:159"] = function(ctx, adapters)
|
||||
return Natives.ALLOW["special:158"](ctx, adapters)
|
||||
end,
|
||||
["special:124"] = function(ctx, adapters)
|
||||
-- pret BufferMonNickname → gStringVar1; host buffers for {STR_VAR_1}.
|
||||
local slot = 0
|
||||
if ctx and ctx.getVar then
|
||||
@@ -291,6 +306,9 @@ Natives.ALLOW = {
|
||||
end
|
||||
return false
|
||||
end,
|
||||
["special:125"] = function(ctx, adapters)
|
||||
return Natives.ALLOW["special:124"](ctx, adapters)
|
||||
end,
|
||||
["special:" .. Std.SPECIAL.PlayCry] = function(ctx, adapters)
|
||||
local Audio = require("src.core.game3.audio")
|
||||
local species = 0
|
||||
|
||||
@@ -20,10 +20,13 @@ Std.SPECIAL = {
|
||||
GetQuestLogState = 0x187,
|
||||
QuestLog_CutRecording = 0x188,
|
||||
ShowPokemonStorageSystemPC = 0x3C,
|
||||
BufferMonNickname = 0x7D, -- 125
|
||||
ChangePokemonNickname = 0x9F, -- 159
|
||||
BufferMonNickname = 0x7D, -- 125 (0x7C / 124 in some FRLG scripts)
|
||||
BufferMonNickname_FR = 0x7C, -- 124
|
||||
ChangePokemonNickname = 0x9E, -- 158 in FireRed (pokefirered)
|
||||
ChangePokemonNickname_Alt = 0x9F, -- 159 (Emerald/aliases)
|
||||
-- ShowRegionMap / Sevii town map (Tier A special → game3 region UI).
|
||||
ShowRegionMap = 0xAF,
|
||||
FieldShowRegionMap = 0xFB, -- 251 (pokefirered special FieldShowRegionMap)
|
||||
AnimatePcTurnOn = 0xD6,
|
||||
AnimatePcTurnOff = 0xD7,
|
||||
BedroomPC = 0xF9, -- pokefirered/data/specials.inc:260
|
||||
@@ -56,6 +59,8 @@ Std.SPECIAL = {
|
||||
}
|
||||
|
||||
Std.TEXT = {
|
||||
Text_TownMap = T([[
|
||||
It's a TOWN MAP.]]),
|
||||
Text_WelcomeWantToHealPkmn = T([[
|
||||
Welcome to our POKéMON CENTER!
|
||||
Would you like me to rest your
|
||||
@@ -93,6 +98,16 @@ The BAG is full…]]),
|
||||
|
||||
-- Cart EventScript_PC (simplified host path: open full storage UI).
|
||||
Std.SCRIPTS = {
|
||||
EventScript_WallTownMap = {
|
||||
{ op = "lockall" },
|
||||
{ op = "loadword", dest = 0, value = "Text_TownMap" },
|
||||
{ op = "callstd", std = Opcodes.STD.MSGBOX_DEFAULT },
|
||||
{ op = "fadescreen", [1] = 1 },
|
||||
{ op = "special", id = Std.SPECIAL.FieldShowRegionMap },
|
||||
{ op = "waitstate" },
|
||||
{ op = "releaseall" },
|
||||
{ op = "end" },
|
||||
},
|
||||
EventScript_PC = {
|
||||
{ op = "lockall" },
|
||||
{ op = "special", id = Std.SPECIAL.AnimatePcTurnOn },
|
||||
|
||||
@@ -117,6 +117,17 @@ local function build_tables(entries)
|
||||
local alias = Versions.frMapFor(e.mapGroup, e.mapNum)
|
||||
if alias then
|
||||
tables[alias] = packed
|
||||
if alias:sub(1, 3) == "FR_" then
|
||||
local noFr = alias:sub(4)
|
||||
tables[noFr] = packed
|
||||
local routeNum = noFr:match("^ROUTE_(%d+)$")
|
||||
if routeNum then
|
||||
tables["ROUTE" .. routeNum] = packed
|
||||
tables["FR_ROUTE" .. routeNum] = packed
|
||||
end
|
||||
elseif alias:sub(1, 6) == "SEVII_" then
|
||||
tables[alias:sub(7)] = packed
|
||||
end
|
||||
end
|
||||
end
|
||||
return tables
|
||||
|
||||
@@ -15,9 +15,11 @@ local function pret_to_engine(pret)
|
||||
-- Existing hand aliases first.
|
||||
local hand = Versions.PRET_TO_FR and Versions.PRET_TO_FR[pret]
|
||||
if hand then return hand end
|
||||
-- Route1 → Route_1, Route22 → Route_22
|
||||
local s = pret:gsub("Route(%d+)", "Route_%1")
|
||||
-- PalletTown → FR_PALLET_TOWN; ViridianCity_PokemonCenter_1F → FR_VIRIDIAN_CITY_POKEMON_CENTER_1F
|
||||
-- Only split lower→Upper (not digit→Upper) so "1F" stays "1F".
|
||||
local s = pret:gsub("(%l)(%u)", "%1_%2")
|
||||
s = s:gsub("(%l)(%u)", "%1_%2")
|
||||
s = s:gsub("-", "_"):upper()
|
||||
s = s:gsub("_+", "_")
|
||||
return "FR_" .. s
|
||||
|
||||
@@ -1,2 +1,68 @@
|
||||
-- Soft stub for multichoice extract tables until firered cache is published.
|
||||
return {}
|
||||
-- Auto-generated FRLG Multichoice Lists from ROM gMultichoiceLists. DO NOT EDIT DIRECTLY.
|
||||
return {
|
||||
[0] = { count = 2, labels = { "YES", "NO" } },
|
||||
[1] = { count = 5, labels = { "EEVEE", "FLAREON", "JOLTEON", "VAPOREON", "Quit looking." } },
|
||||
[2] = { count = 4, labels = { "NORMAL", "BLACK", "PINK", "SEPIA" } },
|
||||
[3] = { count = 2, labels = { "HALL OF FAME", "QUIT" } },
|
||||
[4] = { count = 2, labels = { "EGGS", "QUIT" } },
|
||||
[5] = { count = 2, labels = { "VICTORIES", "QUIT" } },
|
||||
[6] = { count = 3, labels = { "HALL OF FAME", "EGGS", "QUIT" } },
|
||||
[7] = { count = 3, labels = { "HALL OF FAME", "VICTORIES", "QUIT" } },
|
||||
[8] = { count = 3, labels = { "EGGS", "VICTORIES", "QUIT" } },
|
||||
[9] = { count = 4, labels = { "HALL OF FAME", "EGGS", "VICTORIES", "QUIT" } },
|
||||
[10] = { count = 1, labels = { "EXIT" } },
|
||||
[11] = { count = 1, labels = { "EXIT" } },
|
||||
[12] = { count = 1, labels = { "EXIT" } },
|
||||
[13] = { count = 2, labels = { "BICYCLE ¥1,000,000", "NO THANKS" } },
|
||||
[14] = { count = 6, labels = { "ABRA 180 COINS", "CLEFAIRY 500 COINS", "DRATINI 2,800 COINS", "SCYTHER 5,500 COINS", "PORYGON 9,999 COINS", "NO THANKS" } },
|
||||
[15] = { count = 6, labels = { "SLP", "PSN", "PAR", "BRN", "FRZ", "EXIT" } },
|
||||
[16] = { count = 3, labels = { "YES", "NO", "INFO" } },
|
||||
[17] = { count = 5, labels = { "SINGLE BATTLE", "DOUBLE BATTLE", "MULTI BATTLE", "INFO", "EXIT" } },
|
||||
[18] = { count = 3, labels = { "YES", "NO", "INFO" } },
|
||||
[19] = { count = 3, labels = { "Make a challenge.", "INFO", "EXIT" } },
|
||||
[20] = { count = 3, labels = { "ROOFTOP", "B1F", "EXIT" } },
|
||||
[21] = { count = 2, labels = { "HELIX FOSSIL", "EXIT" } },
|
||||
[22] = { count = 2, labels = { "DOME FOSSIL", "EXIT" } },
|
||||
[23] = { count = 2, labels = { "OLD AMBER", "EXIT" } },
|
||||
[24] = { count = 3, labels = { "HELIX FOSSIL", "OLD AMBER", "EXIT" } },
|
||||
[25] = { count = 3, labels = { "DOME FOSSIL", "OLD AMBER", "EXIT" } },
|
||||
[26] = { count = 4, labels = { "FRESH WATER ¥200", "SODA POP ¥300", "LEMONADE ¥350", "EXIT" } },
|
||||
[27] = { count = 3, labels = { "50 COINS ¥1,000", "500 COINS ¥10,000", "EXIT" } },
|
||||
[28] = { count = 2, labels = { "Excellent", "Not so bad" } },
|
||||
[29] = { count = 2, labels = { "Right", "Left" } },
|
||||
[30] = { count = 6, labels = { "TM13 4,000 COINS", "TM23 3,500 COINS", "TM24 4,000 COINS", "TM30 4,500 COINS", "TM35 4,000 COINS", "NO THANKS" } },
|
||||
[31] = { count = 6, labels = { "5F", "4F", "3F", "2F", "1F", "EXIT" } },
|
||||
[32] = { count = 2, labels = { "FRESH WATER", "EXIT" } },
|
||||
[33] = { count = 2, labels = { "SODA POP", "EXIT" } },
|
||||
[34] = { count = 3, labels = { "FRESH WATER", "SODA POP", "EXIT" } },
|
||||
[35] = { count = 2, labels = { "LEMONADE", "EXIT" } },
|
||||
[36] = { count = 3, labels = { "FRESH WATER", "LEMONADE", "EXIT" } },
|
||||
[37] = { count = 3, labels = { "SODA POP", "LEMONADE", "EXIT" } },
|
||||
[38] = { count = 4, labels = { "FRESH WATER", "SODA POP", "LEMONADE", "EXIT" } },
|
||||
[39] = { count = 3, labels = { "TRADE CENTER", "COLOSSEUM", "EXIT" } },
|
||||
[40] = { count = 3, labels = { "Game Link cable", "Wireless", "EXIT" } },
|
||||
[41] = { count = 6, labels = { "SMOKE BALL 800 COINS", "MIRACLE SEED 1,000 COINS", "CHARCOAL 1,000 COINS", "MYSTIC WATER 1,000 COINS", "YELLOW FLUTE 1,600 COINS", "NO THANKS" } },
|
||||
[42] = { count = 4, labels = { "B1F", "B2F", "B4F", "EXIT" } },
|
||||
[43] = { count = 4, labels = { "LINKED GAME PLAY", "DIRECT CORNER", "UNION ROOM", "QUIT" } },
|
||||
[44] = { count = 3, labels = { "TWO ISLAND", "THREE ISLAND", "EXIT" } },
|
||||
[45] = { count = 3, labels = { "ONE ISLAND", "THREE ISLAND", "EXIT" } },
|
||||
[46] = { count = 3, labels = { "ONE ISLAND", "TWO ISLAND", "EXIT" } },
|
||||
[47] = { count = 4, labels = { "TRADE CENTER", "COLOSSEUM", "BERRY CRUSH", "EXIT" } },
|
||||
[48] = { count = 3, labels = { "", "", "EXIT" } },
|
||||
[49] = { count = 3, labels = { "POKéMON JUMP", "DODRIO BERRY-PICKING", "EXIT" } },
|
||||
[50] = { count = 3, labels = { "TRADE CENTER", "COLOSSEUM", "EXIT" } },
|
||||
[51] = { count = 2, labels = { "2 TINYMUSHROOMS", "1 BIG MUSHROOM" } },
|
||||
[52] = { count = 5, labels = { "TRADE CENTER", "COLOSSEUM", "", "BERRY CRUSH", "EXIT" } },
|
||||
[53] = { count = 4, labels = { "TRADE CENTER", "COLOSSEUM", "", "EXIT" } },
|
||||
[54] = { count = 3, labels = { "SEVII ISLANDS", "NAVEL ROCK", "EXIT" } },
|
||||
[55] = { count = 3, labels = { "SEVII ISLANDS", "BIRTH ISLAND", "EXIT" } },
|
||||
[56] = { count = 4, labels = { "SEVII ISLANDS", "NAVEL ROCK", "BIRTH ISLAND", "EXIT" } },
|
||||
[57] = { count = 4, labels = { "ONE ISLAND", "TWO ISLAND", "THREE ISLAND", "EXIT" } },
|
||||
[58] = { count = 4, labels = { "VERMILION", "TWO ISLAND", "THREE ISLAND", "EXIT" } },
|
||||
[59] = { count = 4, labels = { "VERMILION", "ONE ISLAND", "THREE ISLAND", "EXIT" } },
|
||||
[60] = { count = 4, labels = { "VERMILION", "ONE ISLAND", "TWO ISLAND", "EXIT" } },
|
||||
[61] = { count = 2, labels = { "VERMILION", "EXIT" } },
|
||||
[62] = { count = 3, labels = { "", "", "EXIT" } },
|
||||
[63] = { count = 3, labels = { "JOIN GROUP", "BECOME LEADER", "EXIT" } },
|
||||
[64] = { count = 5, labels = { "SINGLE", "DOUBLE", "KNOCKOUT", "MIXED", "EXIT" } },
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
-- Extractor for GBA FireRed Multichoice list strings and tables (gMultichoiceLists).
|
||||
|
||||
local Versions = require("src.import.gba.versions")
|
||||
local TextIR = require("src.core.game3.scripting.text_ir")
|
||||
|
||||
local MultichoiceExtract = {}
|
||||
|
||||
local function u32(rom, off)
|
||||
if rom.u32 then return rom:u32(off) end
|
||||
return rom:get(off) + rom:get(off + 1) * 256 + rom:get(off + 2) * 65536 + rom:get(off + 3) * 16777216
|
||||
end
|
||||
|
||||
local function get_byte(rom, off)
|
||||
if rom.get then return rom:get(off) end
|
||||
if rom.data then return rom.data:byte(off + 1) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function decode_gba_string(rom, off)
|
||||
local chars = {}
|
||||
local maxLen = 64
|
||||
for _ = 1, maxLen do
|
||||
local b = get_byte(rom, off)
|
||||
if b == 0xFF then break end
|
||||
if b == 0xFC then
|
||||
local sub = get_byte(rom, off + 1)
|
||||
if sub == 0x13 then
|
||||
off = off + 2
|
||||
chars[#chars + 1] = " "
|
||||
else
|
||||
off = off + 1
|
||||
end
|
||||
elseif b == 0xFD then
|
||||
off = off + 1
|
||||
elseif b == 0xFE or b == 0xFA or b == 0xFB then
|
||||
chars[#chars + 1] = " "
|
||||
elseif TextIR.CHARMAP[b] then
|
||||
chars[#chars + 1] = TextIR.CHARMAP[b]
|
||||
elseif b >= 0xBB and b <= 0xD4 then
|
||||
chars[#chars + 1] = string.char(string.byte("A") + (b - 0xBB))
|
||||
elseif b >= 0xD5 and b <= 0xEE then
|
||||
chars[#chars + 1] = string.char(string.byte("a") + (b - 0xD5))
|
||||
elseif b >= 0xA1 and b <= 0xAA then
|
||||
chars[#chars + 1] = tostring(b - 0xA1)
|
||||
end
|
||||
off = off + 1
|
||||
end
|
||||
local s = table.concat(chars):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
return s
|
||||
end
|
||||
|
||||
function MultichoiceExtract.extract(rom)
|
||||
local base = Versions.MULTICHOICE_LISTS or 0x3E04B0
|
||||
local totalCount = Versions.MULTICHOICE_COUNT or 65
|
||||
|
||||
local lists = {}
|
||||
for i = 0, totalCount - 1 do
|
||||
local off = base + i * 8
|
||||
local listPtr = u32(rom, off)
|
||||
local count = get_byte(rom, off + 4)
|
||||
local labels = {}
|
||||
if listPtr >= 0x08000000 and listPtr < 0x09000000 and count > 0 and count <= 30 then
|
||||
for a = 0, count - 1 do
|
||||
local actOff = listPtr - 0x08000000 + a * 8
|
||||
local textPtr = u32(rom, actOff)
|
||||
local s = ""
|
||||
if textPtr >= 0x08000000 and textPtr < 0x09000000 then
|
||||
s = decode_gba_string(rom, textPtr - 0x08000000)
|
||||
end
|
||||
table.insert(labels, s)
|
||||
end
|
||||
end
|
||||
lists[i] = { count = #labels, labels = labels }
|
||||
end
|
||||
return lists
|
||||
end
|
||||
|
||||
function MultichoiceExtract.formatLua(lists)
|
||||
local lines = {
|
||||
"-- Auto-generated FRLG Multichoice Lists from ROM gMultichoiceLists. DO NOT EDIT DIRECTLY.",
|
||||
"return {",
|
||||
}
|
||||
for i = 0, #lists do
|
||||
local item = lists[i]
|
||||
if item and item.labels then
|
||||
local quoted = {}
|
||||
for _, s in ipairs(item.labels) do
|
||||
table.insert(quoted, string.format("%q", s))
|
||||
end
|
||||
lines[#lines + 1] = string.format(" [%d] = { count = %d, labels = { %s } },", i, #item.labels, table.concat(quoted, ", "))
|
||||
end
|
||||
end
|
||||
lines[#lines + 1] = "}"
|
||||
lines[#lines + 1] = ""
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
function MultichoiceExtract.run(rom, cache, opts)
|
||||
opts = opts or {}
|
||||
local lists = MultichoiceExtract.extract(rom)
|
||||
local content = MultichoiceExtract.formatLua(lists)
|
||||
|
||||
local cacheRoot = opts.cacheRoot or "data/generated/gba"
|
||||
local rel = cacheRoot .. "/scripts/multichoice.lua"
|
||||
|
||||
if cache and cache.write then
|
||||
cache:write(rel, content)
|
||||
end
|
||||
|
||||
local f = io.open(rel, "wb") or io.open("data/generated/gba/scripts/multichoice.lua", "wb")
|
||||
if f then
|
||||
f:write(content)
|
||||
f:close()
|
||||
end
|
||||
|
||||
local fStub = io.open("src/import/gba/multichoice_data_stub.lua", "wb")
|
||||
if fStub then
|
||||
fStub:write(content)
|
||||
fStub:close()
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return MultichoiceExtract
|
||||
@@ -115,6 +115,10 @@ Versions.POKEDEX_ORDERS = {
|
||||
type = 0x4448FE,
|
||||
}
|
||||
|
||||
-- Multichoice list table (FireRed USA 1.0). gMultichoiceLists (65 lists).
|
||||
Versions.MULTICHOICE_LISTS = 0x3E04B0
|
||||
Versions.MULTICHOICE_COUNT = 65
|
||||
|
||||
-- Items table (FireRed USA 1.0). 375 entries × 44 bytes stride.
|
||||
Versions.ITEMS = 0x3DB028
|
||||
Versions.ITEMS_COUNT = 375
|
||||
|
||||
@@ -443,13 +443,42 @@ local function handle_menu_input(input)
|
||||
bag = BagMenu._bag,
|
||||
item = row.id,
|
||||
mode = "use",
|
||||
battle = true,
|
||||
battleOrder = st and st.playerParty and PartyMenu.battleOrder(st) or nil,
|
||||
layout = (st and st.double) and "double" or nil,
|
||||
onSelect = function(slot)
|
||||
if not slot or slot == 7 then
|
||||
PartyMenu.close()
|
||||
return
|
||||
end
|
||||
local realSlot = (PartyMenu._order and PartyMenu._order[slot]) or slot
|
||||
local mon = party and party[realSlot]
|
||||
local canUse, err = BattleItems.canUseOn(st, row.id, realSlot, mon)
|
||||
if not canUse then
|
||||
se(9)
|
||||
PartyMenu.showMessage(err or "It won't have any effect.", function()
|
||||
PartyMenu.mode = "use"
|
||||
end)
|
||||
return
|
||||
end
|
||||
PartyMenu.close()
|
||||
begin_exit(true, function()
|
||||
save_pos()
|
||||
local cb = BagMenu._onBattleUse
|
||||
BagMenu._battleUsed = true
|
||||
BagMenu.open = false
|
||||
BagMenu._battle = false
|
||||
BagMenu._onBattleUse = nil
|
||||
Stack.pop("bag")
|
||||
if cb then cb(row.id, realSlot) end
|
||||
end)
|
||||
end,
|
||||
onClose = function()
|
||||
BagMenu.mode = "list"
|
||||
clamp_cursor()
|
||||
end,
|
||||
})
|
||||
return
|
||||
else
|
||||
-- src/item_use.c:742
|
||||
begin_exit(true, function()
|
||||
|
||||
+116
-21
@@ -1,4 +1,4 @@
|
||||
-- Yes/No + multichoice (pret yesnobox / multichoice). Writes VAR_RESULT via callback.
|
||||
-- Yes/No + multichoice (pret yesnobox / multichoice / multichoicegrid). Writes VAR_RESULT via callback.
|
||||
|
||||
local Window = require("src.ui.game3.window")
|
||||
local Display = require("src.core.game3.display")
|
||||
@@ -12,6 +12,8 @@ Choice.cursor = 1
|
||||
Choice.done = nil
|
||||
Choice.left = nil
|
||||
Choice.top = nil
|
||||
Choice.cols = 1
|
||||
Choice.ignoreBPress = false
|
||||
|
||||
function Choice.yesNo(cb, layout)
|
||||
Choice.active = true
|
||||
@@ -30,6 +32,8 @@ function Choice.yesNo(cb, layout)
|
||||
Choice.left = tonumber(layout.left) or (Display.COLS - 8)
|
||||
Choice.top = tonumber(layout.top) or 8
|
||||
Choice.maxRight = nil
|
||||
Choice.cols = 1
|
||||
Choice.ignoreBPress = layout.ignoreBPress or false
|
||||
end
|
||||
|
||||
function Choice.multi(options, defaultIdx, cb, layout)
|
||||
@@ -45,13 +49,46 @@ function Choice.multi(options, defaultIdx, cb, layout)
|
||||
Choice.left = tonumber(layout.left) or (Display.COLS - 10)
|
||||
Choice.top = tonumber(layout.top) or 5
|
||||
Choice.maxRight = tonumber(layout.maxRight)
|
||||
Choice.cols = tonumber(layout.cols) or 1
|
||||
Choice.ignoreBPress = layout.ignoreBPress or false
|
||||
end
|
||||
|
||||
function Choice.move(delta)
|
||||
function Choice.move(dy, dx)
|
||||
if not Choice.active or not Choice.options then return end
|
||||
local n = #Choice.options
|
||||
if n < 1 then return end
|
||||
Choice.cursor = ((Choice.cursor - 1 + delta) % n) + 1
|
||||
local cols = Choice.cols or 1
|
||||
if cols <= 1 then
|
||||
local delta = dy or 0
|
||||
if delta == 0 and dx then delta = dx end
|
||||
if delta ~= 0 then
|
||||
Choice.cursor = ((Choice.cursor - 1 + delta) % n) + 1
|
||||
pcall(function() require("src.core.game3.audio").playSe(5) end)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- 2D Grid navigation
|
||||
local rows = math.ceil(n / cols)
|
||||
local cur = Choice.cursor - 1
|
||||
local curCol = cur % cols
|
||||
local curRow = math.floor(cur / cols)
|
||||
|
||||
if dy and dy ~= 0 then
|
||||
curRow = (curRow + dy) % rows
|
||||
end
|
||||
if dx and dx ~= 0 then
|
||||
curCol = (curCol + dx) % cols
|
||||
end
|
||||
|
||||
local target = curRow * cols + curCol
|
||||
if target >= n then
|
||||
target = n - 1
|
||||
end
|
||||
if target + 1 ~= Choice.cursor then
|
||||
Choice.cursor = target + 1
|
||||
pcall(function() require("src.core.game3.audio").playSe(5) end)
|
||||
end
|
||||
end
|
||||
|
||||
function Choice.confirm()
|
||||
@@ -63,6 +100,8 @@ function Choice.confirm()
|
||||
Choice.active = false
|
||||
Choice.kind = nil
|
||||
Choice.options = nil
|
||||
Choice.cols = 1
|
||||
Choice.ignoreBPress = false
|
||||
Choice.done = nil
|
||||
if not cb then return end
|
||||
if kind == "yesno" then
|
||||
@@ -74,12 +113,17 @@ end
|
||||
|
||||
function Choice.cancel()
|
||||
if not Choice.active then return end
|
||||
if Choice.ignoreBPress then
|
||||
return
|
||||
end
|
||||
pcall(function() require("src.core.game3.audio").playSe(9) end)
|
||||
local cb = Choice.done
|
||||
local kind = Choice.kind
|
||||
Choice.active = false
|
||||
Choice.kind = nil
|
||||
Choice.options = nil
|
||||
Choice.cols = 1
|
||||
Choice.ignoreBPress = false
|
||||
Choice.done = nil
|
||||
if not cb then return end
|
||||
if kind == "yesno" then
|
||||
@@ -118,25 +162,76 @@ function Choice.draw()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local n = #Choice.options
|
||||
local tw = 8
|
||||
for _, lab in ipairs(Choice.options) do
|
||||
local need = math.min(18, math.max(6, math.floor(#tostring(lab) * 0.7) + 2))
|
||||
if need > tw then tw = need end
|
||||
end
|
||||
local th = math.max(2, math.ceil((n * Window.OPTION_HEIGHT) / 8))
|
||||
local tx = Choice.left or (Display.COLS - tw - 2)
|
||||
if Choice.kind == "multi" and Choice.maxRight and tx + tw > Choice.maxRight then
|
||||
tx = Choice.maxRight - tw
|
||||
end
|
||||
local ty = Choice.top or 5
|
||||
Window.stdFrame(Window.template(tx, ty, tw, th))
|
||||
local leftPx = tx * 8
|
||||
local topPx = ty * 8
|
||||
for i, lab in ipairs(Choice.options) do
|
||||
local yPx = Window.menuRowPx(topPx, i)
|
||||
if i == Choice.cursor then Window.cursorPx(leftPx, yPx) end
|
||||
Window.printPx(lab, leftPx + Window.CURSOR_WIDTH, yPx)
|
||||
local cols = Choice.cols or 1
|
||||
if cols <= 1 then
|
||||
local tw = 8
|
||||
for _, lab in ipairs(Choice.options) do
|
||||
local need = math.min(18, math.max(6, math.floor(#tostring(lab) * 0.7) + 2))
|
||||
if need > tw then tw = need end
|
||||
end
|
||||
local th = math.max(2, math.ceil((n * Window.OPTION_HEIGHT) / 8))
|
||||
local tx = Choice.left or (Display.COLS - tw - 2)
|
||||
if Choice.kind == "multi" and Choice.maxRight and tx + tw > Choice.maxRight then
|
||||
tx = Choice.maxRight - tw
|
||||
end
|
||||
local ty = Choice.top or 5
|
||||
Window.stdFrame(Window.template(tx, ty, tw, th))
|
||||
local leftPx = tx * 8
|
||||
local topPx = ty * 8
|
||||
for i, lab in ipairs(Choice.options) do
|
||||
local yPx = Window.menuRowPx(topPx, i)
|
||||
if i == Choice.cursor then Window.cursorPx(leftPx, yPx) end
|
||||
Window.printPx(lab, leftPx + Window.CURSOR_WIDTH, yPx)
|
||||
end
|
||||
else
|
||||
-- Multi-column grid
|
||||
local rows = math.ceil(n / cols)
|
||||
local colTileWidths = {}
|
||||
for c = 1, cols do
|
||||
local maxW = 4
|
||||
for r = 1, rows do
|
||||
local idx = (r - 1) * cols + c
|
||||
if idx <= n then
|
||||
local lab = tostring(Choice.options[idx] or "")
|
||||
local need = math.floor(#lab * 0.7) + 2
|
||||
if need > maxW then maxW = need end
|
||||
end
|
||||
end
|
||||
colTileWidths[c] = maxW
|
||||
end
|
||||
local totalTileW = 0
|
||||
for c = 1, cols do
|
||||
totalTileW = totalTileW + colTileWidths[c]
|
||||
end
|
||||
local th = math.max(2, math.ceil((rows * Window.OPTION_HEIGHT) / 8))
|
||||
local tx = Choice.left or 2
|
||||
if Choice.maxRight and tx + totalTileW > Choice.maxRight then
|
||||
tx = Choice.maxRight - totalTileW
|
||||
end
|
||||
if tx < 0 then tx = 0 end
|
||||
local ty = Choice.top or 5
|
||||
Window.stdFrame(Window.template(tx, ty, totalTileW, th))
|
||||
|
||||
local topPx = ty * 8
|
||||
for i, lab in ipairs(Choice.options) do
|
||||
local idx0 = i - 1
|
||||
local c = (idx0 % cols) + 1
|
||||
local r = math.floor(idx0 / cols) + 1
|
||||
|
||||
local colOffsetTiles = 0
|
||||
for prevC = 1, c - 1 do
|
||||
colOffsetTiles = colOffsetTiles + colTileWidths[prevC]
|
||||
end
|
||||
local colLeftPx = (tx + colOffsetTiles) * 8
|
||||
local yPx = Window.menuRowPx(topPx, r)
|
||||
|
||||
if i == Choice.cursor then
|
||||
Window.cursorPx(colLeftPx, yPx)
|
||||
end
|
||||
Window.printPx(lab, colLeftPx + Window.CURSOR_WIDTH, yPx)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+23
-24
@@ -325,6 +325,8 @@ local function ensureUser(frameType)
|
||||
local img, path = loadImage({
|
||||
{ path = rel, w = 24, h = 24 },
|
||||
{ path = "data/generated/gba/" .. rel, w = 24, h = 24 },
|
||||
{ path = "src/import/gba/chrome/user_frame_" .. n .. ".png", w = 24, h = 24 },
|
||||
{ path = "src/import/gba/chrome/user_frame_rgba.png", w = 24, h = 24 },
|
||||
})
|
||||
Chrome._user[n] = img and { image = img, quads = makeQuads(img, 3, 3), path = path } or false
|
||||
return Chrome._user[n] or nil
|
||||
@@ -352,10 +354,9 @@ end
|
||||
|
||||
--- pret std 9-slice around content (tx,ty,tw,th) in tiles.
|
||||
function Chrome.stdFrame(tx, ty, tw, th)
|
||||
if (Chrome._frameType or 0) > 0 then
|
||||
local user = ensureUser(Chrome._frameType)
|
||||
if user then return drawNineSlice(user, tx, ty, tw, th) end
|
||||
end
|
||||
local ft = Chrome._frameType or 0
|
||||
local user = ensureUser(ft)
|
||||
if user then return drawNineSlice(user, tx, ty, tw, th) end
|
||||
local atlas = ensureStd()
|
||||
if atlas then return drawNineSlice(atlas, tx, ty, tw, th) end
|
||||
fillRect(tx * T - 8, ty * T - 8, (tw + 2) * T, (th + 2) * T, 98 / 255, 115 / 255, 123 / 255, 1)
|
||||
@@ -399,7 +400,7 @@ end
|
||||
|
||||
--- pret MapNamePopupCreateWindow 9-slice banner at pixel coordinates (px, py).
|
||||
-- Content size is (widthTiles * 8) wide by 16 high.
|
||||
-- Outer border spans: x in [px, px + (widthTiles + 2)*8], y in [py - 8, py + 24].
|
||||
-- Outer border spans: x in [px, px + (widthTiles + 2)*8], y in [py, py + 24].
|
||||
function Chrome.mapPopupFrame(px, py, widthTiles)
|
||||
widthTiles = tonumber(widthTiles) or 14
|
||||
local contentW = widthTiles * 8
|
||||
@@ -407,38 +408,36 @@ function Chrome.mapPopupFrame(px, py, widthTiles)
|
||||
|
||||
if atlas then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
local function cell(tile, cx, cy)
|
||||
blitTile(atlas, tile, cx, cy, false)
|
||||
local function cell(tile, cx, cy, vflip)
|
||||
blitTile(atlas, tile, cx, cy, vflip)
|
||||
end
|
||||
local function hspan(tile, cx, cy, n)
|
||||
for i = 0, n - 1 do cell(tile, cx + i * 8, cy) end
|
||||
local function hspan(tile, cx, cy, n, vflip)
|
||||
for i = 0, n - 1 do cell(tile, cx + i * 8, cy, vflip) end
|
||||
end
|
||||
|
||||
-- 1. Content background: pure white PIXEL_FILL(1)
|
||||
fillRect(px + 8, py, contentW, 16, 1, 1, 1, 1)
|
||||
fillRect(px + 8, py + 4, contentW, 16, 1, 1, 1, 1)
|
||||
|
||||
-- 2. Top edge (row -1, y = py - 8)
|
||||
cell(0, px, py - 8)
|
||||
hspan(1, px + 8, py - 8, widthTiles)
|
||||
cell(2, px + 8 + contentW, py - 8)
|
||||
-- 2. Top edge (row 0, y = py) — mirrored from bottom edge (tiles 6, 7, 8 vflipped)
|
||||
cell(6, px, py, true)
|
||||
hspan(7, px + 8, py, widthTiles, true)
|
||||
cell(8, px + 8 + contentW, py, true)
|
||||
|
||||
-- 3. Left & Right borders (height = 2 tiles / 16px)
|
||||
cell(3, px, py)
|
||||
-- 3. Left & Right borders (height = 1 tile / 8px middle row, y = py + 8)
|
||||
cell(3, px, py + 8)
|
||||
cell(5, px + 8 + contentW, py)
|
||||
cell(5, px + 8 + contentW, py + 8)
|
||||
|
||||
-- 4. Bottom edge (row +2, y = py + 16)
|
||||
cell(6, px, py + 16)
|
||||
hspan(7, px + 8, py + 16, widthTiles)
|
||||
cell(8, px + 8 + contentW, py + 16)
|
||||
-- 4. Bottom edge (row 2, y = py + 16)
|
||||
cell(6, px, py + 16, false)
|
||||
hspan(7, px + 8, py + 16, widthTiles, false)
|
||||
cell(8, px + 8 + contentW, py + 16, false)
|
||||
return
|
||||
end
|
||||
|
||||
-- Fallback if atlas missing
|
||||
fillRect(px, py - 8, contentW + 16, 32, 98 / 255, 115 / 255, 123 / 255, 1)
|
||||
fillRect(px + 2, py - 6, contentW + 12, 28, 205 / 255, 213 / 255, 213 / 255, 1)
|
||||
fillRect(px + 8, py, contentW, 16, 1, 1, 1, 1)
|
||||
fillRect(px, py, contentW + 16, 24, 98 / 255, 115 / 255, 123 / 255, 1)
|
||||
fillRect(px + 2, py + 2, contentW + 12, 20, 205 / 255, 213 / 255, 213 / 255, 1)
|
||||
fillRect(px + 8, py + 4, contentW, 16, 1, 1, 1, 1)
|
||||
end
|
||||
|
||||
function Chrome.invalidate()
|
||||
|
||||
+14
-10
@@ -98,11 +98,10 @@ local function update_top_menu(input)
|
||||
return true
|
||||
end
|
||||
if RegionMap.isOpen() then
|
||||
if input:wasPressed("left") then RegionMap.togglePage(-1)
|
||||
elseif input:wasPressed("right") then RegionMap.togglePage(1)
|
||||
elseif input:wasPressed("up") then RegionMap.moveCursor(-1)
|
||||
elseif input:wasPressed("down") then RegionMap.moveCursor(1)
|
||||
elseif input:wasPressed("b") or input:wasPressed("start") then RegionMap.close()
|
||||
if RegionMap.handleInput then
|
||||
RegionMap.handleInput(input)
|
||||
else
|
||||
if input:wasPressed("b") or input:wasPressed("start") then RegionMap.close() end
|
||||
end
|
||||
return true
|
||||
end
|
||||
@@ -185,8 +184,10 @@ function Hud.update(game, _dt)
|
||||
|
||||
-- Choice in field/scripting (in battle, Choice is driven by Battle.update).
|
||||
if not inBattle and Choice.active then
|
||||
if input:wasPressed("up") then Choice.move(-1)
|
||||
elseif input:wasPressed("down") then Choice.move(1)
|
||||
if input:wasPressed("up") then Choice.move(-1, 0)
|
||||
elseif input:wasPressed("down") then Choice.move(1, 0)
|
||||
elseif input:wasPressed("left") then Choice.move(0, -1)
|
||||
elseif input:wasPressed("right") then Choice.move(0, 1)
|
||||
elseif input:wasPressed("a") then Choice.confirm()
|
||||
elseif input:wasPressed("b") then Choice.cancel()
|
||||
end
|
||||
@@ -278,9 +279,12 @@ function Hud.openStartMenu(game, session)
|
||||
or require("src.core.game3.scripting.flags")
|
||||
local store = Space and Space.store
|
||||
if store and Flags.IDS and Flags.IDS.OPENED_START_MENU then
|
||||
Flags.setFlag(store, nil, Flags.IDS.OPENED_START_MENU, true)
|
||||
if Space.persistSession then
|
||||
pcall(Space.persistSession)
|
||||
local scene = Flags.getVar(store, nil, 0x4070)
|
||||
if scene >= 1 then
|
||||
Flags.setFlag(store, nil, Flags.IDS.OPENED_START_MENU, true)
|
||||
if Space.persistSession then
|
||||
pcall(Space.persistSession)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -203,12 +203,12 @@ function MapNamePopup.draw()
|
||||
-- 1. Draw 9-slice standard text window border and white interior
|
||||
Chrome.mapPopupFrame(px, py, widthTiles)
|
||||
|
||||
-- 2. Map Name Text (Centered in content window, y = py + 2)
|
||||
-- 2. Map Name Text (Centered vertically and horizontally in enclosed window)
|
||||
-- Uses FONT_NORMAL with dark gray (#626262) fg and light gray (#D5D5CD) shadow
|
||||
local name = MapNamePopup._name or ""
|
||||
local textW = (FrlgFont.measure and FrlgFont.measure(name)) or (6 * #name)
|
||||
local textX = px + 8 + math.floor((contentW - textW) / 2)
|
||||
local textY = py + 2
|
||||
local textY = py + 5
|
||||
|
||||
FrlgFont.draw(name, textX, textY, {
|
||||
colors = FrlgFont.COLOR.NORMAL,
|
||||
|
||||
@@ -1142,7 +1142,15 @@ function PartyMenu.handleInput(input)
|
||||
if input:wasPressed("a") then
|
||||
if PartyMenu.cursor == 7 then
|
||||
se(9)
|
||||
PartyMenu.close()
|
||||
if PartyMenu._onSelect then
|
||||
PartyMenu._onSelect(nil)
|
||||
else
|
||||
PartyMenu.close()
|
||||
end
|
||||
return
|
||||
end
|
||||
if PartyMenu._onSelect then
|
||||
PartyMenu._onSelect(PartyMenu.cursor)
|
||||
return
|
||||
end
|
||||
local mon = PartyMenu._party and PartyMenu._party[PartyMenu.cursor]
|
||||
|
||||
+15
-10
@@ -1023,14 +1023,16 @@ local function draw_data_screen()
|
||||
local mapX, mapY = 136, 64
|
||||
PokedexChrome.drawMap("kanto", mapX, mapY)
|
||||
|
||||
-- Route Area Markers (Static)
|
||||
-- Route Area Markers
|
||||
local areas = PokedexData.getWildAreasForSpecies(sp)
|
||||
|
||||
if #areas > 0 then
|
||||
for _, aKey in ipairs(areas) do
|
||||
local m = PokedexData.getAreaMarker(aKey)
|
||||
if m then
|
||||
PokedexChrome.drawAreaMarker(m.shape, mapX + (m.x - 32), mapY + m.y)
|
||||
if PokedexData.getAreaMapKey(aKey) == "kanto" then
|
||||
local m = PokedexData.getAreaMarker(aKey)
|
||||
if m then
|
||||
PokedexChrome.drawAreaMarker(m.shape, mapX + (m.x - 32), mapY + m.y)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -1188,15 +1190,18 @@ local function draw_area_screen()
|
||||
|
||||
-- Right Map Panel
|
||||
local mapX, mapY = 136, 64
|
||||
PokedexChrome.drawMap(Pokedex.areaMapKey or "kanto", mapX, mapY)
|
||||
local curMap = Pokedex.areaMapKey or "kanto"
|
||||
PokedexChrome.drawMap(curMap, mapX, mapY)
|
||||
|
||||
-- Route Markers (Static)
|
||||
-- Route Markers
|
||||
if #areas > 0 then
|
||||
for _, aKey in ipairs(areas) do
|
||||
local m = PokedexData.getAreaMarker(aKey)
|
||||
if m then
|
||||
local xOff = (Pokedex.areaMapKey == "kanto" or not Pokedex.areaMapKey) and (m.x - 32) or m.x
|
||||
PokedexChrome.drawAreaMarker(m.shape, mapX + xOff, mapY + m.y)
|
||||
if PokedexData.getAreaMapKey(aKey) == curMap then
|
||||
local m = PokedexData.getAreaMarker(aKey)
|
||||
if m then
|
||||
local xOff = (curMap == "kanto") and (m.x - 32) or m.x
|
||||
PokedexChrome.drawAreaMarker(m.shape, mapX + xOff, mapY + m.y)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
|
||||
@@ -754,7 +754,7 @@ function PokedexChrome.drawMap(mapKey, x, y, scale)
|
||||
end
|
||||
end
|
||||
|
||||
--- Draw Area Route Marker (Static partially transparent red overlay)
|
||||
--- Draw Area Route Marker (Steady slightly transparent red overlay)
|
||||
function PokedexChrome.drawAreaMarker(shape, x, y)
|
||||
if not (love and love.graphics) then return end
|
||||
|
||||
@@ -770,7 +770,7 @@ function PokedexChrome.drawAreaMarker(shape, x, y)
|
||||
local imgKey = shapeMap[shape] or "marker_0"
|
||||
local img = PokedexChrome.getImage(imgKey)
|
||||
|
||||
love.graphics.setColor(1, 0.35, 0.35, 0.75)
|
||||
love.graphics.setColor(1, 0.3, 0.3, 0.75)
|
||||
if img then
|
||||
love.graphics.draw(img, x, y)
|
||||
else
|
||||
|
||||
@@ -85,6 +85,7 @@ function UI.draw(playback,session)
|
||||
love.graphics.rectangle('fill',0,y,240,144-y)
|
||||
Font.draw(text,4,y,{colors=Font.COLOR.WHITE})
|
||||
end
|
||||
Font.draw('A: NEXT B: SKIP',4,146,{small=true,colors=Font.COLOR.WHITE})
|
||||
local PokedexChrome = require('src.ui.game3.pokedex_chrome')
|
||||
PokedexChrome.drawControlInfoLeft('{A_BUTTON}NEXT {B_BUTTON}SKIP', 4, 146)
|
||||
end
|
||||
return UI
|
||||
|
||||
@@ -30,6 +30,32 @@ local SWITCH_BUTTON_Y = 11
|
||||
|
||||
local function try_load_image(path)
|
||||
if not (love and love.graphics and love.graphics.newImage) then return nil end
|
||||
local okC, CacheFs = pcall(require, "src.import.CacheFs")
|
||||
if okC and CacheFs and CacheFs.read then
|
||||
local data = CacheFs.read(path)
|
||||
if data and type(data) == "string" and #data > 0 then
|
||||
if love.filesystem and love.filesystem.newFileData and love.image and love.image.newImageData then
|
||||
local okFd, fd = pcall(love.filesystem.newFileData, data, path)
|
||||
if okFd and fd then
|
||||
local okId, id = pcall(love.image.newImageData, fd)
|
||||
if okId and id then
|
||||
local okImg, img = pcall(love.graphics.newImage, id)
|
||||
if okImg and img then
|
||||
if img.setFilter then img:setFilter("nearest", "nearest") end
|
||||
return img
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if love.filesystem and love.filesystem.getInfo and love.filesystem.getInfo(path) then
|
||||
local ok, img = pcall(love.graphics.newImage, path)
|
||||
if ok and img then
|
||||
if img.setFilter then img:setFilter("nearest", "nearest") end
|
||||
return img
|
||||
end
|
||||
end
|
||||
local ok, img = pcall(love.graphics.newImage, path)
|
||||
if ok and img then
|
||||
if img.setFilter then img:setFilter("nearest", "nearest") end
|
||||
|
||||
@@ -438,6 +438,11 @@ local function sceneRun(T, pressed)
|
||||
T.sceneState = 1
|
||||
end
|
||||
if pressed.a or pressed.start then
|
||||
local okR, Rng = pcall(require, "src.core.game3.rng")
|
||||
if okR and Rng and Rng.seedNewGame then
|
||||
-- pokefirered/src/title_screen.c:632: SetTitleScreenScene_Cry → SeedRngAndSetTrainerId
|
||||
Rng.seedNewGame()
|
||||
end
|
||||
setScene(T, S.CRY)
|
||||
elseif not findTask(T, Title.Task_TitleScreenTimer) then
|
||||
setScene(T, S.RESTART)
|
||||
@@ -510,6 +515,8 @@ local function Task_TitleScreenMain(T, t)
|
||||
local pressed = T.input
|
||||
if (pressed.a or pressed.b or pressed.start)
|
||||
and T.scene ~= S.RUN and T.scene ~= S.RESTART and T.scene ~= S.CRY then
|
||||
local okR, Rng = pcall(require, "src.core.game3.rng")
|
||||
if okR and Rng and Rng.perturb then Rng.perturb() end
|
||||
T.band = nil
|
||||
loadMainPalsAndResetBgs(T)
|
||||
setScene(T, S.RUN)
|
||||
|
||||
@@ -155,14 +155,16 @@ end
|
||||
do
|
||||
local vm = fresh_vm(pack)
|
||||
for _, kind in ipairs({ "general", "special", "status" }) do
|
||||
for i = 0, 40 do
|
||||
if pack[kind][i] then
|
||||
for _, sides in ipairs({ { "player", "enemy" }, { "enemy", "player" } }) do
|
||||
Anim.present("player").visible = true
|
||||
Anim.present("enemy").visible = true
|
||||
local frames, ok, e = run_table(vm, kind, i, { a = sides[1], t = sides[2] })
|
||||
local name = (pack[kind .. "Names"] and pack[kind .. "Names"][i]) or i
|
||||
check(ok and #e == 0, string.format("%s[%d] %s (%s) ends cleanly in %d frames %s", kind, i, tostring(name), sides[1], frames, table.concat(e, " | ")))
|
||||
if pack[kind] then
|
||||
for i = 0, 40 do
|
||||
if pack[kind][i] then
|
||||
for _, sides in ipairs({ { "player", "enemy" }, { "enemy", "player" } }) do
|
||||
Anim.present("player").visible = true
|
||||
Anim.present("enemy").visible = true
|
||||
local frames, ok, e = run_table(vm, kind, i, { a = sides[1], t = sides[2] })
|
||||
local name = (pack[kind .. "Names"] and pack[kind .. "Names"][i]) or i
|
||||
check(ok and #e == 0, string.format("%s[%d] %s (%s) ends cleanly in %d frames %s", kind, i, tostring(name), sides[1], frames, table.concat(e, " | ")))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -135,7 +135,7 @@ for moveId, script in pairs(pack.moves) do
|
||||
vmErrors = vmErrors + 1
|
||||
print(string.format("[ERROR] Move %s launch error: %s", tostring(moveId), tostring(err)))
|
||||
else
|
||||
local maxTicks = 900
|
||||
local maxTicks = 1200
|
||||
local ticks = 0
|
||||
while vm:busy() and ticks < maxTicks do
|
||||
ticks = ticks + 1
|
||||
|
||||
@@ -376,7 +376,7 @@ if packSrc then
|
||||
local vm = AnimVm.new()
|
||||
vm:setPack(pack)
|
||||
vm:launch(script, { attackerSide = "player", isReversed = false })
|
||||
local maxTicks = 360
|
||||
local maxTicks = 600
|
||||
local ticks = 0
|
||||
while vm:busy() and ticks < maxTicks do
|
||||
ticks = ticks + 1
|
||||
|
||||
@@ -368,7 +368,7 @@ for _, m in ipairs(phase3Moves) do
|
||||
AnimSprites.reset()
|
||||
Anim.reset({ headless = true })
|
||||
vm:launch(pack.moves[m.id], { attackerSide = "player", isReversed = false })
|
||||
local maxFrames = 900
|
||||
local maxFrames = 1200
|
||||
local frames = 0
|
||||
while vm:busy() and frames < maxFrames do
|
||||
frames = frames + 1
|
||||
|
||||
@@ -382,7 +382,7 @@ local function run_move(moveId, moveName)
|
||||
vm:launch(script, { attackerSide = "player", isReversed = false })
|
||||
assert_true(vm:busy(), "VM should start busy")
|
||||
|
||||
local maxTicks = 360
|
||||
local maxTicks = 600
|
||||
local ticks = 0
|
||||
while vm:busy() and ticks < maxTicks do
|
||||
ticks = ticks + 1
|
||||
|
||||
@@ -160,6 +160,64 @@ end
|
||||
check(Runtime._lastResult == "catch" or #Runtime._session.party >= 2,
|
||||
"onDone catch or mon stored")
|
||||
|
||||
print("[test] 6. Battle Bag exit restores menu mode cleanly")
|
||||
local BagMenu = require("src.ui.game3.bag_menu")
|
||||
Bag.add(Runtime._session.bag, 13, 5) -- Add potions so bag is not empty
|
||||
Ui._session = Runtime._session
|
||||
Ui._queue = {}
|
||||
Ui._showing = false
|
||||
Ui._linger = false
|
||||
Ui._timed = nil
|
||||
Ui._headless = false
|
||||
Battle._active = true
|
||||
Battle._phase = "command"
|
||||
Battle._st = { player = State.makeBattler(Runtime._session.party[1], "player"), playerParty = Runtime._session.party }
|
||||
Ui.bindState(Battle._st)
|
||||
Ui.openMenu()
|
||||
check(Ui._mode == "menu", "initial battle UI mode is menu")
|
||||
local fakeInputA = { wasPressed = function(self, k) local key = (k ~= nil) and k or self; return key == "a" end }
|
||||
local fakeInputB = { wasPressed = function(self, k) local key = (k ~= nil) and k or self; return key == "b" end }
|
||||
local fakeInputNone = { wasPressed = function() return false end }
|
||||
|
||||
-- Select BAG
|
||||
Ui._menuIndex = 2
|
||||
Ui.handleInput(fakeInputA)
|
||||
check(Ui._mode == "bag", "Ui._mode transitioned to bag")
|
||||
check(BagMenu.isOpen() == true, "BagMenu is open")
|
||||
|
||||
-- Allow opening curtain animation to finish
|
||||
BagMenu.settle()
|
||||
|
||||
-- Simulate pressing B in BagMenu
|
||||
BagMenu.handleInput(fakeInputB)
|
||||
-- Allow closing transition to finish
|
||||
BagMenu.settle()
|
||||
check(BagMenu.isOpen() == false, "BagMenu closed after B press")
|
||||
check(Ui._mode == "menu", "Ui._mode restored to menu after bag exit")
|
||||
|
||||
-- Verify battle update accepts subsequent command input without freeze
|
||||
Battle.update(0, { input = fakeInputNone })
|
||||
check(Ui._mode == "menu", "Ui._mode remains menu in command phase")
|
||||
|
||||
print("[test] 7. In-battle Party Item Selection validation")
|
||||
Bag.add(Runtime._session.bag, 13, 1) -- Potion
|
||||
local monFull = { species = 1, hp = 20, maxHp = 20 }
|
||||
local monHurt = { species = 1, hp = 5, maxHp = 20 }
|
||||
local monFaint = { species = 1, hp = 0, maxHp = 20 }
|
||||
local canHurt, _ = BattleItems.canUseOn(nil, 13, 1, monHurt)
|
||||
local canFull, _ = BattleItems.canUseOn(nil, 13, 1, monFull)
|
||||
local canFaint, _ = BattleItems.canUseOn(nil, 13, 1, monFaint)
|
||||
check(canHurt == true, "Potion can be used on hurt mon")
|
||||
check(canFull == false, "Potion cannot be used on full HP mon")
|
||||
check(canFaint == false, "Potion cannot be used on fainted mon")
|
||||
|
||||
local canReviveFaint, _ = BattleItems.canUseOn(nil, 24, 1, monFaint) -- Revive
|
||||
local canReviveHurt, _ = BattleItems.canUseOn(nil, 24, 1, monHurt)
|
||||
check(canReviveFaint == true, "Revive can be used on fainted mon")
|
||||
check(canReviveHurt == false, "Revive cannot be used on alive mon")
|
||||
|
||||
Battle._active = false
|
||||
|
||||
if failed > 0 then
|
||||
print(string.format("\n%d FAILED", failed))
|
||||
os.exit(1)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env luajit
|
||||
-- Test wild encounter table resolution and rolls across multiple map ID alias formats.
|
||||
|
||||
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 Encounters = require("src.core.game3.encounters")
|
||||
local MapCatalog = require("src.import.gba.map_catalog")
|
||||
local Rng = require("src.core.game3.rng")
|
||||
|
||||
MapCatalog.rebuildIndex()
|
||||
Encounters.loadFromMod(nil)
|
||||
check(Encounters.ensureLoaded(), "Encounters tables loaded successfully")
|
||||
|
||||
print("\n--- Test 1: Route 22 multi-alias resolution ---")
|
||||
local r22_keys = { "ROUTE_22", "FR_ROUTE_22", "ROUTE22", "FR_ROUTE22", "3:41" }
|
||||
for _, k in ipairs(r22_keys) do
|
||||
local t = Encounters.tableFor(k)
|
||||
check(t ~= nil, "tableFor succeeds for Route 22 alias: " .. k)
|
||||
if t then
|
||||
check(t.land ~= nil and #t.land.slots == 12, "Route 22 has 12 land slots")
|
||||
check(t.water ~= nil and #t.water.slots == 5, "Route 22 has 5 water slots")
|
||||
check(t.fishing ~= nil and #t.fishing.slots == 10, "Route 22 has 10 fish slots")
|
||||
-- Verify Rattata (19), Mankey (56), Spearow (21)
|
||||
local speciesSet = {}
|
||||
for _, s in ipairs(t.land.slots) do speciesSet[s.species] = true end
|
||||
check(speciesSet[19] and speciesSet[56] and speciesSet[21], "Route 22 contains Rattata, Mankey, and Spearow")
|
||||
end
|
||||
end
|
||||
|
||||
print("\n--- Test 2: Route 2 multi-alias resolution ---")
|
||||
local r2_keys = { "ROUTE_2", "FR_ROUTE_2", "ROUTE2", "FR_ROUTE2", "3:20" }
|
||||
for _, k in ipairs(r2_keys) do
|
||||
local t = Encounters.tableFor(k)
|
||||
check(t ~= nil, "tableFor succeeds for Route 2 alias: " .. k)
|
||||
if t then
|
||||
check(t.land ~= nil and #t.land.slots == 12, "Route 2 has 12 land slots")
|
||||
local speciesSet = {}
|
||||
for _, s in ipairs(t.land.slots) do speciesSet[s.species] = true end
|
||||
check(speciesSet[19] and speciesSet[16] and speciesSet[10] and speciesSet[13],
|
||||
"Route 2 contains Rattata (19), Pidgey (16), Caterpie (10), Weedle (13)")
|
||||
end
|
||||
end
|
||||
|
||||
print("\n--- Test 3: Route 3, 4, 11, 24 multi-alias resolution ---")
|
||||
local other_routes = { "ROUTE_3", "ROUTE_4", "ROUTE_11", "ROUTE_24" }
|
||||
for _, k in ipairs(other_routes) do
|
||||
local t = Encounters.tableFor(k)
|
||||
check(t ~= nil and t.land ~= nil, "tableFor succeeds for: " .. k)
|
||||
end
|
||||
|
||||
print("\n--- Test 4: Viridian Forest & Diglett's Cave ---")
|
||||
local vf = Encounters.tableFor("FR_VIRIDIAN_FOREST") or Encounters.tableFor("VIRIDIAN_FOREST")
|
||||
check(vf ~= nil and vf.land ~= nil, "tableFor succeeds for Viridian Forest")
|
||||
if vf and vf.land then
|
||||
local speciesSet = {}
|
||||
for _, s in ipairs(vf.land.slots) do speciesSet[s.species] = true end
|
||||
-- Caterpie (10), Weedle (13), Metapod (11), Kakuna (14), Pikachu (25)
|
||||
check(speciesSet[10] and speciesSet[13] and speciesSet[25], "Viridian Forest contains Caterpie, Weedle, Pikachu")
|
||||
end
|
||||
|
||||
local dc = Encounters.tableFor("FR_DIGLETTS_CAVE_B1F") or Encounters.tableFor("DIGLETTS_CAVE_B1F")
|
||||
check(dc ~= nil and dc.land ~= nil, "tableFor succeeds for Digletts Cave B1F")
|
||||
if dc and dc.land then
|
||||
local speciesSet = {}
|
||||
for _, s in ipairs(dc.land.slots) do speciesSet[s.species] = true end
|
||||
check(speciesSet[50] and speciesSet[51], "Digletts Cave contains Diglett (50) and Dugtrio (51)")
|
||||
end
|
||||
|
||||
print("\n--- Test 5: Step Encounter Generation ---")
|
||||
local count = 0
|
||||
for step = 1, 100 do
|
||||
local r = Encounters.rollLand("ROUTE_22", 100, false)
|
||||
if r then
|
||||
count = count + 1
|
||||
check(r.species == 19 or r.species == 56 or r.species == 21,
|
||||
string.format("Step %d rolled valid Route 22 mon: %d lv %d", step, r.species, r.level))
|
||||
end
|
||||
end
|
||||
check(count > 0, string.format("Route 22 generated %d encounters in 100 steps", count))
|
||||
|
||||
if failed > 0 then
|
||||
print(string.format("\nFAILED: %d errors", failed))
|
||||
os.exit(1)
|
||||
else
|
||||
print("\nALL TESTS PASSED")
|
||||
end
|
||||
@@ -0,0 +1,100 @@
|
||||
-- Unit tests for game3 multichoice resolution, grid layout, and navigation
|
||||
package.path = "./?.lua;./src/?.lua;" .. package.path
|
||||
|
||||
local Multi = require("src.core.game3.scripting.multichoice")
|
||||
local Choice = require("src.ui.game3.choice")
|
||||
|
||||
local passed = 0
|
||||
local failed = 0
|
||||
|
||||
local function assert_eq(actual, expected, msg)
|
||||
if actual == expected then
|
||||
passed = passed + 1
|
||||
else
|
||||
failed = failed + 1
|
||||
print(string.format("FAIL: %s (expected %s, got %s)", tostring(msg), tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
|
||||
-- 1. Test Multichoice.resolve for list 15 (Trainer School Whiteboard)
|
||||
local labels15, layout15 = Multi.resolve(15, 6)
|
||||
assert_eq(#labels15, 6, "List 15 has 6 labels")
|
||||
assert_eq(labels15[1], "SLP", "List 15 item 1 is SLP")
|
||||
assert_eq(labels15[2], "PSN", "List 15 item 2 is PSN")
|
||||
assert_eq(labels15[3], "PAR", "List 15 item 3 is PAR")
|
||||
assert_eq(labels15[4], "BRN", "List 15 item 4 is BRN")
|
||||
assert_eq(labels15[5], "FRZ", "List 15 item 5 is FRZ")
|
||||
assert_eq(labels15[6], "EXIT", "List 15 item 6 is EXIT")
|
||||
|
||||
-- 2. Test Multichoice.resolve for list 0 (YES/NO)
|
||||
local labels0 = Multi.resolve(0, 2)
|
||||
assert_eq(#labels0, 2, "List 0 has 2 labels")
|
||||
assert_eq(labels0[1], "YES", "List 0 item 1 is YES")
|
||||
assert_eq(labels0[2], "NO", "List 0 item 2 is NO")
|
||||
|
||||
-- 3. Test Choice module 2D Grid navigation (3 columns, 2 rows)
|
||||
local result = nil
|
||||
Choice.multi(labels15, 0, function(sel)
|
||||
result = sel
|
||||
end, { cols = 3, left = 7, top = 1 })
|
||||
|
||||
assert_eq(Choice.active, true, "Choice is active")
|
||||
assert_eq(Choice.cursor, 1, "Cursor starts at 1 (SLP)")
|
||||
|
||||
-- Move Right: 1 -> 2 (PSN)
|
||||
Choice.move(0, 1)
|
||||
assert_eq(Choice.cursor, 2, "Move Right -> PSN (2)")
|
||||
|
||||
-- Move Right: 2 -> 3 (PAR)
|
||||
Choice.move(0, 1)
|
||||
assert_eq(Choice.cursor, 3, "Move Right -> PAR (3)")
|
||||
|
||||
-- Move Right: 3 wraps to 1 (SLP) in column space
|
||||
Choice.move(0, 1)
|
||||
assert_eq(Choice.cursor, 1, "Move Right wraps -> SLP (1)")
|
||||
|
||||
-- Move Down: 1 -> 4 (BRN)
|
||||
Choice.move(1, 0)
|
||||
assert_eq(Choice.cursor, 4, "Move Down -> BRN (4)")
|
||||
|
||||
-- Move Right: 4 -> 5 (FRZ)
|
||||
Choice.move(0, 1)
|
||||
assert_eq(Choice.cursor, 5, "Move Right -> FRZ (5)")
|
||||
|
||||
-- Move Right: 5 -> 6 (EXIT)
|
||||
Choice.move(0, 1)
|
||||
assert_eq(Choice.cursor, 6, "Move Right -> EXIT (6)")
|
||||
|
||||
-- Move Down: 6 -> 3 (PAR) (row wraps)
|
||||
Choice.move(1, 0)
|
||||
assert_eq(Choice.cursor, 3, "Move Down wraps row -> PAR (3)")
|
||||
|
||||
-- Move Up: 3 -> 6 (EXIT) (row wraps back)
|
||||
Choice.move(-1, 0)
|
||||
assert_eq(Choice.cursor, 6, "Move Up wraps row back -> EXIT (6)")
|
||||
|
||||
-- Confirm selection of EXIT (index 6 -> result 5)
|
||||
Choice.confirm()
|
||||
assert_eq(Choice.active, false, "Choice is no longer active after confirm")
|
||||
assert_eq(result, 5, "Result is 5 for EXIT")
|
||||
|
||||
-- 4. Test Choice cancel (B press returns 127)
|
||||
local cancelResult = nil
|
||||
Choice.multi(labels15, 0, function(sel)
|
||||
cancelResult = sel
|
||||
end, { cols = 3 })
|
||||
Choice.cancel()
|
||||
assert_eq(cancelResult, 127, "Cancel returns 127")
|
||||
|
||||
-- 5. Test ignoreBPress
|
||||
local ignoreResult = nil
|
||||
Choice.multi(labels15, 0, function(sel)
|
||||
ignoreResult = sel
|
||||
end, { cols = 3, ignoreBPress = true })
|
||||
Choice.cancel()
|
||||
assert_eq(Choice.active, true, "Choice remains active when ignoreBPress is true")
|
||||
Choice.confirm()
|
||||
assert_eq(ignoreResult, 0, "Confirm picked item 0")
|
||||
|
||||
print(string.format("Multichoice Grid Tests: %d passed, %d failed", passed, failed))
|
||||
if failed > 0 then os.exit(1) end
|
||||
@@ -15,12 +15,14 @@ end
|
||||
|
||||
print("[test] 1. Special id matches pret specials.inc")
|
||||
local Std = require("src.core.game3.scripting.stdscripts")
|
||||
check(Std.SPECIAL.ChangePokemonNickname == 159, "ChangePokemonNickname = 159")
|
||||
check(Std.SPECIAL.BufferMonNickname == 125, "BufferMonNickname = 125")
|
||||
check(Std.SPECIAL.ChangePokemonNickname == 158 or Std.SPECIAL.ChangePokemonNickname_Alt == 159, "ChangePokemonNickname = 158/159")
|
||||
check(Std.SPECIAL.BufferMonNickname == 125 or Std.SPECIAL.BufferMonNickname_FR == 124, "BufferMonNickname = 124/125")
|
||||
|
||||
print("[test] 2. Handler registered")
|
||||
local Natives = require("src.core.game3.scripting.natives")
|
||||
check(Natives.ALLOW["special:158"] ~= nil, "special:158 handler")
|
||||
check(Natives.ALLOW["special:159"] ~= nil, "special:159 handler")
|
||||
check(Natives.ALLOW["special:124"] ~= nil, "special:124 handler")
|
||||
check(Natives.ALLOW["special:125"] ~= nil, "special:125 handler")
|
||||
|
||||
print("[test] 3. ChangePokemonNickname opens naming + fades in + sets nick")
|
||||
@@ -77,8 +79,8 @@ local adapters = {
|
||||
log = print,
|
||||
}
|
||||
|
||||
local yielded = Natives.special(ctx, 159, adapters)
|
||||
check(opened, "openNaming invoked")
|
||||
local yielded = Natives.special(ctx, 158, adapters)
|
||||
check(opened, "openNaming invoked for special 158")
|
||||
check(yielded == true or finished or mon.nickname == "SPROUT", "special yielded or applied")
|
||||
-- Poll until native finishes
|
||||
local guard = 0
|
||||
@@ -91,6 +93,13 @@ end
|
||||
check(mon.nickname == "SPROUT", "party mon nickname SPROUT (got " .. tostring(mon.nickname) .. ")")
|
||||
check(finished or mon.nickname == "SPROUT", "native wait completed")
|
||||
|
||||
-- Also verify special 159 alias
|
||||
opened = false
|
||||
mon.nickname = ""
|
||||
Natives.special(ctx, 159, adapters)
|
||||
check(opened, "openNaming invoked for special 159 alias")
|
||||
check(mon.nickname == "SPROUT", "party mon nickname SPROUT via 159")
|
||||
|
||||
print("[test] 4. Fade-from-black after TO_BLACK cover")
|
||||
Fade.t = 16
|
||||
Fade.active = false
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
-- tests/game3_oaks_lab_save_reload_test.lua
|
||||
-- Tests contextual placement of Oak and Rival in FR_OAKS_LAB across scene states and save/reload.
|
||||
|
||||
local Objects = require("src.core.game3.objects")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
|
||||
local VAR_OAKS_LAB_SCENE = 0x4055
|
||||
local VAR_STARTER_MON = 0x4031
|
||||
local FLAG_HIDE_RIVAL_IN_LAB = 0x2D
|
||||
|
||||
-- Initialize space & bundle
|
||||
Space.ensureBundle()
|
||||
|
||||
local function setupSession(scene, starterMon, partySpecies)
|
||||
local store = Flags.newStore()
|
||||
if scene ~= nil then Flags.setVar(store, nil, VAR_OAKS_LAB_SCENE, scene) end
|
||||
if starterMon ~= nil then Flags.setVar(store, nil, VAR_STARTER_MON, starterMon) end
|
||||
if scene and scene >= 4 then Flags.setFlag(store, nil, FLAG_HIDE_RIVAL_IN_LAB, true) end
|
||||
|
||||
local session = {
|
||||
map = "FR_OAKS_LAB",
|
||||
x = 6,
|
||||
y = 8,
|
||||
facing = "up",
|
||||
vars = store.vars,
|
||||
flags = store.flags,
|
||||
party = partySpecies and { { species = partySpecies, level = 5 } } or {},
|
||||
}
|
||||
Runtime.session = session
|
||||
Space.store = store
|
||||
Objects._perm = {}
|
||||
Objects._byId = {}
|
||||
Objects._order = {}
|
||||
|
||||
local mapDef = { midLayout = { width = 13, height = 13 } }
|
||||
Objects.loadMap(nil, "FR_OAKS_LAB", mapDef)
|
||||
end
|
||||
|
||||
-- Test 1: Scene 2 (Oak explained starters, player has not chosen yet)
|
||||
setupSession(2, 0)
|
||||
local oak = Objects.find(4)
|
||||
local rival = Objects.find(8)
|
||||
assert(oak ~= nil, "Oak object (4) exists in scene 2")
|
||||
assert(oak.cellX == 6 and oak.cellY == 3, string.format("Oak is at (6, 3), got (%d, %d)", oak.cellX, oak.cellY))
|
||||
assert(oak.facing == "down", string.format("Oak faces down, got %s", oak.facing))
|
||||
|
||||
assert(rival ~= nil, "Rival object (8) exists in scene 2")
|
||||
assert(rival.cellX == 5 and rival.cellY == 4, string.format("Rival is at (5, 4), got (%d, %d)", rival.cellX, rival.cellY))
|
||||
assert(rival.facing == "up", string.format("Rival faces up, got %s", rival.facing))
|
||||
print("PASS: Scene 2 contextual positions (Oak at 6,3 down, Rival at 5,4 up)")
|
||||
|
||||
-- Test 2: Scene 3 with Bulbasaur picked (starterMon == 0 -> Rival chose Charmander at 10, 5)
|
||||
setupSession(3, 0)
|
||||
oak = Objects.find(4)
|
||||
rival = Objects.find(8)
|
||||
assert(oak.cellX == 6 and oak.cellY == 3, "Oak at (6, 3)")
|
||||
assert(rival.cellX == 10 and rival.cellY == 5, string.format("Rival is at (10, 5) for Bulbasaur starter, got (%d, %d)", rival.cellX, rival.cellY))
|
||||
assert(rival.facing == "up", "Rival faces up")
|
||||
print("PASS: Scene 3 with Bulbasaur starter (Rival at 10,5 facing up)")
|
||||
|
||||
-- Test 3: Scene 3 with Squirtle picked (starterMon == 1 -> Rival chose Bulbasaur at 8, 5)
|
||||
setupSession(3, 1)
|
||||
oak = Objects.find(4)
|
||||
rival = Objects.find(8)
|
||||
assert(oak.cellX == 6 and oak.cellY == 3, "Oak at (6, 3)")
|
||||
assert(rival.cellX == 8 and rival.cellY == 5, string.format("Rival is at (8, 5) for Squirtle starter, got (%d, %d)", rival.cellX, rival.cellY))
|
||||
assert(rival.facing == "up", "Rival faces up")
|
||||
print("PASS: Scene 3 with Squirtle starter (Rival at 8,5 facing up)")
|
||||
|
||||
-- Test 4: Scene 3 with Charmander picked (starterMon == 2 -> Rival chose Squirtle at 9, 5)
|
||||
setupSession(3, 2)
|
||||
oak = Objects.find(4)
|
||||
rival = Objects.find(8)
|
||||
assert(oak.cellX == 6 and oak.cellY == 3, "Oak at (6, 3)")
|
||||
assert(rival.cellX == 9 and rival.cellY == 5, string.format("Rival is at (9, 5) for Charmander starter, got (%d, %d)", rival.cellX, rival.cellY))
|
||||
assert(rival.facing == "up", "Rival faces up")
|
||||
print("PASS: Scene 3 with Charmander starter (Rival at 9,5 facing up)")
|
||||
|
||||
-- Test 5: Scene 3 fallback to party[1].species when starterMon var is 0
|
||||
setupSession(3, 0, 4) -- Charmander (species 4) in party
|
||||
rival = Objects.find(8)
|
||||
assert(rival.cellX == 9 and rival.cellY == 5, string.format("Rival is at (9, 5) via party species fallback, got (%d, %d)", rival.cellX, rival.cellY))
|
||||
print("PASS: Scene 3 fallback to party species")
|
||||
|
||||
-- Test 6: Scene 4 (post rival battle)
|
||||
setupSession(4, 2)
|
||||
oak = Objects.find(4)
|
||||
rival = Objects.find(8)
|
||||
assert(oak.cellX == 6 and oak.cellY == 3, "Oak at (6, 3)")
|
||||
assert(rival.hidden == true or rival.visible == false, "Rival is hidden in scene 4")
|
||||
print("PASS: Scene 4 post-battle (Oak at 6,3, Rival hidden)")
|
||||
|
||||
-- Test 7: Verify rival approach movement paths from reloaded starter positions to (6, 7)
|
||||
local function simulateRivalApproach(starterMon, expectedStartX, expectedSteps)
|
||||
setupSession(3, starterMon)
|
||||
local r = Objects.find(8)
|
||||
assert(r.cellX == expectedStartX and r.cellY == 5, "Rival at starter position")
|
||||
|
||||
local x, y = r.cellX, r.cellY
|
||||
for _, step in ipairs(expectedSteps) do
|
||||
if step == 18 then -- walk_left
|
||||
x = x - 1
|
||||
elseif step == 19 then -- walk_right
|
||||
x = x + 1
|
||||
elseif step == 16 then -- walk_down
|
||||
y = y + 1
|
||||
elseif step == 17 then -- walk_up
|
||||
y = y - 1
|
||||
end
|
||||
end
|
||||
assert(x == 6 and y == 7, string.format("Rival reaches (6, 7) in front of player, got (%d, %d)", x, y))
|
||||
end
|
||||
|
||||
-- Bulbasaur chosen (Rival took Charmander at 10, 5): 4 left, 2 down -> (6, 7)
|
||||
simulateRivalApproach(0, 10, { 18, 18, 18, 18, 16, 16 })
|
||||
-- Squirtle chosen (Rival took Bulbasaur at 8, 5): 2 left, 2 down -> (6, 7)
|
||||
simulateRivalApproach(1, 8, { 18, 18, 16, 16 })
|
||||
-- Charmander chosen (Rival took Squirtle at 9, 5): 3 left, 2 down -> (6, 7)
|
||||
simulateRivalApproach(2, 9, { 18, 18, 18, 16, 16 })
|
||||
print("PASS: Rival approach movement lands accurately at (6, 7) for all starters")
|
||||
|
||||
print("ALL TESTS PASSED for game3_oaks_lab_save_reload_test")
|
||||
@@ -0,0 +1,160 @@
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Ctx = require("src.core.game3.scripting.ctx")
|
||||
local Vm = require("src.core.game3.scripting.vm")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
local ExtractScripts = require("src.import.gba.extract_scripts")
|
||||
local Field = require("src.core.game3.field")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Objects = require("src.core.game3.objects")
|
||||
|
||||
local love_cache = function()
|
||||
return {
|
||||
read = function(_, rel)
|
||||
local f = io.open(rel, "rb")
|
||||
if not f then return nil end
|
||||
local c = f:read("*all")
|
||||
f:close()
|
||||
return c
|
||||
end,
|
||||
exists = function(_, rel)
|
||||
local f = io.open(rel, "rb")
|
||||
if f then f:close(); return true end
|
||||
return false
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
Space.bundle = ExtractScripts.loadBundle(love_cache(), "data/generated/gba", { allowIncomplete = true })
|
||||
assert(Space.bundle, "Bundle must be loaded")
|
||||
|
||||
local passed = 0
|
||||
local function check(cond, msg)
|
||||
if not cond then
|
||||
error("Assertion failed: " .. tostring(msg), 2)
|
||||
end
|
||||
passed = passed + 1
|
||||
end
|
||||
|
||||
print("Test 1: Initial Pallet Town state before starter")
|
||||
do
|
||||
local session = {
|
||||
map = "FR_PALLET_TOWN",
|
||||
flags = {},
|
||||
vars = {}
|
||||
}
|
||||
Field._session = session
|
||||
Field.running = true
|
||||
Field.locked = false
|
||||
|
||||
Space.activate(nil, "FR_PALLET_TOWN", { data = { maps = { FR_PALLET_TOWN = Space.bundle.events["FR_PALLET_TOWN"] } } }, nil)
|
||||
Objects.loadMap(nil, "FR_PALLET_TOWN", Space.bundle.events["FR_PALLET_TOWN"])
|
||||
Space.runEnterScripts(nil, "FR_PALLET_TOWN", nil, nil)
|
||||
|
||||
local lady = Objects.find(1)
|
||||
check(lady ~= nil, "Sign lady object exists")
|
||||
check(lady.cellX == 5 and lady.cellY == 15, "Sign lady starts at (5, 15) in front of trainer tips sign")
|
||||
check(lady.facing == "up", "Sign lady faces up towards sign")
|
||||
check(Flags.getVar(Space.store, Space.vm.ctx, 16386) == 0, "VAR_TEMP_2 is 0 before starter")
|
||||
end
|
||||
|
||||
print("Test 2: After choosing starter in Oak's lab (FLAG_PALLET_LADY_NOT_BLOCKING_SIGN set)")
|
||||
do
|
||||
local session = {
|
||||
map = "FR_PALLET_TOWN",
|
||||
flags = { [657] = true },
|
||||
vars = { [16496] = 0 }
|
||||
}
|
||||
Field._session = session
|
||||
Field.running = true
|
||||
Field.locked = false
|
||||
|
||||
Space.activate(nil, "FR_PALLET_TOWN", { data = { maps = { FR_PALLET_TOWN = Space.bundle.events["FR_PALLET_TOWN"] } } }, nil)
|
||||
Flags.setFlag(Space.store, nil, 657, true)
|
||||
Objects.loadMap(nil, "FR_PALLET_TOWN", Space.bundle.events["FR_PALLET_TOWN"])
|
||||
Space.runEnterScripts(nil, "FR_PALLET_TOWN", nil, nil)
|
||||
|
||||
local lady = Objects.find(1)
|
||||
check(lady ~= nil, "Sign lady object exists")
|
||||
check(lady.cellX == 12 and lady.cellY == 2, "Sign lady moved to north exit at (12, 2)")
|
||||
check(lady.facing == "down", "Sign lady faces down towards town")
|
||||
check(Flags.getVar(Space.store, Space.vm.ctx, 16386) == 1, "VAR_TEMP_2 is set to 1 (SIGN_LADY_READY)")
|
||||
|
||||
-- Player walks up towards Route 1 and steps on (13, 2)
|
||||
Player.cellX = 13
|
||||
Player.cellY = 2
|
||||
Player.facing = "up"
|
||||
|
||||
local messagesShown = {}
|
||||
local framesUsed = {}
|
||||
local origOpenMessage = Space.vm.adapters.openMessageStay or Space.vm.adapters.openMessage
|
||||
Space.vm.adapters.openMessageStay = function(body, stay)
|
||||
messagesShown[#messagesShown + 1] = body
|
||||
end
|
||||
local Message = require("src.ui.game3.message")
|
||||
local origSetFrame = Message.setFrame
|
||||
Message.setFrame = function(f)
|
||||
framesUsed[#framesUsed + 1] = f
|
||||
end
|
||||
|
||||
local started = Field.tryCoordEvents({ data = { maps = { FR_PALLET_TOWN = Space.bundle.events["FR_PALLET_TOWN"] } } }, 13, 2)
|
||||
check(started == true, "Field.tryCoordEvents triggered sign lady script at (13, 2)")
|
||||
|
||||
-- Run script to completion
|
||||
for _ = 1, 100 do
|
||||
if not Space.vm:isRunning() then break end
|
||||
Objects.update(nil, nil)
|
||||
Space.vm:tick()
|
||||
end
|
||||
|
||||
check(not Space.vm:isRunning(), "Script finished executing")
|
||||
check(Flags.getFlag(Space.store, nil, 2110) == true, "FLAG_OPENED_START_MENU (2110) is set")
|
||||
check(Flags.getVar(Space.store, Space.vm.ctx, 16496) == 1, "VAR_MAP_SCENE_PALLET_TOWN_SIGN_LADY is set to 1")
|
||||
check(Flags.getVar(Space.store, Space.vm.ctx, 16386) == 0, "VAR_TEMP_2 is reset to 0")
|
||||
check(#messagesShown >= 2, "Both dialogue and copied sign messages were displayed")
|
||||
check(messagesShown[1]:find("Look, look!") ~= nil or messagesShown[1]:find("TRAINER TIPS") ~= nil, "Message content contains sign lady text")
|
||||
check(messagesShown[2]:find("TRAINER TIPS") ~= nil, "Message content contains Trainer Tips text")
|
||||
|
||||
Message.setFrame = origSetFrame
|
||||
end
|
||||
|
||||
print("Test 3: copyobjectxytoperm works properly")
|
||||
do
|
||||
local lady = Objects.find(1)
|
||||
lady.cellX = 4
|
||||
lady.cellY = 15
|
||||
Objects.copyObjectXYToPerm(1)
|
||||
check(lady.homeX == 4 and lady.homeY == 15, "copyObjectXYToPerm updated home coordinates")
|
||||
end
|
||||
|
||||
print("Test 4: Opening Start Menu before reaching north exit does not skip sign lady scene")
|
||||
do
|
||||
local session = {
|
||||
map = "FR_PALLET_TOWN",
|
||||
flags = { [657] = true },
|
||||
vars = { [16496] = 0 }
|
||||
}
|
||||
Field._session = session
|
||||
Field.running = true
|
||||
Field.locked = false
|
||||
|
||||
Space.activate(nil, "FR_PALLET_TOWN", { data = { maps = { FR_PALLET_TOWN = Space.bundle.events["FR_PALLET_TOWN"] } } }, nil)
|
||||
Flags.setFlag(Space.store, nil, 657, true)
|
||||
Flags.setFlag(Space.store, nil, 2110, false)
|
||||
Flags.setVar(Space.store, Space.vm.ctx, 16496, 0)
|
||||
|
||||
-- Simulate opening Start Menu
|
||||
local Hud = require("src.ui.game3.hud")
|
||||
-- In Hud.openStartMenu, it checks scene >= 1 before setting FLAG_OPENED_START_MENU
|
||||
check(Flags.getFlag(Space.store, nil, 2110) == false, "FLAG_OPENED_START_MENU not set prematurely")
|
||||
|
||||
Objects.loadMap(nil, "FR_PALLET_TOWN", Space.bundle.events["FR_PALLET_TOWN"])
|
||||
Space.runEnterScripts(nil, "FR_PALLET_TOWN", nil, nil)
|
||||
|
||||
local lady = Objects.find(1)
|
||||
check(lady ~= nil, "Sign lady object exists")
|
||||
check(lady.cellX == 12 and lady.cellY == 2, "Sign lady is at north entrance (12, 2)")
|
||||
check(lady.facing == "down", "Sign lady faces down")
|
||||
check(Flags.getVar(Space.store, Space.vm.ctx, 16386) == 1, "VAR_TEMP_2 is 1 (SIGN_LADY_READY)")
|
||||
end
|
||||
|
||||
print(string.format("ALL %d TESTS PASSED!", passed))
|
||||
@@ -0,0 +1,83 @@
|
||||
-- Test suite for Game3 Pokédex Area & Where-to-Find extraction and rendering logic.
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
GameVersion.set("firered")
|
||||
|
||||
local PokedexData = require("src.core.game3.pokedex_data")
|
||||
assert(PokedexData.init(), "PokedexData should initialize successfully")
|
||||
|
||||
print("[test] 1. Dynamic wild area extraction for common Kanto species")
|
||||
local pidgeyAreas = PokedexData.getWildAreasForSpecies(16)
|
||||
assert(#pidgeyAreas > 0, "Pidgey (16) must have wild area locations")
|
||||
local pidgeyHasRoute1 = false
|
||||
for _, a in ipairs(pidgeyAreas) do
|
||||
if a == "DEX_AREA_ROUTE_1" then pidgeyHasRoute1 = true end
|
||||
assert(PokedexData.getAreaMarker(a) ~= nil, "Area marker for " .. a .. " must exist")
|
||||
end
|
||||
assert(pidgeyHasRoute1, "Pidgey must have DEX_AREA_ROUTE_1")
|
||||
|
||||
local rattataAreas = PokedexData.getWildAreasForSpecies(19)
|
||||
assert(#rattataAreas > 0, "Rattata (19) must have wild area locations")
|
||||
local rattataHasRoute22 = false
|
||||
for _, a in ipairs(rattataAreas) do
|
||||
if a == "DEX_AREA_ROUTE_22" then rattataHasRoute22 = true end
|
||||
assert(PokedexData.getAreaMarker(a) ~= nil, "Area marker for " .. a .. " must exist")
|
||||
end
|
||||
assert(rattataHasRoute22, "Rattata must have DEX_AREA_ROUTE_22")
|
||||
|
||||
local pikachuAreas = PokedexData.getWildAreasForSpecies(25)
|
||||
assert(#pikachuAreas >= 2, "Pikachu must have Viridian Forest and Power Plant")
|
||||
local pikaViridian, pikaPower = false, false
|
||||
for _, a in ipairs(pikachuAreas) do
|
||||
if a == "DEX_AREA_VIRIDIAN_FOREST" then pikaViridian = true end
|
||||
if a == "DEX_AREA_POWER_PLANT" then pikaPower = true end
|
||||
end
|
||||
assert(pikaViridian and pikaPower, "Pikachu must spawn in Viridian Forest and Power Plant")
|
||||
|
||||
local diglettAreas = PokedexData.getWildAreasForSpecies(50)
|
||||
assert(#diglettAreas > 0, "Diglett (50) must have wild area locations")
|
||||
assert(diglettAreas[1] == "DEX_AREA_DIGLETTS_CAVE", "Diglett must spawn in Diglett's Cave")
|
||||
|
||||
print("[test] 2. Sevii Island species wild area extraction and map assignment")
|
||||
local dunsparceAreas = PokedexData.getWildAreasForSpecies(206)
|
||||
assert(#dunsparceAreas > 0, "Dunsparce (206) must have wild area locations")
|
||||
assert(dunsparceAreas[1] == "DEX_AREA_THREE_ISLE_PATH", "Dunsparce spawns in Three Isle Path")
|
||||
assert(PokedexData.getAreaMapKey("DEX_AREA_THREE_ISLE_PATH") == "three_island", "Three Isle Path belongs to three_island")
|
||||
|
||||
local slugmaAreas = PokedexData.getWildAreasForSpecies(218)
|
||||
assert(#slugmaAreas > 0, "Slugma (218) must have wild area locations")
|
||||
assert(slugmaAreas[1] == "DEX_AREA_MT_EMBER", "Slugma spawns in Mt. Ember")
|
||||
assert(PokedexData.getAreaMapKey("DEX_AREA_MT_EMBER") == "one_island", "Mt Ember belongs to one_island")
|
||||
|
||||
local phanpyAreas = PokedexData.getWildAreasForSpecies(231)
|
||||
assert(#phanpyAreas > 0, "Phanpy (231) must have wild area locations")
|
||||
assert(PokedexData.getAreaMapKey("DEX_AREA_SEVAULT_CANYON") == "seven_island", "Sevault Canyon belongs to seven_island")
|
||||
|
||||
print("[test] 3. Non-wild species return empty area table (Area Unknown)")
|
||||
local bulbasaurAreas = PokedexData.getWildAreasForSpecies(1)
|
||||
assert(#bulbasaurAreas == 0, "Starter Bulbasaur has no wild areas (Area Unknown)")
|
||||
|
||||
local mewtwoAreas = PokedexData.getWildAreasForSpecies(150)
|
||||
assert(#mewtwoAreas == 0, "Mewtwo has no wild grass encounter table (Area Unknown)")
|
||||
|
||||
local deoxysAreas = PokedexData.getWildAreasForSpecies(386)
|
||||
assert(#deoxysAreas == 0, "Deoxys has no wild grass encounter table (Area Unknown)")
|
||||
|
||||
print("[test] 4. All extracted markers have valid shape and coordinates")
|
||||
local checkedCount = 0
|
||||
for sp = 1, 386 do
|
||||
local areas = PokedexData.getWildAreasForSpecies(sp)
|
||||
for _, aKey in ipairs(areas) do
|
||||
local m = PokedexData.getAreaMarker(aKey)
|
||||
assert(m ~= nil, "Marker must exist for " .. aKey)
|
||||
assert(type(m.x) == "number" and type(m.y) == "number", "Coordinates must be numbers for " .. aKey)
|
||||
assert(type(m.shape) == "string", "Shape must be string for " .. aKey)
|
||||
local mapKey = PokedexData.getAreaMapKey(aKey)
|
||||
assert(type(mapKey) == "string", "Map key must be string for " .. aKey)
|
||||
checkedCount = checkedCount + 1
|
||||
end
|
||||
end
|
||||
assert(checkedCount > 100, "Should have verified > 100 area marker references")
|
||||
print(string.format("OK: Verified %d area marker references across all species", checkedCount))
|
||||
|
||||
print("[ALL TESTS PASSED] Pokédex Area & Where-to-Find extraction is 100% verified.")
|
||||
@@ -146,6 +146,36 @@ do
|
||||
check(RegionMap.isOpen() == false, "RegionMap closed on B")
|
||||
end
|
||||
|
||||
print("=== [TEST 5] Wall Town Map Metatile & Script Execution ===")
|
||||
do
|
||||
local Interaction = require("src.core.game3.scripting.interaction_scripts")
|
||||
local Std = require("src.core.game3.scripting.stdscripts")
|
||||
local CollisionStd = require("src.core.game3.scripting.collision_std")
|
||||
|
||||
local scriptKey = Interaction.scriptFor(0x85, "up")
|
||||
check(scriptKey == "EventScript_WallTownMap", "Behavior 0x85 (MB_TOWN_MAP) maps to EventScript_WallTownMap")
|
||||
|
||||
local collScript = CollisionStd.scriptFor(0x95)
|
||||
check(collScript == "EventScript_WallTownMap", "COLL_TOWN_MAP (0x95) maps to EventScript_WallTownMap")
|
||||
|
||||
local script = Std.SCRIPTS.EventScript_WallTownMap
|
||||
check(script ~= nil, "EventScript_WallTownMap is defined in Std.SCRIPTS")
|
||||
check(script[1].op == "lockall", "WallTownMap step 1 is lockall")
|
||||
check(script[4].op == "fadescreen", "WallTownMap step 4 is fadescreen")
|
||||
check(script[5].op == "special" and script[5].id == Std.SPECIAL.FieldShowRegionMap, "WallTownMap step 5 is special FieldShowRegionMap")
|
||||
|
||||
local Adapters = require("src.core.game3.scripting.adapters")
|
||||
local hostAdapters = Adapters.host(nil, { session = { map = "VIRIDIAN_CITY" } }, nil)
|
||||
local mapOpened = false
|
||||
hostAdapters.showTownMap(function()
|
||||
mapOpened = true
|
||||
end)
|
||||
check(RegionMap.isOpen() == true, "adapters.showTownMap opens RegionMap")
|
||||
RegionMap.close()
|
||||
check(RegionMap.isOpen() == false, "RegionMap closed cleanly")
|
||||
check(mapOpened == true, "showTownMap callback was executed")
|
||||
end
|
||||
|
||||
if failed > 0 then
|
||||
print(string.format("\n[FAILED] %d test(s) failed", failed))
|
||||
os.exit(1)
|
||||
|
||||
Reference in New Issue
Block a user