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).
112 lines
3.9 KiB
GDScript
112 lines
3.9 KiB
GDScript
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)
|