-- Game3 EventObject instances (localId-keyed). -- Owns idle AI + applymovement tracks; optional host NPC mirror for adapters. -- Player localId 0xFF delegates to game3.player. Talk is Field.interact. local Movement = require("src.core.game3.scripting.movement") local Opcodes = require("src.core.game3.scripting.opcodes") local GfxIds = require("src.core.game3.scripting.gfx_ids") local ModRuntime = require("src.mods.Runtime") local VirtualObjects = require("src.core.game3.virtual_objects") local Objects = {} Objects.PLAYER_LOCAL_ID = Opcodes.LOCALID_PLAYER or 0xFF local CELL = 16 local WALK_FRAMES = 16 -- pokefirered/src/event_object_movement.c:5333 StartRunningAnim local RUN_FRAMES = 8 -- pokefirered/src/event_object_movement.c:9029 UpdateRunSlowAnim local RUN_SLOW_FRAMES = 11 local DELTA = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 }, } Objects._byId = {} -- [localId] = EventObject Objects._order = {} -- stable draw/list order Objects._tracks = {} -- [localId] = movement track Objects._mapId = nil Objects._defs = nil -- original mapDef.objects (for addobject) Objects._bounds = nil -- Permanent template overrides from setobjectxyperm / setobjectmovementtype. -- Survives loadMap within a session (pret objectEventTemplates). Objects._perm = {} -- [mapId] = { [localId] = { x=, y=, movementType= } } Objects._templateMt = {} Objects._logged = false local function log(msg) print("[game3/objects] " .. tostring(msg)) end -- pokefirered/src/overworld.c local function layoutBounds(mapDef) local L = mapDef and mapDef.midLayout local w = L and L.width or 0 local h = L and L.height or 0 if w < 1 or h < 1 then return nil end return { w = w, h = h } end Objects.layoutBounds = layoutBounds local function offMap(bounds, eo) if not bounds then return false end local x = eo.cellX or 0 local y = eo.cellY or 0 return x < 0 or y < 0 or x >= bounds.w or y >= bounds.h end Objects.offMap = offMap local function Collision() return package.loaded["src.core.game3.collision"] or require("src.core.game3.collision") end local function Player() return package.loaded["src.core.game3.player"] or require("src.core.game3.player") end local function Space() return package.loaded["src.core.game3.scripting.space"] end function Objects.isPlayer(localId) local id = tonumber(localId) return id == Objects.PLAYER_LOCAL_ID or id == 0xFF or id == 0x800F end local function facingFromDef(def) local r = tostring(def.facing or def.range or "DOWN"):lower() if r == "up" or r == "down" or r == "left" or r == "right" then return r end if r == "any_dir" or r == "up_down" or r == "left_right" then return "down" end return "down" end local function dirsForRange(range) local r = tostring(range or "DOWN"):upper() if r == "ANY_DIR" then return { "up", "down", "left", "right" } end if r == "UP_DOWN" then return { "up", "down" } end if r == "LEFT_RIGHT" then return { "left", "right" } end if r == "UP" then return { "up" } end if r == "DOWN" then return { "down" } end if r == "LEFT" then return { "left" } end if r == "RIGHT" then return { "right" } end return { "down", "up", "left", "right" } end local function objectVisible(def) local SpaceMod = Space() if SpaceMod and SpaceMod.objectVisible then return SpaceMod.objectVisible(def) end local flag = def.flag if not flag or flag == 0 or flag == 0xFFFF or flag == 65535 then return true end return true end local function newEventObject(def) -- Shallow-copy template so setobjectxy / removeobject cannot poison the -- shared events.lua / mapDef.objects tables for the rest of the session. local src = def or {} def = {} for k, v in pairs(src) do def[k] = v end local lid = tonumber(def.localId or def.index) or 0 local x = tonumber(def.x) or 0 local y = tonumber(def.y) or 0 local mt = tonumber(def.movementType) or 0 local hostMv = GfxIds.hostMovement and GfxIds.hostMovement(mt, def.radius and def.radius.x, def.radius and def.radius.y) or nil local movement = def.movement or (hostMv and hostMv.movement) or "STAY" local range = def.range or (hostMv and hostMv.range) or "DOWN" local radius = def.radius or (hostMv and hostMv.radius) local sprite = def.sprite local resolvedGfx = def.graphicsId or def.graphics do local okS, Space = pcall(require, "src.core.game3.scripting.space") if okS and Space and Space.resolveObjectGraphicsId then local gid = Space.resolveObjectGraphicsId(def) if gid then resolvedGfx = gid end end end if not sprite and resolvedGfx then sprite = GfxIds.spriteFor(resolvedGfx) end local Coll = Collision() local elev = (def.elevation and def.elevation ~= 0 and def.elevation) or (Coll and Coll.elevationAt and Coll.elevationAt(x, y)) or 0 return { localId = lid, def = def, cellX = x, cellY = y, px = x * CELL, py = y * CELL, homeX = x, homeY = y, facing = facingFromDef(def), sprite = sprite or "SPRITE_YOUNGSTER", graphicsId = resolvedGfx, elevation = elev, movementType = mt, movement = movement, range = range, radius = radius or { x = 1, y = 1 }, sight = tonumber(def.sight or def.trainerRange) or 0, trainerType = tonumber(def.trainerType) or 0, scriptKey = def.scriptKey, flag = def.flag, visible = objectVisible(def), hidden = not objectVisible(def), frozen = false, passable = def.passable and true or false, moving = false, progress = 0, stepFrames = WALK_FRAMES, targetX = x, targetY = y, stepFlip = false, animClock = 0, idleTimer = 30 + (lid * 17) % 60, scriptBusy = false, } end function Objects.clear() Objects._byId = {} Objects._order = {} Objects._tracks = {} Objects._mapId = nil Objects._defs = nil Objects._bounds = nil -- src/event_object_movement.c:9225 VirtualObjects.clear() end -- pokefirered/src/overworld.c:405 function Objects.reset() Objects.clear() Objects._perm = {} Objects._templateMt = {} Objects._logged = false VirtualObjects.reset() end function Objects.hasMap() return Objects._mapId ~= nil end --- Re-resolve OBJ_EVENT_GFX_VAR_* after ON_TRANSITION sets VAR_OBJ_GFX_ID_*. function Objects.refreshGraphics() local okS, Space = pcall(require, "src.core.game3.scripting.space") if not (okS and Space and Space.resolveObjectGraphicsId) then return 0 end local n = 0 for _, eo in pairs(Objects._byId or {}) do if eo and eo.def then local gid = Space.resolveObjectGraphicsId(eo.def) if gid and gid ~= eo.graphicsId then eo.graphicsId = gid eo.sprite = GfxIds.spriteFor(gid) or eo.sprite n = n + 1 elseif gid then eo.graphicsId = gid end end end local okFv, FieldView = pcall(require, "src.core.game3.field_view") if okFv and FieldView then FieldView._nativeDirty = true end return n end function Objects.hasActiveTracks() for _, tr in pairs(Objects._tracks) do if tr and not tr.done then return true end end return false end local function rememberPerm(mapId, localId, fields) mapId = mapId or Objects._mapId localId = tonumber(localId) or 0 if not mapId or localId <= 0 then return end local bucket = Objects._perm[mapId] if not bucket then bucket = {} Objects._perm[mapId] = bucket end local row = bucket[localId] if not row then row = {} bucket[localId] = row end for k, v in pairs(fields) do row[k] = v end end Objects.rememberPerm = rememberPerm local function applyPerm(eo, mapId) local bucket = Objects._perm[mapId] local row = bucket and bucket[eo.localId] if not row then return end if row.x ~= nil then eo.cellX = row.x eo.homeX = row.x eo.px = row.x * CELL eo.targetX = row.x if eo.def then eo.def.x = row.x end end if row.y ~= nil then eo.cellY = row.y eo.homeY = row.y eo.py = row.y * CELL eo.targetY = row.y if eo.def then eo.def.y = row.y end end if row.movementType ~= nil then eo.movementType = row.movementType local hostMv = GfxIds.hostMovement and GfxIds.hostMovement(row.movementType) if hostMv then eo.movement = hostMv.movement eo.range = hostMv.range if hostMv.radius then eo.radius = hostMv.radius end 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 --- Spawn EventObjects from mapDef.objects (extract / content). function Objects.loadMap(game, mapId, mapDef) local sameMap = Objects._mapId == mapId -- Never wipe in-flight applymovement on a same-map rebind (host setMap echo). if not sameMap then Objects._tracks = {} Objects._templateMt = {} -- pokefirered/src/overworld.c:405 Objects._perm = {} end Objects._byId = {} Objects._order = {} Objects._mapId = mapId -- Prefer ROM event bundle (source of truth). mapDef.objects is the same -- table reference after attachEventsToMaps and may have been mutated. local defs = nil local Sp = Space() local ev = Sp and Sp.bundle and Sp.bundle.events and Sp.bundle.events[mapId] if ev then defs = ev.objects or ev.objectEvents end if type(defs) ~= "table" then defs = mapDef and mapDef.objects end Objects._defs = defs or {} Objects._bounds = layoutBounds(mapDef) if mapId == "FR_PLAYERS_HOUSE_1F" then for _, def in ipairs(Objects._defs) do if tonumber(def.localId or def.index) == 1 then def.x, def.y = 8, 4 end end end resolveContextualMapObjects(mapId) local announce = ModRuntime.wants("world.npc_spawned") for _, def in ipairs(Objects._defs) do local eo = newEventObject(def) if eo.localId > 0 then applyPerm(eo, mapId) local tmt = Objects._templateMt[eo.localId] if tmt then Objects.setTrainerMovementType(eo, tmt) local face = ({ [7] = "up", [8] = "down", [9] = "left", [10] = "right" })[tmt] if face then eo.facing = face end end Objects._byId[eo.localId] = eo Objects._order[#Objects._order + 1] = eo.localId if announce then ModRuntime.emit("world.npc_spawned", { mapId = mapId, npcId = eo.localId, runtime = eo }) end end end if not Objects._logged then log(string.format("spawned %d objects on %s", #Objects._order, tostring(mapId))) Objects._logged = true else log(string.format("reloaded %d objects on %s%s", #Objects._order, tostring(mapId), sameMap and " (kept tracks)" or "")) end return #Objects._order end function Objects.find(localId) localId = tonumber(localId) or 0 if Objects.isPlayer(localId) then return Player() end return Objects._byId[localId] end -- src/event_object_movement.c:2089-2116 local function on_named_map(mapGroup, mapNum) if mapGroup == nil or mapNum == nil then return true end local ok, MapCatalog = pcall(require, "src.import.gba.map_catalog") if not (ok and type(MapCatalog) == "table" and MapCatalog.mapIdFor) then return true end local engineId = MapCatalog.mapIdFor(tonumber(mapGroup), tonumber(mapNum)) if engineId == nil then return false end return engineId == Objects._mapId end -- src/event_object_movement.c:2089-2101, scrcmd.c:1130 function Objects.setSubpriority(localId, mapGroup, mapNum, subpriority) local eo = Objects._byId[tonumber(localId) or -1] if not eo then return false end if not on_named_map(mapGroup, mapNum) then return false end eo.fixedPriority = true eo.subpriority = tonumber(subpriority) or 0 eo.fixedClass = nil return true end -- src/event_object_movement.c:2104-2116 function Objects.resetSubpriority(localId, mapGroup, mapNum) local eo = Objects._byId[tonumber(localId) or -1] if not eo then return false end if not on_named_map(mapGroup, mapNum) then return false end eo.fixedPriority = nil eo.subpriority = nil eo.fixedClass = nil return true end function Objects.listActive(_mod, _game, _mapId) local ids = {} for _, lid in ipairs(Objects._order) do local eo = Objects._byId[lid] if eo and eo.visible and not eo.hidden then ids[#ids + 1] = lid end end return ids end local VIRT_DIR_FACE = { [1] = "down", [2] = "up", [3] = "left", [4] = "right" } function Objects.forDraw() local list = {} for _, lid in ipairs(Objects._order) do local eo = Objects._byId[lid] if eo and eo.visible and not eo.hidden and not offMap(Objects._bounds, eo) then list[#list + 1] = eo end end -- src/event_object_movement.c:1719 if VirtualObjects.count() > 0 then for _, vo in ipairs(VirtualObjects.list()) do local gid = tonumber(vo.graphicsId) or 0 local vrec = { virtualId = vo.id, cellX = tonumber(vo.x) or 0, cellY = tonumber(vo.y) or 0, elevation = tonumber(vo.elevation) or 3, facing = VIRT_DIR_FACE[tonumber(vo.direction)] or "down", sprite = GfxIds.spriteFor(gid), graphicsId = gid, visible = true, hidden = false, } if not offMap(Objects._bounds, vrec) then list[#list + 1] = vrec end end end return list end --- First visible EventObject standing on (tx, ty), or nil if moving onto it. function Objects.at(tx, ty) tx, ty = tonumber(tx), tonumber(ty) if not tx or not ty then return nil end for _, lid in ipairs(Objects._order) do local eo = Objects._byId[lid] if eo and eo.visible and not eo.hidden then if eo.cellX == tx and eo.cellY == ty then if eo.moving then return nil end return eo end end end return nil end --- True if any non-passable EO occupies (tx,ty) or is stepping onto it. function Objects.blocks(tx, ty, exceptLocalId) exceptLocalId = tonumber(exceptLocalId) for _, lid in ipairs(Objects._order) do if lid ~= exceptLocalId then local eo = Objects._byId[lid] if eo and eo.visible and not eo.hidden and not eo.passable then if eo.cellX == tx and eo.cellY == ty then return true end if eo.moving and eo.targetX == tx and eo.targetY == ty then return true end end end end return false end -- pokefirered/src/event_object_movement.c:4899 function Objects.playerBlocks(tx, ty) local P = Player() if not P then return false end if P.cellX == tx and P.cellY == ty then return true end if P.moving and P.targetX == tx and P.targetY == ty then return true end return false end local function walkPhaseOf(eo) if not eo.moving then return 0 end -- pokefirered/src/event_object_movement.c:7040 MovementAction_DisableAnimation_Step0 if eo.inanimate then return 0 end local frames = eo.stepFrames or WALK_FRAMES local p = eo.animClock % frames local mid = math.floor(frames / 2) return (p >= math.floor(frames / 4) and p < mid + math.floor(frames / 4)) and 1 or 0 end function Objects.walkPhase(eo) return walkPhaseOf(eo) end local function beginStep(eo, tx, ty) eo.moving = true eo.progress = 0 eo.targetX = tx eo.targetY = ty eo.stepFrames = WALK_FRAMES eo.animClock = 0 end local function finishStep(eo, game) eo.cellX = eo.targetX eo.cellY = eo.targetY eo.px = eo.cellX * CELL eo.py = eo.cellY * CELL eo.moving = false eo.progress = 0 eo.stepFlip = not eo.stepFlip if eo.def then eo.def.x, eo.def.y = eo.cellX, eo.cellY end local Coll = Collision() local curElev = Coll and Coll.elevationAt and Coll.elevationAt(eo.cellX, eo.cellY) if curElev and curElev ~= 0 and curElev ~= 15 then eo.elevation = curElev end if eo.sight and eo.sight > 0 and not eo.scriptBusy and not eo.frozen then local okTs, TrainerSight = pcall(require, "src.core.game3.trainer_sight") if okTs and TrainerSight and TrainerSight.check then TrainerSight.check(game, eo) end end end local function tickMotion(eo, game) if not eo.moving then return false end eo.progress = eo.progress + 1 eo.animClock = eo.animClock + 1 local frames = eo.stepFrames or WALK_FRAMES local dx = eo.targetX - eo.cellX local dy = eo.targetY - eo.cellY local t = math.min(1, eo.progress / frames) eo.px = eo.cellX * CELL + dx * CELL * t eo.py = eo.cellY * CELL + dy * CELL * t if eo.progress >= frames then finishStep(eo, game) return true end return false end --- Scripted one-cell step (no collision — FRLG applymovement forces). function Objects.scriptStep(eo, dir, run, slow) if not eo then return false end local P = Player() if eo == P then return P.scriptStep and P.scriptStep(dir, run, slow) end if eo.moving then return false end local d = DELTA[dir] if not d then return false end -- pokefirered/src/event_object_movement.c:6796 MovementAction_LockFacingDirection_Step0 if not eo.facingLocked then eo.facing = dir end beginStep(eo, eo.cellX + d[1], eo.cellY + d[2]) -- pokefirered/src/event_object_movement.c:5333 StartRunningAnim if run then eo.stepFrames = slow and RUN_SLOW_FRAMES or RUN_FRAMES end eo.frozen = true eo.scriptBusy = true return true end function Objects.scriptJump(eo, dir, distance) if not eo then return false end local P = Player() if eo == P then return P.scriptJump and P.scriptJump(dir, distance) end if eo.moving then return false end distance = distance or 1 local d = DELTA[dir] if not d then return false end eo.facing = dir beginStep(eo, eo.cellX + d[1] * distance, eo.cellY + d[2] * distance) eo.frozen = true eo.scriptBusy = true return true end function Objects.scriptFace(eo, dir) if not eo then return end local P = Player() if eo == P then if P.scriptFace then P.scriptFace(dir) else P.facing = dir end return end -- pokefirered/src/event_object_movement.c:2501 SetObjectEventDirection if eo.facingLocked then return end eo.facing = dir end -- pokefirered/src/event_object_movement.c:5208 GetOppositeDirection local OPPOSITE_DIR = { down = "up", up = "down", left = "right", right = "left" } -- pokefirered/src/event_object_movement.c:4789 GetDirectionToFace local function directionToFace(x1, y1, x2, y2) if x1 > x2 then return "left" end if x1 < x2 then return "right" end if y1 > y2 then return "up" end return "down" end local function advanceTrack(lid, tr, game) if tr.done then return end local eo = Objects.find(lid) if tr.sleep and tr.sleep > 0 then tr.sleep = tr.sleep - 1 if tr.sleep > 0 then return end end -- Wait until current step finishes. if eo and eo.moving then return end if eo == Player() and Player().moving then return end local act = tr.actions[tr.i] if not act then tr.done = true if eo and eo ~= Player() then eo.scriptBusy = false end if tr.onDone then local cb = tr.onDone tr.onDone = nil cb() end return end tr.i = tr.i + 1 if type(act) == "string" then local sdir = act:match("^walk_(.*)$") or act:match("^step_(.*)$") if sdir then act = { kind = "step", dir = sdir } else local tdir = act:match("^turn_(.*)$") or act:match("^face_(.*)$") if tdir then act = { kind = "turn", dir = tdir } end end end if type(act) == "table" then if act.kind == "step" then if eo == Player() then if Player().scriptStep then Player().scriptStep(act.dir, act.run, act.slow) end elseif eo then Objects.scriptStep(eo, act.dir, act.run, act.slow) end elseif act.kind == "jump" then if eo == Player() then if Player().scriptJump then Player().scriptJump(act.dir, act.distance or 1) elseif Player().scriptStep then for _ = 1, (act.distance or 1) do Player().scriptStep(act.dir) end end elseif eo then if Objects.scriptJump then Objects.scriptJump(eo, act.dir, act.distance or 1) else Objects.scriptStep(eo, act.dir) end end elseif act.kind == "turn" then Objects.scriptFace(eo, act.dir) elseif act.kind == "face_player" then -- pokefirered/src/event_object_movement.c:6772 MovementAction_FacePlayer_Step0 if eo and eo ~= Player() then local dir = directionToFace(eo.cellX, eo.cellY, Player().cellX, Player().cellY) if act.away then dir = OPPOSITE_DIR[dir] end Objects.scriptFace(eo, dir) end elseif act.kind == "lock_facing" then -- pokefirered/src/event_object_movement.c:6796 MovementAction_LockFacingDirection_Step0 if eo and eo ~= Player() then eo.facingLocked = act.locked and true or false end elseif act.kind == "animate" then -- pokefirered/src/event_object_movement.c:7040 MovementAction_DisableAnimation_Step0 if eo and eo ~= Player() then eo.inanimate = act.inanimate and true or false end elseif act.kind == "remove_obstacle" then -- pokefirered/src/event_object_movement.c:7135 MovementAction_RockSmashBreak_Step0 tr.sleep = act.frames or 32 elseif act.kind == "face_original" then if eo and eo ~= Player() and eo.def then local origFace = facingFromDef(eo.def) Objects.scriptFace(eo, origFace) end elseif act.kind == "bow" then if eo and eo ~= Player() then eo.bowFrames = act.frames or 48 eo.facing = "down" end tr.sleep = act.frames or 48 elseif act.kind == "emote" then if eo then local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects") if okFx and FieldEffects then if FieldEffects.startEmote then FieldEffects.startEmote(eo, act.emoteType or "exclamation") elseif FieldEffects.startExclamation then FieldEffects.startExclamation(eo) end end end tr.sleep = act.frames or 60 elseif act.kind == "sleep" then tr.sleep = act.frames or 1 elseif act.kind == "hide" then if eo and eo ~= Player() then eo.hidden = true eo.visible = false end elseif act.kind == "show" then if eo and eo ~= Player() then eo.hidden = false eo.visible = true end end end end function Objects.applyMovement(localId, stream, onDone) local lid = tonumber(localId) or localId local actions = Movement.actionsFromBytes(stream) local eo = Objects.find(lid) if eo and eo ~= Player() then eo.frozen = true eo.scriptBusy = true end Objects._tracks[lid] = { actions = actions, i = 1, sleep = 0, done = false, onDone = onDone, } advanceTrack(lid, Objects._tracks[lid], nil) end function Objects.startTrack(localId, actions, onDone) local lid = tonumber(localId) or localId local eo = Objects.find(lid) if eo and eo ~= Player() then eo.frozen = true eo.scriptBusy = true end if not actions or #actions == 0 then if eo and eo ~= Player() then eo.scriptBusy = false end if onDone then onDone() end return end Objects._tracks[lid] = { actions = actions, i = 1, sleep = 0, done = false, onDone = onDone, } advanceTrack(lid, Objects._tracks[lid], nil) end function Objects.pollMovement(localId) -- Tracks advance in Objects.update. Missing track ≠ done: returning true -- here after loadMap wiped tracks was skipping Bill's walk_up / MeetCelio. local lid = tonumber(localId) or localId if lid == 0 then local any = false for _, tr in pairs(Objects._tracks) do any = true if not tr.done then return false end end return any end local tr = Objects._tracks[lid] if not tr then return false end return tr.done == true end function Objects.clearMovements() Objects._tracks = {} end local function sine(i) local v = 256 * math.sin((i % 256) * math.pi / 128) return v >= 0 and math.floor(v + 0.5) or -math.floor(-v + 0.5) end -- src/event_object_movement.c:7812 local function raiseHandTick(eo) local rh = eo.raiseHandState if not rh then rh = { mode = 0, angle = 0, hops = 0, timer = 0, swing = 0 } eo.raiseHandState = rh eo.raiseHand = true end local mt = tonumber(eo.movementType) or 0 if mt == 0x4F then rh.swing = (rh.swing + 4) % 256 eo.raiseX = math.floor(sine(rh.swing) / 128) return end if mt ~= 0x4E then return end if rh.mode == 0 then rh.angle = rh.angle + 10 if rh.angle > 127 then rh.angle = 0 rh.hops = rh.hops + 1 rh.mode = rh.hops eo.raiseHand = false end eo.raiseY = -math.floor(3 * sine(rh.angle) / 128) elseif rh.mode == 1 then rh.timer = rh.timer + 1 if rh.timer > 16 then rh.timer = 0 eo.raiseHand = true rh.mode = 0 end else rh.timer = rh.timer + 1 if rh.timer > 80 then eo.raiseHandState = nil end end end local function idleTick(eo, game, ctx) if eo.frozen or eo.scriptBusy or eo.moving or eo.hidden or not eo.visible then return end local Field = package.loaded["src.core.game3.field"] if Field and Field.locked then return end local Hud = package.loaded["src.ui.game3.hud"] if Hud and Hud.isMenuOpen and Hud.isMenuOpen() then return end if eo.movement == "RAISE_HAND" then raiseHandTick(eo) return end eo.idleTimer = (eo.idleTimer or 0) - 1 if eo.idleTimer > 0 then return end local mv = tostring(eo.movement or "STAY"):upper() if mv == "STAY" then eo.idleTimer = 120 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 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 TrainerSight.check(game, eo) end end return end if mv == "WALK" then 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 + Rng.compat(0, 29) return end local ok -- pokefirered/src/event_object_movement.c:4830 GetCollisionAtCoords if ctx then local Coll = Collision() ok = ctx.canEnter(tx, ty, eo.cellX, eo.cellY, dir) and not ctx.blocks(tx, ty, eo.localId) if ok and eo.mapDef and Coll.directionallyImpassableOn( eo.mapDef, eo.cellX, eo.cellY, tx, ty, dir) then ok = false end else local Coll = Collision() -- pokefirered/src/event_object_movement.c:8346 IsElevationMismatchAt local onWater = Coll.isWater(eo.cellX, eo.cellY) ok = Coll.canEnter(game, tx, ty, { fromX = eo.cellX, fromY = eo.cellY, dir = dir, surfing = onWater }) if ok and Coll.isWater(tx, ty) ~= onWater then ok = false end if Objects.playerBlocks(tx, ty) then ok = false end if Objects.blocks(tx, ty, eo.localId) then ok = false end end if ok then eo.facing = dir beginStep(eo, tx, ty) else eo.facing = dir -- turn toward blocked anyway if not ctx 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 TrainerSight.check(game, eo) end end end eo.idleTimer = 40 + Rng.compat(0, 49) end end function Objects.update(game) -- Advance script tracks then motion + idle. for lid, tr in pairs(Objects._tracks) do advanceTrack(lid, tr, game) end for _, lid in ipairs(Objects._order) do local eo = Objects._byId[lid] if eo then if eo.bowFrames and eo.bowFrames > 0 then eo.bowFrames = eo.bowFrames - 1 if eo.bowFrames <= 0 then eo.bowFrames = nil end end tickMotion(eo, game) idleTick(eo, game) end end end function Objects.spawnFromDefs(defs, mapDef) local pool = { byId = {}, order = {}, bounds = layoutBounds(mapDef), mapDef = mapDef } for _, def in ipairs(defs or {}) do local eo = newEventObject(def) if eo.localId > 0 then eo.mapDef = mapDef pool.byId[eo.localId] = eo pool.order[#pool.order + 1] = eo.localId end end return pool end function Objects.tickPool(pool, game, ctx) if type(pool) ~= "table" then return end for _, lid in ipairs(pool.order or {}) do local eo = pool.byId[lid] if eo then if eo.bowFrames and eo.bowFrames > 0 then eo.bowFrames = eo.bowFrames - 1 if eo.bowFrames <= 0 then eo.bowFrames = nil end end tickMotion(eo, game) idleTick(eo, game, ctx) end end end function Objects.poolForDraw(pool) local list = {} if type(pool) ~= "table" then return list end for _, lid in ipairs(pool.order or {}) do local eo = pool.byId[lid] if eo and eo.visible and not eo.hidden and not offMap(pool.bounds, eo) then list[#list + 1] = eo end end return list end function Objects.snapshotPool() return { byId = Objects._byId, order = Objects._order, mapId = Objects._mapId, bounds = Objects._bounds, } end function Objects.adoptPool(pool) if type(pool) ~= "table" then return false end for _, lid in ipairs(pool.order or {}) do local live = Objects._byId[lid] local ghost = pool.byId[lid] local wanders = live and tostring(live.movement or "STAY"):upper() == "WALK" if live and ghost and wanders and not ghost.moving then live.cellX, live.cellY = ghost.cellX, ghost.cellY live.px, live.py = ghost.px, ghost.py live.facing = ghost.facing live.stepFlip = ghost.stepFlip live.idleTimer = ghost.idleTimer if live.def then live.def.x, live.def.y = live.cellX, live.cellY end end end return true end function Objects.addObject(localId) localId = tonumber(localId) or 0 local eo = Objects._byId[localId] if eo then applyPerm(eo, Objects._mapId) eo.hidden = false eo.visible = true if eo.def then eo.def.hidden = false end return true end -- Respawn from template defs. for _, def in ipairs(Objects._defs or {}) do if tonumber(def.localId or def.index) == localId then eo = newEventObject(def) applyPerm(eo, Objects._mapId) eo.hidden = false eo.visible = true Objects._byId[localId] = eo Objects._order[#Objects._order + 1] = localId return true end end return false end --- Re-evaluate hide flags after sidecar load / setflag mid-map. function Objects.refreshVisibility() for _, lid in ipairs(Objects._order) do local eo = Objects._byId[lid] if eo and eo.def then local vis = objectVisible(eo.def) eo.visible = vis eo.hidden = not vis end end end --- pret FlagClear/FlagSet on an object template hide flag. -- clearflag after removeobject must bring the NPC back at perm coords. function Objects.syncFlagVisibility(flagId, hidden) flagId = tonumber(flagId) or 0 if flagId == 0 or flagId == 0xFFFF or flagId == 65535 then return end for _, lid in ipairs(Objects._order) do local eo = Objects._byId[lid] if eo then local f = tonumber(eo.flag) or (eo.def and tonumber(eo.def.flag or eo.def.flagId)) or 0 if f == flagId then if hidden then eo.hidden = true eo.visible = false if eo.def then eo.def.hidden = true end else applyPerm(eo, Objects._mapId) eo.hidden = false eo.visible = true if eo.def then eo.def.hidden = false end end end end end -- Template exists in defs but was never spawned (or despawned from order). if not hidden then for _, def in ipairs(Objects._defs or {}) do local f = tonumber(def.flag or def.flagId) or 0 local lid = tonumber(def.localId or def.index) or 0 if f == flagId and lid > 0 and not Objects._byId[lid] then Objects.addObject(lid) end end end end function Objects.removeObject(localId) localId = tonumber(localId) or 0 local eo = Objects._byId[localId] if not eo then return false end local flag = eo.def and (eo.def.flag or eo.def.flagId) if flag and flag ~= 0 and flag ~= 0xFFFF and flag ~= 65535 then local Space = package.loaded["src.core.game3.scripting.space"] if Space and Space.store then local Flags = require("src.core.game3.scripting.flags") Flags.setFlag(Space.store, nil, flag, true) end end eo.hidden = true eo.visible = false if eo.def then eo.def.hidden = true end Objects._tracks[localId] = nil return true end function Objects.hideObject(localId) return Objects.removeObject(localId) end function Objects.showObject(localId) return Objects.addObject(localId) end function Objects.turnObject(localId, dir) local dirs = { [1] = "down", [2] = "up", [3] = "left", [4] = "right" } local facing = dirs[tonumber(dir) or 0] or ({ down = "down", up = "up", left = "left", right = "right" })[tostring(dir or ""):lower()] local eo = Objects.find(localId) if eo and facing then Objects.scriptFace(eo, facing) end end function Objects.setObjectXY(localId, x, y) local lid = tonumber(localId) or 0 local eo = Objects._byId[lid] x, y = tonumber(x) or 0, tonumber(y) or 0 -- Key perm by script map (Space.mapId) when active — not the previous map's -- Objects._mapId if enter order ever regresses. local Sp = Space() local mapKey = (Sp and Sp.mapId) or Objects._mapId rememberPerm(mapKey, lid, { x = x, y = y }) if not eo then return end eo.cellX, eo.cellY = x, y eo.homeX, eo.homeY = x, y eo.px, eo.py = eo.cellX * CELL, eo.cellY * CELL eo.targetX, eo.targetY = eo.cellX, eo.cellY eo.moving = false 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] mt = tonumber(mt) or 0 local Sp = Space() local mapKey = (Sp and Sp.mapId) or Objects._mapId rememberPerm(mapKey, lid, { movementType = mt }) if not eo then return end Objects.setTrainerMovementType(eo, mt) local face = ({ [7] = "up", [8] = "down", [9] = "left", [10] = "right" })[mt] if face then eo.facing = face end end local function clearRaiseHand(eo) eo.raiseHandState = nil eo.raiseHand = nil eo.raiseX = nil eo.raiseY = nil end -- src/event_object_movement.c:4806 function Objects.setTrainerMovementType(localId, mt) local eo = type(localId) == "table" and localId or Objects._byId[tonumber(localId) or 0] if not eo then return end mt = tonumber(mt) or 0 eo.movementType = mt local hostMv = GfxIds.hostMovement and GfxIds.hostMovement(mt) if hostMv then eo.movement = hostMv.movement eo.range = hostMv.range if hostMv.radius then eo.radius = hostMv.radius end end clearRaiseHand(eo) if eo.movement == "RAISE_HAND" then eo.facing = "down" end end -- src/event_object_movement.c:2640 function Objects.overrideTemplateMovementType(localId, mt) local lid = tonumber(localId) or 0 if lid <= 0 then return end Objects._templateMt[lid] = tonumber(mt) end function Objects.templateMovementType(localId) local lid = tonumber(localId) or 0 local o = Objects._templateMt[lid] if o then return o end for _, def in ipairs(Objects._defs or {}) do if tonumber(def.localId or def.index) == lid then return tonumber(def.movementType) or 0 end end return nil end function Objects.facePlayer(localId, game) local eo = Objects._byId[tonumber(localId) or 0] local P = Player() if not eo or not P then return end local dx = P.cellX - eo.cellX local dy = P.cellY - eo.cellY if math.abs(dx) > math.abs(dy) then eo.facing = dx > 0 and "right" or "left" else eo.facing = dy > 0 and "down" or "up" end end function Objects.freeze(localId) local eo = Objects._byId[tonumber(localId) or 0] if eo then eo.frozen = true end end function Objects.unfreeze(localId) local eo = Objects._byId[tonumber(localId) or 0] if eo and not eo.scriptBusy then eo.frozen = false end end --- No-op. EventObjects are owned by game3; Field.interact + adapters read them -- directly via Objects.find / forDraw. Kept for call-site compatibility. function Objects.syncToHost(_game) end -- Back-compat thin wrappers used by older Objects.addObject(adapters, id) calls. function Objects.addObjectVia(adapters, localId) if Objects.addObject(localId) then return true end if adapters and adapters.addObject then return adapters.addObject(localId) end return false end function Objects.removeObjectVia(adapters, localId) Objects.removeObject(localId) if adapters and adapters.removeObject then return adapters.removeObject(localId) end return true end return Objects