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 Node
|
|
|
|
|
|
2026-08-14 20:32:58 +02:00
|
|
|
const INVENTORY = preload("res://Inventory.gd")
|
|
|
|
|
const RESOURCE_DEFINITIONS = preload("res://ResourceDefinitions.gd")
|
2026-08-14 21:44:28 +02:00
|
|
|
const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
|
2026-08-14 22:12:57 +02:00
|
|
|
const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
2026-08-14 20:32:58 +02:00
|
|
|
|
2026-08-14 16:05:32 +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
|
|
|
# Autoload singleton - the one thing that survives change_scene_to_file()
|
|
|
|
|
# scene swaps between space (Main.tscn) and a planet's surface
|
|
|
|
|
# (PlanetSurface.tscn). Everything else in either scene gets torn down
|
|
|
|
|
# and rebuilt from scratch on each swap, so anything that needs to
|
|
|
|
|
# survive the trip has to live here instead.
|
|
|
|
|
|
|
|
|
|
var has_return_position: bool = false
|
|
|
|
|
var surviving_ship_count: int = 5
|
2026-08-14 23:15:12 +02:00
|
|
|
var surviving_vessel_types: PackedStringArray = PackedStringArray()
|
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 return_world_position: Vector2 = Vector2.ZERO
|
|
|
|
|
var return_camera_zoom: float = 1.0
|
|
|
|
|
|
|
|
|
|
var planet_name: String = ""
|
2026-08-14 22:16:40 +02:00
|
|
|
var planet_id: String = ""
|
2026-08-14 16:13:24 +02:00
|
|
|
var planet_definition_id: String = "rocky"
|
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
|
|
|
|
|
|
|
|
# Explored-chunk history for the world map's fog-of-war reveal. Lives
|
|
|
|
|
# here (not on ExplorationTracker, which gets destroyed and recreated on
|
|
|
|
|
# every scene swap) so exploration progress isn't lost every time the
|
|
|
|
|
# player lands on and leaves a planet.
|
|
|
|
|
var explored_chunks: Dictionary = {}
|
|
|
|
|
|
|
|
|
|
# --- Resources -------------------------------------------------------------
|
|
|
|
|
|
2026-08-14 20:32:58 +02:00
|
|
|
const RESOURCE_ORE: String = RESOURCE_DEFINITIONS.ORE_ID
|
|
|
|
|
const RESOURCE_GAS: String = RESOURCE_DEFINITIONS.GAS_ID
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
const RESOURCE_ENERGY: String = RESOURCE_DEFINITIONS.ENERGY_ID
|
2026-08-16 13:13:38 +02:00
|
|
|
const RESOURCE_DARK_MATTER: String = RESOURCE_DEFINITIONS.DARK_MATTER_ID
|
2026-08-14 20:32:58 +02:00
|
|
|
const RESOURCE_TYPES: PackedStringArray = RESOURCE_DEFINITIONS.ORDER
|
|
|
|
|
|
|
|
|
|
# All resource-bearing locations use the same container. The old public
|
|
|
|
|
# names below are computed compatibility accessors, not separate storage.
|
|
|
|
|
var bank_inventory: RefCounted = INVENTORY.new({RESOURCE_ORE: 500})
|
|
|
|
|
var fleet_inventory: RefCounted = INVENTORY.new()
|
|
|
|
|
|
|
|
|
|
var rocks: int:
|
|
|
|
|
get: return bank_inventory.amount(RESOURCE_ORE)
|
|
|
|
|
set(value): bank_inventory.set_amount(RESOURCE_ORE, value)
|
|
|
|
|
|
|
|
|
|
var gas: int:
|
|
|
|
|
get: return bank_inventory.amount(RESOURCE_GAS)
|
|
|
|
|
set(value): bank_inventory.set_amount(RESOURCE_GAS, value)
|
|
|
|
|
|
|
|
|
|
var fleet_cargo: int:
|
|
|
|
|
get: return fleet_inventory.amount(RESOURCE_ORE)
|
|
|
|
|
set(value): fleet_inventory.set_amount(RESOURCE_ORE, value)
|
|
|
|
|
|
|
|
|
|
var fleet_gas_cargo: int:
|
|
|
|
|
get: return fleet_inventory.amount(RESOURCE_GAS)
|
|
|
|
|
set(value): fleet_inventory.set_amount(RESOURCE_GAS, value)
|
2026-08-14 11:54:31 +02:00
|
|
|
|
2026-08-11 15:55:45 +02:00
|
|
|
func collect_cargo(amount: int = 1) -> void:
|
2026-08-14 20:32:58 +02:00
|
|
|
fleet_inventory.deposit(RESOURCE_ORE, amount)
|
2026-08-11 15:55:45 +02:00
|
|
|
|
2026-08-13 22:17:24 +02:00
|
|
|
func collect_gas_cargo(amount: int = 1) -> void:
|
2026-08-14 20:32:58 +02:00
|
|
|
fleet_inventory.deposit(RESOURCE_GAS, amount)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func resource_types() -> PackedStringArray:
|
|
|
|
|
return RESOURCE_DEFINITIONS.ids()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func resource_definition(resource_type: String) -> Resource:
|
|
|
|
|
return RESOURCE_DEFINITIONS.get_definition(resource_type)
|
2026-08-13 22:17:24 +02:00
|
|
|
|
2026-08-11 15:55:45 +02:00
|
|
|
|
2026-08-14 11:54:31 +02:00
|
|
|
func resource_name(resource_type: String) -> String:
|
2026-08-14 20:32:58 +02:00
|
|
|
var definition: Resource = resource_definition(resource_type)
|
|
|
|
|
return String(definition.display_name) if definition != null else resource_type.capitalize()
|
2026-08-14 11:54:31 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func resource_color(resource_type: String) -> Color:
|
2026-08-14 20:32:58 +02:00
|
|
|
var definition: Resource = resource_definition(resource_type)
|
|
|
|
|
return definition.color if definition != null else Color.WHITE
|
2026-08-14 11:54:31 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func banked_resource(resource_type: String) -> int:
|
2026-08-14 20:32:58 +02:00
|
|
|
return bank_inventory.amount(resource_type)
|
2026-08-14 11:54:31 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func fleet_resource(resource_type: String) -> int:
|
2026-08-14 20:32:58 +02:00
|
|
|
return fleet_inventory.amount(resource_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _inventory_for_record(record: Dictionary) -> RefCounted:
|
|
|
|
|
var inventory: RefCounted = record.get("inventory") as RefCounted
|
|
|
|
|
if inventory == null:
|
|
|
|
|
inventory = INVENTORY.new()
|
|
|
|
|
record.inventory = inventory
|
|
|
|
|
# Compatibility migration for state made before #33. Erase each legacy
|
|
|
|
|
# field once absorbed so it cannot drift out of step with the inventory.
|
|
|
|
|
if record.has("stock"):
|
|
|
|
|
inventory.set_amount(RESOURCE_ORE, int(record.stock))
|
|
|
|
|
record.erase("stock")
|
|
|
|
|
if record.has("gas_stock"):
|
|
|
|
|
inventory.set_amount(RESOURCE_GAS, int(record.gas_stock))
|
|
|
|
|
record.erase("gas_stock")
|
|
|
|
|
return inventory
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func building_inventory(id: int) -> RefCounted:
|
2026-08-14 11:54:31 +02:00
|
|
|
var building: Dictionary = find_building(id)
|
|
|
|
|
if building.is_empty():
|
2026-08-14 20:32:58 +02:00
|
|
|
return null
|
2026-08-14 21:44:28 +02:00
|
|
|
if building_has_capability(building, "bank"):
|
2026-08-14 20:32:58 +02:00
|
|
|
return bank_inventory
|
|
|
|
|
return _inventory_for_record(building)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func building_resource(id: int, resource_type: String) -> int:
|
|
|
|
|
var inventory: RefCounted = building_inventory(id)
|
|
|
|
|
return 0 if inventory == null else inventory.amount(resource_type)
|
2026-08-14 11:54:31 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func transfer_resource_to_building(id: int, resource_type: String, amount: int) -> int:
|
2026-08-14 20:32:58 +02:00
|
|
|
return fleet_inventory.transfer_to(building_inventory(id), resource_type, amount)
|
2026-08-14 11:54:31 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func transfer_resource_to_fleet(id: int, resource_type: String, amount: int) -> int:
|
2026-08-14 20:32:58 +02:00
|
|
|
var source: RefCounted = building_inventory(id)
|
|
|
|
|
return 0 if source == null else source.transfer_to(fleet_inventory, resource_type, amount)
|
2026-08-14 11:54:31 +02:00
|
|
|
|
|
|
|
|
|
2026-08-12 13:07:52 +02:00
|
|
|
# The batch size the transfer menu offers, next to "All". A batch rather
|
|
|
|
|
# than only all-or-nothing: the hold is the part that can be lost, and
|
|
|
|
|
# emptying the bank in one tap would strand research, which spends from it.
|
Supply a building by hand: withdraw at the mothership, unload on arrival
Forgejo #17, part 4 of #12. #16 lets an order queue waiting for ore, so
something has to be able to fulfil one. Until transporters exist (#19)
this is the only route, which is why it lands before them rather than
after: otherwise a player with a queued order and no Transport Hub is
stuck forever.
A "Load ore" button at the mothership moves a batch from the bank into
the fleet's hold, and opening a building hands it whatever the fleet is
carrying.
Delivery at the mothership is now EDGE triggered on arrival rather than
running every frame the fleet is parked there. Continuous delivery made
withdrawing impossible - ore loaded into the hold was banked again on the
very next frame, so the button would have looked broken. A batch rather
than the whole bank, because the hold is the part that can be lost and
emptying the bank in one tap would also strand research, which still
spends from it.
Both new logistics tests now clear existing asteroids: disabling
AsteroidField only stops new chunks spawning, and a drifting rock
destroying a parked ship changed the fleet size the tests assert on.
That was a real intermittent failure, seen once in a full run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:36:11 +02:00
|
|
|
const CARGO_LOAD_BATCH: int = 25
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 13:07:52 +02:00
|
|
|
# --- moving ore by hand, in either direction (#24) --------------------------
|
|
|
|
|
#
|
|
|
|
|
# One pair of primitives the transfer menu drives, rather than the two
|
|
|
|
|
# one-way special cases that used to live inside the skill and craft panels
|
|
|
|
|
# ("load ore" at the mothership, "store ore here" at a crafter).
|
|
|
|
|
#
|
|
|
|
|
# The mothership is deliberately not a special case in the UI: it is simply
|
|
|
|
|
# the building whose holdings ARE the bank. Depositing at one banks the ore,
|
|
|
|
|
# withdrawing from one draws on the bank, and every other building keeps its
|
|
|
|
|
# own stock (#16). That is what lets one menu serve every structure.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func building_holdings(id: int) -> int:
|
2026-08-14 11:54:31 +02:00
|
|
|
return building_resource(id, RESOURCE_ORE)
|
Supply a building by hand: withdraw at the mothership, unload on arrival
Forgejo #17, part 4 of #12. #16 lets an order queue waiting for ore, so
something has to be able to fulfil one. Until transporters exist (#19)
this is the only route, which is why it lands before them rather than
after: otherwise a player with a queued order and no Transport Hub is
stuck forever.
A "Load ore" button at the mothership moves a batch from the bank into
the fleet's hold, and opening a building hands it whatever the fleet is
carrying.
Delivery at the mothership is now EDGE triggered on arrival rather than
running every frame the fleet is parked there. Continuous delivery made
withdrawing impossible - ore loaded into the hold was banked again on the
very next frame, so the button would have looked broken. A batch rather
than the whole bank, because the hold is the part that can be lost and
emptying the bank in one tap would also strand research, which still
spends from it.
Both new logistics tests now clear existing asteroids: disabling
AsteroidField only stops new chunks spawning, and a drifting rock
destroying a parked ship changed the fleet size the tests assert on.
That was a real intermittent failure, seen once in a full run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:36:11 +02:00
|
|
|
|
2026-08-13 22:17:24 +02:00
|
|
|
func building_gas_holdings(id: int) -> int:
|
2026-08-14 11:54:31 +02:00
|
|
|
return building_resource(id, RESOURCE_GAS)
|
2026-08-13 22:17:24 +02:00
|
|
|
|
Supply a building by hand: withdraw at the mothership, unload on arrival
Forgejo #17, part 4 of #12. #16 lets an order queue waiting for ore, so
something has to be able to fulfil one. Until transporters exist (#19)
this is the only route, which is why it lands before them rather than
after: otherwise a player with a queued order and no Transport Hub is
stuck forever.
A "Load ore" button at the mothership moves a batch from the bank into
the fleet's hold, and opening a building hands it whatever the fleet is
carrying.
Delivery at the mothership is now EDGE triggered on arrival rather than
running every frame the fleet is parked there. Continuous delivery made
withdrawing impossible - ore loaded into the hold was banked again on the
very next frame, so the button would have looked broken. A batch rather
than the whole bank, because the hold is the part that can be lost and
emptying the bank in one tap would also strand research, which still
spends from it.
Both new logistics tests now clear existing asteroids: disabling
AsteroidField only stops new chunks spawning, and a drifting rock
destroying a parked ship changed the fleet size the tests assert on.
That was a real intermittent failure, seen once in a full run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:36:11 +02:00
|
|
|
|
2026-08-12 13:07:52 +02:00
|
|
|
func transfer_to_building(id: int, amount: int) -> int:
|
|
|
|
|
# Hold -> this building. Returns how much actually moved, which is capped
|
|
|
|
|
# by the hold: a caller asking for "all" passes a number too big on
|
|
|
|
|
# purpose and reads the result back.
|
2026-08-14 11:54:31 +02:00
|
|
|
return transfer_resource_to_building(id, RESOURCE_ORE, amount)
|
Supply a building by hand: withdraw at the mothership, unload on arrival
Forgejo #17, part 4 of #12. #16 lets an order queue waiting for ore, so
something has to be able to fulfil one. Until transporters exist (#19)
this is the only route, which is why it lands before them rather than
after: otherwise a player with a queued order and no Transport Hub is
stuck forever.
A "Load ore" button at the mothership moves a batch from the bank into
the fleet's hold, and opening a building hands it whatever the fleet is
carrying.
Delivery at the mothership is now EDGE triggered on arrival rather than
running every frame the fleet is parked there. Continuous delivery made
withdrawing impossible - ore loaded into the hold was banked again on the
very next frame, so the button would have looked broken. A batch rather
than the whole bank, because the hold is the part that can be lost and
emptying the bank in one tap would also strand research, which still
spends from it.
Both new logistics tests now clear existing asteroids: disabling
AsteroidField only stops new chunks spawning, and a drifting rock
destroying a parked ship changed the fleet size the tests assert on.
That was a real intermittent failure, seen once in a full run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:36:11 +02:00
|
|
|
|
|
|
|
|
|
2026-08-12 13:07:52 +02:00
|
|
|
func transfer_to_fleet(id: int, amount: int) -> int:
|
|
|
|
|
# This building -> hold. Capped by what the building actually holds, so
|
|
|
|
|
# there is no overdraft in either direction.
|
2026-08-14 11:54:31 +02:00
|
|
|
return transfer_resource_to_fleet(id, RESOURCE_ORE, amount)
|
2026-08-11 15:55:45 +02:00
|
|
|
|
2026-08-13 22:17:24 +02:00
|
|
|
func transfer_gas_to_building(id: int, amount: int) -> int:
|
2026-08-14 11:54:31 +02:00
|
|
|
return transfer_resource_to_building(id, RESOURCE_GAS, amount)
|
2026-08-13 22:17:24 +02:00
|
|
|
|
|
|
|
|
func transfer_gas_to_fleet(id: int, amount: int) -> int:
|
2026-08-14 11:54:31 +02:00
|
|
|
return transfer_resource_to_fleet(id, RESOURCE_GAS, amount)
|
2026-08-13 22:17:24 +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
|
|
|
const MOTHERSHIP_COST: int = 20
|
|
|
|
|
const SHIP_CRAFTER_COST: int = 12
|
|
|
|
|
const SHIP_COST: int = 6
|
|
|
|
|
|
|
|
|
|
# --- Buildings -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# Player-placed structures, as [{"type": String, "position": Vector2}].
|
|
|
|
|
# Buildings exist only in space (Main.tscn) but are stored here so they
|
|
|
|
|
# survive the scene swap to a planet's surface and back.
|
|
|
|
|
const BUILDING_MOTHERSHIP: String = "mothership"
|
|
|
|
|
const BUILDING_SHIP_CRAFTER: String = "ship_crafter"
|
|
|
|
|
const BUILDING_COMMAND_CENTER: String = "command_center"
|
|
|
|
|
# Extractors are not placed in space like the others - they are built ON a
|
|
|
|
|
# planet's surface and tracked separately (see `extractors`).
|
|
|
|
|
const BUILDING_EXTRACTOR: String = "extractor"
|
Transport Hub building
Forgejo #18, part 5 of #12. The building only - the vessels that do the
hauling are #19. Placeable, costed at 30, gated behind a new "Freight
Handling" research node hanging off Logistics, with its own info popup,
build-menu glyph and map marker.
Drawn as an octagonal dock ring with berth arms and a squared-off freight
core: deliberately radial-but-flat so it does not read as a small
mothership, which is the only structure with swept arms. Its stationed
transports are NeonPiece children of one rotating holder, per #10 - the
geometry is rasterised once and only the holder's rotation changes, so
the traffic costs no redraws.
Building._draw()'s match has a catch-all that renders anything
unrecognised as a ship crafter, so a missing case is silent and looks
almost right. The test checks for the hub's own _hub_traffic holder
rather than trusting that something plausible appeared - which caught
exactly that: a _ready() patch had silently not applied and the hub was
rendering through the crafter branch.
The build menu grew a third row. Every button, label and price moved:
the old row occupied 332px against a 372px pitch, and three of those do
not fit above a 1280px screen. Rows are now shorter (128px buttons, 40px
price digits) at a 290px pitch, ending at y=1176. Rendered and looked at
- the first attempt tightened the pitch alone and left every price
overlapping the row beneath it, which no headless test would have seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:45:07 +02:00
|
|
|
# Hosts the transport vessels that supply other buildings (#18). Built in
|
|
|
|
|
# space like the rest; it is the transporters (#19) that do the moving.
|
|
|
|
|
const BUILDING_TRANSPORT_HUB: String = "transport_hub"
|
2026-08-11 23:10:07 +02:00
|
|
|
# Where vessel types are researched and new ones unlocked (#5).
|
|
|
|
|
const BUILDING_SHIP_LAB: String = "ship_lab"
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
# Converts gas into energy, both stored/moved through the building's own
|
|
|
|
|
# inventory exactly like every other building (#54).
|
|
|
|
|
const BUILDING_GAS_BURNER: String = "gas_burner"
|
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
|
|
|
|
|
|
|
|
const COMMAND_CENTER_COST: int = 15
|
|
|
|
|
const EXTRACTOR_COST: int = 25
|
Transport Hub building
Forgejo #18, part 5 of #12. The building only - the vessels that do the
hauling are #19. Placeable, costed at 30, gated behind a new "Freight
Handling" research node hanging off Logistics, with its own info popup,
build-menu glyph and map marker.
Drawn as an octagonal dock ring with berth arms and a squared-off freight
core: deliberately radial-but-flat so it does not read as a small
mothership, which is the only structure with swept arms. Its stationed
transports are NeonPiece children of one rotating holder, per #10 - the
geometry is rasterised once and only the holder's rotation changes, so
the traffic costs no redraws.
Building._draw()'s match has a catch-all that renders anything
unrecognised as a ship crafter, so a missing case is silent and looks
almost right. The test checks for the hub's own _hub_traffic holder
rather than trusting that something plausible appeared - which caught
exactly that: a _ready() patch had silently not applied and the hub was
rendering through the crafter branch.
The build menu grew a third row. Every button, label and price moved:
the old row occupied 332px against a 372px pitch, and three of those do
not fit above a 1280px screen. Rows are now shorter (128px buttons, 40px
price digits) at a 290px pitch, ending at y=1176. Rendered and looked at
- the first attempt tightened the pitch alone and left every price
overlapping the row beneath it, which no headless test would have seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:45:07 +02:00
|
|
|
const TRANSPORT_HUB_COST: int = 30
|
2026-08-11 23:10:07 +02:00
|
|
|
const SHIP_LAB_COST: int = 35
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
const GAS_BURNER_COST: int = 30
|
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 buildings: Array = []
|
|
|
|
|
|
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
|
|
|
# --- Skill tree ------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# Researched at the mothership. Two things hang off it: which structures the
|
|
|
|
|
# player is allowed to build at all, and permanent upgrades that have no
|
|
|
|
|
# building of their own. The mothership itself is deliberately NOT in the
|
|
|
|
|
# tree - it is the thing you research AT, so gating it behind research would
|
|
|
|
|
# be a deadlock.
|
|
|
|
|
#
|
|
|
|
|
# `x` is a horizontal position on a 0..1 grid; the vertical position is NOT
|
|
|
|
|
# stored, it is derived from how many prerequisites deep the node is, so the
|
|
|
|
|
# tree always reads root-at-the-bottom and cannot drift out of step with the
|
|
|
|
|
# dependencies when nodes are added. `requires` is a single prerequisite id
|
|
|
|
|
# ("" for a root).
|
|
|
|
|
const SKILL_TREE: Array = [
|
|
|
|
|
{
|
|
|
|
|
"id": "shipyards", "name": "Shipyards", "cost": 15, "requires": "",
|
|
|
|
|
"x": 0.5, "unlocks": BUILDING_SHIP_CRAFTER,
|
|
|
|
|
"desc": "Build ship crafters, and buy extra vessels at them.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"id": "logistics", "name": "Logistics", "cost": 25, "requires": "shipyards",
|
|
|
|
|
"x": 0.18, "unlocks": BUILDING_EXTRACTOR,
|
|
|
|
|
"desc": "Build extractors on planets. They mine ore whether or not you are nearby.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"id": "coordination", "name": "Coordination", "cost": 20, "requires": "shipyards",
|
2026-08-13 16:25:41 +02:00
|
|
|
"x": 0.50, "unlocks": BUILDING_COMMAND_CENTER,
|
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
|
|
|
"desc": "Build command centers, where the fleet formation is arranged.",
|
|
|
|
|
},
|
Transport Hub building
Forgejo #18, part 5 of #12. The building only - the vessels that do the
hauling are #19. Placeable, costed at 30, gated behind a new "Freight
Handling" research node hanging off Logistics, with its own info popup,
build-menu glyph and map marker.
Drawn as an octagonal dock ring with berth arms and a squared-off freight
core: deliberately radial-but-flat so it does not read as a small
mothership, which is the only structure with swept arms. Its stationed
transports are NeonPiece children of one rotating holder, per #10 - the
geometry is rasterised once and only the holder's rotation changes, so
the traffic costs no redraws.
Building._draw()'s match has a catch-all that renders anything
unrecognised as a ship crafter, so a missing case is silent and looks
almost right. The test checks for the hub's own _hub_traffic holder
rather than trusting that something plausible appeared - which caught
exactly that: a _ready() patch had silently not applied and the hub was
rendering through the crafter branch.
The build menu grew a third row. Every button, label and price moved:
the old row occupied 332px against a 372px pitch, and three of those do
not fit above a 1280px screen. Rows are now shorter (128px buttons, 40px
price digits) at a 290px pitch, ending at y=1176. Rendered and looked at
- the first attempt tightened the pitch alone and left every price
overlapping the row beneath it, which no headless test would have seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:45:07 +02:00
|
|
|
{
|
|
|
|
|
"id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics",
|
|
|
|
|
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB,
|
2026-08-15 00:09:10 +02:00
|
|
|
"desc": "Build transport hubs. Each hub includes its own automatic transporters.",
|
Transport Hub building
Forgejo #18, part 5 of #12. The building only - the vessels that do the
hauling are #19. Placeable, costed at 30, gated behind a new "Freight
Handling" research node hanging off Logistics, with its own info popup,
build-menu glyph and map marker.
Drawn as an octagonal dock ring with berth arms and a squared-off freight
core: deliberately radial-but-flat so it does not read as a small
mothership, which is the only structure with swept arms. Its stationed
transports are NeonPiece children of one rotating holder, per #10 - the
geometry is rasterised once and only the holder's rotation changes, so
the traffic costs no redraws.
Building._draw()'s match has a catch-all that renders anything
unrecognised as a ship crafter, so a missing case is silent and looks
almost right. The test checks for the hub's own _hub_traffic holder
rather than trusting that something plausible appeared - which caught
exactly that: a _ready() patch had silently not applied and the hub was
rendering through the crafter branch.
The build menu grew a third row. Every button, label and price moved:
the old row occupied 332px against a 372px pitch, and three of those do
not fit above a 1280px screen. Rows are now shorter (128px buttons, 40px
price digits) at a 290px pitch, ending at y=1176. Rendered and looked at
- the first attempt tightened the pitch alone and left every price
overlapping the row beneath it, which no headless test would have seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:45:07 +02:00
|
|
|
},
|
2026-08-11 23:10:07 +02:00
|
|
|
{
|
|
|
|
|
"id": "vessel_science", "name": "Vessel Science", "cost": 35, "requires": "shipyards",
|
2026-08-13 16:25:41 +02:00
|
|
|
"x": 0.82, "unlocks": BUILDING_SHIP_LAB,
|
2026-08-11 23:10:07 +02:00
|
|
|
"desc": "Build ship labs, where each vessel type is upgraded and new ones unlocked.",
|
|
|
|
|
},
|
2026-08-11 19:43:55 +02:00
|
|
|
{
|
|
|
|
|
"id": "freight_yards", "name": "Freight Yards", "cost": 80, "requires": "freight",
|
2026-08-13 16:25:41 +02:00
|
|
|
"x": 0.18, "hub_bonus": 1,
|
2026-08-11 19:43:55 +02:00
|
|
|
"desc": "Permanent: +1 transporter stationed at every hub, including ones already built.",
|
|
|
|
|
},
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
{
|
|
|
|
|
"id": "power_systems", "name": "Power Systems", "cost": 45, "requires": "freight",
|
|
|
|
|
"x": 0.50, "unlocks": BUILDING_GAS_BURNER,
|
|
|
|
|
"desc": "Build gas burners. Each converts gas into energy at its own site - hub transporters keep it stocked and collect what it makes.",
|
|
|
|
|
},
|
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
|
|
|
{
|
|
|
|
|
"id": "wing", "name": "Wing Doctrine", "cost": 30, "requires": "coordination",
|
|
|
|
|
"x": 0.82, "fleet_bonus": 1,
|
|
|
|
|
"desc": "Permanent: +1 vessel in the fleet.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"id": "squadron", "name": "Squadron Doctrine", "cost": 70, "requires": "wing",
|
2026-08-13 16:25:41 +02:00
|
|
|
"x": 0.50, "fleet_bonus": 1,
|
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
|
|
|
"desc": "Permanent: +1 vessel in the fleet.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"id": "armada", "name": "Armada Doctrine", "cost": 150, "requires": "squadron",
|
2026-08-13 16:25:41 +02:00
|
|
|
"x": 0.50, "fleet_bonus": 1,
|
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
|
|
|
"desc": "Permanent: +1 vessel in the fleet.",
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const BASE_FLEET_SIZE: int = 5
|
|
|
|
|
|
|
|
|
|
var unlocked: Array = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func skill_node(id: String) -> Dictionary:
|
|
|
|
|
for node in SKILL_TREE:
|
|
|
|
|
if node.id == id:
|
|
|
|
|
return node
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func skill_depth(id: String) -> int:
|
|
|
|
|
# How many prerequisites deep this node sits. Drives its height in the
|
|
|
|
|
# tree view: roots at the bottom, everything that depends on something
|
|
|
|
|
# above it.
|
|
|
|
|
var depth: int = 0
|
|
|
|
|
var node: Dictionary = skill_node(id)
|
|
|
|
|
while not node.is_empty() and node.requires != "":
|
|
|
|
|
depth += 1
|
|
|
|
|
node = skill_node(node.requires)
|
|
|
|
|
if depth > SKILL_TREE.size():
|
|
|
|
|
break # a cycle in the data would otherwise hang here
|
|
|
|
|
return depth
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func max_skill_depth() -> int:
|
|
|
|
|
var deepest: int = 0
|
|
|
|
|
for node in SKILL_TREE:
|
|
|
|
|
deepest = maxi(deepest, skill_depth(node.id))
|
|
|
|
|
return deepest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func is_unlocked(id: String) -> bool:
|
|
|
|
|
return id in unlocked
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func prerequisite_met(id: String) -> bool:
|
|
|
|
|
var node: Dictionary = skill_node(id)
|
|
|
|
|
if node.is_empty():
|
|
|
|
|
return false
|
|
|
|
|
return node.requires == "" or is_unlocked(node.requires)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func can_unlock(id: String) -> bool:
|
|
|
|
|
return not is_unlocked(id) and prerequisite_met(id) and can_afford(int(skill_node(id).cost))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func unlock(id: String) -> bool:
|
|
|
|
|
if not can_unlock(id):
|
|
|
|
|
return false
|
|
|
|
|
if not spend(int(skill_node(id).cost)):
|
|
|
|
|
return false
|
|
|
|
|
unlocked.append(id)
|
2026-08-15 00:09:10 +02:00
|
|
|
if int(skill_node(id).get("hub_bonus", 0)) > 0:
|
|
|
|
|
_sync_hub_transporters()
|
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 true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func is_building_unlocked(type: String) -> bool:
|
|
|
|
|
for node in SKILL_TREE:
|
|
|
|
|
if node.get("unlocks", "") == type:
|
|
|
|
|
return is_unlocked(node.id)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func research_for_building(type: String) -> String:
|
|
|
|
|
for node in SKILL_TREE:
|
|
|
|
|
if node.get("unlocks", "") == type:
|
|
|
|
|
return node.name
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func max_fleet_size() -> int:
|
|
|
|
|
var n: int = BASE_FLEET_SIZE
|
|
|
|
|
for id in unlocked:
|
|
|
|
|
n += int(skill_node(id).get("fleet_bonus", 0))
|
|
|
|
|
return mini(n, DEFAULT_FORMATION.size())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func reset_progression() -> void:
|
|
|
|
|
unlocked.clear()
|
2026-08-11 22:50:09 +02:00
|
|
|
vessel_levels.clear()
|
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
|
|
|
|
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-15 14:43:46 +02:00
|
|
|
# Called from the main menu's Play button (#47): the autoload survives
|
|
|
|
|
# change_scene_to_file(), so without this a second playthrough in the same
|
|
|
|
|
# process would start from wherever the last one left off instead of a
|
|
|
|
|
# fresh game.
|
|
|
|
|
func reset_new_game() -> void:
|
|
|
|
|
has_return_position = false
|
|
|
|
|
surviving_ship_count = BASE_FLEET_SIZE
|
|
|
|
|
surviving_vessel_types = PackedStringArray()
|
|
|
|
|
return_world_position = Vector2.ZERO
|
|
|
|
|
return_camera_zoom = 1.0
|
|
|
|
|
planet_name = ""
|
|
|
|
|
planet_id = ""
|
|
|
|
|
planet_definition_id = "rocky"
|
|
|
|
|
explored_chunks.clear()
|
|
|
|
|
bank_inventory = INVENTORY.new({RESOURCE_ORE: 500})
|
|
|
|
|
fleet_inventory = INVENTORY.new()
|
|
|
|
|
buildings.clear()
|
|
|
|
|
next_building_id = 1
|
|
|
|
|
stored_vessels.clear()
|
|
|
|
|
vessel_deliveries.clear()
|
|
|
|
|
extractors.clear()
|
|
|
|
|
transporters.clear()
|
|
|
|
|
planet_capture.clear()
|
|
|
|
|
reset_progression()
|
|
|
|
|
reset_formation()
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
# --- building stock and production queue (#16) -----------------------------
|
|
|
|
|
|
|
|
|
|
# Since #12, a building cannot just deduct from the bank at the moment of
|
|
|
|
|
# purchase: what it produces has to be paid for out of resources physically
|
|
|
|
|
# delivered to it. So each building carries its own `stock`, and an order it
|
|
|
|
|
# cannot afford yet is QUEUED rather than refused, and completes by itself
|
|
|
|
|
# once the stock turns up.
|
|
|
|
|
#
|
|
|
|
|
# `id` exists 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.
|
|
|
|
|
var next_building_id: int = 1
|
|
|
|
|
|
2026-08-12 22:47:46 +02:00
|
|
|
# Finished vessels waiting at the mothership for the player to collect
|
|
|
|
|
# (#23), as a list of VESSEL_* types - a shield vessel must not come back as
|
|
|
|
|
# an interceptor. They are NOT handed to the fleet automatically: the player
|
|
|
|
|
# picks what to crew from the hangar, so a full fleet no longer stops a
|
|
|
|
|
# crafter from building.
|
|
|
|
|
#
|
|
|
|
|
# Production is simulated here, in the autoload, the same way extractor
|
|
|
|
|
# haulers are, so a build finishes whether or not the player is in space.
|
|
|
|
|
var stored_vessels: Array = []
|
2026-08-13 21:39:05 +02:00
|
|
|
# Finished crafter orders travel to the mothership before appearing in its
|
|
|
|
|
# hangar. State is authoritative here so delivery continues off-screen.
|
|
|
|
|
# {"type", "from", "to", "progress"}
|
|
|
|
|
var vessel_deliveries: Array = []
|
2026-08-14 22:30:19 +02:00
|
|
|
const VESSEL_DELIVERY_SPEED: float = 180.0
|
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
|
|
|
|
|
|
|
|
signal order_completed(building_id: int, type: String)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func add_building(type: String, at_position: Vector2) -> int:
|
|
|
|
|
var id: int = next_building_id
|
|
|
|
|
next_building_id += 1
|
|
|
|
|
buildings.append({
|
2026-08-14 20:32:58 +02:00
|
|
|
"id": id, "type": type, "position": at_position,
|
|
|
|
|
"inventory": INVENTORY.new(), "orders": [],
|
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
|
|
|
})
|
2026-08-15 00:09:10 +02:00
|
|
|
var building: Dictionary = buildings[-1]
|
|
|
|
|
if building_has_capability(building, "transport_hub"):
|
|
|
|
|
_provision_hub_transporters(building)
|
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
|
|
|
return id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func find_building(id: int) -> Dictionary:
|
|
|
|
|
for b in buildings:
|
|
|
|
|
if b.get("id", 0) == id:
|
|
|
|
|
return b
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 21:44:28 +02:00
|
|
|
func building_has_capability(building: Dictionary, capability: String) -> bool:
|
|
|
|
|
if building.is_empty():
|
|
|
|
|
return false
|
|
|
|
|
var definition: Resource = BUILDING_DEFINITIONS.get_definition(String(building.type))
|
|
|
|
|
return definition != null and capability in definition.capabilities
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 22:50:09 +02:00
|
|
|
# --- vessel types and their upgrades (#5) ---------------------------------
|
|
|
|
|
|
2026-08-15 00:09:10 +02:00
|
|
|
# Combat vessels can join the player fleet. Transporter metadata lives in the
|
|
|
|
|
# same definition catalog so the lab can upgrade its cargo and travel stats,
|
|
|
|
|
# but transporters themselves belong to hubs and are never production orders.
|
2026-08-11 22:50:09 +02:00
|
|
|
const VESSEL_INTERCEPTOR: String = "interceptor"
|
|
|
|
|
const VESSEL_SHIELD: String = "shield"
|
2026-08-15 04:45:15 +02:00
|
|
|
const VESSEL_GAS_BOMBER: String = "gas_bomber"
|
2026-08-11 22:50:09 +02:00
|
|
|
const VESSEL_TRANSPORTER: String = "transporter"
|
|
|
|
|
|
2026-08-14 22:12:57 +02:00
|
|
|
# Compatibility constants for callers and old tests. Runtime metadata is
|
|
|
|
|
# resolved from VesselDefinition below.
|
2026-08-11 22:50:09 +02:00
|
|
|
const SHIELD_COST: int = 14
|
2026-08-13 22:04:21 +02:00
|
|
|
const LAB_RESEARCH_TIME: float = 8.0
|
2026-08-12 22:47:46 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# Test seam. Headless tests need the timed path - queued, started, finishing
|
|
|
|
|
# - without sitting through a real 10-second build each, and a suite that
|
|
|
|
|
# slow stops being run. Left at 1.0 in the game.
|
|
|
|
|
var build_time_scale: float = 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func build_time(product: String) -> float:
|
2026-08-14 22:12:57 +02:00
|
|
|
var definition: Resource = VESSEL_DEFINITIONS.get_definition(product)
|
|
|
|
|
return (definition.build_time if definition != null else 10.0) * build_time_scale
|
2026-08-12 22:47:46 +02:00
|
|
|
|
2026-08-13 22:04:21 +02:00
|
|
|
|
|
|
|
|
func lab_research_time() -> float:
|
|
|
|
|
return LAB_RESEARCH_TIME * build_time_scale
|
|
|
|
|
|
2026-08-14 22:12:57 +02:00
|
|
|
# Compatibility constants. VesselDefinition owns the live base figures.
|
2026-08-11 22:50:09 +02:00
|
|
|
const SHIP_BASE_DAMAGE: int = 1
|
|
|
|
|
const SHIP_BASE_COOLDOWN: float = 0.5
|
|
|
|
|
const SHIP_BASE_RANGE: float = 320.0
|
|
|
|
|
const SHIP_BASE_SPEED: float = 460.0
|
|
|
|
|
const SHIP_BASE_ACCEL: float = 450.0
|
|
|
|
|
const SHIELD_BASE_CHARGES: int = 1
|
|
|
|
|
const SHIELD_BASE_RECHARGE: float = 6.0
|
|
|
|
|
var vessel_levels: Dictionary = {}
|
|
|
|
|
|
|
|
|
|
signal vessel_upgraded(upgrade_id: String)
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 23:10:07 +02:00
|
|
|
func has_ship_lab() -> bool:
|
|
|
|
|
for b in buildings:
|
2026-08-14 21:44:28 +02:00
|
|
|
if building_has_capability(b, "vessel_research"):
|
2026-08-11 23:10:07 +02:00
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func shield_vessel_unlocked() -> bool:
|
2026-08-14 22:12:57 +02:00
|
|
|
return vessel_is_unlocked(VESSEL_SHIELD)
|
2026-08-11 23:10:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
const UNLOCK_SHIELD_VESSEL: String = "unlock_shield"
|
|
|
|
|
const UNLOCK_SHIELD_COST: int = 45
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func unlock_shield_vessel() -> bool:
|
2026-08-14 22:12:57 +02:00
|
|
|
return unlock_vessel(VESSEL_SHIELD)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func vessel_definition(vessel: String) -> Resource:
|
|
|
|
|
return VESSEL_DEFINITIONS.get_definition(vessel)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func vessel_name(vessel: String) -> String:
|
|
|
|
|
var definition: Resource = vessel_definition(vessel)
|
|
|
|
|
return definition.display_name if definition != null else vessel.capitalize()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func vessel_has_capability(vessel: String, capability: String) -> bool:
|
|
|
|
|
var definition: Resource = vessel_definition(vessel)
|
|
|
|
|
return definition != null and definition.has_capability(capability)
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 00:09:10 +02:00
|
|
|
func vessel_is_fleet_eligible(vessel: String) -> bool:
|
|
|
|
|
var definition: Resource = vessel_definition(vessel)
|
|
|
|
|
return definition != null and definition.fleet_eligible
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 22:12:57 +02:00
|
|
|
func vessel_is_unlocked(vessel: String) -> bool:
|
|
|
|
|
var definition: Resource = vessel_definition(vessel)
|
|
|
|
|
if definition == null:
|
2026-08-11 23:10:07 +02:00
|
|
|
return false
|
2026-08-14 22:12:57 +02:00
|
|
|
return definition.unlock_id == "" or int(vessel_levels.get(definition.unlock_id, 0)) > 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func vessel_for_unlock(unlock_id: String) -> Resource:
|
|
|
|
|
for definition in VESSEL_DEFINITIONS.all():
|
|
|
|
|
if definition.unlock_id == unlock_id:
|
|
|
|
|
return definition
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func unlock_vessel(vessel: String) -> bool:
|
|
|
|
|
var definition: Resource = vessel_definition(vessel)
|
|
|
|
|
if definition == null or definition.unlock_id == "" or vessel_is_unlocked(vessel) or not has_ship_lab():
|
|
|
|
|
return false
|
2026-08-15 04:45:15 +02:00
|
|
|
var costs: Dictionary = research_costs(definition.unlock_id)
|
|
|
|
|
if not can_afford_resources(costs):
|
2026-08-11 23:10:07 +02:00
|
|
|
return false
|
2026-08-15 04:45:15 +02:00
|
|
|
for resource_type in costs:
|
|
|
|
|
bank_inventory.withdraw(String(resource_type), int(costs[resource_type]))
|
2026-08-14 22:12:57 +02:00
|
|
|
vessel_levels[definition.unlock_id] = 1
|
|
|
|
|
vessel_upgraded.emit(definition.unlock_id)
|
2026-08-11 23:10:07 +02:00
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 22:50:09 +02:00
|
|
|
func upgrade_def(id: String) -> Dictionary:
|
2026-08-14 22:12:57 +02:00
|
|
|
for definition in VESSEL_DEFINITIONS.all():
|
|
|
|
|
for upgrade in definition.upgrades:
|
|
|
|
|
if upgrade.upgrade_id == id:
|
|
|
|
|
return {"id": upgrade.upgrade_id, "vessel": definition.definition_id,
|
|
|
|
|
"name": upgrade.display_name, "desc": upgrade.description,
|
|
|
|
|
"cost": upgrade.base_cost, "max_level": upgrade.max_level,
|
|
|
|
|
"icon": upgrade.icon, "resource": upgrade}
|
2026-08-11 22:50:09 +02:00
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func upgrades_for(vessel: String) -> Array:
|
|
|
|
|
var out: Array = []
|
2026-08-14 22:12:57 +02:00
|
|
|
var definition: Resource = vessel_definition(vessel)
|
|
|
|
|
if definition == null:
|
|
|
|
|
return out
|
|
|
|
|
for upgrade in definition.upgrades:
|
|
|
|
|
out.append({"id": upgrade.upgrade_id, "vessel": vessel,
|
|
|
|
|
"name": upgrade.display_name, "desc": upgrade.description,
|
|
|
|
|
"cost": upgrade.base_cost, "max_level": upgrade.max_level,
|
|
|
|
|
"icon": upgrade.icon, "resource": upgrade})
|
2026-08-11 22:50:09 +02:00
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func upgrade_level(id: String) -> int:
|
|
|
|
|
return int(vessel_levels.get(id, 0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func upgrade_maxed(id: String) -> bool:
|
|
|
|
|
var d: Dictionary = upgrade_def(id)
|
|
|
|
|
return d.is_empty() or upgrade_level(id) >= int(d.max_level)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func upgrade_cost(id: String) -> int:
|
|
|
|
|
var d: Dictionary = upgrade_def(id)
|
|
|
|
|
if d.is_empty():
|
|
|
|
|
return 0
|
|
|
|
|
return int(d.cost) * (upgrade_level(id) + 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func can_buy_upgrade(id: String) -> bool:
|
2026-08-11 23:10:07 +02:00
|
|
|
return has_ship_lab() and not upgrade_maxed(id) and can_afford(upgrade_cost(id))
|
2026-08-11 22:50:09 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func buy_upgrade(id: String) -> bool:
|
|
|
|
|
if not can_buy_upgrade(id):
|
|
|
|
|
return false
|
|
|
|
|
if not spend(upgrade_cost(id)):
|
|
|
|
|
return false
|
|
|
|
|
vessel_levels[id] = upgrade_level(id) + 1
|
|
|
|
|
vessel_upgraded.emit(id)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 22:12:57 +02:00
|
|
|
func vessel_stat(vessel: String, stat: String) -> float:
|
|
|
|
|
var definition: Resource = vessel_definition(vessel)
|
|
|
|
|
if definition == null:
|
|
|
|
|
return 0.0
|
|
|
|
|
var value: float = float(definition.base_stats.get(stat, 0.0))
|
|
|
|
|
for upgrade in definition.upgrades:
|
|
|
|
|
var level: int = upgrade_level(upgrade.upgrade_id)
|
|
|
|
|
value += float(upgrade.additive_modifiers.get(stat, 0.0)) * level
|
|
|
|
|
if upgrade.multiplicative_modifiers.has(stat):
|
|
|
|
|
value *= pow(float(upgrade.multiplicative_modifiers[stat]), level)
|
|
|
|
|
if definition.minimum_stats.has(stat):
|
|
|
|
|
value = maxf(value, float(definition.minimum_stats[stat]))
|
|
|
|
|
return value
|
2026-08-11 22:50:09 +02:00
|
|
|
|
|
|
|
|
|
2026-08-14 22:12:57 +02:00
|
|
|
func ship_damage(vessel: String) -> int: return int(vessel_stat(vessel, "damage"))
|
|
|
|
|
func ship_fire_cooldown(vessel: String) -> float: return vessel_stat(vessel, "fire_cooldown")
|
|
|
|
|
func ship_fire_range(vessel: String) -> float: return vessel_stat(vessel, "fire_range")
|
|
|
|
|
func ship_max_speed(vessel: String) -> float: return vessel_stat(vessel, "max_speed")
|
|
|
|
|
func ship_acceleration(vessel: String) -> float: return vessel_stat(vessel, "acceleration")
|
2026-08-11 22:50:09 +02:00
|
|
|
|
|
|
|
|
func shield_max_charges() -> int:
|
2026-08-14 22:12:57 +02:00
|
|
|
var definition: Resource = VESSEL_DEFINITIONS.with_capability("shield")
|
|
|
|
|
return int(vessel_stat(definition.definition_id, "shield_charges"))
|
2026-08-11 22:50:09 +02:00
|
|
|
|
|
|
|
|
func shield_recharge_time() -> float:
|
2026-08-14 22:12:57 +02:00
|
|
|
var definition: Resource = VESSEL_DEFINITIONS.with_capability("shield")
|
|
|
|
|
return vessel_stat(definition.definition_id, "shield_recharge")
|
2026-08-11 22:50:09 +02:00
|
|
|
|
|
|
|
|
func transporter_load_limit() -> int:
|
2026-08-14 22:12:57 +02:00
|
|
|
var definition: Resource = VESSEL_DEFINITIONS.with_capability("transport")
|
|
|
|
|
return int(vessel_stat(definition.definition_id, "cargo_capacity"))
|
2026-08-11 22:50:09 +02:00
|
|
|
|
|
|
|
|
func transporter_speed() -> float:
|
2026-08-14 22:12:57 +02:00
|
|
|
var definition: Resource = VESSEL_DEFINITIONS.with_capability("transport")
|
|
|
|
|
return vessel_stat(definition.definition_id, "transport_speed")
|
2026-08-11 22:50:09 +02:00
|
|
|
|
|
|
|
|
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
const PRODUCT_SHIP: String = "interceptor"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func product_cost(product: String) -> int:
|
2026-08-15 04:45:15 +02:00
|
|
|
return int(product_costs(product).get(RESOURCE_ORE, 0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func product_costs(product: String) -> Dictionary:
|
2026-08-14 22:12:57 +02:00
|
|
|
var definition: Resource = vessel_definition(product)
|
2026-08-15 04:45:15 +02:00
|
|
|
if definition == null:
|
|
|
|
|
return {}
|
|
|
|
|
if not definition.build_resource_costs.is_empty():
|
|
|
|
|
return definition.build_resource_costs.duplicate()
|
|
|
|
|
return {RESOURCE_ORE: definition.build_cost} if definition.build_cost > 0 else {}
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func can_produce(type: String) -> bool:
|
2026-08-14 21:44:28 +02:00
|
|
|
var definition: Resource = BUILDING_DEFINITIONS.get_definition(type)
|
|
|
|
|
return definition != null and "production" in definition.capabilities
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
func building_stock(id: int) -> int:
|
2026-08-14 20:32:58 +02:00
|
|
|
return building_resource(id, RESOURCE_ORE)
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
func pending_orders(id: int) -> int:
|
|
|
|
|
var b: Dictionary = find_building(id)
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
return 0 if b.is_empty() else (b.get("orders", []) as Array).size()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func pending_orders_of(id: int, product: String) -> int:
|
|
|
|
|
var b: Dictionary = find_building(id)
|
|
|
|
|
if b.is_empty():
|
|
|
|
|
return 0
|
|
|
|
|
var n: int = 0
|
|
|
|
|
for o in b.get("orders", []):
|
2026-08-12 22:47:46 +02:00
|
|
|
if String(o.product) == product:
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
n += 1
|
|
|
|
|
return n
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 22:47:46 +02:00
|
|
|
func order_list(id: int) -> Array:
|
|
|
|
|
# What the queue panel draws: product, and how far along the one being
|
|
|
|
|
# built is. Everything behind it reads as simply waiting.
|
|
|
|
|
var out: Array = []
|
|
|
|
|
var b: Dictionary = find_building(id)
|
|
|
|
|
if b.is_empty():
|
|
|
|
|
return out
|
|
|
|
|
for o in b.get("orders", []):
|
|
|
|
|
var product: String = String(o.product)
|
|
|
|
|
out.append({
|
|
|
|
|
"product": product,
|
|
|
|
|
"started": bool(o.started),
|
|
|
|
|
"progress": clampf(float(o.elapsed) / maxf(build_time(product), 0.001), 0.0, 1.0),
|
|
|
|
|
})
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func cancel_order(id: int, index: int) -> bool:
|
|
|
|
|
# Refunds whatever was actually taken: a build that has started has its
|
|
|
|
|
# ore in the building already, one still waiting never paid.
|
|
|
|
|
var b: Dictionary = find_building(id)
|
|
|
|
|
if b.is_empty():
|
|
|
|
|
return false
|
|
|
|
|
var orders: Array = b.get("orders", [])
|
|
|
|
|
if index < 0 or index >= orders.size():
|
|
|
|
|
return false
|
|
|
|
|
var order: Dictionary = orders[index]
|
|
|
|
|
if bool(order.started):
|
2026-08-15 04:45:15 +02:00
|
|
|
var inventory: RefCounted = _inventory_for_record(b)
|
|
|
|
|
var costs: Dictionary = product_costs(String(order.product))
|
|
|
|
|
for resource_type in costs:
|
|
|
|
|
inventory.deposit(String(resource_type), int(costs[resource_type]))
|
2026-08-12 22:47:46 +02:00
|
|
|
orders.remove_at(index)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
func outstanding_cost(id: int) -> int:
|
2026-08-15 04:45:15 +02:00
|
|
|
return outstanding_resource_cost(id, RESOURCE_ORE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func outstanding_costs(id: int) -> Dictionary:
|
|
|
|
|
var result: Dictionary = {}
|
|
|
|
|
for resource_type in resource_types():
|
|
|
|
|
var need: int = outstanding_resource_cost(id, resource_type)
|
|
|
|
|
if need > 0:
|
|
|
|
|
result[resource_type] = need
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func outstanding_resource_cost(id: int, resource_type: String) -> int:
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
# What this building still needs delivered to clear everything queued.
|
|
|
|
|
var b: Dictionary = find_building(id)
|
|
|
|
|
if b.is_empty():
|
|
|
|
|
return 0
|
|
|
|
|
var need: int = 0
|
|
|
|
|
for o in b.get("orders", []):
|
2026-08-12 22:47:46 +02:00
|
|
|
if not bool(o.started):
|
2026-08-15 04:45:15 +02:00
|
|
|
need += int(product_costs(String(o.product)).get(resource_type, 0))
|
2026-08-14 22:46:25 +02:00
|
|
|
for order in b.get("research_orders", []):
|
|
|
|
|
if not bool(order.started):
|
2026-08-15 04:45:15 +02:00
|
|
|
need += int(research_costs(String(order.id)).get(resource_type, 0))
|
|
|
|
|
return maxi(need - _inventory_for_record(b).amount(resource_type), 0)
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
func deposit_to_building(id: int, amount: int) -> int:
|
2026-08-15 04:45:15 +02:00
|
|
|
return deposit_resource_to_building(id, RESOURCE_ORE, amount)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func deposit_resource_to_building(id: int, resource_type: String, amount: int) -> int:
|
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 amount <= 0:
|
|
|
|
|
return 0
|
|
|
|
|
var b: Dictionary = find_building(id)
|
|
|
|
|
if b.is_empty():
|
|
|
|
|
return 0
|
2026-08-15 04:45:15 +02:00
|
|
|
return _inventory_for_record(b).deposit(resource_type, amount)
|
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
|
|
|
|
|
|
|
|
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool:
|
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 b: Dictionary = find_building(id)
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
if b.is_empty() or not can_produce(b.type):
|
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
|
|
|
return false
|
2026-08-14 22:12:57 +02:00
|
|
|
var definition: Resource = vessel_definition(product)
|
2026-08-15 00:09:10 +02:00
|
|
|
if definition == null or not definition.fleet_eligible \
|
|
|
|
|
or not vessel_is_unlocked(product):
|
2026-08-11 19:43:55 +02:00
|
|
|
return false
|
2026-08-12 22:47:46 +02:00
|
|
|
(b.orders as Array).append({"product": product, "elapsed": 0.0, "started": false})
|
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
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 22:47:46 +02:00
|
|
|
func _advance_production(delta: float) -> 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
|
|
|
for b in buildings:
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
if not can_produce(b.type):
|
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
|
|
|
continue
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
var orders: Array = b.get("orders", [])
|
2026-08-12 22:47:46 +02:00
|
|
|
if orders.is_empty():
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# ONE at a time, in order. A queue the player can watch and cancel
|
|
|
|
|
# has to have a thing being built and things waiting behind it -
|
|
|
|
|
# clearing the whole queue the instant the ore lands (which is what
|
|
|
|
|
# this did before #23) leaves nothing to see or cancel.
|
|
|
|
|
var order: Dictionary = orders[0]
|
|
|
|
|
var product: String = String(order.product)
|
2026-08-15 04:45:15 +02:00
|
|
|
var costs: Dictionary = product_costs(product)
|
2026-08-14 20:32:58 +02:00
|
|
|
var inventory: RefCounted = _inventory_for_record(b)
|
2026-08-14 22:12:57 +02:00
|
|
|
var definition: Resource = vessel_definition(product)
|
2026-08-12 22:47:46 +02:00
|
|
|
|
|
|
|
|
if not bool(order.started):
|
2026-08-15 04:45:15 +02:00
|
|
|
var affordable := true
|
|
|
|
|
for resource_type in costs:
|
|
|
|
|
if inventory.amount(String(resource_type)) < int(costs[resource_type]):
|
|
|
|
|
affordable = false
|
|
|
|
|
break
|
|
|
|
|
if not affordable:
|
2026-08-12 22:47:46 +02:00
|
|
|
continue
|
2026-08-15 04:45:15 +02:00
|
|
|
for resource_type in costs:
|
|
|
|
|
inventory.withdraw(String(resource_type), int(costs[resource_type]))
|
2026-08-12 22:47:46 +02:00
|
|
|
order.started = true
|
|
|
|
|
|
|
|
|
|
order.elapsed = float(order.elapsed) + delta
|
|
|
|
|
if float(order.elapsed) < build_time(product):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
orders.pop_front()
|
2026-08-15 00:09:10 +02:00
|
|
|
# A finished combat hull launches from the crafter and physically flies
|
|
|
|
|
# to the mothership hangar. Hub transporters are provisioned by hubs and
|
|
|
|
|
# never enter this production path.
|
|
|
|
|
var mothership: Variant = get_mothership_position()
|
|
|
|
|
if mothership == null:
|
|
|
|
|
stored_vessels.append(product)
|
2026-08-12 22:47:46 +02:00
|
|
|
else:
|
2026-08-15 00:09:10 +02:00
|
|
|
vessel_deliveries.append({"type": product, "from": b.position,
|
|
|
|
|
"to": mothership, "progress": 0.0})
|
2026-08-12 22:47:46 +02:00
|
|
|
order_completed.emit(int(b.id), product)
|
|
|
|
|
|
|
|
|
|
|
2026-08-13 22:04:21 +02:00
|
|
|
func queue_lab_research(lab_id: int, upgrade_id: String) -> bool:
|
|
|
|
|
var lab: Dictionary = find_building(lab_id)
|
2026-08-14 21:44:28 +02:00
|
|
|
if not building_has_capability(lab, "vessel_research") or upgrade_id == "":
|
2026-08-13 22:04:21 +02:00
|
|
|
return false
|
|
|
|
|
var orders: Array = lab.get("research_orders", [])
|
2026-08-14 22:12:57 +02:00
|
|
|
var unlock_definition: Resource = vessel_for_unlock(upgrade_id)
|
|
|
|
|
if not orders.is_empty() or (unlock_definition == null and upgrade_maxed(upgrade_id)):
|
2026-08-13 22:04:21 +02:00
|
|
|
return false
|
|
|
|
|
orders.append({"id": upgrade_id, "elapsed": 0.0, "started": false})
|
|
|
|
|
lab.research_orders = orders
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func lab_research_order(lab_id: int) -> Dictionary:
|
|
|
|
|
var lab: Dictionary = find_building(lab_id)
|
|
|
|
|
var orders: Array = lab.get("research_orders", [])
|
|
|
|
|
return {} if orders.is_empty() else orders[0]
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 22:46:25 +02:00
|
|
|
func research_cost(id: String) -> int:
|
2026-08-15 04:45:15 +02:00
|
|
|
return int(research_costs(id).get(RESOURCE_ORE, 0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func research_costs(id: String) -> Dictionary:
|
2026-08-14 22:46:25 +02:00
|
|
|
var unlock_definition: Resource = vessel_for_unlock(id)
|
2026-08-15 04:45:15 +02:00
|
|
|
if unlock_definition != null:
|
|
|
|
|
if not unlock_definition.unlock_resource_costs.is_empty():
|
|
|
|
|
return unlock_definition.unlock_resource_costs.duplicate()
|
|
|
|
|
return {RESOURCE_ORE: unlock_definition.unlock_cost} if unlock_definition.unlock_cost > 0 else {}
|
|
|
|
|
var cost: int = upgrade_cost(id)
|
|
|
|
|
return {RESOURCE_ORE: cost} if cost > 0 else {}
|
2026-08-14 22:46:25 +02:00
|
|
|
|
|
|
|
|
|
2026-08-13 22:04:21 +02:00
|
|
|
func _advance_lab_research(delta: float) -> void:
|
|
|
|
|
for lab in buildings:
|
2026-08-14 21:44:28 +02:00
|
|
|
if not building_has_capability(lab, "vessel_research"):
|
2026-08-13 22:04:21 +02:00
|
|
|
continue
|
|
|
|
|
var orders: Array = lab.get("research_orders", [])
|
|
|
|
|
if orders.is_empty():
|
|
|
|
|
continue
|
|
|
|
|
var order: Dictionary = orders[0]
|
|
|
|
|
var id: String = String(order.id)
|
2026-08-14 22:12:57 +02:00
|
|
|
var unlock_definition: Resource = vessel_for_unlock(id)
|
2026-08-15 04:45:15 +02:00
|
|
|
var costs: Dictionary = research_costs(id)
|
2026-08-14 20:32:58 +02:00
|
|
|
var inventory: RefCounted = _inventory_for_record(lab)
|
2026-08-13 22:04:21 +02:00
|
|
|
if not bool(order.started):
|
2026-08-15 04:45:15 +02:00
|
|
|
var affordable := true
|
|
|
|
|
for resource_type in costs:
|
|
|
|
|
if inventory.amount(String(resource_type)) < int(costs[resource_type]):
|
|
|
|
|
affordable = false
|
|
|
|
|
break
|
|
|
|
|
if not affordable:
|
2026-08-13 22:04:21 +02:00
|
|
|
continue
|
2026-08-15 04:45:15 +02:00
|
|
|
for resource_type in costs:
|
|
|
|
|
inventory.withdraw(String(resource_type), int(costs[resource_type]))
|
2026-08-13 22:04:21 +02:00
|
|
|
order.started = true
|
|
|
|
|
order.elapsed = float(order.elapsed) + delta
|
|
|
|
|
if float(order.elapsed) < lab_research_time():
|
|
|
|
|
continue
|
2026-08-14 22:12:57 +02:00
|
|
|
if unlock_definition != null:
|
2026-08-13 22:04:21 +02:00
|
|
|
vessel_levels[id] = 1
|
|
|
|
|
else:
|
|
|
|
|
vessel_levels[id] = upgrade_level(id) + 1
|
|
|
|
|
vessel_upgraded.emit(id)
|
|
|
|
|
orders.pop_front()
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 22:47:46 +02:00
|
|
|
func claim_stored_vessel(index: int) -> String:
|
|
|
|
|
# Hands one finished vessel over to be crewed. Returns its type, or ""
|
|
|
|
|
# when there is nothing there - the caller decides whether the fleet
|
|
|
|
|
# actually has room.
|
|
|
|
|
if index < 0 or index >= stored_vessels.size():
|
|
|
|
|
return ""
|
|
|
|
|
return String(stored_vessels[index])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func take_stored_vessel(index: int) -> String:
|
|
|
|
|
var product: String = claim_stored_vessel(index)
|
|
|
|
|
if product == "":
|
|
|
|
|
return ""
|
|
|
|
|
stored_vessels.remove_at(index)
|
|
|
|
|
return product
|
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 has_mothership() -> bool:
|
|
|
|
|
return get_mothership_position() != null
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 21:44:28 +02:00
|
|
|
func has_building_of_type(type: String) -> bool:
|
|
|
|
|
for building in buildings:
|
|
|
|
|
if building.type == type:
|
|
|
|
|
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 remove_buildings_of_type(type: String) -> int:
|
|
|
|
|
var kept: Array = []
|
2026-08-15 00:09:10 +02:00
|
|
|
var removed_ids: Array = []
|
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 removed: int = 0
|
|
|
|
|
for b in buildings:
|
|
|
|
|
if b.type == type:
|
|
|
|
|
removed += 1
|
2026-08-15 00:09:10 +02:00
|
|
|
removed_ids.append(int(b.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
|
|
|
else:
|
|
|
|
|
kept.append(b)
|
|
|
|
|
buildings = kept
|
2026-08-15 00:09:10 +02:00
|
|
|
if type == BUILDING_TRANSPORT_HUB and not removed_ids.is_empty():
|
|
|
|
|
var kept_transporters: Array = []
|
|
|
|
|
for transporter in transporters:
|
|
|
|
|
if int(transporter.get("hub_id", 0)) not in removed_ids:
|
|
|
|
|
kept_transporters.append(transporter)
|
|
|
|
|
transporters = kept_transporters
|
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 removed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_mothership_position() -> Variant:
|
|
|
|
|
# Where a wiped-out fleet gets rebuilt. Returns null when no mothership
|
|
|
|
|
# has been built yet, so the caller can fall back to the world origin.
|
|
|
|
|
for b in buildings:
|
2026-08-14 21:44:28 +02:00
|
|
|
if building_has_capability(b, "bank"):
|
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 b.position
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Fleet formation -------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# Where each vessel sits relative to the fleet centre, editable at a command
|
|
|
|
|
# centre. Lives here (not as a const on Fleet) so a layout the player has
|
|
|
|
|
# arranged survives the trip to a planet's surface and back, and so a
|
|
|
|
|
# rebuilt fleet keeps their arrangement.
|
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
|
|
|
# Slots beyond BASE_FLEET_SIZE only become usable once the matching fleet
|
|
|
|
|
# capacity upgrade is researched, but they are defined up front so the wedge
|
|
|
|
|
# keeps a deliberate shape as it grows rather than having positions invented
|
|
|
|
|
# at unlock time.
|
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
|
|
|
const DEFAULT_FORMATION: Array = [
|
|
|
|
|
Vector2(0, -48), Vector2(-30, -8), Vector2(30, -8), Vector2(-60, 32), Vector2(60, 32),
|
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
|
|
|
Vector2(-90, 72), Vector2(90, 72), Vector2(0, 56),
|
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 formation: Array = DEFAULT_FORMATION.duplicate()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func reset_formation() -> void:
|
|
|
|
|
formation = DEFAULT_FORMATION.duplicate()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func rotate_formation(radians: float) -> void:
|
|
|
|
|
# Pivots every slot about the fleet centre, keeping the shape intact
|
|
|
|
|
# and only changing which way it faces.
|
|
|
|
|
for i in range(formation.size()):
|
|
|
|
|
formation[i] = formation[i].rotated(radians)
|
|
|
|
|
|
|
|
|
|
|
2026-08-13 16:38:41 +02:00
|
|
|
# --- Extractors ------------------------------------------------------------
|
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 16:38:41 +02:00
|
|
|
# Extractors mine into their OWN local stock. They do not own a magical
|
|
|
|
|
# hauler: ore becomes spendable only when a hub-stationed transporter picks
|
|
|
|
|
# up a full load and physically delivers it to the mothership. Entries are
|
2026-08-14 22:16:40 +02:00
|
|
|
# {"position", "planet_id", "planet_name", "resource_type", "inventory", "mining_progress"}.
|
2026-08-13 16:38:41 +02:00
|
|
|
const EXTRACTOR_RATE: float = 2.0 # ore per second
|
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 extractors: Array = []
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 22:16:40 +02:00
|
|
|
func has_extractor_at(planet_key: String) -> bool:
|
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
|
|
|
for e in extractors:
|
2026-08-14 22:16:40 +02:00
|
|
|
# Old saves have no planet_id; their name remains a migration fallback.
|
|
|
|
|
if String(e.get("planet_id", e.get("planet_name", ""))) == planet_key:
|
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 true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 22:21:01 +02:00
|
|
|
func extractor_index_at(planet_key: String) -> int:
|
|
|
|
|
for i in range(extractors.size()):
|
|
|
|
|
var extractor: Dictionary = extractors[i]
|
|
|
|
|
if String(extractor.get("planet_id", extractor.get("planet_name", ""))) == planet_key:
|
|
|
|
|
return i
|
|
|
|
|
return -1
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 15:43:43 +02:00
|
|
|
signal extractor_added(planet_name: String)
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 22:16:40 +02:00
|
|
|
func add_extractor(planet_name: String, at_position: Vector2, resource_type: String = "ore",
|
|
|
|
|
planet_key: String = "") -> void:
|
|
|
|
|
var stable_id: String = planet_key if planet_key != "" else planet_name
|
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
|
|
|
extractors.append({
|
|
|
|
|
"position": at_position,
|
2026-08-14 22:16:40 +02:00
|
|
|
"planet_id": stable_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
|
|
|
"planet_name": planet_name,
|
2026-08-13 21:07:35 +02:00
|
|
|
"resource_type": resource_type,
|
2026-08-14 20:32:58 +02:00
|
|
|
"inventory": INVENTORY.new(),
|
2026-08-13 16:38:41 +02:00
|
|
|
"mining_progress": 0.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
|
|
|
})
|
2026-08-11 15:43:43 +02:00
|
|
|
# An extractor is built from SPACE, with the fleet parked on the planet -
|
|
|
|
|
# so the Planet node is already in the tree with its _ready() long since
|
|
|
|
|
# run. Without this it kept showing no derrick until it happened to
|
|
|
|
|
# despawn and respawn, which reads as the build having done nothing.
|
2026-08-14 22:16:40 +02:00
|
|
|
extractor_added.emit(stable_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
|
|
|
|
|
|
|
|
|
2026-08-13 16:38:41 +02:00
|
|
|
func extractor_stock(index: int) -> int:
|
|
|
|
|
if index < 0 or index >= extractors.size():
|
|
|
|
|
return 0
|
2026-08-14 20:32:58 +02:00
|
|
|
var extractor: Dictionary = extractors[index]
|
|
|
|
|
return _extractor_inventory(extractor).amount(String(extractor.get("resource_type", RESOURCE_ORE)))
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 22:21:01 +02:00
|
|
|
func extractor_resource(index: int, resource_type: String) -> int:
|
|
|
|
|
if index < 0 or index >= extractors.size():
|
|
|
|
|
return 0
|
|
|
|
|
return _extractor_inventory(extractors[index]).amount(resource_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func transfer_resource_to_extractor(index: int, resource_type: String, amount: int) -> int:
|
|
|
|
|
if index < 0 or index >= extractors.size():
|
|
|
|
|
return 0
|
|
|
|
|
return fleet_inventory.transfer_to(_extractor_inventory(extractors[index]), resource_type, amount)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func transfer_resource_from_extractor(index: int, resource_type: String, amount: int) -> int:
|
|
|
|
|
if index < 0 or index >= extractors.size():
|
|
|
|
|
return 0
|
|
|
|
|
return _extractor_inventory(extractors[index]).transfer_to(fleet_inventory, resource_type, amount)
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 20:32:58 +02:00
|
|
|
func _extractor_inventory(extractor: Dictionary) -> RefCounted:
|
|
|
|
|
var inventory: RefCounted = extractor.get("inventory") as RefCounted
|
|
|
|
|
if inventory == null:
|
|
|
|
|
inventory = INVENTORY.new()
|
|
|
|
|
extractor.inventory = inventory
|
|
|
|
|
# Old extractor state had one stock field whose material was described by
|
|
|
|
|
# resource_type. Absorb it into that key on first access.
|
|
|
|
|
if extractor.has("stock"):
|
|
|
|
|
inventory.set_amount(String(extractor.get("resource_type", RESOURCE_ORE)), int(extractor.stock))
|
|
|
|
|
extractor.erase("stock")
|
|
|
|
|
return inventory
|
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
|
|
|
|
|
|
|
|
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
# --- gas burners (#54) -----------------------------------------------------
|
|
|
|
|
#
|
|
|
|
|
# Converts gas into energy at the building's own site, simulated here
|
|
|
|
|
# alongside extractor mining and production so it keeps running whether or
|
|
|
|
|
# not the player is nearby. Deliberately withdraws only from the building's
|
|
|
|
|
# OWN inventory, never the bank - a burner with no local gas simply stalls,
|
|
|
|
|
# exactly like a lab or crafter waiting on materials, rather than conjuring
|
|
|
|
|
# energy for free.
|
|
|
|
|
|
|
|
|
|
const GAS_BURN_RATE: float = 1.0 # gas/second
|
|
|
|
|
const ENERGY_PER_GAS: int = 2
|
|
|
|
|
# Local gas reserve a burner tries to keep topped up. Transporters treat
|
|
|
|
|
# falling short of this the same way they treat a queued production/research
|
|
|
|
|
# order (see _dispatch_transporters()'s gas_burner_gas_deficit() use).
|
|
|
|
|
const GAS_BURNER_BUFFER: int = 40
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func gas_burner_gas_deficit(id: int) -> int:
|
|
|
|
|
var b: Dictionary = find_building(id)
|
|
|
|
|
if not building_has_capability(b, "gas_burner"):
|
|
|
|
|
return 0
|
|
|
|
|
return maxi(0, GAS_BURNER_BUFFER - building_resource(id, RESOURCE_GAS))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _advance_gas_burners(delta: float) -> void:
|
|
|
|
|
for b in buildings:
|
|
|
|
|
if not building_has_capability(b, "gas_burner"):
|
|
|
|
|
continue
|
|
|
|
|
var inventory: RefCounted = _inventory_for_record(b)
|
|
|
|
|
if inventory.amount(RESOURCE_GAS) <= 0:
|
|
|
|
|
continue # stalled - no local gas to burn
|
|
|
|
|
var progress: float = float(b.get("gas_burn_progress", 0.0)) + delta * GAS_BURN_RATE
|
|
|
|
|
var whole: int = mini(int(floor(progress)), inventory.amount(RESOURCE_GAS))
|
|
|
|
|
if whole > 0:
|
|
|
|
|
inventory.withdraw(RESOURCE_GAS, whole)
|
|
|
|
|
inventory.deposit(RESOURCE_ENERGY, whole * ENERGY_PER_GAS)
|
|
|
|
|
progress -= whole
|
|
|
|
|
b.gas_burn_progress = progress
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 14:41:19 +02:00
|
|
|
# Planet conquest: a planet must have every one of its capture points held
|
|
|
|
|
# before an extractor can be built there (see BuildController's placement
|
|
|
|
|
# gates). Keyed by the same stable planet_id as `extractors`, entirely
|
|
|
|
|
# separate from it - a planet can be conquered long before anyone builds an
|
|
|
|
|
# extractor on it, or never get one at all.
|
|
|
|
|
var planet_capture: Dictionary = {}
|
|
|
|
|
|
|
|
|
|
signal planet_conquered(planet_id: String)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func register_capture_points(planet_key: String, total: int) -> void:
|
|
|
|
|
# Idempotent: landing again on a planet must not wipe points already
|
|
|
|
|
# captured on a previous visit.
|
|
|
|
|
if planet_capture.has(planet_key):
|
|
|
|
|
return
|
|
|
|
|
var captured: Array = []
|
|
|
|
|
captured.resize(total)
|
|
|
|
|
captured.fill(false)
|
2026-08-16 13:13:38 +02:00
|
|
|
# A planet demanding zero points is vacuously already held (#55: a
|
|
|
|
|
# black hole's "void" profile has no authored PlanetSetup at all, so
|
|
|
|
|
# this is the only registration it ever gets) - every planet WITH real
|
|
|
|
|
# points still starts unconquered, exactly as before.
|
|
|
|
|
planet_capture[planet_key] = {"captured": captured, "conquered": total <= 0}
|
2026-08-15 14:41:19 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func capture_state(planet_key: String) -> Dictionary:
|
|
|
|
|
return planet_capture.get(planet_key, {"captured": [], "conquered": false})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func is_point_captured(planet_key: String, index: int) -> bool:
|
|
|
|
|
var captured: Array = capture_state(planet_key).get("captured", [])
|
|
|
|
|
if index < 0 or index >= captured.size():
|
|
|
|
|
return false
|
|
|
|
|
return bool(captured[index])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func mark_point_captured(planet_key: String, index: int) -> void:
|
|
|
|
|
if not planet_capture.has(planet_key):
|
|
|
|
|
return
|
|
|
|
|
var state: Dictionary = planet_capture[planet_key]
|
|
|
|
|
var captured: Array = state.captured
|
|
|
|
|
if index < 0 or index >= captured.size():
|
|
|
|
|
return
|
|
|
|
|
captured[index] = true
|
|
|
|
|
if state.conquered:
|
|
|
|
|
return
|
|
|
|
|
if captured.all(func(c): return bool(c)):
|
|
|
|
|
state.conquered = true
|
|
|
|
|
planet_conquered.emit(planet_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func is_planet_conquered(planet_key: String) -> bool:
|
|
|
|
|
return bool(capture_state(planet_key).get("conquered", false))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func captured_point_count(planet_key: String) -> int:
|
|
|
|
|
var captured: Array = capture_state(planet_key).get("captured", [])
|
|
|
|
|
var count: int = 0
|
|
|
|
|
for c in captured:
|
|
|
|
|
if c:
|
|
|
|
|
count += 1
|
|
|
|
|
return count
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 21:22:10 +02:00
|
|
|
func total_capture_points(planet_key: String) -> int:
|
|
|
|
|
return capture_state(planet_key).get("captured", []).size()
|
|
|
|
|
|
|
|
|
|
|
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 _process(delta: float) -> void:
|
2026-08-13 16:38:41 +02:00
|
|
|
# Runs in the autoload, so mining and transport continue while the player
|
|
|
|
|
# is on a planet's surface or nowhere near either.
|
2026-08-12 22:47:46 +02:00
|
|
|
_advance_production(delta)
|
2026-08-13 22:04:21 +02:00
|
|
|
_advance_lab_research(delta)
|
2026-08-13 21:39:05 +02:00
|
|
|
_advance_vessel_deliveries(delta)
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
_advance_gas_burners(delta)
|
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
|
|
|
for e in extractors:
|
2026-08-13 16:38:41 +02:00
|
|
|
e.mining_progress = float(e.get("mining_progress", 0.0)) + delta * EXTRACTOR_RATE
|
|
|
|
|
var whole: int = int(floor(float(e.mining_progress)))
|
|
|
|
|
if whole > 0:
|
2026-08-14 20:32:58 +02:00
|
|
|
_extractor_inventory(e).deposit(String(e.get("resource_type", RESOURCE_ORE)), whole)
|
2026-08-13 16:38:41 +02:00
|
|
|
e.mining_progress = float(e.mining_progress) - whole
|
|
|
|
|
_advance_transporters(delta)
|
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 21:39:05 +02:00
|
|
|
func vessel_delivery_position(delivery: Dictionary) -> Vector2:
|
|
|
|
|
return (delivery.from as Vector2).lerp(delivery.to as Vector2,
|
|
|
|
|
clampf(float(delivery.progress), 0.0, 1.0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _advance_vessel_deliveries(delta: float) -> void:
|
|
|
|
|
for i in range(vessel_deliveries.size() - 1, -1, -1):
|
|
|
|
|
var delivery: Dictionary = vessel_deliveries[i]
|
|
|
|
|
var distance: float = (delivery.from as Vector2).distance_to(delivery.to as Vector2)
|
2026-08-14 22:30:19 +02:00
|
|
|
var delivery_speed: float = vessel_stat(String(delivery.type), "delivery_speed")
|
|
|
|
|
if delivery_speed <= 0.0:
|
|
|
|
|
delivery_speed = VESSEL_DELIVERY_SPEED
|
|
|
|
|
delivery.progress = float(delivery.progress) + delta / maxf(distance / delivery_speed, 0.001)
|
2026-08-13 21:39:05 +02:00
|
|
|
if float(delivery.progress) >= 1.0:
|
|
|
|
|
stored_vessels.append(String(delivery.type))
|
|
|
|
|
vessel_deliveries.remove_at(i)
|
|
|
|
|
|
|
|
|
|
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
# --- transporters (#19) ---------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# Vessels stationed at a transport hub that fetch ore from the mothership and
|
|
|
|
|
# deliver it to whichever building is waiting on it.
|
|
|
|
|
#
|
|
|
|
|
# Simulated HERE rather than by any node, exactly like the extractor haulers
|
|
|
|
|
# above and for the same reason: a route the player is nowhere near still has
|
|
|
|
|
# to run. `progress` drives both the drawn position and the moment the ore
|
|
|
|
|
# actually moves, so what you see and what happens cannot drift apart.
|
|
|
|
|
#
|
|
|
|
|
# Entries are {"hub_id", "state", "target_id", "carrying", "progress",
|
|
|
|
|
# "from", "to"}.
|
2026-08-15 00:09:10 +02:00
|
|
|
# How many transporters each hub owns. Raised for EVERY hub - built already
|
|
|
|
|
# or not - by research, and provisioned automatically rather than crafted.
|
2026-08-11 19:43:55 +02:00
|
|
|
const BASE_HUB_CAPACITY: int = 2
|
|
|
|
|
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
const TRANSPORTER_IDLE: String = "idle"
|
|
|
|
|
const TRANSPORTER_TO_MOTHERSHIP: String = "to_mothership"
|
|
|
|
|
const TRANSPORTER_TO_BUILDING: String = "to_building"
|
|
|
|
|
const TRANSPORTER_RETURNING: String = "returning"
|
2026-08-13 16:38:41 +02:00
|
|
|
const TRANSPORTER_TO_EXTRACTOR: String = "to_extractor"
|
|
|
|
|
const TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR: String = "extractor_to_mothership"
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
# Collecting an output resource (energy) directly from a regular building's
|
|
|
|
|
# own inventory - a gas burner, not an extractor - and banking it. Reuses
|
|
|
|
|
# `target_id` exactly like the ordinary supply path does, rather than
|
|
|
|
|
# `extractor_index`: the source is a building, already addressable that way.
|
|
|
|
|
const TRANSPORTER_TO_BUILDING_PICKUP: String = "to_building_pickup"
|
|
|
|
|
const TRANSPORTER_TO_MOTHERSHIP_FROM_BUILDING: String = "building_to_mothership"
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
|
|
|
|
|
var transporters: Array = []
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 19:43:55 +02:00
|
|
|
func hub_capacity() -> int:
|
|
|
|
|
# Per hub.
|
|
|
|
|
var n: int = BASE_HUB_CAPACITY
|
|
|
|
|
for id in unlocked:
|
|
|
|
|
n += int(skill_node(id).get("hub_bonus", 0))
|
|
|
|
|
return n
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func hub_count() -> int:
|
|
|
|
|
var n: int = 0
|
|
|
|
|
for b in buildings:
|
2026-08-14 21:44:28 +02:00
|
|
|
if building_has_capability(b, "transport_hub"):
|
2026-08-11 19:43:55 +02:00
|
|
|
n += 1
|
|
|
|
|
return n
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 00:09:10 +02:00
|
|
|
func _add_hub_transporter(hub: Dictionary) -> void:
|
|
|
|
|
transporters.append({
|
|
|
|
|
"hub_id": int(hub.id), "state": TRANSPORTER_IDLE, "target_id": 0,
|
|
|
|
|
"extractor_index": -1, "carrying": 0, "resource_type": RESOURCE_ORE,
|
|
|
|
|
"progress": 1.0, "from": hub.position, "to": hub.position,
|
|
|
|
|
})
|
2026-08-11 19:43:55 +02:00
|
|
|
|
|
|
|
|
|
2026-08-15 00:09:10 +02:00
|
|
|
func _provision_hub_transporters(hub: Dictionary) -> void:
|
|
|
|
|
var stationed: int = 0
|
|
|
|
|
for transporter in transporters:
|
|
|
|
|
if int(transporter.get("hub_id", 0)) == int(hub.id):
|
|
|
|
|
stationed += 1
|
|
|
|
|
while stationed < hub_capacity():
|
|
|
|
|
_add_hub_transporter(hub)
|
|
|
|
|
stationed += 1
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
|
|
|
|
|
|
2026-08-15 00:09:10 +02:00
|
|
|
func _sync_hub_transporters() -> void:
|
|
|
|
|
for hub in buildings:
|
|
|
|
|
if building_has_capability(hub, "transport_hub"):
|
|
|
|
|
_provision_hub_transporters(hub)
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func transporter_position(t: Dictionary) -> Vector2:
|
|
|
|
|
return (t.from as Vector2).lerp(t.to as Vector2, clampf(t.progress, 0.0, 1.0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _hub_position(hub_id: int) -> Variant:
|
|
|
|
|
var hub: Dictionary = find_building(hub_id)
|
|
|
|
|
return null if hub.is_empty() else hub.position
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _begin_leg(t: Dictionary, from: Vector2, to: Vector2, state: String) -> void:
|
|
|
|
|
t.from = from
|
|
|
|
|
t.to = to
|
|
|
|
|
t.state = state
|
|
|
|
|
t.progress = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _transporter_inbound_to(building_id: int) -> bool:
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
# Delivery-purpose states only (#54): a building can have BOTH a
|
|
|
|
|
# transporter delivering it something (gas, for a burner) and a
|
|
|
|
|
# separate one collecting something from it (energy) at once, since
|
|
|
|
|
# they are two different transporters and two different legs. Matching
|
|
|
|
|
# on target_id alone, regardless of state, would make either purpose
|
|
|
|
|
# block the other from ever being dispatched to the same building.
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
for t in transporters:
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
if (t.state == TRANSPORTER_TO_MOTHERSHIP or t.state == TRANSPORTER_TO_BUILDING) \
|
|
|
|
|
and int(t.target_id) == building_id:
|
|
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _transporter_inbound_for_pickup(building_id: int) -> bool:
|
|
|
|
|
for t in transporters:
|
|
|
|
|
if (t.state == TRANSPORTER_TO_BUILDING_PICKUP or t.state == TRANSPORTER_TO_MOTHERSHIP_FROM_BUILDING) \
|
|
|
|
|
and int(t.target_id) == building_id:
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
2026-08-13 16:38:41 +02:00
|
|
|
func _transporter_inbound_to_extractor(index: int) -> bool:
|
|
|
|
|
for t in transporters:
|
|
|
|
|
if t.state == TRANSPORTER_TO_EXTRACTOR \
|
|
|
|
|
or t.state == TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR:
|
|
|
|
|
if int(t.get("extractor_index", -1)) == index:
|
|
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
func _dispatch_transporters() -> void:
|
|
|
|
|
var mothership: Variant = get_mothership_position()
|
|
|
|
|
if mothership == null:
|
|
|
|
|
return # nowhere to load from
|
2026-08-15 10:41:18 +02:00
|
|
|
# Supply already-queued production and research before collecting passive
|
|
|
|
|
# extractor output. Extractors can refill forever, so servicing them first
|
|
|
|
|
# can otherwise consume every transporter each time one becomes idle and
|
|
|
|
|
# permanently starve a partly supplied order.
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
for b in buildings:
|
2026-08-15 04:45:15 +02:00
|
|
|
var needed_resource := ""
|
|
|
|
|
for resource_type in resource_types():
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
# A gas burner's ongoing gas deficit is folded in alongside
|
|
|
|
|
# queued production/research costs (#54) - both are just "this
|
|
|
|
|
# building wants more of resource_type", so the one dispatch
|
|
|
|
|
# path already below serves both without any new transporter
|
|
|
|
|
# state.
|
|
|
|
|
var need: int = maxi(outstanding_resource_cost(int(b.id), resource_type),
|
|
|
|
|
gas_burner_gas_deficit(int(b.id)) if resource_type == RESOURCE_GAS else 0)
|
|
|
|
|
if need > 0 and bank_inventory.amount(resource_type) > 0:
|
2026-08-15 04:45:15 +02:00
|
|
|
needed_resource = resource_type
|
|
|
|
|
break
|
|
|
|
|
if needed_resource == "":
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
continue
|
|
|
|
|
# One at a time per building: without this every idle transporter
|
|
|
|
|
# would be sent after the same shortfall and they would all arrive
|
2026-08-15 10:41:18 +02:00
|
|
|
# carrying resources nobody needs any more.
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
if _transporter_inbound_to(int(b.id)):
|
|
|
|
|
continue
|
|
|
|
|
var chosen: Dictionary = {}
|
|
|
|
|
var best_d: float = INF
|
|
|
|
|
for t in transporters:
|
|
|
|
|
if t.state != TRANSPORTER_IDLE:
|
|
|
|
|
continue
|
|
|
|
|
var here: Vector2 = transporter_position(t)
|
|
|
|
|
var d: float = here.distance_squared_to(b.position)
|
|
|
|
|
if d < best_d:
|
|
|
|
|
best_d = d
|
|
|
|
|
chosen = t
|
|
|
|
|
if chosen.is_empty():
|
|
|
|
|
return # every transporter is busy
|
|
|
|
|
chosen.target_id = int(b.id)
|
2026-08-15 04:45:15 +02:00
|
|
|
chosen.resource_type = needed_resource
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
_begin_leg(chosen, transporter_position(chosen), mothership, TRANSPORTER_TO_MOTHERSHIP)
|
|
|
|
|
|
2026-08-15 10:41:18 +02:00
|
|
|
# A distant extractor is worth visiting only once it has a full hold ready.
|
|
|
|
|
# This keeps the transporter system meaningful: mining is local until a
|
|
|
|
|
# vessel physically carries a researched-sized load home.
|
|
|
|
|
var load: int = transporter_load_limit()
|
|
|
|
|
for i in range(extractors.size()):
|
|
|
|
|
var extractor: Dictionary = extractors[i]
|
|
|
|
|
var resource_id: String = String(extractor.get("resource_type", RESOURCE_ORE))
|
|
|
|
|
if _extractor_inventory(extractor).amount(resource_id) < load or _transporter_inbound_to_extractor(i):
|
|
|
|
|
continue
|
|
|
|
|
var chosen_extractor: Dictionary = {}
|
|
|
|
|
var best_extractor_distance: float = INF
|
|
|
|
|
for t in transporters:
|
|
|
|
|
if t.state != TRANSPORTER_IDLE:
|
|
|
|
|
continue
|
|
|
|
|
var d: float = transporter_position(t).distance_squared_to(extractor.position)
|
|
|
|
|
if d < best_extractor_distance:
|
|
|
|
|
best_extractor_distance = d
|
|
|
|
|
chosen_extractor = t
|
|
|
|
|
if chosen_extractor.is_empty():
|
|
|
|
|
return
|
|
|
|
|
chosen_extractor.extractor_index = i
|
|
|
|
|
chosen_extractor.resource_type = resource_id
|
|
|
|
|
chosen_extractor.target_id = 0
|
|
|
|
|
_begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position,
|
|
|
|
|
TRANSPORTER_TO_EXTRACTOR)
|
|
|
|
|
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
# Collecting a gas burner's energy output, last: queued deliveries and
|
|
|
|
|
# extractor collection both outrank it, same "supply what's needed
|
|
|
|
|
# before what's merely ready" ordering _dispatch_transporters() already
|
|
|
|
|
# holds for extractors over queued orders (f5c60f8).
|
|
|
|
|
for b in buildings:
|
|
|
|
|
if not building_has_capability(b, "gas_burner"):
|
|
|
|
|
continue
|
|
|
|
|
if building_resource(int(b.id), RESOURCE_ENERGY) < load or _transporter_inbound_for_pickup(int(b.id)):
|
|
|
|
|
continue
|
|
|
|
|
var chosen_pickup: Dictionary = {}
|
|
|
|
|
var best_pickup_distance: float = INF
|
|
|
|
|
for t in transporters:
|
|
|
|
|
if t.state != TRANSPORTER_IDLE:
|
|
|
|
|
continue
|
|
|
|
|
var d: float = transporter_position(t).distance_squared_to(b.position)
|
|
|
|
|
if d < best_pickup_distance:
|
|
|
|
|
best_pickup_distance = d
|
|
|
|
|
chosen_pickup = t
|
|
|
|
|
if chosen_pickup.is_empty():
|
|
|
|
|
return
|
|
|
|
|
chosen_pickup.target_id = int(b.id)
|
|
|
|
|
chosen_pickup.resource_type = RESOURCE_ENERGY
|
|
|
|
|
chosen_pickup.extractor_index = -1
|
|
|
|
|
_begin_leg(chosen_pickup, transporter_position(chosen_pickup), b.position,
|
|
|
|
|
TRANSPORTER_TO_BUILDING_PICKUP)
|
|
|
|
|
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
|
|
|
|
|
func _advance_transporters(delta: float) -> void:
|
2026-08-15 00:09:10 +02:00
|
|
|
_sync_hub_transporters()
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
if transporters.is_empty():
|
|
|
|
|
return
|
|
|
|
|
_dispatch_transporters()
|
|
|
|
|
for t in transporters:
|
|
|
|
|
if t.state == TRANSPORTER_IDLE:
|
|
|
|
|
continue
|
|
|
|
|
var distance: float = (t.from as Vector2).distance_to(t.to as Vector2)
|
2026-08-11 22:50:09 +02:00
|
|
|
var leg: float = maxf(distance / transporter_speed(), 0.0001)
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
t.progress += delta / leg
|
|
|
|
|
if t.progress < 1.0:
|
|
|
|
|
continue
|
|
|
|
|
t.progress = 1.0
|
|
|
|
|
_arrive(t)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _arrive(t: Dictionary) -> void:
|
|
|
|
|
var here: Vector2 = t.to
|
|
|
|
|
var hub_pos: Variant = _hub_position(int(t.hub_id))
|
|
|
|
|
match t.state:
|
|
|
|
|
TRANSPORTER_TO_MOTHERSHIP:
|
|
|
|
|
var target: Dictionary = find_building(int(t.target_id))
|
2026-08-15 04:45:15 +02:00
|
|
|
var resource_type: String = String(t.get("resource_type", RESOURCE_ORE))
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
var want: int = maxi(outstanding_resource_cost(int(t.target_id), resource_type),
|
|
|
|
|
gas_burner_gas_deficit(int(t.target_id)) if resource_type == RESOURCE_GAS else 0)
|
2026-08-15 04:45:15 +02:00
|
|
|
var take: int = mini(mini(want, bank_inventory.amount(resource_type)), transporter_load_limit())
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
if take <= 0 or target.is_empty():
|
|
|
|
|
# Nothing to carry, or the building is gone. Go home empty.
|
|
|
|
|
t.target_id = 0
|
|
|
|
|
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
|
|
|
|
return
|
2026-08-15 04:45:15 +02:00
|
|
|
bank_inventory.withdraw(resource_type, take)
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
t.carrying = take
|
|
|
|
|
_begin_leg(t, here, target.position, TRANSPORTER_TO_BUILDING)
|
|
|
|
|
TRANSPORTER_TO_BUILDING:
|
2026-08-15 04:45:15 +02:00
|
|
|
# The cargo moves at exactly the moment the vessel arrives.
|
|
|
|
|
var resource_type: String = String(t.get("resource_type", RESOURCE_ORE))
|
|
|
|
|
if deposit_resource_to_building(int(t.target_id), resource_type, int(t.carrying)) <= 0:
|
|
|
|
|
# Building demolished in transit - put the cargo back rather
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
# than quietly destroying it.
|
2026-08-15 04:45:15 +02:00
|
|
|
bank_inventory.deposit(resource_type, int(t.carrying))
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
t.carrying = 0
|
|
|
|
|
t.target_id = 0
|
|
|
|
|
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
2026-08-13 16:38:41 +02:00
|
|
|
TRANSPORTER_TO_EXTRACTOR:
|
|
|
|
|
var extractor_index: int = int(t.get("extractor_index", -1))
|
|
|
|
|
if extractor_index < 0 or extractor_index >= extractors.size():
|
|
|
|
|
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
|
|
|
|
return
|
|
|
|
|
var extractor: Dictionary = extractors[extractor_index]
|
|
|
|
|
var load: int = transporter_load_limit()
|
2026-08-14 20:32:58 +02:00
|
|
|
var resource_id: String = String(extractor.get("resource_type", RESOURCE_ORE))
|
|
|
|
|
var extractor_inventory: RefCounted = _extractor_inventory(extractor)
|
|
|
|
|
if extractor_inventory.amount(resource_id) < load:
|
2026-08-13 16:38:41 +02:00
|
|
|
t.extractor_index = -1
|
|
|
|
|
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
|
|
|
|
return
|
2026-08-14 20:32:58 +02:00
|
|
|
extractor_inventory.withdraw(resource_id, load)
|
2026-08-13 16:38:41 +02:00
|
|
|
t.carrying = load
|
2026-08-14 20:32:58 +02:00
|
|
|
t.resource_type = resource_id
|
2026-08-13 16:38:41 +02:00
|
|
|
_begin_leg(t, here, get_mothership_position(), TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR)
|
|
|
|
|
TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR:
|
2026-08-13 21:07:35 +02:00
|
|
|
# Bank only on arrival; cargo retains the extractor's resource type.
|
2026-08-14 20:32:58 +02:00
|
|
|
bank_inventory.deposit(String(t.get("resource_type", RESOURCE_ORE)), int(t.carrying))
|
2026-08-13 16:38:41 +02:00
|
|
|
t.carrying = 0
|
|
|
|
|
t.extractor_index = -1
|
|
|
|
|
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
TRANSPORTER_TO_BUILDING_PICKUP:
|
|
|
|
|
var source: Dictionary = find_building(int(t.target_id))
|
|
|
|
|
var resource_type: String = String(t.get("resource_type", RESOURCE_ENERGY))
|
|
|
|
|
var take: int = 0 if source.is_empty() \
|
|
|
|
|
else mini(building_resource(int(t.target_id), resource_type), transporter_load_limit())
|
|
|
|
|
if take <= 0 or source.is_empty():
|
|
|
|
|
t.target_id = 0
|
|
|
|
|
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
|
|
|
|
return
|
|
|
|
|
_inventory_for_record(source).withdraw(resource_type, take)
|
|
|
|
|
t.carrying = take
|
|
|
|
|
_begin_leg(t, here, get_mothership_position(), TRANSPORTER_TO_MOTHERSHIP_FROM_BUILDING)
|
|
|
|
|
TRANSPORTER_TO_MOTHERSHIP_FROM_BUILDING:
|
|
|
|
|
bank_inventory.deposit(String(t.get("resource_type", RESOURCE_ENERGY)), int(t.carrying))
|
|
|
|
|
t.carrying = 0
|
|
|
|
|
t.target_id = 0
|
|
|
|
|
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
Transporter vessels, an explicit store button, and bottom-anchored menus
Forgejo #19, part 6 of #12. Transporters are built at the ship crafter,
fly themselves to the nearest hub and wait there. When a building is
short of what its queue needs, the nearest idle transporter runs
hub -> mothership -> building -> hub. Ore leaves the bank when it is
loaded and lands in the building's stock on arrival, so what is drawn
and what happens are the same event - the extractor hauler's rule.
Production orders are now TYPED (interceptor / transporter) rather than
a bare count, since the crafter builds two different things. One at a
time is dispatched per building: without that, every idle transporter
chases the same shortfall and they all arrive carrying ore nobody needs.
Three fixes from play-testing feedback:
- Storing ore into a building is now a deliberate button press, not a
side effect of opening the menu.
- The mothership's load-ore controls were positioned from the TOP of the
screen, so on any viewport shorter than ~1300px the label fell off the
bottom. They are anchored to the bottom edge now, and the label had no
explicit font size so it inherited the theme's 78px and wrapped off
screen. Checked at 720x960 and 720x1280.
- The craft button looked live and did nothing. It was not the transport
work: BASE_FLEET_SIZE is 5 and a new game starts with exactly 5 ships,
so the fleet is at its cap from the first frame and every interceptor
order was refused. The button is now visibly disabled and says why.
The underlying balance (crafter useless until Wing Doctrine) is left
alone - that is a design call, not a bug to quietly change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:14:27 +02:00
|
|
|
TRANSPORTER_RETURNING:
|
|
|
|
|
t.state = TRANSPORTER_IDLE
|
|
|
|
|
t.progress = 1.0
|
|
|
|
|
|
|
|
|
|
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
func can_afford(cost: int) -> bool:
|
2026-08-14 20:32:58 +02:00
|
|
|
return bank_inventory.amount(RESOURCE_ORE) >= cost
|
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-15 04:45:15 +02:00
|
|
|
func can_afford_resources(costs: Dictionary) -> bool:
|
|
|
|
|
for resource_type in costs:
|
|
|
|
|
if bank_inventory.amount(String(resource_type)) < int(costs[resource_type]):
|
|
|
|
|
return false
|
|
|
|
|
return 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
|
|
|
func spend(cost: int) -> bool:
|
|
|
|
|
if not can_afford(cost):
|
|
|
|
|
return false
|
2026-08-14 20:32:58 +02:00
|
|
|
bank_inventory.withdraw(RESOURCE_ORE, cost)
|
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 true
|