extends Area2D signal destroyed const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") @export var max_speed: float = 460.0 @export var acceleration: float = 450.0 @export var rotation_speed: float = 8.0 @export var arrive_radius: float = 6.0 @export var vessel_type: String = "interceptor" var definition: Resource var glow_color: Color = Color(0.25, 0.85, 1.0) var core_color: Color = Color.WHITE var velocity: Vector2 = Vector2.ZERO var fleet_velocity: Vector2 = Vector2.ZERO var move_target: Vector2 # Compatibility accessors keep old saves/tests readable while shield state # itself belongs exclusively to the optional ShieldComponent. var shield_charges: int: get: var component: Node = get_component("shield") return component.charges if component != null else 0 set(value): var component: Node = get_component("shield") if component != null: component.set_charges(value) var shield_recharge_left: float: get: var component: Node = get_component("shield") return component.recharge_left if component != null else 0.0 set(value): var component: Node = get_component("shield") if component != null: component.recharge_left = value var projectile_damage: int: get: var component: Node = get_component("weapon") return component.projectile_damage if component != null else 0 func _ready() -> void: move_target = position add_to_group("player_ships") area_entered.connect(_on_area_entered) definition = VESSEL_DEFINITIONS.get_definition(vessel_type) if definition == null: return glow_color = definition.glow_color for component_scene in definition.component_scenes: var component: Node = component_scene.instantiate() add_child(component) if component.has_method("setup"): component.setup() apply_vessel_stats() queue_redraw() func get_component(capability: String) -> Node: for child in get_children(): if child.get("capability_id") == capability: return child return null func has_capability(capability: String) -> bool: return get_component(capability) != null func set_move_target(world_pos: Vector2) -> void: move_target = world_pos func apply_vessel_stats() -> void: if definition == null: definition = VESSEL_DEFINITIONS.get_definition(vessel_type) if definition == null: return max_speed = GameState.vessel_stat(vessel_type, "max_speed") acceleration = GameState.vessel_stat(vessel_type, "acceleration") for child in get_children(): if child.has_method("apply_stats"): child.apply_stats() func has_shield() -> bool: var component: Node = get_component("shield") return component != null and component.is_active() func _physics_process(delta: float) -> void: for child in get_children(): if child.has_method("tick"): child.tick(delta) var to_target: Vector2 = move_target - position var distance: float = to_target.length() if distance <= arrive_radius: position = move_target velocity = Vector2.ZERO else: var braking_speed: float = sqrt(2.0 * acceleration * distance) var desired_speed: float = min(max_speed, braking_speed) var desired_velocity: Vector2 = to_target.normalized() * desired_speed velocity = velocity.move_toward(desired_velocity, acceleration * delta) position += velocity * delta var motion: Vector2 = velocity + fleet_velocity if motion.length() > 8.0: var target_angle: float = motion.angle() + PI / 2.0 rotation = lerp_angle(rotation, target_angle, rotation_speed * delta) func take_damage(_amount: int) -> bool: # Ships have no hit points - any unabsorbed hit is lethal. A shield # charge absorbs a hit entirely regardless of its amount, matching # VesselShieldComponent's existing all-or-nothing semantics; `_amount` # is accepted (not `int`-clamped away) so callers like EnemyProjectile # have a normal damage parameter to pass even though it isn't graded here. for child in get_children(): if child.has_method("absorb_hit") and child.absorb_hit(): return true ShipExplosionVFX.spawn(get_tree().root, position) destroyed.emit() queue_free() return false func _on_area_entered(area: Area2D) -> void: if not (area.is_in_group("enemies") or area.is_in_group("asteroids")): return var absorbed: bool = take_damage(1) if absorbed and area.is_in_group("asteroids") and is_instance_valid(area): area.queue_free() func _draw() -> void: var points: PackedVector2Array = definition.hull_points if definition != null else PackedVector2Array([ Vector2(0, -22), Vector2(-14, 16), Vector2(14, 16), ]) NeonDraw.draw_glowing_polygon(self, points, glow_color, 1.0, core_color)