-
-
Notifications
You must be signed in to change notification settings - Fork 3
World Objects
Chests, clickable objects, map icons and the names that let a record address something you placed. All of it is the engine's own object layer — the SDK only builds the records the shipped scripts build.
Verified on Sacred Gold Steam, build 2.0.2.28 (2006-10-13), live in game.
- A chest
- A quest item on the ground
- Loot
- Clickable objects
- Map icons
- Names and handles
- Doors and locks
- What the engine will not do
local W = require "world"
local c = W.chest{
x = 2796, y = 2287,
gold = 100, -- paid once, when it is first opened
spill = { 5135 }, -- a pile of coins on the floor beside it
on_open = function(c) sacred.log("opened " .. c.name) end,
}A chest is one CreateObj record carrying a name and a take section —
the shape StartCode uses 1,805 times. Opening it runs that section, which also
reaches your Lua as the on_open callback.
| Field | Meaning |
|---|---|
x, y (or pos) |
Where. A named position works too. |
type |
W.CHEST.plain (default), .small, .wide, .tall, .book. Types 5214..5226 may hold a trap or a mimic, so they are not in that list. |
name |
The object's own name; other records address it by this. Generated if omitted. |
gold |
Gold paid once on the first open, with the engine's "+N" popup. The paid flag is an engine variable, so a savegame remembers it; c:arm() lets it pay again. |
spill |
Object types dropped on the floor around it when opened. |
records |
Extra records for the take section (a banner, a journal line). |
on_open |
Lua callback, on the heartbeat after it opens. |
create = false |
Do not place an object, only (re)define its sections — what a later session needs for a chest already in the world. |
Methods: c:find() (its handle), c:fill(opts) / c:refill(opts),
c:arm(), c:lock() / c:unlock() / c:open() / c:close().
A chest can be opened once. That is the engine's behaviour, not ours: plan one payout per chest, and place a second chest if the quest owes another.
Two ways, and vanilla uses both:
-
On the floor.
spill = { 5135, 5134 }drops piles the player picks up. The types are the four the engine's own chest fill uses — 5132, 5133, 5134, 5135 — and the engine prices them itself, scaling with the hero: 600 gold per pile at the level this was measured on, the same for 5134 and 5135, so the type only chooses the sprite. Any object type can be spilled: a weapon, a potion, a quest item. -
An exact amount.
gold = 100pays exactly that, through the engine's AddGold record with its "+N" popup, wrapped in an IF on a variable so it pays once.
W.hotspot{ x = 2793, y = 2288, type = 6029,
records = Vb.info("YOU_PICKED_THE_BERRIES"),
on_click = function() ... end }A hotspot is a MouseEvent record: the object of that type at that spot runs a
section when the player clicks it. Both shipped forms work — by type (type =)
and by name (res = "res:MY_BUSH", with mode = 1).
A container is not a hotspot. Clicking a chest or a bookshelf opens it, and the click never reaches a hotspot. Give containers a take section instead (
W.chest{...}does), and put hotspots on pickable objects — the gold-berries type 6029 the shipped quest NQ04 uses is a good test object.
W.map_icon(2796, 2287, 2) -- 2 = a cave mouth, 1 = a portal glyphIcon 2 is what the shipped scripts use almost everywhere (303 records against 30 for icon 1).
A record addresses an object by name, and the engine only binds a name it already knows. The SDK asks it to learn one:
| Call | What it does |
|---|---|
sacred.name_register(name) |
Give a new name an id, through the engine's own registrar (89 dynamic slots). Without this a CreateObj name is silently dropped and nothing can address the object afterwards. world.lua does it for you. |
sacred.object_by_name(name) |
The handle of a named object. |
sacred.object_at(type, x, y [, r]) |
The nearest object of that type within r tiles — no name needed at all. |
sacred.section_run(name, handle) |
Run a section with that object as the context, for records that act on "this object". |
The daily fetch quests' target, as the shipped scripts write it (DQ_15054, "The
Axe of Galadius"): 01 'res:<name>' 02 <type> 04 <pos> 41 'od_<id>' 8e 61.
local S, A, Vb = require "sections", require "actions", require "verbs"
S.define("od_my_axe") -- its Take: section
sacred.on_trigger("SECTION:od_my_axe", function() ... end)
A.run(Vb.create_obj(1722, { 2510, 2395, 0 },
{ res = "res:MY_AXE_NAME", take = "od_my_axe", place = true, give = true }))-
place(op0x8e) lays it in the world; without itgivehands the item over straight away.give(op0x61) makes it a quest item: picked up, it goes to the hero's quest-item list. - Picking it up runs the
Take:section, which reaches Lua asSECTION:<take>. Live.sacred.hero_items()makes a good second signal: the entry shows up withwhere = "quest". - The object is in the savegame, the
Take:section is not — define it again every session.sacred.object_at(type, x, y, r)tells whether the item still lies there. - To drop one where a creature died, use the position name
"CPOS:res:<name>"in its death hook (Runtime NPCs).
W.lock(name) / W.unlock(name) / W.open(name) / W.close(name) act on a
named object through SetObjState. They are a trigger thing — doors and gates
— because the record sets the lock bit on a cTrigger. A container ignores
them; gate a chest by when you create or fill it instead.
-
Fill a chest the SDK created. The engine's own FillChest record leaves it
empty even when the chest is the section's context and the type is a real
container. Its
Take:hook cannot fill it either: the engine runs that hook with the hero as the context, and FillChest fills its context. Usespillorgold. - Lock a container — see above.
Getting started
Authoring
- Quests and Dialog Authoring
- Native Quests
- Runtime NPCs
- Hero Classes
- Dialog Text
- Dialog Nodes (catalogue)
The engine's own verbs
Reference