menu interactive, refactor, perf tests, charge bar, debug menu stuff
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user