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,
|
2026-08-12 13:07:52 +02:00
|
|
|
# Moving ore between the fleet and a building (#24). Two arrows passing
|
|
|
|
|
# each other: the single up/down arrows already mean "load" and "store"
|
|
|
|
|
# one way each, and this button is the place where both live.
|
|
|
|
|
TRANSFER,
|
2026-08-13 20:13:32 +02:00
|
|
|
# Ship-lab upgrade glyphs. These deliberately describe the effect rather
|
|
|
|
|
# than the building hosting it, so cards remain scannable at a glance.
|
|
|
|
|
UPGRADE_DAMAGE, UPGRADE_RATE, UPGRADE_RANGE, UPGRADE_THRUSTERS,
|
|
|
|
|
UPGRADE_SHIELD_HITS, UPGRADE_SHIELD_RECHARGE, UPGRADE_CARGO,
|
|
|
|
|
UPGRADE_ENGINES, UPGRADE_UNLOCK_SHIELD,
|
2026-08-13 20:49:15 +02:00
|
|
|
ACTION_MENU,
|
2026-08-13 22:17:24 +02:00
|
|
|
GAS,
|
2026-08-14 11:54:31 +02:00
|
|
|
LEFT_ARROW, RIGHT_ARROW,
|
2026-08-14 13:44:10 +02:00
|
|
|
MINUS, PLUS,
|
2026-08-14 22:46:30 +02:00
|
|
|
SHIELD_VESSEL,
|
2026-08-15 04:45:15 +02:00
|
|
|
GAS_BOMBER, GAS_BOMB,
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
# Appended, not inserted alongside the other building icons above -
|
|
|
|
|
# inserting there would silently renumber every icon int already
|
|
|
|
|
# stored in existing scenes (#54).
|
|
|
|
|
GAS_BURNER,
|
2026-08-16 16:20:39 +02:00
|
|
|
# Same append-only rule (#57): a wide hauler hull, distinct from every
|
|
|
|
|
# vessel glyph above.
|
|
|
|
|
CARRIER_VESSEL,
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
# #58: LASER_VESSEL is the ship glyph, LASER_SHOT the action button -
|
|
|
|
|
# same two-icon split as GAS_BOMBER/GAS_BOMB above.
|
|
|
|
|
LASER_VESSEL, LASER_SHOT,
|
Implement #77, #78 and #79
#77 - the Distorder becomes the Energy Propulser. Renamed everywhere the
player reads it, reskinned to match: a symmetric hull flaring into an engine
skirt instead of the lopsided warped lump, an engine bell with a thrust plume
instead of the wobbling ring glyph, and energy's own yellow instead of purple,
since energy is what it burns. The internal id stays "distorder" - it is the
capability string, the unlock key and the component scene name, and renaming
the wiring would be a large mechanical change with no player-visible effect.
Its speed ceiling now grows with the LOGARITHM of how many fly. Each one used
to add a flat 1.0, so a fleet that could afford six simply drove at six times
cruise and the vessel stopped being a choice. Base-2 makes the rule one
sentence - every doubling adds another 1.0 - and leaves two propulsers worth
exactly what they were worth before, so nothing already built is devalued.
#78 - a gas burner can be paused. It converts gas the player may want for
bombs into energy they may not need yet, and the only way to stop it was to
demolish it. The action is data-driven like every other: the visual's
ActionProvider lists "pause", and any converter can adopt it. Pause is the
burner's PRIMARY action, so it is one tap rather than a menu; the button shows
what it will DO, bars while running and a play triangle while stopped; and the
building itself goes visibly cold, because a paused building that looked
identical to a running one would be a setting to remember rather than see.
Conversion progress is left where it was rather than reset, so switching off
and on again does not discard a part-finished unit.
#79 - picking something up says so. A "+N" with the resource's own mark rises
and fades where it happened. Built from the SAME glyph the resource tables and
transfer rows use, so a material added to the catalog later cannot end up with
one mark in the HUD and a different one in the world. A pickup a full hold
REFUSES raises nothing.
Two Godot traps cost real time here and are now written down where the next
person will hit them:
- a `#` comment inside a .tres [resource] block is not a comment. It parses
as part of the preceding value and silently blanks the property after it.
One in DistorderVesselDefinition.tres left definition_id empty, which
surfaced as "<blank> did not resolve its definition" in a test far from
the edit. The note lives in VesselDefinition.gd now.
- a global class_name is not reliably resolvable, INCLUDING a script's own
inside its own static function. ResourcePickupVFX.new() failed to compile
when reached through a preview scene, taking Rock and the gas vents with
it. It has no class_name now and constructs itself by path.
Suite 114/114.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:38:30 +02:00
|
|
|
# #59 drew this as a warped ring, for a vessel then called a space-time
|
|
|
|
|
# distorder. #77 renamed it the Energy Propulser and the glyph is now an
|
|
|
|
|
# engine bell; the enum name stays put because renumbering it would
|
|
|
|
|
# silently renumber every icon int stored in every scene.
|
2026-08-16 17:59:04 +02:00
|
|
|
DISTORDER_VESSEL,
|
Implement #77, #78 and #79
#77 - the Distorder becomes the Energy Propulser. Renamed everywhere the
player reads it, reskinned to match: a symmetric hull flaring into an engine
skirt instead of the lopsided warped lump, an engine bell with a thrust plume
instead of the wobbling ring glyph, and energy's own yellow instead of purple,
since energy is what it burns. The internal id stays "distorder" - it is the
capability string, the unlock key and the component scene name, and renaming
the wiring would be a large mechanical change with no player-visible effect.
Its speed ceiling now grows with the LOGARITHM of how many fly. Each one used
to add a flat 1.0, so a fleet that could afford six simply drove at six times
cruise and the vessel stopped being a choice. Base-2 makes the rule one
sentence - every doubling adds another 1.0 - and leaves two propulsers worth
exactly what they were worth before, so nothing already built is devalued.
#78 - a gas burner can be paused. It converts gas the player may want for
bombs into energy they may not need yet, and the only way to stop it was to
demolish it. The action is data-driven like every other: the visual's
ActionProvider lists "pause", and any converter can adopt it. Pause is the
burner's PRIMARY action, so it is one tap rather than a menu; the button shows
what it will DO, bars while running and a play triangle while stopped; and the
building itself goes visibly cold, because a paused building that looked
identical to a running one would be a setting to remember rather than see.
Conversion progress is left where it was rather than reset, so switching off
and on again does not discard a part-finished unit.
#79 - picking something up says so. A "+N" with the resource's own mark rises
and fades where it happened. Built from the SAME glyph the resource tables and
transfer rows use, so a material added to the catalog later cannot end up with
one mark in the HUD and a different one in the world. A pickup a full hold
REFUSES raises nothing.
Two Godot traps cost real time here and are now written down where the next
person will hit them:
- a `#` comment inside a .tres [resource] block is not a comment. It parses
as part of the preceding value and silently blanks the property after it.
One in DistorderVesselDefinition.tres left definition_id empty, which
surfaced as "<blank> did not resolve its definition" in a test far from
the edit. The note lives in VesselDefinition.gd now.
- a global class_name is not reliably resolvable, INCLUDING a script's own
inside its own static function. ResourcePickupVFX.new() failed to compile
when reached through a preview scene, taking Rock and the gas vents with
it. It has no class_name now and constructs itself by path.
Suite 114/114.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:38:30 +02:00
|
|
|
# #78: switching a converter off and on. Appended, like everything above -
|
|
|
|
|
# inserting anywhere earlier renumbers every icon int already stored in a
|
|
|
|
|
# scene file.
|
|
|
|
|
PAUSE, RESUME,
|
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)
|
2026-08-13 20:56:24 +02:00
|
|
|
# Selection is an additional frame, never a recolour. It lets menus retain
|
|
|
|
|
# semantic colours (vessel class, danger, cargo) while still making the
|
|
|
|
|
# active choice unmistakable.
|
|
|
|
|
@export var highlighted: bool = false: set = set_highlighted
|
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
|
|
|
# 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()
|
|
|
|
|
|
|
|
|
|
|
2026-08-13 20:56:24 +02:00
|
|
|
func set_highlighted(value: bool) -> void:
|
|
|
|
|
if highlighted == value:
|
|
|
|
|
return
|
|
|
|
|
highlighted = value
|
|
|
|
|
queue_redraw()
|
|
|
|
|
|
|
|
|
|
|
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 _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"):
|
Fix every button being permanently blocked after closing the build menu
The real cause of "the crafter buttons do nothing", reported three
times. IconButton._blocked_by_modal tested `menu.visible`, but hiding a
parent leaves a child's own `visible` flag true - and the build menu's
info popup is itself one of the modal panels. So opening any building's
info popup and then closing the build menu with the X left an invisible
popup still claiming to be open, and from that moment EVERY button in
the game was blocked, with no way for a player to get out of it.
It tests is_visible_in_tree() now, and closing the build menu also
closes its popup rather than leaving it half-open.
My earlier attempts kept missing it because the probes called the
handlers directly - they exercised the crafter logic, which was fine all
along, and never the input path in that state. The regression test
reproduces the exact sequence: open the build menu, show a building's
info, hide the menu, then check an unrelated button still fires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:41:00 +02:00
|
|
|
# is_visible_in_tree(), NOT visible. Hiding a parent leaves the
|
|
|
|
|
# child's own `visible` flag true, and the build menu's info popup is
|
|
|
|
|
# itself a modal panel - so closing the build menu with the X left an
|
|
|
|
|
# invisible popup still claiming to be open, and every button in the
|
|
|
|
|
# game was blocked from then on. That is not a state a player can
|
|
|
|
|
# get out of.
|
|
|
|
|
if menu.is_visible_in_tree() and not menu.is_ancestor_of(self):
|
2026-08-11 20:17:58 +02:00
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
Fix the web build: a wasm trap on integer % 0, and buttons that never fired
The web export reached the main menu and went no further. Two separate bugs,
one hiding the other.
BUTTONS NEVER FIRED. A browser on a phone delivers the touch DOWN and never
the release. IconButton emits `pressed` only on release, deliberately, so
that dragging off cancels - so on the web it never emitted at all, on every
button in the game. fire_on_press (default OS.has_feature("web")) completes
the press there instead, and does not latch, since nothing is coming to
unlatch it. The cost is drag-off-to-cancel, which every other platform keeps.
Measured on the device rather than guessed: an on-screen probe reported
`touch=1 mouse=1 held=y` after one tap - one down, one emulated mouse down,
the button latched through ordinary GUI routing, then nothing.
THE ENGINE THEN TRAPPED. With the button working, pressing Play loaded and
built Main.tscn and the whole engine stopped mid-frame, page still alive.
An error overlay in a custom HTML shell finally showed why:
Uncaught RuntimeError: remainder by zero
An integer % 0. Wasm TRAPS on that; native builds silently tolerate it. So
the bug was equally present on Android and in all 100 tests, doing nothing
in both - which is why nothing runnable here could ever have caught it, and
why reasoning from frozen screenshots got nowhere for several rounds. Every
variable modulo is now guarded: the hangar grid's `i % columns`, planet
naming's `% names.size()`, and two in the map editor.
WHAT IS KEPT, AND WHY
- web_shell.html mirrors console errors onto the page, hidden until one
occurs. Godot's errors go to the browser console, which is unreachable on
a phone; this found in one shot what inference had failed to find all
evening. It costs nothing when there is nothing to report.
- MainMenu._start does load -> instantiate -> add_child by hand instead of
change_scene_to_file. That call does all three opaquely, so a failure is
indistinguishable from an unpressed button; split, each step reports.
- PlanetAtmosphereController defers the forced world populate out of the
frame that builds Main.tscn. It did not fix the hang - the trap did - but
25 chunks of asteroids and 49 of planets do not belong in the one frame
that already constructs 197 nodes, and nothing can paint until it ends.
Removed the direct-input-routing experiment and its test: it was written on
the theory that touches were not reaching Controls, which `held=y` later
disproved, and its set_input_as_handled() swallowed the very events the
probe was reading.
Full suite green at 100/100.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 01:08:09 +02:00
|
|
|
# Complete the press immediately instead of waiting for a release.
|
|
|
|
|
#
|
|
|
|
|
# Measured on the device, in the web build: an on-screen probe reported
|
|
|
|
|
# `touch=1 mouse=1 held=y` after one tap - one touch down, one emulated mouse
|
|
|
|
|
# down, the button latched into its held state through ordinary GUI routing,
|
|
|
|
|
# and then nothing. The release is never delivered. A button that only emits
|
|
|
|
|
# on release therefore never emits at all.
|
|
|
|
|
#
|
|
|
|
|
# GUI routing itself works fine; `held=y` is the proof. An earlier attempt to
|
|
|
|
|
# bypass it made things worse and hid the evidence.
|
|
|
|
|
#
|
|
|
|
|
# The cost is the drag-off-to-cancel affordance below. Everywhere that
|
|
|
|
|
# delivers releases keeps it.
|
|
|
|
|
@export var fire_on_press: bool = OS.has_feature("web")
|
|
|
|
|
|
Twin-stick combat: aim and fire with a second stick, MOBA-style ability ring
The fleet's guns picked their own targets and fired on their own, so combat
happened whether or not the player did anything - there was nothing to aim and
nothing to decide, and the right half of the screen held nothing at all.
Firing is now an action. VesselWeaponComponent no longer seeks a target; it
shoots along a heading, and the only thing that supplies one is the new
right-hand stick via FireStickController. A fleet arriving from another scene
starts with its guns silent, which is the correct resting state.
A subtle dashed guide runs from the fleet along the aimed heading while the
stick is held, out to the guns' OWN range - read from a weapon that is
actually aboard, so the line can never promise reach the fleet does not have.
Layout: the two sticks take the bottom corners, the pivot pair is centred
between them at the bottom edge, and the laser and gas bomb sit on an arc up
and left of the fire stick where a thumb already is. Those four are now round
- IconButton grew a round_frame mode, and the in-world action controls use it
while panel and menu buttons stay square, so the two kinds of button read as
different kinds of thing.
Worth recording, because it cost a screenshot to find: a script-declared
property set BEFORE the `script = ExtResource(...)` line in a .tscn is
silently dropped. Godot's parser has no idea the property exists yet. The
round frames were all correctly set in both scenes and none of them applied.
Also fixed researtch_lab_garrison.tres, which was authored under the void
profile - that is the black hole surface, so a lab garrison was spawning on
black holes.
Suite 110/110. test_enemy_combat's whole premise was auto-fire; it now holds
the real stick toward the enemy rather than poking weapon components, so it
exercises the path the player uses. test_fire_stick is new and covers the
change itself: an idle stick leaves a target parked well inside range
untouched, which is precisely what the old gun would never have done.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:29:21 +02:00
|
|
|
# Round rather than square. The in-world action controls - the abilities
|
|
|
|
|
# arranged around the fire stick, and the pivot pair - read as a different
|
|
|
|
|
# kind of thing from the square panel and menu buttons, and a round target is
|
|
|
|
|
# what a thumb reaching for an ability expects. Purely how the frame is drawn;
|
|
|
|
|
# the hit area is still the whole rect.
|
|
|
|
|
@export var round_frame: bool = false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# The frame outline, inset by `inset` pixels on every side. Round buttons trace
|
|
|
|
|
# a circle through the same inset box, so a round and a square button of the
|
|
|
|
|
# same size present the same footprint.
|
|
|
|
|
func _frame_points(inset: float) -> PackedVector2Array:
|
|
|
|
|
if not round_frame:
|
|
|
|
|
return PackedVector2Array([
|
|
|
|
|
Vector2(inset, inset),
|
|
|
|
|
Vector2(size.x - inset, inset),
|
|
|
|
|
Vector2(size.x - inset, size.y - inset),
|
|
|
|
|
Vector2(inset, size.y - inset),
|
|
|
|
|
])
|
|
|
|
|
var centre: Vector2 = size / 2.0
|
|
|
|
|
var radius: Vector2 = centre - Vector2(inset, inset)
|
|
|
|
|
var points: PackedVector2Array = PackedVector2Array()
|
|
|
|
|
for i in range(32):
|
|
|
|
|
var angle: float = TAU * float(i) / 32.0
|
|
|
|
|
points.append(centre + Vector2(cos(angle) * radius.x, sin(angle) * radius.y))
|
|
|
|
|
return points
|
|
|
|
|
|
Fix the web build: a wasm trap on integer % 0, and buttons that never fired
The web export reached the main menu and went no further. Two separate bugs,
one hiding the other.
BUTTONS NEVER FIRED. A browser on a phone delivers the touch DOWN and never
the release. IconButton emits `pressed` only on release, deliberately, so
that dragging off cancels - so on the web it never emitted at all, on every
button in the game. fire_on_press (default OS.has_feature("web")) completes
the press there instead, and does not latch, since nothing is coming to
unlatch it. The cost is drag-off-to-cancel, which every other platform keeps.
Measured on the device rather than guessed: an on-screen probe reported
`touch=1 mouse=1 held=y` after one tap - one down, one emulated mouse down,
the button latched through ordinary GUI routing, then nothing.
THE ENGINE THEN TRAPPED. With the button working, pressing Play loaded and
built Main.tscn and the whole engine stopped mid-frame, page still alive.
An error overlay in a custom HTML shell finally showed why:
Uncaught RuntimeError: remainder by zero
An integer % 0. Wasm TRAPS on that; native builds silently tolerate it. So
the bug was equally present on Android and in all 100 tests, doing nothing
in both - which is why nothing runnable here could ever have caught it, and
why reasoning from frozen screenshots got nowhere for several rounds. Every
variable modulo is now guarded: the hangar grid's `i % columns`, planet
naming's `% names.size()`, and two in the map editor.
WHAT IS KEPT, AND WHY
- web_shell.html mirrors console errors onto the page, hidden until one
occurs. Godot's errors go to the browser console, which is unreachable on
a phone; this found in one shot what inference had failed to find all
evening. It costs nothing when there is nothing to report.
- MainMenu._start does load -> instantiate -> add_child by hand instead of
change_scene_to_file. That call does all three opaquely, so a failure is
indistinguishable from an unpressed button; split, each step reports.
- PlanetAtmosphereController defers the forced world populate out of the
frame that builds Main.tscn. It did not fix the hang - the trap did - but
25 chunks of asteroids and 49 of planets do not belong in the one frame
that already constructs 197 nodes, and nothing can paint until it ends.
Removed the direct-input-routing experiment and its test: it was written on
the theory that touches were not reaching Controls, which `held=y` later
disproved, and its set_input_as_handled() swallowed the very events the
probe was reading.
Full suite green at 100/100.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 01:08:09 +02:00
|
|
|
|
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_press(is_pressed: bool, at_position: Vector2) -> void:
|
|
|
|
|
if is_pressed:
|
|
|
|
|
pressed_down = true
|
|
|
|
|
queue_redraw()
|
Fire buttons on press where the browser never delivers a release
Every button in the web build was dead - reported as "Play does nothing".
Diagnosed on the deployed build with a temporary on-screen input probe,
because none of it reproduces here: the suite passes, and pressing Play under
the real gl_compatibility renderer (the same path WebGL2 takes) reaches
Main.tscn fine. The probe reported, from a phone browser:
#6 touch down @ 362,781
play rect 296,725 128x128
hit=YES
Three taps, six events, every one a DOWN and not a single UP - and the button
left drawn in its bright held state, pressed_down stuck true because nothing
ever came to clear it. So touches arrive, their coordinates are correct, and
they land inside the button; the browser simply never delivers the release,
in _gui_input or in raw _input.
IconButton emits `pressed` only on release, deliberately, so that dragging
off a button cancels it. With no release that means it never emits at all -
on every button in the game, not just Play. Which also explains why the
earlier suspects went nowhere: the scene change was never reached, so it was
never the scene change, and hit=YES ruled out coordinate mapping.
fire_on_press defaults to OS.has_feature("web"). There, a press completes the
tap itself and does NOT latch, since nothing is coming to unlatch it. Every
other platform is untouched and keeps drag-off-to-cancel.
The cost, stated plainly: a web tap can no longer be cancelled by sliding off
before lifting, and that applies to destructive buttons too. The alternative -
keeping release-firing and treating a new press as completing the previous
one - preserves cancellation but leaves the very first tap dead, which is
worse.
tests/test_icon_button_press_fires.gd drives the flag directly, since a
headless Linux run cannot BE a web build: one tap emits exactly once, a late
release does not emit again, the button does not latch, and with the flag off
the release still drives the press and dragging off still cancels.
MainMenu keeps the load/start bisection introduced while chasing this - a
failed change_scene_to_packed otherwise leaves the menu up, which looks
exactly like an unpressed button.
Full suite green at 100/100.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:02:20 +02:00
|
|
|
if fire_on_press:
|
Fix the web build: a wasm trap on integer % 0, and buttons that never fired
The web export reached the main menu and went no further. Two separate bugs,
one hiding the other.
BUTTONS NEVER FIRED. A browser on a phone delivers the touch DOWN and never
the release. IconButton emits `pressed` only on release, deliberately, so
that dragging off cancels - so on the web it never emitted at all, on every
button in the game. fire_on_press (default OS.has_feature("web")) completes
the press there instead, and does not latch, since nothing is coming to
unlatch it. The cost is drag-off-to-cancel, which every other platform keeps.
Measured on the device rather than guessed: an on-screen probe reported
`touch=1 mouse=1 held=y` after one tap - one down, one emulated mouse down,
the button latched through ordinary GUI routing, then nothing.
THE ENGINE THEN TRAPPED. With the button working, pressing Play loaded and
built Main.tscn and the whole engine stopped mid-frame, page still alive.
An error overlay in a custom HTML shell finally showed why:
Uncaught RuntimeError: remainder by zero
An integer % 0. Wasm TRAPS on that; native builds silently tolerate it. So
the bug was equally present on Android and in all 100 tests, doing nothing
in both - which is why nothing runnable here could ever have caught it, and
why reasoning from frozen screenshots got nowhere for several rounds. Every
variable modulo is now guarded: the hangar grid's `i % columns`, planet
naming's `% names.size()`, and two in the map editor.
WHAT IS KEPT, AND WHY
- web_shell.html mirrors console errors onto the page, hidden until one
occurs. Godot's errors go to the browser console, which is unreachable on
a phone; this found in one shot what inference had failed to find all
evening. It costs nothing when there is nothing to report.
- MainMenu._start does load -> instantiate -> add_child by hand instead of
change_scene_to_file. That call does all three opaquely, so a failure is
indistinguishable from an unpressed button; split, each step reports.
- PlanetAtmosphereController defers the forced world populate out of the
frame that builds Main.tscn. It did not fix the hang - the trap did - but
25 chunks of asteroids and 49 of planets do not belong in the one frame
that already constructs 197 nodes, and nothing can paint until it ends.
Removed the direct-input-routing experiment and its test: it was written on
the theory that touches were not reaching Controls, which `held=y` later
disproved, and its set_input_as_handled() swallowed the very events the
probe was reading.
Full suite green at 100/100.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 01:08:09 +02:00
|
|
|
# Not latched: nothing is coming to clear it.
|
Fire buttons on press where the browser never delivers a release
Every button in the web build was dead - reported as "Play does nothing".
Diagnosed on the deployed build with a temporary on-screen input probe,
because none of it reproduces here: the suite passes, and pressing Play under
the real gl_compatibility renderer (the same path WebGL2 takes) reaches
Main.tscn fine. The probe reported, from a phone browser:
#6 touch down @ 362,781
play rect 296,725 128x128
hit=YES
Three taps, six events, every one a DOWN and not a single UP - and the button
left drawn in its bright held state, pressed_down stuck true because nothing
ever came to clear it. So touches arrive, their coordinates are correct, and
they land inside the button; the browser simply never delivers the release,
in _gui_input or in raw _input.
IconButton emits `pressed` only on release, deliberately, so that dragging
off a button cancels it. With no release that means it never emits at all -
on every button in the game, not just Play. Which also explains why the
earlier suspects went nowhere: the scene change was never reached, so it was
never the scene change, and hit=YES ruled out coordinate mapping.
fire_on_press defaults to OS.has_feature("web"). There, a press completes the
tap itself and does NOT latch, since nothing is coming to unlatch it. Every
other platform is untouched and keeps drag-off-to-cancel.
The cost, stated plainly: a web tap can no longer be cancelled by sliding off
before lifting, and that applies to destructive buttons too. The alternative -
keeping release-firing and treating a new press as completing the previous
one - preserves cancellation but leaves the very first tap dead, which is
worse.
tests/test_icon_button_press_fires.gd drives the flag directly, since a
headless Linux run cannot BE a web build: one tap emits exactly once, a late
release does not emit again, the button does not latch, and with the flag off
the release still drives the press and dragging off still cancels.
MainMenu keeps the load/start bisection introduced while chasing this - a
failed change_scene_to_packed otherwise leaves the menu up, which looks
exactly like an unpressed button.
Full suite green at 100/100.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:02:20 +02:00
|
|
|
pressed_down = false
|
|
|
|
|
pressed.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
|
|
|
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
|
Twin-stick combat: aim and fire with a second stick, MOBA-style ability ring
The fleet's guns picked their own targets and fired on their own, so combat
happened whether or not the player did anything - there was nothing to aim and
nothing to decide, and the right half of the screen held nothing at all.
Firing is now an action. VesselWeaponComponent no longer seeks a target; it
shoots along a heading, and the only thing that supplies one is the new
right-hand stick via FireStickController. A fleet arriving from another scene
starts with its guns silent, which is the correct resting state.
A subtle dashed guide runs from the fleet along the aimed heading while the
stick is held, out to the guns' OWN range - read from a weapon that is
actually aboard, so the line can never promise reach the fleet does not have.
Layout: the two sticks take the bottom corners, the pivot pair is centred
between them at the bottom edge, and the laser and gas bomb sit on an arc up
and left of the fire stick where a thumb already is. Those four are now round
- IconButton grew a round_frame mode, and the in-world action controls use it
while panel and menu buttons stay square, so the two kinds of button read as
different kinds of thing.
Worth recording, because it cost a screenshot to find: a script-declared
property set BEFORE the `script = ExtResource(...)` line in a .tscn is
silently dropped. Godot's parser has no idea the property exists yet. The
round frames were all correctly set in both scenes and none of them applied.
Also fixed researtch_lab_garrison.tres, which was authored under the void
profile - that is the black hole surface, so a lab garrison was spawning on
black holes.
Suite 110/110. test_enemy_combat's whole premise was auto-fire; it now holds
the real stick toward the enemy rather than poking weapon components, so it
exercises the path the player uses. test_fire_stick is new and covers the
change itself: an idle stick leaves a target parked well inside range
untouched, which is precisely what the old gun would never have done.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:29:21 +02:00
|
|
|
NeonDraw.draw_glowing_polygon(self, _frame_points(margin), _active_color(), pulse, core_color)
|
2026-08-13 20:56:24 +02:00
|
|
|
if highlighted:
|
|
|
|
|
# The inset frame reads as a selection focus ring. It deliberately uses
|
|
|
|
|
# the bright core rather than the button's glow colour, so selecting a
|
|
|
|
|
# blue shield or pink transporter does not change what that colour means.
|
Twin-stick combat: aim and fire with a second stick, MOBA-style ability ring
The fleet's guns picked their own targets and fired on their own, so combat
happened whether or not the player did anything - there was nothing to aim and
nothing to decide, and the right half of the screen held nothing at all.
Firing is now an action. VesselWeaponComponent no longer seeks a target; it
shoots along a heading, and the only thing that supplies one is the new
right-hand stick via FireStickController. A fleet arriving from another scene
starts with its guns silent, which is the correct resting state.
A subtle dashed guide runs from the fleet along the aimed heading while the
stick is held, out to the guns' OWN range - read from a weapon that is
actually aboard, so the line can never promise reach the fleet does not have.
Layout: the two sticks take the bottom corners, the pivot pair is centred
between them at the bottom edge, and the laser and gas bomb sit on an arc up
and left of the fire stick where a thumb already is. Those four are now round
- IconButton grew a round_frame mode, and the in-world action controls use it
while panel and menu buttons stay square, so the two kinds of button read as
different kinds of thing.
Worth recording, because it cost a screenshot to find: a script-declared
property set BEFORE the `script = ExtResource(...)` line in a .tscn is
silently dropped. Godot's parser has no idea the property exists yet. The
round frames were all correctly set in both scenes and none of them applied.
Also fixed researtch_lab_garrison.tres, which was authored under the void
profile - that is the black hole surface, so a lab garrison was spawning on
black holes.
Suite 110/110. test_enemy_combat's whole premise was auto-fire; it now holds
the real stick toward the enemy rather than poking weapon components, so it
exercises the path the player uses. test_fire_stick is new and covers the
change itself: an idle stick leaves a target parked well inside range
untouched, which is precisely what the old gun would never have done.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:29:21 +02:00
|
|
|
NeonDraw.draw_glowing_polygon(self, _frame_points(10.0 * s), core_color, 1.0,
|
|
|
|
|
core_color, 0.72 * 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
|
|
|
|
|
|
|
|
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)
|
2026-08-12 13:07:52 +02:00
|
|
|
Icon.TRANSFER:
|
|
|
|
|
_draw_transfer_icon(center, pulse, s)
|
2026-08-13 20:13:32 +02:00
|
|
|
Icon.UPGRADE_DAMAGE:
|
|
|
|
|
_draw_upgrade_damage(center, pulse, s)
|
|
|
|
|
Icon.UPGRADE_RATE:
|
|
|
|
|
_draw_upgrade_rate(center, pulse, s)
|
|
|
|
|
Icon.UPGRADE_RANGE:
|
|
|
|
|
_draw_upgrade_range(center, pulse, s)
|
|
|
|
|
Icon.UPGRADE_THRUSTERS:
|
|
|
|
|
_draw_upgrade_thrusters(center, pulse, s)
|
|
|
|
|
Icon.UPGRADE_SHIELD_HITS:
|
|
|
|
|
_draw_upgrade_shield_hits(center, pulse, s)
|
|
|
|
|
Icon.UPGRADE_SHIELD_RECHARGE:
|
|
|
|
|
_draw_upgrade_shield_recharge(center, pulse, s)
|
|
|
|
|
Icon.UPGRADE_CARGO:
|
|
|
|
|
_draw_upgrade_cargo(center, pulse, s)
|
|
|
|
|
Icon.UPGRADE_ENGINES:
|
|
|
|
|
_draw_upgrade_engines(center, pulse, s)
|
|
|
|
|
Icon.UPGRADE_UNLOCK_SHIELD:
|
|
|
|
|
_draw_upgrade_unlock_shield(center, pulse, s)
|
2026-08-13 20:49:15 +02:00
|
|
|
Icon.ACTION_MENU:
|
|
|
|
|
_draw_action_menu(center, pulse, s)
|
2026-08-13 22:17:24 +02:00
|
|
|
Icon.GAS:
|
|
|
|
|
_draw_gas(center, pulse, s)
|
2026-08-14 11:54:31 +02:00
|
|
|
Icon.LEFT_ARROW:
|
|
|
|
|
_draw_horizontal_arrow(center, pulse, -1.0, s)
|
|
|
|
|
Icon.RIGHT_ARROW:
|
|
|
|
|
_draw_horizontal_arrow(center, pulse, 1.0, s)
|
2026-08-14 13:44:10 +02:00
|
|
|
Icon.MINUS:
|
|
|
|
|
_draw_minus(center, pulse, s)
|
|
|
|
|
Icon.PLUS:
|
|
|
|
|
_draw_plus(center, pulse, s)
|
2026-08-14 22:46:30 +02:00
|
|
|
Icon.SHIELD_VESSEL:
|
|
|
|
|
_draw_shield_vessel(center, pulse, s)
|
2026-08-15 04:45:15 +02:00
|
|
|
Icon.GAS_BOMBER:
|
|
|
|
|
_draw_gas_bomber(center, pulse, s)
|
|
|
|
|
Icon.GAS_BOMB:
|
|
|
|
|
_draw_gas_bomb(center, pulse, s)
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
Icon.GAS_BURNER:
|
|
|
|
|
_draw_gas_burner_icon(center, pulse, s)
|
Implement #77, #78 and #79
#77 - the Distorder becomes the Energy Propulser. Renamed everywhere the
player reads it, reskinned to match: a symmetric hull flaring into an engine
skirt instead of the lopsided warped lump, an engine bell with a thrust plume
instead of the wobbling ring glyph, and energy's own yellow instead of purple,
since energy is what it burns. The internal id stays "distorder" - it is the
capability string, the unlock key and the component scene name, and renaming
the wiring would be a large mechanical change with no player-visible effect.
Its speed ceiling now grows with the LOGARITHM of how many fly. Each one used
to add a flat 1.0, so a fleet that could afford six simply drove at six times
cruise and the vessel stopped being a choice. Base-2 makes the rule one
sentence - every doubling adds another 1.0 - and leaves two propulsers worth
exactly what they were worth before, so nothing already built is devalued.
#78 - a gas burner can be paused. It converts gas the player may want for
bombs into energy they may not need yet, and the only way to stop it was to
demolish it. The action is data-driven like every other: the visual's
ActionProvider lists "pause", and any converter can adopt it. Pause is the
burner's PRIMARY action, so it is one tap rather than a menu; the button shows
what it will DO, bars while running and a play triangle while stopped; and the
building itself goes visibly cold, because a paused building that looked
identical to a running one would be a setting to remember rather than see.
Conversion progress is left where it was rather than reset, so switching off
and on again does not discard a part-finished unit.
#79 - picking something up says so. A "+N" with the resource's own mark rises
and fades where it happened. Built from the SAME glyph the resource tables and
transfer rows use, so a material added to the catalog later cannot end up with
one mark in the HUD and a different one in the world. A pickup a full hold
REFUSES raises nothing.
Two Godot traps cost real time here and are now written down where the next
person will hit them:
- a `#` comment inside a .tres [resource] block is not a comment. It parses
as part of the preceding value and silently blanks the property after it.
One in DistorderVesselDefinition.tres left definition_id empty, which
surfaced as "<blank> did not resolve its definition" in a test far from
the edit. The note lives in VesselDefinition.gd now.
- a global class_name is not reliably resolvable, INCLUDING a script's own
inside its own static function. ResourcePickupVFX.new() failed to compile
when reached through a preview scene, taking Rock and the gas vents with
it. It has no class_name now and constructs itself by path.
Suite 114/114.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:38:30 +02:00
|
|
|
Icon.PAUSE:
|
|
|
|
|
_draw_pause(center, pulse, s)
|
|
|
|
|
Icon.RESUME:
|
|
|
|
|
_draw_resume(center, pulse, s)
|
2026-08-16 16:20:39 +02:00
|
|
|
Icon.CARRIER_VESSEL:
|
|
|
|
|
_draw_carrier(center, pulse, s)
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
Icon.LASER_VESSEL:
|
|
|
|
|
_draw_laser_vessel(center, pulse, s)
|
|
|
|
|
Icon.LASER_SHOT:
|
|
|
|
|
_draw_laser_shot(center, pulse, s)
|
2026-08-16 17:59:04 +02:00
|
|
|
Icon.DISTORDER_VESSEL:
|
|
|
|
|
_draw_distorder_vessel(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)
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 22:46:30 +02:00
|
|
|
func _draw_shield_vessel(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
var points: PackedVector2Array = PackedVector2Array()
|
|
|
|
|
for i in range(6):
|
|
|
|
|
var angle: float = -PI / 2.0 + float(i) * TAU / 6.0
|
|
|
|
|
points.append(center + Vector2(cos(angle), sin(angle)) * 11.0 * s)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, points, _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
|
2026-08-16 16:20:39 +02:00
|
|
|
func _draw_carrier(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# Wide and flat-bottomed rather than pointed - reads as a hauler, not a
|
|
|
|
|
# combat silhouette, matching the ship's own hull_points.
|
|
|
|
|
var hull := PackedVector2Array([
|
|
|
|
|
center + Vector2(-13, -4) * s,
|
|
|
|
|
center + Vector2(-7, -8) * s,
|
|
|
|
|
center + Vector2(7, -8) * s,
|
|
|
|
|
center + Vector2(13, -4) * s,
|
|
|
|
|
center + Vector2(13, 6) * s,
|
|
|
|
|
center + Vector2(-13, 6) * s,
|
|
|
|
|
])
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, hull, _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
|
2026-08-16 17:59:04 +02:00
|
|
|
func _draw_distorder_vessel(center: Vector2, pulse: float, s: float) -> void:
|
Implement #77, #78 and #79
#77 - the Distorder becomes the Energy Propulser. Renamed everywhere the
player reads it, reskinned to match: a symmetric hull flaring into an engine
skirt instead of the lopsided warped lump, an engine bell with a thrust plume
instead of the wobbling ring glyph, and energy's own yellow instead of purple,
since energy is what it burns. The internal id stays "distorder" - it is the
capability string, the unlock key and the component scene name, and renaming
the wiring would be a large mechanical change with no player-visible effect.
Its speed ceiling now grows with the LOGARITHM of how many fly. Each one used
to add a flat 1.0, so a fleet that could afford six simply drove at six times
cruise and the vessel stopped being a choice. Base-2 makes the rule one
sentence - every doubling adds another 1.0 - and leaves two propulsers worth
exactly what they were worth before, so nothing already built is devalued.
#78 - a gas burner can be paused. It converts gas the player may want for
bombs into energy they may not need yet, and the only way to stop it was to
demolish it. The action is data-driven like every other: the visual's
ActionProvider lists "pause", and any converter can adopt it. Pause is the
burner's PRIMARY action, so it is one tap rather than a menu; the button shows
what it will DO, bars while running and a play triangle while stopped; and the
building itself goes visibly cold, because a paused building that looked
identical to a running one would be a setting to remember rather than see.
Conversion progress is left where it was rather than reset, so switching off
and on again does not discard a part-finished unit.
#79 - picking something up says so. A "+N" with the resource's own mark rises
and fades where it happened. Built from the SAME glyph the resource tables and
transfer rows use, so a material added to the catalog later cannot end up with
one mark in the HUD and a different one in the world. A pickup a full hold
REFUSES raises nothing.
Two Godot traps cost real time here and are now written down where the next
person will hit them:
- a `#` comment inside a .tres [resource] block is not a comment. It parses
as part of the preceding value and silently blanks the property after it.
One in DistorderVesselDefinition.tres left definition_id empty, which
surfaced as "<blank> did not resolve its definition" in a test far from
the edit. The note lives in VesselDefinition.gd now.
- a global class_name is not reliably resolvable, INCLUDING a script's own
inside its own static function. ResourcePickupVFX.new() failed to compile
when reached through a preview scene, taking Rock and the gas vents with
it. It has no class_name now and constructs itself by path.
Suite 114/114.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:38:30 +02:00
|
|
|
# #77: an engine bell with a thrust plume under it. The glyph used to be a
|
|
|
|
|
# ring with a wobbling radius, which read as warped space - correct for a
|
|
|
|
|
# "space-time distorder" and wrong for the Energy Propulser it now is.
|
|
|
|
|
var hull: PackedVector2Array = PackedVector2Array([
|
|
|
|
|
center + Vector2(0.0, -13.0) * s,
|
|
|
|
|
center + Vector2(5.0, -4.0) * s,
|
|
|
|
|
center + Vector2(7.0, 3.0) * s,
|
|
|
|
|
center + Vector2(10.0, 10.0) * s,
|
|
|
|
|
center + Vector2(-10.0, 10.0) * s,
|
|
|
|
|
center + Vector2(-7.0, 3.0) * s,
|
|
|
|
|
center + Vector2(-5.0, -4.0) * s,
|
|
|
|
|
])
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, hull, _active_color(), pulse, core_color)
|
|
|
|
|
# Three exhaust strokes, the middle one longest, so the thrust reads as
|
|
|
|
|
# coming OUT of the bell rather than as decoration beside it.
|
|
|
|
|
for offset in [-5.0, 0.0, 5.0]:
|
|
|
|
|
var length: float = 16.0 if is_zero_approx(offset) else 12.0
|
|
|
|
|
NeonDraw.draw_glowing_line(self,
|
|
|
|
|
center + Vector2(offset, 11.0) * s,
|
|
|
|
|
center + Vector2(offset, length) * s,
|
|
|
|
|
_active_color(), pulse * 0.8, 0.5, core_color)
|
2026-08-16 17:59:04 +02:00
|
|
|
|
|
|
|
|
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
func _draw_laser_vessel(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# A slim dart rather than the interceptor's plain triangle - the notch at
|
|
|
|
|
# the nose reads as a fixed forward-facing emitter, not a hull point.
|
|
|
|
|
var hull := PackedVector2Array([
|
|
|
|
|
center + Vector2(0, -13) * s,
|
|
|
|
|
center + Vector2(-8, 9) * s,
|
|
|
|
|
center + Vector2(-3, 6) * s,
|
|
|
|
|
center + Vector2(3, 6) * s,
|
|
|
|
|
center + Vector2(8, 9) * s,
|
|
|
|
|
])
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, hull, _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_laser_shot(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
var from: Vector2 = center + Vector2(-9, 9) * s
|
|
|
|
|
var to: Vector2 = center + Vector2(9, -9) * s
|
|
|
|
|
NeonDraw.draw_glowing_line(self, from, to, _active_color(), pulse, 1.1, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_circle(self, to, 3.0 * s, _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 04:45:15 +02:00
|
|
|
func _draw_gas_bomber(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
var hull := PackedVector2Array([
|
|
|
|
|
center + Vector2(0, -12) * s,
|
|
|
|
|
center + Vector2(-12, 7) * s,
|
|
|
|
|
center + Vector2(-5, 5) * s,
|
|
|
|
|
center + Vector2(0, 11) * s,
|
|
|
|
|
center + Vector2(5, 5) * s,
|
|
|
|
|
center + Vector2(12, 7) * s,
|
|
|
|
|
])
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, hull, _active_color(), pulse, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_circle(self, center + Vector2(0, 2) * s, 4.0 * s,
|
|
|
|
|
_active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_gas_bomb(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
NeonDraw.draw_glowing_circle(self, center, 7.0 * s, _active_color(), pulse, core_color)
|
|
|
|
|
for direction in [Vector2.UP, Vector2.DOWN, Vector2.LEFT, Vector2.RIGHT]:
|
|
|
|
|
NeonDraw.draw_glowing_circle(self, center + direction * 9.0 * s, 3.5 * s,
|
|
|
|
|
_active_color(), pulse * 0.85, core_color)
|
|
|
|
|
|
|
|
|
|
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
func _draw_gas_burner_icon(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# A hexagonal chamber with a hot core and vents, miniaturised from the
|
|
|
|
|
# building's own silhouette (BuildingVisualBase._draw_gas_burner_static)
|
|
|
|
|
# so the menu icon reads as the same structure being placed.
|
|
|
|
|
var outer: PackedVector2Array = PackedVector2Array()
|
|
|
|
|
var core: PackedVector2Array = PackedVector2Array()
|
|
|
|
|
for i in range(6):
|
|
|
|
|
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
|
|
|
|
|
outer.append(center + Vector2(cos(a), sin(a)) * 11.0 * s)
|
|
|
|
|
core.append(center + Vector2(cos(a), sin(a)) * 5.5 * s)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, outer, _active_color(), pulse, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, core, _active_color(), pulse * 0.85, core_color)
|
|
|
|
|
|
|
|
|
|
for i in range(4):
|
|
|
|
|
var a: float = (float(i) / 4.0) * TAU + PI / 4.0
|
|
|
|
|
var dir: Vector2 = Vector2(cos(a), sin(a))
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + dir * 11.0 * s, center + dir * 15.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_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)
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 11:54:31 +02:00
|
|
|
func _draw_horizontal_arrow(center: Vector2, pulse: float, direction: float, s: float) -> void:
|
|
|
|
|
var r: float = 10.0 * s
|
|
|
|
|
var tip: Vector2 = center + Vector2(r * direction, 0.0)
|
|
|
|
|
var tail: Vector2 = center + Vector2(-r * direction, 0.0)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, tail, tip, _active_color(), pulse, 0.5 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, tip, tip + Vector2(-r * 0.8 * direction, -r * 0.7), _active_color(), pulse, 0.5 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, tip, tip + Vector2(-r * 0.8 * direction, r * 0.7), _active_color(), pulse, 0.5 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
Implement #77, #78 and #79
#77 - the Distorder becomes the Energy Propulser. Renamed everywhere the
player reads it, reskinned to match: a symmetric hull flaring into an engine
skirt instead of the lopsided warped lump, an engine bell with a thrust plume
instead of the wobbling ring glyph, and energy's own yellow instead of purple,
since energy is what it burns. The internal id stays "distorder" - it is the
capability string, the unlock key and the component scene name, and renaming
the wiring would be a large mechanical change with no player-visible effect.
Its speed ceiling now grows with the LOGARITHM of how many fly. Each one used
to add a flat 1.0, so a fleet that could afford six simply drove at six times
cruise and the vessel stopped being a choice. Base-2 makes the rule one
sentence - every doubling adds another 1.0 - and leaves two propulsers worth
exactly what they were worth before, so nothing already built is devalued.
#78 - a gas burner can be paused. It converts gas the player may want for
bombs into energy they may not need yet, and the only way to stop it was to
demolish it. The action is data-driven like every other: the visual's
ActionProvider lists "pause", and any converter can adopt it. Pause is the
burner's PRIMARY action, so it is one tap rather than a menu; the button shows
what it will DO, bars while running and a play triangle while stopped; and the
building itself goes visibly cold, because a paused building that looked
identical to a running one would be a setting to remember rather than see.
Conversion progress is left where it was rather than reset, so switching off
and on again does not discard a part-finished unit.
#79 - picking something up says so. A "+N" with the resource's own mark rises
and fades where it happened. Built from the SAME glyph the resource tables and
transfer rows use, so a material added to the catalog later cannot end up with
one mark in the HUD and a different one in the world. A pickup a full hold
REFUSES raises nothing.
Two Godot traps cost real time here and are now written down where the next
person will hit them:
- a `#` comment inside a .tres [resource] block is not a comment. It parses
as part of the preceding value and silently blanks the property after it.
One in DistorderVesselDefinition.tres left definition_id empty, which
surfaced as "<blank> did not resolve its definition" in a test far from
the edit. The note lives in VesselDefinition.gd now.
- a global class_name is not reliably resolvable, INCLUDING a script's own
inside its own static function. ResourcePickupVFX.new() failed to compile
when reached through a preview scene, taking Rock and the gas vents with
it. It has no class_name now and constructs itself by path.
Suite 114/114.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:38:30 +02:00
|
|
|
func _draw_pause(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# Two upright bars. Shown while something is RUNNING, because that is the
|
|
|
|
|
# action the button performs, not the state it is in.
|
|
|
|
|
for side in [-1.0, 1.0]:
|
|
|
|
|
var x: float = 5.0 * side * s
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(x, -11.0 * s),
|
|
|
|
|
center + Vector2(x, 11.0 * s), _active_color(), pulse, 0.9, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_resume(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# A play triangle, shown while something is stopped.
|
|
|
|
|
var play: PackedVector2Array = PackedVector2Array([
|
|
|
|
|
center + Vector2(-7.0, -11.0) * s,
|
|
|
|
|
center + Vector2(11.0, 0.0) * s,
|
|
|
|
|
center + Vector2(-7.0, 11.0) * s,
|
|
|
|
|
])
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, play, _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 13:44:10 +02:00
|
|
|
func _draw_minus(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
var r: float = 9.0 * s
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-r, 0.0), center + Vector2(r, 0.0),
|
|
|
|
|
_active_color(), pulse, 0.55 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_plus(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
_draw_minus(center, pulse, s)
|
|
|
|
|
var r: float = 9.0 * s
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(0.0, -r), center + Vector2(0.0, r),
|
|
|
|
|
_active_color(), pulse, 0.55 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 13:07:52 +02:00
|
|
|
func _draw_transfer_icon(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# Two shafts side by side pointing opposite ways. Drawn as a pair rather
|
|
|
|
|
# than as one double-headed arrow because the button's whole job is that
|
|
|
|
|
# ore moves BOTH ways here, and a double-headed arrow reads as one thing
|
|
|
|
|
# sliding back and forth.
|
|
|
|
|
var r: float = 10.0 * s
|
|
|
|
|
var gap: float = 7.0 * s
|
|
|
|
|
for side in [-1.0, 1.0]:
|
|
|
|
|
# Left shaft points up (into the fleet), right shaft points down.
|
|
|
|
|
var direction: float = side
|
|
|
|
|
var x: float = gap * side
|
|
|
|
|
var tip: Vector2 = center + Vector2(x, r * direction)
|
|
|
|
|
var tail: Vector2 = center + Vector2(x, -r * direction)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, tail, tip, _active_color(), pulse, 0.42 * s, core_color)
|
|
|
|
|
var head: float = 5.0 * s
|
|
|
|
|
NeonDraw.draw_glowing_line(self, tip, tip + Vector2(-head, -head * direction),
|
|
|
|
|
_active_color(), pulse, 0.42 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, tip, tip + Vector2(head, -head * direction),
|
|
|
|
|
_active_color(), pulse, 0.42 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
2026-08-13 20:13:32 +02:00
|
|
|
# --- ship-lab upgrade glyphs ---------------------------------------------
|
|
|
|
|
|
|
|
|
|
func _draw_upgrade_damage(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# A heavy projectile striking a target.
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-13, 0) * s, center + Vector2(5, 0) * s, _active_color(), pulse, 0.7 * s, core_color)
|
|
|
|
|
for a in [0.0, PI * 0.5, PI, PI * 1.5]:
|
|
|
|
|
var dir := Vector2(cos(a), sin(a))
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(8, 0) * s, center + Vector2(8, 0) * s + dir * 7.0 * s, _active_color(), pulse, 0.4 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_upgrade_rate(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# Three successive shots: more fire per second.
|
|
|
|
|
for x in [-9.0, 0.0, 9.0]:
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(x - 3, 7) * s, center + Vector2(x + 3, -7) * s, _active_color(), pulse, 0.55 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_upgrade_range(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# Reticle with a long sight line.
|
|
|
|
|
var r := 10.0 * s
|
|
|
|
|
for i in range(12):
|
|
|
|
|
var a := TAU * float(i) / 12.0
|
|
|
|
|
var b := TAU * float(i + 1) / 12.0
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(cos(a), sin(a)) * r, center + Vector2(cos(b), sin(b)) * r, _active_color(), pulse, 0.32 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-15, 0) * s, center + Vector2(15, 0) * s, _active_color(), pulse, 0.38 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(0, -15) * s, center + Vector2(0, 15) * s, _active_color(), pulse, 0.38 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_upgrade_thrusters(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# Ship nozzle with two exhaust flames.
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([center + Vector2(-10, -7) * s, center + Vector2(5, -7) * s, center + Vector2(11, 0) * s, center + Vector2(5, 7) * s, center + Vector2(-10, 7) * s]), _active_color(), pulse, core_color)
|
|
|
|
|
for y in [-4.0, 4.0]:
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-10, y) * s, center + Vector2(-17, y * 1.5) * s, _active_color(), pulse, 0.45 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_upgrade_shield_hits(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# Layered hexagonal shield walls.
|
|
|
|
|
for r in [8.0, 13.0]:
|
|
|
|
|
var hex := PackedVector2Array()
|
|
|
|
|
for i in range(6):
|
|
|
|
|
var a := TAU * float(i) / 6.0 + PI / 6.0
|
|
|
|
|
hex.append(center + Vector2(cos(a), sin(a)) * r * s)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, hex, _active_color(), pulse, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_upgrade_shield_recharge(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# A shield with a circular recovery arrow.
|
|
|
|
|
var arc := PackedVector2Array()
|
|
|
|
|
for i in range(10):
|
|
|
|
|
var a := lerpf(-PI * 0.2, PI * 1.35, float(i) / 9.0)
|
|
|
|
|
arc.append(center + Vector2(cos(a), sin(a)) * 11.0 * s)
|
|
|
|
|
NeonDraw.draw_glowing_path(self, arc, _active_color(), pulse, 0.45 * s, core_color)
|
|
|
|
|
var tip: Vector2 = arc[arc.size() - 1]
|
|
|
|
|
NeonDraw.draw_glowing_line(self, tip, tip + Vector2(6, -1) * s, _active_color(), pulse, 0.45 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, tip, tip + Vector2(1, 6) * s, _active_color(), pulse, 0.45 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_upgrade_cargo(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# Freight crate, with outward arrows suggesting expanded volume.
|
|
|
|
|
var box := 8.0 * 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)
|
|
|
|
|
for side in [-1.0, 1.0]:
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(side * 10, 0) * s, center + Vector2(side * 16, 0) * s, _active_color(), pulse, 0.38 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_upgrade_engines(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# Engine core with speed streaks.
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([center + Vector2(-4, -8) * s, center + Vector2(7, -5) * s, center + Vector2(10, 0) * s, center + Vector2(7, 5) * s, center + Vector2(-4, 8) * s, center + Vector2(-8, 0) * s]), _active_color(), pulse, core_color)
|
|
|
|
|
for y in [-7.0, 0.0, 7.0]:
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-18, y) * s, center + Vector2(-9, y) * s, _active_color(), pulse, 0.4 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_upgrade_unlock_shield(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# A shield silhouette with a plus: unlock the whole vessel, not a stat.
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([center + Vector2(0, -13) * s, center + Vector2(11, -7) * s, center + Vector2(8, 7) * s, center + Vector2(0, 13) * s, center + Vector2(-8, 7) * s, center + Vector2(-11, -7) * s]), _active_color(), pulse, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-4, 0) * s, center + Vector2(4, 0) * s, _active_color(), pulse, 0.45 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(0, -4) * s, center + Vector2(0, 4) * s, _active_color(), pulse, 0.45 * s, core_color)
|
|
|
|
|
|
|
|
|
|
|
2026-08-13 20:49:15 +02:00
|
|
|
func _draw_action_menu(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# A small, intentionally neutral stack of choices. It marks a button that
|
|
|
|
|
# opens context actions rather than carrying out one particular action.
|
|
|
|
|
for y in [-8.0, 0.0, 8.0]:
|
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-10, y) * s, center + Vector2(10, y) * s, _active_color(), pulse, 0.42 * s, core_color)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
|
|
|
|
|
center + Vector2(-15, y - 2) * s, center + Vector2(-11, y) * s,
|
|
|
|
|
center + Vector2(-15, y + 2) * s,
|
|
|
|
|
]), _active_color(), pulse, core_color)
|
|
|
|
|
|
2026-08-13 22:17:24 +02:00
|
|
|
func _draw_gas(center: Vector2, pulse: float, s: float) -> void:
|
|
|
|
|
# Three drifting bubbles: the visual language for gaseous resources.
|
|
|
|
|
for data in [[Vector2(-6,-3), 6.0], [Vector2(6,4), 8.0], [Vector2(4,-9), 4.0]]:
|
|
|
|
|
var ring := PackedVector2Array()
|
|
|
|
|
for i in range(10):
|
|
|
|
|
var a := TAU * float(i) / 10.0
|
|
|
|
|
ring.append(center + (data[0] + Vector2(cos(a), sin(a)) * data[1]) * s)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(self, ring, _active_color(), pulse, core_color)
|
|
|
|
|
|
2026-08-13 20:49:15 +02:00
|
|
|
|
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_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)
|