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
78
mark.lua
Normal file
78
mark.lua
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
require "entity"
|
||||
|
||||
-- A named destination on the grid. NPCs are sent to a Mark by name (see
|
||||
-- Npc:sendTo / World:markByName), so the name is the whole point of the object.
|
||||
--
|
||||
-- A Mark is a *debug non-collidable*: an editor-only annotation that everything
|
||||
-- passes through, shown only while the debug overlay is on. It is NOT a Note and
|
||||
-- shares no code with one — the two just happen to be siblings in that category.
|
||||
-- Its icon is a target, drawn procedurally so it needs no art asset.
|
||||
Mark = class("Mark", Entity)
|
||||
|
||||
-- Entity only ever calls :getWidth/:getHeight on the sizing sprite (Mark:draw
|
||||
-- paints a target instead of blitting it), so a lightweight stub is enough and
|
||||
-- keeps Mark free of any borrowed artwork.
|
||||
local sizeStub = { getWidth = function() return 16 end, getHeight = function() return 16 end }
|
||||
|
||||
-- The target is pixel art: a 16x16 bullseye (two rings + a centre dot) baked once
|
||||
-- into a list of on-pixels, drawn as blocky rectangles so it stays crisp at the
|
||||
-- game's nearest-neighbour scale instead of a smooth vector circle.
|
||||
local GRID = 16
|
||||
local targetPixels = {}
|
||||
do
|
||||
local c = (GRID - 1) / 2 -- 7.5, the sub-pixel centre
|
||||
for row = 0, GRID - 1 do
|
||||
for col = 0, GRID - 1 do
|
||||
local dist = math.sqrt((col - c) ^ 2 + (row - c) ^ 2)
|
||||
local on = math.abs(dist - 6.5) < 0.7 -- outer ring
|
||||
or math.abs(dist - 3.5) < 0.7 -- inner ring
|
||||
or dist < 1.3 -- centre dot
|
||||
if on then targetPixels[#targetPixels + 1] = { col, row } end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Mark:initialize(x, y, name)
|
||||
self.markName = name or ""
|
||||
self.saveClass = "mark"
|
||||
-- programmatic marks are created from code on every room load, so they are
|
||||
-- skipped by Room:serialize (see there) to avoid duplicating on disk.
|
||||
self.programmatic = false
|
||||
Entity.initialize(self, {
|
||||
x = x, y = y,
|
||||
color = "white", -- representative only; Mark:draw is channel-agnostic
|
||||
collision = "none",
|
||||
sprite = sizeStub,
|
||||
})
|
||||
end
|
||||
|
||||
-- Drawn once (not per-channel) from the room's debug overlay pass, so it takes no
|
||||
-- channel argument. A faint pixel-art red target with its name beside it, kept at
|
||||
-- the same low opacity as a Note so it reads as an annotation, not a game object.
|
||||
function Mark:draw()
|
||||
local p = self:getDrawPos()
|
||||
love.graphics.setColor(1, 0.3, 0.25, 0.85)
|
||||
for _, px in ipairs(targetPixels) do
|
||||
love.graphics.rectangle("fill", p.x + px[1] * drawScale, p.y + px[2] * drawScale, drawScale, drawScale)
|
||||
end
|
||||
if self.markName ~= "" then
|
||||
love.graphics.setColor(1, 0.5, 0.42, 0.85)
|
||||
love.graphics.print(self.markName, p.x, p.y - 12)
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
-- The same pixel-art target baked into a 16x16 Image, for the editor toolbar
|
||||
-- button (full opacity there — it's a control, not a faint overlay). Cached, and
|
||||
-- built lazily so it only touches love.graphics once the editor needs it.
|
||||
function Mark.static.icon()
|
||||
if Mark.static._icon then return Mark.static._icon end
|
||||
local data = love.image.newImageData(GRID, GRID)
|
||||
for _, px in ipairs(targetPixels) do
|
||||
data:setPixel(px[1], px[2], 1, 0.3, 0.25, 1)
|
||||
end
|
||||
local img = love.graphics.newImage(data)
|
||||
img:setFilter("nearest", "nearest")
|
||||
Mark.static._icon = img
|
||||
return img
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue