Files
Bullosseum/player.gd
T

230 lines
8.2 KiB
GDScript

extends CharacterBody3D
# 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),
Vector3(-0.30, -0.25, 0.60),
Vector3( 0.30, -0.25, 0.60),
]
@onready var camera_pivot: Node3D = $SpringArmPivot
@onready var cube_guy: Node3D = $bull_v10
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
enum _DustState { NONE, WALK, CHARGE }
var _dust_state: _DustState = _DustState.NONE
func _ready() -> void:
add_to_group(&"player")
_setup_hoof_dust()
_setup_legs()
DP.any_changed.connect(_on_dp_changed)
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:
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 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))
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 = DP.f("dust_lifetime")
p.explosiveness = DP.f("dust_explosiveness")
p.randomness = 0.6
p.local_coords = false
p.direction = Vector3(0.0, 1.0, 0.0)
p.spread = DP.f("dust_spread")
p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
p.initial_velocity_min = DP.f("walk_vel_min")
p.initial_velocity_max = DP.f("walk_vel_max")
p.scale_amount_min = DP.f("walk_scale_min")
p.scale_amount_max = DP.f("walk_scale_max")
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:
p.lifetime = DP.f("dust_lifetime")
p.explosiveness = DP.f("dust_explosiveness")
p.spread = DP.f("dust_spread")
p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
match new_state:
_DustState.NONE:
p.emitting = false
_DustState.WALK:
p.scale_amount_min = DP.f("walk_scale_min")
p.scale_amount_max = DP.f("walk_scale_max")
p.initial_velocity_min = DP.f("walk_vel_min")
p.initial_velocity_max = DP.f("walk_vel_max")
p.emitting = true
_DustState.CHARGE:
p.scale_amount_min = DP.f("charge_scale_min")
p.scale_amount_max = DP.f("charge_scale_max")
p.initial_velocity_min = DP.f("charge_vel_min")
p.initial_velocity_max = DP.f("charge_vel_max")
p.emitting = true
func _on_dp_changed(_key: String, _val: Variant) -> void:
if _hoof_emitters.is_empty() or _dust_state == _DustState.NONE:
return
var prev := _dust_state
_dust_state = _DustState.NONE
_set_dust_state(prev)
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, 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():
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()
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()
var charging := Input.is_action_pressed(&"charge")
var on_floor := is_on_floor()
# ── Left / right click thrust (only when mouse is captured) ─────────────
# Left click → thrust NE (forward-right diagonal)
# Right click → thrust NW (forward-left diagonal)
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
if Input.is_action_just_pressed(&"thrust_left"):
var ne := (flat_forward + flat_right).normalized()
_apply_thrust(ne if ne.length() > 0.1 else flat_forward)
if Input.is_action_just_pressed(&"thrust_right"):
var nw := (flat_forward - flat_right).normalized()
_apply_thrust(nw if nw.length() > 0.1 else flat_forward)
# 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, charging)
_kick_timer = DP.f("kick_interval")
# ── Rolling friction (exponential, frame-rate independent) ────────────────
var retain := pow(DP.f("roll_damping"), delta)
velocity.x *= retain
velocity.z *= retain
# ── 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()
# ── 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)