diff --git a/project.godot b/project.godot index e66d5b0..5e45ed0 100644 --- a/project.godot +++ b/project.godot @@ -101,6 +101,7 @@ NetworkManager="*res://scripts/net/NetworkManager.gd" PlayerProgress="*res://scripts/progression/PlayerProgress.gd" QuestManager="*res://scripts/progression/QuestManager.gd" AchievementManager="*res://scripts/progression/AchievementManager.gd" +ComboTracker="*res://scripts/progression/ComboTracker.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 76c0bed..8acdffb 100644 --- a/scripts/Main.gd +++ b/scripts/Main.gd @@ -247,7 +247,12 @@ func _award_break_xp(block_id: int, hit_position: Vector3) -> void: var pp: Node = get_node_or_null("/root/PlayerProgress") if pp == null: return - var gain: int = pp.XP_BREAK_BY_BLOCK.get(block_id, pp.XP_BREAK_DEFAULT) + var base_gain: int = pp.XP_BREAK_BY_BLOCK.get(block_id, pp.XP_BREAK_DEFAULT) + var combo: Node = get_node_or_null("/root/ComboTracker") + var mult: float = 1.0 + if combo != null: + mult = combo.bump() + var gain: int = int(round(base_gain * mult)) pp.award(gain, "bloc", hit_position) _spawn_xp_popup(gain, hit_position) var qm: Node = get_node_or_null("/root/QuestManager") diff --git a/scripts/dolphin/HUD.gd b/scripts/dolphin/HUD.gd index 7e1ac84..62f0111 100644 --- a/scripts/dolphin/HUD.gd +++ b/scripts/dolphin/HUD.gd @@ -53,9 +53,14 @@ func _ready() -> void: _on_quests_updated() _build_toast_container() _build_consumable_hint() + _build_combo_panel() 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: @@ -242,6 +247,91 @@ func _on_quest_completed(q: Dictionary) -> void: 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() @@ -455,6 +545,7 @@ func _process(delta: float) -> void: _update_toasts(delta) _update_consumable_hint() + _update_combo_bar() if not is_instance_valid(_dolphin): return diff --git a/scripts/progression/ComboTracker.gd b/scripts/progression/ComboTracker.gd new file mode 100644 index 0000000..a9392da --- /dev/null +++ b/scripts/progression/ComboTracker.gd @@ -0,0 +1,48 @@ +extends Node + +signal combo_changed(count: int, multiplier: float) +signal combo_broken(final_count: int) + +const WINDOW_SEC: float = 1.8 +const STEP_BONUS: float = 0.15 # +15% per combo step +const MAX_MULT: float = 3.0 + +var count: int = 0 +var multiplier: float = 1.0 +var _timer: float = 0.0 +var _active: bool = false + + +func _process(delta: float) -> void: + if not _active: + return + _timer -= delta + if _timer <= 0.0: + _break() + + +func bump() -> float: + # Advances combo and returns multiplier to apply to the triggering gain. + count += 1 + multiplier = minf(1.0 + float(count - 1) * STEP_BONUS, MAX_MULT) + _timer = WINDOW_SEC + _active = true + combo_changed.emit(count, multiplier) + return multiplier + + +func _break() -> void: + var final_count: int = count + _active = false + combo_changed.emit(0, 1.0) + if final_count >= 2: + combo_broken.emit(final_count) + count = 0 + multiplier = 1.0 + _timer = 0.0 + + +func time_remaining_ratio() -> float: + if not _active: + return 0.0 + return clampf(_timer / WINDOW_SEC, 0.0, 1.0)