feat(progression): quêtes rotatives court-terme + panel HUD

Nouvel autoload QuestManager avec 11 templates (casser X sable, récolter
coraux, collecter perles, plonger à -25m, crafter 2 objets, etc.).
3 quêtes actives simultanément; complétion → récompense XP + roll d'une
nouvelle quête.

HUD: panneau "OBJECTIFS" top-right avec progression couleur (gris→vert),
bannière centrale "✓ QUÊTE" + son bulle au complete.

Motivation moyen-terme (5-15 min): le joueur a toujours qqch à faire.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 18:10:01 +00:00
parent 984754183f
commit 610d766cb2
6 changed files with 228 additions and 4 deletions

View File

@@ -0,0 +1,107 @@
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()