spaceship/GameState.gd

693 lines
22 KiB
GDScript3
Raw Normal View History

extends Node
# 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
var return_world_position: Vector2 = Vector2.ZERO
var return_camera_zoom: float = 1.0
var planet_name: String = ""
var planet_type: String = ""
# 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 -------------------------------------------------------------
# Ore that has been BANKED at the mothership. This is the only pool anything
# can be bought with.
# Starts stocked for play-testing so buildings can be tried without mining
# first - drop this back to 0 for a real playthrough.
var rocks: int = 500
# Ore the fleet is physically carrying and has not delivered yet (#14). Mining
# fills this, not `rocks`: ore is cargo until the fleet brings it home, so a
# long mining run is a real trip with something to lose rather than instant
# income from anywhere on the map.
#
# Lives here, like everything else that has to survive Main.tscn being torn
# down and rebuilt on every trip to a planet's surface - and cargo mined ON a
# surface would otherwise be dropped the moment the player took off.
var fleet_cargo: int = 0
signal cargo_delivered(amount: int)
func collect_cargo(amount: int = 1) -> void:
fleet_cargo += amount
# One press of "load ore" at the mothership. A batch rather than the whole
# bank: 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.
const CARGO_LOAD_BATCH: int = 25
func withdraw_to_fleet(amount: int = CARGO_LOAD_BATCH) -> int:
# Banked ore back into the hold, so the fleet can carry it to a building
# that is waiting on it (#17).
var taken: int = mini(amount, rocks)
if taken <= 0:
return 0
rocks -= taken
fleet_cargo += taken
return taken
func unload_into_building(id: int) -> int:
# Everything the fleet is carrying, into this building's stock. Returns
# how much moved so a caller can tell the player something happened.
if fleet_cargo <= 0:
return 0
var moved: int = fleet_cargo
if deposit_to_building(id, moved) <= 0:
return 0
fleet_cargo = 0
return moved
func deliver_cargo() -> int:
# Everything the fleet is carrying, banked at once. Returns how much moved
# so a caller can react to a delivery actually having happened rather than
# to merely being in range.
if fleet_cargo <= 0:
return 0
var delivered: int = fleet_cargo
rocks += delivered
fleet_cargo = 0
cargo_delivered.emit(delivered)
return delivered
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"
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
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",
"x": 0.82, "unlocks": BUILDING_COMMAND_CENTER,
"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,
"desc": "Build transport hubs, which supply your other buildings with ore.",
},
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",
"x": 0.65, "fleet_bonus": 1,
"desc": "Permanent: +1 vessel in the fleet.",
},
{
"id": "armada", "name": "Armada Doctrine", "cost": 150, "requires": "squadron",
"x": 0.3, "fleet_bonus": 1,
"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)
return true
func is_building_unlocked(type: String) -> bool:
# The mothership is always buildable: it is where research happens, so
# locking it would leave a player with no mothership unable to ever get
# one.
if type == BUILDING_MOTHERSHIP:
return true
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()
# --- 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
# Ships that have been paid for and built but not yet handed to the fleet.
# Production is simulated here, in the autoload, the same way extractor
# haulers are - so an order can complete while the player is down on a
# planet's surface, where there is no Fleet node to hand a ship to. The fleet
# claims them when it next can.
var ships_owed: int = 0
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({
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
"id": id, "type": type, "position": at_position, "stock": 0, "orders": [],
})
return id
func find_building(id: int) -> Dictionary:
for b in buildings:
if b.get("id", 0) == id:
return b
return {}
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"
const PRODUCT_TRANSPORTER: String = "transporter"
const TRANSPORTER_COST: int = 10
func product_cost(product: String) -> int:
if product == PRODUCT_TRANSPORTER:
return TRANSPORTER_COST
return SHIP_COST
func can_produce(type: String) -> bool:
# Only the ship crafter builds anything. The hub STATIONS transporters,
# it does not make them (#12).
return type == BUILDING_SHIP_CRAFTER
func building_stock(id: int) -> int:
var b: Dictionary = find_building(id)
return 0 if b.is_empty() else int(b.get("stock", 0))
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", []):
if o == product:
n += 1
return n
func outstanding_cost(id: int) -> int:
# 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", []):
need += product_cost(o)
return maxi(need - int(b.get("stock", 0)), 0)
func deposit_to_building(id: int, amount: int) -> int:
if amount <= 0:
return 0
var b: Dictionary = find_building(id)
if b.is_empty():
return 0
b.stock = int(b.get("stock", 0)) + amount
return amount
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:
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):
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
if product == PRODUCT_TRANSPORTER and not has_transport_hub():
# Nowhere to station it. Refused rather than queued, so the player is
# told now instead of watching an order sit forever.
return false
(b.orders as Array).append(product)
return true
func _advance_production() -> void:
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):
continue
# `while`, not `if`: a delivery big enough to cover several queued
# orders should clear them all rather than dribbling one out per
# frame.
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", [])
while not orders.is_empty():
var product: String = String(orders[0])
var cost: int = product_cost(product)
if int(b.get("stock", 0)) < cost:
break
if product == PRODUCT_TRANSPORTER and nearest_hub_to(b.position).is_empty():
# Every hub was demolished while this sat in the queue. Leave
# it queued and the stock untouched rather than producing a
# vessel with nowhere to live.
break
b.stock = int(b.stock) - cost
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
orders.pop_front()
if product == PRODUCT_TRANSPORTER:
_station_transporter(b.position)
else:
ships_owed += 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
order_completed.emit(int(b.id), product)
func has_mothership() -> bool:
return get_mothership_position() != null
func remove_buildings_of_type(type: String) -> int:
var kept: Array = []
var removed: int = 0
for b in buildings:
if b.type == type:
removed += 1
else:
kept.append(b)
buildings = kept
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:
if b.type == BUILDING_MOTHERSHIP:
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.
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),
]
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)
# --- Extractors and their haulers -----------------------------------------
# An extractor sits on a planet and runs a single automated hauler back and
# forth to the mothership, delivering ore on each arrival.
#
# The round trip is SIMULATED HERE, in the autoload, rather than by any
# node: it advances whether the player is in space, on a planet's surface,
# or far enough away that nothing about the route is loaded. `progress` is
# a fraction of one full there-and-back cycle, so a hauler's drawn position
# is derived from the same number that decides when ore is delivered - the
# visual arrival and the payout cannot drift apart, because they are the
# same event.
#
# Entries are {"position": Vector2 (the planet's position in SPACE),
# "planet_name": String, "progress": float}.
const HAULER_SPEED: float = 260.0
const HAULER_YIELD: int = 4
# Progress value at which the hauler reaches the mothership: exactly half
# way through the cycle, the outbound leg being the first half.
const HAULER_ARRIVAL: float = 0.5
var extractors: Array = []
func has_extractor_at(planet_name: String) -> bool:
for e in extractors:
if e.planet_name == planet_name:
return true
return false
signal extractor_added(planet_name: String)
func add_extractor(planet_name: String, at_position: Vector2) -> void:
extractors.append({
"position": at_position,
"planet_name": planet_name,
"progress": 0.0,
})
# 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.
extractor_added.emit(planet_name)
func get_hauler_route_time(extractor: Dictionary) -> float:
# Full cycle: out to the mothership and back again.
var mothership: Variant = get_mothership_position()
if mothership == null:
return 0.0
var distance: float = extractor.position.distance_to(mothership)
if distance < 1.0:
return 0.0
return (distance * 2.0) / HAULER_SPEED
func get_hauler_position(extractor: Dictionary) -> Variant:
# Where the hauler is right now, derived purely from `progress`. Returns
# null when there is no route (no mothership built yet).
var mothership: Variant = get_mothership_position()
if mothership == null:
return null
var t: float = extractor.progress
if t <= HAULER_ARRIVAL:
return extractor.position.lerp(mothership, t / HAULER_ARRIVAL)
return mothership.lerp(extractor.position, (t - HAULER_ARRIVAL) / (1.0 - HAULER_ARRIVAL))
func _process(delta: float) -> void:
# Runs in the autoload, so extraction keeps earning - and queued orders
# keep completing - while the player is on a planet's surface or nowhere
# near either.
_advance_production()
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
_advance_transporters(delta)
if extractors.is_empty():
return
for e in extractors:
var cycle: float = get_hauler_route_time(e)
if cycle <= 0.0:
continue # nothing to deliver to yet
var before: float = e.progress
e.progress += delta / cycle
# Ore lands the instant the hauler reaches the mothership - the same
# instant get_hauler_position() puts it there.
if before < HAULER_ARRIVAL and e.progress >= HAULER_ARRIVAL:
rocks += HAULER_YIELD
while e.progress >= 1.0:
e.progress -= 1.0
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"}.
const TRANSPORTER_SPEED: float = 340.0
const TRANSPORTER_IDLE: String = "idle"
const TRANSPORTER_TO_MOTHERSHIP: String = "to_mothership"
const TRANSPORTER_TO_BUILDING: String = "to_building"
const TRANSPORTER_RETURNING: String = "returning"
var transporters: Array = []
func has_transport_hub() -> bool:
return not nearest_hub_to(Vector2.ZERO).is_empty()
func nearest_hub_to(at: Vector2) -> Dictionary:
var best: Dictionary = {}
var best_d: float = INF
for b in buildings:
if b.type != BUILDING_TRANSPORT_HUB:
continue
var d: float = at.distance_squared_to(b.position)
if d < best_d:
best_d = d
best = b
return best
func _station_transporter(built_at: Vector2) -> void:
# Built at the crafter, then it flies itself to the nearest hub and waits
# there for orders (#12).
var hub: Dictionary = nearest_hub_to(built_at)
if hub.is_empty():
return
transporters.append({
"hub_id": int(hub.id), "state": TRANSPORTER_RETURNING, "target_id": 0,
"carrying": 0, "progress": 0.0, "from": built_at, "to": hub.position,
})
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:
for t in transporters:
if t.state != TRANSPORTER_IDLE and int(t.target_id) == building_id:
return true
return false
func _dispatch_transporters() -> void:
var mothership: Variant = get_mothership_position()
if mothership == null:
return # nowhere to load from
for b in buildings:
if outstanding_cost(int(b.id)) <= 0:
continue
# One at a time per building: without this every idle transporter
# would be sent after the same shortfall and they would all arrive
# carrying ore nobody needs any more.
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)
_begin_leg(chosen, transporter_position(chosen), mothership, TRANSPORTER_TO_MOTHERSHIP)
func _advance_transporters(delta: float) -> void:
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)
var leg: float = maxf(distance / TRANSPORTER_SPEED, 0.0001)
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))
var want: int = outstanding_cost(int(t.target_id))
var take: int = mini(want, rocks)
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
rocks -= take
t.carrying = take
_begin_leg(t, here, target.position, TRANSPORTER_TO_BUILDING)
TRANSPORTER_TO_BUILDING:
# The ore moves at exactly the moment the vessel arrives.
if deposit_to_building(int(t.target_id), int(t.carrying)) <= 0:
# Building demolished in transit - put the ore back rather
# than quietly destroying it.
rocks += 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_RETURNING:
t.state = TRANSPORTER_IDLE
t.progress = 1.0
func can_afford(cost: int) -> bool:
return rocks >= cost
func spend(cost: int) -> bool:
if not can_afford(cost):
return false
rocks -= cost
return true