spaceship/TouchJoystick.gd
Loïc Wisniewski b999031e6a Recentre a released stick, slow extractors again, and unbreak the lab layout
Both sticks float - the base jumps to wherever the finger lands, so a player
does not have to hit an exact spot on a screen they cannot feel. Releasing put
only the KNOB back onto the base and left the base parked wherever that touch
had been, so an idle stick sat off-centre in a different place every time and
read as a control still in use. Release now returns base and knob to the
middle of the rect.

Extractors produce at 0.5 ore per second, halved again: at 1.0 they still
outran the rest of the economy. An extractor now pays back its 25-ore cost in
fifty seconds rather than twelve.

The ship lab and the crafter were both overlapping themselves. Enlarging the
shared resource table to 136px (so it could breathe and show bigger type)
pushed its bottom edge 34px into the vessel row beneath it in one and the
status text in the other - my regression, visible immediately on a real
screen and not at all in any test. The lab's second row of upgrade cards was
also already running into the detail text below it, by 13px, before any of
this. Both menus are re-spaced end to end rather than nudged, since moving one
collision into another is how the second one got there.

Two tests carried waits tuned to an extraction rate that has now changed
twice. Both derive them from EXTRACTOR_RATE instead, so the next balance pass
does not report a broken extractor that is working perfectly - the same fix
test_extractor_income already needed one round ago, in the file next to it.

Suite 112/112.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:31:46 +02:00

125 lines
4.1 KiB
GDScript

class_name TouchJoystick
extends Control
# Touch joystick for steering the fleet. Lives in the bottom-left corner.
#
# Behaves like a "floating" stick within its own rect: the base re-centres
# on wherever the finger first lands, so the player doesn't have to hit an
# exact spot on a phone screen they can't feel. The knob then follows the
# finger out to max_radius, and `direction` reports the offset normalised
# against that radius (so magnitude is 0..1, usable for throttle later).
#
# Releasing puts BOTH the base and the knob back in the middle of the rect, so
# an idle stick always looks the same and always looks idle.
#
# Only claims touches that start inside its own rect - anything else falls
# through untouched, which is what keeps pinch-to-zoom working everywhere
# else on screen.
signal direction_changed(direction: Vector2)
signal released
# How far the knob travels from centre, as a fraction of the control's
# shorter side. Derived rather than a fixed number of pixels so that
# enlarging the stick's rect enlarges the ring the player actually sees -
# otherwise the touch area grows and the visible control stays small.
const RADIUS_RATIO: float = 0.28
@export var max_radius: float = 90.0
@export var deadzone: float = 0.12
@export var glow_color: Color = Color(0.25, 0.85, 1.0)
@export var core_color: Color = Color(1.0, 1.0, 1.0)
var direction: Vector2 = Vector2.ZERO
var active_touch_index: int = -1
var base_pos: Vector2 = Vector2.ZERO
var knob_pos: Vector2 = Vector2.ZERO
func _ready() -> void:
resized.connect(_relayout)
_relayout()
# Where the stick sits when nobody is touching it: the middle of its own rect.
# The base only moves away from here while a finger is down (see handle_touch),
# and returns the moment it lifts.
func home_pos() -> Vector2:
return size / 2.0
func _relayout() -> void:
base_pos = home_pos()
knob_pos = base_pos
max_radius = minf(size.x, size.y) * RADIUS_RATIO
queue_redraw()
func is_active() -> bool:
return active_touch_index != -1
# Returns true if the event was consumed, so the caller knows not to treat
# it as a world gesture (a fleet move, or one finger of a pinch).
func handle_touch(event: InputEventScreenTouch) -> bool:
var local: Vector2 = event.position - global_position
if event.pressed:
if active_touch_index != -1:
return false
if not Rect2(Vector2.ZERO, size).has_point(local):
return false
active_touch_index = event.index
base_pos = local
knob_pos = local
_update_direction()
return true
if event.index != active_touch_index:
return false
active_touch_index = -1
# Back to the middle of the rect, BASE included. This used to reset the
# knob onto base_pos and leave the base wherever the finger had landed, so
# a released stick stayed parked off-centre - which reads as a control
# still in use, and left the ring somewhere different every time.
base_pos = home_pos()
knob_pos = base_pos
direction = Vector2.ZERO
queue_redraw()
direction_changed.emit(direction)
released.emit()
return true
func handle_drag(event: InputEventScreenDrag) -> bool:
if event.index != active_touch_index:
return false
knob_pos = event.position - global_position
_update_direction()
return true
func _update_direction() -> void:
var offset: Vector2 = knob_pos - base_pos
if offset.length() > max_radius:
offset = offset.normalized() * max_radius
knob_pos = base_pos + offset
var raw: Vector2 = offset / max_radius
direction = Vector2.ZERO if raw.length() < deadzone else raw
queue_redraw()
direction_changed.emit(direction)
func _draw() -> void:
var ring: PackedVector2Array = PackedVector2Array()
var segments: int = 28
for i in range(segments):
var a: float = (float(i) / segments) * TAU
ring.append(base_pos + Vector2(cos(a), sin(a)) * max_radius)
NeonDraw.draw_glowing_polygon(self, ring, glow_color, 0.55, core_color)
var knob: PackedVector2Array = PackedVector2Array()
var knob_radius: float = max_radius * 0.34
for i in range(segments):
var a: float = (float(i) / segments) * TAU
knob.append(knob_pos + Vector2(cos(a), sin(a)) * knob_radius)
NeonDraw.draw_glowing_polygon(self, knob, glow_color, 1.0 if is_active() else 0.75, core_color)