Files
gen1recomp/src/world/NPC.lua
T

110 lines
3.5 KiB
Lua

-- Map object (NPC/item) built from a generated object_event entry.
-- STAY objects keep their facing; WALK objects wander randomly within the
-- roam constraint (ANY_DIR / UP_DOWN / LEFT_RIGHT), like the original.
local Collision = require("src.world.Collision")
local SpriteRenderer = require("src.render.SpriteRenderer")
local NPC = {}
NPC.__index = NPC
local STEP_FRAMES = 16
local FACING_FROM_RANGE = {
DOWN = "down", UP = "up", LEFT = "left", RIGHT = "right",
}
local ROAM_DIRS = {
ANY_DIR = { "up", "down", "left", "right" },
UP_DOWN = { "up", "down" },
LEFT_RIGHT = { "left", "right" },
}
function NPC.new(data, mapId, objDef)
local self = setmetatable({}, NPC)
self.def = objDef
self.id = string.format("%s_obj_%d", mapId, objDef.index)
local spriteDef = data.sprites[objDef.sprite]
assert(spriteDef, "unknown sprite " .. tostring(objDef.sprite))
self.sprite = SpriteRenderer.new(spriteDef, self.id)
-- object_event coordinates are already walk-grid cells
self.cellX, self.cellY = objDef.x, objDef.y
self.px, self.py = self.cellX * 16, self.cellY * 16
self.facing = FACING_FROM_RANGE[objDef.range] or "down"
self.moving = false
self.progress = 0
self.stepFlip = false
self.frozen = false -- scripts freeze NPCs while talking
self.wanders = objDef.movement == "WALK"
self.roamDirs = ROAM_DIRS[objDef.range] or ROAM_DIRS.ANY_DIR
self.timer = love.math.random(30, 120)
return self
end
function NPC:facePlayer(player)
local dx = player.cellX - self.cellX
local dy = player.cellY - self.cellY
if math.abs(dx) > math.abs(dy) then
self.facing = dx > 0 and "right" or "left"
else
self.facing = dy > 0 and "down" or "up"
end
end
function NPC:update(map, entities)
if self.moving then
self.progress = self.progress + 1
-- NPC_CHANGE_FACING: animate the walk cycle in place, no translation
-- (movement.asm ChangeFacingDirection zeroes the delta); px/py stay
-- pinned to the current cell while walkPhase() cycles.
if self.marching then
if self.progress >= STEP_FRAMES then
self.progress = 0
self.moving = false
self.marching = false
self.stepFlip = not self.stepFlip
end
return
end
local d = Collision.DELTA[self.facing]
self.px = self.cellX * 16 + d[1] * self.progress
self.py = self.cellY * 16 + d[2] * self.progress
if self.progress >= STEP_FRAMES then
self.cellX, self.cellY = self.targetX, self.targetY
self.targetX, self.targetY = nil, nil
self.px, self.py = self.cellX * 16, self.cellY * 16
self.moving = false
self.stepFlip = not self.stepFlip
end
return
end
if self.frozen or not self.wanders then return end
self.timer = self.timer - 1
if self.timer > 0 then return end
self.timer = love.math.random(30, 180)
local dir = self.roamDirs[love.math.random(#self.roamDirs)]
self.facing = dir
if love.math.random() < 0.5 then return end -- sometimes just turn
-- never wander onto warps, so NPCs don't walk out of the map
local tx, ty = Collision.target(self.cellX, self.cellY, dir)
if map:warpAtCell(tx, ty) then return end
if Collision.canMove(map, entities, self, dir) then
self.targetX, self.targetY = tx, ty
self.moving = true
self.progress = 0
end
end
function NPC:walkPhase()
if not self.moving then return 0 end
local p = self.progress % 16
return (p >= 4 and p < 12) and 1 or 0
end
function NPC:draw(camX, camY)
self.sprite:draw(self.px, self.py, camX, camY, self.facing,
self:walkPhase(), self.stepFlip)
end
return NPC