-- 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 -- include/constants/event_object_movement.h:81 local MOVEMENT_TYPE_INVISIBLE = 0x4C Objects.MOVEMENT_TYPE_INVISIBLE = MOVEMENT_TYPE_INVISIBLE 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 EMPTY = {} local function fieldBlock() local Profile = package.loaded["src.core.game3.profile"] or require("src.core.game3.profile") local ok, row = pcall(Profile.forSession) return ok and row and row.field or EMPTY end local function isRse() local Profile = package.loaded["src.core.game3.profile"] or require("src.core.game3.profile") local ok, row = pcall(Profile.forSession) return ok and row ~= nil and row.family == "rse" end Objects.isRse = isRse local function canonMt(mt) mt = tonumber(mt) if mt == nil then return nil end local MovementTypes = require("src.core.game3.movement_types") local ok, v = pcall(MovementTypes.canon, require("src.core.GameVersion").get(), mt) return ok and v or mt end Objects.canonMovementType = canonMt local inPlace local function inPlaceTable() if inPlace then return inPlace end local MovementTypes = require("src.core.game3.movement_types") inPlace = {} local dirs = { DOWN = "down", UP = "up", LEFT = "left", RIGHT = "right" } -- pokeemerald/src/event_object_movement.c:4422 local kinds = { { "firered", "MOVEMENT_TYPE_WALK_IN_PLACE_", 16, false }, { "firered", "MOVEMENT_TYPE_WALK_IN_PLACE_FAST_", 8, false }, { "firered", "MOVEMENT_TYPE_JOG_IN_PLACE_", 4, false }, { "emerald", "MOVEMENT_TYPE_WALK_SLOWLY_IN_PLACE_", 32, true }, } for _, k in ipairs(kinds) do for suffix, dir in pairs(dirs) do local id = MovementTypes.canonOf(k[1], k[2] .. suffix) if id then inPlace[id] = { dir = dir, frames = k[3], always = k[4] } end end end return inPlace end local function hostSpec(mt, rangeX, rangeY) local spec = GfxIds.hostMovement(mt, rangeX, rangeY) local ip = inPlaceTable()[tonumber(mt) or -1] if ip and (ip.always or fieldBlock().inPlaceMovementTypes) then spec.movement = "IN_PLACE" spec.face = ip.dir spec.range = ip.dir:upper() spec.frames = ip.frames end return spec end Objects.hostSpec = hostSpec 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 -- include/constants/event_objects.h:195 local OBJ_KIND_CLONE = 255 -- pokefirered/src/overworld.c:410 local function cloneTemplate(def) local t = def.cloneTarget if tonumber(def.kind) ~= OBJ_KIND_CLONE or type(t) ~= "table" then return nil end local mapId = t.mapId if not mapId then local ok, MapCatalog = pcall(require, "src.import.gba.map_catalog") if ok and type(MapCatalog) == "table" and MapCatalog.mapIdFor then mapId = MapCatalog.mapIdFor(tonumber(t.mapGroup), tonumber(t.mapNum)) end end local Sp = Space() local ev = mapId and Sp and Sp.bundle and Sp.bundle.events and Sp.bundle.events[mapId] local defs = ev and (ev.objects or ev.objectEvents) local tpl = type(defs) == "table" and defs[tonumber(t.localId) or 0] if type(tpl) ~= "table" or tonumber(tpl.kind) == OBJ_KIND_CLONE then return nil end return tpl end Objects.cloneTemplate = cloneTemplate local function newEventObject(def, neighbor) -- 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 {} local tpl = cloneTemplate(src) def = {} for k, v in pairs(tpl or src) do def[k] = v end if tpl then def.localId, def.index, def.x, def.y = src.localId, src.index, src.x, src.y def.kind, def.cloneTarget = src.kind, src.cloneTarget 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 rawMt = canonMt(def.movementType) local mt = rawMt or 0 local spec if rawMt then spec = hostSpec(rawMt, def.rangeX, def.rangeY) else local r = def.radius or { x = 1, y = 1 } spec = { movement = def.movement or "STAY", range = def.range or "DOWN", rangeX = r.x, rangeY = r.y, radius = r, } end local facing = (def.facing and facingFromDef(def)) or (rawMt and spec.face) or facingFromDef(def) 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, neighbor) 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 local MapCatalog = package.loaded["src.import.gba.map_catalog"] or (pcall(require, "src.import.gba.map_catalog") and package.loaded["src.import.gba.map_catalog"]) local mg, mn = nil, nil if MapCatalog and MapCatalog.groupNumFor and (def.mapId or Objects._mapId) then mg, mn = MapCatalog.groupNumFor(def.mapId or Objects._mapId) end local eo = { localId = lid, originLocalId = tonumber(def.originLocalId or def.localId or def.index) or lid, originMapId = def.originMapId or def.mapId or Objects._mapId, originMapGroup = tonumber(def.originMapGroup or def.mapGroup) or mg, originMapNum = tonumber(def.originMapNum or def.mapNum) or mn, def = def, cellX = x, cellY = y, px = x * CELL, py = y * CELL, homeX = x, homeY = y, facing = facing, sprite = sprite or "SPRITE_YOUNGSTER", graphicsId = resolvedGfx, elevation = elev, currentElevation = tonumber(def.elevation) or 0, movementType = mt, movement = spec.movement, range = spec.range, radius = spec.radius or { x = spec.rangeX, y = spec.rangeY }, rangeX = spec.rangeX, rangeY = spec.rangeY, spec = spec, seqIndex = 0, 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), -- src/event_object_movement.c:1569 invisible = mt == MOVEMENT_TYPE_INVISIBLE, frozen = false, passable = def.passable and true or false, moving = false, progress = 0, stepFrames = WALK_FRAMES, targetX = x, targetY = y, stepFlip = false, animClock = 0, scriptBusy = false, } if isRse() then Objects.initRseKind(eo) end return eo 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 -- src/event_object_movement.c:4806 local function setSpec(eo, mt) mt = canonMt(mt) or 0 local spec = hostSpec(mt, eo.rangeX, eo.rangeY) spec.rangeX = tonumber(eo.rangeX) or 0 spec.rangeY = tonumber(eo.rangeY) or 0 spec.radius = { x = spec.rangeX, y = spec.rangeY } eo.movementType = tonumber(mt) or 0 eo.spec = spec eo.movement = spec.movement eo.range = spec.range eo.radius = spec.radius eo.seqIndex = 0 eo.idleTimer = nil if isRse() then Objects.initRseKind(eo) end end 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 setSpec(eo, row.movementType) -- src/event_object_movement.c:1378 eo.facing = eo.spec.face -- src/event_object_movement.c:1569 eo.invisible = tonumber(row.movementType) == MOVEMENT_TYPE_INVISIBLE 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 local function spawnFromTemplate(def, mapId) local eo = newEventObject(def) if mapId and (not eo.originMapGroup or not eo.originMapNum) then local okC, MapCatalog = pcall(require, "src.import.gba.map_catalog") if okC and MapCatalog and MapCatalog.groupNumFor then local g, n = MapCatalog.groupNumFor(mapId) if g and n then eo.originMapGroup = g eo.originMapNum = n end end eo.originMapId = mapId end if eo.localId > 0 then applyPerm(eo, mapId) local tmt = Objects._templateMt[eo.localId] if tmt then Objects.setTrainerMovementType(eo, tmt) -- src/event_object_movement.c:1569 eo.invisible = tmt == MOVEMENT_TYPE_INVISIBLE local face = ({ [7] = "up", [8] = "down", [9] = "left", [10] = "right" })[tmt] if face then eo.facing = face end end end return eo end -- src/event_object_movement.c:1651 local function respawnFromTemplate(lid) local tpl for _, def in ipairs(Objects._defs or {}) do if tonumber(def.localId or def.index) == lid then tpl = def end end if not tpl then return nil end local eo = spawnFromTemplate(tpl, Objects._mapId) eo.hidden, eo.visible = false, true if eo.def then eo.def.hidden = false end local present = Objects._byId[lid] ~= nil if not present then for _, id in ipairs(Objects._order) do if id == lid then present = true break end end end Objects._byId[lid] = eo Objects._tracks[lid] = nil if not present then Objects._order[#Objects._order + 1] = lid end if ModRuntime.wants("world.npc_spawned") then ModRuntime.emit("world.npc_spawned", { mapId = Objects._mapId, npcId = lid, runtime = eo }) end return eo 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). Objects._foreign = nil 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 = spawnFromTemplate(def, mapId) if eo.localId > 0 then 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 local FOREIGN_BASE = 0xC0 local function shiftObject(eo, dx, dy) eo.cellX, eo.cellY = (eo.cellX or 0) + dx, (eo.cellY or 0) + dy eo.px, eo.py = (eo.px or 0) + dx * CELL, (eo.py or 0) + dy * CELL if eo.targetX then eo.targetX, eo.targetY = eo.targetX + dx, eo.targetY + dy end if eo.homeX then eo.homeX, eo.homeY = eo.homeX + dx, eo.homeY + dy end end -- pokeemerald/src/fieldmap.c:603 CameraMove, pokeemerald/src/event_object_movement.c:2217 function Objects.carryOut(dx, dy) local carry = { map = Objects._mapId, list = {}, player = nil } local playerTrack = Objects._tracks[Objects.PLAYER_LOCAL_ID] if playerTrack then carry.player = playerTrack end for _, lid in ipairs(Objects._order) do local eo = Objects._byId[lid] local tr = Objects._tracks[lid] if eo and eo.visible and not eo.hidden and ((tr and not tr.done) or eo.foreignMap) then shiftObject(eo, dx, dy) eo.foreignMap = eo.foreignMap or Objects._mapId eo.foreignLid = eo.foreignLid or lid eo.originLocalId = eo.originLocalId or lid eo.originMapId = eo.originMapId or eo.foreignMap if not eo.originMapGroup or not eo.originMapNum then local okC, MapCatalog = pcall(require, "src.import.gba.map_catalog") if okC and MapCatalog and MapCatalog.groupNumFor then local g, n = MapCatalog.groupNumFor(eo.originMapId) if g and n then eo.originMapGroup = g eo.originMapNum = n end end end carry.list[#carry.list + 1] = { eo = eo, track = tr } end end return carry end function Objects.carryIn(carry) if not carry then return end Objects._foreign = {} if carry.player then Objects._tracks[Objects.PLAYER_LOCAL_ID] = carry.player end for i, c in ipairs(carry.list) do local key = FOREIGN_BASE + i - 1 if key >= Objects.PLAYER_LOCAL_ID then break end c.eo.localId = key Objects._byId[key] = c.eo Objects._order[#Objects._order + 1] = key if c.track then Objects._tracks[key] = c.track end Objects._foreign[c.eo.foreignMap .. ":" .. c.eo.foreignLid] = key end end function Objects.foreignKey(mapId, localId) local f = Objects._foreign return f and f[tostring(mapId) .. ":" .. tostring(tonumber(localId) or localId)] or nil 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:1234-1249 GetObjectEventIdByLocalIdAndMap / TryGetObjectEventIdByLocalIdAndMap function Objects.findObjectByLocalIdAndMap(localId, mapGroup, mapNum) localId = tonumber(localId) or 0 if Objects.isPlayer(localId) then return Player() end local g = tonumber(mapGroup) local n = tonumber(mapNum) local okC, MapCatalog = pcall(require, "src.import.gba.map_catalog") local targetMapId = (okC and MapCatalog and g ~= nil and n ~= nil and MapCatalog.mapIdFor(g, n)) or nil for _, lid in ipairs(Objects._order) do local eo = Objects._byId[lid] if eo then local idMatch = (eo.localId == localId) or (eo.originLocalId == localId) or (eo.foreignLid == localId) if idMatch then if g ~= nil and n ~= nil then if (eo.originMapGroup == g and eo.originMapNum == n) or (targetMapId and (eo.originMapId == targetMapId or eo.foreignMap == targetMapId)) then return eo elseif not eo.foreignMap and Objects._mapId == targetMapId and eo.localId == localId then return eo end else return eo end end end end return nil 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:1967, scrcmd.c:1139 function Objects.setSubpriority(localId, mapGroup, mapNum, subpriority) localId = tonumber(localId) or 0 if Objects.isPlayer(localId) then local P = Player() if P then P.fixedPriority = true P.subpriority = tonumber(subpriority) or 0 return true end end local eo = Objects.findObjectByLocalIdAndMap(localId, mapGroup, mapNum) or (on_named_map(mapGroup, mapNum) and Objects._byId[localId] or nil) if not eo then return false end eo.fixedPriority = true eo.subpriority = tonumber(subpriority) or 0 eo.fixedClass = nil return true end -- src/event_object_movement.c:1982, scrcmd.c:1149 function Objects.resetSubpriority(localId, mapGroup, mapNum) localId = tonumber(localId) or 0 if Objects.isPlayer(localId) then local P = Player() if P then P.fixedPriority = nil P.subpriority = nil return true end end local eo = Objects.findObjectByLocalIdAndMap(localId, mapGroup, mapNum) or (on_named_map(mapGroup, mapNum) and Objects._byId[localId] or nil) if not eo 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" } local drawList = {} local vrecs = {} function Objects.forDraw() local list = drawList local n = 0 for _, lid in ipairs(Objects._order) do local eo = Objects._byId[lid] -- src/event_object_movement.c:8014 if eo and eo.visible and not eo.hidden and not eo.invisible and (eo.foreignMap ~= nil or not offMap(Objects._bounds, eo)) then n = n + 1 list[n] = eo end end -- src/event_object_movement.c:1719 for i = 1, VirtualObjects.slots() do local vo = VirtualObjects.nth(i) if vo then local vrec = vrecs[vo.id] if not vrec then vrec = { virtualId = vo.id, visible = true, hidden = false } vrecs[vo.id] = vrec end local gid = tonumber(vo.graphicsId) or 0 vrec.cellX = tonumber(vo.x) or 0 vrec.cellY = tonumber(vo.y) or 0 vrec.elevation = tonumber(vo.elevation) or 3 vrec.facing = VIRT_DIR_FACE[tonumber(vo.direction)] or "down" vrec.sprite = GfxIds.spriteFor(gid) vrec.graphicsId = gid vrec.raiseY = tonumber(vo.y2) or 0 if not offMap(Objects._bounds, vrec) then n = n + 1 list[n] = vrec end end end for i = #list, n + 1, -1 do list[i] = nil 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 -- src/event_object_movement.c:1281 local cx = eo.moving and eo.targetX or eo.cellX local cy = eo.moving and eo.targetY or eo.cellY if cx == tx and cy == ty then return eo end end end return nil end -- pokefirered/src/event_object_movement.c:8432 AreElevationsCompatible function Objects.elevationsCompatible(a, b) a, b = tonumber(a) or 0, tonumber(b) or 0 return a == 0 or b == 0 or a == b end --- True if any non-passable EO occupies (tx,ty) or is stepping onto it. function Objects.blocks(tx, ty, exceptLocalId, elevation) 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 and Objects.elevationsCompatible(elevation, eo.currentElevation) 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 -- pokefirered/src/union_room_player_avatar.c:475 for i = 1, VirtualObjects.slots() do local vo = VirtualObjects.nth(i) if vo and vo.solid == true and tonumber(vo.x) == tx and tonumber(vo.y) == ty then return true end end return false end -- pokefirered/src/event_object_movement.c:4899 function Objects.playerBlocks(tx, ty, elevation) local P = Player() if not P then return false end if not Objects.elevationsCompatible(elevation, P.currentElevation) 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 -- pokefirered/src/event_object_movement.c:8400 function Objects.updateElevation(eo) local Coll = Collision() if not (eo and Coll and Coll.nextElevation) then return end local mapDef = eo.mapDef or Coll._mapDef local cx, cy = eo.cellX, eo.cellY if eo.moving then cx, cy = eo.targetX, eo.targetY end eo.currentElevation = Coll.nextElevation(mapDef, eo.currentElevation or 0, cx, cy, eo.cellX, eo.cellY) end local function beginStep(eo, tx, ty) eo.moving = true eo.progress = 0 eo.targetX = tx eo.targetY = ty Objects.updateElevation(eo) eo.stepFrames = WALK_FRAMES eo.animClock = 0 end local function finishStep(eo, game, ctx) 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 if ctx then return 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 -- src/trainer_see.c:94 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 -- src/event_object_movement.c:8866 local STEP_PIXELS = { [16] = { 1 }, [8] = { 2 }, [6] = { 2, 3, 3 }, [4] = { 4 }, [2] = { 8 }, -- src/event_object_movement.c:9029 [11] = { 1, 2 }, -- src/event_object_movement.c:8984 [24] = { 1, 1, 0 }, -- src/event_object_movement.c:8959 [32] = { 1, 0 }, } local STEP_OFFSETS = {} for frames, pat in pairs(STEP_PIXELS) do local cum, n = {}, 0 for k = 1, frames do n = math.min(CELL, n + pat[(k - 1) % #pat + 1]) cum[k] = n end STEP_OFFSETS[frames] = cum end local function stepOffset(frames, progress, cells) local cum = cells == 1 and STEP_OFFSETS[frames] if cum then return cum[math.min(progress, frames)] end return math.floor(cells * CELL * math.min(progress, frames) / frames) end local function tickMotion(eo, game, ctx) Objects.updateElevation(eo) 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 off = stepOffset(frames, eo.progress, math.max(math.abs(dx), math.abs(dy))) eo.px = eo.cellX * CELL + (dx > 0 and off or dx < 0 and -off or 0) eo.py = eo.cellY * CELL + (dy > 0 and off or dy < 0 and -off or 0) local arc = eo.jumpArc if arc then -- pokeemerald/src/event_object_movement.c:8462 local i = math.floor((eo.progress - 1) / (2 ^ arc.shift)) eo.raiseY = arc.table[i + 1] or 0 if arc.thenFace and eo.progress == math.floor(arc.frames / 2) then eo.facing = arc.thenFace end end if eo.progress >= frames then if arc then eo.jumpArc = nil eo.raiseY = nil end finishStep(eo, game, ctx) return true end return false end --- Scripted one-cell step (no collision — FRLG applymovement forces). function Objects.scriptStep(eo, dir, run, slow, fast) if not eo then return false end local P = Player() if eo == P then return P.scriptStep and P.scriptStep(dir, run, slow, fast) 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 if fast then eo.stepFrames = RUN_FRAMES end eo.frozen = true eo.scriptBusy = true return true end function Objects.scriptJump(eo, dir, distance, opts) 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) if isRse() then Objects.startJumpArc(eo, distance, opts) end eo.frozen = true eo.scriptBusy = true return true end -- pokeemerald/src/event_object_movement.c:8424 local JUMP_Y = { high = { -4, -6, -8, -10, -11, -12, -12, -12, -11, -10, -9, -8, -6, -4, 0, 0 }, low = { 0, -2, -3, -4, -5, -6, -6, -6, -5, -5, -4, -3, -2, 0, 0, 0 }, normal = { -2, -4, -6, -8, -9, -10, -10, -10, -9, -8, -6, -5, -3, -2, 0, 0 }, } Objects.JUMP_Y = JUMP_Y -- pokeemerald/src/event_object_movement.c:8454 function Objects.startJumpArc(eo, distance, opts) opts = opts or {} local frames = distance == 2 and 32 or 16 local kind = opts.type or ((distance == 1) and "normal" or "high") eo.stepFrames = frames eo.jumpArc = { table = JUMP_Y[kind] or JUMP_Y.high, shift = distance == 2 and 1 or 0, frames = frames, thenFace = opts.thenFace, shadow = true, } end -- pokefirered/src/event_object_movement.c:5351 InitNpcForWalkSlower function Objects.pushStep(eo, dir, frames) local d = DELTA[dir] if not eo or not d or eo.moving then return false end eo.facing = dir beginStep(eo, eo.cellX + d[1], eo.cellY + d[2]) eo.stepFrames = frames or WALK_FRAMES * 2 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 if eo and eo.trackWait then if not eo.trackWait(eo) then return end eo.trackWait = nil 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, act.fast) end elseif eo then Objects.scriptStep(eo, act.dir, act.run, act.slow, act.fast) 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, { thenFace = act.thenFace, type = act.jumpType }) else Objects.scriptStep(eo, act.dir) end end elseif act.kind == "step_diagonal" then Objects.scriptDiagonal(eo, act) elseif act.kind == "levitate" then Objects.setLevitate(eo, act.on, act.atTop) elseif act.kind == "figure8" then Objects.startFigure8(eo) elseif act.kind == "lock_anim" then -- pokeemerald/src/event_object_movement.c:8809 if eo and eo ~= Player() then eo.inanimate = act.locked and true or false eo.facingLocked = act.locked and true or false end elseif act.kind == "reflection" then -- pokeemerald/src/event_object_movement.c:6617 if eo then eo.hideReflection = act.hidden and true or false end elseif act.kind == "jump_landing_effect" then -- pokeemerald/src/event_object_movement.c:6444 if eo == Player() then local okF, FxRse = pcall(require, "src.core.game3.field_effects_rse") if okF and FxRse then FxRse.setJumpLandingEffect(act.on) end elseif eo then eo.disableJumpLanding = not act.on end elseif act.kind == "reveal_trainer" then Objects.revealTrainer(eo) 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 -- src/event_object_movement.c:7016 local origFace = eo.def.movementType ~= nil and GfxIds.initialFacing(eo.movementType) or 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 -- pokefirered/src/event_object_movement.c:7054 if eo == Player() then Player().setVisible(false) elseif eo then eo.hidden = true eo.visible = false end elseif act.kind == "show" then -- pokefirered/src/event_object_movement.c:7061 if eo == Player() then Player().setVisible(true) elseif eo 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 checkSight(game, eo) local okTs, TrainerSight = pcall(require, "src.core.game3.trainer_sight") if okTs and TrainerSight and TrainerSight.check then TrainerSight.check(game, eo) end end -- src/event_object_movement.c:4830 local function stepCollision(eo, game, ctx, dir) local d = DELTA[dir] local tx, ty = eo.cellX + d[1], eo.cellY + d[2] -- src/event_object_movement.c:4861 local rx = tonumber(eo.rangeX or (eo.radius and eo.radius.x)) or 0 local ry = tonumber(eo.rangeY or (eo.radius and eo.radius.y)) or 0 if (rx ~= 0 and math.abs(tx - eo.homeX) > rx) or (ry ~= 0 and math.abs(ty - eo.homeY) > ry) then return "range" end local ok 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 -- pokefirered/src/event_object_movement.c:4839 if ok and eo.mapDef and Coll.elevationMismatchOn(eo.mapDef, eo.currentElevation, tx, ty) 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, elevation = eo.currentElevation }) if ok and Coll.isWater(tx, ty) ~= onWater then ok = false end -- pokefirered/src/event_object_movement.c:4841 DoesObjectCollideWithObjectAt if Objects.playerBlocks(tx, ty, eo.currentElevation) then ok = false end if Objects.blocks(tx, ty, eo.localId, eo.currentElevation) then ok = false end end if not ok then return "blocked" end return nil end -- src/event_object_movement.c:3884 local function walkOrInPlace(eo, dir, collision) eo.facing = dir if collision then beginStep(eo, eo.cellX, eo.cellY) else local d = DELTA[dir] beginStep(eo, eo.cellX + d[1], eo.cellY + d[2]) end end -- src/event_object_movement.c:2770 local function playerCellFor(ctx) local P = Player() local px = P.moving and P.targetX or P.cellX local py = P.moving and P.targetY or P.cellY return px - (ctx and ctx.ox or 0), py - (ctx and ctx.oy or 0) end local function trainerCloseToRunningPlayer(eo, ctx) local P = Player() if not (P and P.running) or P.biking or P.surfing then return false end local tt = tonumber(eo.trainerType) or 0 if tt ~= 1 and tt ~= 3 then return false end local r = tonumber(eo.sight) or 0 local px, py = playerCellFor(ctx) return math.abs(px - eo.cellX) <= r and math.abs(py - eo.cellY) <= r end -- src/event_object_movement.c:2801 local function vectorDirection(dx, dy) if math.abs(dx) > math.abs(dy) then return dx < 0 and "left" or "right" end return dy < 0 and "up" or "down" end local function southNorth(dy) return dy < 0 and "up" or "down" end local function westEast(dx) return dx < 0 and "left" or "right" end -- src/data/object_events/movement_type_func_tables.h:185 local FOLLOW = { [0] = vectorDirection, [1] = function(_, dy) return southNorth(dy) end, [2] = function(dx) return westEast(dx) end, [3] = function(dx, dy) local d = vectorDirection(dx, dy) if d == "down" then d = westEast(dx); if d == "right" then d = "up" end elseif d == "right" then d = southNorth(dy); if d == "down" then d = "up" end end return d end, [4] = function(dx, dy) local d = vectorDirection(dx, dy) if d == "down" then d = westEast(dx); if d == "left" then d = "up" end elseif d == "left" then d = southNorth(dy); if d == "down" then d = "up" end end return d end, [5] = function(dx, dy) local d = vectorDirection(dx, dy) if d == "up" then d = westEast(dx); if d == "right" then d = "down" end elseif d == "right" then d = southNorth(dy); if d == "up" then d = "down" end end return d end, [6] = function(dx, dy) local d = vectorDirection(dx, dy) if d == "up" then d = westEast(dx); if d == "left" then d = "down" end elseif d == "left" then d = southNorth(dy); if d == "up" then d = "down" end end return d end, [7] = function(dx, dy) local d = vectorDirection(dx, dy) if d == "right" then d = southNorth(dy) end return d end, [8] = function(dx, dy) local d = vectorDirection(dx, dy) if d == "left" then d = southNorth(dy) end return d end, [9] = function(dx, dy) local d = vectorDirection(dx, dy) if d == "down" then d = westEast(dx) end return d end, [10] = function(dx, dy) local d = vectorDirection(dx, dy) if d == "up" then d = westEast(dx) end return d end, } -- src/event_object_movement.c:2992 local function followDirection(eo, follow, ctx) local px, py = playerCellFor(ctx) local fn = FOLLOW[follow] or FOLLOW[0] return fn(px - eo.cellX, py - eo.cellY) end local idleRng local function pick(t) idleRng = idleRng or require("src.core.game3.rng") return t[(idleRng.Random() % #t) + 1] 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 local mv = tostring(eo.movement or "STAY"):upper() if mv == "STAY" then return end if mv == "IN_PLACE" then -- pokeemerald/src/event_object_movement.c:4422 local frames = eo.spec and eo.spec.frames or WALK_FRAMES beginStep(eo, eo.cellX, eo.cellY) eo.stepFrames = frames return end local spec = eo.spec if not spec or spec.movement ~= mv then spec = { movement = mv, dirs = dirsForRange(eo.range), delays = "MEDIUM", follow = 0 } end if mv == "BACK_FORTH" then -- src/event_object_movement.c:3849 local dir = (eo.seqIndex or 0) ~= 0 and OPPOSITE_DIR[spec.face] or spec.face if (eo.seqIndex or 0) ~= 0 and eo.cellX == eo.homeX and eo.cellY == eo.homeY then eo.seqIndex = 0 dir = OPPOSITE_DIR[dir] end local c = stepCollision(eo, game, ctx, dir) if c == "range" then eo.seqIndex = (eo.seqIndex or 0) + 1 dir = OPPOSITE_DIR[dir] c = stepCollision(eo, game, ctx, dir) end walkOrInPlace(eo, dir, c) return end if mv == "SEQUENCE" then -- src/event_object_movement.c:3949 local idx = eo.seqIndex or 0 if idx == spec.skipFrom and ((spec.skipAxis == "x" and eo.cellX == eo.homeX) or (spec.skipAxis == "y" and eo.cellY == eo.homeY)) then idx = spec.skipFrom + 1 end -- src/event_object_movement.c:3914 if idx == 3 and eo.cellX == eo.homeX and eo.cellY == eo.homeY then idx = 0 end local dir = spec.route[idx + 1] local c = stepCollision(eo, game, ctx, dir) if c == "range" then idx = (idx + 1) % 4 dir = spec.route[idx + 1] c = stepCollision(eo, game, ctx, dir) end eo.seqIndex = idx walkOrInPlace(eo, dir, c) return end if mv == "LOOK" or mv == "LOOK_AROUND" or mv == "ROTATE" then -- src/event_object_movement.c:3044 local close = trainerCloseToRunningPlayer(eo, ctx) if eo.idleTimer == nil then eo.idleTimer = mv == "ROTATE" and 48 or pick(GfxIds.DELAYS[spec.delays or "MEDIUM"]) end eo.idleTimer = eo.idleTimer - 1 if eo.idleTimer > 0 and not close then return end local oldFacing = eo.facing -- src/event_object_movement.c:2992 local dir = close and followDirection(eo, spec.follow or 0, ctx) if not dir then dir = mv == "ROTATE" and spec.next[eo.facing] or pick(spec.dirs) end eo.facing = dir eo.idleTimer = mv == "ROTATE" and 48 or pick(GfxIds.DELAYS[spec.delays or "MEDIUM"]) if not ctx and eo.facing ~= oldFacing and eo.sight and eo.sight > 0 then checkSight(game, eo) end return end if mv == "WALK" then -- src/event_object_movement.c:2716 if eo.idleTimer == nil then eo.idleTimer = pick(GfxIds.DELAYS.MEDIUM) end eo.idleTimer = eo.idleTimer - 1 if eo.idleTimer > 0 then return end -- src/event_object_movement.c:2731 local dir = pick(spec.dirs or dirsForRange(eo.range)) eo.facing = dir eo.idleTimer = pick(GfxIds.DELAYS.MEDIUM) if stepCollision(eo, game, ctx, dir) then if not ctx and eo.sight and eo.sight > 0 then checkSight(game, eo) end return end local d = DELTA[dir] beginStep(eo, eo.cellX + d[1], eo.cellY + d[2]) -- src/event_object_movement.c:8959 if spec.slow then eo.stepFrames = WALK_FRAMES * 2 end end end local trackIds, trackRefs, trackActors = {}, {}, {} function Objects.update(game) local count = 0 for lid, tr in pairs(Objects._tracks) do count = count + 1 trackIds[count], trackRefs[count], trackActors[count] = lid, tr, Objects.find(lid) end -- pokeemerald/src/event_object_movement.c:2167 for i = 1, count do local tr, eo = trackRefs[i], trackActors[i] local lid = (eo and eo.localId) or trackIds[i] trackIds[i], trackRefs[i], trackActors[i] = nil, nil, nil if Objects._tracks[lid] == tr and (not eo or Objects.find(lid) == eo) then advanceTrack(lid, tr, game) end 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) if eo.rseKind or eo.levitate or eo.fig8 then Objects.tickRse(eo) end end end end -- pokeemerald/src/event_object_movement.c:5144 function Objects.scriptDiagonal(eo, act) if not eo or eo == Player() or eo.moving then return false end if not eo.facingLocked then eo.facing = act.dirs and act.dirs[1] or eo.facing end beginStep(eo, eo.cellX + (act.dx or 0), eo.cellY + (act.dy or 0)) if act.slow then eo.stepFrames = WALK_FRAMES * 2 end eo.frozen = true eo.scriptBusy = true return true end -- pokeemerald/src/event_object_movement.c:8895 function Objects.setLevitate(eo, on, atTop) if not eo or eo == Player() then return end if on then eo.levitate = { t = 0, d = -1 } return end if atTop then -- pokeemerald/src/event_object_movement.c:7307 eo.trackWait = function(o) if (o.raiseY or 0) == 0 then o.levitate = nil return true end return false end return end eo.levitate = nil eo.raiseY = nil end -- pokeemerald/src/event_object_movement.c:8347 local FIG8_X = { 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 1, 2, 2, 1, 2, 2, 1, 2, 1, 1, 2, 1, 1, 2, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, } local FIG8_Y = { 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, -1, -1, 0, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -2, } Objects.FIG8_X, Objects.FIG8_Y = FIG8_X, FIG8_Y function Objects.startFigure8(eo) if not eo or eo == Player() then return end eo.fig8 = { i = 0, part = 0, x2 = 0, y2 = 0 } eo.trackWait = function(o) return o.fig8 == nil end end -- pokeemerald/src/event_object_movement.c:8387 local function figure8Step(eo) local f = eo.fig8 local n = #FIG8_X local j = (n - 1) - f.i if f.part == 0 then f.x2, f.y2 = f.x2 + FIG8_X[f.i + 1], f.y2 + FIG8_Y[f.i + 1] elseif f.part == 1 then f.x2, f.y2 = f.x2 - FIG8_X[j + 1], f.y2 + FIG8_Y[j + 1] elseif f.part == 2 then f.x2, f.y2 = f.x2 - FIG8_X[f.i + 1], f.y2 + FIG8_Y[f.i + 1] else f.x2, f.y2 = f.x2 + FIG8_X[j + 1], f.y2 + FIG8_Y[j + 1] end f.i = f.i + 1 if f.i == n then f.i = 0 f.part = f.part + 1 end if f.part == 4 then eo.fig8 = nil eo.raiseX, eo.raiseY = nil, nil return end eo.raiseX, eo.raiseY = f.x2, f.y2 end -- pokeemerald/src/event_object_movement.c:3075 local MT_BERRY_TREE = 0x0C -- pokeemerald/include/constants/event_object_movement.h:61 local MT_TREE_DISGUISE = 0x39 local MT_MOUNTAIN_DISGUISE = 0x3A local MT_BURIED = 0x3F Objects.MT_BERRY_TREE, Objects.MT_BURIED = MT_BERRY_TREE, MT_BURIED Objects.MT_TREE_DISGUISE, Objects.MT_MOUNTAIN_DISGUISE = MT_TREE_DISGUISE, MT_MOUNTAIN_DISGUISE -- pokeemerald/src/event_object_movement.c:1153 local COPY_TYPES = { [0x35] = { init = "up" }, [0x36] = { init = "down" }, [0x37] = { init = "left" }, [0x38] = { init = "right" }, [0x3B] = { init = "up", grass = true }, [0x3C] = { init = "down", grass = true }, [0x3D] = { init = "left", grass = true }, [0x3E] = { init = "right", grass = true }, } local DIR_IDX = { down = 1, up = 2, left = 3, right = 4 } local IDX_DIR = { "down", "up", "left", "right" } -- pokeemerald/src/event_object_movement.c:1124 local COPY_FOR = { { 2, 1, 4, 3 }, { 1, 2, 3, 4 }, { 3, 4, 2, 1 }, { 4, 3, 1, 2 }, } local COPY_TO = { { 2, 1, 4, 3 }, { 1, 2, 3, 4 }, { 4, 3, 1, 2 }, { 3, 4, 2, 1 }, } -- pokeemerald/src/event_object_movement.c:5012 function Objects.copyDirection(copyInit, playerInit, playerMove) local pi, pm, ci = DIR_IDX[playerInit], DIR_IDX[playerMove], DIR_IDX[copyInit] if not (pi and pm and ci) then return nil end return IDX_DIR[COPY_TO[ci][COPY_FOR[pi][pm]]] end function Objects.initRseKind(eo) local mt = tonumber(eo.movementType) or 0 eo.rseKind = nil local copy = COPY_TYPES[mt] if copy then eo.rseKind = "copy" eo.copy = { init = copy.init, grass = copy.grass } else eo.copy = nil end if mt == MT_BERRY_TREE then eo.rseKind = "berry_tree" eo.invisible = true eo.berryTree = eo.berryTree or { id = tonumber(eo.def and (eo.def.berryTreeId or eo.def.trainerRange)) or 0 } else eo.berryTree = nil end if mt == MT_TREE_DISGUISE or mt == MT_MOUNTAIN_DISGUISE then eo.rseKind = "disguise" -- pokeemerald/src/event_object_movement.c:4354 local sheet = mt == MT_TREE_DISGUISE and "tree_disguise" or "mountain_disguise" if not (eo.disguise and eo.disguise.sheet == sheet) then eo.disguise = { sheet = sheet } end else eo.disguise = nil end if mt == MT_BURIED then -- pokeemerald/src/event_object_movement.c:4390 eo.rseKind = "buried" eo.buried = true eo.invisible = true elseif eo.buried then eo.buried = nil eo.invisible = false end end -- pokeemerald/src/event_object_movement.c:1890 local function setBerryTreeGraphics(eo, bt, tree) local stage = tree and tree.stage or 0 eo.invisible = true bt.visible = false if stage == 0 then return end bt.visible = true local berryStage = stage - 1 local FxRse = require("src.core.game3.field_effects_rse") local m = FxRse.berryManifest() local berry = tonumber(tree.berry) or 1 local entry = m and m.trees and (m.trees[berry] or m.trees[1]) local st = entry and entry.stages and entry.stages[berryStage + 1] if entry and st then bt.tree = entry local cmds = {} for _, a in ipairs(st.anim or {}) do cmds[#cmds + 1] = { "frame", a.frame, a.duration } end cmds[#cmds + 1] = { "jump", 0 } bt.anim = FxRse.anim(cmds) eo.graphicsId = st.gfxId or eo.graphicsId end bt.animNum = berryStage bt.gfxKey = tostring(tree.berry) .. ":" .. tostring(stage) end -- pokeemerald/src/event_object_movement.c:3093 local function berryTreeTick(eo) local bt = eo.berryTree local okB, BerryTrees = pcall(require, "src.core.game3.rse.berry_trees") if not (okB and BerryTrees and bt) then return end local tree = BerryTrees.peek(nil, bt.id) local stage = tree and tree.stage or 0 local FxRse = require("src.core.game3.field_effects_rse") if not bt.init then -- pokeemerald/src/event_object_movement.c:3075 bt.init, bt.func = true, "normal" setBerryTreeGraphics(eo, bt, tree) return end if bt.func == "sparkle" or bt.func == "sparkle_end" then -- pokeemerald/src/event_object_movement.c:3154 bt.timer = bt.timer + 1 bt.visible = math.floor(bt.timer / 2) % 2 == 0 and bt.animNum ~= nil if bt.timer > 64 then if bt.func == "sparkle" then setBerryTreeGraphics(eo, bt, tree) bt.func, bt.timer = "sparkle_end", 0 else bt.func = "normal" bt.visible = stage ~= 0 end end return end if stage == 0 then if not bt.justPicked and bt.animNum == 4 then FxRse.startBerryTreeSparkle(eo.cellX, eo.cellY) end bt.animNum = 0 bt.visible = false return end bt.visible = true if bt.animNum ~= stage - 1 then -- pokeemerald/src/event_object_movement.c:3139 bt.func, bt.timer = "sparkle", 0 FxRse.startBerryTreeSparkle(eo.cellX, eo.cellY) return end if bt.gfxKey ~= tostring(tree.berry) .. ":" .. tostring(stage) then setBerryTreeGraphics(eo, bt, tree) end end local function isPokeGrass(x, y) local Coll = Collision() local MB = require("src.core.game3.mb") local b = Coll and Coll.behavior and Coll.behavior(x, y) return b ~= nil and (b == MB.id("TALL_GRASS") or b == MB.id("LONG_GRASS")) end -- pokeemerald/src/event_object_movement.c:4168 local function copyTick(eo) local c = eo.copy local P = Player() if not (c and P) then return end if not c.playerInit then c.playerInit = P.facing end local moving = P.moving and true or false local started = moving and not c.wasMoving local finished = c.wasMoving and not moving c.wasMoving = moving local okF, Faraway = pcall(require, "src.core.game3.faraway_island") local mew = okF and Faraway and Faraway.isMew(eo) if finished and mew then Faraway.updateStepCounter() end if not started or eo.moving or eo.frozen or eo.scriptBusy then return end local dir if mew then -- pokeemerald/src/event_object_movement.c:4206 dir = Faraway.mewDirection(eo, P) if not dir then eo.facing = Objects.copyDirection(c.init, c.playerInit, P.moveDir or P.facing) or eo.facing return end else dir = Objects.copyDirection(c.init, c.playerInit, P.moveDir or P.facing) end if not dir then return end local d = DELTA[dir] eo.facing = dir if stepCollision(eo, nil, nil, dir) or (c.grass and not isPokeGrass(eo.cellX + d[1], eo.cellY + d[2])) then return end beginStep(eo, eo.cellX + d[1], eo.cellY + d[2]) if P.running or P.biking then eo.stepFrames = RUN_FRAMES end end function Objects.tickRse(eo) if eo.levitate then -- pokeemerald/src/event_object_movement.c:8908 local l = eo.levitate if l.t % 4 == 0 then eo.raiseY = (eo.raiseY or 0) + l.d end if l.t % 16 == 0 then l.d = -l.d end l.t = l.t + 1 end if eo.fig8 then figure8Step(eo) end if eo.rseKind == "berry_tree" then berryTreeTick(eo) end if eo.rseKind == "copy" then copyTick(eo) end end -- pokeemerald/src/event_object_movement.c:6503 function Objects.revealTrainer(eo) if not eo or eo == Player() then return end local mt = tonumber(eo.movementType) or 0 if mt == MT_BURIED then local TrainerSight = require("src.core.game3.trainer_sight") local done = false TrainerSight.revealBuried(eo, function() done = true end) eo.trackWait = function() return done end return end local d = eo.disguise if not d then return end -- pokeemerald/src/field_effect_helpers.c:1380 local FxRse = require("src.core.game3.field_effects_rse") d.a = FxRse.anim(FxRse.animCmds(d.sheet, 2)) d.revealing = true eo.trackWait = function(o) return o.disguise == nil or o.disguise.done == true end end function Objects.spawnFromDefs(defs, mapDef, mapId) local pool = { byId = {}, order = {}, bounds = layoutBounds(mapDef), mapDef = mapDef } local Sp = mapId and Space() local nb = Sp and Sp.neighborObjectState and Sp.neighborObjectState(mapId) if mapId and not nb and not (Sp and Sp.mapId == mapId) then nb = { store = { flags = {}, vars = {} }, perm = {}, movementType = {} } end for _, def in ipairs(defs or {}) do local eo = newEventObject(def, nb) if eo.localId > 0 then eo.mapDef = mapDef if nb then local p = nb.perm[eo.localId] if p then eo.cellX, eo.cellY, eo.homeX, eo.homeY = p.x, p.y, p.x, p.y eo.targetX, eo.targetY = p.x, p.y eo.px, eo.py = p.x * CELL, p.y * CELL eo.def.x, eo.def.y = p.x, p.y end local mt = nb.movementType[eo.localId] if mt then Objects.setTrainerMovementType(eo, mt) -- pokefirered/src/event_object_movement.c:359 eo.facing = GfxIds.initialFacing(mt) end if (tonumber(eo.graphicsId) or 0) >= 240 then eo.invisible = true end end 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, ctx or pool) 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 eo.invisible 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 mv = live and ghost and live.movement == ghost.movement and tostring(live.movement or "STAY"):upper() if mv == "WALK" or mv == "BACK_FORTH" or mv == "SEQUENCE" then live.cellX, live.cellY = ghost.cellX, ghost.cellY live.px, live.py = ghost.px, ghost.py live.targetX, live.targetY = ghost.targetX, ghost.targetY live.moving, live.progress = ghost.moving, ghost.progress live.stepFrames, live.animClock = ghost.stepFrames, ghost.animClock live.facing = ghost.facing live.stepFlip = ghost.stepFlip live.idleTimer = ghost.idleTimer live.seqIndex = ghost.seqIndex if live.def then live.def.x, live.def.y = live.cellX, live.cellY end elseif mv == "LOOK" or mv == "ROTATE" then live.facing = ghost.facing live.idleTimer = ghost.idleTimer end end return true end function Objects.addObject(localId) localId = tonumber(localId) or 0 local eo = Objects._byId[localId] if eo and not eo.hidden then return true end return respawnFromTemplate(localId) ~= nil 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 -- src/event_object_movement.c:1841 local function inCameraView(eo) local P = Player() if not P then return false end local px, py = tonumber(P.cellX), tonumber(P.cellY) if not px or not py then return false end local function inside(x, y) x, y = tonumber(x), tonumber(y) return x ~= nil and y ~= nil and x >= px - 9 and x <= px + 10 and y >= py - 7 and y <= py + 9 end return inside(eo.cellX, eo.cellY) or inside(eo.homeX, eo.homeY) end Objects.inCameraView = inCameraView --- pret FlagClear/FlagSet on an object template hide flag. -- src/scrcmd.c:558 function Objects.syncFlagVisibility(flagId, hidden, force) 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 if force or not inCameraView(eo) then eo.hidden = true eo.visible = false if eo.def then eo.def.hidden = true end end elseif eo.hidden then -- src/event_object_movement.c:1792 respawnFromTemplate(lid) 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 -- pokeemerald/src/event_object_movement.c:1939 SetObjectInvisibility function Objects.hideObjectAt(localId, mapGroup, mapNum) localId = tonumber(localId) or 0 if Objects.isPlayer(localId) then local P = Player() if P and P.setVisible then P.setVisible(false) end return true end local eo = Objects.findObjectByLocalIdAndMap(localId, mapGroup, mapNum) or (on_named_map(mapGroup, mapNum) and Objects._byId[localId] or nil) if not eo then return false end eo.invisible = true eo.hidden = true eo.visible = false if eo.def then eo.def.hidden = true end Objects._tracks[eo.localId] = nil return true end function Objects.showObjectAt(localId, mapGroup, mapNum) localId = tonumber(localId) or 0 if Objects.isPlayer(localId) then local P = Player() if P and P.setVisible then P.setVisible(true) end return true end local eo = Objects.findObjectByLocalIdAndMap(localId, mapGroup, mapNum) or (on_named_map(mapGroup, mapNum) and Objects._byId[localId] or nil) if not eo then if on_named_map(mapGroup, mapNum) then return Objects.addObject(localId) end return false end eo.invisible = false eo.hidden = false eo.visible = true if eo.def then eo.def.hidden = false end return true end function Objects.hideObject(localId) return Objects.hideObjectAt(localId, nil, nil) end function Objects.showObject(localId) return Objects.showObjectAt(localId, nil, nil) 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 = canonMt(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 = canonMt(mt) or 0 setSpec(eo, mt) clearRaiseHand(eo) -- src/event_object_movement.c:4543 if mt == MOVEMENT_TYPE_INVISIBLE then eo.invisible = true end 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] = canonMt(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