class_name GameBall extends RigidBody3D ## Base class for all gfolf balls ## Fired as soon as this ball enters a water hazard signal entered_water ## Types of game balls enum Type { NONE, BASIC, PLASMA, } const TERRAIN_DAMPING_EPSILON := 1e-6 const MAGNUS_EPSILON := 1e-3 ## Angular damping while in air @export var air_damping := 0.0 ## Angular damping while in collision with rough terrain @export var rough_damping := 8.0 #@export var fluid_density := 1.225 #@export var lift_coefficient := 0.05 #@export var radius := 0.05 ## Coefficient of angular velocity influence on linear velocity ## This is approximately 1/2 * rho * C_L * pi * r^2 ## where `rho` is the fluid density of the medium, or 1.225 for air at sea level, ## and `C_L` is the lift coefficient which for our purposes is 0.05, ## and `r` is the radius of the ball, which is 5cm. @export var magnus_coefficient := 0.00024 ## Causes the ball to stick to surfaces @export var magnetic := false ## Base damage inflicted on impact with a player @export var base_damage := 15.0 ## Scaling factor for additional force-based damage @export var damage_force_scale := 0.01 var _last_contact_normal: Vector3 = Vector3.UP var _position_on_last_wake: Vector3 var _awake := false var _zones: Array[BallZone] = [] @onready var manual_sleep_timer: Timer = %ManualSleepTimer @onready var sfx: BallSFX = %SFX @onready var effects: BallParticleEffects = %ParticleEffects @onready var normal_physics: PhysicsMaterial = preload( "res://src/equipment/balls/physics_ball/normal_physics.tres" ) @onready var _debug_draw: Control = %DebugDraw ## Should this ball stick to surfaces, rather than bounce? func is_sticky() -> bool: return magnetic ## Called by a water area when this ball enters it func enter_water() -> void: entered_water.emit() func get_damage() -> float: print("velocity: ", linear_velocity.length()) return base_damage + linear_velocity.length_squared() * damage_force_scale func _total_terrain_angular_damping() -> float: return _zones.reduce( func(a: float, b: BallZone) -> float: return a + b.terrain_angular_damping, 0.0 ) func _magnus_force() -> Vector3: return magnus_coefficient * angular_velocity.cross(linear_velocity) func _integrate_forces(state: PhysicsDirectBodyState3D) -> void: if not _awake: # Triggered on first frame after waking _awake = true _position_on_last_wake = global_position _last_contact_normal = Vector3.UP # TODO something's fucky here... I think this gets called once after the ball sleeps var damping := air_damping if state.get_contact_count(): constant_force = Vector3.ZERO # We want the contact normal which minimizes the angle to the up vector var min_dot := -1.0 for i: int in range(state.get_contact_count()): var norm := state.get_contact_local_normal(i) var dot := norm.dot(Vector3.UP) if dot > min_dot: min_dot = dot _last_contact_normal = norm damping = _total_terrain_angular_damping() if damping <= TERRAIN_DAMPING_EPSILON: damping = rough_damping angular_damp = damping func _physics_process(_delta: float) -> void: # Simulate magnus effect var magnus := _magnus_force() if magnus.length_squared() > MAGNUS_EPSILON: apply_central_force(magnus) func enter_zone(zone: BallZone) -> void: _zones.push_back(zone) if zone.water_hazard: entered_water.emit() func exit_zone(zone: BallZone) -> void: _zones.erase(zone) func get_reoriented_basis() -> Basis: var up := _last_contact_normal.normalized() var forward := (_position_on_last_wake - global_position).normalized() var right := up.cross(forward).normalized() forward = right.cross(up) # orthonormalize return Basis(right, up, forward) func _on_sleeping_state_changed() -> void: print("SLEEPING STATE: ", sleeping) if sleeping: # Trigger to reassign on wake _awake = false func _on_collision(body: Node) -> void: if is_sticky(): # Freeze physics as soon as we hit something freeze = true manual_sleep_timer.start() var terrain: Terrain.Type if body is Terrain3D: terrain = Terrain.at_position(global_position, body as Terrain3D) elif body is CSGShape3D: terrain = Terrain.from_physical_layer((body as CSGShape3D).collision_layer) elif body is CollisionObject3D: terrain = Terrain.from_physical_layer((body as CollisionObject3D).collision_layer) else: print_debug("COLLIDER: ", body) if terrain: sfx.play_sfx(terrain) effects.play_effect(terrain) func _fire_sleep_signal() -> void: sleeping = true sleeping_state_changed.emit()