Walkthrough: Lamp return Inhalt

Plugins

Walkthrough: Lamp return

Kelister's death built step by step from its real source: resting at a Lamp, the stain where you fell, waking at nightfall, walking back, and trying it before it meets a player.

What we're building

In Kelister, death is a setback, not an end. When a character dies:

  1. the money they carried is left where they fell, as a stain;
  2. they wake at the last Lamp they rested at, at nightfall, whole;
  3. they can walk back and gather the stain up;
  4. if they die again first, the old stain is gone for good.

This page builds that step by step from the real source of Lamp return in the catalog. You can type it into a new plugin as you go, or add Lamp return from the catalog, which has all of it, and read along. You can also just ask Hob for it.

1. Start a plugin

In the Plugins tab, press New plugin and call it Lamp return. Under What it does, say in plain words what it makes happen, for you, your builders and Hob:

What it does
Death is a setback, not an end. Dying drops the money a character carries as a stain where they fell; they wake at the last rest place they rested at, at nightfall, whole. Reaching the stain gives it back; dying again first loses it for good.

2. Settings

Lamp return lets each world choose what counts as a rest place and when you wake. These are its settings, declared with it (the catalog copy has them, and Hob declares them for a plugin it writes). The code reads them from wt.config.

restTag:        text    "Tag of a rest place"                        default: lamp
restKinds:      list    "Kinds of place where any rest counts"       default: (none)
restPlaces:     list    "Rest places by name"                        default: Lamp
wakeAt:         choice  "They wake at"  nightfall, dawn, morning, evening, night
hollowingTrack: text    "A track that rises with each death"         default: (none)

3. Words

Everything players read, and every fact the storyteller is told, is kept under Words players see, so a language edition can translate it. A name in braces is filled in by the code. These are Lamp return's:

deathRule       A player character can be killed. They do not stay dead: the rules wake them at the last place they rested at, and what they carried is left where they fell.
restBound       {name} rested at {place}: if they die, this is where they will wake.
oldStainGone    The stain they left at {place} before is gone for good.
wakeReason      woke at their last rest
woke            {name} was killed by {killer} at {fell}. They have just woken at {lamp} (not where they fell), at nightfall, whole, unwounded, but shaken: they remember dying. Their purse is empty. Open the scene on that waking, short and plain.
stainLine       Everything they carried ({amount}) was left behind where they fell, at {place}, far from here: they do not have it and nobody here has seen it.
stainStanding   Their stain: {amount} they lost when they died lies at {place}; it is theirs again only if they go back there.
stainTitle      Where you fell
diedTitle       You died
diedBody        You wake at {lamp}.
stainFound      {name} reaches the place they fell and gathers up what they lost there ({amount}).
stainFoundTitle Recovered
stainFoundBody  You gather up {amount} where you fell.

Notice what the facts for the storyteller say: where they woke, that it is not where they fell, that their purse is empty, that nobody here has seen the stain. Each line closes a door the storyteller might otherwise walk through.

4. Death can happen here

By default the storyteller never narrates a player's death. A plugin that takes deaths over sets the world's own rule, once, in onEnable:

export default {
  onEnable(wt) {
    wt.gm.deathRule(wt.t('deathRule'));

    // the pieces below go here, inside onEnable
  },
};

5. What counts as a rest place

A small helper answers one question: is this place a rest place? It is if it carries the tag, if it is one of the kinds where any rest counts, or if its name contains one of the rest place names.

const isRestPlace = (place) => {
  if (!place) return false;
  if ((place.tags || []).includes(wt.config.restTag)) return true;
  if ((wt.config.restKinds || []).includes(place.kind)) return true;
  const names = wt.config.restPlaces || [];
  return names.some((n) => n && place.name.toLowerCase().includes(String(n).toLowerCase()));
};

6. Resting binds the waking

rest fires when the storyteller gives a character a rest. If it is a rest place and not already theirs, the plugin keeps it in this character's store and tells the storyteller.

wt.on('rest', async (e) => {
  if (!isRestPlace(e.place)) return;
  if (e.character.store.lamp !== e.place.id) {
    e.character.store.lamp = e.place.id;
    e.character.store.lampName = e.place.name;
    await wt.gm.tell(wt.t('restBound', { name: e.character.name, place: e.place.name }));
  }
});

The store is saved when the event ends: no save call is needed.

7. The death

The death listener runs at high priority, so it decides before ordinary listeners, and cancels the event: this world's death replaces the usual fall. Then it takes what they carried, raises a Hollowing track if the world has a rule system, and records the stain (or none, if they carried nothing). If an old stain was still lying somewhere, the storyteller is told it is gone.

