Both read raw touches, and the project no longer turns clicks into them - the same routing gap #93 fixed in the research tree. The boost was unreachable on a desktop and the formation could not be rearranged at all. Wrappers over the same press/drag logic in each case, never a second implementation, so the touch and mouse paths cannot drift apart. The wheel replaces the pinch in the formation editor: a mouse has no second finger, and zoom is the other half of what that panel does. The fader gets first refusal on a mouse press in GameController, so dragging the boost does not also pan or zoom the world behind it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
155 lines
5.6 KiB
GDScript
155 lines
5.6 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. The top is
|
|
# GameState.max_fleet_speed_multiplier, which widens with each distorder
|
|
# vessel past the first - so the track always spans the whole usable range.
|
|
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
|
|
|
|
|
|
# #118: the same fader, driven by a mouse.
|
|
#
|
|
# The project used to set emulate_touch_from_mouse, which handed every raw
|
|
# touch handler a synthetic touch per click; with that gone this control was
|
|
# inert on a desktop and there was no other way to reach the boost. Written
|
|
# as thin wrappers over the touch path rather than as a second
|
|
# implementation, so the two cannot drift - the same mistake #93 fixed in
|
|
# the research tree.
|
|
const MOUSE_TOUCH_INDEX: int = -2
|
|
|
|
|
|
func handle_mouse_button(event: InputEventMouseButton) -> bool:
|
|
if event.button_index != MOUSE_BUTTON_LEFT:
|
|
return false
|
|
return _press(event.pressed, event.position, MOUSE_TOUCH_INDEX)
|
|
|
|
|
|
func handle_mouse_motion(event: InputEventMouseMotion) -> bool:
|
|
if active_touch_index != MOUSE_TOUCH_INDEX:
|
|
return false
|
|
_update_from_local(event.position - global_position)
|
|
return true
|
|
|
|
|
|
func handle_touch(event: InputEventScreenTouch) -> bool:
|
|
return _press(event.pressed, event.position, event.index)
|
|
|
|
|
|
func _press(pressed: bool, at: Vector2, index: int) -> bool:
|
|
var local: Vector2 = at - global_position
|
|
if 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 = index
|
|
_update_from_local(local)
|
|
return true
|
|
if 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)
|