- 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>
78 lines
3.1 KiB
Lua
78 lines
3.1 KiB
Lua
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
|