Files
2026-07-10 17:26:23 +02:00

93 lines
3.3 KiB
GDScript

class_name Boat
extends FloatingBody
## All values multiplied by mass, handling the same regardless of it
@export var engine_power := 12.0 # forward thrust, m/s² at full throttle
@export var reverse_ratio := 0.4 # reverse is weaker, like a real prop
@export var throttle_response := 1.5 # how fast the engine spools up/down
@export var rudder_strength := 2.5 # turning torque at speed
@export var keel_grip := 3.0 # resistance to sliding sideways
@export var loadout: Dictionary[StringName, WeaponDef] = {} # WeaponSlot Name -> WeaponDef
@export var weapon_groups: Dictionary[int, Array] = {} # group id -> slot names; group 0 is auto-built (all slots), editor value ignored
var desired_throttle := 0.0 # written by controller, move_toward target for actual throttle
var steer_input := 0.0 # written by controllers
var throttle := 0.0 # current throttle state of ship
var _slots: Array[WeaponSlot] = []
var _slots_by_name: Dictionary[StringName, WeaponSlot] = {}
var fire_intents: Dictionary[int, bool] = {} # written by controllers, like steer_input
var aim_point = null # Vector3 or null — written by controllers, like steer_input
func _ready() -> void:
super()
add_to_group(&"boats")
for child in get_children():
if child is WeaponSlot:
_slots.append(child)
_slots_by_name[child.name] = child
var def: WeaponDef = loadout.get(child.name)
if def != null:
child.mount(def)
# Weapon group 0 should always be all weapons, regardless
# TODO: If ever weapon slots would dynamically be added during a scene, change
var all_names: Array = []
for slot in _slots:
all_names.append(slot.name)
weapon_groups[0] = all_names
func _physics_process(delta: float) -> void:
super(delta)
# Propulsion
throttle = move_toward(throttle, desired_throttle, throttle_response * delta)
# Weapons
_process_aim(delta)
_process_fire()
if submerged_ratio <= 0.0:
return # airborne: no thrust, no rudder, no keel
# boat's forward direction, flattened onto the water plane
var forward := -global_basis.z
forward.y = 0.0
forward = forward.normalized()
# --- engine ---
var power := engine_power * (reverse_ratio if throttle < 0.0 else 1.0)
apply_central_force(forward * throttle * power * mass * submerged_ratio)
# --- rudder: only bites when water flows past it ---
var forward_speed := linear_velocity.dot(forward)
var rudder_bite := clampf(forward_speed / 3.0, -1.0, 1.0)
apply_torque(Vector3.UP * steer_input * rudder_strength * rudder_bite * mass * submerged_ratio)
# --- keel: kill sideways sliding ---
var right := global_basis.x
right.y = 0.0
right = right.normalized()
var lateral_speed := linear_velocity.dot(right)
apply_central_force(-right * lateral_speed * keel_grip * mass * submerged_ratio)
func _process_fire() -> void:
for group_id in weapon_groups:
if not fire_intents.get(group_id, false):
continue
for slot_name in weapon_groups[group_id]:
var slot: WeaponSlot = _slots_by_name.get(slot_name)
if slot != null and slot.weapon != null:
slot.weapon.try_fire()
func _process_aim(delta: float) -> void:
for slot in _slots:
if slot.weapon != null:
slot.weapon.aim_at(aim_point, delta)
func aim_ray_exclusions() -> Array[RID]:
var rids: Array[RID] = [get_rid()]
for slot in _slots:
if slot.weapon != null and slot.weapon.hitbox != null:
rids.append(slot.weapon.hitbox.get_rid())
return rids