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>
367 lines
12 KiB
GDScript
367 lines
12 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 _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()
|
|
|
|
|
|
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_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)
|
|
|
|
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)
|
|
|
|
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
|