Add named Marks and cross-room NPC pathfinding
- Mark: a debug non-collidable named destination (pixel-art target icon), editor tool + name modal, globally-unique names, programmatic Room:addMark - NPCs walk to Marks: each RGB copy is a per-room resident (like the player), running its own A* on its own colour layer, transferring rooms as it crosses seams; rendered and collided by the room it currently stands in - Player copies are obstacles in NPC pathfinding - A* is footprint-aware for multi-cell NPCs - Editor: click an NPC to open a filterable destination picker - Ink: Dialogue.open binds walk_to; Jorge can be sent to "end"/"begin" - Fix: a blocked NPC now replans each tick and resumes instead of freezing - [npc] move diagnostics under DEBUG (start/goal/route or block reason) - Docs (adding-npcs) + CLAUDE.md LÖVE 11.x/float-colour corrections - Fix test harness loadSprite stub; movement tests green (28) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ad405ed590
commit
c348b81a93
17 changed files with 2755 additions and 43 deletions
63
world.lua
63
world.lua
|
|
@ -213,6 +213,9 @@ end
|
|||
function World:rebuildCollision()
|
||||
self.collidableMatrices = {red = {}, green = {}, blue = {}}
|
||||
self.playerBlockerMatrices = {red = {}, green = {}, blue = {}}
|
||||
-- Per-colour set of world cells occupied by a player copy. NPC pathfinding
|
||||
-- treats these as solid (a walker can't push the player) — see pathfinding.lua.
|
||||
self.playerCells = {red = {}, green = {}, blue = {}}
|
||||
self.entityRooms = {}
|
||||
local function put(matrix, cell, entity)
|
||||
matrix[cell.y] = matrix[cell.y] or {}
|
||||
|
|
@ -221,6 +224,13 @@ function World:rebuildCollision()
|
|||
for name, room in pairs(self.levels) do
|
||||
local location = self:roomLocation(name)
|
||||
if location then
|
||||
for color, player in pairs(room.player) do
|
||||
for _, cell in ipairs(player:getOccupiedCells()) do
|
||||
local x = (location.x - 1) * gridWidth + cell.x
|
||||
local y = (location.y - 1) * gridHeight + cell.y
|
||||
self.playerCells[color][x .. ":" .. y] = true
|
||||
end
|
||||
end
|
||||
for color, entities in pairs(room.collideables) do
|
||||
for _, entity in pairs(entities) do
|
||||
self.entityRooms[entity] = room
|
||||
|
|
@ -232,16 +242,19 @@ function World:rebuildCollision()
|
|||
end
|
||||
end
|
||||
end
|
||||
-- NPCs are local room state, but globally collidable: a movement lookup
|
||||
-- must find them so its normal bump() path can open dialogue.
|
||||
for _, npc in ipairs(room.npcs) do
|
||||
self.entityRooms[npc] = room
|
||||
for _, cell in ipairs(npc:getOccupiedCells()) do
|
||||
local worldCell = {
|
||||
x = (location.x - 1) * gridWidth + cell.x,
|
||||
y = (location.y - 1) * gridHeight + cell.y,
|
||||
}
|
||||
for color in pairs(self.collidableMatrices) do put(self.collidableMatrices[color], worldCell, npc) end
|
||||
-- NPC copies are per-colour room residents (see npc.lua). Each colour
|
||||
-- stamps only its own copy's cells — "red can only be hit by red" — at the
|
||||
-- copy's position in the room it currently stands in. The controller
|
||||
-- entity (copy.npc) is stored so a bump opens its dialogue.
|
||||
for color, copies in pairs(room.npcCopies) do
|
||||
for _, copy in ipairs(copies) do
|
||||
self.entityRooms[copy.npc] = room
|
||||
for _, shapeCell in ipairs(copy.npc:getCellShape()) do
|
||||
put(self.collidableMatrices[color], {
|
||||
x = (location.x - 1) * gridWidth + copy.x + shapeCell.x - 1,
|
||||
y = (location.y - 1) * gridHeight + copy.y + shapeCell.y - 1,
|
||||
}, copy.npc)
|
||||
end
|
||||
end
|
||||
end
|
||||
for color, entities in pairs(room.playerBlockers) do
|
||||
|
|
@ -271,6 +284,17 @@ function World:roomCell(cell)
|
|||
return room, {x = cell.x - (roomX - 1) * gridWidth, y = cell.y - (roomY - 1) * gridHeight}
|
||||
end
|
||||
|
||||
-- Marks are globally unique named destinations. This resolves one for an NPC's
|
||||
-- Walker:sendTo, scanning every loaded room's mark registry.
|
||||
function World:markByName(name)
|
||||
for _, room in pairs(self.levels or {}) do
|
||||
for _, mark in ipairs(room.marks or {}) do
|
||||
if mark.markName == name then return mark, room end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function cellKey(cell)
|
||||
return cell.x .. ":" .. cell.y
|
||||
end
|
||||
|
|
@ -795,6 +819,25 @@ function World:tick(room)
|
|||
self:nextRoomCheck(room)
|
||||
end
|
||||
|
||||
-- Real-time NPC movement across every placed room, independent of player turns.
|
||||
-- Snapshot the NPC list first: Walker:arrive can hand an NPC to another room's
|
||||
-- registry, which would otherwise mutate a list we are iterating.
|
||||
function World:update(dt)
|
||||
local walking = {}
|
||||
for name, room in pairs(self.levels or {}) do
|
||||
if self:roomLocation(name) then
|
||||
for _, npc in ipairs(room.npcs) do
|
||||
if npc.step then table.insert(walking, npc) end
|
||||
end
|
||||
end
|
||||
end
|
||||
local moved = false
|
||||
for _, npc in ipairs(walking) do
|
||||
if npc:step(dt) then moved = true end
|
||||
end
|
||||
if moved then self:rebuildCollision() end
|
||||
end
|
||||
|
||||
function World:nextRoomCheck(room)
|
||||
for _, color in ipairs(room:getActiveColors()) do
|
||||
local player = room.player[color]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue