diff --git a/project.godot b/project.godot index 40cc3ee..e66d5b0 100644 --- a/project.godot +++ b/project.godot @@ -100,6 +100,7 @@ ItemDatabase="*res://scripts/inventory/ItemDatabase.gd" NetworkManager="*res://scripts/net/NetworkManager.gd" PlayerProgress="*res://scripts/progression/PlayerProgress.gd" QuestManager="*res://scripts/progression/QuestManager.gd" +AchievementManager="*res://scripts/progression/AchievementManager.gd" [rendering] environment/defaults/default_clear_color=Color(0.05, 0.15, 0.25, 1) diff --git a/scripts/Main.gd b/scripts/Main.gd index 2f7f4a3..58577bb 100644 --- a/scripts/Main.gd +++ b/scripts/Main.gd @@ -251,6 +251,9 @@ func _award_break_xp(block_id: int, hit_position: Vector3) -> void: var qm: Node = get_node_or_null("/root/QuestManager") if qm != null: qm.note_block_break(block_id) + var am: Node = get_node_or_null("/root/AchievementManager") + if am != null: + am.note_block_break(block_id) func _spawn_xp_popup(amount: int, world_pos: Vector3) -> void: @@ -289,6 +292,9 @@ func _on_block_place(hit_position: Vector3, normal: Vector3) -> void: func _on_echolocation(position: Vector3, _radius: float) -> void: AudioManager.play_bubble_sfx(position) + var am: Node = get_node_or_null("/root/AchievementManager") + if am != null: + am.note_echolocation() # Called by WorldSyncComponent when server confirms a block was broken by this client diff --git a/scripts/dolphin/HUD.gd b/scripts/dolphin/HUD.gd index 02724de..9805fb0 100644 --- a/scripts/dolphin/HUD.gd +++ b/scripts/dolphin/HUD.gd @@ -25,6 +25,9 @@ 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 = "" @@ -48,6 +51,10 @@ func _ready() -> void: 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: @@ -232,6 +239,91 @@ func _on_quest_completed(q: Dictionary) -> void: 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 @@ -320,6 +412,8 @@ func _process(delta: float) -> void: 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 @@ -334,6 +428,9 @@ func _process(delta: float) -> void: 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) diff --git a/scripts/inventory/CraftingRecipes.gd b/scripts/inventory/CraftingRecipes.gd index ac0e1f4..2e6e48b 100644 --- a/scripts/inventory/CraftingRecipes.gd +++ b/scripts/inventory/CraftingRecipes.gd @@ -64,3 +64,6 @@ static func _award_craft_xp(recipe: Dictionary) -> void: var qm: Node = root.get_node_or_null("/root/QuestManager") if qm != null: qm.note_craft() + var ach: Node = root.get_node_or_null("/root/AchievementManager") + if ach != null: + ach.note_craft() diff --git a/scripts/progression/AchievementManager.gd b/scripts/progression/AchievementManager.gd new file mode 100644 index 0000000..e2571c0 --- /dev/null +++ b/scripts/progression/AchievementManager.gd @@ -0,0 +1,91 @@ +extends Node + +signal achievement_unlocked(ach: Dictionary) + +const ACHIEVEMENTS: Array = [ + {"id": "first_break", "name": "Premier éclat", "desc": "Casser ton premier bloc", "xp": 10, "icon": "⛏"}, + {"id": "ten_breaks", "name": "Mineur apprenti", "desc": "Casser 10 blocs", "xp": 20, "icon": "⛏"}, + {"id": "hundred_breaks", "name": "Mineur confirmé", "desc": "Casser 100 blocs", "xp": 80, "icon": "⛏"}, + {"id": "first_pearl", "name": "Lueur nacrée", "desc": "Collecter ta première perle", "xp": 20, "icon": "◉"}, + {"id": "ten_pearls", "name": "Chasseur de perles", "desc": "Collecter 10 perles", "xp": 120, "icon": "◉"}, + {"id": "first_craft", "name": "Artisan", "desc": "Crafter ton premier objet", "xp": 15, "icon": "✦"}, + {"id": "first_wreck", "name": "Fouilleur d'épaves", "desc": "Piller une épave", "xp": 30, "icon": "⚓"}, + {"id": "depth_50", "name": "Plongée profonde", "desc": "Atteindre -50 m", "xp": 60, "icon": "▼"}, + {"id": "depth_100", "name": "Abysses", "desc": "Atteindre -100 m", "xp": 200, "icon": "▼"}, + {"id": "level_5", "name": "Dauphin aguerri", "desc": "Atteindre le niveau 5", "xp": 50, "icon": "★"}, + {"id": "level_10", "name": "Légende des océans", "desc": "Atteindre le niveau 10", "xp": 150, "icon": "★"}, + {"id": "echo_first", "name": "Sonar aiguisé", "desc": "Utiliser l'écholocation", "xp": 10, "icon": "))"}, +] + +var unlocked: Dictionary = {} # id -> true + +var _break_count: int = 0 +var _pearl_count: int = 0 + + +func _ready() -> void: + var pp: Node = get_node_or_null("/root/PlayerProgress") + if pp != null: + pp.level_up.connect(_on_level_up) + + +func note_block_break(block_id: int) -> void: + _break_count += 1 + if _break_count >= 1: + _unlock("first_break") + if _break_count >= 10: + _unlock("ten_breaks") + if _break_count >= 100: + _unlock("hundred_breaks") + if block_id == 7: + _unlock("first_wreck") + + +func note_pearl_collected() -> void: + _pearl_count += 1 + if _pearl_count >= 1: + _unlock("first_pearl") + if _pearl_count >= 10: + _unlock("ten_pearls") + + +func note_craft() -> void: + _unlock("first_craft") + + +func note_depth(depth_m: int) -> void: + if depth_m >= 50: + _unlock("depth_50") + if depth_m >= 100: + _unlock("depth_100") + + +func note_echolocation() -> void: + _unlock("echo_first") + + +func _on_level_up(new_level: int) -> void: + if new_level >= 5: + _unlock("level_5") + if new_level >= 10: + _unlock("level_10") + + +func _unlock(id: String) -> void: + if unlocked.has(id): + return + var ach: Dictionary = _find(id) + if ach.is_empty(): + return + unlocked[id] = true + achievement_unlocked.emit(ach) + var pp: Node = get_node_or_null("/root/PlayerProgress") + if pp != null: + pp.award(ach["xp"], "succès: %s" % ach["name"], Vector3.ZERO) + + +func _find(id: String) -> Dictionary: + for a: Dictionary in ACHIEVEMENTS: + if a["id"] == id: + return a + return {} diff --git a/scripts/world/PearlSpawner.gd b/scripts/world/PearlSpawner.gd index dd4336d..1da19c9 100644 --- a/scripts/world/PearlSpawner.gd +++ b/scripts/world/PearlSpawner.gd @@ -125,3 +125,6 @@ func _on_pearl_collected(item_id: int) -> void: var qm: Node = get_node_or_null("/root/QuestManager") if qm != null: qm.note_pearl_collected() + var ach: Node = get_node_or_null("/root/AchievementManager") + if ach != null: + ach.note_pearl_collected()