menu interactive, refactor, perf tests, charge bar, debug menu stuff

This commit is contained in:
2026-07-22 22:57:02 +03:00
parent 631df017d1
commit 603a674e08
11 changed files with 772 additions and 315 deletions
+159 -122
View File
@@ -35,6 +35,16 @@ var _charge_ready_emitter: CPUParticles3D = null
var _slam_pending: bool = false
var _charge_trail_emitter: CPUParticles3D = null
# Floating 3D charge bar (mouse-button thrust charge indicator)
const _CHARGE_BAR_WIDTH: float = 1.1
const _CHARGE_BAR_HEIGHT: float = 1.45
var _charge_bar: Node3D = null
var _charge_bar_fill: MeshInstance3D = null
var _charge_bar_fill_mat: StandardMaterial3D = null
var _charge_bar_ramp: Gradient = null
var _charge_bar_time: float = 0.0
var _charge_bar_shown: float = 0.0
const MAX_HEALTH: int = 10
var health: int = MAX_HEALTH
signal health_changed(new_health: int)
@@ -55,6 +65,7 @@ func _ready() -> void:
_setup_hoof_dust()
_setup_legs()
_setup_charge_indicators()
_setup_charge_bar()
_setup_audio()
_bull_anim = _find_anim_player(cube_guy)
DP.any_changed.connect(_on_dp_changed)
@@ -69,18 +80,7 @@ func _setup_legs() -> void:
func _setup_hoof_dust() -> void:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var sphere := SphereMesh.new()
sphere.radius = 0.10
sphere.height = 0.20
sphere.radial_segments = 4
sphere.rings = 2
sphere.material = mat
var sphere := _particle_sphere(0.10, 4, 2, _unshaded_material())
_walk_dust_ramp = Gradient.new()
_walk_dust_ramp.set_color(0, Color(0.80, 0.69, 0.46, 1.0))
@@ -120,18 +120,7 @@ func _setup_hoof_dust() -> void:
func _setup_charge_indicators() -> void:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var sphere := SphereMesh.new()
sphere.radius = 0.07
sphere.height = 0.14
sphere.radial_segments = 4
sphere.rings = 2
sphere.material = mat
var sphere := _particle_sphere(0.07, 4, 2, _unshaded_material())
# Orange spark burst when single-button charge hits max
var burst_ramp := Gradient.new()
@@ -163,18 +152,7 @@ func _setup_charge_indicators() -> void:
cube_guy.add_child(_charge_ready_emitter)
# Blue energy trail — streams backward during charge
var trail_mat := StandardMaterial3D.new()
trail_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
trail_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
trail_mat.vertex_color_use_as_albedo = true
trail_mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var trail_sphere := SphereMesh.new()
trail_sphere.radius = 0.065
trail_sphere.height = 0.13
trail_sphere.radial_segments = 4
trail_sphere.rings = 2
trail_sphere.material = trail_mat
var trail_sphere := _particle_sphere(0.065, 4, 2, _unshaded_material())
var trail_ramp := Gradient.new()
trail_ramp.set_color(0, Color(0.30, 0.65, 1.0, 1.0))
@@ -206,6 +184,95 @@ func _setup_charge_indicators() -> void:
cube_guy.add_child(_charge_trail_emitter)
func _setup_charge_bar() -> void:
_charge_bar = Node3D.new()
_charge_bar.visible = false
add_child(_charge_bar)
var w := _CHARGE_BAR_WIDTH
var thickness := 0.16
# Dark translucent track behind the fill, slightly larger for a border look.
var track_mat := StandardMaterial3D.new()
track_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
track_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
track_mat.albedo_color = Color(0.02, 0.02, 0.04, 0.55)
var track_box := BoxMesh.new()
track_box.size = Vector3(w + 0.07, thickness + 0.07, thickness + 0.07)
track_box.material = track_mat
var track := MeshInstance3D.new()
track.mesh = track_box
track.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_charge_bar.add_child(track)
# Glowing fill bar; scaled along X each frame to show progress.
_charge_bar_fill_mat = StandardMaterial3D.new()
_charge_bar_fill_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
_charge_bar_fill_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
_charge_bar_fill_mat.emission_enabled = true
var fill_box := BoxMesh.new()
fill_box.size = Vector3(w, thickness, thickness)
fill_box.material = _charge_bar_fill_mat
_charge_bar_fill = MeshInstance3D.new()
_charge_bar_fill.mesh = fill_box
_charge_bar_fill.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_charge_bar.add_child(_charge_bar_fill)
# Cool → hot colour ramp as the charge fills (matches the spark FX palette).
_charge_bar_ramp = Gradient.new()
_charge_bar_ramp.offsets = PackedFloat32Array([0.0, 0.6, 1.0])
_charge_bar_ramp.colors = PackedColorArray([
Color(0.25, 0.65, 1.0),
Color(1.0, 0.85, 0.2),
Color(1.0, 0.28, 0.05),
])
func _process(delta: float) -> void:
_update_charge_bar(delta)
func _update_charge_bar(delta: float) -> void:
if _charge_bar == null:
return
_charge_bar_time += delta
var max_charge := DP.f("thrust_charge_time")
var charge := maxf(_thrust_left_charge, _thrust_right_charge)
var frac := clampf(charge / max_charge, 0.0, 1.0) if max_charge > 0.0 else 0.0
# Ease the whole bar in/out so it pops up when charging and fades when done.
var target := 1.0 if frac > 0.001 else 0.0
_charge_bar_shown = move_toward(_charge_bar_shown, target, delta * 6.0)
if _charge_bar_shown <= 0.001:
_charge_bar.visible = false
return
_charge_bar.visible = true
# Billboard toward the camera on the Y axis, staying upright.
var to_cam := camera_pivot.global_position - _charge_bar.global_position
_charge_bar.rotation.y = atan2(to_cam.x, to_cam.z)
# Grow the fill from the left edge.
var w := _CHARGE_BAR_WIDTH
_charge_bar_fill.scale.x = maxf(frac, 0.0001)
_charge_bar_fill.position.x = -0.5 * w + 0.5 * w * frac
# Colour + glow, with a fast pulse once fully charged.
var col := _charge_bar_ramp.sample(frac)
var pulse := 1.0 + (0.4 * sin(_charge_bar_time * 16.0) if frac >= 1.0 else 0.0)
_charge_bar_fill_mat.albedo_color = Color(col.r, col.g, col.b, 0.95)
_charge_bar_fill_mat.emission = col
_charge_bar_fill_mat.emission_energy_multiplier = 1.7 * pulse
# Float above the bull with a gentle bob; a small bounce + rise on the pop-in.
var bob := 0.03 * sin(_charge_bar_time * 4.0)
_charge_bar.position.y = _CHARGE_BAR_HEIGHT + bob - (1.0 - _charge_bar_shown) * 0.3
var appear := _charge_bar_shown * (1.06 - 0.06 * cos(_charge_bar_shown * PI))
var full_pop := 1.0 + (0.08 * sin(_charge_bar_time * 16.0) if frac >= 1.0 else 0.0)
_charge_bar.scale = Vector3.ONE * appear * full_pop
func _setup_audio() -> void:
if ResourceLoader.exists("res://sounds/bull_huff.ogg"):
_huff_player = AudioStreamPlayer.new()
@@ -273,8 +340,8 @@ func _find_anim_player(node: Node) -> AnimationPlayer:
func _unhandled_input(event: InputEvent) -> void:
if _ability_active:
return
# Abilities can be chained: a new one interrupts whatever is currently
# active, gated only by each ability's own cooldown.
if event.is_action_pressed(&"ability_kick") and ability_cd[0] <= 0.0:
_activate_kick()
elif event.is_action_pressed(&"ability_slam") and ability_cd[1] <= 0.0:
@@ -301,6 +368,8 @@ func _activate_kick() -> void:
_ability_active = true
_active_ability = 0
_ability_timer = 0.6
if _bull_anim:
_bull_anim.speed_scale = 1.0
if _bull_anim and _bull_anim.has_animation(&"Armature|FOOTKICK"):
_bull_anim.play(&"Armature|FOOTKICK")
_ability_timer = _bull_anim.get_animation(&"Armature|FOOTKICK").length
@@ -335,6 +404,8 @@ func _activate_dash() -> void:
_ability_active = true
_active_ability = 2
_ability_timer = 0.45
if _bull_anim:
_bull_anim.speed_scale = 1.0
if _bull_anim and _bull_anim.has_animation(&"Armature|DASH"):
_bull_anim.play(&"Armature|DASH")
_ability_timer = _bull_anim.get_animation(&"Armature|DASH").length
@@ -356,9 +427,10 @@ func _hit_matadors_cone(range_m: float, half_angle_rad: float, strength: float,
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
var to_mat: Vector3 = (mat as Node3D).global_position - global_position
to_mat.y = 0.0
if to_mat.length() > range_m:
var dist := to_mat.length()
if dist > range_m:
continue
if to_mat.length() > 0.01 and forward.dot(to_mat.normalized()) < cos(half_angle_rad):
if dist > 0.01 and forward.dot(to_mat / dist) < cos(half_angle_rad):
continue
mat.call(&"apply_ability_hit", (forward + Vector3.UP * 0.4).normalized(), strength)
@@ -367,13 +439,46 @@ func _hit_matadors_radius(range_m: float, strength: float, up_boost: float = 0.0
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
var to_mat: Vector3 = (mat as Node3D).global_position - global_position
to_mat.y = 0.0
if to_mat.length() > range_m:
var dist := to_mat.length()
if dist > range_m:
continue
var away := to_mat.normalized() if to_mat.length() > 0.01 \
var away := to_mat / dist if dist > 0.01 \
else Vector3(randf() - 0.5, 0.0, randf() - 0.5).normalized()
mat.call(&"apply_ability_hit", (away + Vector3.UP * 0.6).normalized(), strength, up_boost)
# ── Particle helpers ──────────────────────────────────────────────────────────
# All FX in this script use unshaded, vertex-coloured, alpha-blended particles;
# these three helpers remove the boilerplate each spawn function used to repeat.
func _unshaded_material() -> StandardMaterial3D:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
return mat
# Low-poly sphere sized for a particle mesh (height is always the diameter).
func _particle_sphere(radius: float, segments: int, rings: int, mat: Material) -> SphereMesh:
var sphere := SphereMesh.new()
sphere.radius = radius
sphere.height = radius * 2.0
sphere.radial_segments = segments
sphere.rings = rings
sphere.material = mat
return sphere
# Queue-free a transient FX node once its particles have finished.
func _free_after(node: Node, delay: float) -> void:
get_tree().create_timer(delay).timeout.connect(func() -> void:
if is_instance_valid(node):
node.queue_free()
)
func _spawn_slam_fx() -> void:
if not _slam_ring_mesh:
_slam_ring_mesh = _make_slam_ring_mesh()
@@ -417,18 +522,7 @@ func _launch_slam_ring(origin: Vector3, max_r: float,
func _spawn_slam_dust(origin: Vector3) -> void:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var sphere := SphereMesh.new()
sphere.radius = 0.09
sphere.height = 0.18
sphere.radial_segments = 4
sphere.rings = 2
sphere.material = mat
var sphere := _particle_sphere(0.09, 4, 2, _unshaded_material())
var ramp := Gradient.new()
ramp.set_color(0, Color(0.88, 0.74, 0.46, 1.0))
@@ -459,9 +553,7 @@ func _spawn_slam_dust(origin: Vector3) -> void:
get_parent().add_child(p)
p.global_position = origin
p.restart()
get_tree().create_timer(2.5).timeout.connect(func() -> void:
if is_instance_valid(p): p.queue_free()
)
_free_after(p, 2.5)
func _spawn_kick_fx(back_dir: Vector3) -> void:
@@ -474,18 +566,7 @@ func _spawn_kick_fx(back_dir: Vector3) -> void:
func _spawn_kick_dust(origin: Vector3, back_dir: Vector3) -> void:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var sphere := SphereMesh.new()
sphere.radius = 0.12
sphere.height = 0.24
sphere.radial_segments = 4
sphere.rings = 2
sphere.material = mat
var sphere := _particle_sphere(0.12, 4, 2, _unshaded_material())
var ramp := Gradient.new()
ramp.set_color(0, Color(1.0, 0.78, 0.18, 0.95))
@@ -516,24 +597,11 @@ func _spawn_kick_dust(origin: Vector3, back_dir: Vector3) -> void:
get_parent().add_child(p)
p.global_position = origin
p.restart()
get_tree().create_timer(2.5).timeout.connect(func() -> void:
if is_instance_valid(p): p.queue_free()
)
_free_after(p, 2.5)
func _spawn_kick_stones(origin: Vector3, back_dir: Vector3) -> void:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var sphere := SphereMesh.new()
sphere.radius = 0.055
sphere.height = 0.11
sphere.radial_segments = 3
sphere.rings = 1
sphere.material = mat
var sphere := _particle_sphere(0.055, 3, 1, _unshaded_material())
var ramp := Gradient.new()
ramp.set_color(0, Color(0.62, 0.58, 0.52, 1.0))
@@ -564,24 +632,14 @@ func _spawn_kick_stones(origin: Vector3, back_dir: Vector3) -> void:
get_parent().add_child(p)
p.global_position = origin
p.restart()
get_tree().create_timer(2.0).timeout.connect(func() -> void:
if is_instance_valid(p): p.queue_free()
)
_free_after(p, 2.0)
func _spawn_kick_flash(origin: Vector3) -> void:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
var mat := _unshaded_material()
mat.albedo_color = Color(1.0, 0.97, 0.40, 0.0)
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var sphere := SphereMesh.new()
sphere.radius = 1.0
sphere.height = 2.0
sphere.radial_segments = 8
sphere.rings = 4
sphere.material = mat
var sphere := _particle_sphere(1.0, 8, 4, mat)
var mi := MeshInstance3D.new()
mi.mesh = sphere
@@ -601,15 +659,9 @@ func _spawn_kick_flash(origin: Vector3) -> void:
func _spawn_dash_burst() -> void:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var box := BoxMesh.new()
box.size = Vector3(0.045, 0.045, 0.30)
box.material = mat
box.material = _unshaded_material()
var ramp := Gradient.new()
ramp.set_color(0, Color(0.50, 0.82, 1.0, 1.0))
@@ -639,24 +691,11 @@ func _spawn_dash_burst() -> void:
get_parent().add_child(p)
p.global_position = global_position + Vector3(0.0, 0.2, 0.0)
p.restart()
get_tree().create_timer(0.6).timeout.connect(func() -> void:
if is_instance_valid(p): p.queue_free()
)
_free_after(p, 0.6)
func _spawn_kick_sparks(origin: Vector3, back_dir: Vector3) -> void:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var sphere := SphereMesh.new()
sphere.radius = 0.04
sphere.height = 0.08
sphere.radial_segments = 3
sphere.rings = 1
sphere.material = mat
var sphere := _particle_sphere(0.04, 3, 1, _unshaded_material())
var ramp := Gradient.new()
ramp.set_color(0, Color(1.0, 0.98, 0.60, 1.0))
@@ -687,9 +726,7 @@ func _spawn_kick_sparks(origin: Vector3, back_dir: Vector3) -> void:
get_parent().add_child(p)
p.global_position = origin
p.restart()
get_tree().create_timer(1.0).timeout.connect(func() -> void:
if is_instance_valid(p): p.queue_free()
)
_free_after(p, 1.0)
func _make_slam_ring_mesh() -> ArrayMesh: