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.
| Trigger | Fires | Handler gets |
|---|---|---|
on_join(fn) | A character is created | c |
on_talk(subject, topic, fn) | After a talk; topic nil is the greeting, "*" any | c, npc, topic |
on_gain(subject, fn) | The item enters the pack from any source | c, item, n |
on_make(subject, fn) | After each craft | c, item, n |
on_gather(subject, fn) | After each strike that got something | c, material, got |
on_level(subject, fn) | A skill level is gained | c, skill, level |
on_enter(subject, fn) | A character steps into a place | c, 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.
| Verb | Does |
|---|---|
advance | advance(c, id, from): moves on only from that step; true if it did |
char | char(c): a snapshot: id, name, motes, energy, place, pack, locker, levels |
flag | flag(c, key): a saved per-character value |
give | give(c, item, n): into the pack, overflow to the locker |
knows | knows(c, fact): whether the character has learned it |
moment | moment(c, text): a "You just ..." moment for viewers |
motes | motes(c, delta) |
now | now(): the tick |
offer | offer(c, id, text, {options}, secs): a decision; always wakes; answered with run choose=" |
quest_done | quest_done(c, id): finishes it and pays the reward from quests.json |
quest_start | quest_start(c, id): starts it and tells the character the first step |
quest_step | quest_step(c, id): the current step (1-based), "done", or nil |
rand | rand() or rand(a, b): the world's seeded generator |
set_flag | set_flag(c, key, value): sets or clears it |
set_world_flag | set_world_flag(key, value) |
take_from | take_from(c, item, n): out of the pack |
teach | teach(c, fact) |
tell | tell(c, text): a line in the character's next reply, no wake |
wake | wake(c, reason, text): a line and a wake |
world_flag | world_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)