mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-01 15:41:29 -04:00
2315 lines
74 KiB
Lua
2315 lines
74 KiB
Lua
-- 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
|