
Godot Quest System
- 185 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-quest-system for development tasks
About
godot-quest-system: A skill for development. This provides functionality for development workflows.
- godot-quest-system
Godot Quest System by the numbers
- 185 all-time installs (skills.sh)
- +18 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #2,166 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-quest-systemAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 185 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-quest-system for development tasks
Files
Quest System
Resource-based data, signal-driven updates, and AutoLoad coordination define scalable quest architectures.
Available Scripts
quest_resource.gd
Data-driven quest definition using Resources for modular objectives and branching rewards.
quest_manager_singleton.gd
Centralized AutoLoad orchestrator for tracking active quests and broadcasting status updates.
kill_objective_trigger.gd
Decoupled trigger logic that bridges game events (enemies dying) to the Quest System.
quest_ui_tracker.gd
Reactive VBox UI that dynamically adds and removes objective labels based on manager signals.
branching_quest_data.gd
Extended quest logic for handling multiple outcomes and player-driven narrative paths.
quest_giver_dialogue_hook.gd
Hook for integrating NPCs with the quest system, allowing for conditional dialogue branches.
quest_persistence_loader.gd
Expert patterns for serializing quest IDs and progress counts for persistent save states.
timed_quest_challenge.gd
Template for time-limited challenges with automatic failure conditions and UI signals.
hidden_objective_logic.gd
Background objective tracker for secret achievements or non-visible quest progress.
localized_quest_description.gd
Strategy for supporting multi-language quest text using tr() keys instead of hardcoded strings.
NEVER Do in Quest Systems
- NEVER store active quest data directly in the Player node — If the player dies or the scene reloads, quest progress is lost. Use an AutoLoad or a persistent Data Resource [20].
- NEVER use hardcoded string IDs for objectives without validation — Typos in
update_objective("kill_slimes")will fail silently. Use StringNames or a central ID registry [21]. - NEVER forget to disconnect completion signals — If a quest signal isn't cleared after completion, it might trigger multiple times, awarding double rewards [22].
- NEVER poll for mission completion in `_process()` — Checking objectives 60 times a second is wasteful. Use a signal-driven approach (e.g.
on_enemy_died) [23]. - NEVER skip save/load logic for quests — Resetting a 10-hour quest line because of a game restart is a player-ending bug. Always persist quest states [24].
- NEVER use `all()` on objective arrays without null/type checks — Attempting to check completion on a null objective entry will crash the entire system [25].
- NEVER hardcode quest logic inside enemy or item scripts — Use a generic
EventBusorQuestTriggernode to bridge the encounter to the QuestManager. - NEVER allow multiple instances of the same Quest Resource to be active — Ensure you're tracking unique Quest IDs to prevent accidental duplication of missions.
- NEVER use complex UI logic to calculate progress — The UI should only display what the
Questresource provides. Keep formulas in theQuestManager. - NEVER award rewards directly inside the quest script — Delegate reward distribution to the
InventoryManagerorEconomyManagervia signals for decoupling.
---
# quest.gd
class_name Quest
extends Resource
signal progress_updated(objective_id: String, progress: int)signal completed
@export var quest_id: String
@export var quest_name: String
@export_multiline var description: String
@export var objectives: Array[QuestObjective] = []
@export var rewards: Array[QuestReward] = []
@export var required_level: int = 1
func is_complete() -> bool:
return objectives.all(func(obj): return obj.is_complete())
func check_completion() -> void:
if is_complete():
completed.emit()Quest Objectives
# quest_objective.gd
class_name QuestObjective
extends Resource
enum Type { KILL, COLLECT, TALK, REACH }
@export var objective_id: String
@export var type: Type
@export var target: String # Enemy name, item ID, NPC name, location
@export var required_amount: int = 1
@export var current_amount: int = 0
func progress(amount: int = 1) -> void:
current_amount += amount
current_amount = mini(current_amount, required_amount)
func is_complete() -> bool:
return current_amount >= required_amountQuest Manager
# quest_manager.gd (AutoLoad)
extends Node
signal quest_accepted(quest: Quest)
signal quest_completed(quest: Quest)
signal objective_updated(quest: Quest, objective: QuestObjective)
var active_quests: Array[Quest] = []
var completed_quests: Array[String] = []
func accept_quest(quest: Quest) -> void:
if quest.quest_id in completed_quests:
return
active_quests.append(quest)
quest.completed.connect(func(): _on_quest_completed(quest))
quest_accepted.emit(quest)
func _on_quest_completed(quest: Quest) -> void:
active_quests.erase(quest)
completed_quests.append(quest.quest_id)
# Give rewards
for reward in quest.rewards:
reward.grant()
quest_completed.emit(quest)
func update_objective(quest_id: String, objective_id: String, amount: int = 1) -> void:
for quest in active_quests:
if quest.quest_id == quest_id:
for obj in quest.objectives:
if obj.objective_id == objective_id:
obj.progress(amount)
objective_updated.emit(quest, obj)
quest.check_completion()
return
func get_active_quest(quest_id: String) -> Quest:
for quest in active_quests:
if quest.quest_id == quest_id:
return quest
return nullQuest Triggers
# Example: Kill quest integration
# enemy.gd
func _on_died() -> void:
QuestManager.update_objective("kill_bandits", "kill_bandit", 1)
# Example: Collection integration
# item_pickup.gd
func _on_collected() -> void:
QuestManager.update_objective("gather_herbs", "collect_herb", 1)
# Example: Talk integration
# npc.gd
func interact() -> void:
DialogueManager.start_dialogue(dialogue_id)
QuestManager.update_objective("find_elder", "talk_to_elder", 1)Quest UI
# quest_ui.gd
extends Control
@onready var quest_list := $QuestList
func _ready() -> void:
QuestManager.quest_accepted.connect(_on_quest_accepted)
QuestManager.objective_updated.connect(_on_objective_updated)
refresh_ui()
func refresh_ui() -> void:
for child in quest_list.get_children():
child.queue_free()
for quest in QuestManager.active_quests:
var quest_entry := create_quest_entry(quest)
quest_list.add_child(quest_entry)
func create_quest_entry(quest: Quest) -> Control:
var entry := VBoxContainer.new()
var title := Label.new()
title.text = quest.quest_name
entry.add_child(title)
for obj in quest.objectives:
var obj_label := Label.new()
obj_label.text = "%s: %d/%d" % [obj.target, obj.current_amount, obj.required_amount]
entry.add_child(obj_label)
return entryBest Practices
1. Signal-Driven - Emit events, systems listen 2. Save Progress - Track completed quests 3. Validation - Check prerequisites before accepting
---
Elite Godot 4.x Patterns
1. Auto-Navigation Waypoint Helper
Dynamically guide players to objectives using NavigationAgent3D. Update the target_position only when objectives change to optimize pathfinding queries.
# quest_waypoint_helper.gd
class_name QuestWaypointHelper extends Node3D
@export var navigation_agent: NavigationAgent3D
func set_objective_pos(target_pos: Vector3) -> void:
navigation_agent.target_position = target_pos
func _physics_process(_delta: float) -> void:
if navigation_agent.is_navigation_finished(): return
var next_pos := navigation_agent.get_next_path_position()
# Logic to point compass or draw 3D UI towards next_pos2. Resource-Based Quest Prerequisites
Avoid hardcoded unlock logic. Use Resource references to define quest dependencies that can be visually linked in the Inspector.
# quest_data.gd
class_name QuestData extends Resource
@export var quest_id: StringName
@export var prerequisites: Array[QuestData] = []
@export var is_completed: bool = false
func can_unlock() -> bool:
for prereq in prerequisites:
if not prereq.is_completed: return false
return true3. Thread-Safe Quest Conflict Resolver
In complex games with concurrent updates (e.g., background networking or parallel combat logic), use a Mutex to prevent race conditions when updating shared quest progress.
# quest_conflict_resolver.gd
class_name QuestConflictResolver extends Node
var _mutex := Mutex.new()
var _progress_map := {}
func safely_add_progress(id: StringName, amount: int) -> void:
_mutex.lock()
_progress_map[id] = _progress_map.get(id, 0) + amount
var current := _progress_map[id]
_mutex.unlock()
# Defer signals/UI to main thread
call_deferred("_notify_update", id, current)Reference
- Master Skill: godot-master
# branching_quest_data.gd
# Handling multi-outcome quest lines
class_name BranchingQuest extends Quest
# EXPERT NOTE: Allow quests to have multiple "Next" paths
# based on player choices or hidden stats.
@export var positive_outcome_quest: Quest
@export var negative_outcome_quest: Quest
func resolve_branch(is_positive: bool):
if is_positive:
QuestManager.accept_quest(positive_outcome_quest)
else:
QuestManager.accept_quest(negative_outcome_quest)
# hidden_objective_logic.gd
# Objectives that aren't visible in the UI
class_name HiddenObjective extends Resource
# EXPERT NOTE: Secret objectives can trigger "Achievement" quests
# when internal game conditions are met.
signal triggered
@export var required_value: int = 100
var current_value: int = 0
func track_value(val: int):
current_value += val
if current_value >= required_value:
triggered.emit()
# kill_objective_trigger.gd
# Decoupled quest trigger via signals
extends Node
# EXPERT NOTE: Use a generic trigger script that listens to game events
# (like an EventBus) to avoid hardcoding quest logic into enemies.
@export var target_enemy_id: String = ""
@export var quest_id: String = ""
func _ready():
# Assumes a GlobalEventBus exits
if get_tree().root.has_node("GlobalBus"):
var bus = get_node("/root/GlobalBus")
bus.enemy_defeated.connect(_on_enemy_defeated)
func _on_enemy_defeated(enemy_id: String, _points: int):
if enemy_id == target_enemy_id:
QuestManager.update_objective(quest_id)
# localized_quest_description.gd
# Strategy for translation-friendly quest text
extends Resource
# EXPERT NOTE: Use 'tr()' on IDs rather than hardcoding strings
# inside the Resource files for professional localization support.
@export var title_key: String = "QUEST_01_TITLE"
@export var desc_key: String = "QUEST_01_DESC"
func get_title() -> String:
return tr(title_key)
func get_desc() -> String:
return tr(desc_key)
# quest_giver_dialogue_hook.gd
# Interfacing quests with a dialogue system
extends Node
# EXPERT NOTE: The NPC logic should check the QuestManager state
# to decide which dialogue branch to play.
@export var quest_to_give: Quest
func interact():
var q_status = QuestManager.get_quest_status(quest_to_give.id)
match q_status:
Quest.Status.AVAILABLE:
_show_offer_dialogue()
Quest.Status.ACTIVE:
_show_reminder_dialogue()
Quest.Status.COMPLETED:
_show_thanks_dialogue()
func _show_offer_dialogue():
# If player accepts in UI:
QuestManager.accept_quest(quest_to_give)
# skills/quest-system/scripts/quest_graph_manager.gd
extends Node
## Quest Graph Manager Expert Pattern
## Runtime manager for traversing quest graphs and tracking state.
class_name QuestGraphManager
signal quest_started(quest_id: String)
signal objective_completed(quest_id: String, objective_id: String)
signal quest_completed(quest_id: String)
var active_quests: Dictionary = {} # { quest_id: QuestState }
var completed_quests: Array[String] = []
func start_quest(quest_resource: QuestGraph) -> void:
if is_quest_active(quest_resource.quest_id) or is_quest_completed(quest_resource.quest_id):
return
var state = QuestState.new()
state.resource = quest_resource
state.current_nodes = quest_resource.get_start_nodes()
active_quests[quest_resource.quest_id] = state
quest_started.emit(quest_resource.quest_id)
_check_auto_complete_objectives(state)
func complete_objective(quest_id: String, objective_id: String) -> void:
var state = active_quests.get(quest_id)
if not state:
return
if state.complete_objective(objective_id):
objective_completed.emit(quest_id, objective_id)
_advance_quest(state)
func _advance_quest(state: QuestState) -> void:
var next_nodes = state.resource.get_next_nodes(state.current_nodes)
state.current_nodes = next_nodes
if state.is_complete():
_finish_quest(state.resource.quest_id)
func _finish_quest(quest_id: String) -> void:
active_quests.erase(quest_id)
completed_quests.append(quest_id)
quest_completed.emit(quest_id)
func is_quest_active(quest_id: String) -> bool:
return active_quests.has(quest_id)
func is_quest_completed(quest_id: String) -> bool:
return completed_quests.has(quest_id)
# Helper Class
class QuestState:
var resource: QuestGraph
var current_nodes: Array[String] = []
var completed_objectives: Array[String] = []
func complete_objective(obj_id: String) -> bool:
if obj_id in completed_objectives: return false
completed_objectives.append(obj_id)
return true
func is_complete() -> bool:
# Logic to check if end node is reached
return false # simplified
## EXPERT USAGE:
## QuestManager.start_quest(load("res://quests/main_quest.tres"))
## QuestManager.complete_objective("main_quest", "kill_rats")
# quest_manager_singleton.gd
# Centralized quest state and event handling
extends Node
# EXPERT NOTE: The QuestManager should be an Autoload to track
# progress across scene changes.
signal quest_accepted(quest: Quest)
signal quest_objective_updated(quest: Quest)
signal quest_completed(quest: Quest)
var active_quests: Dictionary = {} # ID -> Quest
func accept_quest(quest: Quest):
if not active_quests.has(quest.id):
quest.status = Quest.Status.ACTIVE
active_quests[quest.id] = quest
quest_accepted.emit(quest)
func update_objective(quest_id: String, amount: int = 1):
if active_quests.has(quest_id):
var q = active_quests[quest_id]
q.current_count += amount
quest_objective_updated.emit(q)
if q.current_count >= q.objective_count:
_complete_quest(q)
func _complete_quest(quest: Quest):
quest.status = Quest.Status.COMPLETED
active_quests.erase(quest.id)
quest_completed.emit(quest)
if quest.next_quest:
accept_quest(quest.next_quest)
# skills/quest-system/code/quest_manager.gd
extends Node
## Quest System Expert Pattern
## Implements DAG-based dependencies and signal-based objective tracking.
signal quest_started(quest_id: String)
signal quest_completed(quest_id: String)
signal objective_updated(quest_id: String, objective_id: String, progress: int)
@export var quest_database: Dictionary = {} # ID: QuestResource
var active_quests: Dictionary = {} # ID: ProgressState
var completed_quests: Array[String] = []
func start_quest(quest_id: String) -> bool:
# 1. Quest Dependency Graphs (DAG)
# Expert logic: Check prerequisites before starting.
var quest_res = quest_database.get(quest_id)
if not quest_res: return false
for prereq in quest_res.prerequisites:
if prereq not in completed_quests:
print("Quest Blocked: Prerequisite '", prereq, "' not met.")
return false
active_quests[quest_id] = {"objectives": {}}
quest_started.emit(quest_id)
return true
func update_objective(quest_id: String, objective_id: String, amount: int) -> void:
# 2. Objective State Sync
# Professional pattern: Central manager handles tracking, not the actor.
if quest_id not in active_quests: return
var quest_state = active_quests[quest_id]
quest_state.objectives[objective_id] = quest_state.objectives.get(objective_id, 0) + amount
objective_updated.emit(quest_id, objective_id, quest_state.objectives[objective_id])
if _check_quest_completion(quest_id):
_complete_quest(quest_id)
func _check_quest_completion(quest_id: String) -> bool:
# Logic to verify all objectives in QuestResource are met...
return true
func _complete_quest(quest_id: String) -> void:
active_quests.erase(quest_id)
completed_quests.append(quest_id)
# 3. Reward Distribution Logic
# Decoupled Reward Provider system.
_distribute_rewards(quest_database[quest_id].rewards)
quest_completed.emit(quest_id)
func _distribute_rewards(rewards: Array) -> void:
for reward in rewards:
# reward.apply(player) pattern
pass
## EXPERT NOTE:
## For 'Save-Game Compatibility', NEVER serialize the QuestResource itself.
## Save a Dictionary of { 'completed': ['q1', 'q2'], 'active': {'q3': {'obj1': 5}} }.
## Use signals to update UI waypoints dynamically when 'objective_updated' is emitted.
## NEVER hardcode quest logic inside enemy scripts. Use the 'Signal Bus' pattern:
## enemy.died.connect(QuestManager._on_enemy_died).
# quest_persistence_loader.gd
# Saving and loading quest progress
extends Node
# EXPERT NOTE: Save quest IDs and their current progress count
# to a dictionary for easy JSON or ConfigFile serialization.
func get_save_data() -> Dictionary:
var data = {}
for id in QuestManager.active_quests:
var q = QuestManager.active_quests[id]
data[id] = q.current_count
return data
func load_save_data(data: Dictionary):
# Assumes you have a central 'QuestDatabase' to load base resources
for id in data:
var q_res = QuestDatabase.get_quest(id)
if q_res:
QuestManager.accept_quest(q_res)
QuestManager.update_objective(id, data[id])
# quest_resource.gd
# Data-driven quest definition
class_name Quest extends Resource
# EXPERT NOTE: Defining quests as Resources allows for
# branching logic and complex reward structures in the Inspector.
enum Status { AVAILABLE, ACTIVE, COMPLETED, FAILED }
@export var id: String = ""
@export var title: String = ""
@export_multiline var description: String = ""
@export var objective_count: int = 1
@export var rewards: Array[InventoryItem] = []
@export var next_quest: Quest # For simple linear chains
var status: Status = Status.AVAILABLE
var current_count: int = 0
# quest_ui_tracker.gd
# Dynamic list of active objectives
extends VBoxContainer
# EXPERT NOTE: The UI should be purely reactive,
# populating itself based on QuestManager signals.
func _ready():
QuestManager.quest_accepted.connect(_add_quest_track)
QuestManager.quest_completed.connect(_remove_quest_track)
QuestManager.quest_objective_updated.connect(_update_quest_track)
func _add_quest_track(quest: Quest):
var label = Label.new()
label.name = quest.id
add_child(label)
_update_quest_track(quest)
func _update_quest_track(quest: Quest):
var label = get_node_or_null(quest.id)
if label:
label.text = "%s: %d/%d" % [quest.title, quest.current_count, quest.objective_count]
func _remove_quest_track(quest: Quest):
var label = get_node_or_null(quest.id)
if label:
label.queue_free()
# timed_quest_challenge.gd
# Adding temporal constraints to quest logic
class_name TimedQuest extends Quest
@export var time_limit: float = 60.0
func start_timer():
get_tree().create_timer(time_limit).timeout.connect(_on_fail)
func _on_fail():
if status == Status.ACTIVE:
status = Status.FAILED
QuestManager.notify_quest_failed(id)