mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-01 23:42:25 -04:00
1632 lines
51 KiB
Lua
1632 lines
51 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 function objectVisible(def)
|
|
local SpaceMod = Space()
|
|
if SpaceMod and SpaceMod.objectVisible then
|
|
return SpaceMod.objectVisible(def)
|
|
end
|
|
local flag = def.flag
|
|
if not flag or flag == 0 or flag == 0xFFFF or flag == 65535 then
|
|
return true
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function newEventObject(def, 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 {}
|
|
def = {}
|
|
for k, v in pairs(src) do
|
|
def[k] = v
|
|
end
|
|
local lid = tonumber(def.localId or def.index) or 0
|
|
local x = tonumber(def.x) or 0
|
|
local y = tonumber(def.y) or 0
|
|
local rawMt = tonumber(def.movementType)
|
|
local mt = rawMt or 0
|
|
local spec
|
|
if rawMt then
|
|
spec = GfxIds.hostMovement(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
|
|
return {
|
|
localId = lid,
|
|
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,
|
|
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,
|
|
}
|
|
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)
|
|
local spec = GfxIds.hostMovement(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
|
|
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
|
|
|
|
--- Spawn EventObjects from mapDef.objects (extract / content).
|
|
function Objects.loadMap(game, mapId, mapDef)
|
|
local sameMap = Objects._mapId == mapId
|
|
-- Never wipe in-flight applymovement on a same-map rebind (host setMap echo).
|
|
if not sameMap then
|
|
Objects._tracks = {}
|
|
Objects._templateMt = {}
|
|
-- pokefirered/src/overworld.c:405
|
|
Objects._perm = {}
|
|
end
|
|
Objects._byId = {}
|
|
Objects._order = {}
|
|
Objects._mapId = mapId
|
|
-- Prefer ROM event bundle (source of truth). mapDef.objects is the same
|
|
-- table reference after attachEventsToMaps and may have been mutated.
|
|
local defs = nil
|
|
local Sp = Space()
|
|
local ev = Sp and Sp.bundle and Sp.bundle.events and Sp.bundle.events[mapId]
|
|
if ev then
|
|
defs = ev.objects or ev.objectEvents
|
|
end
|
|
if type(defs) ~= "table" then
|
|
defs = mapDef and mapDef.objects
|
|
end
|
|
Objects._defs = defs or {}
|
|
Objects._bounds = layoutBounds(mapDef)
|
|
if mapId == "FR_PLAYERS_HOUSE_1F" then
|
|
for _, def in ipairs(Objects._defs) do
|
|
if tonumber(def.localId or def.index) == 1 then
|
|
def.x, def.y = 8, 4
|
|
end
|
|
end
|
|
end
|
|
resolveContextualMapObjects(mapId)
|
|
local announce = ModRuntime.wants("world.npc_spawned")
|
|
for _, def in ipairs(Objects._defs) do
|
|
local eo = newEventObject(def)
|
|
if eo.localId > 0 then
|
|
applyPerm(eo, mapId)
|
|
local tmt = Objects._templateMt[eo.localId]
|
|
if tmt then
|
|
Objects.setTrainerMovementType(eo, tmt)
|
|
-- 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
|
|
Objects._byId[eo.localId] = eo
|
|
Objects._order[#Objects._order + 1] = eo.localId
|
|
if announce then
|
|
ModRuntime.emit("world.npc_spawned", { mapId = mapId, npcId = eo.localId, runtime = eo })
|
|
end
|
|
end
|
|
end
|
|
if not Objects._logged then
|
|
log(string.format("spawned %d objects on %s", #Objects._order, tostring(mapId)))
|
|
Objects._logged = true
|
|
else
|
|
log(string.format("reloaded %d objects on %s%s", #Objects._order, tostring(mapId),
|
|
sameMap and " (kept tracks)" or ""))
|
|
end
|
|
return #Objects._order
|
|
end
|
|
|
|
function Objects.find(localId)
|
|
localId = tonumber(localId) or 0
|
|
if Objects.isPlayer(localId) then
|
|
return Player()
|
|
end
|
|
return Objects._byId[localId]
|
|
end
|
|
|
|
-- src/event_object_movement.c:2089-2116
|
|
local function on_named_map(mapGroup, mapNum)
|
|
if mapGroup == nil or mapNum == nil then return true end
|
|
local ok, MapCatalog = pcall(require, "src.import.gba.map_catalog")
|
|
if not (ok and type(MapCatalog) == "table" and MapCatalog.mapIdFor) then return true end
|
|
local engineId = MapCatalog.mapIdFor(tonumber(mapGroup), tonumber(mapNum))
|
|
if engineId == nil then return false end
|
|
return engineId == Objects._mapId
|
|
end
|
|
|
|
-- src/event_object_movement.c:2089-2101, scrcmd.c:1130
|
|
function Objects.setSubpriority(localId, mapGroup, mapNum, subpriority)
|
|
local eo = Objects._byId[tonumber(localId) or -1]
|
|
if not eo then return false end
|
|
if not on_named_map(mapGroup, mapNum) then return false end
|
|
eo.fixedPriority = true
|
|
eo.subpriority = tonumber(subpriority) or 0
|
|
eo.fixedClass = nil
|
|
return true
|
|
end
|
|
|
|
-- src/event_object_movement.c:2104-2116
|
|
function Objects.resetSubpriority(localId, mapGroup, mapNum)
|
|
local eo = Objects._byId[tonumber(localId) or -1]
|
|
if not eo then return false end
|
|
if not on_named_map(mapGroup, mapNum) then return false end
|
|
eo.fixedPriority = nil
|
|
eo.subpriority = nil
|
|
eo.fixedClass = nil
|
|
return true
|
|
end
|
|
|
|
function Objects.listActive(_mod, _game, _mapId)
|
|
local ids = {}
|
|
for _, lid in ipairs(Objects._order) do
|
|
local eo = Objects._byId[lid]
|
|
if eo and eo.visible and not eo.hidden then
|
|
ids[#ids + 1] = lid
|
|
end
|
|
end
|
|
return ids
|
|
end
|
|
|
|
local VIRT_DIR_FACE = { [1] = "down", [2] = "up", [3] = "left", [4] = "right" }
|
|
|
|
function Objects.forDraw()
|
|
local list = {}
|
|
for _, lid in ipairs(Objects._order) do
|
|
local eo = Objects._byId[lid]
|
|
-- src/event_object_movement.c:8014
|
|
if eo and eo.visible and not eo.hidden and not eo.invisible
|
|
and not offMap(Objects._bounds, eo) then
|
|
list[#list + 1] = eo
|
|
end
|
|
end
|
|
-- src/event_object_movement.c:1719
|
|
if VirtualObjects.count() > 0 then
|
|
for _, vo in ipairs(VirtualObjects.list()) do
|
|
local gid = tonumber(vo.graphicsId) or 0
|
|
local vrec = {
|
|
virtualId = vo.id,
|
|
cellX = tonumber(vo.x) or 0,
|
|
cellY = tonumber(vo.y) or 0,
|
|
elevation = tonumber(vo.elevation) or 3,
|
|
facing = VIRT_DIR_FACE[tonumber(vo.direction)] or "down",
|
|
sprite = GfxIds.spriteFor(gid),
|
|
graphicsId = gid,
|
|
visible = true,
|
|
hidden = false,
|
|
}
|
|
if not offMap(Objects._bounds, vrec) then list[#list + 1] = vrec end
|
|
end
|
|
end
|
|
return list
|
|
end
|
|
|
|
--- First visible EventObject standing on (tx, ty), or nil if moving onto it.
|
|
function Objects.at(tx, ty)
|
|
tx, ty = tonumber(tx), tonumber(ty)
|
|
if not tx or not ty then return nil end
|
|
for _, lid in ipairs(Objects._order) do
|
|
local eo = Objects._byId[lid]
|
|
if eo and eo.visible and not eo.hidden then
|
|
-- 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
|
|
|
|
--- True if any non-passable EO occupies (tx,ty) or is stepping onto it.
|
|
function Objects.blocks(tx, ty, exceptLocalId)
|
|
exceptLocalId = tonumber(exceptLocalId)
|
|
for _, lid in ipairs(Objects._order) do
|
|
if lid ~= exceptLocalId then
|
|
local eo = Objects._byId[lid]
|
|
if eo and eo.visible and not eo.hidden and not eo.passable then
|
|
if eo.cellX == tx and eo.cellY == ty then return true end
|
|
if eo.moving and eo.targetX == tx and eo.targetY == ty then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- pokefirered/src/event_object_movement.c:4899
|
|
function Objects.playerBlocks(tx, ty)
|
|
local P = Player()
|
|
if not P then return false end
|
|
if P.cellX == tx and P.cellY == ty then return true end
|
|
if P.moving and P.targetX == tx and P.targetY == ty then return true end
|
|
return false
|
|
end
|
|
|
|
local function walkPhaseOf(eo)
|
|
if not eo.moving then return 0 end
|
|
-- pokefirered/src/event_object_movement.c:7040 MovementAction_DisableAnimation_Step0
|
|
if eo.inanimate then return 0 end
|
|
local frames = eo.stepFrames or WALK_FRAMES
|
|
local p = eo.animClock % frames
|
|
local mid = math.floor(frames / 2)
|
|
return (p >= math.floor(frames / 4) and p < mid + math.floor(frames / 4)) and 1 or 0
|
|
end
|
|
|
|
function Objects.walkPhase(eo)
|
|
return walkPhaseOf(eo)
|
|
end
|
|
|
|
local function beginStep(eo, tx, ty)
|
|
eo.moving = true
|
|
eo.progress = 0
|
|
eo.targetX = tx
|
|
eo.targetY = ty
|
|
eo.stepFrames = WALK_FRAMES
|
|
eo.animClock = 0
|
|
end
|
|
|
|
local function finishStep(eo, game)
|
|
eo.cellX = eo.targetX
|
|
eo.cellY = eo.targetY
|
|
eo.px = eo.cellX * CELL
|
|
eo.py = eo.cellY * CELL
|
|
eo.moving = false
|
|
eo.progress = 0
|
|
eo.stepFlip = not eo.stepFlip
|
|
if eo.def then
|
|
eo.def.x, eo.def.y = eo.cellX, eo.cellY
|
|
end
|
|
local Coll = Collision()
|
|
local curElev = Coll and Coll.elevationAt and Coll.elevationAt(eo.cellX, eo.cellY)
|
|
if curElev and curElev ~= 0 and curElev ~= 15 then
|
|
eo.elevation = curElev
|
|
end
|
|
if eo.sight and eo.sight > 0 and not eo.scriptBusy and not eo.frozen then
|
|
local okTs, TrainerSight = pcall(require, "src.core.game3.trainer_sight")
|
|
if okTs and TrainerSight and TrainerSight.check then
|
|
TrainerSight.check(game, eo)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- 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)
|
|
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.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)
|
|
if eo.progress >= frames then
|
|
finishStep(eo, game)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- Scripted one-cell step (no collision — FRLG applymovement forces).
|
|
function Objects.scriptStep(eo, dir, run, slow)
|
|
if not eo then return false end
|
|
local P = Player()
|
|
if eo == P then
|
|
return P.scriptStep and P.scriptStep(dir, run, slow)
|
|
end
|
|
if eo.moving then return false end
|
|
local d = DELTA[dir]
|
|
if not d then return false end
|
|
-- pokefirered/src/event_object_movement.c:6796 MovementAction_LockFacingDirection_Step0
|
|
if not eo.facingLocked then eo.facing = dir end
|
|
beginStep(eo, eo.cellX + d[1], eo.cellY + d[2])
|
|
-- pokefirered/src/event_object_movement.c:5333 StartRunningAnim
|
|
if run then eo.stepFrames = slow and RUN_SLOW_FRAMES or RUN_FRAMES end
|
|
eo.frozen = true
|
|
eo.scriptBusy = true
|
|
return true
|
|
end
|
|
|
|
function Objects.scriptJump(eo, dir, distance)
|
|
if not eo then return false end
|
|
local P = Player()
|
|
if eo == P then
|
|
return P.scriptJump and P.scriptJump(dir, distance)
|
|
end
|
|
if eo.moving then return false end
|
|
distance = distance or 1
|
|
local d = DELTA[dir]
|
|
if not d then return false end
|
|
eo.facing = dir
|
|
beginStep(eo, eo.cellX + d[1] * distance, eo.cellY + d[2] * distance)
|
|
eo.frozen = true
|
|
eo.scriptBusy = true
|
|
return true
|
|
end
|
|
|
|
-- 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
|
|
|
|
local act = tr.actions[tr.i]
|
|
if not act then
|
|
tr.done = true
|
|
if eo and eo ~= Player() then
|
|
eo.scriptBusy = false
|
|
end
|
|
if tr.onDone then
|
|
local cb = tr.onDone
|
|
tr.onDone = nil
|
|
cb()
|
|
end
|
|
return
|
|
end
|
|
tr.i = tr.i + 1
|
|
if type(act) == "string" then
|
|
local sdir = act:match("^walk_(.*)$") or act:match("^step_(.*)$")
|
|
if sdir then
|
|
act = { kind = "step", dir = sdir }
|
|
else
|
|
local tdir = act:match("^turn_(.*)$") or act:match("^face_(.*)$")
|
|
if tdir then
|
|
act = { kind = "turn", dir = tdir }
|
|
end
|
|
end
|
|
end
|
|
if type(act) == "table" then
|
|
if act.kind == "step" then
|
|
if eo == Player() then
|
|
if Player().scriptStep then Player().scriptStep(act.dir, act.run, act.slow) end
|
|
elseif eo then
|
|
Objects.scriptStep(eo, act.dir, act.run, act.slow)
|
|
end
|
|
elseif act.kind == "jump" then
|
|
if eo == Player() then
|
|
if Player().scriptJump then
|
|
Player().scriptJump(act.dir, act.distance or 1)
|
|
elseif Player().scriptStep then
|
|
for _ = 1, (act.distance or 1) do
|
|
Player().scriptStep(act.dir)
|
|
end
|
|
end
|
|
elseif eo then
|
|
if Objects.scriptJump then
|
|
Objects.scriptJump(eo, act.dir, act.distance or 1)
|
|
else
|
|
Objects.scriptStep(eo, act.dir)
|
|
end
|
|
end
|
|
elseif act.kind == "turn" then
|
|
Objects.scriptFace(eo, act.dir)
|
|
elseif act.kind == "face_player" then
|
|
-- pokefirered/src/event_object_movement.c:6772 MovementAction_FacePlayer_Step0
|
|
if eo and eo ~= Player() then
|
|
local dir = directionToFace(eo.cellX, eo.cellY, Player().cellX, Player().cellY)
|
|
if act.away then dir = OPPOSITE_DIR[dir] end
|
|
Objects.scriptFace(eo, dir)
|
|
end
|
|
elseif act.kind == "lock_facing" then
|
|
-- pokefirered/src/event_object_movement.c:6796 MovementAction_LockFacingDirection_Step0
|
|
if eo and eo ~= Player() then eo.facingLocked = act.locked and true or false end
|
|
elseif act.kind == "animate" then
|
|
-- pokefirered/src/event_object_movement.c:7040 MovementAction_DisableAnimation_Step0
|
|
if eo and eo ~= Player() then eo.inanimate = act.inanimate and true or false end
|
|
elseif act.kind == "remove_obstacle" then
|
|
-- pokefirered/src/event_object_movement.c:7135 MovementAction_RockSmashBreak_Step0
|
|
tr.sleep = act.frames or 32
|
|
elseif act.kind == "face_original" then
|
|
if eo and eo ~= Player() and eo.def then
|
|
-- 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
|
|
else
|
|
local Coll = Collision()
|
|
-- pokefirered/src/event_object_movement.c:8346 IsElevationMismatchAt
|
|
local onWater = Coll.isWater(eo.cellX, eo.cellY)
|
|
ok = Coll.canEnter(game, tx, ty,
|
|
{ fromX = eo.cellX, fromY = eo.cellY, dir = dir, surfing = onWater })
|
|
if ok and Coll.isWater(tx, ty) ~= onWater then ok = false end
|
|
if Objects.playerBlocks(tx, ty) then ok = false end
|
|
if Objects.blocks(tx, ty, eo.localId) then ok = false end
|
|
end
|
|
if 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 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
|
|
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
|
|
|
|
local Rng = require("src.core.game3.rng")
|
|
local function pick(t) return t[(Rng.Random() % #t) + 1] 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
|
|
|
|
function Objects.update(game)
|
|
-- Advance script tracks then motion + idle.
|
|
for lid, tr in pairs(Objects._tracks) do
|
|
advanceTrack(lid, tr, game)
|
|
end
|
|
for _, lid in ipairs(Objects._order) do
|
|
local eo = Objects._byId[lid]
|
|
if eo then
|
|
if eo.bowFrames and eo.bowFrames > 0 then
|
|
eo.bowFrames = eo.bowFrames - 1
|
|
if eo.bowFrames <= 0 then eo.bowFrames = nil end
|
|
end
|
|
tickMotion(eo, game)
|
|
idleTick(eo, game)
|
|
end
|
|
end
|
|
end
|
|
|
|
function Objects.spawnFromDefs(defs, mapDef, 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)
|
|
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 then
|
|
if eo.hidden then
|
|
-- src/event_object_movement.c:1569
|
|
eo.invisible = Objects.templateMovementType(localId) == MOVEMENT_TYPE_INVISIBLE
|
|
end
|
|
applyPerm(eo, Objects._mapId)
|
|
eo.hidden = false
|
|
eo.visible = true
|
|
if eo.def then eo.def.hidden = false end
|
|
return true
|
|
end
|
|
-- Respawn from template defs.
|
|
for _, def in ipairs(Objects._defs or {}) do
|
|
if tonumber(def.localId or def.index) == localId then
|
|
eo = newEventObject(def)
|
|
applyPerm(eo, Objects._mapId)
|
|
eo.hidden = false
|
|
eo.visible = true
|
|
Objects._byId[localId] = eo
|
|
Objects._order[#Objects._order + 1] = localId
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- Re-evaluate hide flags after sidecar load / setflag mid-map.
|
|
function Objects.refreshVisibility()
|
|
for _, lid in ipairs(Objects._order) do
|
|
local eo = Objects._byId[lid]
|
|
if eo and eo.def then
|
|
local vis = objectVisible(eo.def)
|
|
eo.visible = vis
|
|
eo.hidden = not vis
|
|
end
|
|
end
|
|
end
|
|
|
|
-- 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.
|
|
-- clearflag after removeobject must bring the NPC back at perm coords.
|
|
-- 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
|
|
else
|
|
if eo.hidden then
|
|
-- src/event_object_movement.c:1569
|
|
eo.invisible = Objects.templateMovementType(lid) == MOVEMENT_TYPE_INVISIBLE
|
|
end
|
|
applyPerm(eo, Objects._mapId)
|
|
eo.hidden = false
|
|
eo.visible = true
|
|
if eo.def then eo.def.hidden = false end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- Template exists in defs but was never spawned (or despawned from order).
|
|
if not hidden then
|
|
for _, def in ipairs(Objects._defs or {}) do
|
|
local f = tonumber(def.flag or def.flagId) or 0
|
|
local lid = tonumber(def.localId or def.index) or 0
|
|
if f == flagId and lid > 0 and not Objects._byId[lid] then
|
|
Objects.addObject(lid)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function Objects.removeObject(localId)
|
|
localId = tonumber(localId) or 0
|
|
local eo = Objects._byId[localId]
|
|
if not eo then return false end
|
|
local flag = eo.def and (eo.def.flag or eo.def.flagId)
|
|
if flag and flag ~= 0 and flag ~= 0xFFFF and flag ~= 65535 then
|
|
local Space = package.loaded["src.core.game3.scripting.space"]
|
|
if Space and Space.store then
|
|
local Flags = require("src.core.game3.scripting.flags")
|
|
Flags.setFlag(Space.store, nil, flag, true)
|
|
end
|
|
end
|
|
eo.hidden = true
|
|
eo.visible = false
|
|
if eo.def then eo.def.hidden = true end
|
|
Objects._tracks[localId] = nil
|
|
return true
|
|
end
|
|
|
|
-- src/event_object_movement.c:2059
|
|
local function setObjectInvisibility(localId, state)
|
|
local lid = tonumber(localId) or 0
|
|
if lid >= Objects.PLAYER_LOCAL_ID then
|
|
Player().setVisible(not state)
|
|
return true
|
|
end
|
|
local eo = Objects._byId[lid]
|
|
if not eo then return false end
|
|
eo.invisible = state
|
|
return true
|
|
end
|
|
|
|
function Objects.hideObject(localId)
|
|
return setObjectInvisibility(localId, true)
|
|
end
|
|
|
|
function Objects.showObject(localId)
|
|
return setObjectInvisibility(localId, false)
|
|
end
|
|
|
|
function Objects.turnObject(localId, dir)
|
|
local dirs = { [1] = "down", [2] = "up", [3] = "left", [4] = "right" }
|
|
local facing = dirs[tonumber(dir) or 0]
|
|
or ({ down = "down", up = "up", left = "left", right = "right" })[tostring(dir or ""):lower()]
|
|
local eo = Objects.find(localId)
|
|
if eo and facing then
|
|
Objects.scriptFace(eo, facing)
|
|
end
|
|
end
|
|
|
|
function Objects.setObjectXY(localId, x, y)
|
|
local lid = tonumber(localId) or 0
|
|
local eo = Objects._byId[lid]
|
|
x, y = tonumber(x) or 0, tonumber(y) or 0
|
|
-- Key perm by script map (Space.mapId) when active — not the previous map's
|
|
-- Objects._mapId if enter order ever regresses.
|
|
local Sp = Space()
|
|
local mapKey = (Sp and Sp.mapId) or Objects._mapId
|
|
rememberPerm(mapKey, lid, { x = x, y = y })
|
|
if not eo then return end
|
|
eo.cellX, eo.cellY = x, y
|
|
eo.homeX, eo.homeY = x, y
|
|
eo.px, eo.py = eo.cellX * CELL, eo.cellY * CELL
|
|
eo.targetX, eo.targetY = eo.cellX, eo.cellY
|
|
eo.moving = false
|
|
if eo.def then eo.def.x, eo.def.y = eo.cellX, eo.cellY end
|
|
end
|
|
|
|
function Objects.copyObjectXYToPerm(localId)
|
|
local lid = tonumber(localId) or 0
|
|
local eo = Objects._byId[lid]
|
|
if not eo then return end
|
|
local Sp = Space()
|
|
local mapKey = (Sp and Sp.mapId) or Objects._mapId
|
|
rememberPerm(mapKey, lid, { x = eo.cellX, y = eo.cellY })
|
|
eo.homeX, eo.homeY = eo.cellX, eo.cellY
|
|
if eo.def then eo.def.x, eo.def.y = eo.cellX, eo.cellY end
|
|
end
|
|
|
|
function Objects.setMovementType(localId, mt)
|
|
local lid = tonumber(localId) or 0
|
|
local eo = Objects._byId[lid]
|
|
mt = tonumber(mt) or 0
|
|
local Sp = Space()
|
|
local mapKey = (Sp and Sp.mapId) or Objects._mapId
|
|
rememberPerm(mapKey, lid, { movementType = mt })
|
|
if not eo then return end
|
|
Objects.setTrainerMovementType(eo, mt)
|
|
local face = ({ [7] = "up", [8] = "down", [9] = "left", [10] = "right" })[mt]
|
|
if face then eo.facing = face end
|
|
end
|
|
|
|
local function clearRaiseHand(eo)
|
|
eo.raiseHandState = nil
|
|
eo.raiseHand = nil
|
|
eo.raiseX = nil
|
|
eo.raiseY = nil
|
|
end
|
|
|
|
-- src/event_object_movement.c:4806
|
|
function Objects.setTrainerMovementType(localId, mt)
|
|
local eo = type(localId) == "table" and localId or Objects._byId[tonumber(localId) or 0]
|
|
if not eo then return end
|
|
mt = tonumber(mt) or 0
|
|
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] = tonumber(mt)
|
|
end
|
|
|
|
function Objects.templateMovementType(localId)
|
|
local lid = tonumber(localId) or 0
|
|
local o = Objects._templateMt[lid]
|
|
if o then return o end
|
|
for _, def in ipairs(Objects._defs or {}) do
|
|
if tonumber(def.localId or def.index) == lid then
|
|
return tonumber(def.movementType) or 0
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function Objects.facePlayer(localId, game)
|
|
local eo = Objects._byId[tonumber(localId) or 0]
|
|
local P = Player()
|
|
if not eo or not P then return end
|
|
local dx = P.cellX - eo.cellX
|
|
local dy = P.cellY - eo.cellY
|
|
if math.abs(dx) > math.abs(dy) then
|
|
eo.facing = dx > 0 and "right" or "left"
|
|
else
|
|
eo.facing = dy > 0 and "down" or "up"
|
|
end
|
|
end
|
|
|
|
function Objects.freeze(localId)
|
|
local eo = Objects._byId[tonumber(localId) or 0]
|
|
if eo then eo.frozen = true end
|
|
end
|
|
|
|
function Objects.unfreeze(localId)
|
|
local eo = Objects._byId[tonumber(localId) or 0]
|
|
if eo and not eo.scriptBusy then eo.frozen = false end
|
|
end
|
|
|
|
--- No-op. EventObjects are owned by game3; Field.interact + adapters read them
|
|
-- directly via Objects.find / forDraw. Kept for call-site compatibility.
|
|
function Objects.syncToHost(_game)
|
|
end
|
|
|
|
-- Back-compat thin wrappers used by older Objects.addObject(adapters, id) calls.
|
|
function Objects.addObjectVia(adapters, localId)
|
|
if Objects.addObject(localId) then return true end
|
|
if adapters and adapters.addObject then return adapters.addObject(localId) end
|
|
return false
|
|
end
|
|
|
|
function Objects.removeObjectVia(adapters, localId)
|
|
Objects.removeObject(localId)
|
|
if adapters and adapters.removeObject then return adapters.removeObject(localId) end
|
|
return true
|
|
end
|
|
|
|
return Objects
|