Files
dauphincraft/scripts/dolphin/HUD.gd

733 lines
24 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 = ""
# Boost bar
var _boost_panel: PanelContainer = null
var _boost_bar: ProgressBar = null
var _boost_label: Label = null
# Speed lines overlay
var _speed_lines: Control = null
var _speed_line_data: Array = []
const SPEED_LINE_COUNT: int = 16
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()
_build_combo_panel()
_build_boost_bar()
_build_speed_lines()
var am: Node = get_node_or_null("/root/AchievementManager")
if am != null:
am.achievement_unlocked.connect(_on_achievement_unlocked)
var combo: Node = get_node_or_null("/root/ComboTracker")
if combo != null:
combo.combo_changed.connect(_on_combo_changed)
combo.combo_broken.connect(_on_combo_broken)
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
var _combo_panel: PanelContainer = null
var _combo_label: Label = null
var _combo_bar: ProgressBar = null
func _build_combo_panel() -> void:
_combo_panel = PanelContainer.new()
_combo_panel.anchor_left = 0.5
_combo_panel.anchor_right = 0.5
_combo_panel.anchor_top = 0.5
_combo_panel.anchor_bottom = 0.5
_combo_panel.offset_left = 120.0
_combo_panel.offset_right = 320.0
_combo_panel.offset_top = 30.0
_combo_panel.offset_bottom = 80.0
var style := StyleBoxFlat.new()
style.bg_color = Color(0.2, 0.08, 0.05, 0.85)
style.border_color = Color(1.0, 0.55, 0.15)
style.set_border_width_all(2)
style.set_corner_radius_all(8)
_combo_panel.add_theme_stylebox_override("panel", style)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 2)
_combo_panel.add_child(vbox)
_combo_label = Label.new()
_combo_label.text = "COMBO x1"
_combo_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_combo_label.add_theme_font_size_override("font_size", 20)
_combo_label.add_theme_color_override("font_color", Color(1.0, 0.85, 0.3))
_combo_label.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
_combo_label.add_theme_constant_override("outline_size", 4)
vbox.add_child(_combo_label)
_combo_bar = ProgressBar.new()
_combo_bar.max_value = 1.0
_combo_bar.value = 0.0
_combo_bar.custom_minimum_size = Vector2(180, 4)
_combo_bar.show_percentage = false
var fill := StyleBoxFlat.new()
fill.bg_color = Color(1.0, 0.55, 0.15)
fill.set_corner_radius_all(2)
_combo_bar.add_theme_stylebox_override("fill", fill)
var bg := StyleBoxFlat.new()
bg.bg_color = Color(0.1, 0.05, 0.03, 0.8)
bg.set_corner_radius_all(2)
_combo_bar.add_theme_stylebox_override("background", bg)
vbox.add_child(_combo_bar)
_combo_panel.visible = false
add_child(_combo_panel)
func _on_combo_changed(cnt: int, mult: float) -> void:
if _combo_panel == null:
return
if cnt < 2:
_combo_panel.visible = false
return
_combo_panel.visible = true
_combo_label.text = "COMBO x%d (+%d%%)" % [cnt, int(round((mult - 1.0) * 100.0))]
_combo_panel.scale = Vector2(1.15, 1.15)
var tween := create_tween()
tween.tween_property(_combo_panel, "scale", Vector2.ONE, 0.18).set_trans(Tween.TRANS_SINE)
func _on_combo_broken(final_count: int) -> void:
if final_count >= 5:
var pp: Node = get_node_or_null("/root/PlayerProgress")
if pp != null:
var bonus: int = final_count * 3
pp.award(bonus, "bonus combo x%d" % final_count, Vector3.ZERO)
var main: Node = get_tree().get_first_node_in_group("main")
if main != null and is_instance_valid(_dolphin) and main.has_method("_spawn_xp_popup"):
main.call("_spawn_xp_popup", bonus, _dolphin.global_position + Vector3(0, 2.0, 0))
func _update_combo_bar() -> void:
var combo: Node = get_node_or_null("/root/ComboTracker")
if combo == null or _combo_bar == null:
return
_combo_bar.value = combo.time_remaining_ratio()
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 _build_boost_bar() -> void:
_boost_panel = PanelContainer.new()
_boost_panel.anchor_left = 0.0
_boost_panel.anchor_right = 0.0
_boost_panel.anchor_top = 1.0
_boost_panel.anchor_bottom = 1.0
_boost_panel.offset_left = 16.0
_boost_panel.offset_right = 220.0
_boost_panel.offset_top = -78.0
_boost_panel.offset_bottom = -48.0
var style := StyleBoxFlat.new()
style.bg_color = Color(0.04, 0.06, 0.12, 0.78)
style.border_color = Color(0.3, 0.7, 1.0, 0.6)
style.set_border_width_all(1)
style.set_corner_radius_all(6)
_boost_panel.add_theme_stylebox_override("panel", style)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 2)
_boost_panel.add_child(vbox)
_boost_label = Label.new()
_boost_label.text = "BOOST"
_boost_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_boost_label.add_theme_font_size_override("font_size", 10)
_boost_label.add_theme_color_override("font_color", Color(0.55, 0.85, 1.0))
vbox.add_child(_boost_label)
_boost_bar = ProgressBar.new()
_boost_bar.custom_minimum_size = Vector2(188, 7)
_boost_bar.max_value = 100.0
_boost_bar.value = 100.0
_boost_bar.show_percentage = false
var fill := StyleBoxFlat.new()
fill.bg_color = Color(0.3, 0.75, 1.0)
fill.set_corner_radius_all(3)
_boost_bar.add_theme_stylebox_override("fill", fill)
var bg := StyleBoxFlat.new()
bg.bg_color = Color(0.06, 0.08, 0.14, 0.8)
bg.set_corner_radius_all(3)
_boost_bar.add_theme_stylebox_override("background", bg)
vbox.add_child(_boost_bar)
add_child(_boost_panel)
func _build_speed_lines() -> void:
_speed_lines = Control.new()
_speed_lines.set_anchors_preset(Control.PRESET_FULL_RECT)
_speed_lines.mouse_filter = Control.MOUSE_FILTER_IGNORE
_speed_lines.modulate.a = 0.0
_speed_lines.draw.connect(_draw_speed_lines)
add_child(_speed_lines)
# Init line data (angle, length ratio, alpha)
for i: int in range(SPEED_LINE_COUNT):
_speed_line_data.append({
"angle": (float(i) / float(SPEED_LINE_COUNT)) * TAU + randf() * 0.3,
"len": randf_range(0.25, 0.75),
"alpha": randf_range(0.4, 1.0),
"offset": randf_range(0.3, 0.85)
})
func _draw_speed_lines() -> void:
if _speed_lines == null:
return
var size: Vector2 = _speed_lines.size
var center: Vector2 = size * 0.5
var max_r: float = size.length() * 0.55
for line: Dictionary in _speed_line_data:
var dir: Vector2 = Vector2(cos(line["angle"]), sin(line["angle"]))
var start: Vector2 = center + dir * max_r * line["offset"]
var end: Vector2 = center + dir * max_r * (line["offset"] + line["len"] * 0.35)
var col: Color = Color(0.55, 0.85, 1.0, line["alpha"] * 0.55)
_speed_lines.draw_line(start, end, col, 1.5)
func connect_to_dolphin(dolphin: CharacterBody3D) -> void:
dolphin.stats_changed.connect(_on_stats_changed)
if dolphin.has_signal("boost_changed"):
dolphin.boost_changed.connect(_on_boost_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()
_update_combo_bar()
_update_speed_lines(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
func _on_boost_changed(energy: float, max_energy: float, active: bool) -> void:
if _boost_bar == null:
return
_boost_bar.max_value = max_energy
_boost_bar.value = energy
# Color shift: full = cyan, low = orange
var t: float = energy / max_energy
var fill := StyleBoxFlat.new()
fill.bg_color = Color(1.0 - t * 0.7, 0.55 + t * 0.3, t)
fill.set_corner_radius_all(3)
_boost_bar.add_theme_stylebox_override("fill", fill)
if _boost_label != null:
_boost_label.text = "BOOST %d%%" % int(energy / max_energy * 100.0)
if active:
_boost_label.add_theme_color_override("font_color", Color(0.3, 0.9, 1.0))
elif energy < max_energy * 0.25:
_boost_label.add_theme_color_override("font_color", Color(1.0, 0.45, 0.2))
else:
_boost_label.add_theme_color_override("font_color", Color(0.55, 0.85, 1.0))
var _boost_active: bool = false
func _update_speed_lines(delta: float) -> void:
if _speed_lines == null:
return
var target_alpha: float = 0.0
if is_instance_valid(_dolphin) and _dolphin.get("is_boosting"):
if _dolphin.is_boosting:
target_alpha = 1.0
_boost_active = true
# Animate line angles for motion feel
var t: float = Time.get_ticks_msec() * 0.001
for i: int in range(_speed_line_data.size()):
_speed_line_data[i]["angle"] += delta * (0.4 + float(i) * 0.05)
else:
_boost_active = false
_speed_lines.modulate.a = lerpf(_speed_lines.modulate.a, target_alpha, delta * 5.0)
if _speed_lines.modulate.a > 0.02:
_speed_lines.queue_redraw()