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-seed → ImportPack). 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
overworldtoggle (atoggle_defsverb). It’s implemented as a pack-globalroomdisplay template that returnsnilfor 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’scoord()—@for the player, landmark icons viahas_room_flag, a coarse!where an adjacent cell holds a visible roaming mob (viahas_visible_creature()— presence only, never identity), and a Midgaard/Darkwood label box where an edge room opens onto a neighbour zone. Every plains room carries theopen_sightflag, which is what permits that nearby-mob disclosure; it is mutual, so anopen_sightroom can disclose only into anotheropen_sightroom — a dungeon can’t scry a field and a field can’t disclose into an unflagged room. (Note: the deployedreferencepack drops the legacy directmarket → groveshortcut 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-instanceself.stateto remember who it has met. Note its seed is writtenstate.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-reschedulingmud.afterloop the instant the reset places it (entity scripts are otherwise lazy, so it would sit inert until a player walked in), and its reset carriesroam: trueso 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_leavehandler readsev.dirand callsself: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.bossworld event withon_world(...)andmud.spawn; when the goblin chief dies, itson("death")firessignal_world("boss.died", ...)and the director reschedules it a week later. - Reactions: an archmage counters a spell with an
on("BeforeCastCommit")hook callingrx:cancel(); a warden buffs its AC for one swing withon("ToHit")andrx:modify("ac", 5). - A room affect: the cave spider webs entrants via
on("enter")andself: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
scoreandwhosheets are content Lua using theuitoolkit; theroomsurface renders the overworld minimap (returningnilto 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
horrordamage type withtarget_resource: sanity, so every source of horror damage routes there automatically — no per-opresourceanywhere in the pack; - a non-vital
sanitypool whoseon_depletedapplies aninsaneaffect. At 0 the character is permanently broken, not dead — which is the whole point, and exactly why the pool must not be markedvital: 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
horrorpack. Publishing a second pack is not a solo change: the pack-set consistency check fatally refuses to boot a process whoseTELOS_CONTENT_PACKSdisagrees with the manifest’s published set, and staging pinsreference— so a new published pack hard-failstelos-worldandtelos-accounton 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.