From 116e0f97d38db9406a9b7ab4b19b681af25605e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Wisniewski?= Date: Sun, 16 Aug 2026 17:59:04 +0200 Subject: [PATCH] Add space-time distorder vessel and fleet speed fader (#59) GameState.fleet_speed_multiplier (1.0-3.0) is applied at the point of use - Fleet's cruise anchor and Ship's own max_speed/acceleration - rather than baked into stored stats, so a stat upgrade researched mid-boost can't corrupt the base value and the fader takes effect immediately with no re-apply step. Ship.arrive_radius is scaled by the same multiplier: at higher speeds each physics tick covers a proportionally larger step, and once that step outgrows a fixed arrive_radius the ship can overshoot its snap-to-target zone every tick while chasing a target that's now also receding faster - visible as the fleet stretching apart under boost. Scaling it preserves the same "snaps onto its slot" tightness at any boost level that 1.0x already has. SpeedFader.gd is a self-contained vertical drag control (TouchJoystick's raw-touch idiom, not _gui_input) that writes straight to the autoload rather than reporting a value for something else to apply. It also resets the multiplier to 1.0 the instant fleet.has_capability("distorder") goes false, so a fleet can never be left permanently boosted by a vessel it no longer has. VesselDistorderComponent carries no per-shot behaviour (energy drain while boosted is #62) - its presence is what matters. Registered in VesselDefinitions, wired into the crafter/hangar/ship-lab menus per #57's note (5/5/6 -> 6/6/7 columns). --- BuildController.gd | 12 +- DistorderVesselDefinition.tres | 24 +++ Fleet.gd | 13 +- GameController.gd | 5 + GameState.gd | 13 ++ IconButton.gd | 16 ++ Main.tscn | 268 +++++++++++++++++++++-------- PlanetSurface.tscn | 27 ++- Ship.gd | 21 ++- SpeedFader.gd | 112 ++++++++++++ VesselDefinitions.gd | 3 +- VesselDistorderComponent.gd | 9 + VesselDistorderComponent.tscn | 6 + tests/test_space_time_distorder.gd | 232 +++++++++++++++++++++++++ 14 files changed, 675 insertions(+), 86 deletions(-) create mode 100644 DistorderVesselDefinition.tres create mode 100644 SpeedFader.gd create mode 100644 VesselDistorderComponent.gd create mode 100644 VesselDistorderComponent.tscn create mode 100644 tests/test_space_time_distorder.gd diff --git a/BuildController.gd b/BuildController.gd index 02de479..c8ed446 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -32,12 +32,14 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") get_node("../UI/LabMenu/VesselA"), get_node("../UI/LabMenu/VesselB"), get_node("../UI/LabMenu/VesselC"), get_node("../UI/LabMenu/VesselD"), get_node("../UI/LabMenu/VesselE"), get_node("../UI/LabMenu/VesselF"), + get_node("../UI/LabMenu/VesselG"), ] @onready var lab_vessel_label: Label = get_node("../UI/LabMenu/VesselLabel") @onready var lab_vessel_labels: Array = [ get_node("../UI/LabMenu/VesselALabel"), get_node("../UI/LabMenu/VesselBLabel"), get_node("../UI/LabMenu/VesselCLabel"), get_node("../UI/LabMenu/VesselDLabel"), get_node("../UI/LabMenu/VesselELabel"), get_node("../UI/LabMenu/VesselFLabel"), + get_node("../UI/LabMenu/VesselGLabel"), ] @onready var lab_detail: Label = get_node("../UI/LabMenu/DetailLabel") @onready var lab_status: Label = get_node("../UI/LabMenu/StatusLabel") @@ -67,6 +69,7 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") get_node("../UI/HangarMenu/AvailableGasBomber"), get_node("../UI/HangarMenu/AvailableCarrier"), get_node("../UI/HangarMenu/AvailableLaser"), + get_node("../UI/HangarMenu/AvailableDistorder"), ] @onready var hangar_available_labels: Array = [ get_node("../UI/HangarMenu/AvailableInterceptorLabel"), @@ -74,6 +77,7 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") get_node("../UI/HangarMenu/AvailableGasBomberLabel"), get_node("../UI/HangarMenu/AvailableCarrierLabel"), get_node("../UI/HangarMenu/AvailableLaserLabel"), + get_node("../UI/HangarMenu/AvailableDistorderLabel"), ] @onready var hangar_fleet_slots: Array = [ get_node("../UI/HangarMenu/FleetInterceptor"), @@ -81,6 +85,7 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") get_node("../UI/HangarMenu/FleetGasBomber"), get_node("../UI/HangarMenu/FleetCarrier"), get_node("../UI/HangarMenu/FleetLaser"), + get_node("../UI/HangarMenu/FleetDistorder"), ] @onready var hangar_fleet_labels: Array = [ get_node("../UI/HangarMenu/FleetInterceptorLabel"), @@ -88,6 +93,7 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") get_node("../UI/HangarMenu/FleetGasBomberLabel"), get_node("../UI/HangarMenu/FleetCarrierLabel"), get_node("../UI/HangarMenu/FleetLaserLabel"), + get_node("../UI/HangarMenu/FleetDistorderLabel"), ] var build_menu_entries: Array = [] @@ -117,13 +123,15 @@ var lab_building_id: int = 0 @onready var craft_carrier_label: Label = get_node("../UI/CraftMenu/CarrierLabel") @onready var craft_laser_button: IconButton = get_node("../UI/CraftMenu/LaserButton") @onready var craft_laser_label: Label = get_node("../UI/CraftMenu/LaserLabel") +@onready var craft_distorder_button: IconButton = get_node("../UI/CraftMenu/DistorderButton") +@onready var craft_distorder_label: Label = get_node("../UI/CraftMenu/DistorderLabel") @onready var craft_vessel_buttons: Array = [ craft_interceptor_button, craft_shield_button, craft_gas_bomber_button, craft_carrier_button, - craft_laser_button, + craft_laser_button, craft_distorder_button, ] @onready var craft_vessel_labels: Array = [ craft_interceptor_label, craft_shield_label, craft_gas_bomber_label, craft_carrier_label, - craft_laser_label, + craft_laser_label, craft_distorder_label, ] # --- the transfer panel (#24) --------------------------------------------- diff --git a/DistorderVesselDefinition.tres b/DistorderVesselDefinition.tres new file mode 100644 index 0000000..9e2a0de --- /dev/null +++ b/DistorderVesselDefinition.tres @@ -0,0 +1,24 @@ +[gd_resource type="Resource" script_class="VesselDefinition" load_steps=3 format=3] +[ext_resource type="Script" path="res://VesselDefinition.gd" id="1"] +[ext_resource type="PackedScene" path="res://VesselDistorderComponent.tscn" id="2"] +[resource] +script = ExtResource("1") +definition_id = "distorder" +display_name = "Distorder" +description = "Unarmed space-time distortion hull. While one flies with the fleet, a fader lets you push cruise speed beyond its default." +build_cost = 14 +build_resource_costs = {"ore": 14, "energy": 8} +build_time = 16.0 +icon = 37 +glow_color = Color(0.7, 0.3, 1, 1) +hull_points = PackedVector2Array(0, -24, -14, -2, -6, 18, 10, 10, 12, -10) +fleet_eligible = true +capabilities = PackedStringArray("distorder") +component_scenes = Array[PackedScene]([ExtResource("2")]) +base_stats = {"max_speed": 430.0, "acceleration": 410.0, "delivery_speed": 180.0} +unlock_id = "unlock_distorder" +unlock_name = "Unlock Distorder" +unlock_description = "Research a space-time distortion hull - while one flies with the fleet, a fader lets you push cruise speed beyond its default." +unlock_cost = 0 +unlock_resource_costs = {"energy": 50} +unlock_icon = -1 diff --git a/Fleet.gd b/Fleet.gd index 78e4627..d51740f 100644 --- a/Fleet.gd +++ b/Fleet.gd @@ -200,18 +200,25 @@ 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 + # Ships may exceed it individually to reach their slots. #59: multiplied + # at the point of use, not baked into cruise_speed itself, so the base + # value stays stable regardless of the current boost. + anchor += steer_direction * _effective_cruise_speed() * delta fleet_target = anchor _assign_slot_targets() +func _effective_cruise_speed() -> float: + return cruise_speed * GameState.fleet_speed_multiplier + + func _assign_slot_targets() -> void: + var effective_cruise_speed: float = _effective_cruise_speed() 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 + ships[i].fleet_velocity = steer_direction * effective_cruise_speed func stop() -> void: diff --git a/GameController.gd b/GameController.gd index e3cb346..957914b 100644 --- a/GameController.gd +++ b/GameController.gd @@ -28,6 +28,7 @@ extends Node2D # indirectly - e.g. Main.tscn loaded from inside a _screenshot_tool.gd # preview - for a class_name this new. Node + duck-typed calls sidesteps it. @onready var laser_controller: Node = get_node_or_null("LaserController") +@onready var speed_fader: Node = get_node_or_null("UI/SpeedFader") var touches: Dictionary = {} var pinch_active: bool = false @@ -263,6 +264,8 @@ func _handle_touch(event: InputEventScreenTouch) -> void: return if laser_controller != null and laser_controller.handle_touch(event): return + if speed_fader != null and speed_fader.handle_touch(event): + return if _handle_pivot_touch(event): return if joystick != null and joystick.visible and joystick.handle_touch(event): @@ -287,6 +290,8 @@ func _handle_drag(event: InputEventScreenDrag) -> void: return if laser_controller != null and laser_controller.handle_drag(event): return + if speed_fader != null and speed_fader.handle_drag(event): + return if joystick != null and joystick.visible and joystick.handle_drag(event): return if not touches.has(event.index): diff --git a/GameState.gd b/GameState.gd index ced08da..b608490 100644 --- a/GameState.gd +++ b/GameState.gd @@ -22,6 +22,17 @@ var planet_name: String = "" var planet_id: String = "" var planet_definition_id: String = "rocky" +# #59: 1.0 is today's behaviour. Applied at the point of use (Fleet's cruise +# anchor, Ship's own max_speed/acceleration) rather than baked into stored +# stats, so a speed upgrade researched while boosted can't corrupt the base +# value - see Fleet.gd/Ship.gd for the multiply sites. +const MAX_FLEET_SPEED_MULTIPLIER: float = 3.0 +var fleet_speed_multiplier: float = 1.0 + + +func set_fleet_speed_multiplier(value: float) -> void: + fleet_speed_multiplier = clampf(value, 1.0, MAX_FLEET_SPEED_MULTIPLIER) + # Explored-chunk history for the world map's fog-of-war reveal. Lives # here (not on ExplorationTracker, which gets destroyed and recreated on # every scene swap) so exploration progress isn't lost every time the @@ -381,6 +392,7 @@ func reset_new_game() -> void: planet_name = "" planet_id = "" planet_definition_id = "rocky" + fleet_speed_multiplier = 1.0 explored_chunks.clear() bank_inventory = INVENTORY.new({RESOURCE_ORE: 500}) fleet_inventory = INVENTORY.new() @@ -465,6 +477,7 @@ const VESSEL_GAS_BOMBER: String = "gas_bomber" const VESSEL_TRANSPORTER: String = "transporter" const VESSEL_CARRIER: String = "carrier" const VESSEL_LASER: String = "laser" +const VESSEL_DISTORDER: String = "distorder" # Compatibility constants for callers and old tests. Runtime metadata is # resolved from VesselDefinition below. diff --git a/IconButton.gd b/IconButton.gd index 62878a0..5ba2701 100644 --- a/IconButton.gd +++ b/IconButton.gd @@ -41,6 +41,8 @@ enum Icon { # #58: LASER_VESSEL is the ship glyph, LASER_SHOT the action button - # same two-icon split as GAS_BOMBER/GAS_BOMB above. LASER_VESSEL, LASER_SHOT, + # #59: a warped ring reads as a space-time distortion rather than a hull. + DISTORDER_VESSEL, } # Size the glyphs were originally drawn against. Every glyph dimension is @@ -239,6 +241,8 @@ func _draw() -> void: _draw_laser_vessel(center, pulse, s) Icon.LASER_SHOT: _draw_laser_shot(center, pulse, s) + Icon.DISTORDER_VESSEL: + _draw_distorder_vessel(center, pulse, s) func _draw_close(center: Vector2, pulse: float, s: float) -> void: @@ -277,6 +281,18 @@ func _draw_carrier(center: Vector2, pulse: float, s: float) -> void: NeonDraw.draw_glowing_polygon(self, hull, _active_color(), pulse, core_color) +func _draw_distorder_vessel(center: Vector2, pulse: float, s: float) -> void: + # A ring whose radius wobbles with angle instead of a plain circle - + # reads as warped space rather than a hull silhouette. + var points: PackedVector2Array = PackedVector2Array() + for i in range(17): + var a: float = TAU * float(i) / 16.0 + var r: float = (10.0 + 3.0 * sin(a * 3.0)) * s + points.append(center + Vector2(cos(a), sin(a)) * r) + NeonDraw.draw_glowing_path(self, points, _active_color(), pulse, 0.8, core_color) + NeonDraw.draw_glowing_circle(self, center, 2.5 * s, _active_color(), pulse, core_color) + + func _draw_laser_vessel(center: Vector2, pulse: float, s: float) -> void: # A slim dart rather than the interceptor's plain triangle - the notch at # the nose reads as a fixed forward-facing emitter, not a hull point. diff --git a/Main.tscn b/Main.tscn index 7eaf569..125ce54 100644 --- a/Main.tscn +++ b/Main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=28 format=3] +[gd_scene load_steps=29 format=3] [ext_resource type="Script" path="res://Starfield.gd" id="1"] [ext_resource type="Script" path="res://Fleet.gd" id="2"] @@ -26,6 +26,7 @@ [ext_resource type="Script" path="res://ResourceTransferList.gd" id="27"] [ext_resource type="Script" path="res://GasBombController.gd" id="29"] [ext_resource type="Script" path="res://LaserController.gd" id="30"] +[ext_resource type="Script" path="res://SpeedFader.gd" id="31"] [sub_resource type="ShaderMaterial" id="1"] shader = ExtResource("5") @@ -221,6 +222,28 @@ offset_top = -424 offset_bottom = -24 script = ExtResource("13") +[node name="SpeedFader" type="Control" parent="UI"] +visible = false +mouse_filter = 2 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -94 +offset_right = -24 +offset_top = 740 +offset_bottom = 920 +script = ExtResource("31") + +[node name="Label" type="Label" parent="UI/SpeedFader"] +mouse_filter = 2 +offset_left = -20 +offset_right = 90 +offset_top = -32 +offset_bottom = -4 +theme_override_font_sizes/font_size = 20 +theme_override_colors/font_color = Color(0.7, 0.3, 1, 1) +horizontal_alignment = 1 +text = "x1.0" + [node name="LaserButton" type="Control" parent="UI"] visible = false mouse_filter = 2 @@ -839,7 +862,7 @@ horizontal_alignment = 1 anchor_left = 0.5 anchor_right = 0.5 offset_left = -290 -offset_right = -198 +offset_right = -215 offset_top = 255 offset_bottom = 375 script = ExtResource("10") @@ -850,7 +873,7 @@ mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 offset_left = -330 -offset_right = -158 +offset_right = -175 offset_top = 380 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -859,8 +882,8 @@ horizontal_alignment = 1 [node name="AvailableShield" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = -168 -offset_right = -76 +offset_left = -189 +offset_right = -114 offset_top = 255 offset_bottom = 375 script = ExtResource("10") @@ -871,8 +894,8 @@ glow_color = Color(0.45, 0.8, 1, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -208 -offset_right = -36 +offset_left = -229 +offset_right = -74 offset_top = 380 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -881,8 +904,8 @@ horizontal_alignment = 1 [node name="AvailableGasBomber" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = -46 -offset_right = 46 +offset_left = -88 +offset_right = -13 offset_top = 255 offset_bottom = 375 script = ExtResource("10") @@ -893,8 +916,8 @@ glow_color = Color(0.96, 0.32, 0.78, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -86 -offset_right = 86 +offset_left = -128 +offset_right = 27 offset_top = 380 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -903,8 +926,8 @@ horizontal_alignment = 1 [node name="AvailableCarrier" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = 76 -offset_right = 168 +offset_left = 13 +offset_right = 88 offset_top = 255 offset_bottom = 375 script = ExtResource("10") @@ -915,8 +938,8 @@ glow_color = Color(1, 0.78, 0.3, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = 36 -offset_right = 208 +offset_left = -27 +offset_right = 128 offset_top = 380 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -925,8 +948,8 @@ horizontal_alignment = 1 [node name="AvailableLaser" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = 198 -offset_right = 290 +offset_left = 114 +offset_right = 189 offset_top = 255 offset_bottom = 375 script = ExtResource("10") @@ -937,7 +960,29 @@ glow_color = Color(0.3, 0.9, 1, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = 158 +offset_left = 74 +offset_right = 229 +offset_top = 380 +offset_bottom = 460 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="AvailableDistorder" type="Control" parent="UI/HangarMenu"] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 215 +offset_right = 290 +offset_top = 255 +offset_bottom = 375 +script = ExtResource("10") +icon = 37 +glow_color = Color(0.7, 0.3, 1, 1) + +[node name="AvailableDistorderLabel" type="Label" parent="UI/HangarMenu"] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 175 offset_right = 330 offset_top = 380 offset_bottom = 460 @@ -957,7 +1002,7 @@ horizontal_alignment = 1 anchor_left = 0.5 anchor_right = 0.5 offset_left = -290 -offset_right = -198 +offset_right = -215 offset_top = 565 offset_bottom = 685 script = ExtResource("10") @@ -968,7 +1013,7 @@ mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 offset_left = -330 -offset_right = -158 +offset_right = -175 offset_top = 690 offset_bottom = 770 theme_override_font_sizes/font_size = 22 @@ -977,8 +1022,8 @@ horizontal_alignment = 1 [node name="FleetShield" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = -168 -offset_right = -76 +offset_left = -189 +offset_right = -114 offset_top = 565 offset_bottom = 685 script = ExtResource("10") @@ -989,8 +1034,8 @@ glow_color = Color(0.45, 0.8, 1, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -208 -offset_right = -36 +offset_left = -229 +offset_right = -74 offset_top = 690 offset_bottom = 770 theme_override_font_sizes/font_size = 22 @@ -999,8 +1044,8 @@ horizontal_alignment = 1 [node name="FleetGasBomber" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = -46 -offset_right = 46 +offset_left = -88 +offset_right = -13 offset_top = 565 offset_bottom = 685 script = ExtResource("10") @@ -1011,8 +1056,8 @@ glow_color = Color(0.96, 0.32, 0.78, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -86 -offset_right = 86 +offset_left = -128 +offset_right = 27 offset_top = 690 offset_bottom = 770 theme_override_font_sizes/font_size = 22 @@ -1021,8 +1066,8 @@ horizontal_alignment = 1 [node name="FleetCarrier" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = 76 -offset_right = 168 +offset_left = 13 +offset_right = 88 offset_top = 565 offset_bottom = 685 script = ExtResource("10") @@ -1033,8 +1078,8 @@ glow_color = Color(1, 0.78, 0.3, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = 36 -offset_right = 208 +offset_left = -27 +offset_right = 128 offset_top = 690 offset_bottom = 770 theme_override_font_sizes/font_size = 22 @@ -1043,8 +1088,8 @@ horizontal_alignment = 1 [node name="FleetLaser" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = 198 -offset_right = 290 +offset_left = 114 +offset_right = 189 offset_top = 565 offset_bottom = 685 script = ExtResource("10") @@ -1055,7 +1100,29 @@ glow_color = Color(0.3, 0.9, 1, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = 158 +offset_left = 74 +offset_right = 229 +offset_top = 690 +offset_bottom = 770 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="FleetDistorder" type="Control" parent="UI/HangarMenu"] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 215 +offset_right = 290 +offset_top = 565 +offset_bottom = 685 +script = ExtResource("10") +icon = 37 +glow_color = Color(0.7, 0.3, 1, 1) + +[node name="FleetDistorderLabel" type="Label" parent="UI/HangarMenu"] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 175 offset_right = 330 offset_top = 690 offset_bottom = 770 @@ -1107,7 +1174,7 @@ script = ExtResource("24") anchor_left = 0.5 anchor_right = 0.5 offset_left = -300 -offset_right = -230 +offset_right = -240 offset_top = 274 offset_bottom = 394 script = ExtResource("10") @@ -1117,8 +1184,8 @@ glow_color = Color(0.25, 0.85, 1, 1) [node name="VesselB" type="Control" parent="UI/LabMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = -194 -offset_right = -124 +offset_left = -210 +offset_right = -150 offset_top = 274 offset_bottom = 394 script = ExtResource("10") @@ -1128,8 +1195,8 @@ glow_color = Color(0.45, 0.8, 1, 1) [node name="VesselC" type="Control" parent="UI/LabMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = -88 -offset_right = -18 +offset_left = -120 +offset_right = -60 offset_top = 274 offset_bottom = 394 script = ExtResource("10") @@ -1139,8 +1206,8 @@ glow_color = Color(0.96, 0.32, 0.78, 1) [node name="VesselD" type="Control" parent="UI/LabMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = 18 -offset_right = 88 +offset_left = -30 +offset_right = 30 offset_top = 274 offset_bottom = 394 script = ExtResource("10") @@ -1150,8 +1217,8 @@ glow_color = Color(0.95, 0.55, 0.85, 1) [node name="VesselE" type="Control" parent="UI/LabMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = 124 -offset_right = 194 +offset_left = 60 +offset_right = 120 offset_top = 274 offset_bottom = 394 script = ExtResource("10") @@ -1161,14 +1228,25 @@ glow_color = Color(1, 0.78, 0.3, 1) [node name="VesselF" type="Control" parent="UI/LabMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = 230 -offset_right = 300 +offset_left = 150 +offset_right = 210 offset_top = 274 offset_bottom = 394 script = ExtResource("10") icon = 35 glow_color = Color(0.3, 0.9, 1, 1) +[node name="VesselG" type="Control" parent="UI/LabMenu"] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 240 +offset_right = 300 +offset_top = 274 +offset_bottom = 394 +script = ExtResource("10") +icon = 37 +glow_color = Color(0.7, 0.3, 1, 1) + [node name="VesselLabel" type="Label" parent="UI/LabMenu"] visible = false mouse_filter = 2 @@ -1186,7 +1264,7 @@ mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 offset_left = -330 -offset_right = -200 +offset_right = -210 offset_top = 400 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -1197,8 +1275,8 @@ text = "Interceptor" mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -224 -offset_right = -94 +offset_left = -240 +offset_right = -120 offset_top = 400 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -1209,8 +1287,8 @@ text = "Shield" mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -118 -offset_right = 12 +offset_left = -150 +offset_right = -30 offset_top = 400 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -1221,8 +1299,8 @@ text = "Gas Bomber" mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = 12 -offset_right = 118 +offset_left = -60 +offset_right = 60 offset_top = 400 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -1233,8 +1311,8 @@ text = "Transporter" mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = 94 -offset_right = 224 +offset_left = 30 +offset_right = 150 offset_top = 400 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -1245,14 +1323,26 @@ text = "Carrier" mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = 200 -offset_right = 330 +offset_left = 120 +offset_right = 240 offset_top = 400 offset_bottom = 460 theme_override_font_sizes/font_size = 22 horizontal_alignment = 1 text = "Laser Ship" +[node name="VesselGLabel" type="Label" parent="UI/LabMenu"] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 210 +offset_right = 330 +offset_top = 400 +offset_bottom = 460 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 +text = "Distorder" + [node name="VesselUpgradeSeparator" type="ColorRect" parent="UI/LabMenu"] mouse_filter = 2 anchor_right = 1.0 @@ -1498,7 +1588,7 @@ anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 offset_left = -310 -offset_right = -232 +offset_right = -245 offset_top = -100 offset_bottom = 48 script = ExtResource("10") @@ -1511,7 +1601,7 @@ anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 offset_left = -330 -offset_right = -212 +offset_right = -225 offset_top = 56 offset_bottom = 136 theme_override_font_sizes/font_size = 24 @@ -1523,8 +1613,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -202 -offset_right = -124 +offset_left = -221 +offset_right = -156 offset_top = -100 offset_bottom = 48 script = ExtResource("10") @@ -1537,8 +1627,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -222 -offset_right = -104 +offset_left = -241 +offset_right = -136 offset_top = 56 offset_bottom = 136 theme_override_font_sizes/font_size = 24 @@ -1551,8 +1641,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -94 -offset_right = -16 +offset_left = -132 +offset_right = -67 offset_top = -100 offset_bottom = 28 script = ExtResource("10") @@ -1565,8 +1655,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -114 -offset_right = 4 +offset_left = -152 +offset_right = -47 offset_top = 56 offset_bottom = 150 theme_override_font_sizes/font_size = 24 @@ -1579,8 +1669,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = 14 -offset_right = 92 +offset_left = -43 +offset_right = 22 offset_top = -100 offset_bottom = 48 script = ExtResource("10") @@ -1593,8 +1683,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -6 -offset_right = 112 +offset_left = -63 +offset_right = 42 offset_top = 56 offset_bottom = 136 theme_override_font_sizes/font_size = 24 @@ -1607,8 +1697,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = 122 -offset_right = 200 +offset_left = 46 +offset_right = 111 offset_top = -100 offset_bottom = 48 script = ExtResource("10") @@ -1621,8 +1711,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = 102 -offset_right = 220 +offset_left = 26 +offset_right = 131 offset_top = 56 offset_bottom = 136 theme_override_font_sizes/font_size = 24 @@ -1630,6 +1720,34 @@ autowrap_mode = 2 text = "Laser Ship" horizontal_alignment = 1 +[node name="DistorderButton" type="Control" parent="UI/CraftMenu"] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 135 +offset_right = 200 +offset_top = -100 +offset_bottom = 48 +script = ExtResource("10") +icon = 37 +glow_color = Color(0.7, 0.3, 1, 1) + +[node name="DistorderLabel" type="Label" parent="UI/CraftMenu"] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 115 +offset_right = 220 +offset_top = 56 +offset_bottom = 136 +theme_override_font_sizes/font_size = 24 +autowrap_mode = 2 +text = "Distorder" +horizontal_alignment = 1 + [node name="QueueLabel" type="Label" parent="UI/CraftMenu"] mouse_filter = 2 anchor_left = 1.0 diff --git a/PlanetSurface.tscn b/PlanetSurface.tscn index afdeb49..e4ab07a 100644 --- a/PlanetSurface.tscn +++ b/PlanetSurface.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=15 format=3] +[gd_scene load_steps=16 format=3] [ext_resource type="Script" path="res://GameController.gd" id="1"] [ext_resource type="Script" path="res://LunarBackground.gd" id="2"] @@ -10,6 +10,7 @@ [ext_resource type="Script" path="res://ResourceReadout.gd" id="9"] [ext_resource type="Script" path="res://GasBombController.gd" id="14"] [ext_resource type="Script" path="res://LaserController.gd" id="16"] +[ext_resource type="Script" path="res://SpeedFader.gd" id="17"] [ext_resource type="Script" path="res://CaptureLabel.gd" id="15"] [sub_resource type="ShaderMaterial" id="1"] @@ -157,6 +158,30 @@ script = ExtResource("7") glow_color = Color(0.06, 0.32, 0.5, 1) core_color = Color(0.02, 0.09, 0.15, 1) +[node name="SpeedFader" type="Control" parent="UI"] +visible = false +mouse_filter = 2 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -94 +offset_right = -24 +offset_top = 740 +offset_bottom = 920 +script = ExtResource("17") +glow_color = Color(0.5, 0.18, 0.6, 1) +core_color = Color(0.02, 0.09, 0.15, 1) + +[node name="Label" type="Label" parent="UI/SpeedFader"] +mouse_filter = 2 +offset_left = -20 +offset_right = 90 +offset_top = -32 +offset_bottom = -4 +theme_override_font_sizes/font_size = 20 +theme_override_colors/font_color = Color(0.7, 0.3, 1, 1) +horizontal_alignment = 1 +text = "x1.0" + [node name="LaserButton" type="Control" parent="UI"] visible = false mouse_filter = 2 diff --git a/Ship.gd b/Ship.gd index cdaa542..074c6b4 100644 --- a/Ship.gd +++ b/Ship.gd @@ -97,16 +97,29 @@ func _physics_process(delta: float) -> void: if child.has_method("tick"): child.tick(delta) + # #59: multiplied here, at the point of use, rather than baked into the + # cached max_speed/acceleration fields apply_vessel_stats() sets - those + # stay the true base values, so a stat upgrade researched mid-boost can't + # corrupt them, and the fader takes effect the instant it moves with no + # re-apply needed. arrive_radius is scaled too: at N physics ticks/sec a + # ship's per-tick step is `speed * delta`, and once that step outgrows + # the (otherwise fixed) arrive_radius it can overshoot the snap-to-target + # zone every tick while chasing a target that is ALSO now receding N + # times faster - a small, boost-proportional radius keeps the same + # "snaps onto its slot" formation tightness at any boost level that + # 1.0x already has. + var boost: float = GameState.fleet_speed_multiplier var to_target: Vector2 = move_target - position var distance: float = to_target.length() - if distance <= arrive_radius: + if distance <= arrive_radius * boost: 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 effective_acceleration: float = acceleration * boost + var braking_speed: float = sqrt(2.0 * effective_acceleration * distance) + var desired_speed: float = min(max_speed * boost, braking_speed) var desired_velocity: Vector2 = to_target.normalized() * desired_speed - velocity = velocity.move_toward(desired_velocity, acceleration * delta) + velocity = velocity.move_toward(desired_velocity, effective_acceleration * delta) position += velocity * delta var motion: Vector2 = velocity + fleet_velocity diff --git a/SpeedFader.gd b/SpeedFader.gd new file mode 100644 index 0000000..6449b77 --- /dev/null +++ b/SpeedFader.gd @@ -0,0 +1,112 @@ +class_name SpeedFader +extends Control + +# Vertical drag control for GameState.fleet_speed_multiplier. Same raw-touch +# idiom as TouchJoystick (handle_touch/handle_drag offered first by +# GameController, not _gui_input) and the same NeonDraw look, but a fader +# rather than a stick: the knob's Y position along a fixed vertical track IS +# the value, no spring-back to centre. +# +# Self-contained: writes straight to GameState.fleet_speed_multiplier rather +# than reporting a value for something else to apply, since the multiplier +# already lives in the one place (the autoload) that needs to survive scene +# swaps. + +@export var glow_color: Color = Color(0.7, 0.3, 1.0) +@export var core_color: Color = Color(1.0, 1.0, 1.0) +@export var track_margin: float = 28.0 + +@onready var fleet: Node2D = get_node("../../Fleet") +@onready var label: Label = get_node("Label") + +var active_touch_index: int = -1 +# 0 = bottom of the track (1.0x), 1 = top (GameState.MAX_FLEET_SPEED_MULTIPLIER). +var knob_t: float = 0.0 + + +func _ready() -> void: + visible = false + queue_redraw() + + +func _process(_delta: float) -> void: + var has_distorder: bool = fleet.has_capability("distorder") + if has_distorder != visible: + visible = has_distorder + if not has_distorder: + # A fleet must never be left permanently boosted by a vessel it no + # longer has - reset the instant the capability disappears, not just + # hide the control on top of a stale multiplier. + active_touch_index = -1 + if GameState.fleet_speed_multiplier != 1.0: + GameState.fleet_speed_multiplier = 1.0 + knob_t = 0.0 + return + knob_t = (GameState.fleet_speed_multiplier - 1.0) \ + / maxf(GameState.MAX_FLEET_SPEED_MULTIPLIER - 1.0, 0.001) + label.text = "x%.1f" % GameState.fleet_speed_multiplier + queue_redraw() + + +func is_active() -> bool: + return active_touch_index != -1 + + +func _blocked_by_modal() -> bool: + # Same check as IconButton._blocked_by_modal() - panels stay transparent + # to raw input (the skill tree/formation editor need _unhandled_input), + # so every raw-touch control checks for itself instead. + for menu in get_tree().get_nodes_in_group("modal_menus"): + if menu.is_visible_in_tree() and not menu.is_ancestor_of(self): + return true + return false + + +func handle_touch(event: InputEventScreenTouch) -> bool: + var local: Vector2 = event.position - global_position + if event.pressed: + if not visible or active_touch_index != -1 or _blocked_by_modal(): + return false + if not Rect2(Vector2.ZERO, size).has_point(local): + return false + active_touch_index = event.index + _update_from_local(local) + return true + if event.index != active_touch_index: + return false + active_touch_index = -1 + queue_redraw() + return true + + +func handle_drag(event: InputEventScreenDrag) -> bool: + if event.index != active_touch_index: + return false + _update_from_local(event.position - global_position) + return true + + +func _update_from_local(local: Vector2) -> void: + var usable: float = maxf(size.y - track_margin * 2.0, 1.0) + # Up is 1.0 (top of the track), matching how a real fader/throttle reads. + var t: float = 1.0 - clampf((local.y - track_margin) / usable, 0.0, 1.0) + knob_t = t + GameState.set_fleet_speed_multiplier(1.0 + t * (GameState.MAX_FLEET_SPEED_MULTIPLIER - 1.0)) + label.text = "x%.1f" % GameState.fleet_speed_multiplier + queue_redraw() + + +func _draw() -> void: + var top: Vector2 = Vector2(size.x * 0.5, track_margin) + var bottom: Vector2 = Vector2(size.x * 0.5, size.y - track_margin) + NeonDraw.draw_glowing_line(self, bottom, top, glow_color, 0.55, 0.5, core_color) + + # Tick marks at the extremes read as "min" and "max" at a glance. + for t in [0.0, 1.0]: + var y: float = lerpf(bottom.y, top.y, t) + NeonDraw.draw_glowing_line(self, Vector2(size.x * 0.5 - 12.0, y), + Vector2(size.x * 0.5 + 12.0, y), glow_color, 0.4, 0.35, core_color) + + var knob_pos: Vector2 = bottom.lerp(top, clampf(knob_t, 0.0, 1.0)) + NeonDraw.draw_glowing_circle(self, knob_pos, 16.0, glow_color, + 1.0 if is_active() else 0.8, core_color) diff --git a/VesselDefinitions.gd b/VesselDefinitions.gd index a037eb7..33ca763 100644 --- a/VesselDefinitions.gd +++ b/VesselDefinitions.gd @@ -1,7 +1,7 @@ class_name VesselDefinitions extends RefCounted -const ORDER: PackedStringArray = ["interceptor", "shield", "gas_bomber", "transporter", "carrier", "laser"] +const ORDER: PackedStringArray = ["interceptor", "shield", "gas_bomber", "transporter", "carrier", "laser", "distorder"] const DEFINITIONS: Dictionary = { "interceptor": preload("res://InterceptorVesselDefinition.tres"), "shield": preload("res://ShieldVesselDefinition.tres"), @@ -9,6 +9,7 @@ const DEFINITIONS: Dictionary = { "transporter": preload("res://TransporterVesselDefinition.tres"), "carrier": preload("res://CarrierVesselDefinition.tres"), "laser": preload("res://LaserVesselDefinition.tres"), + "distorder": preload("res://DistorderVesselDefinition.tres"), } static func get_definition(definition_id: String) -> Resource: diff --git a/VesselDistorderComponent.gd b/VesselDistorderComponent.gd new file mode 100644 index 0000000..25a31d7 --- /dev/null +++ b/VesselDistorderComponent.gd @@ -0,0 +1,9 @@ +class_name VesselDistorderComponent +extends Node + +# Pure presence marker (#59) - no per-shot behaviour, no tick(). Its only +# job is to make Ship.has_capability("distorder") true, which is what +# SpeedFader.gd/Fleet.gd key off to reveal and apply the speed boost. +# Energy drain while boosted is a later ticket (#62). + +var capability_id := "distorder" diff --git a/VesselDistorderComponent.tscn b/VesselDistorderComponent.tscn new file mode 100644 index 0000000..862f0d3 --- /dev/null +++ b/VesselDistorderComponent.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://VesselDistorderComponent.gd" id="1"] + +[node name="DistorderComponent" type="Node"] +script = ExtResource("1") diff --git a/tests/test_space_time_distorder.gd b/tests/test_space_time_distorder.gd new file mode 100644 index 0000000..8aff0e7 --- /dev/null +++ b/tests/test_space_time_distorder.gd @@ -0,0 +1,232 @@ +extends SceneTree + +# Headless gameplay test: run with +# godot --headless -s res://tests/test_space_time_distorder.gd +# Forgejo #59: a researchable vessel that lets the whole fleet travel faster +# than its default cruise speed, via a fader driving +# game_state.fleet_speed_multiplier. +# +# Covers: +# A. the fader is absent with no distorder ship, appears once one joins +# B. dragging it up measurably increases distance travelled per second; +# at 1.0 the fleet moves exactly as it does today +# C. formation holds together under boost - ships stay near their slot +# D. losing the vessel resets the multiplier and hides the fader +# E. the multiplier survives the space -> surface scene swap (it lives +# in the autoload) + +var game_state: Node +var main: Node +var fleet: Node +var fader: Node +var stage: int = 0 +var elapsed: float = 0.0 +var baseline_distance: float = 0.0 +var boosted_distance: float = 0.0 +var start_centroid: Vector2 = Vector2.ZERO +var unboosted_lag: float = 0.0 + +const MEASURE_TIME: float = 1.0 + + +func _initialize() -> void: + game_state = root.get_node("GameState") + game_state.buildings.clear() + game_state.extractors.clear() + game_state.transporters.clear() + game_state.stored_vessels.clear() + game_state.reset_progression() + game_state.vessel_levels["unlock_distorder"] = 1 + main = load("res://Main.tscn").instantiate() + root.add_child(main) + fleet = main.get_node("Fleet") + fader = main.get_node("UI/SpeedFader") + + +func _process(delta: float) -> bool: + elapsed += delta + match stage: + 0: + if elapsed < 0.2: + return false + main.get_node("EnemySpawner").set_process(false) + main.get_node("AsteroidField").set_process(false) + + # --- A. absent, then appears ------------------------------------- + if fader.visible: + return _fail_now("fader appeared without a distorder ship") + + fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([ + GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_DISTORDER])) + fader._process(0.0) + if not fader.visible: + return _fail_now("a distorder ship in the fleet did not reveal the fader") + print("fader hidden with no distorder ship, visible once one joins") + + # --- B. baseline @ 1.0x ------------------------------------------- + if not is_equal_approx(game_state.fleet_speed_multiplier, 1.0): + return _fail_now("multiplier is not 1.0 by default: %f" % game_state.fleet_speed_multiplier) + start_centroid = fleet.get_centroid() + fleet.move_in_direction(Vector2(1.0, 0.0)) + elapsed = 0.0 + stage = 1 + return false + 1: + if elapsed < MEASURE_TIME: + return false + baseline_distance = fleet.get_centroid().distance_to(start_centroid) + # Reference for the formation check below: pure pursuit (a ship + # always aiming straight at its CURRENT, still-moving target + # rather than leading it) has its own inherent non-zero steady- + # state following distance even at 1.0x - it scales with the + # speeds involved, so "holds together under boost" has to mean + # staying proportional to this, not shrinking to some fixed + # absolute number regardless of how fast the fleet is going. + for i in range(fleet.ships.size()): + var ship: Node2D = fleet.ships[i] + if not is_instance_valid(ship): + continue + var expected: Vector2 = fleet.fleet_target + fleet.formation_offsets[i] + unboosted_lag = maxf(unboosted_lag, ship.position.distance_to(expected)) + fleet.stop() + print("baseline (1.0x): travelled %.1f over %.1fs, formation lag %.1f" \ + % [baseline_distance, MEASURE_TIME, unboosted_lag]) + elapsed = 0.0 + stage = 2 + return false + 2: + # Let the braking manoeuvre from stop() fully settle before + # measuring the boosted leg, so leftover momentum from the + # baseline pass can't pad the comparison either way. + if elapsed < 0.5: + return false + + var press := InputEventScreenTouch.new() + press.index = 5 + press.position = fader.global_position + Vector2(fader.size.x * 0.5, fader.size.y - 4.0) + press.pressed = true + main._handle_touch(press) + if not fader.is_active(): + return _fail_now("pressing the fader did not begin dragging") + var drag := InputEventScreenDrag.new() + drag.index = 5 + # All the way to the top of the track = MAX_FLEET_SPEED_MULTIPLIER. + drag.position = fader.global_position + Vector2(fader.size.x * 0.5, 0.0) + main._handle_drag(drag) + if not is_equal_approx(game_state.fleet_speed_multiplier, GameState.MAX_FLEET_SPEED_MULTIPLIER): + return _fail_now("dragging to the top did not reach the max multiplier: %f" \ + % game_state.fleet_speed_multiplier) + print("fader dragged to %.1fx" % game_state.fleet_speed_multiplier) + + start_centroid = fleet.get_centroid() + fleet.move_in_direction(Vector2(1.0, 0.0)) + elapsed = 0.0 + stage = 3 + return false + 3: + if elapsed < MEASURE_TIME: + return false + boosted_distance = fleet.get_centroid().distance_to(start_centroid) + print("boosted (%.1fx): travelled %.1f over %.1fs" \ + % [game_state.fleet_speed_multiplier, boosted_distance, MEASURE_TIME]) + if boosted_distance <= baseline_distance * 1.3: + return _fail_now("boosted travel (%.1f) was not measurably more than baseline (%.1f)" \ + % [boosted_distance, baseline_distance]) + elapsed = 0.0 + stage = 4 + return false + 4: + # --- C. formation holds together under boost ---------------------- + # Both the anchor's advance and the ships' own catch-up speed are + # scaled by the SAME multiplier, so the initial accelerate-from- + # rest transient covers a proportionally larger absolute distance + # under a 3x boost - a moment right after the boost begins isn't + # "trailing", it just hasn't finished the same (time-invariant) + # catch-up burst yet. Keep steering a while longer so that burst + # is over before judging whether formation holds. + if elapsed < 1.5: + return false + # Allow the lag to scale with the boost (it inherently does, even + # with both the anchor and ship speeds multiplied by the exact + # same factor - see the note by unboosted_lag above) but not + # beyond that, with slack for measurement noise. + var allowed_lag: float = maxf(unboosted_lag, 20.0) * game_state.fleet_speed_multiplier * 1.5 + for i in range(fleet.ships.size()): + var ship: Node2D = fleet.ships[i] + if not is_instance_valid(ship): + continue + var expected: Vector2 = fleet.fleet_target + fleet.formation_offsets[i] + var strayed: float = ship.position.distance_to(expected) + if strayed > allowed_lag: + return _fail_now("a ship strayed %.1f from its formation slot under sustained boost (unboosted lag was %.1f, allowed %.1f)" \ + % [strayed, unboosted_lag, allowed_lag]) + print("formation held together under sustained boost (lag stayed proportional to the boost)") + + fleet.stop() + var release := InputEventScreenTouch.new() + release.index = 5 + release.position = fader.global_position + Vector2(fader.size.x * 0.5, 0.0) + release.pressed = false + main._handle_touch(release) + + # --- E. survives the space -> surface scene swap ------------------- + # Done here, with the distorder ship still crewed and the fleet + # still boosted: tearing this scene down while nothing else + # changes the fleet's own composition proves the multiplier + # lives in the autoload rather than being reset as a side effect + # of the old scene's own SpeedFader still ticking once more + # during teardown (it would, correctly, self-reset the instant + # it next saw a fleet with no distorder ship - not a real + # scenario here since this fleet keeps its distorder ship the + # whole way across the swap, via GameState.surviving_vessel_types + # exactly like every other vessel type). + game_state.surviving_ship_count = fleet.ships.size() + game_state.surviving_vessel_types = fleet.vessel_types() + game_state.planet_id = "test_space_time_distorder" + game_state.planet_name = "Distorder Test" + main.queue_free() + elapsed = 0.0 + stage = 5 + return false + 5: + if elapsed < 0.3: + return false + main = load("res://PlanetSurface.tscn").instantiate() + root.add_child(main) + current_scene = main + fleet = main.get_node("Fleet") + fader = main.get_node("UI/SpeedFader") + elapsed = 0.0 + stage = 6 + return false + 6: + if elapsed < 0.3: + return false + if not is_equal_approx(game_state.fleet_speed_multiplier, 3.0): + return _fail_now("multiplier did not survive the space -> surface swap: %f" \ + % game_state.fleet_speed_multiplier) + fader._process(0.0) + if not fader.visible: + return _fail_now("the surface scene's fader did not show the surviving distorder ship") + print("multiplier and fader both survived the space -> surface swap") + + # --- D. losing the vessel resets the multiplier ------------------- + fleet.spawn_ships(1, fleet.get_centroid(), PackedStringArray([GameState.VESSEL_INTERCEPTOR])) + fader._process(0.0) + if fader.visible: + return _fail_now("fader stayed visible after losing the last distorder ship") + if not is_equal_approx(game_state.fleet_speed_multiplier, 1.0): + return _fail_now("multiplier was not reset after losing the distorder ship: %f" \ + % game_state.fleet_speed_multiplier) + print("losing the distorder ship reset the multiplier and hid the fader") + + print("TEST_SPACE_TIME_DISTORDER: PASS") + quit(0) + return true + return false + + +func _fail_now(message: String) -> bool: + print("TEST_SPACE_TIME_DISTORDER: FAIL (%s)" % message) + quit(1) + return true