wt.on('death', { priority: 'high' }, async (e) => {
  e.cancel();
  const c = e.character;
  const fell = e.place || c.place;
  const carried = c.money.amount;
  const carriedWords = c.money.words;
  const old = c.store.stain;
  if (old) await wt.gm.tell(wt.t('oldStainGone', { place: old.placeName }));
  if (carried > 0) await c.addMoney(-carried);
  if (wt.config.hollowingTrack && c.sheet) {
    try { await c.effects([{ kind: 'track', track: wt.config.hollowingTrack, op: '+', amount: '1' }]); }
    catch (err) { wt.log('no track', wt.config.hollowingTrack, String(err)); }
  }
  c.store.stain = carried > 0 && fell
    ? { place: fell.id, placeName: fell.name, amount: carried, words: carriedWords }
    : null;
  if (!c.store.stain) delete c.store.stain;

8. Waking at the Lamp

Where they wake: the Lamp they bound, else the first tagged rest place in their home region, else any tagged one. They are moved there at once (no travel time, out of any fight), the clock moves on to nightfall, and they are healed in full.

  let lamp = c.store.lamp ? await wt.places.get(c.store.lamp) : null;
  if (!lamp) lamp = await wt.places.find({ tag: wt.config.restTag, region: c.homeRegion || undefined });
  if (!lamp) lamp = await wt.places.find({ tag: wt.config.restTag });
  if (lamp) await c.moveTo(lamp.id, { mode: 'respawn', reason: wt.t('wakeReason') });
  await wt.clock.advanceTo(wt.config.wakeAt || 'nightfall');
  await c.heal('full');

9. Telling the storyteller and the player

Last, the facts. A standing line keeps the stain in every turn's context until it is gathered, so the storyteller never forgets it or makes up another. The card takes the player to the room they woke in (navigate), and a headline section on the sheet shows where they fell.

  const stainLine = c.store.stain
    ? wt.t('stainLine', { amount: c.store.stain.words, place: c.store.stain.placeName }) : '';
  await wt.gm.remember('stain', c.store.stain
    ? wt.t('stainStanding', { amount: c.store.stain.words, place: c.store.stain.placeName }) : null);
  await wt.gm.tell(wt.t('woke', {
    name: c.name,
    killer: (e.killer && e.killer.name) || 'their wounds',
    fell: fell ? fell.name : 'the dark',
    lamp: lamp ? lamp.name : c.place.name,
  }) + (stainLine ? ' ' + stainLine : ''));
  await wt.ui.card(c, {
    title: wt.t('diedTitle'),
    body: wt.t('diedBody', { lamp: lamp ? lamp.name : c.place.name }) + (stainLine ? ' ' + stainLine : ''),
    tone: 'grave',
    navigate: true,
  });
  if (c.store.stain) await wt.ui.sheet(c, {
    id: 'stain', title: wt.t('stainTitle'), tone: 'grave', headline: true,
    lines: [wt.t('stainStanding', { amount: c.store.stain.words, place: c.store.stain.placeName })],
  });
  else await wt.ui.sheet(c, { id: 'stain', remove: true });
});

10. Walking back

When a scene moves the character to the place where their stain lies, they gather it up: the money comes back, the standing line and the sheet section go, and the storyteller and the player are told.

wt.on('place.enter', async (e) => {
  const s = e.character.store.stain;
  if (!s || !e.place || e.place.id !== s.place) return;
  await e.character.addMoney(s.amount);
  delete e.character.store.stain;
  await wt.gm.remember('stain', null);
  await wt.gm.tell(wt.t('stainFound', { name: e.character.name, amount: s.words }));
  await wt.ui.card(e.character, { title: wt.t('stainFoundTitle'), body: wt.t('stainFoundBody', { amount: s.words }), tone: 'bright' });
  await wt.ui.sheet(e.character, { id: 'stain', remove: true });
});

11. Rest places in your world

The plugin needs rest places to find. Name them with a word from Rest places by name (any place called something with "Lamp" in it), count every rest in a kind of place (Kelister counts towns and their landmarks), or tag places. Tagged places matter for one more reason: a character who never rested at a Lamp wakes at a tagged one, so tag at least one per region you expect people to die in.

12. Try it

Save, then open Try it. It runs the plugin on your own character in this world as a dry run: nothing changes, and what it would do is listed.

  • Pick A hero rests and give the moment a rest place in What it carries. The list shows the fact it would tell the storyteller and the store it would keep: the Lamp is bound.
{ "kind": "long", "place": { "id": "site-feldon", "name": "Feldon", "tags": ["lamp"] } }
  • Pick A hero dies (the cause is filled in) and run it. It says it cancelled the event, and lists the money taken, the move, the clock set to nightfall, the heal, the standing line, the facts for the storyteller, the card and the sheet section.

13. In play

Switch it on and play. Rest at a Lamp, then die. The death is narrated plainly; the next scene opens where they woke, at nightfall. What it did shows each run, and undo refuses the turns the plugin changed: a death taken over can't be undone.

Making it yours

  • A bonfire, a waystone, a campfire: change Rest places by name and the words.
  • Wake at dawn instead: change They wake at.
  • In a world with a rule system, add a track for each death and name its id in A track that rises with each death; a card on that track can do something when it fills.
  • Want something other than money left behind? Ask Hob: describe it in words, and it will change the death listener and try it.