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:
107
scripts/progression/QuestManager.gd
Normal file
107
scripts/progression/QuestManager.gd
Normal 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()
|
||||
Reference in New Issue
Block a user