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 IconButton
|
|
|
|
|
extends Control
|
|
|
|
|
|
|
|
|
|
# Square neon-outlined button, hand-drawn to match the game's existing
|
|
|
|
|
# no-texture glowing look (same NeonDraw technique as ships/enemies) -
|
|
|
|
|
# no icon image assets, just a glyph drawn inside the frame.
|
|
|
|
|
|
|
|
|
|
signal pressed
|
|
|
|
|
|
|
|
|
|
# Scenes store `icon` as a plain int, so NEW ENTRIES MUST BE APPENDED -
|
|
|
|
|
# inserting one would silently repoint every button after it.
|
|
|
|
|
enum Icon {
|
|
|
|
|
MAP, CLOSE, SHIP, DOWN_ARROW, UP_ARROW, BUILD, ROTATE_CW, ROTATE_CCW,
|
|
|
|
|
# One per building, each a miniature of what actually gets placed. The
|
|
|
|
|
# build menu used to pick from the generic set, which left the mothership
|
|
|
|
|
# and the command centre sharing a folded-map glyph and neither of them
|
|
|
|
|
# looking anything like the structure they build.
|
Transport Hub building
Forgejo #18, part 5 of #12. The building only - the vessels that do the
hauling are #19. Placeable, costed at 30, gated behind a new "Freight
Handling" research node hanging off Logistics, with its own info popup,
build-menu glyph and map marker.
Drawn as an octagonal dock ring with berth arms and a squared-off freight
core: deliberately radial-but-flat so it does not read as a small
mothership, which is the only structure with swept arms. Its stationed
transports are NeonPiece children of one rotating holder, per #10 - the
geometry is rasterised once and only the holder's rotation changes, so
the traffic costs no redraws.
Building._draw()'s match has a catch-all that renders anything
unrecognised as a ship crafter, so a missing case is silent and looks
almost right. The test checks for the hub's own _hub_traffic holder
rather than trusting that something plausible appeared - which caught
exactly that: a _ready() patch had silently not applied and the hub was
rendering through the crafter branch.
The build menu grew a third row. Every button, label and price moved:
the old row occupied 332px against a 372px pitch, and three of those do
not fit above a 1280px screen. Rows are now shorter (128px buttons, 40px
price digits) at a 290px pitch, ending at y=1176. Rendered and looked at
- the first attempt tightened the pitch alone and left every price
overlapping the row beneath it, which no headless test would have seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:45:07 +02:00
|
|
|
MOTHERSHIP, CRAFTER, COMMAND_CENTER, EXTRACTOR, TRANSPORT_HUB,
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Size the glyphs were originally drawn against. Every glyph dimension is
|
|
|
|
|
# scaled by (actual size / this), so the button can be resized in the scene
|
|
|
|
|
# without the icon inside it staying small.
|
|
|
|
|
const REFERENCE_SIZE: float = 56.0
|
|
|
|
|
|
|
|
|
|
@export var icon: int = Icon.MAP
|
|
|
|
|
@export var glow_color: Color = Color(0.25, 0.85, 1.0)
|
|
|
|
|
# Colour of the bright core stroke. Defaults to near-white, which reads well
|
|
|
|
|
# on the dark space background; scenes drawing over a bright backdrop (the
|
|
|
|
|
# lunar surface) override this with a dark colour so the button doesn't wash
|
|
|
|
|
# out. See NeonDraw.
|
|
|
|
|
@export var core_color: Color = Color(1.0, 1.0, 1.0)
|
|
|
|
|
# When false the button is drawn greyed out, but STILL emits `pressed` -
|
|
|
|
|
# the caller decides what to do, which is what lets an unavailable option
|
|
|
|
|
# explain itself instead of silently doing nothing.
|
|
|
|
|
@export var enabled: bool = true: set = set_enabled
|
|
|
|
|
@export var disabled_color: Color = Color(0.42, 0.45, 0.5)
|
|
|
|
|
|
|
|
|
|
var pressed_down: bool = false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func set_enabled(value: bool) -> void:
|
|
|
|
|
if enabled == value:
|
|
|
|
|
return
|
|
|
|
|
enabled = value
|
|
|
|
|
queue_redraw()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
queue_redraw()
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 20:17:58 +02:00
|
|
|
func _blocked_by_modal() -> bool:
|
|
|
|
|
# A panel is open and this button is not part of it, so it is sitting
|
|
|
|
|
# BEHIND the panel and must not react.
|
|
|
|
|
#
|
|
|
|
|
# The panels cannot simply swallow GUI input with a mouse_filter of STOP:
|
|
|
|
|
# the skill tree and the formation editor are driven from raw
|
|
|
|
|
# _unhandled_input (see SkillTreeView), and a Control that consumes
|
|
|
|
|
# touches through the GUI system swallows them before they ever get
|
|
|
|
|
# there. So the panels stay transparent to input and the buttons check
|
|
|
|
|
# instead.
|
|
|
|
|
for menu in get_tree().get_nodes_in_group("modal_menus"):
|
|
|
|
|
if menu.visible and not menu.is_ancestor_of(self):
|
|
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
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 _gui_input(event: InputEvent) -> void:
|
2026-08-11 20:17:58 +02:00
|
|
|
if _blocked_by_modal():
|
|
|
|
|
return
|
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
|
|
|
if event is InputEventScreenTouch:
|
|
|
|
|
_handle_press(event.pressed, event.position)
|
|
|
|
|
accept_event()
|
|
|
|
|
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
|
|
|
|
_handle_press(event.pressed, event.position)
|
|
|
|
|
accept_event()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _handle_press(is_pressed: bool, at_position: Vector2) -> void:
|
|
|
|
|
if is_pressed:
|
|
|
|
|
pressed_down = true
|
|
|
|
|
queue_redraw()
|
|
|
|
|
elif pressed_down:
|
|
|
|
|
pressed_down = false
|
|
|
|
|
queue_redraw()
|
|
|
|
|
# Only counts as a real press if the finger/cursor is still over
|
|
|
|
|
# the button when it lifts - dragging off before releasing (a
|
|
|
|
|
# common way to "cancel" a touch) must not still trigger it.
|
|
|
|
|
if Rect2(Vector2.ZERO, size).has_point(at_position):
|
|
|
|
|
pressed.emit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _active_color() -> Color:
|
|
|
|
|
return glow_color if enabled else disabled_color
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw() -> void:
|
|
|
|
|
var rect_size: Vector2 = size
|
|
|
|
|
var s: float = min(rect_size.x, rect_size.y) / REFERENCE_SIZE
|
|
|
|
|
# Constant glow, brighter while held. Only the press state changes it,
|
|
|
|
|
# so the button redraws on press/release instead of every frame.
|
|
|
|
|
var pulse: float = 1.0 if pressed_down else 0.85
|
|
|
|
|
|
|
|
|
|
var margin: float = 4.0 * s
|
|
|
|
|
var square: PackedVector2Array = PackedVector2Array([
|
|
|
|
|
Vector2(margin, margin),
|
|
|
|
|
Vector2(rect_size.x - margin, margin),
|
|
|
|
|
Vector2(rect_size.x - margin, rect_size.y - margin),
|
|
|
|
|
Vector2(margin, rect_size.y - margin),
|
|
|
|
|
])
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, square, _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
var center: Vector2 = rect_size / 2.0
|
|
|
|
|
match icon:
|
|
|
|
|
Icon.CLOSE:
|
|
|
|
|
_draw_close(center, pulse, s)
|
|
|
|
|
Icon.SHIP:
|
|
|
|
|
_draw_ship(center, pulse, s)
|
|
|
|
|
Icon.MAP:
|
|
|
|
|
_draw_map(center, pulse, s)
|
|
|
|
|
Icon.DOWN_ARROW:
|
|
|
|
|
_draw_arrow(center, pulse, 1.0, s)
|
|
|
|
|
Icon.UP_ARROW:
|
|
|
|
|
_draw_arrow(center, pulse, -1.0, s)
|
|
|
|
|
Icon.BUILD:
|
|
|
|
|
_draw_build(center, pulse, s)
|
|
|
|
|
Icon.ROTATE_CW:
|
|
|
|
|
_draw_rotate(center, pulse, s, 1.0)
|
|
|
|
|
Icon.ROTATE_CCW:
|
|
|
|
|
_draw_rotate(center, pulse, s, -1.0)
|
|
|
|
|
Icon.MOTHERSHIP:
|
|
|
|
|
_draw_mothership_icon(center, pulse, s)
|
|
|
|
|
Icon.CRAFTER:
|
|
|
|
|
_draw_crafter_icon(center, pulse, s)
|
|
|
|
|
Icon.COMMAND_CENTER:
|
|
|
|
|
_draw_command_icon(center, pulse, s)
|
|
|
|
|
Icon.EXTRACTOR:
|
|
|
|
|
_draw_extractor_icon(center, pulse, s)
|
Transport Hub building
Forgejo #18, part 5 of #12. The building only - the vessels that do the
hauling are #19. Placeable, costed at 30, gated behind a new "Freight
Handling" research node hanging off Logistics, with its own info popup,
build-menu glyph and map marker.
Drawn as an octagonal dock ring with berth arms and a squared-off freight
core: deliberately radial-but-flat so it does not read as a small
mothership, which is the only structure with swept arms. Its stationed
transports are NeonPiece children of one rotating holder, per #10 - the
geometry is rasterised once and only the holder's rotation changes, so
the traffic costs no redraws.
Building._draw()'s match has a catch-all that renders anything
unrecognised as a ship crafter, so a missing case is silent and looks
almost right. The test checks for the hub's own _hub_traffic holder
rather than trusting that something plausible appeared - which caught
exactly that: a _ready() patch had silently not applied and the hub was
rendering through the crafter branch.
The build menu grew a third row. Every button, label and price moved:
the old row occupied 332px against a 372px pitch, and three of those do
not fit above a 1280px screen. Rows are now shorter (128px buttons, 40px
price digits) at a 290px pitch, ending at y=1176. Rendered and looked at
- the first attempt tightened the pitch alone and left every price
overlapping the row beneath it, which no headless test would have seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:45:07 +02:00
|
|
|
Icon.TRANSPORT_HUB:
|
|
|
|
|
_draw_transport_hub_icon(center, pulse, s)
|
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 _draw_close(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
var r: float = 10.0 * s
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-r, -r), center + Vector2(r, r), _active_color(), pulse, 0.5 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-r, r), center + Vector2(r, -r), _active_color(), pulse, 0.5 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_ship(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
var nose: Vector2 = center + Vector2(0, -11) * s
|
|
|
|
|
var left: Vector2 = center + Vector2(-7, 8) * s
|
|
|
|
|
var right: Vector2 = center + Vector2(7, 8) * s
|
|
|
|
|
var points: PackedVector2Array = PackedVector2Array([nose, left, right])
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, points, _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_arrow(center: Vector2, pulse: float, direction: float, s: float) -> void:
|
|
|
|
|
var r: float = 9.0 * s
|
|
|
|
|
var tip: Vector2 = center + Vector2(0, r * direction)
|
|
|
|
|
var shaft_top: Vector2 = center + Vector2(0, -r * direction)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, shaft_top, tip, _active_color(), pulse, 0.5 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, tip, tip + Vector2(-r * 0.7, -r * 0.8 * direction), _active_color(), pulse, 0.5 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, tip, tip + Vector2(r * 0.7, -r * 0.8 * direction), _active_color(), pulse, 0.5 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_build(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# A plus inside a frame - "add a structure".
|
|
|
|
|
var r: float = 11.0 * s
|
|
|
|
|
var frame: PackedVector2Array = PackedVector2Array([
|
|
|
|
|
center + Vector2(-r, -r), center + Vector2(r, -r),
|
|
|
|
|
center + Vector2(r, r), center + Vector2(-r, r),
|
|
|
|
|
])
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, frame, _active_color(), pulse, core_color)
|
|
|
|
|
var arm: float = r * 0.55
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-arm, 0), center + Vector2(arm, 0), _active_color(), pulse, 0.4 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(0, -arm), center + Vector2(0, arm), _active_color(), pulse, 0.4 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_rotate(center: Vector2, pulse: float, s: float, direction: float) -> void:
|
|
|
|
|
# Three-quarter arc with an arrowhead on the leading end, mirrored for
|
|
|
|
|
# the counter-clockwise variant.
|
|
|
|
|
var r: float = 11.0 * s
|
|
|
|
|
var segments: int = 14
|
|
|
|
|
var sweep: float = TAU * 0.75
|
|
|
|
|
var start: float = -PI * 0.5
|
|
|
|
|
var arc: PackedVector2Array = PackedVector2Array()
|
|
|
|
|
for i in range(segments + 1):
|
|
|
|
|
var t: float = float(i) / segments
|
|
|
|
|
var a: float = start + sweep * t * direction
|
|
|
|
|
arc.append(center + Vector2(cos(a), sin(a)) * r)
|
|
|
|
|
for i in range(segments):
|
|
|
|
|
NeonDraw.draw_glowing_line(self, arc[i], arc[i + 1], _active_color(), pulse, 0.34 * s, core_color)
|
|
|
|
|
|
|
|
|
|
var tip: Vector2 = arc[arc.size() - 1]
|
|
|
|
|
var tangent: Vector2 = (tip - arc[arc.size() - 2]).normalized()
|
|
|
|
|
var normal: Vector2 = Vector2(-tangent.y, tangent.x)
|
|
|
|
|
var head: float = 5.0 * s
|
|
|
|
|
NeonDraw.draw_glowing_line(self, tip, tip - tangent * head + normal * head, _active_color(), pulse, 0.34 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, tip, tip - tangent * head - normal * head, _active_color(), pulse, 0.34 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- building glyphs, each echoing the placed structure's silhouette ------
|
|
|
|
|
|
|
|
|
|
func _draw_mothership_icon(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# Radial flagship: a reactor core with six swept talons around it, the
|
|
|
|
|
# same silhouette as the placed structure.
|
|
|
|
|
for i in range(6):
|
|
|
|
|
var base: float = (float(i) / 6.0) * TAU
|
|
|
|
|
var talon: PackedVector2Array = PackedVector2Array()
|
|
|
|
|
for step in [0.0, 0.5, 1.0]:
|
|
|
|
|
var a: float = base + 0.72 * pow(step, 1.3)
|
|
|
|
|
talon.append(center + Vector2(cos(a), sin(a)) * lerpf(7.0, 16.0, step) * s)
|
|
|
|
|
for step in [1.0, 0.5, 0.0]:
|
|
|
|
|
var a: float = base + 0.72 * pow(step, 1.3) - (0.34 * (1.0 - step) + 0.05)
|
|
|
|
|
talon.append(center + Vector2(cos(a), sin(a)) * lerpf(7.0, 16.0, step) * s)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, talon, _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
for radius in [6.5, 3.0]:
|
|
|
|
|
var ring: PackedVector2Array = PackedVector2Array()
|
|
|
|
|
for i in range(6):
|
|
|
|
|
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
|
|
|
|
|
ring.append(center + Vector2(cos(a), sin(a)) * radius * s)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, ring, _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
|
Transport Hub building
Forgejo #18, part 5 of #12. The building only - the vessels that do the
hauling are #19. Placeable, costed at 30, gated behind a new "Freight
Handling" research node hanging off Logistics, with its own info popup,
build-menu glyph and map marker.
Drawn as an octagonal dock ring with berth arms and a squared-off freight
core: deliberately radial-but-flat so it does not read as a small
mothership, which is the only structure with swept arms. Its stationed
transports are NeonPiece children of one rotating holder, per #10 - the
geometry is rasterised once and only the holder's rotation changes, so
the traffic costs no redraws.
Building._draw()'s match has a catch-all that renders anything
unrecognised as a ship crafter, so a missing case is silent and looks
almost right. The test checks for the hub's own _hub_traffic holder
rather than trusting that something plausible appeared - which caught
exactly that: a _ready() patch had silently not applied and the hub was
rendering through the crafter branch.
The build menu grew a third row. Every button, label and price moved:
the old row occupied 332px against a 372px pitch, and three of those do
not fit above a 1280px screen. Rows are now shorter (128px buttons, 40px
price digits) at a 290px pitch, ending at y=1176. Rendered and looked at
- the first attempt tightened the pitch alone and left every price
overlapping the row beneath it, which no headless test would have seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:45:07 +02:00
|
|
|
func _draw_transport_hub_icon(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# A ring of docking cradles with a freight box in the middle: reads as
|
|
|
|
|
# "things are stationed here and go out from here", which is what the hub
|
|
|
|
|
# does, and shares no silhouette with the crafter's open cradle.
|
|
|
|
|
var ring_r: float = 11.0 * s
|
|
|
|
|
var ring: PackedVector2Array = PackedVector2Array()
|
|
|
|
|
for i in range(8):
|
|
|
|
|
var a: float = (float(i) / 8.0) * TAU + PI / 8.0
|
|
|
|
|
ring.append(center + Vector2(cos(a), sin(a)) * ring_r)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, ring, _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
var box: float = 4.2 * s
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
|
|
|
|
|
center + Vector2(-box, -box), center + Vector2(box, -box),
|
|
|
|
|
center + Vector2(box, box), center + Vector2(-box, box),
|
|
|
|
|
]), _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
# Three stubby berths, so the ring reads as occupied rather than as a
|
|
|
|
|
# plain octagon.
|
|
|
|
|
for i in range(3):
|
|
|
|
|
var a: float = (float(i) / 3.0) * TAU - PI / 2.0
|
|
|
|
|
var dir: Vector2 = Vector2(cos(a), sin(a))
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + dir * (box * 1.5),
|
|
|
|
|
center + dir * (ring_r - 1.5 * s), _active_color(), pulse, 0.35 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
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 _draw_crafter_icon(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# Reactor core, truss spine, and an open cradle holding a hull.
|
|
|
|
|
var core_centre: Vector2 = center + Vector2(-11, 0) * s
|
|
|
|
|
var core: PackedVector2Array = PackedVector2Array()
|
|
|
|
|
for i in range(6):
|
|
|
|
|
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
|
|
|
|
|
core.append(core_centre + Vector2(cos(a), sin(a)) * 5.0 * s)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, core, _active_color(), pulse, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-6, 0) * s,
|
|
|
|
|
center + Vector2(-2, 0) * s, _active_color(), pulse, 0.35 * s, core_color)
|
|
|
|
|
|
|
|
|
|
var bay: Vector2 = center + Vector2(6, 0) * s
|
|
|
|
|
var cradle: PackedVector2Array = PackedVector2Array()
|
|
|
|
|
for i in range(13):
|
|
|
|
|
var a: float = lerpf(-0.25 * PI, 1.25 * PI, float(i) / 12.0)
|
|
|
|
|
cradle.append(bay + Vector2(cos(a), sin(a)) * 10.0 * s)
|
|
|
|
|
NeonDraw.draw_glowing_path(self, cradle, _active_color(), pulse, 0.4 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
|
|
|
|
|
bay + Vector2(0, -6) * s, bay + Vector2(-5, 4) * s, bay + Vector2(5, 4) * s,
|
|
|
|
|
]), _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_command_icon(center: Vector2, pulse: float, s: float) -> void:
|
Skill tree, node-based building animation, bigger UI, and a 19x faster test suite
Gameplay and UI
- Research tree at the mothership gates which structures can be built and
grants permanent fleet-capacity upgrades (GameState.SKILL_TREE,
SkillTreeView). Node depth is derived from prerequisites, so the layout
cannot drift out of step with the dependencies.
- Building info popup before committing to a build, with per-building cost
and description.
- Ship crafter redrawn as a space factory with an assembly animation;
mothership redrawn as a radial flagship; command centre as a plotting hub.
- Keyboard and mouse support (WASD, Q/E pivot, wheel zoom) for the web build.
- UI and font scaled up for phone use, via a custom Theme resource -
gui/theme/default_font_size does not reach the default theme.
- Asteroids no longer spawn on top of the fleet's start position.
- Fleet formation origin derived from the layout's own offsets rather than
the centroid, fixing the lurch when a lopsided formation started moving.
- Fleet.stop() brakes from the ship's TRUE motion (velocity +
fleet_velocity). Ship zeroes its velocity when seated in its slot even
while the anchor drags that slot forward, so braking from velocity alone
stopped the fleet dead instead of coasting - and whether it did depended
on the frame rate.
Animation architecture (issue #10)
- Buildings, projectiles and explosion VFX no longer animate by redrawing.
Static geometry is rasterised once in _draw(); moving parts are NeonPiece
children animated through node properties, which reuse the cached
geometry. Every NeonDraw call expands into four antialiased polylines, so
redrawing to move something re-tessellated all of it.
Test suite (issue #8): 1075s -> ~55s
- Runs from a copy on Termux's own filesystem. The project lives on
Android's FUSE bridge, read through proot, where load("res://Main.tscn")
alone cost 8.5s per test against ~2s natively - and nearly every test
loads it.
- Runs 6 tests at a time, in a single proot-distro login, with output
buffered per test and printed only on failure.
- --fixed-fps 60 pins delta instead of measuring the wall clock, so a test's
result no longer depends on how busy the machine is. Without it,
parallelism made two tests fail on 2 of 3 runs.
- A test passes only if it both exits 0 and prints ": PASS".
- 38 tests, green on three consecutive runs.
Bundles Carrois Gothic SC (Apache 2.0) with its licence text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:00:41 +02:00
|
|
|
# Hex plotting rim with spokes to a diamond hub, echoing the placed
|
|
|
|
|
# structure's silhouette: half the ports carry a formation token.
|
|
|
|
|
var hex_r: float = 11.0 * s
|
|
|
|
|
var hub_r: float = 4.0 * s
|
|
|
|
|
var hex: PackedVector2Array = PackedVector2Array()
|
|
|
|
|
for i in range(6):
|
|
|
|
|
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
|
|
|
|
|
hex.append(center + Vector2(cos(a), sin(a)) * hex_r)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, hex, _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
for i in range(6):
|
|
|
|
|
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
|
|
|
|
|
var dir: Vector2 = Vector2(cos(a), sin(a))
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + dir * hub_r, center + dir * hex_r, _active_color(), pulse, 0.32 * s, core_color)
|
|
|
|
|
if i % 2 == 0:
|
|
|
|
|
var tip: Vector2 = center + dir * hex_r
|
|
|
|
|
var t: float = 2.6 * s
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
|
|
|
|
|
tip + Vector2(0, -t), tip + Vector2(t, 0), tip + Vector2(0, t), tip + Vector2(-t, 0),
|
|
|
|
|
]), _active_color(), pulse, core_color)
|
|
|
|
|
|
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
|
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
|
Skill tree, node-based building animation, bigger UI, and a 19x faster test suite
Gameplay and UI
- Research tree at the mothership gates which structures can be built and
grants permanent fleet-capacity upgrades (GameState.SKILL_TREE,
SkillTreeView). Node depth is derived from prerequisites, so the layout
cannot drift out of step with the dependencies.
- Building info popup before committing to a build, with per-building cost
and description.
- Ship crafter redrawn as a space factory with an assembly animation;
mothership redrawn as a radial flagship; command centre as a plotting hub.
- Keyboard and mouse support (WASD, Q/E pivot, wheel zoom) for the web build.
- UI and font scaled up for phone use, via a custom Theme resource -
gui/theme/default_font_size does not reach the default theme.
- Asteroids no longer spawn on top of the fleet's start position.
- Fleet formation origin derived from the layout's own offsets rather than
the centroid, fixing the lurch when a lopsided formation started moving.
- Fleet.stop() brakes from the ship's TRUE motion (velocity +
fleet_velocity). Ship zeroes its velocity when seated in its slot even
while the anchor drags that slot forward, so braking from velocity alone
stopped the fleet dead instead of coasting - and whether it did depended
on the frame rate.
Animation architecture (issue #10)
- Buildings, projectiles and explosion VFX no longer animate by redrawing.
Static geometry is rasterised once in _draw(); moving parts are NeonPiece
children animated through node properties, which reuse the cached
geometry. Every NeonDraw call expands into four antialiased polylines, so
redrawing to move something re-tessellated all of it.
Test suite (issue #8): 1075s -> ~55s
- Runs from a copy on Termux's own filesystem. The project lives on
Android's FUSE bridge, read through proot, where load("res://Main.tscn")
alone cost 8.5s per test against ~2s natively - and nearly every test
loads it.
- Runs 6 tests at a time, in a single proot-distro login, with output
buffered per test and printed only on failure.
- --fixed-fps 60 pins delta instead of measuring the wall clock, so a test's
result no longer depends on how busy the machine is. Without it,
parallelism made two tests fail on 2 of 3 runs.
- A test passes only if it both exits 0 and prints ": PASS".
- 38 tests, green on three consecutive runs.
Bundles Carrois Gothic SC (Apache 2.0) with its licence text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:00:41 +02:00
|
|
|
center + Vector2(0, -hub_r), center + Vector2(hub_r, 0),
|
|
|
|
|
center + Vector2(0, hub_r), center + Vector2(-hub_r, 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
|
|
|
]), _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_extractor_icon(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# A derrick standing on a planet's limb, with ore lifting off it - the
|
|
|
|
|
# one building that only works over a planet, so the planet is the point.
|
|
|
|
|
var limb: PackedVector2Array = PackedVector2Array()
|
|
|
|
|
for i in range(11):
|
|
|
|
|
var a: float = lerpf(-0.82 * PI, -0.18 * PI, float(i) / 10.0)
|
|
|
|
|
limb.append(center + (Vector2(0, 22) + Vector2(cos(a), sin(a)) * 17.0) * s)
|
|
|
|
|
NeonDraw.draw_glowing_path(self, limb, _active_color(), pulse, 0.45 * s, core_color)
|
|
|
|
|
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-6, 5) * s, center + Vector2(0, -9) * s, _active_color(), pulse, 0.4 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(6, 5) * s, center + Vector2(0, -9) * s, _active_color(), pulse, 0.4 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-3.5, -1) * s, center + Vector2(3.5, -1) * s, _active_color(), pulse, 0.35 * s, core_color)
|
|
|
|
|
# Shaft driven down through the limb - without it the derrick reads as a
|
|
|
|
|
# tower standing on a hill rather than as something mining the planet.
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(0, -1) * s, center + Vector2(0, 14) * s, _active_color(), pulse, 0.3 * s, core_color)
|
|
|
|
|
|
|
|
|
|
var ore_centre: Vector2 = center + Vector2(0, -15) * s
|
|
|
|
|
var ore: PackedVector2Array = PackedVector2Array()
|
|
|
|
|
for i in range(6):
|
|
|
|
|
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
|
|
|
|
|
ore.append(ore_centre + Vector2(cos(a), sin(a)) * 4.0 * s)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, ore, _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_map(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
var w: float = 11.0 * s
|
|
|
|
|
var h: float = 9.0 * s
|
|
|
|
|
var outline: PackedVector2Array = PackedVector2Array([
|
|
|
|
|
center + Vector2(-w, -h), center + Vector2(w, -h * 0.7),
|
|
|
|
|
center + Vector2(w, h), center + Vector2(-w, h * 0.7),
|
|
|
|
|
])
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, outline, _active_color(), pulse, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-w * 0.34, -h * 0.9), center + Vector2(-w * 0.34, h * 0.77), _active_color(), pulse, 0.4 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(w * 0.34, -h * 0.77), center + Vector2(w * 0.34, h * 0.9), _active_color(), pulse, 0.4 * s, core_color)
|