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

@@ -20,6 +20,11 @@ var _xp_label: Label = null
var _level_banner: Label = null
var _level_banner_timer: float = 0.0
var _quest_panel: PanelContainer = null
var _quest_list: VBoxContainer = null
var _quest_complete_banner: Label = null
var _quest_banner_timer: float = 0.0
var _biome_cache_pos: Vector3 = Vector3(99999, 99999, 99999)
var _biome_cached_name: String = ""
@@ -31,12 +36,18 @@ func _ready() -> void:
_build_info_panel()
_build_xp_panel()
_build_level_banner()
_build_quest_panel()
if Engine.has_singleton("PlayerProgress") or has_node("/root/PlayerProgress"):
var pp: Node = get_node_or_null("/root/PlayerProgress")
if pp != null:
pp.xp_changed.connect(_on_xp_changed)
pp.level_up.connect(_on_level_up)
_on_xp_changed(pp.current_xp, pp.xp_for_next(), pp.level)
var qm: Node = get_node_or_null("/root/QuestManager")
if qm != null:
qm.quests_updated.connect(_on_quests_updated)
qm.quest_completed.connect(_on_quest_completed)
_on_quests_updated()
func _build_info_panel() -> void:
@@ -133,6 +144,94 @@ func _build_xp_panel() -> void:
add_child(_xp_panel)
func _build_quest_panel() -> void:
_quest_panel = PanelContainer.new()
_quest_panel.anchor_left = 1.0
_quest_panel.anchor_right = 1.0
_quest_panel.anchor_top = 0.0
_quest_panel.anchor_bottom = 0.0
_quest_panel.offset_left = -260.0
_quest_panel.offset_top = 130.0
_quest_panel.offset_right = -16.0
_quest_panel.offset_bottom = 260.0
var style := StyleBoxFlat.new()
style.bg_color = Color(0.04, 0.08, 0.12, 0.72)
style.border_color = Color(0.35, 0.85, 0.95, 0.65)
style.set_border_width_all(1)
style.set_corner_radius_all(8)
_quest_panel.add_theme_stylebox_override("panel", style)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 3)
_quest_panel.add_child(vbox)
var title := Label.new()
title.text = "◈ OBJECTIFS"
title.add_theme_font_size_override("font_size", 12)
title.add_theme_color_override("font_color", Color(0.55, 0.95, 1.0))
vbox.add_child(title)
_quest_list = VBoxContainer.new()
_quest_list.add_theme_constant_override("separation", 2)
vbox.add_child(_quest_list)
add_child(_quest_panel)
func _on_quests_updated() -> void:
if _quest_list == null:
return
for c in _quest_list.get_children():
c.queue_free()
var qm: Node = get_node_or_null("/root/QuestManager")
if qm == null:
return
for q: Dictionary in qm.active:
var row := VBoxContainer.new()
row.add_theme_constant_override("separation", 0)
var desc := Label.new()
desc.text = "%s" % q["desc"]
desc.add_theme_font_size_override("font_size", 11)
desc.add_theme_color_override("font_color", Color(0.92, 0.96, 1.0))
row.add_child(desc)
var prog := Label.new()
prog.text = " %d/%d +%d XP" % [q["progress"], q["target"], q["reward_xp"]]
prog.add_theme_font_size_override("font_size", 10)
var completion: float = float(q["progress"]) / float(q["target"])
var col: Color = Color(0.6, 0.7, 0.8).lerp(Color(0.4, 1.0, 0.5), completion)
prog.add_theme_color_override("font_color", col)
row.add_child(prog)
_quest_list.add_child(row)
func _on_quest_completed(q: Dictionary) -> void:
if _quest_complete_banner == null:
_quest_complete_banner = Label.new()
_quest_complete_banner.anchor_left = 0.5
_quest_complete_banner.anchor_right = 0.5
_quest_complete_banner.anchor_top = 0.33
_quest_complete_banner.anchor_bottom = 0.33
_quest_complete_banner.offset_left = -260.0
_quest_complete_banner.offset_right = 260.0
_quest_complete_banner.offset_top = -22.0
_quest_complete_banner.offset_bottom = 22.0
_quest_complete_banner.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_quest_complete_banner.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_quest_complete_banner.add_theme_font_size_override("font_size", 22)
_quest_complete_banner.add_theme_color_override("font_color", Color(0.55, 1.0, 0.7))
_quest_complete_banner.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
_quest_complete_banner.add_theme_constant_override("outline_size", 5)
add_child(_quest_complete_banner)
_quest_complete_banner.text = "✓ QUÊTE : %s (+%d XP)" % [q["desc"], q["reward_xp"]]
_quest_complete_banner.modulate.a = 1.0
_quest_banner_timer = 2.8
var am: Node = get_node_or_null("/root/AudioManager")
if am != null and is_instance_valid(_dolphin):
if am.has_method("play_bubble_sfx"):
am.call("play_bubble_sfx", _dolphin.global_position)
func _build_level_banner() -> void:
_level_banner = Label.new()
_level_banner.anchor_left = 0.5
@@ -216,6 +315,11 @@ func _process(delta: float) -> void:
_level_banner.modulate.a = a
_level_banner.scale = Vector2.ONE * (1.0 + (1.0 - a) * 0.2)
if _quest_banner_timer > 0.0:
_quest_banner_timer -= delta
if _quest_complete_banner != null:
_quest_complete_banner.modulate.a = clampf(_quest_banner_timer / 0.6, 0.0, 1.0)
if not is_instance_valid(_dolphin):
return
@@ -227,6 +331,9 @@ func _process(delta: float) -> void:
var pp: Node = get_node_or_null("/root/PlayerProgress")
if pp != null and pp.has_method("note_depth"):
pp.call("note_depth", depth_m)
var qm: Node = get_node_or_null("/root/QuestManager")
if qm != null and qm.has_method("note_depth"):
qm.call("note_depth", depth_m)
if _compass_label != null:
_compass_label.text = _compass_text(_dolphin.rotation.y)