extends CharacterBody3D const WALK_SPEED = 6.0 const CHARGE_SPEED = 180.0 const WALK_ACCEL = 14.0 const CHARGE_ACCEL = 40.0 const DECELERATION = 12.0 const CHARGE_DECEL = 3.5 const WALK_TURN_FAST = 8.0 const WALK_TURN_SLOW = 3.5 const CHARGE_TURN = 0.55 const JUMP_VELOCITY = 4.5 # Hoof positions in bull_v10 local space — tweak y/z if hooves don't line up with the model const HOOF_OFFSETS: Array = [ Vector3(-0.30, -0.25, -0.50), Vector3( 0.30, -0.25, -0.50), Vector3(-0.30, -0.25, 0.60), Vector3( 0.30, -0.25, 0.60), ] @export var visual_turn_speed: float = 12.0 @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] = [] enum _DustState { NONE, WALK, CHARGE } var _dust_state: _DustState = _DustState.NONE func _ready() -> void: _setup_hoof_dust() 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 # 4×2 sphere = chunky low-poly diamond shape var sphere := SphereMesh.new() sphere.radius = 0.10 sphere.height = 0.20 sphere.radial_segments = 4 sphere.rings = 2 sphere.material = mat # sand colour, fades to transparent over lifetime var ramp := Gradient.new() ramp.set_color(0, Color(0.80, 0.69, 0.46, 1.0)) ramp.set_color(1, Color(0.80, 0.69, 0.46, 0.0)) # puff blooms up then vanishes var scale_curve := Curve.new() scale_curve.add_point(Vector2(0.0, 0.2)) scale_curve.add_point(Vector2(0.4, 1.0)) scale_curve.add_point(Vector2(1.0, 0.0)) for offset: Vector3 in HOOF_OFFSETS: var p := CPUParticles3D.new() p.position = offset p.emitting = false p.amount = 12 p.lifetime = 0.9 p.explosiveness = 0.35 p.randomness = 0.6 p.local_coords = false p.direction = Vector3(0.0, 1.0, 0.0) p.spread = 45.0 p.gravity = Vector3(0.0, -1.5, 0.0) p.initial_velocity_min = 0.8 p.initial_velocity_max = 1.8 p.scale_amount_min = 0.4 p.scale_amount_max = 0.7 p.mesh = sphere p.color_ramp = ramp p.scale_amount_curve = scale_curve cube_guy.add_child(p) _hoof_emitters.append(p) func _set_dust_state(new_state: _DustState) -> void: if new_state == _dust_state: return _dust_state = new_state for p: CPUParticles3D in _hoof_emitters: match new_state: _DustState.NONE: p.emitting = false _DustState.WALK: p.scale_amount_min = 0.35 p.scale_amount_max = 0.65 p.initial_velocity_min = 0.8 p.initial_velocity_max = 1.8 p.emitting = true _DustState.CHARGE: p.scale_amount_min = 1.0 p.scale_amount_max = 2.2 p.initial_velocity_min = 2.8 p.initial_velocity_max = 5.5 p.emitting = true func _physics_process(delta: float) -> void: if not is_on_floor(): velocity += get_gravity() * delta if Input.is_action_just_pressed("jump") and is_on_floor(): velocity.y = JUMP_VELOCITY 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() var flat_right := Vector3(cam_basis.x.x, 0.0, cam_basis.x.z).normalized() var direction := (flat_forward * -input_dir.y + flat_right * input_dir.x).normalized() if direction: var target_angle := atan2(direction.x, direction.z) if current_speed < 0.01: facing_angle = target_angle var charging := Input.is_key_pressed(KEY_SHIFT) var speed_t: float = clampf(current_speed / WALK_SPEED, 0.0, 1.0) var turn_rate: float = CHARGE_TURN if charging else lerpf(WALK_TURN_FAST, WALK_TURN_SLOW, speed_t) var diff := wrapf(target_angle - facing_angle, -PI, PI) facing_angle += clampf(diff, -turn_rate * delta, turn_rate * delta) var top_speed: float = CHARGE_SPEED if charging else WALK_SPEED var accel: float = CHARGE_ACCEL if charging else WALK_ACCEL current_speed = move_toward(current_speed, top_speed, accel * delta) else: var charging := Input.is_key_pressed(KEY_SHIFT) var decel: float = CHARGE_DECEL if charging else 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 * visual_turn_speed) move_and_slide() var charging := Input.is_key_pressed(KEY_SHIFT) if current_speed > 0.5 and is_on_floor(): _set_dust_state(_DustState.CHARGE if charging else _DustState.WALK) else: _set_dust_state(_DustState.NONE)