New screens, new hud, new intro menu, some UI element changes
This commit is contained in:
@@ -22,12 +22,8 @@ const _ANIM_ROLL: StringName = &"Armature|ROLL"
|
||||
var _hoof_emitters: Array[CPUParticles3D] = []
|
||||
var _legs: Node
|
||||
|
||||
var _kick_timer: float = 0.0
|
||||
var _was_on_floor: bool = false
|
||||
var _pre_slide_vel_y: float = 0.0
|
||||
var _thrust_left_charge: float = 0.0
|
||||
var _thrust_right_charge: float = 0.0
|
||||
var _charge_was_full: bool = false
|
||||
|
||||
var _slam_ring_mesh: ArrayMesh = null
|
||||
|
||||
@@ -39,21 +35,13 @@ var _active_ability: int = -1
|
||||
var _dash_dir: Vector3 = Vector3.ZERO
|
||||
var _roll_dir: Vector3 = Vector3.ZERO
|
||||
var _roll_ramp: float = 0.0
|
||||
var _roll_spin: float = 0.0
|
||||
var _bull_anim: AnimationPlayer = null
|
||||
|
||||
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
|
||||
var _roll_spark_emitter: CPUParticles3D = null
|
||||
var _cube_scale: Vector3 = Vector3.ONE
|
||||
|
||||
# One clean hit ends the run — see take_sword_hit(). The HUD listens for `died`
|
||||
# to raise the lose screen (the `cause` tags how it happened, for the death notice);
|
||||
@@ -64,7 +52,6 @@ var _dead: bool = false
|
||||
var _huff_player: AudioStreamPlayer = null
|
||||
var _crowd_player: AudioStreamPlayer = null
|
||||
var _huff_timer: float = 0.0
|
||||
var _roll_damping: float = 0.15
|
||||
|
||||
enum DustState { NONE, WALK, CHARGE }
|
||||
var _dust_state: DustState = DustState.NONE
|
||||
@@ -74,15 +61,15 @@ var _charge_dust_ramp: Gradient = null
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group(&"player")
|
||||
_cube_scale = cube_guy.scale
|
||||
_setup_hoof_dust()
|
||||
_setup_legs()
|
||||
_setup_charge_indicators()
|
||||
_setup_charge_bar()
|
||||
_setup_charge_trail()
|
||||
_setup_roll_sparks()
|
||||
_setup_audio()
|
||||
_bull_anim = _find_anim_player(cube_guy)
|
||||
_setup_bull_idle()
|
||||
DP.any_changed.connect(_on_dp_changed)
|
||||
_roll_damping = DP.f("roll_damping")
|
||||
|
||||
|
||||
func _setup_legs() -> void:
|
||||
@@ -132,39 +119,8 @@ func _setup_hoof_dust() -> void:
|
||||
_hoof_emitters.append(p)
|
||||
|
||||
|
||||
func _setup_charge_indicators() -> void:
|
||||
var sphere := _particle_sphere(0.07, 4, 2, _unshaded_material())
|
||||
|
||||
# Orange spark burst when single-button charge hits max
|
||||
var burst_ramp := Gradient.new()
|
||||
burst_ramp.set_color(0, Color(1.0, 0.55, 0.0, 1.0))
|
||||
burst_ramp.set_color(1, Color(1.0, 0.15, 0.0, 0.0))
|
||||
|
||||
var burst_scale := Curve.new()
|
||||
burst_scale.add_point(Vector2(0.0, 1.0))
|
||||
burst_scale.add_point(Vector2(1.0, 0.0))
|
||||
|
||||
_charge_ready_emitter = CPUParticles3D.new()
|
||||
_charge_ready_emitter.position = Vector3(0.0, 0.3, 0.0)
|
||||
_charge_ready_emitter.emitting = false
|
||||
_charge_ready_emitter.one_shot = true
|
||||
_charge_ready_emitter.amount = 16
|
||||
_charge_ready_emitter.lifetime = 0.5
|
||||
_charge_ready_emitter.explosiveness = 1.0
|
||||
_charge_ready_emitter.randomness = 0.4
|
||||
_charge_ready_emitter.local_coords = false
|
||||
_charge_ready_emitter.direction = Vector3.UP
|
||||
_charge_ready_emitter.spread = 120.0
|
||||
_charge_ready_emitter.initial_velocity_min = 2.5
|
||||
_charge_ready_emitter.initial_velocity_max = 5.0
|
||||
_charge_ready_emitter.scale_amount_min = 0.2
|
||||
_charge_ready_emitter.scale_amount_max = 0.5
|
||||
_charge_ready_emitter.mesh = sphere
|
||||
_charge_ready_emitter.color_ramp = burst_ramp
|
||||
_charge_ready_emitter.scale_amount_curve = burst_scale
|
||||
cube_guy.add_child(_charge_ready_emitter)
|
||||
|
||||
# Blue energy trail — streams backward during charge
|
||||
func _setup_charge_trail() -> void:
|
||||
# Blue energy trail — streams backward during a fast run / roll
|
||||
var trail_sphere := _particle_sphere(0.065, 4, 2, _unshaded_material())
|
||||
|
||||
var trail_ramp := Gradient.new()
|
||||
@@ -197,93 +153,39 @@ 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)
|
||||
func _setup_roll_sparks() -> void:
|
||||
# Rammus Powerball ground spray — dirt/sparks flung outward from the spinning
|
||||
# ball. World-space (local_coords off) so they peel off into a trail behind it.
|
||||
var spark := _particle_sphere(0.05, 4, 2, _unshaded_material())
|
||||
|
||||
var w := _CHARGE_BAR_WIDTH
|
||||
var thickness := 0.16
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(1.00, 0.78, 0.32, 1.0))
|
||||
ramp.set_color(1, Color(0.70, 0.42, 0.14, 0.0))
|
||||
|
||||
# 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)
|
||||
var sc := Curve.new()
|
||||
sc.add_point(Vector2(0.0, 0.30))
|
||||
sc.add_point(Vector2(0.3, 1.00))
|
||||
sc.add_point(Vector2(1.0, 0.00))
|
||||
|
||||
# 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
|
||||
_roll_spark_emitter = CPUParticles3D.new()
|
||||
_roll_spark_emitter.position = Vector3(0.0, 0.10, 0.0)
|
||||
_roll_spark_emitter.emitting = false
|
||||
_roll_spark_emitter.one_shot = false
|
||||
_roll_spark_emitter.amount = 46
|
||||
_roll_spark_emitter.lifetime = 0.45
|
||||
_roll_spark_emitter.randomness = 0.7
|
||||
_roll_spark_emitter.local_coords = false
|
||||
_roll_spark_emitter.direction = Vector3(0.0, 1.0, 0.0)
|
||||
_roll_spark_emitter.spread = 180.0
|
||||
_roll_spark_emitter.gravity = Vector3(0.0, -6.0, 0.0)
|
||||
_roll_spark_emitter.initial_velocity_min = 3.0
|
||||
_roll_spark_emitter.initial_velocity_max = 8.0
|
||||
_roll_spark_emitter.scale_amount_min = 0.5
|
||||
_roll_spark_emitter.scale_amount_max = 1.3
|
||||
_roll_spark_emitter.mesh = spark
|
||||
_roll_spark_emitter.color_ramp = ramp
|
||||
_roll_spark_emitter.scale_amount_curve = sc
|
||||
cube_guy.add_child(_roll_spark_emitter)
|
||||
|
||||
|
||||
func _setup_audio() -> void:
|
||||
@@ -334,7 +236,6 @@ func _set_dust_state(new_state: DustState) -> void:
|
||||
|
||||
|
||||
func _on_dp_changed(_key: String, _val: Variant) -> void:
|
||||
_roll_damping = DP.f("roll_damping")
|
||||
if _hoof_emitters.is_empty() or _dust_state == DustState.NONE:
|
||||
return
|
||||
var prev := _dust_state
|
||||
@@ -464,6 +365,7 @@ func _activate_roll() -> void:
|
||||
_active_ability = 3
|
||||
_ability_timer = DP.f("roll_duration")
|
||||
_roll_ramp = 0.0
|
||||
_roll_spin = 0.0
|
||||
# Curl into the ball along the current heading (or facing when standing still)
|
||||
# and launch at the base speed — it ramps up from here the longer you roll.
|
||||
var flat := Vector3(velocity.x, 0.0, velocity.z)
|
||||
@@ -480,6 +382,9 @@ func _activate_roll() -> void:
|
||||
_bull_anim.play(_ANIM_ROLL)
|
||||
_legs.set(&"tail_ragdoll", true) # tail streams out behind
|
||||
_legs.set(&"charge_pitch_target", DP.f("charge_head_pitch")) # horns down, plowing
|
||||
_legs.set(&"external_rotation", true) # roll owns the model's full orientation (tumble)
|
||||
if _roll_spark_emitter:
|
||||
_roll_spark_emitter.emitting = true
|
||||
_spawn_dash_burst()
|
||||
camera_pivot.call(&"trigger_hit", 0.35)
|
||||
if _huff_player:
|
||||
@@ -505,7 +410,14 @@ func _tick_roll(delta: float, steer: Vector3) -> void:
|
||||
velocity.x = _roll_dir.x * speed
|
||||
velocity.z = _roll_dir.z * speed
|
||||
|
||||
cube_guy.rotation.y = lerp_angle(cube_guy.rotation.y, atan2(_roll_dir.x, _roll_dir.z), delta * 12.0)
|
||||
# Tumble the whole model like a Powerball: face the travel direction (yaw) then
|
||||
# spin forward about the horizontal axis perpendicular to it, angular speed = v/r
|
||||
# so it "rolls" without slipping. Legs yield rotation control while active.
|
||||
var radius := maxf(DP.f("roll_spin_radius"), 0.1)
|
||||
_roll_spin = wrapf(_roll_spin + (speed / radius) * delta, 0.0, TAU)
|
||||
var facing := Basis(Vector3.UP, atan2(_roll_dir.x, _roll_dir.z))
|
||||
var tumble_axis := Vector3.UP.cross(_roll_dir).normalized()
|
||||
cube_guy.basis = (Basis(tumble_axis, _roll_spin) * facing).scaled(_cube_scale)
|
||||
if _bull_anim and _bull_anim.has_animation(_ANIM_ROLL):
|
||||
_bull_anim.speed_scale = clampf(speed / maxf(top, 1.0), 0.6, 2.4)
|
||||
|
||||
@@ -547,6 +459,9 @@ func _end_roll() -> void:
|
||||
_roll_ramp = 0.0
|
||||
_legs.set(&"charge_pitch_target", 0.0)
|
||||
_legs.set(&"tail_ragdoll", false)
|
||||
_legs.set(&"external_rotation", false)
|
||||
if _roll_spark_emitter:
|
||||
_roll_spark_emitter.emitting = false
|
||||
velocity.x *= 0.15
|
||||
velocity.z *= 0.15
|
||||
_play_idle()
|
||||
@@ -889,36 +804,6 @@ func _make_slam_ring_mesh() -> ArrayMesh:
|
||||
return mesh
|
||||
|
||||
|
||||
func _apply_thrust(dir: Vector3) -> void:
|
||||
var impulse := DP.f("thrust_impulse")
|
||||
velocity.x += dir.x * impulse
|
||||
velocity.z += dir.z * impulse
|
||||
if is_on_floor():
|
||||
velocity.y += DP.f("thrust_vertical")
|
||||
var flat := Vector2(velocity.x, velocity.z)
|
||||
var spd := flat.length()
|
||||
if spd > DP.f("max_speed"):
|
||||
var s := DP.f("max_speed") / spd
|
||||
velocity.x *= s
|
||||
velocity.z *= s
|
||||
|
||||
|
||||
func _apply_kick(direction: Vector3) -> void:
|
||||
var force := DP.f("kick_impulse") * randf_range(1.0 - DP.f("kick_force_var"), 1.0 + DP.f("kick_force_var"))
|
||||
var spread := deg_to_rad(randf_range(-DP.f("kick_spread"), DP.f("kick_spread")))
|
||||
var kick_dir := direction.rotated(Vector3.UP, spread)
|
||||
velocity.x += kick_dir.x * force
|
||||
velocity.z += kick_dir.z * force
|
||||
if is_on_floor():
|
||||
velocity.y += DP.f("kick_vertical") * randf_range(0.75, 1.25)
|
||||
var flat := Vector2(velocity.x, velocity.z)
|
||||
var spd := flat.length()
|
||||
if spd > DP.f("max_speed"):
|
||||
var s := DP.f("max_speed") / spd
|
||||
velocity.x *= s
|
||||
velocity.z *= s
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# ── Gravity ───────────────────────────────────────────────────────────────
|
||||
if not is_on_floor():
|
||||
@@ -944,12 +829,15 @@ func _physics_process(delta: float) -> void:
|
||||
if _ability_active:
|
||||
_ability_timer -= delta
|
||||
if _ability_timer <= 0.0:
|
||||
_ability_active = false
|
||||
_active_ability = -1
|
||||
_legs.set(&"charge_pitch_target", 0.0)
|
||||
_legs.set(&"tail_ragdoll", false)
|
||||
_slam_pending = false
|
||||
_play_idle()
|
||||
if _active_ability == 3:
|
||||
_end_roll() # tears down roll-specific state (sparks, tumble control)
|
||||
else:
|
||||
_ability_active = false
|
||||
_active_ability = -1
|
||||
_legs.set(&"charge_pitch_target", 0.0)
|
||||
_legs.set(&"tail_ragdoll", false)
|
||||
_slam_pending = false
|
||||
_play_idle()
|
||||
|
||||
# ── Boulder roll ──────────────────────────────────────────────────────────
|
||||
# While rolling, the ability owns movement entirely (its own accel, low friction
|
||||
@@ -958,56 +846,23 @@ func _physics_process(delta: float) -> void:
|
||||
_tick_roll(delta, direction)
|
||||
return
|
||||
|
||||
# ── Mouse button single-thrust actions ───────────────────────────────────────
|
||||
# Left only → thrust NE (forward-right diagonal)
|
||||
# Right only → thrust NW (forward-left diagonal)
|
||||
var left_held := Input.is_action_pressed(&"thrust_left")
|
||||
var right_held := Input.is_action_pressed(&"thrust_right")
|
||||
|
||||
if left_held and right_held:
|
||||
_thrust_left_charge = 0.0
|
||||
_thrust_right_charge = 0.0
|
||||
else:
|
||||
if left_held:
|
||||
_thrust_left_charge = minf(_thrust_left_charge + delta, DP.f("thrust_charge_time"))
|
||||
elif Input.is_action_just_released(&"thrust_left") and _thrust_left_charge > 0.0:
|
||||
var t := _thrust_left_charge / DP.f("thrust_charge_time")
|
||||
var angle := lerpf(deg_to_rad(DP.f("thrust_min_angle")), deg_to_rad(DP.f("thrust_max_angle")), t)
|
||||
var raw := cube_guy.global_transform.basis.z
|
||||
_apply_thrust(Vector3(raw.x, 0.0, raw.z).normalized().rotated(Vector3.UP, -angle))
|
||||
_thrust_left_charge = 0.0
|
||||
|
||||
if right_held:
|
||||
_thrust_right_charge = minf(_thrust_right_charge + delta, DP.f("thrust_charge_time"))
|
||||
elif Input.is_action_just_released(&"thrust_right") and _thrust_right_charge > 0.0:
|
||||
var t := _thrust_right_charge / DP.f("thrust_charge_time")
|
||||
var angle := lerpf(deg_to_rad(DP.f("thrust_min_angle")), deg_to_rad(DP.f("thrust_max_angle")), t)
|
||||
var raw := cube_guy.global_transform.basis.z
|
||||
_apply_thrust(Vector3(raw.x, 0.0, raw.z).normalized().rotated(Vector3.UP, angle))
|
||||
_thrust_right_charge = 0.0
|
||||
|
||||
# Fire spark burst the frame either single-button charge first hits max
|
||||
var max_charge := DP.f("thrust_charge_time")
|
||||
var either_full := _thrust_left_charge >= max_charge or _thrust_right_charge >= max_charge
|
||||
if either_full and not _charge_was_full:
|
||||
_charge_ready_emitter.restart()
|
||||
_charge_was_full = either_full
|
||||
|
||||
# Reset kick timer on landing so the first kick after a bounce is immediate
|
||||
if on_floor and not _was_on_floor and direction:
|
||||
_kick_timer = 0.0
|
||||
_was_on_floor = on_floor
|
||||
|
||||
# ── Periodic kick ─────────────────────────────────────────────────────────
|
||||
_kick_timer = maxf(0.0, _kick_timer - delta)
|
||||
if direction and _kick_timer <= 0.0:
|
||||
_apply_kick(direction)
|
||||
_kick_timer = DP.f("kick_interval")
|
||||
|
||||
# ── Rolling friction (exponential, frame-rate independent) ────────────────
|
||||
var retain := pow(_roll_damping, delta)
|
||||
velocity.x *= retain
|
||||
velocity.z *= retain
|
||||
# ── Ground locomotion — smooth accelerate / friction ──────────────────────
|
||||
# WASD/arrows drive the bull directly: hold a direction to ramp up to max_speed
|
||||
# and steer, release to coast to a stop. Ability bursts (dash) and wall bounces
|
||||
# can exceed max_speed — while a direction is held we steer that momentum without
|
||||
# braking it (cap tracks the current speed); friction only bleeds the excess once
|
||||
# input is released.
|
||||
var flat_vel := Vector3(velocity.x, 0.0, velocity.z)
|
||||
var max_speed := DP.f("max_speed")
|
||||
if direction.length_squared() > 0.01:
|
||||
var cap := maxf(max_speed, flat_vel.length())
|
||||
flat_vel = flat_vel.move_toward(direction * cap, DP.f("move_accel") * delta)
|
||||
else:
|
||||
flat_vel = flat_vel.move_toward(Vector3.ZERO, DP.f("move_friction") * delta)
|
||||
velocity.x = flat_vel.x
|
||||
velocity.z = flat_vel.z
|
||||
|
||||
# ── Visual rotation follows velocity direction ─────────────────────────────
|
||||
var flat_speed := Vector2(velocity.x, velocity.z).length()
|
||||
|
||||
Reference in New Issue
Block a user