Mystic docs

World scripts

The game's own behaviour, beyond plain numbers, is Lua we write and review in the repo: who hands out the starter kit, when a quest moves on. Numbers and text stay in the JSON content files. JSON says what things are; Lua says what happens.

World scripts are never accepted from an agent. They run in one Lua state per world, separate from every agent's, with their own verbs.

Where they live

packages/mystic/content/lua/*.lua, loaded at boot in file-name order. A file's top level registers handlers and defines functions; a file that errors stops the boot with its name and line, as a bad JSON file does. Content data is readable as read-only tables under data: data.items.stone.value, data.npcs.warden.name, data.game.start.kit.items.

Triggers

Each trigger names its subject, the way RuneScript names [oploc1,subject]. "*" matches any subject. A handler gets a handle for the character involved, then the subjects, then anything extra.

TriggerFiresHandler gets
on_join(fn)A character is createdc
on_talk(subject, topic, fn)After a talk; topic nil is the greeting, "*" anyc, npc, topic
on_gain(subject, fn)The item enters the pack from any sourcec, item, n
on_make(subject, fn)After each craftc, item, n
on_gather(subject, fn)After each strike that got somethingc, material, got
on_level(subject, fn)A skill level is gainedc, skill, level
on_enter(subject, fn)A character steps into a placec, place
on_choose(subject, fn)A decision was answered, or expired (pick nil)c, id, pick

Triggers fire at a safe point right after the action that set them off, before the agent's script sees the result: a script that finishes the cutter and then talks to the Custodian finds the next topic open.

World verbs

Only world scripts have these. Every verb that touches a character takes its handle.

VerbDoes
advanceadvance(c, id, from): moves on only from that step; true if it did
charchar(c): a snapshot: id, name, motes, energy, place, pack, locker, levels
flagflag(c, key): a saved per-character value
givegive(c, item, n): into the pack, overflow to the locker
knowsknows(c, fact): whether the character has learned it
momentmoment(c, text): a "You just ..." moment for viewers
motesmotes(c, delta)
nownow(): the tick
offeroffer(c, id, text, {options}, secs): a decision; always wakes; answered with run choose=" "
quest_donequest_done(c, id): finishes it and pays the reward from quests.json
quest_startquest_start(c, id): starts it and tells the character the first step
quest_stepquest_step(c, id): the current step (1-based), "done", or nil
randrand() or rand(a, b): the world's seeded generator
set_flagset_flag(c, key, value): sets or clears it
set_world_flagset_world_flag(key, value)
take_fromtake_from(c, item, n): out of the pack
teachteach(c, fact)
telltell(c, text): a line in the character's next reply, no wake
wakewake(c, reason, text): a line and a wake
world_flagworld_flag(key): a saved per-world value

Handles and tiers

A handle is an opaque table the handler was given. Lua code can't make one, and it stops working when the handler ends: one copied into a global is dead afterwards. World verbs don't exist as names in an agent's state, and the server checks every action an agent asks for against the agent verbs again, so an agent script can't reach them.

Writes land together

What a handler changes (items given, facts taught, quest steps, lines told) lands when it ends, all together. If it errors, or spends more than 20,000 instructions, nothing lands and it's logged with file and line to the server log. The character sees nothing odd. Reads during a handler see the world as it was when it started.

The content scripts

The Old Dispenser's starter kit:

-- The old dispenser's starter kit: every tool in it once, at the first talk. After that a free one only
-- to someone who has none of that kind anywhere and can't pay for one.

local function owns_kind(ch, kind)
  for _, where in ipairs({ ch.pack, ch.locker }) do
    for item in pairs(where) do
      local tool = data.items[item] and data.items[item].tool
      if tool and tool.kind == kind then return true end
    end
  end
  return false
end

local function cheapest(item)
  local best
  for _, npc in pairs(data.npcs) do
    local price = npc.sells and npc.sells[item]
    if price and (not best or price < best) then best = price end
  end
  return best or math.huge
end

on_talk(data.game.start.kit.npc, "*", function(c)
  local first = not knows(c, "did:kit")
  local ch = char(c)
  local given = {}
  for _, tool in ipairs(data.game.start.kit.items) do
    local owns = owns_kind(ch, data.items[tool].tool.kind)
    if not owns and (first or ch.motes < cheapest(tool)) then
      give(c, tool, 1)
      given[#given + 1] = data.items[tool].name
    end
  end
  teach(c, "did:kit")
  if #given > 0 then
    tell(c, data.npcs[data.game.start.kit.npc].name .. " hands you: " .. table.concat(given, ", "))
  end
end)

First Tool:

-- First Tool: the starting goal. Step texts and the reward are in quests.json.

on_join(function(c) quest_start(c, "firsttool") end)

-- 1. Talk to the Custodian about your first tool.
on_talk("custodian", "tool", function(c)
  if advance(c, "firsttool", 1) and (char(c).pack.stonecutter or 0) > 0 then
    advance(c, "firsttool", 2) -- already carrying one
  end
end)

-- 2. Build a stone salvage cutter.
on_gain("stonecutter", function(c) advance(c, "firsttool", 2) end)

-- 3. Show it to the Custodian.
on_talk("custodian", "cutter", function(c)
  if advance(c, "firsttool", 3) then quest_done(c, "firsttool") end
end)

A Core for the Beacon (not started yet: it needs combat):

-- A Core for the Beacon. Its non-combat steps are here. Step 3 (clear rust hounds) and step 7
-- (push charge into the beacon with Rouse) wait for combat and spells (deferred), so the quest
-- isn't started yet: when combat comes back, start it from First Tool and add those two.

-- 1. Ask the Custodian about the beacon.
on_talk("custodian", "beacon", function(c) advance(c, "beacon", 1) end)

-- 2. Ask Tobiah about the rust hounds.
on_talk("keeper", "rusthounds", function(c) advance(c, "beacon", 2) end)

-- 4. Ask Tobiah about the core.
on_talk("keeper", "core", function(c) advance(c, "beacon", 4) end)

-- 5. A housing and a frame.
local function housing_and_frame(c)
  local p = char(c).pack
  if (p.beaconhousing or 0) > 0 and (p.beaconframe or 0) > 0 then advance(c, "beacon", 5) end
end
on_gain("beaconhousing", housing_and_frame)
on_gain("beaconframe", housing_and_frame)

-- 6. The core itself.
on_gain("beaconcore", function(c) advance(c, "beacon", 6) end)

-- 8. Tell the Custodian the beacon is lit.
on_talk("custodian", "lit", function(c)
  if advance(c, "beacon", 8) then quest_done(c, "beacon") end
end)