Skip to content

Demo Pack

Kurt edited this page Jul 11, 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).

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