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).
This commit is contained in:
parent
ed46cd77e6
commit
116e0f97d3
14 changed files with 675 additions and 86 deletions
|
|
@ -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) ---------------------------------------------
|
||||
|
|
|
|||
24
DistorderVesselDefinition.tres
Normal file
24
DistorderVesselDefinition.tres
Normal file
|
|
@ -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
|
||||
13
Fleet.gd
13
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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
13
GameState.gd
13
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
268
Main.tscn
268
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
21
Ship.gd
21
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
|
||||
|
|
|
|||
112
SpeedFader.gd
Normal file
112
SpeedFader.gd
Normal file
|
|
@ -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)
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
9
VesselDistorderComponent.gd
Normal file
9
VesselDistorderComponent.gd
Normal file
|
|
@ -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"
|
||||
6
VesselDistorderComponent.tscn
Normal file
6
VesselDistorderComponent.tscn
Normal file
|
|
@ -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")
|
||||
232
tests/test_space_time_distorder.gd
Normal file
232
tests/test_space_time_distorder.gd
Normal file
|
|
@ -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
|
||||
Loading…
Reference in a new issue