[agent:claude-cli] feat(boost): turbo boost avec énergie/cooldown, barre HUD dynamique, speed lines overlay et son distinct au déclenchement
This commit is contained in:
@@ -19,6 +19,13 @@ signal echolocation_triggered(position: Vector3, radius: float)
|
|||||||
signal stats_changed(oxygen: float, health: float, hunger: float)
|
signal stats_changed(oxygen: float, health: float, hunger: float)
|
||||||
signal player_died()
|
signal player_died()
|
||||||
signal use_consumable_requested()
|
signal use_consumable_requested()
|
||||||
|
signal boost_changed(energy: float, max_energy: float, is_active: bool)
|
||||||
|
|
||||||
|
# --- Boost parameters ---
|
||||||
|
@export var boost_max_energy: float = 100.0
|
||||||
|
@export var boost_drain_rate: float = 40.0 # energy/s while boosting
|
||||||
|
@export var boost_regen_rate: float = 20.0 # energy/s when not boosting
|
||||||
|
@export var boost_min_energy_to_start: float = 15.0 # minimum to engage boost
|
||||||
|
|
||||||
# --- Internal State ---
|
# --- Internal State ---
|
||||||
var oxygen: float
|
var oxygen: float
|
||||||
@@ -26,6 +33,7 @@ var health: float
|
|||||||
var hunger: float
|
var hunger: float
|
||||||
var is_boosting: bool = false
|
var is_boosting: bool = false
|
||||||
var is_echolocating: bool = false
|
var is_echolocating: bool = false
|
||||||
|
var boost_energy: float = 100.0
|
||||||
|
|
||||||
var _yaw: float = 0.0
|
var _yaw: float = 0.0
|
||||||
var _pitch: float = 0.0
|
var _pitch: float = 0.0
|
||||||
@@ -47,6 +55,7 @@ func _ready() -> void:
|
|||||||
oxygen = max_oxygen
|
oxygen = max_oxygen
|
||||||
health = max_health
|
health = max_health
|
||||||
hunger = max_hunger
|
hunger = max_hunger
|
||||||
|
boost_energy = boost_max_energy
|
||||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||||
_setup_input_actions()
|
_setup_input_actions()
|
||||||
_emit_stats()
|
_emit_stats()
|
||||||
@@ -125,7 +134,25 @@ func _physics_process(delta: float) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _update_movement(delta: float) -> void:
|
func _update_movement(delta: float) -> void:
|
||||||
is_boosting = Input.is_action_pressed("boost")
|
var wants_boost: bool = Input.is_action_pressed("boost")
|
||||||
|
# Only boost if we have energy and enough to start
|
||||||
|
if wants_boost and boost_energy >= boost_min_energy_to_start:
|
||||||
|
is_boosting = true
|
||||||
|
elif wants_boost and is_boosting and boost_energy > 0.0:
|
||||||
|
is_boosting = true # keep boosting until empty
|
||||||
|
else:
|
||||||
|
is_boosting = false
|
||||||
|
|
||||||
|
# Update boost energy
|
||||||
|
if is_boosting:
|
||||||
|
boost_energy = max(0.0, boost_energy - boost_drain_rate * delta)
|
||||||
|
if boost_energy <= 0.0:
|
||||||
|
is_boosting = false
|
||||||
|
else:
|
||||||
|
boost_energy = min(boost_max_energy, boost_energy + boost_regen_rate * delta)
|
||||||
|
|
||||||
|
boost_changed.emit(boost_energy, boost_max_energy, is_boosting)
|
||||||
|
|
||||||
var speed: float = swim_speed * (boost_multiplier if is_boosting else 1.0)
|
var speed: float = swim_speed * (boost_multiplier if is_boosting else 1.0)
|
||||||
|
|
||||||
# forward/right using camera pitch for intuitive swim direction
|
# forward/right using camera pitch for intuitive swim direction
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ var _toasts: Array = [] # each: {"node": PanelContainer, "timer": float}
|
|||||||
var _biome_cache_pos: Vector3 = Vector3(99999, 99999, 99999)
|
var _biome_cache_pos: Vector3 = Vector3(99999, 99999, 99999)
|
||||||
var _biome_cached_name: String = ""
|
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:
|
func _ready() -> void:
|
||||||
_style_bar(_oxygen_bar, Color(0.31, 0.76, 0.97))
|
_style_bar(_oxygen_bar, Color(0.31, 0.76, 0.97))
|
||||||
@@ -54,6 +64,8 @@ func _ready() -> void:
|
|||||||
_build_toast_container()
|
_build_toast_container()
|
||||||
_build_consumable_hint()
|
_build_consumable_hint()
|
||||||
_build_combo_panel()
|
_build_combo_panel()
|
||||||
|
_build_boost_bar()
|
||||||
|
_build_speed_lines()
|
||||||
var am: Node = get_node_or_null("/root/AchievementManager")
|
var am: Node = get_node_or_null("/root/AchievementManager")
|
||||||
if am != null:
|
if am != null:
|
||||||
am.achievement_unlocked.connect(_on_achievement_unlocked)
|
am.achievement_unlocked.connect(_on_achievement_unlocked)
|
||||||
@@ -513,8 +525,88 @@ func _style_bar(bar: ProgressBar, color: Color) -> void:
|
|||||||
bar.add_theme_stylebox_override("background", bg_style)
|
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:
|
func connect_to_dolphin(dolphin: CharacterBody3D) -> void:
|
||||||
dolphin.stats_changed.connect(_on_stats_changed)
|
dolphin.stats_changed.connect(_on_stats_changed)
|
||||||
|
if dolphin.has_signal("boost_changed"):
|
||||||
|
dolphin.boost_changed.connect(_on_boost_changed)
|
||||||
_dolphin = dolphin
|
_dolphin = dolphin
|
||||||
|
|
||||||
var main: Node = get_tree().get_first_node_in_group("main")
|
var main: Node = get_tree().get_first_node_in_group("main")
|
||||||
@@ -546,6 +638,7 @@ func _process(delta: float) -> void:
|
|||||||
_update_toasts(delta)
|
_update_toasts(delta)
|
||||||
_update_consumable_hint()
|
_update_consumable_hint()
|
||||||
_update_combo_bar()
|
_update_combo_bar()
|
||||||
|
_update_speed_lines(delta)
|
||||||
|
|
||||||
if not is_instance_valid(_dolphin):
|
if not is_instance_valid(_dolphin):
|
||||||
return
|
return
|
||||||
@@ -594,3 +687,46 @@ func _update_biome_label() -> void:
|
|||||||
_biome_cache_pos = pos
|
_biome_cache_pos = pos
|
||||||
_biome_cached_name = WorldGenerator.biome_name_at(pos.x, pos.z, pos.y, _world_seed)
|
_biome_cached_name = WorldGenerator.biome_name_at(pos.x, pos.z, pos.y, _world_seed)
|
||||||
_biome_label.text = "Biome : %s" % _biome_cached_name
|
_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()
|
||||||
|
|||||||
Reference in New Issue
Block a user