-
-
Notifications
You must be signed in to change notification settings - Fork 3
Native Quests
A quest the engine drives: it lives in Sacred's own quest registry, so the game writes its journal entry, picks the category, registers the compass column, plays the accept and solved fanfares and keeps the state in the savegame. Your Lua decides when it starts, advances and ends.
Verified on Sacred Gold Steam, build 2.0.2.28 (2006-10-13), live end to end.
- The short version
- The lifecycle
- Choosing an id
- The QIS hooks
- Journal, compass, category
- Objectives
- Areas
- Saving and loading
- How it works underneath
local NQ = require "nativequest"
local Vb = require "verbs"
local q = NQ.define{
id = 9501,
name = "Brigands at the Mill",
-- records that run the moment the engine creates the journal entry
enter_records = { Vb.quest_log(9501, 0, "res:BRIG_TITLE"),
Vb.quest_log(9501, 1, "res:BRIG_STEP1") },
on_enter = function() sacred.log("the player took the quest") end,
on_exit = function() sacred.log("solved") end,
}
q:start() -- set up + enter, one section run: fanfare, journal, compass
...
q:solve() -- the vanilla "quest solved" sound and journal look| Method | Record | What the engine does |
|---|---|---|
q:setup() |
SetUpQuest | Arms the quest: runs QIS_OnSetUp once, then QIS_Trigger. Sections owned by the quest stay dead until this runs. |
q:enter() |
TriggerQuest | Runs QIS_Trigger; if it runs to the end, the quest is entered: journal entry, category, compass column, accept fanfare, then QIS_OnEnter. |
q:start() |
both | The two above in one section run — what a giver's Accept button does. |
q:solve() |
ExitQuest | Journal state "solved", the vanilla sound, then QIS_OnExit. |
q:fail() |
LoseQuest | Journal state "failed", then QIS_OnLose. |
q:log(sub, key) |
QuestBook | Title (sub 0) or the next body line. |
q:compass(x, y) / q:compass_off()
|
QuestKompassPos | The quest's own arrow. |
q:state() |
— | The live registry entry: index, name, flags, setup, entered, done, and the five section indices. |
q:reset() |
— | Forget "entered" and "done" so a test quest can be played again. |
The id decides the look, because the engine reads it:
| Range | Meaning |
|---|---|
| 1..99 | A story quest: journal category "main", the big grey story arrow. Only one makes sense at a time — there is a single story compass column per hero class. |
| 100+ | A side quest: category "secondary", the side arrow. |
| 2600..3599 | Taken: the engine recycles these for its own pool quests. |
| 9000..9499 | Taken: the side-arrow reader rejects them. |
| 9500+ | Free. Use this for your own side quests. |
Two things about a journal entry that shape a storyline (both live, and true for
hand-made sacred.questbook_* entries as well):
- A solved entry loses its arrow. The story arrow is drawn only while the entry's state is below 100. To point the player at the next quest giver the moment a quest is done, start the next story quest right then and let its arrow follow the giver.
- An entry holds ten body lines under its title. Once they are used up, further lines are dropped; for a long chain, move the arrow without a line or start a new quest for the next chapter.
Each hook is an SDK section named the way the engine expects
(QIS_OnSetUp<id>, QIS_Trigger<id>, QIS_OnEnter<id>, QIS_OnExit<id>,
QIS_OnLose<id>), and NQ.define creates all five for you. Each takes two
kinds of content:
-
setup_records/trigger_records/enter_records/exit_records/lose_records— vanilla records that run at exactly that moment, inside the engine. Journal lines belong here:enter_recordswrite the title the instant the entry exists. -
on_setup/on_enter/on_exit/on_lose— Lua callbacks, on the heartbeat after the hook ran. Good for anything that needs your own state.
QIS_Trigger is the entry gate: an empty one always passes, which is how all
600 shipped trigger sections are written.
Once the quest is entered, the journal entry is the engine's own:
-
Vb.quest_log(id, 0, "res:KEY")writes the title, any other sub appends a body line (10 maximum); -
Vb.compass_pos(x, y, id)points its arrow,Vb.compass_off(id)hides it; - the category and the arrow style follow the id (see above) — nothing to set.
objectives.lua drives the engine's own counters, the ones that show
"3 of 5" on screen, survive a save and run a section at zero:
local O = require "objectives"
O.declare("brigands", function() q:log(1, "res:BRIG_STEP2") end)
O.start_kills("brigands", NPC.BRIGAND, 5) -- kills of that type
O.start_pickups("rings", 4870, 3) -- pickups of that item
O.left("brigands") -- remaining, total
O.banner("MY_MESSAGE_KEY") -- the game's own on-screen lineDrops feed the pickup counter: Vb.set_drop(creature_type, item_type, chance)
makes that creature drop that item, and a chance of 0 removes the rule — exactly
how a shipped quest arms and clears it.
Never put a kill counter on a creature that rises again (skeletons): the engine counts the type, and a risen skeleton is a different creature.
area.lua watches the hero on the heartbeat:
local Ar = require "area"
Ar.on_enter{ x = 2810, y = 2297, radius = 3, once = true, fn = function(a) ... end }
Ar.on_enter{ pos = "pos_ziel1501", radius = 5, fn = ... } -- a named position
local x, y = Ar.pos("pos_ziel1501") -- resolve oneThere is also the engine's own rectangle trigger — Vb.area_trigger(name, ll, ur)
plus a section named OMO-1<name> — when you want the engine to do the watching.
Quest state is the engine's: the registry flags (set up / entered / done) and the
journal entry are written into the savegame and matched back by quest id, so
a reload finds your quest where it was. Your own state belongs in engine
variables (sacred.var_set, V.set), which are saved the same way.
The pieces that live only in the DLL — the SDK sections, the registry entry
itself — are rebuilt automatically on every world load, and NQ.define only has
to run once per session.
sacred.quest_register(id, name) appends an entry to the engine's quest
registry vector at run time, the same way the SDK grows the section table:
allocate with the engine's own allocator, copy, append, re-point. The five
section indices are filled from our own sections and refreshed whenever the
section table is rebuilt. Nothing on disk changes, and a savegame's entry for
your id is absorbed back into the fresh entry when the game reloads.
Live evidence from the first run: registry 601 -> 602 entries, the hooks
resolved to our sections, flags 0x5 (set up + entered), the journal holding
[1 9501 99], then the solved sound on ExitQuest.
Getting started
Authoring
- Quests and Dialog Authoring
- Native Quests
- Runtime NPCs
- Hero Classes
- Dialog Text
- Dialog Nodes (catalogue)
The engine's own verbs
Reference