Files
dauphincraft/scripts/dolphin/HUD.gd
Poulpe (Silver Surfer deploy) 7f6811995d feat(progression): succès/achievements + toasts notification
Nouvel autoload AchievementManager avec 12 succès (Premier éclat, Mineur
confirmé, Chasseur de perles, Fouilleur d'épaves, Abysses, Légende des
océans, etc.). Chaque déblocage donne un bonus XP et joue un son.

HUD: toasts dorés avec icône + titre + desc + fade-in/out, empilés en
haut centre. Son bulle au déblocage.

Long-terme: cible des milestones cumulatifs qui donnent un sentiment de
progression durable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:11:33 +00:00

464 lines
15 KiB
GDScript

extends CanvasLayer
const WATER_SURFACE_Y: float = 60.0
@onready var _oxygen_bar: ProgressBar = %OxygenBar
@onready var _health_bar: ProgressBar = %HealthBar
@onready var _hunger_bar: ProgressBar = %HungerBar
var _dolphin: Node3D = null
var _world_seed: int = 12345
var _info_panel: PanelContainer = null
var _depth_label: Label = null
var _biome_label: Label = null
var _compass_label: Label = null
var _xp_panel: PanelContainer = null
var _xp_bar: ProgressBar = null
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 _toast_container: VBoxContainer = null
var _toasts: Array = [] # each: {"node": PanelContainer, "timer": float}
var _biome_cache_pos: Vector3 = Vector3(99999, 99999, 99999)
var _biome_cached_name: String = ""
func _ready() -> void:
_style_bar(_oxygen_bar, Color(0.31, 0.76, 0.97))
_style_bar(_health_bar, Color(0.91, 0.30, 0.24))
_style_bar(_hunger_bar, Color(0.95, 0.61, 0.07))
_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()
_build_toast_container()
var am: Node = get_node_or_null("/root/AchievementManager")
if am != null:
am.achievement_unlocked.connect(_on_achievement_unlocked)
func _build_info_panel() -> void:
_info_panel = PanelContainer.new()
_info_panel.anchor_left = 1.0
_info_panel.anchor_right = 1.0
_info_panel.anchor_top = 0.0
_info_panel.anchor_bottom = 0.0
_info_panel.offset_left = -220.0
_info_panel.offset_top = 16.0
_info_panel.offset_right = -16.0
_info_panel.offset_bottom = 112.0
var style := StyleBoxFlat.new()
style.bg_color = Color(0.05, 0.05, 0.05, 0.7)
style.corner_radius_top_left = 10
style.corner_radius_top_right = 10
style.corner_radius_bottom_left = 10
style.corner_radius_bottom_right = 10
_info_panel.add_theme_stylebox_override("panel", style)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 4)
_info_panel.add_child(vbox)
_compass_label = Label.new()
_compass_label.text = "⬆ N"
_compass_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_compass_label.add_theme_font_size_override("font_size", 20)
_compass_label.add_theme_color_override("font_color", Color(0.85, 0.95, 1.0))
vbox.add_child(_compass_label)
_depth_label = Label.new()
_depth_label.text = "Prof. 0 m"
_depth_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_depth_label.add_theme_font_size_override("font_size", 14)
_depth_label.add_theme_color_override("font_color", Color(0.70, 0.88, 1.0))
vbox.add_child(_depth_label)
_biome_label = Label.new()
_biome_label.text = "Biome : —"
_biome_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_biome_label.add_theme_font_size_override("font_size", 14)
_biome_label.add_theme_color_override("font_color", Color(0.85, 0.95, 0.75))
vbox.add_child(_biome_label)
add_child(_info_panel)
func _build_xp_panel() -> void:
_xp_panel = PanelContainer.new()
_xp_panel.anchor_left = 0.5
_xp_panel.anchor_right = 0.5
_xp_panel.anchor_top = 1.0
_xp_panel.anchor_bottom = 1.0
_xp_panel.offset_left = -180.0
_xp_panel.offset_right = 180.0
_xp_panel.offset_top = -78.0
_xp_panel.offset_bottom = -68.0
var style := StyleBoxFlat.new()
style.bg_color = Color(0.03, 0.05, 0.09, 0.75)
style.set_corner_radius_all(6)
style.border_color = Color(1.0, 0.84, 0.2, 0.55)
style.set_border_width_all(1)
_xp_panel.add_theme_stylebox_override("panel", style)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 0)
_xp_panel.add_child(vbox)
_xp_label = Label.new()
_xp_label.text = "Niv. 1 — 0 / 50"
_xp_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_xp_label.add_theme_font_size_override("font_size", 11)
_xp_label.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4))
vbox.add_child(_xp_label)
_xp_bar = ProgressBar.new()
_xp_bar.custom_minimum_size = Vector2(340, 6)
_xp_bar.max_value = 50.0
_xp_bar.value = 0.0
_xp_bar.show_percentage = false
var fill := StyleBoxFlat.new()
fill.bg_color = Color(1.0, 0.82, 0.25)
fill.set_corner_radius_all(3)
_xp_bar.add_theme_stylebox_override("fill", fill)
var bg := StyleBoxFlat.new()
bg.bg_color = Color(0.08, 0.08, 0.10, 0.8)
bg.set_corner_radius_all(3)
_xp_bar.add_theme_stylebox_override("background", bg)
vbox.add_child(_xp_bar)
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_toast_container() -> void:
_toast_container = VBoxContainer.new()
_toast_container.anchor_left = 0.5
_toast_container.anchor_right = 0.5
_toast_container.anchor_top = 0.0
_toast_container.anchor_bottom = 0.0
_toast_container.offset_left = -220.0
_toast_container.offset_right = 220.0
_toast_container.offset_top = 16.0
_toast_container.offset_bottom = 16.0
_toast_container.add_theme_constant_override("separation", 6)
add_child(_toast_container)
func _on_achievement_unlocked(ach: Dictionary) -> void:
if _toast_container == null:
return
var panel := PanelContainer.new()
var style := StyleBoxFlat.new()
style.bg_color = Color(0.18, 0.14, 0.05, 0.92)
style.border_color = Color(1.0, 0.82, 0.2)
style.set_border_width_all(2)
style.set_corner_radius_all(8)
panel.add_theme_stylebox_override("panel", style)
var hbox := HBoxContainer.new()
hbox.add_theme_constant_override("separation", 10)
panel.add_child(hbox)
var icon := Label.new()
icon.text = ach.get("icon", "")
icon.add_theme_font_size_override("font_size", 28)
icon.add_theme_color_override("font_color", Color(1.0, 0.92, 0.4))
hbox.add_child(icon)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 0)
hbox.add_child(vbox)
var title := Label.new()
title.text = "SUCCÈS DÉBLOQUÉ : %s" % ach["name"]
title.add_theme_font_size_override("font_size", 13)
title.add_theme_color_override("font_color", Color(1.0, 0.95, 0.6))
vbox.add_child(title)
var desc := Label.new()
desc.text = "%s (+%d XP)" % [ach["desc"], ach["xp"]]
desc.add_theme_font_size_override("font_size", 11)
desc.add_theme_color_override("font_color", Color(0.88, 0.88, 0.88))
vbox.add_child(desc)
_toast_container.add_child(panel)
panel.modulate.a = 0.0
_toasts.append({"node": panel, "timer": 4.0, "age": 0.0})
var audio: Node = get_node_or_null("/root/AudioManager")
if audio != null and is_instance_valid(_dolphin) and audio.has_method("play_bubble_sfx"):
audio.call("play_bubble_sfx", _dolphin.global_position)
func _update_toasts(delta: float) -> void:
if _toasts.is_empty():
return
var i: int = _toasts.size() - 1
while i >= 0:
var t: Dictionary = _toasts[i]
t["age"] += delta
t["timer"] -= delta
var node: PanelContainer = t["node"]
if not is_instance_valid(node):
_toasts.remove_at(i)
i -= 1
continue
if t["age"] < 0.35:
node.modulate.a = t["age"] / 0.35
elif t["timer"] < 0.5:
node.modulate.a = maxf(t["timer"] / 0.5, 0.0)
else:
node.modulate.a = 1.0
if t["timer"] <= 0.0:
node.queue_free()
_toasts.remove_at(i)
i -= 1
func _build_level_banner() -> void:
_level_banner = Label.new()
_level_banner.anchor_left = 0.5
_level_banner.anchor_right = 0.5
_level_banner.anchor_top = 0.25
_level_banner.anchor_bottom = 0.25
_level_banner.offset_left = -200.0
_level_banner.offset_right = 200.0
_level_banner.offset_top = -28.0
_level_banner.offset_bottom = 28.0
_level_banner.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_level_banner.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_level_banner.add_theme_font_size_override("font_size", 40)
_level_banner.add_theme_color_override("font_color", Color(1.0, 0.95, 0.45))
_level_banner.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
_level_banner.add_theme_constant_override("outline_size", 6)
_level_banner.modulate.a = 0.0
add_child(_level_banner)
func _on_xp_changed(current_xp: int, xp_for_next: int, level: int) -> void:
if _xp_bar == null or _xp_label == null:
return
_xp_bar.max_value = float(xp_for_next)
_xp_bar.value = float(current_xp)
_xp_label.text = "Niv. %d%d / %d" % [level, current_xp, xp_for_next]
func _on_level_up(new_level: int) -> void:
if _level_banner == null:
return
_level_banner.text = "⬆ NIVEAU %d" % new_level
_level_banner.modulate.a = 1.0
_level_banner_timer = 2.2
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 _style_bar(bar: ProgressBar, color: Color) -> void:
var style := StyleBoxFlat.new()
style.bg_color = color
style.corner_radius_top_left = 4
style.corner_radius_top_right = 4
style.corner_radius_bottom_left = 4
style.corner_radius_bottom_right = 4
bar.add_theme_stylebox_override("fill", style)
var bg_style := StyleBoxFlat.new()
bg_style.bg_color = Color(0.1, 0.1, 0.1, 0.6)
bg_style.corner_radius_top_left = 4
bg_style.corner_radius_top_right = 4
bg_style.corner_radius_bottom_left = 4
bg_style.corner_radius_bottom_right = 4
bar.add_theme_stylebox_override("background", bg_style)
func connect_to_dolphin(dolphin: CharacterBody3D) -> void:
dolphin.stats_changed.connect(_on_stats_changed)
_dolphin = dolphin
var main: Node = get_tree().get_first_node_in_group("main")
if main != null:
var cm: Node = main.get_node_or_null("World/ChunkManager")
if cm != null and cm.get("world_seed") != null:
_world_seed = cm.world_seed
func _on_stats_changed(oxygen: float, hp: float, hunger: float) -> void:
_oxygen_bar.value = oxygen
_health_bar.value = hp
_hunger_bar.value = hunger
func _process(delta: float) -> void:
if _level_banner_timer > 0.0:
_level_banner_timer -= delta
if _level_banner != null:
var a: float = clampf(_level_banner_timer / 0.6, 0.0, 1.0)
_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)
_update_toasts(delta)
if not is_instance_valid(_dolphin):
return
var depth_m: int = int(round(WATER_SURFACE_Y - _dolphin.global_position.y))
if _depth_label != null:
_depth_label.text = "Prof. %d m" % depth_m
if depth_m > 0:
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)
var am2: Node = get_node_or_null("/root/AchievementManager")
if am2 != null and am2.has_method("note_depth"):
am2.call("note_depth", depth_m)
if _compass_label != null:
_compass_label.text = _compass_text(_dolphin.rotation.y)
if _biome_label != null:
_update_biome_label()
func _compass_text(yaw: float) -> String:
# Player forward is -Z when yaw=0 → North. Yaw rotates counterclockwise in Godot.
# Convert yaw to a heading [0, 360) where 0 = North, 90 = East.
var heading: float = rad_to_deg(-yaw)
heading = fposmod(heading, 360.0)
var labels: PackedStringArray = PackedStringArray(["N", "NE", "E", "SE", "S", "SO", "O", "NO"])
var arrows: PackedStringArray = PackedStringArray([
"", "", "", "", "", "", "", ""
])
var idx: int = int(round(heading / 45.0)) % 8
return "%s %s" % [arrows[idx], labels[idx]]
func _update_biome_label() -> void:
var pos: Vector3 = _dolphin.global_position
if pos.distance_to(_biome_cache_pos) < 4.0 and _biome_cached_name != "":
_biome_label.text = "Biome : %s" % _biome_cached_name
return
_biome_cache_pos = pos
_biome_cached_name = WorldGenerator.biome_name_at(pos.x, pos.z, pos.y, _world_seed)
_biome_label.text = "Biome : %s" % _biome_cached_name