bouncy bull
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
extends CharacterBody3D
|
||||
|
||||
# Hoof positions in bull_v10 local space — adjust y/z if puffs don't land on the hooves
|
||||
# Hoof positions in bull_v10 local space — dust emitter offsets
|
||||
const HOOF_OFFSETS: Array = [
|
||||
Vector3(-0.30, -0.25, -0.50),
|
||||
Vector3( 0.30, -0.25, -0.50),
|
||||
@@ -11,10 +11,13 @@ const HOOF_OFFSETS: Array = [
|
||||
@onready var camera_pivot: Node3D = $SpringArmPivot
|
||||
@onready var cube_guy: Node3D = $bull_v10
|
||||
|
||||
var current_speed: float = 0.0
|
||||
var facing_angle: float = 0.0
|
||||
|
||||
var _hoof_emitters: Array[CPUParticles3D] = []
|
||||
var _legs: Node
|
||||
var _debug_shapes: Array[MeshInstance3D] = []
|
||||
|
||||
var _kick_timer: float = 0.0
|
||||
var _was_on_floor: bool = false
|
||||
var _pre_slide_vel_y: float = 0.0
|
||||
|
||||
enum _DustState { NONE, WALK, CHARGE }
|
||||
var _dust_state: _DustState = _DustState.NONE
|
||||
@@ -22,7 +25,16 @@ var _dust_state: _DustState = _DustState.NONE
|
||||
|
||||
func _ready() -> void:
|
||||
_setup_hoof_dust()
|
||||
_setup_legs()
|
||||
DP.any_changed.connect(_on_dp_changed)
|
||||
_sync_debug_vis()
|
||||
|
||||
|
||||
func _setup_legs() -> void:
|
||||
_legs = Node.new()
|
||||
_legs.set_script(load("res://bull_legs.gd"))
|
||||
add_child(_legs)
|
||||
_legs.call(&"setup", self, cube_guy)
|
||||
|
||||
|
||||
func _setup_hoof_dust() -> void:
|
||||
@@ -97,8 +109,10 @@ func _set_dust_state(new_state: _DustState) -> void:
|
||||
p.emitting = true
|
||||
|
||||
|
||||
func _on_dp_changed(_key: String, _val: Variant) -> void:
|
||||
# Force re-apply so slider changes take effect without needing a state transition
|
||||
func _on_dp_changed(key: String, _val: Variant) -> void:
|
||||
if key == "show_collisions" or key == "__all__":
|
||||
_sync_debug_vis()
|
||||
|
||||
if _hoof_emitters.is_empty() or _dust_state == _DustState.NONE:
|
||||
return
|
||||
var prev := _dust_state
|
||||
@@ -106,13 +120,79 @@ func _on_dp_changed(_key: String, _val: Variant) -> void:
|
||||
_set_dust_state(prev)
|
||||
|
||||
|
||||
func _sync_debug_vis() -> void:
|
||||
_clear_debug_vis()
|
||||
if not DP.b("show_collisions"):
|
||||
return
|
||||
|
||||
# Material for wireframes
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.no_depth_test = true
|
||||
mat.albedo_color = Color(0, 1, 0, 0.5)
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
|
||||
# Find all collision shapes in self and children
|
||||
_create_debug_meshes(self, mat)
|
||||
|
||||
|
||||
func _create_debug_meshes(node: Node, mat: Material) -> void:
|
||||
if node is CollisionShape3D and node.shape:
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = node.shape.get_debug_mesh()
|
||||
mi.material_override = mat
|
||||
# We add it as child of the collision shape to follow its transform
|
||||
node.add_child(mi)
|
||||
_debug_shapes.append(mi)
|
||||
|
||||
for child in node.get_children():
|
||||
_create_debug_meshes(child, mat)
|
||||
|
||||
|
||||
func _clear_debug_vis() -> void:
|
||||
for mi in _debug_shapes:
|
||||
if is_instance_valid(mi):
|
||||
mi.get_parent().remove_child(mi)
|
||||
mi.queue_free()
|
||||
_debug_shapes.clear()
|
||||
|
||||
|
||||
func _apply_kick(direction: Vector3, charging: bool) -> void:
|
||||
var base := DP.f("kick_impulse") * (DP.f("charge_kick_mult") if charging else 1.0)
|
||||
|
||||
# Random force variance — each kick punches a different amount
|
||||
var force := base * randf_range(1.0 - DP.f("kick_force_var"), 1.0 + DP.f("kick_force_var"))
|
||||
|
||||
# Random angle spread — slight mis-kick like a real ball
|
||||
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
|
||||
|
||||
# Vertical arc — only when grounded so arcs don't chain infinitely in the air
|
||||
if is_on_floor():
|
||||
velocity.y += DP.f("kick_vertical") * randf_range(0.75, 1.25)
|
||||
|
||||
# Cap immediately after kick so charge doesn't overshoot by too much
|
||||
var flat := Vector2(velocity.x, velocity.z)
|
||||
var spd := flat.length()
|
||||
var cap := DP.f("max_speed") * (DP.f("charge_kick_mult") if charging else 1.0)
|
||||
if spd > cap:
|
||||
var s := cap / spd
|
||||
velocity.x *= s
|
||||
velocity.z *= s
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# ── Gravity & jump ────────────────────────────────────────────────────────
|
||||
if not is_on_floor():
|
||||
velocity += get_gravity() * delta
|
||||
|
||||
if Input.is_action_just_pressed("jump") and is_on_floor():
|
||||
if Input.is_action_just_pressed(&"jump") and is_on_floor():
|
||||
velocity.y = DP.f("jump_velocity")
|
||||
|
||||
# ── Camera-relative input direction ───────────────────────────────────────
|
||||
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
||||
var cam_basis := camera_pivot.global_transform.basis
|
||||
var flat_forward := -Vector3(cam_basis.z.x, 0.0, cam_basis.z.z).normalized()
|
||||
@@ -120,36 +200,44 @@ func _physics_process(delta: float) -> void:
|
||||
var direction := (flat_forward * -input_dir.y + flat_right * input_dir.x).normalized()
|
||||
|
||||
var charging := Input.is_action_pressed(&"charge")
|
||||
var on_floor := is_on_floor()
|
||||
|
||||
if direction:
|
||||
var target_angle := atan2(direction.x, direction.z)
|
||||
# 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
|
||||
|
||||
if current_speed < 0.01:
|
||||
facing_angle = target_angle
|
||||
# ── Periodic kick ─────────────────────────────────────────────────────────
|
||||
_kick_timer = maxf(0.0, _kick_timer - delta)
|
||||
if direction and _kick_timer <= 0.0:
|
||||
_apply_kick(direction, charging)
|
||||
_kick_timer = DP.f("kick_interval")
|
||||
|
||||
var speed_t: float = clampf(current_speed / DP.f("walk_speed"), 0.0, 1.0)
|
||||
var turn_rate: float = DP.f("charge_turn") if charging \
|
||||
else lerpf(DP.f("walk_turn_fast"), DP.f("walk_turn_slow"), speed_t)
|
||||
# ── Rolling friction (exponential, frame-rate independent) ────────────────
|
||||
var retain := pow(DP.f("roll_damping"), delta)
|
||||
velocity.x *= retain
|
||||
velocity.z *= retain
|
||||
|
||||
var diff := wrapf(target_angle - facing_angle, -PI, PI)
|
||||
facing_angle += clampf(diff, -turn_rate * delta, turn_rate * delta)
|
||||
|
||||
var top_speed: float = DP.f("charge_speed") if charging else DP.f("walk_speed")
|
||||
var accel: float = DP.f("charge_accel") if charging else DP.f("walk_accel")
|
||||
current_speed = move_toward(current_speed, top_speed, accel * delta)
|
||||
else:
|
||||
var decel: float = DP.f("charge_decel") if charging else DP.f("deceleration")
|
||||
current_speed = move_toward(current_speed, 0.0, decel * delta)
|
||||
|
||||
velocity.x = sin(facing_angle) * current_speed
|
||||
velocity.z = cos(facing_angle) * current_speed
|
||||
|
||||
cube_guy.rotation.y = lerp_angle(cube_guy.rotation.y, facing_angle,
|
||||
delta * DP.f("visual_turn_speed"))
|
||||
# ── Visual rotation follows velocity direction ─────────────────────────────
|
||||
var flat_speed := Vector2(velocity.x, velocity.z).length()
|
||||
if flat_speed > 0.4:
|
||||
var target_angle := atan2(velocity.x, velocity.z)
|
||||
cube_guy.rotation.y = lerp_angle(
|
||||
cube_guy.rotation.y, target_angle, delta * DP.f("visual_turn_speed"))
|
||||
|
||||
_pre_slide_vel_y = velocity.y
|
||||
move_and_slide()
|
||||
|
||||
if current_speed > 0.5 and is_on_floor():
|
||||
_set_dust_state(_DustState.CHARGE if charging else _DustState.WALK)
|
||||
# ── Bounce on landing ─────────────────────────────────────────────────────
|
||||
if is_on_floor() and not _was_on_floor:
|
||||
var impact := -_pre_slide_vel_y
|
||||
if impact > DP.f("bounce_min_impact"):
|
||||
velocity.y = impact * DP.f("bounce_restitution")
|
||||
|
||||
# ── Dust ──────────────────────────────────────────────────────────────────
|
||||
if flat_speed > DP.f("dust_charge_spd") and on_floor:
|
||||
_set_dust_state(_DustState.CHARGE)
|
||||
elif flat_speed > DP.f("dust_walk_spd") and on_floor:
|
||||
_set_dust_state(_DustState.WALK)
|
||||
else:
|
||||
_set_dust_state(_DustState.NONE)
|
||||
|
||||
Reference in New Issue
Block a user