chroma_solstice/docs/adding-npcs.md
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

3.8 KiB
Raw Blame History

Adding a talkable NPC

An NPC/sign is just an immovable Entity that mixes in Dialogable. Bumping it (a rejected move) opens the dialogue box. Talking is the same everywhere — you only supply art + a story.

1. Write the story

Create stories/<name>.ink — a line, a few choices, an ending:

yo wassup

* kick the sign
    it doesnt budge.
* [say nothing] you have nothing to say to a sign.
- -> END

2. Compile it

The game can't parse .ink (LÖVE has no lpeg), so compile to a .lua book:

lua tools/compile_ink.lua <name>

Re-run this every time you edit the .ink — the game loads the .lua, not the .ink. (Needs lua + lua-lpeg installed.)

3. Load the book

In main.lua, next to startSignBook:

<name>Book = require "stories.<name>"

4. Make the entity

In npc.lua, copy Sign — pick your art and name your book. Include Walker too (it's what lets the NPC path to a Mark; see "Making an NPC walk" below), and call self:initWalker() after Entity.initialize:

Oracle = class("Oracle", Entity)
Oracle:include(Dialogable)
Oracle:include(Walker)

function Oracle:initialize(x, y)
	self.book = oracleBook            -- from step 3
	Entity.initialize(self, {
		x = x, y = y, color = "red",
		collision = "unmoveable",
		sprites = {
			red   = loadSprite("art/npc/oracle_r.png"),
			green = loadSprite("art/npc/oracle_g.png"),
			blue  = loadSprite("art/npc/oracle_b.png"),
		},
	})
	self:initWalker()
end

Per-channel art lives in art/npc/ as <name>_r/_g/_b.png (16px cells).

5. Place it in a room

Map a save class to your entity in Room:createEntity (room.lua), alongside the npc → Sign branch:

if class == "oracle" then
	local entity = Oracle:new(x, y)
	self:registerNpc(entity)
	return entity
end

Then add it to a room's rooms/<room>.sav objects list:

{ class = "oracle", x = 4, y = 8 }

That's it — walk into it and it talks. NPCs are stamped into every colour's collision matrix, so they block movement and obey the world's colour rules (a red-only player reads them in red).

Marks — named destinations

A Mark is a debug non-collidable: an editor-only annotation the game reads back. It carries a globally-unique name, and it's the only thing an NPC can be sent to. It is not a Note — it just shares the debug overlay toggle. In the editor, click the target tool at the bottom of the panel, then click a cell to drop one and type its name (colliding names auto-suffix _2, _3, …). Left-click an existing mark to rename it; right-click to delete. Marks show only under DEBUG while the overlay is on (Tab).

You can also create marks from code so they exist without living in a .sav:

room:addMark("gate", 5, 9)   -- registers + renders in debug; skipped by serialize

Making an NPC walk

Any NPC that includes Walker (see step 4) can be sent to a Mark. From Ink, call the bound external function — e.g. to leave once the conversation ends:

Oracle: The way is open now. Go.
~ walk_to("gate")
-> END

walk_to(name) is wired for every Dialogable (dialogable.lua); it resolves the Mark by name and calls npc:sendTo. Under the hood the NPC becomes the player's RGB model — up to three colour copies, each running its own A* on its own collision layer (pathfinding.lua), stepping in real time and pathing across room seams over the world grid. Copies split apart and additively re-merge to white on arrival. Pathfinding is footprint-aware, so a 2×2 NPC routes its whole body around obstacles rather than threading a 1-wide gap.

Testing without a story

You don't need an Ink book to try it: in the editor (DEBUG), left-click an NPC to open the destination picker — a filterable list of every Mark. Type to filter, click a row (or Enter for the top match) to send it; Esc cancels.