chroma_solstice/docs/adding-npcs.md

130 lines
3.8 KiB
Markdown
Raw Normal View History

2026-08-08 00:09:44 -04:00
# 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:
```ink
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:
```sh
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`:
```lua
<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`:
2026-08-08 00:09:44 -04:00
```lua
Oracle = class("Oracle", Entity)
Oracle:include(Dialogable)
Oracle:include(Walker)
2026-08-08 00:09:44 -04:00
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()
2026-08-08 00:09:44 -04:00
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:
```lua
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:
```lua
{ 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`:
```lua
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:
```ink
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.