chroma_solstice/dialogue_manager.lua
Your Name c348b81a93 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>
2026-08-21 15:56:53 -04:00

116 lines
3.8 KiB
Lua

-- Dead-simple modal dialogue overlay driven by a narrator (Ink) story.
-- No variables, no saved state: open a book, walk its paragraphs, pick a
-- choice, then a keypress dismisses it. `narrator` is the global set up in
-- main.lua; Dialogable entities (see npc.lua) call Dialogue.open on bump.
Dialogue = {}
local story -- active narrator story, or nil when the box is closed
local paragraphs = {} -- text lines currently shown
local choices = {} -- current choices (empty once we reach an ending)
local selected = 1
function Dialogue.isOpen()
return story ~= nil
end
-- Run the story forward to the next choice point (or the end), gathering every
-- paragraph printed along the way.
local function advance()
paragraphs = {}
while story:can_continue() do
for _, p in ipairs(story:continue()) do
table.insert(paragraphs, p.text)
end
end
choices = story:can_choose() and story:get_choices() or {}
selected = 1
end
-- `bindings` (optional) is a {name = function} table wired in as Ink external
-- functions before the story starts, so a book can call e.g. `~ walk_to("gate")`
-- to drive game state. Dialogable supplies these per talker (see dialogable.lua).
function Dialogue.open(book, bindings)
if story then return end -- already talking: ignore repeat bumps
story = narrator.init_story(book)
if bindings then
for name, fn in pairs(bindings) do story:bind(name, fn) end
end
story:begin()
advance()
end
function Dialogue.close()
story, paragraphs, choices, selected = nil, {}, {}, 1
end
function Dialogue.keypressed(key)
if #choices == 0 then
-- an ending is on screen: any key dismisses the box
Dialogue.close()
elseif key == "up" then
selected = selected > 1 and selected - 1 or #choices
elseif key == "down" then
selected = selected < #choices and selected + 1 or 1
elseif key == "return" or key == "space" or key == "x" then
story:choose(selected)
advance()
end
end
-- LCD font sized to the current play area, rebuilt only when the size changes,
-- so dialogue text scales with the game like every sprite does.
local dlgFont, dlgFontSize
local function dialogueFont()
local size = math.max(8, math.floor(height / gridHeight * 0.4))
if not dlgFont or size ~= dlgFontSize then
dlgFont = love.graphics.newFont("font/LCD_Solid.ttf", size)
dlgFontSize = size
end
return dlgFont
end
function Dialogue.draw()
if not story then return end
local fnt = dialogueFont()
local fh = fnt:getHeight()
local pad, lineH = fh * 0.8, fh * 1.3
local boxH = height * 0.4
local y0 = height - boxH
local wrap = width - pad * 2
-- Dialogue obeys the world's colour rule: only the channels the player has
-- are rendered. One colour -> that colour; all three -> white (additive), so
-- a red-only player literally reads the sign in red.
local has = { red = false, green = false, blue = false }
for _, c in ipairs(currentRoom.colorsPlayerHas) do has[c] = true end
local r = has.red and 1 or 0
local g = has.green and 1 or 0
local b = has.blue and 1 or 0
local prevFont = love.graphics.getFont()
love.graphics.setFont(fnt)
love.graphics.setColor(0, 0, 0, 0.82)
love.graphics.rectangle("fill", 0, y0, width, boxH)
local y = y0 + pad
love.graphics.setColor(r, g, b, 1)
for _, text in ipairs(paragraphs) do
love.graphics.printf(text, pad, y, wrap)
local _, lines = fnt:getWrap(text, wrap)
y = y + math.max(1, #lines) * lineH
end
y = y + lineH * 0.5
for i, choice in ipairs(choices) do
local dim = (i == selected) and 1 or 0.5 -- selected is brighter + "> "
love.graphics.setColor(r * dim, g * dim, b * dim, 1)
local line = ((i == selected) and "> " or " ") .. choice.text
love.graphics.printf(line, pad, y, wrap)
local _, lines = fnt:getWrap(line, wrap)
y = y + math.max(1, #lines) * lineH
end
love.graphics.setFont(prevFont)
end