extends Node # Short-term objectives that rotate. Each quest has: # id (string), desc, kind (break/pearl/depth/craft), target (int), reward_xp (int) # Player always has up to MAX_ACTIVE quests active; completing one rolls a new one. signal quests_updated() signal quest_completed(quest: Dictionary) const MAX_ACTIVE: int = 3 const QUEST_POOL: Array = [ {"id": "break_sand", "desc": "Casser 15 blocs de sable", "kind": "break", "item_id": 2, "target": 15, "reward_xp": 40}, {"id": "break_rock", "desc": "Casser 10 blocs de roche", "kind": "break", "item_id": 3, "target": 10, "reward_xp": 50}, {"id": "break_kelp", "desc": "Récolter 8 kelp", "kind": "break", "item_id": 6, "target": 8, "reward_xp": 35}, {"id": "break_coral_r", "desc": "Récolter 5 coraux rouges", "kind": "break", "item_id": 4, "target": 5, "reward_xp": 60}, {"id": "break_coral_b", "desc": "Récolter 5 coraux bleus", "kind": "break", "item_id": 5, "target": 5, "reward_xp": 60}, {"id": "find_wreck", "desc": "Fouiller 3 épaves", "kind": "break", "item_id": 7, "target": 3, "reward_xp": 80}, {"id": "pearl_3", "desc": "Collecter 3 perles", "kind": "pearl", "target": 3, "reward_xp": 70}, {"id": "pearl_5", "desc": "Collecter 5 perles", "kind": "pearl", "target": 5, "reward_xp": 110}, {"id": "depth_25", "desc": "Plonger à -25 m", "kind": "depth", "target": 25, "reward_xp": 50}, {"id": "depth_50", "desc": "Plonger à -50 m", "kind": "depth", "target": 50, "reward_xp": 90}, {"id": "craft_2", "desc": "Crafter 2 objets", "kind": "craft", "target": 2, "reward_xp": 45}, ] var active: Array = [] # each: Dictionary {template_ref, progress} var _used_ids: Array[String] = [] func _ready() -> void: _ensure_active_count() func _ensure_active_count() -> void: while active.size() < MAX_ACTIVE: var q: Dictionary = _pick_next_quest() if q.is_empty(): break active.append({ "id": q["id"], "desc": q["desc"], "kind": q["kind"], "item_id": q.get("item_id", -1), "target": q["target"], "reward_xp": q["reward_xp"], "progress": 0, }) quests_updated.emit() func _pick_next_quest() -> Dictionary: var candidates: Array = [] var active_ids: Array[String] = [] for q: Dictionary in active: active_ids.append(q["id"]) for tpl: Dictionary in QUEST_POOL: if active_ids.has(tpl["id"]): continue candidates.append(tpl) if candidates.is_empty(): # all in use; allow recycle _used_ids.clear() candidates = QUEST_POOL return candidates[randi() % candidates.size()] func note_block_break(block_id: int) -> void: for q: Dictionary in active: if q["kind"] == "break" and q["item_id"] == block_id: _progress(q, 1) func note_pearl_collected() -> void: for q: Dictionary in active: if q["kind"] == "pearl": _progress(q, 1) func note_craft() -> void: for q: Dictionary in active: if q["kind"] == "craft": _progress(q, 1) func note_depth(depth_m: int) -> void: for q: Dictionary in active: if q["kind"] == "depth" and depth_m >= q["target"]: q["progress"] = q["target"] _check_complete(q) func _progress(q: Dictionary, amount: int) -> void: q["progress"] = mini(q["progress"] + amount, q["target"]) _check_complete(q) quests_updated.emit() func _check_complete(q: Dictionary) -> void: if q["progress"] < q["target"]: return # Complete var snapshot: Dictionary = q.duplicate() quest_completed.emit(snapshot) var pp: Node = get_node_or_null("/root/PlayerProgress") if pp != null: pp.award(q["reward_xp"], "quête: %s" % q["desc"], Vector3.ZERO) active.erase(q) _ensure_active_count()