Quest Systeem
Missies, objectives, en beloningen.
Overzicht
┌─────────────────────────────────────────────────────────────┐
│ Quest Types │
├─────────────────────────────────────────────────────────────┤
│ │
│ Main Quests Side Quests Daily Quests │
│ ──────────── ──────────── ───────────── │
│ • Story-driven • NPC requests • Reset daily │
│ • Linear • Optional • Quick tasks │
│ • Big rewards • Friendship+ • Consistent XP │
│ │
│ Objective Types: │
│ • Collect (items verzamelen) │
│ • Deliver (items afleveren) │
│ • Talk (NPC dialoog) │
│ • Craft (items maken) │
│ • Explore (locaties bezoeken) │
│ • Farming (gewassen oogsten) │
│ │
└─────────────────────────────────────────────────────────────┘
Quest Data Model
Quest Definition
# quest_definition.gd
class_name QuestDefinition extends Resource
@export var quest_id: String = ""
@export var title: String = ""
@export var description: String = ""
@export var quest_type: String = "side" # main, side, daily
@export var giver_npc: String = ""
@export var dialog_start: String = ""
@export var dialog_progress: String = ""
@export var dialog_complete: String = ""
@export var objectives: Array[QuestObjective] = []
@export var rewards: Dictionary = {
"xp": 0,
"eikels": 0,
"items": []
}
@export var prerequisites: Array[String] = [] # Required completed quests
@export var required_level: int = 1
Quest Objective
# quest_objective.gd
class_name QuestObjective extends Resource
@export var objective_id: String = ""
@export var objective_type: String = "collect"
@export var description: String = ""
@export var target_id: String = "" # item_id, npc_id, location_id
@export var target_quantity: int = 1
@export var current_quantity: int = 0
@export var optional: bool = false
Player Quest State
# player_quest.gd
class_name PlayerQuest extends Resource
@export var quest_id: String = ""
@export var status: String = "active" # active, completed, failed
@export var started_at: int = 0
@export var completed_at: int = 0
@export var objectives_progress: Dictionary = {} # objective_id -> current_count
@export var is_tracked: bool = false
Quest Manager
# quest_manager.gd
extends Node
signal quest_started(quest_id: String)
signal quest_progress(quest_id: String, objective_id: String, current: int, target: int)
signal quest_completed(quest_id: String)
signal quest_failed(quest_id: String)
signal objective_completed(quest_id: String, objective_id: String)
var active_quests: Dictionary = {} # quest_id -> PlayerQuest
var completed_quests: Array[String] = []
var tracked_quest_id: String = ""
func _ready() -> void:
_load_quests_from_server()
_connect_signals()
func _connect_signals() -> void:
# Connect to game events
InventoryManager.item_added.connect(_on_item_collected)
InventoryManager.item_removed.connect(_on_item_delivered)
DialogManager.dialog_ended.connect(_on_dialog_completed)
FarmManager.crop_harvested.connect(_on_crop_harvested)
CraftingManager.recipe_crafted.connect(_on_item_crafted)
Quest Starten
func start_quest(quest_id: String) -> bool:
if quest_id in active_quests:
return false # Already active
if quest_id in completed_quests:
return false # Already completed
var definition = QuestDatabase.get_quest(quest_id)
if definition == null:
push_error("Quest not found: " + quest_id)
return false
# Check prerequisites
if not _check_prerequisites(definition):
return false
# Create player quest
var player_quest = PlayerQuest.new()
player_quest.quest_id = quest_id
player_quest.status = "active"
player_quest.started_at = Time.get_unix_time_from_system()
# Initialize objective progress
for objective in definition.objectives:
player_quest.objectives_progress[objective.objective_id] = 0
active_quests[quest_id] = player_quest
# Sync to server
SpacetimeDB.call_reducer("start_quest", [quest_id])
# Notification
NotificationManager.show_quest_update(definition, "started")
quest_started.emit(quest_id)
return true
func _check_prerequisites(definition: QuestDefinition) -> bool:
# Check level
if PlayerStats.get_level() < definition.required_level:
return false
# Check completed quests
for prereq in definition.prerequisites:
if prereq not in completed_quests:
return false
return true
Progress Tracking
Item Collection
func _on_item_collected(item_id: String, quantity: int) -> void:
for quest_id in active_quests:
var quest = active_quests[quest_id]
var definition = QuestDatabase.get_quest(quest_id)
for objective in definition.objectives:
if objective.objective_type != "collect":
continue
if objective.target_id != item_id:
continue
# Update progress
var current = quest.objectives_progress[objective.objective_id]
var new_value = min(current + quantity, objective.target_quantity)
quest.objectives_progress[objective.objective_id] = new_value
quest_progress.emit(
quest_id,
objective.objective_id,
new_value,
objective.target_quantity
)
# Check if objective completed
if new_value >= objective.target_quantity:
objective_completed.emit(quest_id, objective.objective_id)
# Check if quest completed
_check_quest_completion(quest_id)
Item Delivery
func _on_item_delivered(item_id: String, quantity: int) -> void:
# Delivery objectives are tracked differently
# They require talking to an NPC while having items
pass
func check_delivery_objective(quest_id: String, npc_id: String) -> bool:
var quest = active_quests.get(quest_id)
if not quest:
return false
var definition = QuestDatabase.get_quest(quest_id)
var inventory = get_node("/root/InventoryManager")
for objective in definition.objectives:
if objective.objective_type != "deliver":
continue
# Check if player has required items
if inventory.has_item(objective.target_id, objective.target_quantity):
# Remove items
inventory.remove_item(objective.target_id, objective.target_quantity)
# Mark as complete
quest.objectives_progress[objective.objective_id] = objective.target_quantity
objective_completed.emit(quest_id, objective.objective_id)
return true
return false
NPC Dialoog
func _on_dialog_completed(dialog_id: String) -> void:
for quest_id in active_quests:
var definition = QuestDatabase.get_quest(quest_id)
for objective in definition.objectives:
if objective.objective_type != "talk":
continue
if objective.target_id != dialog_id:
continue
var quest = active_quests[quest_id]
quest.objectives_progress[objective.objective_id] = 1
objective_completed.emit(quest_id, objective.objective_id)
_check_quest_completion(quest_id)
Farming & Crafting
func _on_crop_harvested(crop_id: String, quantity: int) -> void:
_update_objectives("farming", crop_id, quantity)
func _on_item_crafted(recipe_id: String) -> void:
var recipe = RecipeDatabase.get_recipe(recipe_id)
_update_objectives("craft", recipe.result.item_id, recipe.result.quantity)
func _update_objectives(type: String, target: String, quantity: int) -> void:
for quest_id in active_quests:
var quest = active_quests[quest_id]
var definition = QuestDatabase.get_quest(quest_id)
for objective in definition.objectives:
if objective.objective_type != type:
continue
if objective.target_id != target:
continue
var current = quest.objectives_progress[objective.objective_id]
var new_value = min(current + quantity, objective.target_quantity)
quest.objectives_progress[objective.objective_id] = new_value
quest_progress.emit(quest_id, objective.objective_id, new_value, objective.target_quantity)
if new_value >= objective.target_quantity:
objective_completed.emit(quest_id, objective.objective_id)
_check_quest_completion(quest_id)
Quest Voltooien
func _check_quest_completion(quest_id: String) -> void:
var quest = active_quests[quest_id]
var definition = QuestDatabase.get_quest(quest_id)
# Check all required objectives
for objective in definition.objectives:
if objective.optional:
continue
var progress = quest.objectives_progress.get(objective.objective_id, 0)
if progress < objective.target_quantity:
return # Not complete yet
# All required objectives complete
complete_quest(quest_id)
func complete_quest(quest_id: String) -> void:
var quest = active_quests.get(quest_id)
if not quest:
return
quest.status = "completed"
quest.completed_at = Time.get_unix_time_from_system()
var definition = QuestDatabase.get_quest(quest_id)
# Give rewards
_give_rewards(definition.rewards)
# Move to completed
active_quests.erase(quest_id)
completed_quests.append(quest_id)
# Sync to server
SpacetimeDB.call_reducer("complete_quest", [quest_id])
# Notification
NotificationManager.show_quest_update(definition, "completed")
quest_completed.emit(quest_id)
# Check for follow-up quests
_check_quest_chains(quest_id)
func _give_rewards(rewards: Dictionary) -> void:
# XP
if rewards.xp > 0:
PlayerStats.add_xp(rewards.xp)
# Eikels (currency)
if rewards.eikels > 0:
PlayerStats.add_eikels(rewards.eikels)
# Items
var inventory = get_node("/root/InventoryManager")
for item_reward in rewards.items:
inventory.add_item(item_reward.item_id, item_reward.quantity)
func _check_quest_chains(completed_quest_id: String) -> void:
# Auto-start follow-up quests
for quest_id in QuestDatabase.get_all_quest_ids():
var definition = QuestDatabase.get_quest(quest_id)
if completed_quest_id in definition.prerequisites:
if _check_prerequisites(definition):
# Delay slightly for better UX
await get_tree().create_timer(1.0).timeout
start_quest(quest_id)
Daily Quests
const DAILY_QUEST_POOL = [
"daily_harvest_5",
"daily_pet_animal",
"daily_craft_item",
"daily_talk_npc",
"daily_collect_acorns"
]
func refresh_daily_quests() -> void:
var current_date = Time.get_date_dict_from_system()
var date_key = "%d-%02d-%02d" % [current_date.year, current_date.month, current_date.day]
var last_refresh = SaveData.get_value("last_daily_refresh", "")
if last_refresh == date_key:
return # Already refreshed today
# Remove old daily quests
for quest_id in active_quests.keys():
var definition = QuestDatabase.get_quest(quest_id)
if definition.quest_type == "daily":
active_quests.erase(quest_id)
# Select new daily quests
var available = DAILY_QUEST_POOL.duplicate()
available.shuffle()
for i in range(min(3, available.size())):
start_quest(available[i])
SaveData.set_value("last_daily_refresh", date_key)
Quest Tracking UI
HUD Tracker
┌─────────────────────────────────┐
│ ★ Dagelijkse Oogst │
│ Oogst 5 gewassen │
│ [████████░░] 4/5 │
└─────────────────────────────────┘
# quest_tracker_hud.gd
extends Control
@onready var quest_title: Label = $Title
@onready var objective_label: Label = $Objective
@onready var progress_bar: ProgressBar = $ProgressBar
func _ready() -> void:
QuestManager.quest_progress.connect(_on_quest_progress)
QuestManager.quest_completed.connect(_on_quest_completed)
_update_display()
func _update_display() -> void:
var quest_id = QuestManager.tracked_quest_id
if quest_id.is_empty():
visible = false
return
var quest = QuestManager.active_quests.get(quest_id)
var definition = QuestDatabase.get_quest(quest_id)
if not quest or not definition:
visible = false
return
visible = true
quest_title.text = "★ " + definition.title
# Find current objective
for objective in definition.objectives:
var progress = quest.objectives_progress.get(objective.objective_id, 0)
if progress < objective.target_quantity:
objective_label.text = objective.description
progress_bar.max_value = objective.target_quantity
progress_bar.value = progress
break
Quest Database
const QUESTS = {
"main_welcome": {
"title": "Welkom in de Treehouse",
"description": "Maak kennis met de bewoners van de treehouse.",
"quest_type": "main",
"giver_npc": "oma_wilma",
"objectives": [
{
"objective_id": "talk_wilma",
"objective_type": "talk",
"description": "Praat met Oma Wilma",
"target_id": "npc_wilma",
"target_quantity": 1
},
{
"objective_id": "explore_treehouse",
"objective_type": "explore",
"description": "Verken de treehouse",
"target_id": "location_treehouse",
"target_quantity": 1
}
],
"rewards": {
"xp": 50,
"eikels": 10,
"items": [
{"item_id": "welcome_gift", "quantity": 1}
]
}
},
"daily_harvest_5": {
"title": "Dagelijkse Oogst",
"description": "Oogst 5 gewassen uit je tuin.",
"quest_type": "daily",
"objectives": [
{
"objective_id": "harvest",
"objective_type": "farming",
"description": "Oogst 5 gewassen",
"target_id": "any",
"target_quantity": 5
}
],
"rewards": {
"xp": 15,
"eikels": 5,
"items": []
}
}
}