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
|
|
|
extends Node2D
|
|
|
|
|
|
#84: move 215 root files into a folder hierarchy
Everything lived in one flat directory, so a diff gave no clue what area it
touched and finding the file you wanted meant knowing its exact name. Twelve
folders now, grouped by what a thing IS rather than what type it is - a
vessel's definition, its component scenes and its upgrade resources sit
together, because that is the set you read when you change one of them.
buildings core controllers enemies logistics planets
resources shaders surface ui vessels vfx world
332 files moved and every res:// reference rewritten - 306 files touched,
including the autoload and main-scene paths in project.godot. Nothing renamed:
this is purely where files sit, so the diff is a pile of pure moves and no
behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at
the root, where Godot and the export preset expect them.
Verified three ways, because a suite alone would not have caught all of it:
the full 117 pass, the web export builds clean, and the game renders.
The export is why that matters. The FIRST export after the move failed with
"Attempt to open script 'res://GameState.gd'" - the old path - while
project.godot on disk plainly said res://core/GameState.gd. That is this
project's documented Android FUSE mtime trap: Godot served a cached copy of a
file whose content had changed. A `touch` fixed it. Nothing in the test suite
would have found that, because the suite runs from a copy on a native
filesystem - the one place the bug cannot happen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:44:14 +02:00
|
|
|
const BUILDING_DEFINITIONS = preload("res://buildings/BuildingDefinitions.gd")
|
|
|
|
|
const VESSEL_DEFINITIONS = preload("res://vessels/VesselDefinitions.gd")
|
2026-08-31 02:18:08 +02:00
|
|
|
const ALL: int = 1 << 30
|
|
|
|
|
const HUD_COLUMN_MARGIN := 18.0
|
|
|
|
|
const HUD_ROW_GAP := 16.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
|
|
|
|
|
|
|
|
@export var prompt_debounce_time: float = 0.15
|
|
|
|
|
|
|
|
|
|
@onready var fleet: Node2D = get_node("../Fleet")
|
|
|
|
|
@onready var building_field: Node2D = get_node("../BuildingField")
|
2026-08-31 02:18:08 +02:00
|
|
|
@onready var planet_field: Node2D = get_node("../PlanetField")
|
|
|
|
|
@onready var landing_controller: Node2D = get_node("../LandingController")
|
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
|
|
|
@onready var world_map: Control = get_node("../UI/WorldMap")
|
2026-08-31 02:18:08 +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
|
|
|
@onready var build_button: IconButton = get_node("../UI/BuildButton")
|
|
|
|
|
@onready var action_button: IconButton = get_node("../UI/BuildActionButton")
|
2026-08-13 20:49:15 +02:00
|
|
|
@onready var building_action_button: IconButton = get_node("../UI/BuildingActionButton")
|
2026-08-14 23:15:12 +02:00
|
|
|
@onready var landing_button: IconButton = get_node("../UI/EnterAtmosphereButton")
|
2026-08-31 02:18:08 +02:00
|
|
|
@onready var transfer_button: IconButton = get_node("../UI/TransferButton")
|
2026-08-12 22:47:46 +02:00
|
|
|
@onready var hangar_button: IconButton = get_node("../UI/HangarButton")
|
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
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
# Menu roots are scene APIs. BuildController deliberately never reaches into
|
|
|
|
|
# their child trees; each scene owns its controls, layout and input routing.
|
|
|
|
|
@onready var build_menu: Control = get_node("../UI/BuildMenu")
|
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
|
|
|
@onready var craft_menu: Control = get_node("../UI/CraftMenu")
|
2026-08-31 02:18:08 +02:00
|
|
|
@onready var lab_menu: Control = get_node("../UI/LabMenu")
|
2026-08-12 13:07:52 +02:00
|
|
|
@onready var transfer_menu: Control = get_node("../UI/TransferMenu")
|
2026-08-31 02:18:08 +02:00
|
|
|
@onready var hangar_menu: Control = get_node("../UI/HangarMenu")
|
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
|
|
|
@onready var formation_menu: Control = get_node("../UI/FormationMenu")
|
2026-08-31 02:18:08 +02:00
|
|
|
@onready var skill_menu: Control = get_node("../UI/SkillMenu")
|
|
|
|
|
@onready var planning_menu: Control = get_node("../UI/PlanningMenu")
|
|
|
|
|
@onready var garrison_menu: Control = get_node("../UI/GarrisonMenu")
|
Build the logistics crews, and let them burn energy
#216: transport hubs and spacial workshops no longer conjure their crews.
Transporters and repair drones are ordered at a ship crafter, assigned per
building through a new Station menu, flown out from the mothership hangar,
and spent by the work they do - 20 runs, 12 repairs, counted where the run
ENDS so nothing retires with cargo in the air or a building half repaired.
A spent vessel is gone, so the structure is short again and the player has
to build another. The menu shows what each one has left, worst first.
Craftability needed its own flag: "the crafter can make it" and "it can take
a berth in the fleet" stopped being the same question.
#217: transporters crawl at 120 rather than 150, and a Burn Drive lets them
carry energy and spend it to travel far faster. Fetching it is a leg of its
own, to the nearest store that has any - so a refuelling transporter cannot
be dispatched mid-errand - and the leg flown to COLLECT fuel is not charged
for it. Unresearched, nothing detours and nothing changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-05 19:09:35 +02:00
|
|
|
# #216: where a hub or a workshop is told how many craft to keep.
|
|
|
|
|
@onready var station_menu: Control = get_node("../UI/StationMenu")
|
2026-09-05 22:36:21 +02:00
|
|
|
# #220: what the fleet costs to keep, and the arithmetic behind it.
|
|
|
|
|
@onready var upkeep_menu: Control = get_node("../UI/FleetUpkeepMenu")
|
2026-09-06 16:24:09 +02:00
|
|
|
# Where research data comes from, shown when a player finds out they have
|
|
|
|
|
# none - the tree used to refuse in silence and the lab used to blame the
|
|
|
|
|
# queue.
|
|
|
|
|
@onready var research_points_menu: Control = get_node("../UI/ResearchPointsMenu")
|
2026-09-04 15:56:07 +02:00
|
|
|
@onready var scanner_menu: Control = get_node("../UI/ScannerMenu")
|
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 pending_build_type: String = ""
|
Buildings produce from their own stock, and queue orders until supplied
Forgejo #16, part 3 of #12. BuildController deducted the cost from the
bank at the moment of purchase and refused outright with "Not enough
rocks", so nothing ever had to be delivered anywhere. Each building now
carries its own stock, and an order it cannot pay for yet is QUEUED and
completes by itself once ore arrives.
Records get an `id` because a building has to be referable across the
space <-> planet-surface scene swap, when every Building NODE has been
destroyed and rebuilt. Position would nearly work and then fail on two
buildings placed on the same spot.
Production advances in the GameState autoload, the same way extractor
haulers already do, so an order can complete while the player is down a
gravity well. A finished vessel becomes `ships_owed` rather than being
handed straight to a Fleet that may not exist, and the fleet claims what
it is owed when it next has room.
The craft menu remembers which crafter it was opened for: _process()
clears current_building while any menu is open, so by the next frame the
menu would no longer know. It shows the ore delivered HERE rather than
the bank, since showing the bank would promise something the crafter
cannot actually build.
Ship crafting cannot be supplied until #17 lands - nothing yet moves ore
into a building.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:18:44 +02:00
|
|
|
var craft_building_id: int = 0
|
2026-08-31 02:18:08 +02:00
|
|
|
var lab_building_id: int = 0
|
|
|
|
|
var lab_vessel: String = GameState.VESSEL_INTERCEPTOR
|
|
|
|
|
var lab_choice: int = 0
|
|
|
|
|
var lab_can_buy: bool = false
|
2026-08-12 13:07:52 +02:00
|
|
|
var transfer_building_id: int = 0
|
2026-08-14 22:21:01 +02:00
|
|
|
var transfer_extractor_index: int = -1
|
2026-08-31 02:18:08 +02:00
|
|
|
var garrison_building_id: int = 0
|
2026-09-04 15:56:07 +02:00
|
|
|
var scanner_building_id: int = 0
|
2026-08-12 13:07:52 +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
|
|
|
var current_building: Node2D = null
|
|
|
|
|
var pending_building: Node2D = null
|
2026-08-14 22:21:01 +02:00
|
|
|
var current_extractor_index: int = -1
|
|
|
|
|
var pending_extractor_index: int = -1
|
2026-08-14 23:15:12 +02:00
|
|
|
var current_landing_planet: Node2D = null
|
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 pending_time: float = 0.0
|
2026-08-13 20:49:15 +02:00
|
|
|
var context_actions_expanded: bool = 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
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _ready() -> void:
|
|
|
|
|
_seat_hud_columns()
|
|
|
|
|
build_button.pressed.connect(_on_build_pressed)
|
|
|
|
|
action_button.pressed.connect(_on_context_action_pressed)
|
|
|
|
|
building_action_button.pressed.connect(_on_action_pressed)
|
|
|
|
|
transfer_button.pressed.connect(_on_transfer_pressed)
|
|
|
|
|
hangar_button.pressed.connect(_on_hangar_pressed)
|
|
|
|
|
|
|
|
|
|
build_menu.building_selected.connect(_show_building_info)
|
|
|
|
|
build_menu.build_confirmed.connect(_confirm_build)
|
|
|
|
|
build_menu.closed.connect(_on_build_menu_closed)
|
|
|
|
|
craft_menu.craft_requested.connect(_on_craft_vessel)
|
|
|
|
|
craft_menu.queue_cancel_requested.connect(_on_queue_slot)
|
|
|
|
|
craft_menu.closed.connect(func(): craft_building_id = 0)
|
|
|
|
|
lab_menu.vessel_selected.connect(_on_lab_vessel)
|
|
|
|
|
lab_menu.upgrade_selected.connect(_on_lab_upgrade)
|
|
|
|
|
lab_menu.closed.connect(func(): lab_building_id = 0)
|
|
|
|
|
transfer_menu.transfer_requested.connect(_on_resource_transfer)
|
|
|
|
|
transfer_menu.closed.connect(_clear_transfer_context)
|
|
|
|
|
hangar_menu.crew_requested.connect(_on_hangar_crew_type)
|
|
|
|
|
hangar_menu.store_requested.connect(_on_hangar_store_type)
|
|
|
|
|
formation_menu.formation_changed.connect(_on_formation_changed)
|
|
|
|
|
formation_menu.reset_requested.connect(_on_reset_formation)
|
|
|
|
|
skill_menu.research_requested.connect(_on_unlock_requested)
|
2026-09-04 15:56:07 +02:00
|
|
|
scanner_menu.scan_requested.connect(_on_scan_requested)
|
|
|
|
|
scanner_menu.closed.connect(func(): scanner_building_id = 0)
|
2026-08-31 02:18:08 +02:00
|
|
|
|
|
|
|
|
for control in [action_button, building_action_button, landing_button,
|
|
|
|
|
transfer_button, hangar_button]:
|
|
|
|
|
control.visible = false
|
|
|
|
|
if GameState.intro_active:
|
|
|
|
|
_hide_all_actions()
|
|
|
|
|
_layout_context_buttons()
|
|
|
|
|
|
|
|
|
|
|
Seat the HUD button columns below the resource readout, and move the planet name
The build button overlapped the resource list. ResourceReadout builds one
row per resource type AT RUNTIME (58px each), so how far down it reaches is
not knowable when the scene is authored: PlanetSurface/Main declared
offset_bottom 154, four resources actually reach 270, and BuildButton sat
at a hardcoded 160. A 110px collision that grew with every resource type
added. Because the readout spans the full width - the fleet hold column is
at x 356 - it hit both button columns, TransferButton and
EnterAtmosphereButton as well as the left one.
Rather than nudge the offsets, the layout now derives from the readout:
ResourceReadout.rows_height()/content_bottom() report the real extent and
_ready() stops the node understating its own size, and
BuildController._seat_hud_columns() seats both columns below that at load.
Adding a fifth resource type pushes the buttons down on its own, so the
overlap cannot come back.
Two details worth keeping: the height is derived from the live resource
list rather than cached in _ready(), because BuildController readies before
UI's children in scene-tree order and a cached value would answer zero. And
it translates the block rather than re-flowing it, since
BuildingActionButton and HangarButton deliberately share a row as mutually
exclusive options.
The planet name moved from the centre - where it sat across resource rows
3 and 4 - to the top-right corner, right-aligned. Right alignment is what
keeps it clear: short names sit against the right edge, well clear of the
fleet hold column, and long ones wrap downward into empty space rather than
growing left into it. That scene edit rode along in the #68 commit, both
being changes to PlanetSurface.tscn.
Also adds three .uid files orphaned by earlier commits this session; the
repo tracks them for every other script.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:51 +02:00
|
|
|
func _seat_hud_columns() -> void:
|
|
|
|
|
var readout: ResourceReadout = get_node_or_null("../UI/ResourceReadout")
|
|
|
|
|
if readout == null:
|
|
|
|
|
return
|
2026-08-31 02:18:08 +02:00
|
|
|
var buttons: Array = [build_button, transfer_button, action_button,
|
|
|
|
|
landing_button, building_action_button, hangar_button]
|
Seat the HUD button columns below the resource readout, and move the planet name
The build button overlapped the resource list. ResourceReadout builds one
row per resource type AT RUNTIME (58px each), so how far down it reaches is
not knowable when the scene is authored: PlanetSurface/Main declared
offset_bottom 154, four resources actually reach 270, and BuildButton sat
at a hardcoded 160. A 110px collision that grew with every resource type
added. Because the readout spans the full width - the fleet hold column is
at x 356 - it hit both button columns, TransferButton and
EnterAtmosphereButton as well as the left one.
Rather than nudge the offsets, the layout now derives from the readout:
ResourceReadout.rows_height()/content_bottom() report the real extent and
_ready() stops the node understating its own size, and
BuildController._seat_hud_columns() seats both columns below that at load.
Adding a fifth resource type pushes the buttons down on its own, so the
overlap cannot come back.
Two details worth keeping: the height is derived from the live resource
list rather than cached in _ready(), because BuildController readies before
UI's children in scene-tree order and a cached value would answer zero. And
it translates the block rather than re-flowing it, since
BuildingActionButton and HangarButton deliberately share a row as mutually
exclusive options.
The planet name moved from the centre - where it sat across resource rows
3 and 4 - to the top-right corner, right-aligned. Right alignment is what
keeps it clear: short names sit against the right edge, well clear of the
fleet hold column, and long ones wrap downward into empty space rather than
growing left into it. That scene edit rode along in the #68 commit, both
being changes to PlanetSurface.tscn.
Also adds three .uid files orphaned by earlier commits this session; the
repo tracks them for every other script.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:51 +02:00
|
|
|
var highest: float = INF
|
|
|
|
|
for button in buttons:
|
|
|
|
|
if button != null:
|
|
|
|
|
highest = minf(highest, button.position.y)
|
|
|
|
|
if is_inf(highest):
|
|
|
|
|
return
|
|
|
|
|
var shift: float = readout.content_bottom() + HUD_COLUMN_MARGIN - highest
|
|
|
|
|
if shift <= 0.0:
|
|
|
|
|
return
|
|
|
|
|
for button in buttons:
|
|
|
|
|
if button != null:
|
|
|
|
|
button.position.y += shift
|
|
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _menu_is_open() -> bool:
|
|
|
|
|
for menu in [build_menu, craft_menu, lab_menu, transfer_menu, hangar_menu,
|
Build the logistics crews, and let them burn energy
#216: transport hubs and spacial workshops no longer conjure their crews.
Transporters and repair drones are ordered at a ship crafter, assigned per
building through a new Station menu, flown out from the mothership hangar,
and spent by the work they do - 20 runs, 12 repairs, counted where the run
ENDS so nothing retires with cargo in the air or a building half repaired.
A spent vessel is gone, so the structure is short again and the player has
to build another. The menu shows what each one has left, worst first.
Craftability needed its own flag: "the crafter can make it" and "it can take
a berth in the fleet" stopped being the same question.
#217: transporters crawl at 120 rather than 150, and a Burn Drive lets them
carry energy and spend it to travel far faster. Fetching it is a leg of its
own, to the nearest store that has any - so a refuelling transporter cannot
be dispatched mid-errand - and the leg flown to COLLECT fuel is not charged
for it. Unresearched, nothing detours and nothing changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-05 19:09:35 +02:00
|
|
|
formation_menu, skill_menu, planning_menu, garrison_menu, scanner_menu,
|
2026-09-06 16:24:09 +02:00
|
|
|
station_menu, upkeep_menu, research_points_menu]:
|
2026-08-31 02:18:08 +02:00
|
|
|
if menu.visible:
|
|
|
|
|
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 is_showing_building_prompt() -> bool:
|
2026-08-31 02:18:08 +02:00
|
|
|
return _menu_is_open()
|
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 _unhandled_input(event: InputEvent) -> void:
|
2026-08-13 20:49:15 +02:00
|
|
|
if context_actions_expanded and (event is InputEventScreenTouch and event.pressed \
|
2026-08-31 02:18:08 +02:00
|
|
|
or event is InputEventMouseButton and event.pressed \
|
|
|
|
|
and event.button_index == MOUSE_BUTTON_LEFT):
|
2026-08-13 20:49:15 +02:00
|
|
|
_collapse_context_actions()
|
2026-08-31 02:18:08 +02:00
|
|
|
if skill_menu.handle_unhandled_input(event):
|
2026-08-11 20:17:58 +02:00
|
|
|
get_viewport().set_input_as_handled()
|
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
|
|
|
return
|
2026-08-31 02:18:08 +02:00
|
|
|
if formation_menu.handle_unhandled_input(event):
|
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
|
|
|
get_viewport().set_input_as_handled()
|
2026-08-11 20:17:58 +02:00
|
|
|
return
|
2026-08-31 02:18:08 +02:00
|
|
|
if world_map.visible or _menu_is_open():
|
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
|
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
|
|
|
_check_fleet_wipe()
|
#108: the prologue - the convoy, the ambush, and the breach
Landed as one piece across #122, #123 and #124: the frame is useless without
a script, and the script has nowhere to run without the frame.
The director drives the REAL scene rather than being a scene of its own. It
wants the real fleet, the real enemies and the real asteroid field, and it
makes "then the game starts" cost nothing - the director stops steering,
clears the flag, and what is left running is the ordinary game.
The script is a list of beats, each with a deadline as well as its own
completion test, so a beat whose condition is never met - a player who
refuses to fly at the ambush - runs out rather than wedging the prologue.
skip() ends the whole thing at any point and lands in exactly the state
playing it through does, so the game has one way to start rather than two
that have to be kept in step. The test pins that equivalence.
"No action button available, only movements" is enforced where the buttons
are decided, not by the director reaching in: the ability controllers set
their own visibility every frame and would have put them straight back, and
the map button belongs to the map. The joystick, the pivot and the fader
stay.
The convoy is ten hulls where a new game's fleet is one, so it needs an
explicit fleet-size override - a plain number rather than a fake doctrine,
because nothing about the player's own progression should be bent to fit a
cutscene. It is carriers rather than transporters: transporters belong to
hubs and are not fleet-eligible, so a convoy of them would be silently
filtered down to its escorts.
The ambush is chasers and only chasers. "Easy enough" is the requirement,
and a sniper against a convoy the player cannot yet fight properly is
neither easy nor a good first impression.
The breach takes the wheel through one check in GameController, covering the
stick, the keyboard and the released-again path together - three separate
gates would be three chances for one of them to keep steering. The shake is
written to camera.offset, never to its position, because GameController owns
where the camera is and would smooth a shake away.
The vessel that survives is chosen as the interceptor rather than whichever
happened to be first in the list, and the prologue ends on exactly a new
game's opening position.
Also: PlanetAtmosphereController no longer spawns the opening fleet while
the prologue is arming, and the tests that are about what the GAME starts
from now say so by turning the prologue off.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 20:59:12 +02:00
|
|
|
if GameState.intro_active:
|
|
|
|
|
_hide_all_actions()
|
|
|
|
|
return
|
|
|
|
|
if not build_button.visible:
|
|
|
|
|
build_button.visible = true
|
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 build_menu.visible:
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.refresh_option_states(_planet_under_fleet())
|
Buildings produce from their own stock, and queue orders until supplied
Forgejo #16, part 3 of #12. BuildController deducted the cost from the
bank at the moment of purchase and refused outright with "Not enough
rocks", so nothing ever had to be delivered anywhere. Each building now
carries its own stock, and an order it cannot pay for yet is QUEUED and
completes by itself once ore arrives.
Records get an `id` because a building has to be referable across the
space <-> planet-surface scene swap, when every Building NODE has been
destroyed and rebuilt. Position would nearly work and then fail on two
buildings placed on the same spot.
Production advances in the GameState autoload, the same way extractor
haulers already do, so an order can complete while the player is down a
gravity well. A finished vessel becomes `ships_owed` rather than being
handed straight to a Fleet that may not exist, and the fleet claims what
it is owed when it next has room.
The craft menu remembers which crafter it was opened for: _process()
clears current_building while any menu is open, so by the next frame the
menu would no longer know. It shows the ore delivered HERE rather than
the bank, since showing the bank would promise something the crafter
cannot actually build.
Ship crafting cannot be supplied until #17 lands - nothing yet moves ore
into a building.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:18:44 +02:00
|
|
|
if craft_menu.visible:
|
2026-08-31 02:18:08 +02:00
|
|
|
craft_menu.refresh()
|
2026-09-03 00:12:08 +02:00
|
|
|
if lab_menu.visible:
|
|
|
|
|
_refresh_lab()
|
2026-08-12 13:07:52 +02:00
|
|
|
if transfer_menu.visible:
|
2026-08-31 02:18:08 +02:00
|
|
|
transfer_menu.refresh()
|
|
|
|
|
if world_map.visible or _menu_is_open():
|
|
|
|
|
_clear_detected_context()
|
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
|
|
|
return
|
|
|
|
|
|
|
|
|
|
var detected: Node2D = building_field.get_building_fleet_is_on()
|
2026-08-14 22:21:01 +02:00
|
|
|
var detected_extractor: int = -1
|
|
|
|
|
if detected == null:
|
|
|
|
|
var planet: Node2D = _planet_under_fleet()
|
|
|
|
|
if planet != null:
|
|
|
|
|
detected_extractor = GameState.extractor_index_at(planet.planet_id)
|
|
|
|
|
if detected != pending_building or detected_extractor != pending_extractor_index:
|
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
|
|
|
pending_building = detected
|
2026-08-14 22:21:01 +02:00
|
|
|
pending_extractor_index = detected_extractor
|
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
|
|
|
pending_time = 0.0
|
|
|
|
|
else:
|
|
|
|
|
pending_time += delta
|
2026-08-31 02:18:08 +02:00
|
|
|
var detected_planet: Node2D = landing_controller.current_planet
|
2026-08-14 22:21:01 +02:00
|
|
|
if pending_time < prompt_debounce_time or (pending_building == current_building \
|
2026-08-14 23:15:12 +02:00
|
|
|
and pending_extractor_index == current_extractor_index \
|
2026-08-31 02:18:08 +02:00
|
|
|
and detected_planet == current_landing_planet):
|
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
|
|
|
return
|
|
|
|
|
current_building = pending_building
|
2026-08-14 22:21:01 +02:00
|
|
|
current_extractor_index = pending_extractor_index
|
2026-08-31 02:18:08 +02:00
|
|
|
current_landing_planet = detected_planet
|
2026-08-13 20:49:15 +02:00
|
|
|
context_actions_expanded = false
|
2026-08-14 21:44:28 +02:00
|
|
|
hangar_button.visible = false
|
2026-08-13 20:49:15 +02:00
|
|
|
building_action_button.visible = false
|
2026-08-14 23:15:12 +02:00
|
|
|
landing_button.visible = false
|
|
|
|
|
transfer_button.visible = _has_transfer_context()
|
|
|
|
|
var actions: Array = _context_action_buttons()
|
|
|
|
|
action_button.visible = not actions.is_empty()
|
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 current_building != null:
|
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
|
|
|
building_action_button.icon = _building_action_icon(current_building)
|
2026-08-13 20:49:15 +02:00
|
|
|
building_action_button.queue_redraw()
|
2026-08-14 23:15:12 +02:00
|
|
|
if not actions.is_empty():
|
2026-08-31 02:18:08 +02:00
|
|
|
action_button.icon = int(actions[0].icon) if actions.size() == 1 \
|
|
|
|
|
else IconButton.Icon.ACTION_MENU
|
2026-08-14 22:21:01 +02:00
|
|
|
action_button.queue_redraw()
|
2026-08-12 23:08:43 +02:00
|
|
|
_layout_context_buttons()
|
|
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _clear_detected_context() -> void:
|
|
|
|
|
current_building = null
|
|
|
|
|
pending_building = null
|
|
|
|
|
current_extractor_index = -1
|
|
|
|
|
pending_extractor_index = -1
|
|
|
|
|
current_landing_planet = null
|
|
|
|
|
for control in [action_button, building_action_button, landing_button,
|
|
|
|
|
transfer_button, hangar_button]:
|
|
|
|
|
control.visible = false
|
|
|
|
|
context_actions_expanded = false
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 23:08:43 +02:00
|
|
|
func _layout_context_buttons() -> void:
|
2026-08-18 12:09:44 +02:00
|
|
|
var row_top: float = build_button.position.y + build_button.size.y + HUD_ROW_GAP
|
|
|
|
|
var row_height: float = build_button.size.y
|
|
|
|
|
action_button.offset_top = row_top
|
|
|
|
|
action_button.offset_bottom = row_top + row_height
|
2026-08-13 20:49:15 +02:00
|
|
|
var buttons: Array = _context_action_buttons()
|
|
|
|
|
if not context_actions_expanded:
|
|
|
|
|
for button in buttons:
|
|
|
|
|
button.visible = false
|
|
|
|
|
return
|
2026-08-12 23:08:43 +02:00
|
|
|
for i in range(buttons.size()):
|
|
|
|
|
var button: Control = buttons[i]
|
2026-08-13 20:49:15 +02:00
|
|
|
button.visible = true
|
2026-08-13 20:54:22 +02:00
|
|
|
button.offset_left = 188 + i * 164
|
|
|
|
|
button.offset_right = button.offset_left + 148
|
2026-08-18 12:09:44 +02:00
|
|
|
button.offset_top = row_top
|
|
|
|
|
button.offset_bottom = row_top + row_height
|
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
|
|
|
|
|
|
|
|
|
2026-08-13 20:49:15 +02:00
|
|
|
func _context_action_buttons() -> Array:
|
2026-08-14 21:44:28 +02:00
|
|
|
var buttons: Array = []
|
2026-08-14 23:15:12 +02:00
|
|
|
if current_building != null:
|
2026-08-31 02:18:08 +02:00
|
|
|
for available_action in current_building.available_actions():
|
|
|
|
|
var button: IconButton = _button_for_building_action(available_action)
|
2026-08-14 23:15:12 +02:00
|
|
|
if button != null and button not in buttons:
|
|
|
|
|
buttons.append(button)
|
|
|
|
|
if current_landing_planet != null:
|
|
|
|
|
buttons.append(landing_button)
|
2026-08-14 21:44:28 +02:00
|
|
|
return buttons
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 23:15:12 +02:00
|
|
|
func _has_transfer_context() -> bool:
|
2026-08-31 02:18:08 +02:00
|
|
|
return current_extractor_index >= 0 or current_building != null \
|
|
|
|
|
and current_building.available_actions().has("transfer")
|
2026-08-14 23:15:12 +02:00
|
|
|
|
|
|
|
|
|
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 _building_action_icon(building: Node) -> int:
|
|
|
|
|
if building.primary_action() == "pause":
|
|
|
|
|
return IconButton.Icon.RESUME if GameState.is_building_paused(building.building_id) \
|
|
|
|
|
else IconButton.Icon.PAUSE
|
|
|
|
|
return int(building.definition.action_icon)
|
|
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _button_for_building_action(available_action: String) -> IconButton:
|
|
|
|
|
if available_action == "transfer":
|
2026-08-14 23:15:12 +02:00
|
|
|
return null
|
2026-08-31 02:18:08 +02:00
|
|
|
if available_action == "hangar":
|
2026-08-14 21:44:28 +02:00
|
|
|
return hangar_button
|
|
|
|
|
return building_action_button
|
2026-08-13 20:49:15 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_context_action_pressed() -> void:
|
2026-08-14 23:15:12 +02:00
|
|
|
if current_building == null and current_extractor_index < 0 \
|
|
|
|
|
and current_landing_planet == null:
|
2026-08-13 20:49:15 +02:00
|
|
|
return
|
|
|
|
|
var actions: Array = _context_action_buttons()
|
|
|
|
|
if actions.size() == 1:
|
2026-08-14 23:15:12 +02:00
|
|
|
if actions[0] == landing_button:
|
|
|
|
|
landing_controller._on_enter_pressed()
|
|
|
|
|
elif actions[0] == hangar_button:
|
|
|
|
|
_on_hangar_pressed()
|
2026-08-13 20:49:15 +02:00
|
|
|
else:
|
|
|
|
|
_on_action_pressed()
|
|
|
|
|
return
|
|
|
|
|
context_actions_expanded = not context_actions_expanded
|
|
|
|
|
_layout_context_buttons()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _collapse_context_actions() -> void:
|
2026-08-31 02:18:08 +02:00
|
|
|
if context_actions_expanded:
|
|
|
|
|
context_actions_expanded = false
|
|
|
|
|
_layout_context_buttons()
|
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 _check_fleet_wipe() -> void:
|
2026-08-31 02:18:08 +02:00
|
|
|
if fleet.ships.is_empty():
|
|
|
|
|
var spawn_pos: Variant = GameState.get_mothership_position()
|
|
|
|
|
fleet.spawn_ships(1, spawn_pos if spawn_pos != null else Vector2.ZERO)
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_action_pressed() -> void:
|
|
|
|
|
if current_building == null:
|
|
|
|
|
return
|
2026-08-31 02:18:08 +02:00
|
|
|
var building_id: int = current_building.building_id
|
|
|
|
|
var building_action: String = current_building.primary_action()
|
|
|
|
|
if building_action == "pause":
|
|
|
|
|
var paused: bool = GameState.toggle_building_paused(building_id)
|
|
|
|
|
build_menu.set_status("%s %s" % [current_building.definition.display_name,
|
|
|
|
|
"paused" if paused else "running"])
|
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
|
|
|
building_action_button.icon = _building_action_icon(current_building)
|
|
|
|
|
building_action_button.queue_redraw()
|
2026-08-31 02:18:08 +02:00
|
|
|
elif building_action == "research":
|
|
|
|
|
skill_menu.open_for(building_id)
|
|
|
|
|
elif building_action == "craft":
|
|
|
|
|
craft_building_id = building_id
|
|
|
|
|
craft_menu.open_for(building_id)
|
|
|
|
|
elif building_action == "lab":
|
|
|
|
|
lab_building_id = building_id
|
|
|
|
|
lab_menu.open_for(building_id)
|
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
|
|
|
_refresh_lab()
|
2026-08-31 02:18:08 +02:00
|
|
|
elif building_action == "formation":
|
|
|
|
|
formation_menu.open_menu()
|
|
|
|
|
elif building_action == "planning":
|
|
|
|
|
planning_menu.open_menu()
|
|
|
|
|
elif building_action == "garrison":
|
|
|
|
|
garrison_building_id = building_id
|
|
|
|
|
garrison_menu.open_for(building_id)
|
2026-09-04 15:56:07 +02:00
|
|
|
elif building_action == "scanner":
|
|
|
|
|
scanner_building_id = building_id
|
|
|
|
|
scanner_menu.open_menu()
|
Build the logistics crews, and let them burn energy
#216: transport hubs and spacial workshops no longer conjure their crews.
Transporters and repair drones are ordered at a ship crafter, assigned per
building through a new Station menu, flown out from the mothership hangar,
and spent by the work they do - 20 runs, 12 repairs, counted where the run
ENDS so nothing retires with cargo in the air or a building half repaired.
A spent vessel is gone, so the structure is short again and the player has
to build another. The menu shows what each one has left, worst first.
Craftability needed its own flag: "the crafter can make it" and "it can take
a berth in the fleet" stopped being the same question.
#217: transporters crawl at 120 rather than 150, and a Burn Drive lets them
carry energy and spend it to travel far faster. Fetching it is a leg of its
own, to the nearest store that has any - so a refuelling transporter cannot
be dispatched mid-errand - and the leg flown to COLLECT fuel is not charged
for it. Unresearched, nothing detours and nothing changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-05 19:09:35 +02:00
|
|
|
elif building_action == "staff":
|
|
|
|
|
station_menu.open_for(building_id)
|
2026-09-05 22:36:21 +02:00
|
|
|
elif building_action == "upkeep":
|
|
|
|
|
upkeep_menu.open_for(building_id)
|
2026-09-04 15:56:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_scan_requested(definition_id: String) -> void:
|
|
|
|
|
var scanner: Dictionary = GameState.find_building(scanner_building_id)
|
|
|
|
|
if scanner.is_empty():
|
|
|
|
|
scanner_menu.show_failure(definition_id)
|
|
|
|
|
return
|
|
|
|
|
var result: Dictionary = GameState.scan_for_planet(definition_id,
|
|
|
|
|
Vector2(scanner.position))
|
|
|
|
|
if result.is_empty():
|
|
|
|
|
scanner_menu.show_failure(definition_id)
|
|
|
|
|
return
|
|
|
|
|
scanner_menu.close_menu()
|
|
|
|
|
world_map.open_map()
|
|
|
|
|
world_map.focus_world_position(Vector2(result.world_position))
|
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 _on_build_pressed() -> void:
|
|
|
|
|
pending_build_type = ""
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.open_menu()
|
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
|
|
|
_refresh_build_status()
|
|
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _on_build_menu_closed() -> void:
|
|
|
|
|
pending_build_type = ""
|
#136, #137, #134: one build grid that cannot stack, one way to show a price
#136 - the gas burner drawn on top of the transport hub. The build menu's
rows were a written-down list of tops read with `tops[mini(row, size - 1)]`,
and with every structure researched the menu wanted a fourth row where the
list had three, so the last entry was placed on the third row's centre slot.
A list that silently stacks whatever will not fit in it is not a layout.
Rows are computed now, from a first top and a pitch that tightens to fit and
is floored at the row's own height - so two rows cannot occupy the same
space however far the catalog grows - and the column count widens rather
than adding a row the band cannot hold. Ten structures now sit in four
columns of three rows. The test unlocks the whole tree and checks every pair
of entries for overlap, which is the shape of the bug rather than a
coordinate that happens to be right today.
#137 - the game had three answers to "what does this cost". The build menu
drew a NeonNumber under each option, the craft menu drew glyph strips, and
the ship lab spelled prices out inside the caption. PriceTag is that answer
once: the resource's glyph, then what you HOLD over what it COSTS. It
replaces all three, plus the info popup and the skill panel, and
VesselPriceStrip - which was the same idea for one menu - goes with them.
The build menu also gains the resource bar the other two already had, in the
same place and at the same size, and the craft menu's - the only one not
running the full width of its panel - now matches. The bar drops its LOCAL
column where there is no building to report on, for the same reason #95
dropped it where it only repeated BANKED.
#134 - the skill panel's explanation and its price were both centred in the
same band, so a long description landed on the number. The text is the left
of the band now and the tag is the right. The shortfall it used to spell out
in a sentence - one per case, ore or research points - is the red on the tag
instead, and research points ride along as an entry of their own, so the tag
says WHICH thing is missing without a line of prose per possibility.
One trap worth recording: PriceTag and ResourceBar read the GameState
autoload, and an `extends SceneTree` test script that so much as names such
a class - or preloads the file - fails to compile, because GameState is not
an identifier in that context. GDScript then caches the failure, so the
scene the test instantiates gets a Control with no script where its resource
bar should be. The tests compare script PATHS, as strings.
2026-08-28 15:05:20 +02:00
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _planet_under_fleet() -> Node2D:
|
|
|
|
|
return planet_field.get_planet_fleet_is_on()
|
#106/#107 (#98a/#98b): the Planning Center
Landed together: a board of standing orders nothing acts on is a notepad,
and a planner with nowhere to read the plan from has nothing to do.
The targets are GLOBAL, not per building. There is one plan for the whole
game, and a second planning centre is another place to read and edit it
rather than a competing one - per-building targets would let two centres
hold contradictory orders and tell the crafters to satisfy both.
The census is the harder half. Every vessel is counted exactly once wherever
it happens to be: flying in the fleet, parked in the hangar, stationed at an
outpost, or in the air between any two of those. Miss the in-flight ones and
the planner queues a replacement for a vessel that is merely en route. The
fleet is the one place GameState cannot see, so Fleet pushes its composition
on every change, alongside the cargo capacity and distorder count that
already went that way.
Three rules keep the planner from fighting the player. It tops up and never
rewrites, so an order placed by hand stays exactly where it is. Everything
counts, including what is already queued, or it re-queues the same shortfall
every tick and buries the player in hulls. And nothing happens without a
planning centre - though the plan is remembered when one is destroyed, so
rebuilding picks up where it left off.
Vessels on order are counted separately from the census on purpose: they do
not exist yet, so the readout can say "4 of 6, 2 building" rather than
claiming six. The per-row note says where the vessels actually are, because
a plan can look unmet while everything it asked for is sitting at an
outpost.
The build menu goes to three columns past eight entries. #54 already
tightened its rows to 223px to fit a fourth, explicitly to avoid a
scrollable menu, and a ninth structure needs a fifth row that will not fit
at any spacing a button, a label and a cost can share. Three columns take
nine into three rows - fewer than it uses today, not more.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:21:52 +02:00
|
|
|
|
Fix the stop stutter, and hide unresearched buildings from the menu
Forgejo #13: releasing the stick paused, then moved again before
settling. stop() aims each ship at a braking point ahead of it, but the
ship's own velocity was ~0 - the anchor had been carrying it - so it
started from REST, accelerated into its own stopping distance, and
halted a second time. Measured: 0 -> 45 -> 90 -> 135 -> 180 -> brake.
stop() now hands the fleet's motion to the ship, so the release is one
continuous decelerating coast (222 -> 0) to the identical endpoint.
test_joystick_control now asserts the speed one frame after release is a
real fraction of cruise, and never rises afterwards.
Forgejo #21: unresearched structures are no longer listed greyed out.
They are hidden, and the menu repacks around what is left - a fixed grid
with entries hidden would otherwise leave holes where they used to be.
An entry alone on its row is centred rather than hanging in the left
column. Researched-but-not-usable is still shown greyed: an extractor
with no planet under the fleet is "not now", not "not yet".
This reverses a documented decision (the old comment argued that always
listing everything stops the player wondering whether the game has it at
all), so test_skill_tree's matching assertion was updated from
greyed-but-listed to hidden. The gate itself is unchanged and still
refuses a build forced past the menu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:38:17 +02:00
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func cost_of(type: String) -> int:
|
|
|
|
|
return int(costs_of(type).get(GameState.RESOURCE_ORE, 0))
|
Fix the stop stutter, and hide unresearched buildings from the menu
Forgejo #13: releasing the stick paused, then moved again before
settling. stop() aims each ship at a braking point ahead of it, but the
ship's own velocity was ~0 - the anchor had been carrying it - so it
started from REST, accelerated into its own stopping distance, and
halted a second time. Measured: 0 -> 45 -> 90 -> 135 -> 180 -> brake.
stop() now hands the fleet's motion to the ship, so the release is one
continuous decelerating coast (222 -> 0) to the identical endpoint.
test_joystick_control now asserts the speed one frame after release is a
real fraction of cruise, and never rises afterwards.
Forgejo #21: unresearched structures are no longer listed greyed out.
They are hidden, and the menu repacks around what is left - a fixed grid
with entries hidden would otherwise leave holes where they used to be.
An entry alone on its row is centred rather than hanging in the left
column. Researched-but-not-usable is still shown greyed: an extractor
with no planet under the fleet is "not now", not "not yet".
This reverses a documented decision (the old comment argued that always
listing everything stops the player wondering whether the game has it at
all), so test_skill_tree's matching assertion was updated from
greyed-but-listed to hidden. The gate itself is unchanged and still
refuses a build forced past the menu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:38:17 +02:00
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func costs_of(type: String) -> Dictionary:
|
|
|
|
|
return GameState.building_costs(type)
|
Fix the stop stutter, and hide unresearched buildings from the menu
Forgejo #13: releasing the stick paused, then moved again before
settling. stop() aims each ship at a braking point ahead of it, but the
ship's own velocity was ~0 - the anchor had been carrying it - so it
started from REST, accelerated into its own stopping distance, and
halted a second time. Measured: 0 -> 45 -> 90 -> 135 -> 180 -> brake.
stop() now hands the fleet's motion to the ship, so the release is one
continuous decelerating coast (222 -> 0) to the identical endpoint.
test_joystick_control now asserts the speed one frame after release is a
real fraction of cruise, and never rises afterwards.
Forgejo #21: unresearched structures are no longer listed greyed out.
They are hidden, and the menu repacks around what is left - a fixed grid
with entries hidden would otherwise leave holes where they used to be.
An entry alone on its row is centred rather than hanging in the left
column. Researched-but-not-usable is still shown greyed: an extractor
with no planet under the fleet is "not now", not "not yet".
This reverses a documented decision (the old comment argued that always
listing everything stops the player wondering whether the game has it at
all), so test_skill_tree's matching assertion was updated from
greyed-but-listed to hidden. The gate itself is unchanged and still
refuses a build forced past the menu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:38:17 +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 _refresh_build_status() -> void:
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.refresh(_planet_under_fleet())
|
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
|
|
|
|
|
|
|
|
|
2026-08-14 21:44:28 +02:00
|
|
|
func _refresh_build_option_states() -> void:
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.refresh_option_states(_planet_under_fleet())
|
#108: the prologue - the convoy, the ambush, and the breach
Landed as one piece across #122, #123 and #124: the frame is useless without
a script, and the script has nowhere to run without the frame.
The director drives the REAL scene rather than being a scene of its own. It
wants the real fleet, the real enemies and the real asteroid field, and it
makes "then the game starts" cost nothing - the director stops steering,
clears the flag, and what is left running is the ordinary game.
The script is a list of beats, each with a deadline as well as its own
completion test, so a beat whose condition is never met - a player who
refuses to fly at the ambush - runs out rather than wedging the prologue.
skip() ends the whole thing at any point and lands in exactly the state
playing it through does, so the game has one way to start rather than two
that have to be kept in step. The test pins that equivalence.
"No action button available, only movements" is enforced where the buttons
are decided, not by the director reaching in: the ability controllers set
their own visibility every frame and would have put them straight back, and
the map button belongs to the map. The joystick, the pivot and the fader
stay.
The convoy is ten hulls where a new game's fleet is one, so it needs an
explicit fleet-size override - a plain number rather than a fake doctrine,
because nothing about the player's own progression should be bent to fit a
cutscene. It is carriers rather than transporters: transporters belong to
hubs and are not fleet-eligible, so a convoy of them would be silently
filtered down to its escorts.
The ambush is chasers and only chasers. "Easy enough" is the requirement,
and a sniper against a convoy the player cannot yet fight properly is
neither easy nor a good first impression.
The breach takes the wheel through one check in GameController, covering the
stick, the keyboard and the released-again path together - three separate
gates would be three chances for one of them to keep steering. The shake is
written to camera.offset, never to its position, because GameController owns
where the camera is and would smooth a shake away.
The vessel that survives is chosen as the interceptor rather than whichever
happened to be first in the list, and the prologue ends on exactly a new
game's opening position.
Also: PlanetAtmosphereController no longer spawns the opening fleet while
the prologue is arming, and the tests that are about what the GAME starts
from now say so by turning the prologue off.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 20:59:12 +02:00
|
|
|
|
#102/#103 (#97d/#97e): the outpost, and the garrison you compose for it
Landed together because an outpost you cannot crew is decoration, and a
garrison menu with no building to hang it off is nothing at all.
#102, the building. Researched at "Forward Bases", two prerequisites deep
and cheaper than any mid-game structure - the ticket asks for early, and the
answer to waves attacking your buildings should arrive at about the time the
waves can hurt them. It is drawn as a watch mast on a tripod with two open
berths, deliberately the least machinery of any structure here: it makes
nothing and refines nothing, it holds a position. 120hp, more than a
crafter, because being shot at is its job.
#103, the garrison. Two separate things live on its record, and conflating
them was the first thing to get wrong: the ORDER, what the player asked for,
and the CREW, who is actually there. This menu edits the order only; #104
fills the crew. Keeping the order as a wish is what lets an outpost ask for
a hull nobody has built yet and wait for it, rather than the request
evaporating - and it is why the panel shows "here / ordered" instead of one
number.
"Only ships that attack can be assigned" is expressed as capabilities rather
than a list of types, so a new armed vessel is eligible the day it is added
and an unarmed one never is by accident. Today that means the interceptor
and the laser vessel; a shield, a hauler and an engine block cannot hold a
position however much one might want them to.
Two berths by default, and Garrison Bays raises it at outposts ALREADY
standing - the same shape as freight_yards for hubs, because a bonus that
only reached new buildings would mean demolishing and rebuilding to collect
it.
The panel builds its own rows from the catalog rather than from nodes placed
in the scene, so adding an armed vessel later needs no scene surgery.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:02:20 +02:00
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _layout_build_menu() -> void:
|
|
|
|
|
build_menu.refresh(_planet_under_fleet())
|
#102/#103 (#97d/#97e): the outpost, and the garrison you compose for it
Landed together because an outpost you cannot crew is decoration, and a
garrison menu with no building to hang it off is nothing at all.
#102, the building. Researched at "Forward Bases", two prerequisites deep
and cheaper than any mid-game structure - the ticket asks for early, and the
answer to waves attacking your buildings should arrive at about the time the
waves can hurt them. It is drawn as a watch mast on a tripod with two open
berths, deliberately the least machinery of any structure here: it makes
nothing and refines nothing, it holds a position. 120hp, more than a
crafter, because being shot at is its job.
#103, the garrison. Two separate things live on its record, and conflating
them was the first thing to get wrong: the ORDER, what the player asked for,
and the CREW, who is actually there. This menu edits the order only; #104
fills the crew. Keeping the order as a wish is what lets an outpost ask for
a hull nobody has built yet and wait for it, rather than the request
evaporating - and it is why the panel shows "here / ordered" instead of one
number.
"Only ships that attack can be assigned" is expressed as capabilities rather
than a list of types, so a new armed vessel is eligible the day it is added
and an unarmed one never is by accident. Today that means the interceptor
and the laser vessel; a shield, a hauler and an engine block cannot hold a
position however much one might want them to.
Two berths by default, and Garrison Bays raises it at outposts ALREADY
standing - the same shape as freight_yards for hubs, because a bonus that
only reached new buildings would mean demolishing and rebuilding to collect
it.
The panel builds its own rows from the catalog rather than from nodes placed
in the scene, so adding an armed vessel later needs no scene surgery.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:02:20 +02:00
|
|
|
|
#106/#107 (#98a/#98b): the Planning Center
Landed together: a board of standing orders nothing acts on is a notepad,
and a planner with nowhere to read the plan from has nothing to do.
The targets are GLOBAL, not per building. There is one plan for the whole
game, and a second planning centre is another place to read and edit it
rather than a competing one - per-building targets would let two centres
hold contradictory orders and tell the crafters to satisfy both.
The census is the harder half. Every vessel is counted exactly once wherever
it happens to be: flying in the fleet, parked in the hangar, stationed at an
outpost, or in the air between any two of those. Miss the in-flight ones and
the planner queues a replacement for a vessel that is merely en route. The
fleet is the one place GameState cannot see, so Fleet pushes its composition
on every change, alongside the cargo capacity and distorder count that
already went that way.
Three rules keep the planner from fighting the player. It tops up and never
rewrites, so an order placed by hand stays exactly where it is. Everything
counts, including what is already queued, or it re-queues the same shortfall
every tick and buries the player in hulls. And nothing happens without a
planning centre - though the plan is remembered when one is destroyed, so
rebuilding picks up where it left off.
Vessels on order are counted separately from the census on purpose: they do
not exist yet, so the readout can say "4 of 6, 2 building" rather than
claiming six. The per-row note says where the vessels actually are, because
a plan can look unmet while everything it asked for is sitting at an
outpost.
The build menu goes to three columns past eight entries. #54 already
tightened its rows to 223px to fit a fourth, explicitly to avoid a
scrollable menu, and a ninth structure needs a fifth row that will not fit
at any spacing a button, a label and a cost can share. Three columns take
nine into three rows - fewer than it uses today, not more.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:21:52 +02:00
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func menu_columns(count: int) -> int:
|
|
|
|
|
return build_menu.menu_columns(count)
|
#106/#107 (#98a/#98b): the Planning Center
Landed together: a board of standing orders nothing acts on is a notepad,
and a planner with nowhere to read the plan from has nothing to do.
The targets are GLOBAL, not per building. There is one plan for the whole
game, and a second planning centre is another place to read and edit it
rather than a competing one - per-building targets would let two centres
hold contradictory orders and tell the crafters to satisfy both.
The census is the harder half. Every vessel is counted exactly once wherever
it happens to be: flying in the fleet, parked in the hangar, stationed at an
outpost, or in the air between any two of those. Miss the in-flight ones and
the planner queues a replacement for a vessel that is merely en route. The
fleet is the one place GameState cannot see, so Fleet pushes its composition
on every change, alongside the cargo capacity and distorder count that
already went that way.
Three rules keep the planner from fighting the player. It tops up and never
rewrites, so an order placed by hand stays exactly where it is. Everything
counts, including what is already queued, or it re-queues the same shortfall
every tick and buries the player in hulls. And nothing happens without a
planning centre - though the plan is remembered when one is destroyed, so
rebuilding picks up where it left off.
Vessels on order are counted separately from the census on purpose: they do
not exist yet, so the readout can say "4 of 6, 2 building" rather than
claiming six. The per-row note says where the vessels actually are, because
a plan can look unmet while everything it asked for is sitting at an
outpost.
The build menu goes to three columns past eight entries. #54 already
tightened its rows to 223px to fit a fourth, explicitly to avoid a
scrollable menu, and a ninth structure needs a fifth row that will not fit
at any spacing a button, a label and a cost can share. Three columns take
nine into three rows - fewer than it uses today, not more.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:21:52 +02:00
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func menu_rows(count: int) -> int:
|
|
|
|
|
return build_menu.menu_rows(count)
|
#106/#107 (#98a/#98b): the Planning Center
Landed together: a board of standing orders nothing acts on is a notepad,
and a planner with nowhere to read the plan from has nothing to do.
The targets are GLOBAL, not per building. There is one plan for the whole
game, and a second planning centre is another place to read and edit it
rather than a competing one - per-building targets would let two centres
hold contradictory orders and tell the crafters to satisfy both.
The census is the harder half. Every vessel is counted exactly once wherever
it happens to be: flying in the fleet, parked in the hangar, stationed at an
outpost, or in the air between any two of those. Miss the in-flight ones and
the planner queues a replacement for a vessel that is merely en route. The
fleet is the one place GameState cannot see, so Fleet pushes its composition
on every change, alongside the cargo capacity and distorder count that
already went that way.
Three rules keep the planner from fighting the player. It tops up and never
rewrites, so an order placed by hand stays exactly where it is. Everything
counts, including what is already queued, or it re-queues the same shortfall
every tick and buries the player in hulls. And nothing happens without a
planning centre - though the plan is remembered when one is destroyed, so
rebuilding picks up where it left off.
Vessels on order are counted separately from the census on purpose: they do
not exist yet, so the readout can say "4 of 6, 2 building" rather than
claiming six. The per-row note says where the vessels actually are, because
a plan can look unmet while everything it asked for is sitting at an
outpost.
The build menu goes to three columns past eight entries. #54 already
tightened its rows to 223px to fit a fourth, explicitly to avoid a
scrollable menu, and a ninth structure needs a fifth row that will not fit
at any spacing a button, a label and a cost can share. Three columns take
nine into three rows - fewer than it uses today, not more.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:21:52 +02:00
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func menu_row_top(row: int, rows: int, columns: int) -> float:
|
|
|
|
|
return build_menu.menu_row_top(row, rows, columns)
|
#138: structures and doctrines cost more than rocks, and more the deeper they are
Both halves of the ticket, in the shape vessels have used since gas bombers
and laser ships: a definition carries an ore figure and, when it wants more
than ore, a dictionary that IS the price. `building_costs()` and
`skill_costs()` answer it the way `product_costs()` already did, so there is
one question with one answer whatever is being bought, and every payment
path now goes through `spend_resources` rather than `spend`.
Seven of ten structures and thirteen of seventeen doctrines now want gas,
energy or dark matter alongside their rocks, and the totals climb with tree
depth - structures 16/31/58/62/96 by depth, doctrines 15/30/73/96/143/258/530.
The constraint that shaped the table is REACHABILITY, and it is the reason
some prices stayed ore-only. Energy exists only where a gas burner stands,
and a gas burner needs Power Systems - so nothing at or before that depth
may be priced in energy, or it is a wall with no way through. Dark matter is
rarer still and appears once, on the deepest node in the game. The extractor
stays rocks-only despite its depth allowing gas, because the quest chain
reaches it before it has sent the player for any.
test_progressive_costs checks those as properties rather than transcribing
the numbers, so rebalancing a price costs nothing while breaking the shape
fails: nothing unreachable at its own depth, prices rising with depth, the
ore field agreeing with the dictionary (repairs quote the field alone, since
a repair drone carries ore and nothing else), and the quest chain never
asking for a structure it has not funded.
Two things cleaned up on the way:
- The per-building cost constants in GameState are gone. Nothing but tests
used them, and they were a second copy of a number the definitions already
hold - exactly what would drift once a price gained a resource.
- research_for_building() returns a display NAME, and the id-keyed half of
the tree wants an id. That mix-up is silent - skill_depth() of a name finds
no node and answers 0, so a deep structure reads as an ungated one, which
is how the first draft of the test let an unreachable price through.
research_node_for_building() is the id, and the name is expressed from it.
PriceTag no longer rebuilds its labels when re-told the same price; it
refreshes the held figures instead. The craft menu re-states prices every
frame and the build menu now does too, so that was a dozen Labels being torn
down and rebuilt sixty times a second.
2026-08-28 15:30:30 +02:00
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func menu_column_spacing(columns: int) -> float:
|
|
|
|
|
return build_menu.menu_column_spacing(columns)
|
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 _show_building_info(type: String) -> void:
|
2026-08-14 21:44:28 +02:00
|
|
|
var definition: Resource = BUILDING_DEFINITIONS.get_definition(type)
|
|
|
|
|
if definition == null:
|
|
|
|
|
return
|
#138: structures and doctrines cost more than rocks, and more the deeper they are
Both halves of the ticket, in the shape vessels have used since gas bombers
and laser ships: a definition carries an ore figure and, when it wants more
than ore, a dictionary that IS the price. `building_costs()` and
`skill_costs()` answer it the way `product_costs()` already did, so there is
one question with one answer whatever is being bought, and every payment
path now goes through `spend_resources` rather than `spend`.
Seven of ten structures and thirteen of seventeen doctrines now want gas,
energy or dark matter alongside their rocks, and the totals climb with tree
depth - structures 16/31/58/62/96 by depth, doctrines 15/30/73/96/143/258/530.
The constraint that shaped the table is REACHABILITY, and it is the reason
some prices stayed ore-only. Energy exists only where a gas burner stands,
and a gas burner needs Power Systems - so nothing at or before that depth
may be priced in energy, or it is a wall with no way through. Dark matter is
rarer still and appears once, on the deepest node in the game. The extractor
stays rocks-only despite its depth allowing gas, because the quest chain
reaches it before it has sent the player for any.
test_progressive_costs checks those as properties rather than transcribing
the numbers, so rebalancing a price costs nothing while breaking the shape
fails: nothing unreachable at its own depth, prices rising with depth, the
ore field agreeing with the dictionary (repairs quote the field alone, since
a repair drone carries ore and nothing else), and the quest chain never
asking for a structure it has not funded.
Two things cleaned up on the way:
- The per-building cost constants in GameState are gone. Nothing but tests
used them, and they were a second copy of a number the definitions already
hold - exactly what would drift once a price gained a resource.
- research_for_building() returns a display NAME, and the id-keyed half of
the tree wants an id. That mix-up is silent - skill_depth() of a name finds
no node and answers 0, so a deep structure reads as an ungated one, which
is how the first draft of the test let an unreachable price through.
research_node_for_building() is the id, and the name is expressed from it.
PriceTag no longer rebuilds its labels when re-told the same price; it
refreshes the held figures instead. The craft menu re-states prices every
frame and the build menu now does too, so that was a dozen Labels being torn
down and rebuilt sixty times a second.
2026-08-28 15:30:30 +02:00
|
|
|
var costs: Dictionary = GameState.building_costs(type)
|
2026-08-14 21:44:28 +02:00
|
|
|
var lines: Array = [definition.description]
|
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 can_build: bool = true
|
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
|
|
|
if not GameState.is_building_unlocked(type):
|
|
|
|
|
lines.append("Locked. Research %s at your mothership." % GameState.research_for_building(type))
|
|
|
|
|
can_build = false
|
2026-08-14 21:44:28 +02:00
|
|
|
if can_build and definition.placement_mode == "planet":
|
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 planet: Node2D = _planet_under_fleet()
|
|
|
|
|
if planet == null:
|
|
|
|
|
lines.append("No planet in reach - fly over one.")
|
|
|
|
|
can_build = false
|
2026-08-15 14:41:19 +02:00
|
|
|
elif not GameState.is_planet_conquered(planet.planet_id):
|
|
|
|
|
lines.append("%s is not captured yet - take all its capture points." % planet.planet_name)
|
|
|
|
|
can_build = false
|
2026-08-14 22:16:40 +02:00
|
|
|
elif GameState.has_extractor_at(planet.planet_id):
|
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
|
|
|
lines.append("%s already has an extractor." % planet.planet_name)
|
|
|
|
|
can_build = false
|
|
|
|
|
else:
|
2026-08-31 02:18:08 +02:00
|
|
|
lines.append("Will mine %s from %s." % [
|
|
|
|
|
GameState.resource_name(planet.resource_type), planet.planet_name])
|
2026-08-14 21:44:28 +02:00
|
|
|
elif can_build and definition.unique and GameState.has_building_of_type(type):
|
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
|
|
|
lines.append("WARNING: only one may exist - this destroys your current one.")
|
2026-09-02 13:43:56 +02:00
|
|
|
if can_build and not GameState.can_afford_resources(costs, fleet.get_centroid()):
|
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
|
|
|
can_build = false
|
|
|
|
|
pending_build_type = type if can_build else ""
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.show_info(definition.display_name, "\n\n".join(lines), costs, can_build)
|
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 _close_info() -> void:
|
|
|
|
|
pending_build_type = ""
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.close_info()
|
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 _confirm_build() -> void:
|
|
|
|
|
var type: String = pending_build_type
|
|
|
|
|
if type == "":
|
|
|
|
|
return
|
|
|
|
|
pending_build_type = ""
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.close_info()
|
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
|
|
|
if not GameState.is_building_unlocked(type):
|
|
|
|
|
return
|
2026-08-14 21:44:28 +02:00
|
|
|
var definition: Resource = BUILDING_DEFINITIONS.get_definition(type)
|
|
|
|
|
if definition == null:
|
|
|
|
|
return
|
|
|
|
|
if definition.placement_mode == "planet":
|
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 planet: Node2D = _planet_under_fleet()
|
2026-08-14 22:16:40 +02:00
|
|
|
if planet == null or GameState.has_extractor_at(planet.planet_id):
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.set_status("No planet in reach any more")
|
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
|
|
|
return
|
2026-08-15 14:41:19 +02:00
|
|
|
if not GameState.is_planet_conquered(planet.planet_id):
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.set_status("%s is not captured yet" % planet.planet_name)
|
2026-08-15 14:41:19 +02:00
|
|
|
return
|
2026-08-16 20:14:29 +02:00
|
|
|
if String(planet.resource_type) == "":
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.set_status("%s has nothing to extract" % planet.planet_name)
|
2026-08-16 20:14:29 +02:00
|
|
|
return
|
2026-09-02 13:43:56 +02:00
|
|
|
if not GameState.spend_resources(GameState.building_costs(type), fleet.get_centroid()):
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.set_status(_cannot_afford(type))
|
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
|
|
|
return
|
2026-08-31 02:18:08 +02:00
|
|
|
GameState.add_extractor(planet.planet_name, planet.position,
|
|
|
|
|
planet.resource_type, planet.planet_id)
|
|
|
|
|
build_menu.close_menu()
|
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
|
|
|
return
|
2026-08-14 21:44:28 +02:00
|
|
|
if definition.unique and GameState.has_building_of_type(type):
|
2026-09-02 13:43:56 +02:00
|
|
|
if not GameState.can_afford_resources(GameState.building_costs(type), fleet.get_centroid()):
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.set_status(_cannot_afford(type))
|
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
|
|
|
return
|
2026-08-14 21:44:28 +02:00
|
|
|
GameState.remove_buildings_of_type(type)
|
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
|
|
|
building_field.rebuild_all()
|
#138: structures and doctrines cost more than rocks, and more the deeper they are
Both halves of the ticket, in the shape vessels have used since gas bombers
and laser ships: a definition carries an ore figure and, when it wants more
than ore, a dictionary that IS the price. `building_costs()` and
`skill_costs()` answer it the way `product_costs()` already did, so there is
one question with one answer whatever is being bought, and every payment
path now goes through `spend_resources` rather than `spend`.
Seven of ten structures and thirteen of seventeen doctrines now want gas,
energy or dark matter alongside their rocks, and the totals climb with tree
depth - structures 16/31/58/62/96 by depth, doctrines 15/30/73/96/143/258/530.
The constraint that shaped the table is REACHABILITY, and it is the reason
some prices stayed ore-only. Energy exists only where a gas burner stands,
and a gas burner needs Power Systems - so nothing at or before that depth
may be priced in energy, or it is a wall with no way through. Dark matter is
rarer still and appears once, on the deepest node in the game. The extractor
stays rocks-only despite its depth allowing gas, because the quest chain
reaches it before it has sent the player for any.
test_progressive_costs checks those as properties rather than transcribing
the numbers, so rebalancing a price costs nothing while breaking the shape
fails: nothing unreachable at its own depth, prices rising with depth, the
ore field agreeing with the dictionary (repairs quote the field alone, since
a repair drone carries ore and nothing else), and the quest chain never
asking for a structure it has not funded.
Two things cleaned up on the way:
- The per-building cost constants in GameState are gone. Nothing but tests
used them, and they were a second copy of a number the definitions already
hold - exactly what would drift once a price gained a resource.
- research_for_building() returns a display NAME, and the id-keyed half of
the tree wants an id. That mix-up is silent - skill_depth() of a name finds
no node and answers 0, so a deep structure reads as an ungated one, which
is how the first draft of the test let an unreachable price through.
research_node_for_building() is the id, and the name is expressed from it.
PriceTag no longer rebuilds its labels when re-told the same price; it
refreshes the held figures instead. The craft menu re-states prices every
frame and the build menu now does too, so that was a dozen Labels being torn
down and rebuilt sixty times a second.
2026-08-28 15:30:30 +02:00
|
|
|
_try_place(type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _cannot_afford(type: String) -> String:
|
|
|
|
|
var missing: Array = []
|
|
|
|
|
for resource_type in GameState.building_costs(type):
|
|
|
|
|
var id: String = String(resource_type)
|
|
|
|
|
var want: int = int(GameState.building_costs(type)[resource_type])
|
2026-09-02 13:43:56 +02:00
|
|
|
var have: int = GameState.available_resource_at(id, fleet.get_centroid())
|
#138: structures and doctrines cost more than rocks, and more the deeper they are
Both halves of the ticket, in the shape vessels have used since gas bombers
and laser ships: a definition carries an ore figure and, when it wants more
than ore, a dictionary that IS the price. `building_costs()` and
`skill_costs()` answer it the way `product_costs()` already did, so there is
one question with one answer whatever is being bought, and every payment
path now goes through `spend_resources` rather than `spend`.
Seven of ten structures and thirteen of seventeen doctrines now want gas,
energy or dark matter alongside their rocks, and the totals climb with tree
depth - structures 16/31/58/62/96 by depth, doctrines 15/30/73/96/143/258/530.
The constraint that shaped the table is REACHABILITY, and it is the reason
some prices stayed ore-only. Energy exists only where a gas burner stands,
and a gas burner needs Power Systems - so nothing at or before that depth
may be priced in energy, or it is a wall with no way through. Dark matter is
rarer still and appears once, on the deepest node in the game. The extractor
stays rocks-only despite its depth allowing gas, because the quest chain
reaches it before it has sent the player for any.
test_progressive_costs checks those as properties rather than transcribing
the numbers, so rebalancing a price costs nothing while breaking the shape
fails: nothing unreachable at its own depth, prices rising with depth, the
ore field agreeing with the dictionary (repairs quote the field alone, since
a repair drone carries ore and nothing else), and the quest chain never
asking for a structure it has not funded.
Two things cleaned up on the way:
- The per-building cost constants in GameState are gone. Nothing but tests
used them, and they were a second copy of a number the definitions already
hold - exactly what would drift once a price gained a resource.
- research_for_building() returns a display NAME, and the id-keyed half of
the tree wants an id. That mix-up is silent - skill_depth() of a name finds
no node and answers 0, so a deep structure reads as an ungated one, which
is how the first draft of the test let an unreachable price through.
research_node_for_building() is the id, and the name is expressed from it.
PriceTag no longer rebuilds its labels when re-told the same price; it
refreshes the held figures instead. The craft menu re-states prices every
frame and the build menu now does too, so that was a dozen Labels being torn
down and rebuilt sixty times a second.
2026-08-28 15:30:30 +02:00
|
|
|
if have < want:
|
|
|
|
|
missing.append("%s (need %d, have %d)" % [GameState.resource_name(id), want, have])
|
2026-08-31 02:18:08 +02:00
|
|
|
return "" if missing.is_empty() else "Not enough %s" % " and ".join(missing)
|
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
|
|
|
|
|
|
|
|
|
#138: structures and doctrines cost more than rocks, and more the deeper they are
Both halves of the ticket, in the shape vessels have used since gas bombers
and laser ships: a definition carries an ore figure and, when it wants more
than ore, a dictionary that IS the price. `building_costs()` and
`skill_costs()` answer it the way `product_costs()` already did, so there is
one question with one answer whatever is being bought, and every payment
path now goes through `spend_resources` rather than `spend`.
Seven of ten structures and thirteen of seventeen doctrines now want gas,
energy or dark matter alongside their rocks, and the totals climb with tree
depth - structures 16/31/58/62/96 by depth, doctrines 15/30/73/96/143/258/530.
The constraint that shaped the table is REACHABILITY, and it is the reason
some prices stayed ore-only. Energy exists only where a gas burner stands,
and a gas burner needs Power Systems - so nothing at or before that depth
may be priced in energy, or it is a wall with no way through. Dark matter is
rarer still and appears once, on the deepest node in the game. The extractor
stays rocks-only despite its depth allowing gas, because the quest chain
reaches it before it has sent the player for any.
test_progressive_costs checks those as properties rather than transcribing
the numbers, so rebalancing a price costs nothing while breaking the shape
fails: nothing unreachable at its own depth, prices rising with depth, the
ore field agreeing with the dictionary (repairs quote the field alone, since
a repair drone carries ore and nothing else), and the quest chain never
asking for a structure it has not funded.
Two things cleaned up on the way:
- The per-building cost constants in GameState are gone. Nothing but tests
used them, and they were a second copy of a number the definitions already
hold - exactly what would drift once a price gained a resource.
- research_for_building() returns a display NAME, and the id-keyed half of
the tree wants an id. That mix-up is silent - skill_depth() of a name finds
no node and answers 0, so a deep structure reads as an ungated one, which
is how the first draft of the test let an unreachable price through.
research_node_for_building() is the id, and the name is expressed from it.
PriceTag no longer rebuilds its labels when re-told the same price; it
refreshes the held figures instead. The craft menu re-states prices every
frame and the build menu now does too, so that was a dozen Labels being torn
down and rebuilt sixty times a second.
2026-08-28 15:30:30 +02:00
|
|
|
func _try_place(type: String) -> void:
|
2026-09-02 13:43:56 +02:00
|
|
|
if not GameState.spend_resources(GameState.building_costs(type), fleet.get_centroid()):
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.set_status(_cannot_afford(type))
|
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
|
|
|
return
|
|
|
|
|
building_field.place(type, fleet.get_centroid())
|
2026-08-31 02:18:08 +02:00
|
|
|
build_menu.close_menu()
|
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
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _on_unlock_requested(id: String) -> void:
|
2026-09-02 13:43:56 +02:00
|
|
|
var research_building: Dictionary = GameState.find_building(skill_menu.building_id)
|
|
|
|
|
if research_building.is_empty():
|
|
|
|
|
skill_menu.set_status("The research building is no longer available")
|
|
|
|
|
return
|
|
|
|
|
var payment_position: Variant = research_building.get("position", null)
|
|
|
|
|
if not GameState.unlock(id, payment_position):
|
2026-09-06 16:24:09 +02:00
|
|
|
# A doctrine can be refused for several reasons, but only one of them
|
|
|
|
|
# is invisible on the node itself: the price in ORE is written there,
|
|
|
|
|
# the prerequisite is drawn as an unlit link, and research data is
|
|
|
|
|
# just a number the player has never been told how to get.
|
|
|
|
|
var points_short: int = GameState.skill_research_point_cost(id)
|
|
|
|
|
if GameState.research_points < points_short:
|
|
|
|
|
research_points_menu.open_for_shortfall(points_short,
|
|
|
|
|
String(GameState.skill_node(id).get("name", "This doctrine")))
|
2026-08-31 02:18:08 +02:00
|
|
|
skill_menu.refresh(id)
|
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
|
|
|
return
|
|
|
|
|
if int(GameState.skill_node(id).get("fleet_bonus", 0)) > 0:
|
|
|
|
|
fleet.apply_formation()
|
2026-08-31 02:18:08 +02:00
|
|
|
skill_menu.research_completed(id)
|
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
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _on_unlock_pressed() -> void:
|
|
|
|
|
var id: String = skill_menu.selected_id()
|
|
|
|
|
if id != "":
|
|
|
|
|
_on_unlock_requested(id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _refresh_skill_menu(id: String = "") -> void:
|
|
|
|
|
skill_menu.refresh(id)
|
2026-08-11 20:17:58 +02:00
|
|
|
|
|
|
|
|
|
2026-08-15 04:45:15 +02:00
|
|
|
func _format_costs(costs: Dictionary) -> String:
|
2026-08-31 02:18:08 +02:00
|
|
|
var parts := PackedStringArray()
|
2026-08-15 04:45:15 +02:00
|
|
|
for resource_type in GameState.resource_types():
|
|
|
|
|
var amount: int = int(costs.get(resource_type, 0))
|
|
|
|
|
if amount > 0:
|
2026-08-31 02:18:08 +02:00
|
|
|
parts.append("%d %s" % [amount,
|
|
|
|
|
GameState.resource_name(resource_type).to_lower()])
|
2026-08-15 04:45:15 +02:00
|
|
|
return " · ".join(parts) if not parts.is_empty() else "free"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _format_missing_costs(costs: Dictionary) -> String:
|
2026-08-31 02:18:08 +02:00
|
|
|
var parts := PackedStringArray()
|
2026-08-15 04:45:15 +02:00
|
|
|
for resource_type in GameState.resource_types():
|
|
|
|
|
var amount: int = int(costs.get(resource_type, 0))
|
|
|
|
|
if amount > 0:
|
|
|
|
|
parts.append("%d more %s" % [amount,
|
|
|
|
|
GameState.resource_name(resource_type).to_lower()])
|
|
|
|
|
return " · ".join(parts)
|
|
|
|
|
|
|
|
|
|
|
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 _on_craft_interceptor() -> void:
|
2026-08-14 22:12:57 +02:00
|
|
|
_on_craft_vessel(GameState.VESSEL_INTERCEPTOR)
|
|
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _on_craft_shield() -> void:
|
|
|
|
|
_on_craft_vessel(GameState.VESSEL_SHIELD)
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 22:12:57 +02:00
|
|
|
func _on_craft_vessel(vessel: String) -> void:
|
Buildings produce from their own stock, and queue orders until supplied
Forgejo #16, part 3 of #12. BuildController deducted the cost from the
bank at the moment of purchase and refused outright with "Not enough
rocks", so nothing ever had to be delivered anywhere. Each building now
carries its own stock, and an order it cannot pay for yet is QUEUED and
completes by itself once ore arrives.
Records get an `id` because a building has to be referable across the
space <-> planet-surface scene swap, when every Building NODE has been
destroyed and rebuilt. Position would nearly work and then fail on two
buildings placed on the same spot.
Production advances in the GameState autoload, the same way extractor
haulers already do, so an order can complete while the player is down a
gravity well. A finished vessel becomes `ships_owed` rather than being
handed straight to a Fleet that may not exist, and the fleet claims what
it is owed when it next has room.
The craft menu remembers which crafter it was opened for: _process()
clears current_building while any menu is open, so by the next frame the
menu would no longer know. It shows the ore delivered HERE rather than
the bank, since showing the bank would promise something the crafter
cannot actually build.
Ship crafting cannot be supplied until #17 lands - nothing yet moves ore
into a building.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:18:44 +02:00
|
|
|
if craft_building_id == 0:
|
2026-08-31 02:18:08 +02:00
|
|
|
craft_menu.say("No crafter selected - reopen this panel from the building")
|
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
|
|
|
return
|
2026-08-14 22:12:57 +02:00
|
|
|
var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel)
|
|
|
|
|
if definition == null:
|
|
|
|
|
return
|
|
|
|
|
if not GameState.queue_order(craft_building_id, vessel):
|
|
|
|
|
if not GameState.vessel_is_unlocked(vessel):
|
2026-08-31 02:18:08 +02:00
|
|
|
craft_menu.say("%s must be unlocked at a ship lab first" % definition.display_name)
|
2026-08-14 22:12:57 +02:00
|
|
|
return
|
2026-08-15 04:45:15 +02:00
|
|
|
var missing: Dictionary = GameState.outstanding_costs(craft_building_id)
|
2026-08-31 02:18:08 +02:00
|
|
|
if missing.is_empty():
|
|
|
|
|
craft_menu.say("%s ordered - building now" % GameState.vessel_name(vessel))
|
2026-08-11 22:50:09 +02:00
|
|
|
else:
|
2026-08-31 02:18:08 +02:00
|
|
|
craft_menu.say("%s queued - deliver %s here" % [GameState.vessel_name(vessel),
|
|
|
|
|
_format_missing_costs(missing)])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_queue_slot(index: int) -> void:
|
|
|
|
|
var orders: Array = GameState.order_list(craft_building_id)
|
|
|
|
|
if index >= orders.size():
|
|
|
|
|
return
|
|
|
|
|
var vessel: String = GameState.vessel_name(String(orders[index].product))
|
|
|
|
|
var refunded: bool = bool(orders[index].started)
|
|
|
|
|
if GameState.cancel_order(craft_building_id, index):
|
|
|
|
|
craft_menu.say("%s cancelled%s" % [vessel,
|
|
|
|
|
" - resources refunded here" if refunded else ""])
|
|
|
|
|
|
2026-08-11 22:50:09 +02:00
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _refresh_craft_status() -> void:
|
|
|
|
|
if craft_building_id != 0 and craft_menu.building_id != craft_building_id:
|
|
|
|
|
craft_menu.building_id = craft_building_id
|
|
|
|
|
craft_menu.resource_bar.building_id = craft_building_id
|
|
|
|
|
craft_menu.refresh()
|
2026-08-11 22:50:09 +02:00
|
|
|
|
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 _lab_options() -> Array:
|
|
|
|
|
var options: Array = []
|
2026-08-14 22:12:57 +02:00
|
|
|
var definition: Resource = VESSEL_DEFINITIONS.get_definition(lab_vessel)
|
|
|
|
|
if definition == null:
|
|
|
|
|
return options
|
|
|
|
|
if not GameState.vessel_is_unlocked(lab_vessel):
|
|
|
|
|
options.append({"unlock": true, "id": definition.unlock_id,
|
|
|
|
|
"name": definition.unlock_name, "desc": definition.unlock_description,
|
2026-08-15 04:45:15 +02:00
|
|
|
"costs": GameState.research_costs(definition.unlock_id),
|
|
|
|
|
"icon": definition.unlock_icon if definition.unlock_icon >= 0 else definition.icon})
|
2026-08-13 16:45:08 +02:00
|
|
|
return options
|
2026-08-31 02:18:08 +02:00
|
|
|
for upgrade in GameState.upgrades_for(lab_vessel):
|
|
|
|
|
options.append({"unlock": false, "id": upgrade.id,
|
|
|
|
|
"name": upgrade.name, "desc": upgrade.desc})
|
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
|
|
|
return options
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_lab_vessel(index: int) -> void:
|
2026-08-14 22:12:57 +02:00
|
|
|
var definitions: Array = VESSEL_DEFINITIONS.all()
|
2026-08-31 02:18:08 +02:00
|
|
|
if index >= 0 and index < definitions.size():
|
|
|
|
|
lab_vessel = definitions[index].definition_id
|
|
|
|
|
lab_choice = 0
|
|
|
|
|
_refresh_lab()
|
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
|
|
|
|
|
|
|
|
|
2026-08-13 16:45:08 +02:00
|
|
|
func _on_lab_upgrade(index: int) -> void:
|
|
|
|
|
if index < 0 or index >= _lab_options().size():
|
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
|
|
|
return
|
Implement #160, #161, #162, #163, #164 and #165
#162 - a single tap bought a research node outright. One physical tap arrives
TWICE: Godot emulates a mouse button from every touch AND a touch from every
mouse button, both project settings are on, and the menu's dispatcher routes
InputEventScreenTouch and InputEventMouseButton alike to the tree. The second
arrival found the node already selected and took itself for the confirming
tap. Measured, not reasoned about - one tap produced two picks and two
confirms.
What is ignored is specifically the TWIN: same frame, same place, arriving
through the other input kind. Deduping on frame and position alone also
swallowed two genuine taps in one frame, which the test caught.
#164 - the build menu predicted a caption's height from the COLUMN WIDTH:
wide column, assume one line. Right for most names and wrong for the long
ones, so "Spacial Workshop" wrapped onto the price beneath it. Captions are
now measured against the font that draws them, and prices sized from
PriceTag.wanted_height(), which existed for exactly this and was not being
asked. Prices are set before anything is measured, because the old order laid
out first and priced afterwards.
The ship lab: its buy button is gone and a second tap on an option buys it,
the same rule as the research tree; the explanation drops into the room that
freed; research points move up into the resource table, where everything else
being spent already is; the second row of options is placed from the first
row's measured bottom rather than a constant; and the vessel strip is spaced
from the panel's own width, so the eighth vessel is no longer half off the
right edge.
#160 - the quest panel was drawn BEFORE the sticks and the pivot pair, so
they sat on top of it. Moved after them, under the narration banner. Its title
rule ran straight through the close button; the header band is now as tall as
that button.
#161 - the prologue widens the formation into a freight convoy by writing over
GameState.formation, and nothing put it back. The player flew the convoy's
600-unit spread for the rest of the game - a shape nobody chose, which reads
as the fleet having drifted apart.
#163 - a quest to station a vessel at the outpost, after the one that asks for
the outpost. Building one and crewing it are different acts, and an empty
outpost holds nothing.
#165 - the gas bomb's blast and a vessel arriving fast both damage a
structure. The blast used to stop politely at anything the player owned, which
made a bomb dropped on a besieged mothership a free way to sweep the attackers
off it.
Collisions are caught on ENTRY, not on the 0.7s strike tick: a vessel is only
moving fast for the instant it arrives, and a ship sent in at 900 did no
damage at all until this moved. Docking is free - the fleet sits on its own
mothership constantly to bank ore - and garrison vessels are exempt outright,
because they launch from their outpost and re-dock with it at speed. The suite
caught that one: the ram rule was destroying the garrison as it launched.
Suite 153/153. Four tests changed because what they pinned moved: the lab's
buy button, the intro's formation, the research tree's tap delivery, and the
build menu's own preview.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 02:27:44 +02:00
|
|
|
var confirming: bool = index == lab_choice
|
2026-08-13 16:45:08 +02:00
|
|
|
lab_choice = index
|
2026-08-31 02:18:08 +02:00
|
|
|
lab_menu.set_status("")
|
Implement #160, #161, #162, #163, #164 and #165
#162 - a single tap bought a research node outright. One physical tap arrives
TWICE: Godot emulates a mouse button from every touch AND a touch from every
mouse button, both project settings are on, and the menu's dispatcher routes
InputEventScreenTouch and InputEventMouseButton alike to the tree. The second
arrival found the node already selected and took itself for the confirming
tap. Measured, not reasoned about - one tap produced two picks and two
confirms.
What is ignored is specifically the TWIN: same frame, same place, arriving
through the other input kind. Deduping on frame and position alone also
swallowed two genuine taps in one frame, which the test caught.
#164 - the build menu predicted a caption's height from the COLUMN WIDTH:
wide column, assume one line. Right for most names and wrong for the long
ones, so "Spacial Workshop" wrapped onto the price beneath it. Captions are
now measured against the font that draws them, and prices sized from
PriceTag.wanted_height(), which existed for exactly this and was not being
asked. Prices are set before anything is measured, because the old order laid
out first and priced afterwards.
The ship lab: its buy button is gone and a second tap on an option buys it,
the same rule as the research tree; the explanation drops into the room that
freed; research points move up into the resource table, where everything else
being spent already is; the second row of options is placed from the first
row's measured bottom rather than a constant; and the vessel strip is spaced
from the panel's own width, so the eighth vessel is no longer half off the
right edge.
#160 - the quest panel was drawn BEFORE the sticks and the pivot pair, so
they sat on top of it. Moved after them, under the narration banner. Its title
rule ran straight through the close button; the header band is now as tall as
that button.
#161 - the prologue widens the formation into a freight convoy by writing over
GameState.formation, and nothing put it back. The player flew the convoy's
600-unit spread for the rest of the game - a shape nobody chose, which reads
as the fleet having drifted apart.
#163 - a quest to station a vessel at the outpost, after the one that asks for
the outpost. Building one and crewing it are different acts, and an empty
outpost holds nothing.
#165 - the gas bomb's blast and a vessel arriving fast both damage a
structure. The blast used to stop politely at anything the player owned, which
made a bomb dropped on a besieged mothership a free way to sweep the attackers
off it.
Collisions are caught on ENTRY, not on the 0.7s strike tick: a vessel is only
moving fast for the instant it arrives, and a ship sent in at 900 did no
damage at all until this moved. Docking is free - the fleet sits on its own
mothership constantly to bank ore - and garrison vessels are exempt outright,
because they launch from their outpost and re-dock with it at speed. The suite
caught that one: the ram rule was destroying the garrison as it launched.
Suite 153/153. Four tests changed because what they pinned moved: the lab's
buy button, the intro's formation, the research tree's tap delivery, and the
build menu's own preview.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 02:27:44 +02:00
|
|
|
if confirming and lab_can_buy:
|
|
|
|
|
_on_lab_buy()
|
2026-08-31 02:18:08 +02:00
|
|
|
else:
|
|
|
|
|
_refresh_lab()
|
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 _on_lab_buy() -> void:
|
|
|
|
|
var options: Array = _lab_options()
|
|
|
|
|
if options.is_empty():
|
|
|
|
|
return
|
2026-08-31 02:18:08 +02:00
|
|
|
var selected: Dictionary = options[lab_choice]
|
|
|
|
|
var id: String = String(selected.id)
|
2026-09-06 16:24:09 +02:00
|
|
|
var lab_points: int = GameState.research_point_cost(id)
|
|
|
|
|
if GameState.research_points < lab_points:
|
|
|
|
|
# Checked BEFORE the queue call, which would otherwise refuse this
|
|
|
|
|
# and report it as "already queued or complete" - which it is not.
|
|
|
|
|
research_points_menu.open_for_shortfall(lab_points, String(selected.name))
|
|
|
|
|
return
|
2026-08-13 22:04:21 +02:00
|
|
|
if not GameState.queue_lab_research(lab_building_id, id):
|
2026-08-31 02:18:08 +02:00
|
|
|
lab_menu.set_status("Research is already queued or complete")
|
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
|
|
|
return
|
2026-08-31 02:18:08 +02:00
|
|
|
lab_menu.set_status("%s queued - deliver %s to this lab" % [
|
|
|
|
|
selected.name, _format_costs(GameState.research_costs(id))])
|
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
|
|
|
_refresh_lab()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _refresh_lab() -> void:
|
2026-09-03 00:12:08 +02:00
|
|
|
lab_menu.set_status("")
|
|
|
|
|
lab_menu.hide_research_progress()
|
2026-08-14 22:12:57 +02:00
|
|
|
var definitions: Array = VESSEL_DEFINITIONS.all()
|
2026-08-31 02:18:08 +02:00
|
|
|
lab_menu.render_vessels(definitions, lab_vessel)
|
|
|
|
|
lab_menu.vessel_heading.text = "%s research · %d RP" % [
|
2026-08-16 20:14:29 +02:00
|
|
|
GameState.vessel_name(lab_vessel), GameState.research_points]
|
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
|
|
|
var options: Array = _lab_options()
|
2026-08-31 02:18:08 +02:00
|
|
|
lab_choice = clampi(lab_choice, 0, maxi(options.size() - 1, 0))
|
|
|
|
|
var rendered: Array = []
|
|
|
|
|
for option in options:
|
#136, #137, #134: one build grid that cannot stack, one way to show a price
#136 - the gas burner drawn on top of the transport hub. The build menu's
rows were a written-down list of tops read with `tops[mini(row, size - 1)]`,
and with every structure researched the menu wanted a fourth row where the
list had three, so the last entry was placed on the third row's centre slot.
A list that silently stacks whatever will not fit in it is not a layout.
Rows are computed now, from a first top and a pitch that tightens to fit and
is floored at the row's own height - so two rows cannot occupy the same
space however far the catalog grows - and the column count widens rather
than adding a row the band cannot hold. Ten structures now sit in four
columns of three rows. The test unlocks the whole tree and checks every pair
of entries for overlap, which is the shape of the bug rather than a
coordinate that happens to be right today.
#137 - the game had three answers to "what does this cost". The build menu
drew a NeonNumber under each option, the craft menu drew glyph strips, and
the ship lab spelled prices out inside the caption. PriceTag is that answer
once: the resource's glyph, then what you HOLD over what it COSTS. It
replaces all three, plus the info popup and the skill panel, and
VesselPriceStrip - which was the same idea for one menu - goes with them.
The build menu also gains the resource bar the other two already had, in the
same place and at the same size, and the craft menu's - the only one not
running the full width of its panel - now matches. The bar drops its LOCAL
column where there is no building to report on, for the same reason #95
dropped it where it only repeated BANKED.
#134 - the skill panel's explanation and its price were both centred in the
same band, so a long description landed on the number. The text is the left
of the band now and the tag is the right. The shortfall it used to spell out
in a sentence - one per case, ore or research points - is the red on the tag
instead, and research points ride along as an entry of their own, so the tag
says WHICH thing is missing without a line of prose per possibility.
One trap worth recording: PriceTag and ResourceBar read the GameState
autoload, and an `extends SceneTree` test script that so much as names such
a class - or preloads the file - fails to compile, because GameState is not
an identifier in that context. GDScript then caches the failure, so the
scene the test instantiates gets a Control with no script where its resource
bar should be. The tests compare script PATHS, as strings.
2026-08-28 15:05:20 +02:00
|
|
|
var maxed: bool = not bool(option.unlock) and GameState.upgrade_maxed(option.id)
|
2026-08-13 16:45:08 +02:00
|
|
|
var level_note: String = "" if bool(option.unlock) else " %d/%d" % [
|
|
|
|
|
GameState.upgrade_level(option.id), int(GameState.upgrade_def(option.id).max_level)]
|
2026-08-31 02:18:08 +02:00
|
|
|
rendered.append({"label": "%s%s" % [option.name, level_note],
|
|
|
|
|
"costs": option.costs if bool(option.unlock) else GameState.research_costs(option.id),
|
|
|
|
|
"points": GameState.research_point_cost(String(option.id)),
|
|
|
|
|
"icon": int(option.get("icon", IconButton.Icon.SHIP_LAB)), "maxed": maxed})
|
|
|
|
|
var definition: Resource = VESSEL_DEFINITIONS.get_definition(lab_vessel)
|
|
|
|
|
lab_menu.render_options(rendered, lab_choice,
|
|
|
|
|
definition.glow_color if definition != null else Color(0.25, 0.85, 1.0))
|
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
|
|
|
if options.is_empty():
|
2026-08-31 02:18:08 +02:00
|
|
|
lab_menu.clear_detail()
|
Implement #160, #161, #162, #163, #164 and #165
#162 - a single tap bought a research node outright. One physical tap arrives
TWICE: Godot emulates a mouse button from every touch AND a touch from every
mouse button, both project settings are on, and the menu's dispatcher routes
InputEventScreenTouch and InputEventMouseButton alike to the tree. The second
arrival found the node already selected and took itself for the confirming
tap. Measured, not reasoned about - one tap produced two picks and two
confirms.
What is ignored is specifically the TWIN: same frame, same place, arriving
through the other input kind. Deduping on frame and position alone also
swallowed two genuine taps in one frame, which the test caught.
#164 - the build menu predicted a caption's height from the COLUMN WIDTH:
wide column, assume one line. Right for most names and wrong for the long
ones, so "Spacial Workshop" wrapped onto the price beneath it. Captions are
now measured against the font that draws them, and prices sized from
PriceTag.wanted_height(), which existed for exactly this and was not being
asked. Prices are set before anything is measured, because the old order laid
out first and priced afterwards.
The ship lab: its buy button is gone and a second tap on an option buys it,
the same rule as the research tree; the explanation drops into the room that
freed; research points move up into the resource table, where everything else
being spent already is; the second row of options is placed from the first
row's measured bottom rather than a constant; and the vessel strip is spaced
from the panel's own width, so the eighth vessel is no longer half off the
right edge.
#160 - the quest panel was drawn BEFORE the sticks and the pivot pair, so
they sat on top of it. Moved after them, under the narration banner. Its title
rule ran straight through the close button; the header band is now as tall as
that button.
#161 - the prologue widens the formation into a freight convoy by writing over
GameState.formation, and nothing put it back. The player flew the convoy's
600-unit spread for the rest of the game - a shape nobody chose, which reads
as the fleet having drifted apart.
#163 - a quest to station a vessel at the outpost, after the one that asks for
the outpost. Building one and crewing it are different acts, and an empty
outpost holds nothing.
#165 - the gas bomb's blast and a vessel arriving fast both damage a
structure. The blast used to stop politely at anything the player owned, which
made a bomb dropped on a besieged mothership a free way to sweep the attackers
off it.
Collisions are caught on ENTRY, not on the 0.7s strike tick: a vessel is only
moving fast for the instant it arrives, and a ship sent in at 900 did no
damage at all until this moved. Docking is free - the fleet sits on its own
mothership constantly to bank ore - and garrison vessels are exempt outright,
because they launch from their outpost and re-dock with it at speed. The suite
caught that one: the ram rule was destroying the garrison as it launched.
Suite 153/153. Four tests changed because what they pinned moved: the lab's
buy button, the intro's formation, the research tree's tap delivery, and the
build menu's own preview.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 02:27:44 +02:00
|
|
|
lab_can_buy = false
|
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
|
|
|
return
|
2026-08-31 02:18:08 +02:00
|
|
|
var selected: Dictionary = options[lab_choice]
|
2026-09-05 23:13:23 +02:00
|
|
|
lab_menu.show_detail(String(selected.name), String(selected.desc))
|
2026-08-13 22:04:21 +02:00
|
|
|
var queued: Dictionary = GameState.lab_research_order(lab_building_id)
|
2026-08-31 02:18:08 +02:00
|
|
|
lab_can_buy = queued.is_empty() and (selected.unlock \
|
|
|
|
|
or not GameState.upgrade_maxed(selected.id))
|
2026-08-13 22:04:21 +02:00
|
|
|
if not queued.is_empty():
|
2026-08-16 20:14:29 +02:00
|
|
|
var queued_points: int = GameState.research_point_cost(String(queued.id))
|
2026-09-03 00:12:08 +02:00
|
|
|
var progress: float = float(queued.elapsed) \
|
|
|
|
|
/ maxf(GameState.lab_research_time(), 0.001)
|
2026-08-16 20:14:29 +02:00
|
|
|
if not bool(queued.started) and GameState.research_points < queued_points:
|
2026-09-03 00:12:08 +02:00
|
|
|
lab_menu.show_research_progress(
|
|
|
|
|
"Need %d research point%s (have %d)" % [queued_points,
|
|
|
|
|
"" if queued_points == 1 else "s", GameState.research_points], progress)
|
2026-08-16 20:14:29 +02:00
|
|
|
else:
|
2026-09-03 00:12:08 +02:00
|
|
|
lab_menu.show_research_progress(
|
|
|
|
|
"Researching" if bool(queued.started) else "Waiting for resources",
|
|
|
|
|
progress)
|
2026-08-12 13:07:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_transfer_pressed() -> void:
|
2026-08-14 22:21:01 +02:00
|
|
|
if current_building == null and current_extractor_index < 0:
|
2026-08-12 13:07:52 +02:00
|
|
|
return
|
2026-08-14 22:21:01 +02:00
|
|
|
transfer_building_id = current_building.building_id if current_building != null else 0
|
|
|
|
|
transfer_extractor_index = current_extractor_index
|
2026-08-31 02:18:08 +02:00
|
|
|
transfer_menu.open_destination(transfer_building_id, transfer_extractor_index)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _clear_transfer_context() -> void:
|
|
|
|
|
transfer_building_id = 0
|
|
|
|
|
transfer_extractor_index = -1
|
2026-08-12 13:07:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_transfer(to_building: bool, amount: int) -> void:
|
2026-08-14 11:54:31 +02:00
|
|
|
_on_resource_transfer(GameState.RESOURCE_ORE, to_building, amount)
|
|
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _on_gas_transfer(to_building: bool) -> void:
|
|
|
|
|
_on_resource_transfer(GameState.RESOURCE_GAS, to_building)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_resource_transfer(resource_type: String, to_building: bool,
|
|
|
|
|
amount: int = ALL) -> void:
|
2026-08-14 22:21:01 +02:00
|
|
|
if transfer_building_id == 0 and transfer_extractor_index < 0:
|
2026-08-31 02:18:08 +02:00
|
|
|
transfer_menu.show_status("No resource store selected - reopen this panel from its action")
|
2026-08-12 13:07:52 +02:00
|
|
|
return
|
2026-08-14 22:21:01 +02:00
|
|
|
var moved: int
|
|
|
|
|
if transfer_extractor_index >= 0:
|
2026-08-31 02:18:08 +02:00
|
|
|
moved = GameState.transfer_resource_to_extractor(transfer_extractor_index,
|
|
|
|
|
resource_type, amount) if to_building else \
|
|
|
|
|
GameState.transfer_resource_from_extractor(transfer_extractor_index,
|
|
|
|
|
resource_type, amount)
|
2026-08-14 22:21:01 +02:00
|
|
|
else:
|
2026-08-31 02:18:08 +02:00
|
|
|
moved = GameState.transfer_resource_to_building(transfer_building_id,
|
|
|
|
|
resource_type, amount) if to_building else \
|
|
|
|
|
GameState.transfer_resource_to_fleet(transfer_building_id,
|
|
|
|
|
resource_type, amount)
|
2026-08-14 11:54:31 +02:00
|
|
|
var resource_name: String = GameState.resource_name(resource_type).to_lower()
|
2026-08-12 13:07:52 +02:00
|
|
|
if moved <= 0:
|
2026-08-31 02:18:08 +02:00
|
|
|
transfer_menu.show_status("The fleet is carrying nothing to unload (no %s)" \
|
|
|
|
|
% resource_name if to_building else "%s has no %s to load" \
|
|
|
|
|
% [_transfer_place_name().capitalize(), resource_name])
|
2026-08-12 13:07:52 +02:00
|
|
|
elif to_building:
|
2026-08-31 02:18:08 +02:00
|
|
|
transfer_menu.show_status("Moved %d %s into %s" % [
|
|
|
|
|
moved, resource_name, _transfer_place_name()])
|
2026-08-12 13:07:52 +02:00
|
|
|
else:
|
2026-08-31 02:18:08 +02:00
|
|
|
transfer_menu.show_status("Loaded %d %s into the fleet" % [moved, resource_name])
|
|
|
|
|
transfer_menu.refresh()
|
2026-08-13 22:17:24 +02:00
|
|
|
|
2026-08-12 13:07:52 +02:00
|
|
|
|
|
|
|
|
func _transfer_place_name() -> String:
|
2026-08-31 02:18:08 +02:00
|
|
|
return transfer_menu.place_name()
|
2026-08-12 13:07:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _refresh_transfer() -> void:
|
2026-08-31 02:18:08 +02:00
|
|
|
if transfer_menu.building_id != transfer_building_id \
|
|
|
|
|
or transfer_menu.extractor_index != transfer_extractor_index:
|
|
|
|
|
transfer_menu.building_id = transfer_building_id
|
|
|
|
|
transfer_menu.extractor_index = transfer_extractor_index
|
|
|
|
|
transfer_menu.refresh()
|
2026-08-12 22:47:46 +02:00
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _on_hangar_pressed() -> void:
|
|
|
|
|
hangar_menu.open_for(fleet)
|
2026-08-12 22:47:46 +02:00
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _hangar_types() -> Array:
|
|
|
|
|
var types: Array = []
|
|
|
|
|
for definition in VESSEL_DEFINITIONS.fleet():
|
|
|
|
|
types.append(String(definition.definition_id))
|
|
|
|
|
return types
|
2026-08-12 22:47:46 +02:00
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _resolve_hangar_type(selection: Variant) -> String:
|
|
|
|
|
if selection is String:
|
|
|
|
|
return String(selection)
|
|
|
|
|
var index: int = int(selection)
|
|
|
|
|
var types: Array = _hangar_types()
|
|
|
|
|
return String(types[index]) if index >= 0 and index < types.size() else ""
|
2026-08-12 22:47:46 +02:00
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _on_hangar_crew_type(selection: Variant) -> void:
|
|
|
|
|
var vessel_type: String = _resolve_hangar_type(selection)
|
|
|
|
|
if vessel_type == "":
|
|
|
|
|
return
|
2026-08-12 22:47:46 +02:00
|
|
|
if fleet.ships.size() >= GameState.max_fleet_size():
|
2026-08-31 02:18:08 +02:00
|
|
|
hangar_menu.set_status("Fleet is full (%d/%d) - research capacity at the mothership" \
|
|
|
|
|
% [fleet.ships.size(), GameState.max_fleet_size()])
|
2026-08-12 22:47:46 +02:00
|
|
|
return
|
2026-08-31 02:18:08 +02:00
|
|
|
var stored_index: int = GameState.stored_vessels.find(vessel_type)
|
2026-08-12 23:08:43 +02:00
|
|
|
if stored_index < 0:
|
2026-08-31 02:18:08 +02:00
|
|
|
hangar_menu.set_status("No %s available" % GameState.vessel_name(vessel_type))
|
2026-08-12 23:08:43 +02:00
|
|
|
return
|
|
|
|
|
if not fleet.add_ship(GameState.take_stored_vessel(stored_index)):
|
2026-08-31 02:18:08 +02:00
|
|
|
hangar_menu.set_status("Could not crew that vessel")
|
2026-08-12 22:47:46 +02:00
|
|
|
return
|
2026-08-31 02:18:08 +02:00
|
|
|
hangar_menu.set_status("%s joined the fleet" % GameState.vessel_name(vessel_type))
|
|
|
|
|
hangar_menu.refresh(fleet)
|
2026-08-12 23:08:43 +02:00
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _on_hangar_store_type(selection: Variant) -> void:
|
|
|
|
|
var vessel_type: String = _resolve_hangar_type(selection)
|
|
|
|
|
if vessel_type == "":
|
|
|
|
|
return
|
2026-08-12 23:08:43 +02:00
|
|
|
var ship_index: int = -1
|
|
|
|
|
for i in range(fleet.ships.size()):
|
2026-08-31 02:18:08 +02:00
|
|
|
if String(fleet.ships[i].vessel_type) == vessel_type:
|
2026-08-12 23:08:43 +02:00
|
|
|
ship_index = i
|
|
|
|
|
break
|
|
|
|
|
if ship_index < 0:
|
2026-08-31 02:18:08 +02:00
|
|
|
hangar_menu.set_status("No %s in the fleet" % GameState.vessel_name(vessel_type))
|
2026-08-12 23:08:43 +02:00
|
|
|
return
|
|
|
|
|
var stored: String = fleet.store_ship(ship_index)
|
|
|
|
|
if stored == "":
|
2026-08-31 02:18:08 +02:00
|
|
|
hangar_menu.set_status("Keep one ship deployed")
|
2026-08-12 23:08:43 +02:00
|
|
|
return
|
|
|
|
|
GameState.stored_vessels.append(stored)
|
2026-08-31 02:18:08 +02:00
|
|
|
hangar_menu.set_status("%s stored in the hangar" % GameState.vessel_name(stored))
|
|
|
|
|
hangar_menu.refresh(fleet)
|
2026-08-12 23:08:43 +02:00
|
|
|
|
2026-08-23 21:59:24 +02:00
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _refresh_hangar() -> void:
|
|
|
|
|
hangar_menu.refresh(fleet)
|
Stacking distorders, menu layouts, lab rewards, enemy detail, artillery cross
DISTORDER STACKING. Each space-time distorder vessel past the first raises
the fleet's speed ceiling by 1.0, so a second one is worth crewing instead of
being dead weight. GameState gains max_fleet_speed_multiplier, pushed by
Fleet whenever composition changes - the same direction as every other
fleet-derived figure, since GameState does not reach into the scene tree to
count ships. The energy drain is normalised against the CURRENT ceiling, so
full boost costs the same per second however wide it is: extra vessels buy
more speed, not cheaper speed. Losing one re-clamps an over-ceiling boost
rather than leaving the fleet travelling faster than its crew can sustain.
TRANSFER MENU. Rows overlapped, and the last one ran into the hint text.
row_height is divided from the available space, but each row's contents used
a fixed pixel ladder - glyph at +10, buttons at +88, selector at +142 - which
needs about 244px. Three resources fit; the fourth cut rows to 180 and every
one spilled 64px into the next. Every offset is now a fraction of the row, so
it cannot overflow. Also dropped the per-row AMOUNT caption (four copies of a
word the footer hint already explains) and moved each glyph and name to the
TOP of its row, where they read as its heading rather than the next row's.
HANGAR. Six vessel types in one row gave each about 100px for a 155px label,
so the names ran together. Now a 3x2 grid per section, sized from the space
between the first heading and the status line, using the empty lower half.
LAB RESEARCH REWARD. A harder enemy lab is worth more research points, and
how hard a lab is depends on the layout authored for it - how many capture
points, how many defenders - which the planet TYPE cannot know. PlanetSetup
carries research_point_reward, set in the map editor, overriding the planet
type's flat rate. 0 means "no opinion", so every map authored before this
keeps its old value.
RESEARCH LAB BACKGROUND. Its own shader instead of the crater field with a
red tint, which read as "rocky planet, tinted" rather than someone's
facility: plated floor, seams, service conduits, lit junctions. Loop-free by
construction (#68's lesson applied up front) - conduits stay continuous
across cells by keying a horizontal run to cell.y and a vertical to cell.x,
so neighbouring cells agree without any cell scanning its neighbours.
ENEMY DETAIL. Every enemy was a single flat outline, which left the artillery
and the turret spawner as the same octagon at two sizes - the silhouette
carried no information about what the thing did. EnemyDefinition gains
detail_lines and detail_rings as DATA, so adding an enemy still means adding
a definition rather than editing Enemy.gd, and the four non-default hulls are
redrawn: a long-barrelled sniper, a squat mortar, a hive with launch bays, a
capital ship with spine and ribs.
Three things drew an enemy from its definition - the enemy, the map editor's
markers, and the palette buttons - each reimplementing it from hull_points
alone, so the new detail appeared in the game and nowhere else and an editor
marker stopped matching what it placed. They now share
EnemyDefinitions.draw_definition().
ARTILLERY. The 1.8s charge-up before the tower fired arrived long before the
threat was legible and mostly read as the tower being stuck. It fires
immediately now; the warning is the shell you can see in the air plus a small
cross on the ground at the impact point. Deliberately not a blast-radius ring
- a large translucent area was easy to read as the explosion having already
happened, where the cross says "move off this spot".
MAIN MENU. No Quit in any build (a web build has nothing to quit to, and
Android's back gesture already does it), a plain black background, and the
game is "Spaceship" - in the title and in config/name, which is what drives
the browser tab for the web export. change_scene_to_file() returns an error
and otherwise fails silently, leaving the menu up, which is indistinguishable
from an unpressed button; it is now checked and reported on screen.
Full suite green at 99/99.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:54:46 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _layout_hangar() -> void:
|
2026-08-31 02:18:08 +02:00
|
|
|
hangar_menu._layout_sections()
|
Stacking distorders, menu layouts, lab rewards, enemy detail, artillery cross
DISTORDER STACKING. Each space-time distorder vessel past the first raises
the fleet's speed ceiling by 1.0, so a second one is worth crewing instead of
being dead weight. GameState gains max_fleet_speed_multiplier, pushed by
Fleet whenever composition changes - the same direction as every other
fleet-derived figure, since GameState does not reach into the scene tree to
count ships. The energy drain is normalised against the CURRENT ceiling, so
full boost costs the same per second however wide it is: extra vessels buy
more speed, not cheaper speed. Losing one re-clamps an over-ceiling boost
rather than leaving the fleet travelling faster than its crew can sustain.
TRANSFER MENU. Rows overlapped, and the last one ran into the hint text.
row_height is divided from the available space, but each row's contents used
a fixed pixel ladder - glyph at +10, buttons at +88, selector at +142 - which
needs about 244px. Three resources fit; the fourth cut rows to 180 and every
one spilled 64px into the next. Every offset is now a fraction of the row, so
it cannot overflow. Also dropped the per-row AMOUNT caption (four copies of a
word the footer hint already explains) and moved each glyph and name to the
TOP of its row, where they read as its heading rather than the next row's.
HANGAR. Six vessel types in one row gave each about 100px for a 155px label,
so the names ran together. Now a 3x2 grid per section, sized from the space
between the first heading and the status line, using the empty lower half.
LAB RESEARCH REWARD. A harder enemy lab is worth more research points, and
how hard a lab is depends on the layout authored for it - how many capture
points, how many defenders - which the planet TYPE cannot know. PlanetSetup
carries research_point_reward, set in the map editor, overriding the planet
type's flat rate. 0 means "no opinion", so every map authored before this
keeps its old value.
RESEARCH LAB BACKGROUND. Its own shader instead of the crater field with a
red tint, which read as "rocky planet, tinted" rather than someone's
facility: plated floor, seams, service conduits, lit junctions. Loop-free by
construction (#68's lesson applied up front) - conduits stay continuous
across cells by keying a horizontal run to cell.y and a vertical to cell.x,
so neighbouring cells agree without any cell scanning its neighbours.
ENEMY DETAIL. Every enemy was a single flat outline, which left the artillery
and the turret spawner as the same octagon at two sizes - the silhouette
carried no information about what the thing did. EnemyDefinition gains
detail_lines and detail_rings as DATA, so adding an enemy still means adding
a definition rather than editing Enemy.gd, and the four non-default hulls are
redrawn: a long-barrelled sniper, a squat mortar, a hive with launch bays, a
capital ship with spine and ribs.
Three things drew an enemy from its definition - the enemy, the map editor's
markers, and the palette buttons - each reimplementing it from hull_points
alone, so the new detail appeared in the game and nowhere else and an editor
marker stopped matching what it placed. They now share
EnemyDefinitions.draw_definition().
ARTILLERY. The 1.8s charge-up before the tower fired arrived long before the
threat was legible and mostly read as the tower being stuck. It fires
immediately now; the warning is the shell you can see in the air plus a small
cross on the ground at the impact point. Deliberately not a blast-radius ring
- a large translucent area was easy to read as the explosion having already
happened, where the cross says "move off this spot".
MAIN MENU. No Quit in any build (a web build has nothing to quit to, and
Android's back gesture already does it), a plain black background, and the
game is "Spaceship" - in the title and in config/name, which is what drives
the browser tab for the web export. change_scene_to_file() returns an error
and otherwise fails silently, leaving the menu up, which is indistinguishable
from an unpressed button; it is now checked and reported on screen.
Full suite green at 99/99.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:54:46 +02:00
|
|
|
|
|
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
func _refresh_garrison() -> void:
|
|
|
|
|
garrison_menu.refresh()
|
Build the logistics crews, and let them burn energy
#216: transport hubs and spacial workshops no longer conjure their crews.
Transporters and repair drones are ordered at a ship crafter, assigned per
building through a new Station menu, flown out from the mothership hangar,
and spent by the work they do - 20 runs, 12 repairs, counted where the run
ENDS so nothing retires with cargo in the air or a building half repaired.
A spent vessel is gone, so the structure is short again and the player has
to build another. The menu shows what each one has left, worst first.
Craftability needed its own flag: "the crafter can make it" and "it can take
a berth in the fleet" stopped being the same question.
#217: transporters crawl at 120 rather than 150, and a Burn Drive lets them
carry energy and spend it to travel far faster. Fetching it is a leg of its
own, to the nearest store that has any - so a refuelling transporter cannot
be dispatched mid-errand - and the leg flown to COLLECT fuel is not charged
for it. Unresearched, nothing detours and nothing changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-05 19:09:35 +02:00
|
|
|
station_menu.refresh()
|
2026-09-05 22:36:21 +02:00
|
|
|
upkeep_menu.refresh()
|
Stacking distorders, menu layouts, lab rewards, enemy detail, artillery cross
DISTORDER STACKING. Each space-time distorder vessel past the first raises
the fleet's speed ceiling by 1.0, so a second one is worth crewing instead of
being dead weight. GameState gains max_fleet_speed_multiplier, pushed by
Fleet whenever composition changes - the same direction as every other
fleet-derived figure, since GameState does not reach into the scene tree to
count ships. The energy drain is normalised against the CURRENT ceiling, so
full boost costs the same per second however wide it is: extra vessels buy
more speed, not cheaper speed. Losing one re-clamps an over-ceiling boost
rather than leaving the fleet travelling faster than its crew can sustain.
TRANSFER MENU. Rows overlapped, and the last one ran into the hint text.
row_height is divided from the available space, but each row's contents used
a fixed pixel ladder - glyph at +10, buttons at +88, selector at +142 - which
needs about 244px. Three resources fit; the fourth cut rows to 180 and every
one spilled 64px into the next. Every offset is now a fraction of the row, so
it cannot overflow. Also dropped the per-row AMOUNT caption (four copies of a
word the footer hint already explains) and moved each glyph and name to the
TOP of its row, where they read as its heading rather than the next row's.
HANGAR. Six vessel types in one row gave each about 100px for a 155px label,
so the names ran together. Now a 3x2 grid per section, sized from the space
between the first heading and the status line, using the empty lower half.
LAB RESEARCH REWARD. A harder enemy lab is worth more research points, and
how hard a lab is depends on the layout authored for it - how many capture
points, how many defenders - which the planet TYPE cannot know. PlanetSetup
carries research_point_reward, set in the map editor, overriding the planet
type's flat rate. 0 means "no opinion", so every map authored before this
keeps its old value.
RESEARCH LAB BACKGROUND. Its own shader instead of the crater field with a
red tint, which read as "rocky planet, tinted" rather than someone's
facility: plated floor, seams, service conduits, lit junctions. Loop-free by
construction (#68's lesson applied up front) - conduits stay continuous
across cells by keying a horizontal run to cell.y and a vertical to cell.x,
so neighbouring cells agree without any cell scanning its neighbours.
ENEMY DETAIL. Every enemy was a single flat outline, which left the artillery
and the turret spawner as the same octagon at two sizes - the silhouette
carried no information about what the thing did. EnemyDefinition gains
detail_lines and detail_rings as DATA, so adding an enemy still means adding
a definition rather than editing Enemy.gd, and the four non-default hulls are
redrawn: a long-barrelled sniper, a squat mortar, a hive with launch bays, a
capital ship with spine and ribs.
Three things drew an enemy from its definition - the enemy, the map editor's
markers, and the palette buttons - each reimplementing it from hull_points
alone, so the new detail appeared in the game and nowhere else and an editor
marker stopped matching what it placed. They now share
EnemyDefinitions.draw_definition().
ARTILLERY. The 1.8s charge-up before the tower fired arrived long before the
threat was legible and mostly read as the tower being stuck. It fires
immediately now; the warning is the shell you can see in the air plus a small
cross on the ground at the impact point. Deliberately not a blast-radius ring
- a large translucent area was easy to read as the explosion having already
happened, where the cross says "move off this spot".
MAIN MENU. No Quit in any build (a web build has nothing to quit to, and
Android's back gesture already does it), a plain black background, and the
game is "Spaceship" - in the title and in config/name, which is what drives
the browser tab for the web export. change_scene_to_file() returns an error
and otherwise fails silently, leaving the menu up, which is indistinguishable
from an unpressed button; it is now checked and reported on screen.
Full suite green at 99/99.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:54:46 +02:00
|
|
|
|
2026-08-31 02:18:08 +02:00
|
|
|
|
|
|
|
|
func _refresh_planning() -> void:
|
|
|
|
|
planning_menu.refresh()
|
2026-08-12 22:47:46 +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 _on_formation_changed() -> void:
|
|
|
|
|
fleet.apply_formation()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_reset_formation() -> void:
|
|
|
|
|
GameState.reset_formation()
|
|
|
|
|
fleet.apply_formation()
|
2026-08-31 02:18:08 +02:00
|
|
|
formation_menu.redraw()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _hide_all_actions() -> void:
|
|
|
|
|
_clear_detected_context()
|
|
|
|
|
build_button.visible = false
|