spaceship/ui/TouchJoystick.gd

140 lines
4.8 KiB
GDScript3
Raw Normal View History

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).
#
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
# 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.
2026-09-02 11:19:54 +02:00
# The scene zones are deliberately wider than they are tall. Forty percent
# makes the resting ring just over twice its former radius while leaving room
# for the floating base to move inside the touch rect.
const RADIUS_RATIO: float = 0.40
@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:
2026-09-02 20:18:44 +02:00
# Dialogs release every spring-loaded stick through one compositional
# contract, without NarrationBanner knowing scene paths or joystick roles.
add_to_group("touch_joysticks")
resized.connect(_relayout)
_relayout()
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
# 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:
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
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
2026-09-02 20:18:44 +02:00
release_input()
return true
# Returns this spring-loaded control to its neutral state. Public and
# idempotent so a dialog can safely release movement and fire sticks before
# pausing their controllers (#182), even when only one is currently held.
func release_input() -> void:
var was_active: bool = is_active() or direction != Vector2.ZERO
active_touch_index = -1
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
# 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()
2026-09-02 20:18:44 +02:00
if was_active:
direction_changed.emit(direction)
released.emit()
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)