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