NPC Behaviour

NPCs in the archipelago act according to two systems: declarative behaviour rules and Rhai scripts. Rules handle the common cases cheaply. Scripts handle everything else. Both execute within the standard signal dispatch pipeline — an NPC’s actions are indistinguishable from a player’s at the engine level.

Behaviour rules

Rules are when/then statements evaluated in order when a signal arrives at the NPC’s room. The first matching rule fires. If no rule matches, the NPC does nothing.

- when: signal.name == "enter" and signal.actor.type == "player"
  then: say "Evening. Road's washed out tonight. You'll stay."

- when: signal.name == "say" and signal.targets includes self
  then: say "Mm."

Predicates

Rules can check:

PredicateMeaning
signal.name == "<name>"The signal type (enter, say, give, take, etc.)
signal.actor.type == "<type>"Actor is a player, NPC, or system
signal.targets includes selfThe NPC was directly addressed
room.occupants.count > NRoom population threshold
self.inventory contains "<tag>"NPC is carrying something tagged
actor.tags includes "<tag>"The actor has a specific tag
has tag "<tag>"The NPC itself has a tag
trust < NActor’s trust level is below threshold
count(signals in 5m where name == "say") > NRate-based triggers
random(0.3)30% chance of firing
actor carries item with origin: "<realm>"Cross-entity provenance check

Effects

EffectWhat it does
say "..."Speak aloud to the room
emote "..."Perform a visible action
move <direction>Leave the room
abortPrevent the triggering action
emit "<signal>"Emit a custom signal

Evaluation order

Rules are evaluated top to bottom. The first match wins. Place specific rules before general ones:

# Specific: respond to being given a weapon
- when: signal.name == "give" and signal.args.item.tags includes "weapon"
  then: say "I don't deal in blades. Take it back."

# General: respond to being given anything else
- when: signal.name == "give"
  then: say "Interesting. I'll hold onto this."

Canned lines

For simple NPCs that do not need conditional logic, a line pool is sufficient:

lines:
  - "Evening. Road's washed out tonight. You'll stay."
  - "Your cargo's your own, long as it stays sheathed."
  - "The beacon's been dark since sundown. Don't expect news."

Lines are selected randomly when the NPC is addressed. They cycle without immediate repetition. This is the cheapest form of NPC personality — no rules evaluation, no script execution.

The harbormaster

Every realm’s border room has a harbormaster NPC. This is a special case — configured in archipelago.toml rather than authored through build mode:

[realm.harbormaster]
name = "Old Cael"
brief = "A weathered man in an oilskin coat, standing by the gate."
lines = [
  "Welcome to the Stair. Road's washed out tonight. You'll stay.",
  "Your cargo's your own, long as it stays sheathed.",
  "The beacon's been dark since sundown. Don't expect news from the north.",
]

The harbormaster has additional responsibilities beyond canned lines:

The harbormaster is scripted — a greeter with a name and voice. Write it well — it is your island’s first word to every visitor.

NPC presence and lifecycle

NPCs are always present in their assigned room. They do not log in or out. They do not have sessions. They are fixtures.

An NPC can be moved between rooms by a builder (chart> move grak to market), but it does not wander on its own unless a behaviour rule or script moves it.

NPCs with the cognitive loop active observe, remember, and respond contextually. The baseline — rules and canned lines — works without it.

Capability scoping

NPCs hold capabilities like any other entity. A guard NPC with an abort rule can prevent players from passing — but only if the NPC holds the capability to abort signals in that context. A shopkeeper can give items from its inventory but cannot access locked rooms.

Scripts attached to NPCs execute with the NPC’s capability bag. A script cannot escalate beyond what the NPC is authorised to do.