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 var status_message_until: int = 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: # Fleet owns persistent composition state. This outgoing UI node can # briefly observe an empty fleet during a scene swap, so it must only # clear its own interaction state here. active_touch_index = -1 knob_t = 0.0 return knob_t = (GameState.fleet_speed_multiplier - 1.0) \ / maxf(GameState.MAX_FLEET_SPEED_MULTIPLIER - 1.0, 0.001) # #62: GameState._process() (not this node's own _process()) is what # drains energy and force-resets the multiplier, since it has to keep # running across the space<->surface swap and while this control might # not even be in the current scene tree. It leaves a one-shot flag for # whichever SpeedFader picks it up next. if GameState.boost_depleted_notice: GameState.boost_depleted_notice = false # Still holding the drag at the moment energy ran out would # otherwise re-boost on the very next drag event, from a knob # position the finger never actually moved away from. active_touch_index = -1 label.text = "Out of energy!" status_message_until = Time.get_ticks_msec() + 3000 elif Time.get_ticks_msec() >= status_message_until: 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)