Skip to content

Demo Pack

Kurt edited this page Jul 23, 2026 · 6 revisions

Demo Pack

Audience: Builder Status: ✅ Ready

The demo pack is the reference world that ships in the repo (internal/content/packs/demo). It's a complete, playable example every builder can read as a template: four connected zones, mobs with scripted behavior, items, abilities, loot, crafting, chargen, and display templates — all in the directory-tree pack format. If you want to see how a real pack fits together, read the demo alongside the Pack Entity Reference.

What it is and how it loads

The demo is embedded into the binary (//go:embed) for tests and bare dev runs, and seeded into Postgres by make seed (telos-seedImportPack). Its manifest (pack.yaml) is deliberately tiny — pack: demo and default_combat: melee — and the rest lives in per-section files and a zones/ subtree. A minimal core pack is its bootstrap counterpart: core is layered under every real pack so the world always has at least a lobby, even before demo content is seeded (see Content Loading & Hot Reload).

The three zones

Zone Feel Notable content
midgaard the home town / hub — where players spawn the temple, the market, a guild hall; the friendly quartermaster mob
darkwood the wilderness across the market boundary a grove, a cave; the goblin-chief world boss + its herald warstone; a cave spider; an archmage; a wandering wisp; a stalker that chases fleeing players
crypt a dungeon skeletons and a tomb-guardian with full stat sheets and aggro
overworld "The Open Plains" — a 6×20 wilderness between town and forest 120 generated grid rooms (gen.go), landmark rooms (lake / house / hill), roaming mobs, and an opt-in minimap

The zones are wired together by cross-zone exits. Two paths link midgaard and darkwood: the direct north out of the market into the darkwood grove (north: darkwood:room:grove), and the scenic route — exit steps out the market's north gate onto the plains, which you cross and then enter into the darkwood forest edge. When zones are co-hosted on one shard a crossing is an in-process move; across shards it becomes a cross-shard handoff. (Cross-pack exits are not allowed — a pack is a self-contained world.)

The overworld minimap is opt-in: it's off until the player flips the pack's overworld toggle (a toggle_defs verb). It's implemented as a pack-global room display template that returns nil for every ordinary room (so the built-in render is used) and, only for a plains room and a toggle-on viewer, draws an ASCII 5×5 minimap centred on the player's coord()@ for the player, landmark icons via has_room_flag, and a Midgaard/Darkwood label box where an edge room opens onto a neighbour zone. (Note: the deployed reference pack drops the legacy direct market → grove shortcut in favour of the plains route; the embedded demo fixture keeps it for test parity.)

Reading the tree as a template

Under zones/00-midgaard/ the numeric-prefixed files show the canonical split:

packs/demo/
  pack.yaml                 # manifest: pack name + default_combat
  attributes.yaml           # pack-global def sections...
  resources.yaml
  damage_types.yaml
  abilities.yaml            # fireball, lightning bolt, life drain, cure, craft/salvage verbs
  affects.yaml
  combat_profiles.yaml      # the "melee" profile
  channels.yaml            # gossip / newbie / guild
  loot_tables.yaml  rarity_tiers.yaml  affix_defs.yaml
  recipes.yaml  wear_slots.yaml  tracks.yaml  bundles.yaml  chargens.yaml
  display_defs.yaml        # score + who sheets
  regions.yaml  spawn_schedules.yaml
  zones/
    00-midgaard/
      00-zone.yaml  10-rooms.yaml  20-items.yaml  30-mobs.yaml  40-resets.yaml
    01-darkwood.yaml       # a whole zone can also be one file
    02-crypt.yaml
    03-overworld/          # the 6×20 plains — rooms generated by gen.go

The prefixes are a readability convention — the loader merges by ref, not by position (see Pack Authoring). Each file maps directly to a section in the Pack Entity Reference.

Scripted behavior worth studying

The demo is also the best worked reference for Lua scripting and hooks:

  • The quartermaster greets each newcomer once, using an on("greet") trigger and per-instance self.state to remember who it has met. Note its seed is written state.greeted = state.greeted or {} — the reload-safe idiom, since a hot reload re-runs the registration body against preserved state (a bare = {} would re-greet everyone after every reload).
  • A wandering wisp drifts along the darkwood chain: an on("spawn") hook arms a self-rescheduling mud.after loop the instant the reset places it (entity scripts are otherwise lazy, so it would sit inert until a player walked in), and its reset carries roam: true so the zone-wide population count doesn't leak a replacement each repop.
  • A stalker lurks in the sanctum and follows a fleeing player: a witness_leave handler reads ev.dir and calls self:move(ev.dir). It fires on the flee path as well as the walk path — which matters, because fleeing is the only way out of combat.
  • The world-boss loop spans zones and the director: a herald warstone reacts to a spawn.boss world event with on_world(...) and mud.spawn; when the goblin chief dies, its on("death") fires signal_world("boss.died", ...) and the director reschedules it a week later.
  • Reactions: an archmage counters a spell with an on("BeforeCastCommit") hook calling rx:cancel(); a warden buffs its AC for one swing with on("ToHit") and rx:modify("ac", 5).
  • A room affect: the cave spider webs entrants via on("enter") and self:apply_affect("web").
  • Lua-scripted abilities: lightning bolt and life drain roll dice in Lua and route damage through the gated ctx.target:damage{...} funnel.
  • Display templates: the score and who sheets are content Lua using the ui toolkit; the room surface renders the overworld minimap (returning nil to fall back to the built-in render everywhere else).

The Sanity track: a worked secondary-vital system

The pack ships a Call of Cthulhu / Delta Green–shaped horror track, and it is worth reading as the acceptance artifact for the resource/vital family — proof those primitives compose into a system rather than merely passing their own tests. Three pieces, no glue:

  • a horror damage type with target_resource: sanity, so every source of horror damage routes there automatically — no per-op resource anywhere in the pack;
  • a non-vital sanity pool whose on_depleted applies an insane affect. At 0 the character is permanently broken, not dead — which is the whole point, and exactly why the pool must not be marked vital: under the any-vital-is-lethal policy that would kill them instead;
  • the depletion arithmetic is there to read if content wants the magnitude of the blow that broke them.

It is inert for everything that already exists — structurally, not by convention. max_sanity derives from an attribute whose base defaults to 0, so every pre-existing player and mob has no capacity in the pool and the engine's own capacity discard makes them immune to horror damage. No engine predicate, no content check, nothing to remember: only content that raises that attribute has a mind to lose. (That is the same lever max_reactions already uses.)

The hook is written to the authoring rules the level-triggered depletion hook forces: it guards with if has_affect and gives the affect stacking: ignore, because the hook re-enters on every blow onto a pool already sitting at 0; and it contains no rewarding op, because a pool held at 0 can be hit on purpose.

Why it lives in the shipped pack rather than a separate horror pack. Publishing a second pack is not a solo change: the pack-set consistency check fatally refuses to boot a process whose TELOS_CONTENT_PACKS disagrees with the manifest's published set, and staging pins reference — so a new published pack hard-fails telos-world and telos-account on the next deploy unless the infra ConfigMap changes in the same window. See Content Pack Operations.

Because it exercises nearly every engine surface, the demo doubles as a regression fixture — it's expected to render consistently across the engine's content migration.

Clone this wiki locally