Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
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.
|
|
|
|
|
#
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
# 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
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
|
|
|
|
|
@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")
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
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()
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
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
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
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()
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
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()
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|