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.
|
Ship Lab: per-vessel research, and the shield vessel
Forgejo #5. A new building gated behind a "Vessel Science" research
node. Its own screen upgrades each vessel type - weapon, movement,
shield and freight - and unlocks the shield vessel, which the ship
crafter can then build.
Vessel research is deliberately NOT more nodes in the mothership tree:
that tree gates what may be built at all and is a strategic spine, while
these are repeatable tuning on vessels already owned. Levels cost
base * (level + 1), so the third point in something is not the price of
the first. Upgrades are read live rather than baked in at build time, so
researching one improves the ships already flying.
The shield is personal, not a bubble: it absorbs a hit that would
destroy the vessel - ships have no health, contact kills - and is then
down until it cycles. Its ring is a NeonPiece child toggled with
`visible` per #10, not a redraw of the hull.
The build menu now holds six entries across three rows, which the #21
repacking already handles.
Enemies still have no weapon, so "blocks shots" has nothing to block
yet; that is filed separately rather than widened into this ticket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:31:49 +02:00
|
|
|
MOTHERSHIP, CRAFTER, COMMAND_CENTER, EXTRACTOR, TRANSPORT_HUB, SHIP_LAB,
|
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)
|
Ship Lab: per-vessel research, and the shield vessel
Forgejo #5. A new building gated behind a "Vessel Science" research
node. Its own screen upgrades each vessel type - weapon, movement,
shield and freight - and unlocks the shield vessel, which the ship
crafter can then build.
Vessel research is deliberately NOT more nodes in the mothership tree:
that tree gates what may be built at all and is a strategic spine, while
these are repeatable tuning on vessels already owned. Levels cost
base * (level + 1), so the third point in something is not the price of
the first. Upgrades are read live rather than baked in at build time, so
researching one improves the ships already flying.
The shield is personal, not a bubble: it absorbs a hit that would
destroy the vessel - ships have no health, contact kills - and is then
down until it cycles. Its ring is a NeonPiece child toggled with
`visible` per #10, not a redraw of the hull.
The build menu now holds six entries across three rows, which the #21
repacking already handles.
Enemies still have no weapon, so "blocks shots" has nothing to block
yet; that is filed separately rather than widened into this ticket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:31:49 +02:00
|
|
|
Icon.SHIP_LAB:
|
|
|
|
|
_draw_ship_lab_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)
|
|
|
|
|
|
|
|
|
|
|
Ship Lab: per-vessel research, and the shield vessel
Forgejo #5. A new building gated behind a "Vessel Science" research
node. Its own screen upgrades each vessel type - weapon, movement,
shield and freight - and unlocks the shield vessel, which the ship
crafter can then build.
Vessel research is deliberately NOT more nodes in the mothership tree:
that tree gates what may be built at all and is a strategic spine, while
these are repeatable tuning on vessels already owned. Levels cost
base * (level + 1), so the third point in something is not the price of
the first. Upgrades are read live rather than baked in at build time, so
researching one improves the ships already flying.
The shield is personal, not a bubble: it absorbs a hit that would
destroy the vessel - ships have no health, contact kills - and is then
down until it cycles. Its ring is a NeonPiece child toggled with
`visible` per #10, not a redraw of the hull.
The build menu now holds six entries across three rows, which the #21
repacking already handles.
Enemies still have no weapon, so "blocks shots" has nothing to block
yet; that is filed separately rather than widened into this ticket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:31:49 +02:00
|
|
|
func _draw_ship_lab_icon(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# A vessel inside brackets: something being examined, not built.
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
|
|
|
|
|
center + Vector2(0, -9) * s, center + Vector2(5, 5) * s,
|
|
|
|
|
center + Vector2(0, 2) * s, center + Vector2(-5, 5) * s,
|
|
|
|
|
]), _active_color(), pulse, core_color)
|
|
|
|
|
for sx in [-1.0, 1.0]:
|
|
|
|
|
var x: float = sx * 11.0 * s
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(x, -11 * s), center + Vector2(x, 11 * s),
|
|
|
|
|
_active_color(), pulse, 0.35 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(x, -11 * s), center + Vector2(x - sx * 4.0 * s, -11 * s),
|
|
|
|
|
_active_color(), pulse, 0.35 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(x, 11 * s), center + Vector2(x - sx * 4.0 * s, 11 * s),
|
|
|
|
|
_active_color(), pulse, 0.35 * s, 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)
|