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() _build_consumable_hint() 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) var _consumable_hint: Label = null func _build_consumable_hint() -> void: _consumable_hint = Label.new() _consumable_hint.anchor_left = 0.5 _consumable_hint.anchor_right = 0.5 _consumable_hint.anchor_top = 1.0 _consumable_hint.anchor_bottom = 1.0 _consumable_hint.offset_left = -220.0 _consumable_hint.offset_right = 220.0 _consumable_hint.offset_top = -105.0 _consumable_hint.offset_bottom = -85.0 _consumable_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _consumable_hint.add_theme_font_size_override("font_size", 11) _consumable_hint.add_theme_color_override("font_color", Color(0.85, 0.95, 1.0, 0.9)) _consumable_hint.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85)) _consumable_hint.add_theme_constant_override("outline_size", 3) _consumable_hint.visible = false add_child(_consumable_hint) func _update_consumable_hint() -> void: if _consumable_hint == null or not is_instance_valid(_dolphin): return var main: Node = get_tree().get_first_node_in_group("main") if main == null or not ("inventory" in main): _consumable_hint.visible = false return var slot: Variant = main.inventory.get_selected_item() if slot == null: _consumable_hint.visible = false return if not ItemDatabase.is_consumable(slot["item_id"]): _consumable_hint.visible = false return _consumable_hint.text = "[F] Consommer : %s" % ItemDatabase.get_item_name(slot["item_id"]) _consumable_hint.visible = true 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) _update_consumable_hint() 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