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