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:
1jamie
2026-09-19 01:29:45 -05:00
parent d95960b2f1
commit aed3bbced7
54 changed files with 1695 additions and 222 deletions
+123 -5
View File
@@ -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]