-
Notifications
You must be signed in to change notification settings - Fork 3
08. Quests tool: Odyssey
The Quest Tool (Odyssey) is a management system designed to handle the creation, management and tracking of quests.
Odyssey consist of 2 parts:
- The Blueprint (Quest Resource): A static template defining the narrative structure, stages, and specific objectives.
-
The Tracker (Quest Manager): The runtime object (
NexusForge.Quests) that tracks active quest states, manages progression, and maintains a historical log of completed tasks.
- Quest: The top-level container for a specific storyline or mission.
- Stage: A major milestone within a quest. A quest can contain any number of stages.
- Objective: A specific task within a stage. A stage can contain any number of objectives.
-
Requirements: Each Objective contains N requirements consisting of an Identifier (string), a Value (variant), and a Comparator (e.g.,
==,>=,<).
-
File-Based: Each quest is saved as an individual
.tresresource file. -
Dynamic Support: The
QuestManagertracks Quest objects rather than files, allowing for dynamically generated quests created via code at runtime to be properly tracked and logged.
The Odyssey editor is a three-column workspace designed for rapid quest development. It features a dynamic breadcrumb navigation at the top (e.g., new_quest / initial_stage / first_objective) to track the current hierarchy level.
The left column handles file operations and the structural hierarchy of the open quest.
-
Files Section:
- New Quest Button: Located next to the "Search Files" bar; use this to create new quest resource files.
-
Quest List: Displays active quest files; an asterisk (
*) indicates unsaved changes. Hovering over an item displays its full path.
-
Quest Tree:
- Use the [+] icons to add new Stages to the Quest or new Objectives to a Stage.
- Selected items are highlighted, updating the data displayed in the other two columns.
-
Context (Right-Click) Menu:
- Add Stage/Objective: Quickly append new children to the hierarchy.
- Edit ID: Modify the unique internal identifier for the resource.
- Duplicate: Create a copy of the selected item and its children.
- Remove: Delete the selected item from the hierarchy.
- Set as Entry: (For Stages) Explicitly define this stage as the starting point of the quest.
Note
The visual order of items in the tree is for organizational purposes only and does not dictate the flow of the quest. This layout is saved within the .godot folder; therefore, the specific tree order is not preserved if the resource is opened on a different computer.
The middle column focuses on the descriptive properties and custom data of the selected resource.
-
Identity Section:
- Type Dropdown: Categorize the item (e.g., Main Quest, Side Quest, or Stage Types like Travel, Status, Collect).
- Pencil Icon: Opens the underlying script for quick enum updates.
- Title & Description: Multi-line text fields for player-facing content.
-
Custom Data Section:
- A versatile dictionary editor for adding metadata.
- Quick-Add Buttons: Specific icons to create Folder, Integer, Float, Boolean, or String variables.
- Variables can be filtered using the "Search Custom Data" bar.
Tip
You can edit the default custom data newly created quests have by modifying the custom_data variable on the respective quest[1], stage[2] and objective[3] resource file.
The right column is context-sensitive, shifting its interface based on the hierarchy level selected in Column 1.
-
Quest Events: Manage global Success and Failure events. These are organized in a tree structure allowing for nested folders and variables (e.g., a
rewardsfolder containing anexperienceinteger and anitemsdictionary).
-
Flow Targets:
- Success Target: Dropdown to select which stage follows upon successful completion.
- Failure Target: Dropdown to select the path taken if the stage fails (or "Quest End" to terminate).
- Stage Events: Success/Failure event tree specific to this milestone.
- Objective Required: A checkbox at the top determines if this task must be finished for the stage to complete.
-
Requirements Editor:
-
Type Toggle: Click the data type icon (e.g.,
int,flt,bool,Str) to change the requirement type. -
Comparator Selection: Click the operator icon (e.g.,
==,!=,>=,<) to choose the comparison logic. - Target Value: Define the goal value that progress must reach to satisfy the requirement.
-
Type Toggle: Click the data type icon (e.g.,
- Objective Events: Success/Failure event tree specific to this task.
Odyssey operates on a hierarchical evaluation chain. While the resources define the "rules" and structure of a quest, the QuestManager acts as the actor that executes those rules to transition through the quest.
An objective is the most granular unit of logic. Its completion status is determined by evaluating live data against the quest's requirements:
-
Requirement Matching: The QuestManager tracks the current progress for each objective. When the progress of an objective changes, the objective resource compares that progress against its requirements list. An objective is only considered "complete" if every registered requirement passes its logic gate (e.g.,
OP_EQUAL,OP_GREATER_EQUAL). -
Manual Override: Calling
set_completed(true)on an objective resource forces its state to complete. This allows bypassing requirement logic for purely scripted or narrative events.
Important
Type Safety: When updating progress, the system validates that the data type of the update matches the type defined in the requirement. Mismatched types are rejected to prevent errors at runtime.
The QuestStage resource is a stateless blueprint; it defines the criteria for progression but does not track its own active state. The QuestManager uses the stage's internal configuration to govern the flow:
- Mandatory Objectives: If an objective is registered to a stage as Required, the QuestManager will not progress the quest automatically until that specific objective is completed.
- Optional Objectives: Objectives not marked as required do not block progression. The manager allows a stage to finish even if these tasks are incomplete, though their individual success or failure is still recorded in the logs.
If auto_advance_stages is enabled when a quest is started, the QuestManager automates the transition between milestones. Every time an objective is updated via set_objective_progress, the manager performs the following sequence:
- Validation: The manager checks if the stage can be completed. This acts as a logic gate, succeeding only if every mandatory objective for that stage is finished.
-
Success Branching: If the check passes, the manager identifies the stage's
success_stage_id. If a valid ID is found, the quest advances and emitsquest_progressed. -
Terminal Check: If the logic passes but the target stage ID (either
success_stage_idorfailure_stage_id) is empty, the manager treats this as the end of the quest line. It finalizes the quest, emitsquest_finished, and moves the data to the history log.
| Level | Logic Source | Executioner | Result |
|---|---|---|---|
| Requirement | Objective Resource | QuestManager | Validates a single value/operator pair. |
| Objective | Objective Resource | QuestManager | Emits objective_completed when all requirements pass. |
| Stage | Stage Resource | QuestManager | Checks for stage completion to authorize a transition. Emits stage_completed when all objectives are completed. |
| Quest | Quest Resource | QuestManager | Emits quest_finished when a final stage is reached. |
To define the list of types each object can be (quest, stage or objective) you need to update the relevan enum of each script. This can be accessed by pressing the pencil button next to the types dropdown when the relevant item is selected on the quest structure tree (bottom left).
Warning
Types are defined as enum, but stored as integers. When editing Quest/Stage/Objective types, always Append new items and avoid removing items or use Explicit Values (e.g., SIDE = 2). Changing the order of an existing Enum will cause saved files to point to the wrong types if they weren't explicitly declared.
The QuestManager handles both file-based and dynamically generated Quest objects.
These signals are emitted by the QuestManager to allow game systems (UI, World, Dialogue) to react to progression.
| Signal | Description |
|---|---|
quest_started |
Emitted when a quest is successfully initialized. |
quest_progressed |
Emitted when a quest moves to a new stage (including the entry stage). |
quest_finished |
Emitted when a quest ends (either via Success or Failure). |
stage_completed |
Emitted when a stage is finalized. |
objective_completed |
Emitted when an individual objective is finished. |
quest_event_triggered |
Emitted when a Success/Failure event from any level is triggered. |
The central hub for managing and persisting all quest data at runtime.
| Method | Return Type | Description |
|---|---|---|
start_quest(quest, auto_advance) |
Begins tracking a quest. If auto_advance is true, the manager handles stage transitions automatically based on objective completion. |
|
remove_quest(id, clear_history) |
Stops tracking an active quest. Can optionally wipe the quest from the historical log. | |
get_quest(quest_id) |
Quest |
Retrieves the active Quest resource object. Returns null if the quest is not active. |
get_quest_current_stage(id) |
QuestStage |
Returns stage object of the stage the player is currently on for the specified quest. |
get_quest_current_stage_id(id) |
StringName |
Returns the ID of the stage the player is currently on for the specified quest. |
is_quest_active(id) |
bool |
Returns true if the quest is currently being tracked in the active dictionary. |
set_objective_progress(...) |
Updates requirement values. If all requirements are met, it completes the objective and may advance the stage if auto_advance is enabled. |
|
complete_objective(...) |
Forces completion of a specific objective with a manual success/failure status. | |
complete_stage(...) |
Forces completion of a stage and advances the quest to the next logical path. | |
complete_quest(id, success) |
Finalizes the entire quest and triggers the associated global quest events. | |
get_quests_data() |
Dictionary |
Serializes all active quest states, duplicated resources, and the history log for saving. |
set_quests_data(data) |
Restores the manager state, log, and quest progress from a save dictionary. | |
quest_success_status(id) |
SuccessStatus (Enum) |
Checks the log to see if a quest finished successfully or failed. |
stage_success_status(...) |
SuccessStatus (Enum) |
Checks the log for the success/failure status of a specific stage. |
objective_success_status(...) |
SuccessStatus (Enum) |
Checks the log for the success/failure status of a specific objective. |
erase_quest_from_log(id) |
Removes the history log for a specific quest. | |
clear_quest_log() |
Completely wipes all historical records from the manager. |
The static blueprint defining the quest structure and initial entry point.
| Method | Return Type | Description |
|---|---|---|
stages() |
Array[StringName] |
Returns an array containing the IDs of all stages registered to this quest. |
add_stage(stage) |
Adds a QuestStage to the quest. Overwrites any existing stage with the same ID. |
|
remove_stage(stage_id) |
Removes a stage from the quest. | |
has_stage(stage_id) |
bool |
Returns true if the quest contains a stage with the given ID. |
get_stage(stage_id) |
QuestStage |
Returns the QuestStage resource or null if it does not exist. |
Manages the logic for a specific milestone and determines the branching path.
| Method | Return Type | Description |
|---|---|---|
objectives() |
Array[StringName] |
Returns an array of IDs for all objectives registered to this stage. |
add_objective(objective, required) |
Registers a new objective. The required flag determines if it blocks stage completion. |
|
remove_objective(objective_id) |
Removes an objective from the stage. | |
has_objective(objective_id) |
bool |
Returns true if the objective is registered in this stage. |
set_objective_required(...) |
Updates the "required" status of an existing objective ID. | |
is_objective_required(id) |
bool |
Returns true if the objective must be completed to finish the stage. |
can_complete_stage() |
bool |
Logic check that returns true only if every objective marked as Required is complete. |
get_objective(objective_id) |
QuestObjective |
Returns the objective resource or null if not found. |
Handles requirement evaluation and progress validation.
| Method | Return Type | Description |
|---|---|---|
requirements() |
Array[String] |
Returns an array of all requirement IDs (keys) for this objective. |
get_requirement_type(id) |
int |
Returns the typeof the requirement value (Godot TYPE_* constants). |
get_requirement_value(id) |
Variant |
Returns the goal/target value for a specific requirement. |
get_requirement_mode(id) |
int |
Returns the operator used for comparison (e.g., OP_EQUAL). |
set_requirement(...) |
Configures a requirement gate. Resets progress if the new value type differs from the old one. | |
set_progress(id, value) |
Updates the internal progress tracker. Rejects values that do not match the requirement type. | |
get_objective_progress() |
Dictionary |
Returns a dictionary containing the mode, current value, and target value for every requirement. |
get_requirement_progress(id) |
Dictionary |
Returns the progress dictionary for a specific individual requirement. |
is_objective_complete() |
bool |
Evaluates all requirements based on current progress. Returns true if all logic gates pass. |
has_requirement(id) |
bool |
Returns true if the logic gate ID exists in this objective. |
clear_requirements() |
Clears all requirements and resets the internal progress dictionary. | |
set_completed(is_completed) |
Manual override to force the objective's completion state, bypassing requirements. |
Odyssey is designed to be reactive. By connecting to the QuestManager signals, you can trigger world changes, UI updates, and reward systems without tightly coupling your quest data to your game's core systems.
Odyssey uses a Success/Failure Event system to pass data dictionaries to your game logic. This is the primary way to handle experience, currency, item rewards or any other event.
Editor Setup:
In the Events section of a Quest, Stage, or Objective, define a key named rewards in the On Success Events dictionary.
GDScript Implementation:
func _ready() -> void:
# Connect to the global QuestManager (Singleton)
NexusForge.Quests.quest_event_triggered.connect(_on_quest_event_triggered)
func _on_quest_event_triggered(event_id: String, event_data: Variant) -> void:
match event_id:
"rewards":
# event_data contains the dictionary:
# {"experience": 500, "items": {"iron_sword": 1, "health_potion": 2}}
if event_data.has("experience"):
PlayerStats.add_xp(event_data.experience)
if event_data.has("items"):
for item_id in event_data.items:
var count = event_data.items[item_id]
Inventory.add_item(item_id, count)
"play_stinger":
AudioPlayer.play_sfx("quest_complete_fanfare")To ensure the manager correctly tracks requirements and triggers auto-advancement, always use NexusForge.Quests.set_objective_progress rather than modifying the resource directly.
Example: A Mob Kill Quest
func _on_enemy_died(enemy_type: String) -> void:
if enemy_type == "Goblin":
# Get the current progress to increment it
var current_stage: QuestStage = NexusForge.Quests.get_quest_current_stage(&"main_story")
var current_objective: QuestObjective = current_stage.get_objective(&"kill_goblins")
var objective_progress: Dictionary = current_objective.get_objective_progress("goblin_count")
var current_kills: int = objective_progress.current # or objective_progress["current"]
# Update the manager; this will automatically check if the objective is complete
NexusForge.Quests.set_objective_progress(
&"main_story",
&"prologue",
&"kill_goblins",
"goblin_count", # The Requirement ID
current_kills + 1)Because the QuestManager emits quest_progressed every time the stage changes, you can use this to update your world state (e.g., spawning NPCs or opening gates) based on the specific path the player took.
func _on_quest_progressed(quest_id: StringName, to_stage: StringName) -> void:
if quest_id == &"kings_gambit":
match to_stage:
&"storm_the_castle":
WorldManager.set_castle_gate_open(true)
WorldManager.spawn_reinforcements()
&"stealth_approach":
WorldManager.set_guards_alert_level("low")
WorldManager.enable_secret_passage()Using get_quests_data() and set_quests_data(), you can easily integrate Odyssey into your existing save system.
func save_game() -> void:
var save_file = FileAccess.open("user://savegame.dat", FileAccess.WRITE)
var all_data = {
"player_pos": player.position,
"quests": NexusForge.Quests.get_quests_data() # Captures active quests and history
}
save_file.store_var(all_data)
func load_game() -> void:
var save_file = FileAccess.open("user://savegame.dat", FileAccess.READ)
var data = save_file.get_var()
# Restores all quest resources, requirements, and logs
NexusForge.Quests.set_quests_data(data["quests"]) The Odyssey export plugin performs automated integrity checks to prevent game-breaking logic errors.
- Duplicate ID Check: Issues a warning if two quest resources share the same internal ID.
-
Entry Stage Check: Verifies that the defined
entry_stageactually exists within the quest's stage dictionary.