extends Node2D const PROJECTILE := preload("res://scenes/game/weapon_projectile.tscn") const FIRE_INTERVAL := 0.25 const HULL_TEXTURE := preload("res://assets/Ships/combat_drone.png") var mother: CharacterBody2D var formation_index := 0 var damage_per_shot := 1.0 var remaining := 10.0 var _fire_timer := 0.0 var _elapsed := 0.0 var _flash := 0.0 var velocity := Vector2.ZERO var _turn_velocity := 0.0 const FORMATION := [Vector2(0, -110), Vector2(-35, -78), Vector2(35, -78)] func _ready() -> void: add_to_group("player_combat_drones") z_index = 4 scale = Vector2.ONE * 0.5 rotation = mother.rotation velocity = mother.velocity texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS _fire_timer = formation_index * FIRE_INTERVAL / 3.0 queue_redraw() func _physics_process(delta: float) -> void: remaining -= delta if remaining <= 0.0 or not is_instance_valid(mother) or bool(mother.get("_is_destroyed")) or bool(mother.get("is_docked")): queue_free() return _elapsed += delta _flash = maxf(0.0, _flash - delta) _fire_timer = maxf(0.0, _fire_timer - delta) var target := _find_target() var destination := formation_position() var escort_velocity := mother.velocity if target != null: var anchor := mother.global_position.lerp(target.global_position, 0.45) destination = anchor + Vector2.from_angle(TAU * formation_index / 3.0 + _elapsed * 0.7) * 85.0 escort_velocity *= 0.55 else: # Rotating formation slots have tangential velocity as the mother turns. var offset := destination - mother.global_position escort_velocity += Vector2(-offset.y, offset.x) * float(mother.get("angular_velocity")) var error := destination - global_position var correction := (error * 3.0).limit_length(260.0) var desired_velocity := (escort_velocity + correction).limit_length(maxf(360.0, mother.velocity.length() + 200.0)) velocity = velocity.move_toward(desired_velocity, maxf(650.0, mother.velocity.length()) * delta) global_position += velocity * delta var desired_rotation := mother.rotation if target != null: var aim := target.global_position - global_position desired_rotation = aim.angle() + PI * 0.5 if _fire_timer <= 0.0 and aim.length() <= 650.0 and _clear_shot(target): _fire(target) _fire_timer = FIRE_INTERVAL elif error.length() > 55.0 and velocity.length() > 30.0: desired_rotation = velocity.angle() + PI * 0.5 var angle_error := angle_difference(rotation, desired_rotation) _turn_velocity = move_toward(_turn_velocity, clampf(angle_error * 6.0, -4.5, 4.5), 14.0 * delta) var turn := _turn_velocity * delta if absf(turn) >= absf(angle_error) and signf(turn) == signf(angle_error): rotation = desired_rotation _turn_velocity = 0.0 else: rotation += turn queue_redraw() func formation_position() -> Vector2: return mother.global_position + FORMATION[clampi(formation_index, 0, 2)].rotated(mother.rotation) func _find_target() -> Node2D: var closest: Node2D var distance := 800.0 for enemy in get_tree().get_nodes_in_group("enemies"): if enemy.is_queued_for_deletion() or float(enemy.get("hull")) <= 0.0: continue var candidate := global_position.distance_to(enemy.global_position) if candidate < distance and mother.global_position.distance_to(enemy.global_position) < 1000.0: distance = candidate closest = enemy return closest func _clear_shot(target: Node2D) -> bool: var query := PhysicsRayQueryParameters2D.create(global_position, target.global_position, 13) var hit := get_world_2d().direct_space_state.intersect_ray(query) return hit.is_empty() or hit.get("collider") == target func _fire(target: Node2D) -> void: var direction := (target.global_position - global_position).normalized() var bullet := PROJECTILE.instantiate() var damage := damage_per_shot * (1.0 + GameState.captain.get_skill_bonus("drone_commander")) bullet.call("configure", global_position + direction * 9.0, direction, ItemDefinition.WeaponKind.MACHINE_GUN, 1100.0, damage, 650.0) get_parent().add_child(bullet) _flash = 0.06 mother.get_node("ExtensionSystems").call("break_cloak_on_fire") func _draw() -> void: modulate.a = minf(clampf(_elapsed / 0.2, 0.0, 1.0), clampf(remaining / 0.6, 0.0, 1.0)) var flicker := sin(_elapsed * 33.0 + formation_index * 2.1) for x in [-5.5, 5.5]: var exhaust := Vector2(x, 19.5) draw_circle(exhaust + Vector2(0, 5), 11.0 + flicker, Color(0.08, 0.65, 1.0, 0.08)) draw_circle(exhaust + Vector2(0, 3), 6.0, Color(0.12, 0.8, 1.0, 0.18)) draw_colored_polygon(PackedVector2Array([exhaust + Vector2(-3, 0), exhaust + Vector2(0, 15 + flicker * 2), exhaust + Vector2(3, 0)]), Color(0.15, 0.72, 1.0, 0.65)) draw_colored_polygon(PackedVector2Array([exhaust + Vector2(-1.4, 0), exhaust + Vector2(0, 8 + flicker), exhaust + Vector2(1.4, 0)]), Color("c8fbff")) var hull_size := HULL_TEXTURE.get_size() * (60.0 / HULL_TEXTURE.get_height()) var hull_rect := Rect2(-hull_size * 0.5, hull_size) draw_texture_rect(HULL_TEXTURE, Rect2(hull_rect.position + Vector2(3, 5), hull_size), false, Color(0, 0, 0, 0.4)) draw_texture_rect(HULL_TEXTURE, hull_rect, false) if _flash > 0.0: for x in [-5.5, 5.5]: var muzzle := Vector2(x, -20) draw_circle(muzzle, 8.0, Color(1.0, 0.6, 0.18, 0.15)) draw_colored_polygon(PackedVector2Array([muzzle + Vector2(-3, 0), muzzle + Vector2(-1.5, -4), muzzle + Vector2(0, -12), muzzle + Vector2(1.5, -4), muzzle + Vector2(3, 0)]), Color("ffbd68")) draw_line(muzzle, muzzle + Vector2(0, -7), Color("fff2cb"), 2.0, true)