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>
464 lines
15 KiB
GDScript
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
|