extends Node2D # Formation slots come from GameState.formation (editable at a command # centre) rather than being fixed here, so a layout the player arranged # survives scene swaps and fleet rebuilds. Ships are always # spawned programmatically (spawn_ships), using however many of these # offsets are needed - both for a fresh start (5 ships) and for restoring # a possibly-smaller surviving fleet after returning from a planet's # surface, so both cases share one code path instead of two that could # drift out of sync. # The fleet's cruise speed. This is the speed of the FLEET as a whole: the # anchor below advances at exactly this rate, and individual ships are # allowed to fly faster than it (Ship.max_speed) so they can close on their # formation slot - which is what makes a pivot while under way look sharp # instead of sluggish. @export var cruise_speed: float = 260.0 # Per-scene neon palette for the fleet. PlanetSurface.tscn overrides these # with darker values, since the lunar surface is bright grey and the default # near-white core would wash out against it. See NeonDraw. @export var ship_glow_color: Color = Color(0.25, 0.85, 1.0) @export var ship_core_color: Color = Color(1.0, 1.0, 1.0) var ship_scene: PackedScene = preload("res://Ship.tscn") var ships: Array = [] var formation_offsets: Array = [] var fleet_target: Vector2 = Vector2.ZERO # Formation centre while under joystick steering, and the heading it is # travelling along. Zero heading means the fleet is not being steered. var anchor: Vector2 = Vector2.ZERO var steer_direction: Vector2 = Vector2.ZERO func spawn_ships(count: int, center: Vector2 = Vector2.ZERO) -> void: for ship in ships: if is_instance_valid(ship): ship.queue_free() ships.clear() formation_offsets.clear() var n: int = clamp(count, 1, GameState.formation.size()) for i in range(n): var offset: Vector2 = GameState.formation[i] var ship: Area2D = ship_scene.instantiate() ship.position = center + offset ship.glow_color = ship_glow_color ship.core_color = ship_core_color add_child(ship) ships.append(ship) formation_offsets.append(offset) if ship.has_signal("destroyed"): ship.destroyed.connect(_on_ship_destroyed.bind(ship)) fleet_target = center func add_ship() -> bool: # Adds one vessel in the next free formation slot (bought from a ship # crafter). Returns false when the wedge is already full. if ships.size() >= GameState.formation.size(): return false var offset: Vector2 = GameState.formation[ships.size()] var ship: Area2D = ship_scene.instantiate() # From the formation origin, not the centroid: with a lopsided layout the # centroid is already displaced by the average offset, so adding the new # ship's own offset to it would drop it well outside the wedge. ship.position = get_formation_origin() + offset ship.glow_color = ship_glow_color ship.core_color = ship_core_color add_child(ship) ships.append(ship) formation_offsets.append(offset) if ship.has_signal("destroyed"): ship.destroyed.connect(_on_ship_destroyed.bind(ship)) ship.set_move_target(fleet_target + offset) return true func apply_formation() -> void: # Re-seats the EXISTING ships into the current GameState.formation. # Without this, editing the layout at a command centre (or pivoting it) # only affected ships built later, which read as the edit doing nothing. for i in range(ships.size()): if i >= GameState.formation.size(): break formation_offsets[i] = GameState.formation[i] _assign_slot_targets() func move_to(world_pos: Vector2) -> void: steer_direction = Vector2.ZERO fleet_target = world_pos anchor = world_pos _assign_slot_targets() func move_in_direction(direction: Vector2) -> void: # Joystick steering. Sets the heading; _process advances the anchor. # # This used to park a target 2000 units ahead and let the ships chase # it. That made formation changes barely register while flying: with # the aim point that far away, a 60-unit formation offset moved a # ship's heading by under two degrees, so pivoting mid-flight looked # like it did almost nothing. Anchoring the formation and having each # ship hold its own slot relative to it fixes that. if direction == Vector2.ZERO: stop() return if steer_direction == Vector2.ZERO: anchor = get_formation_origin() steer_direction = direction.normalized() func _process(delta: float) -> void: if steer_direction == Vector2.ZERO: return # The anchor - the fleet's formation centre - travels at cruise_speed. # Ships may exceed it individually to reach their slots. anchor += steer_direction * cruise_speed * delta fleet_target = anchor _assign_slot_targets() func _assign_slot_targets() -> void: for i in range(ships.size()): ships[i].set_move_target(fleet_target + formation_offsets[i]) # Context, not a command: the ship adds this to its own velocity to # work out how it is really moving. See Ship.fleet_velocity. ships[i].fleet_velocity = steer_direction * cruise_speed func stop() -> void: steer_direction = Vector2.ZERO # Aim each ship at the point where its own braking curve brings it to # rest, so releasing the stick coasts to a halt instead of snapping to # a dead stop (which is what targeting the current position would do, # since that lands inside arrive_radius immediately). fleet_target = get_formation_origin() for ship in ships: var speed: float = ship.velocity.length() if speed <= 0.001: ship.set_move_target(ship.position) continue var braking_distance: float = (speed * speed) / (2.0 * ship.acceleration) ship.set_move_target(ship.position + ship.velocity.normalized() * braking_distance) func get_move_targets() -> Array: var result: Array = [] for ship in ships: result.append(ship.move_target) return result func restore(previous_fleet_target: Vector2, previous_move_targets: Array) -> void: fleet_target = previous_fleet_target for i in range(ships.size()): ships[i].move_target = previous_move_targets[i] func _on_ship_destroyed(ship: Node) -> void: var idx: int = ships.find(ship) if idx != -1: ships.remove_at(idx) formation_offsets.remove_at(idx) func get_centroid() -> Vector2: if ships.is_empty(): return fleet_target var sum: Vector2 = Vector2.ZERO for ship in ships: sum += ship.position return sum / ships.size() func get_formation_origin() -> Vector2: # Where the anchor must be for the ships to be standing in their slots # right now. This is NOT the centroid: ships sit at anchor + offset, so # the centroid is anchor + the AVERAGE offset, and the two only coincide # when the layout happens to be balanced around its origin. # # The default wedge is balanced, which is why using the centroid as the # anchor looked fine - until a layout dragged off to one corner at a # command centre made the average offset large. Seeding the anchor from # the centroid then displaced every slot by that average the instant # steering began, so the fleet lurched towards the formation's own # lopsided side before the anchor caught up and it flew the right way. if ships.is_empty(): return fleet_target var sum: Vector2 = Vector2.ZERO for offset in formation_offsets: sum += offset return get_centroid() - sum / formation_offsets.size()