extends Node const INVENTORY = preload("res://Inventory.gd") const RESOURCE_DEFINITIONS = preload("res://ResourceDefinitions.gd") const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd") const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") # 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 surviving_vessel_types: PackedStringArray = PackedStringArray() var return_world_position: Vector2 = Vector2.ZERO var return_camera_zoom: float = 1.0 var planet_name: String = "" var planet_id: String = "" var planet_definition_id: String = "rocky" # 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 ------------------------------------------------------------- const RESOURCE_ORE: String = RESOURCE_DEFINITIONS.ORE_ID const RESOURCE_GAS: String = RESOURCE_DEFINITIONS.GAS_ID const RESOURCE_ENERGY: String = RESOURCE_DEFINITIONS.ENERGY_ID const RESOURCE_DARK_MATTER: String = RESOURCE_DEFINITIONS.DARK_MATTER_ID 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) # Both return the amount actually accepted (#56: the fleet hold now has a # finite total_capacity), so a collector can tell a full hold apart from a # successful pickup instead of assuming everything offered was taken. func collect_cargo(amount: int = 1) -> int: return fleet_inventory.deposit(RESOURCE_ORE, amount) func collect_gas_cargo(amount: int = 1) -> int: return 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) func resource_name(resource_type: String) -> String: var definition: Resource = resource_definition(resource_type) return String(definition.display_name) if definition != null else resource_type.capitalize() func resource_color(resource_type: String) -> Color: var definition: Resource = resource_definition(resource_type) return definition.color if definition != null else Color.WHITE func banked_resource(resource_type: String) -> int: return bank_inventory.amount(resource_type) func fleet_resource(resource_type: String) -> int: return fleet_inventory.amount(resource_type) # #56: total across every resource type in the hold, and the combined cap # that limits it - a HUD readout wants both, not any one resource's amount. func fleet_cargo_used() -> int: return fleet_inventory.total() func fleet_cargo_total_capacity() -> int: return fleet_inventory.total_capacity 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: var building: Dictionary = find_building(id) if building.is_empty(): return null if building_has_capability(building, "bank"): 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) func transfer_resource_to_building(id: int, resource_type: String, amount: int) -> int: return fleet_inventory.transfer_to(building_inventory(id), resource_type, amount) func transfer_resource_to_fleet(id: int, resource_type: String, amount: int) -> int: var source: RefCounted = building_inventory(id) return 0 if source == null else source.transfer_to(fleet_inventory, resource_type, amount) # 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. const CARGO_LOAD_BATCH: int = 25 # --- 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: return building_resource(id, RESOURCE_ORE) func building_gas_holdings(id: int) -> int: return building_resource(id, RESOURCE_GAS) 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. return transfer_resource_to_building(id, RESOURCE_ORE, amount) 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. return transfer_resource_to_fleet(id, RESOURCE_ORE, amount) func transfer_gas_to_building(id: int, amount: int) -> int: return transfer_resource_to_building(id, RESOURCE_GAS, amount) func transfer_gas_to_fleet(id: int, amount: int) -> int: return transfer_resource_to_fleet(id, RESOURCE_GAS, amount) 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" # 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" # Where vessel types are researched and new ones unlocked (#5). const BUILDING_SHIP_LAB: String = "ship_lab" # 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" const COMMAND_CENTER_COST: int = 15 const EXTRACTOR_COST: int = 25 const TRANSPORT_HUB_COST: int = 30 const SHIP_LAB_COST: int = 35 const GAS_BURNER_COST: int = 30 var buildings: Array = [] # --- 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.50, "unlocks": BUILDING_COMMAND_CENTER, "desc": "Build command centers, where the fleet formation is arranged.", }, { "id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics", "x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, "desc": "Build transport hubs. Each hub includes its own automatic transporters.", }, { "id": "vessel_science", "name": "Vessel Science", "cost": 35, "requires": "shipyards", "x": 0.82, "unlocks": BUILDING_SHIP_LAB, "desc": "Build ship labs, where each vessel type is upgraded and new ones unlocked.", }, { "id": "freight_yards", "name": "Freight Yards", "cost": 80, "requires": "freight", "x": 0.18, "hub_bonus": 1, "desc": "Permanent: +1 transporter stationed at every hub, including ones already built.", }, { "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.", }, { "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.50, "fleet_bonus": 1, "desc": "Permanent: +1 vessel in the fleet.", }, { "id": "armada", "name": "Armada Doctrine", "cost": 150, "requires": "squadron", "x": 0.50, "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) if int(skill_node(id).get("hub_bonus", 0)) > 0: _sync_hub_transporters() 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() vessel_levels.clear() # 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() # --- 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 # 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 = [] # 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 = [] const VESSEL_DELIVERY_SPEED: float = 180.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({ "id": id, "type": type, "position": at_position, "inventory": INVENTORY.new(), "orders": [], }) var building: Dictionary = buildings[-1] if building_has_capability(building, "transport_hub"): _provision_hub_transporters(building) return id func find_building(id: int) -> Dictionary: for b in buildings: if b.get("id", 0) == id: return b return {} 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 # --- vessel types and their upgrades (#5) --------------------------------- # 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. const VESSEL_INTERCEPTOR: String = "interceptor" const VESSEL_SHIELD: String = "shield" const VESSEL_GAS_BOMBER: String = "gas_bomber" const VESSEL_TRANSPORTER: String = "transporter" const VESSEL_CARRIER: String = "carrier" # Compatibility constants for callers and old tests. Runtime metadata is # resolved from VesselDefinition below. const SHIELD_COST: int = 14 const LAB_RESEARCH_TIME: float = 8.0 # 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: var definition: Resource = VESSEL_DEFINITIONS.get_definition(product) return (definition.build_time if definition != null else 10.0) * build_time_scale func lab_research_time() -> float: return LAB_RESEARCH_TIME * build_time_scale # Compatibility constants. VesselDefinition owns the live base figures. 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) func has_ship_lab() -> bool: for b in buildings: if building_has_capability(b, "vessel_research"): return true return false func shield_vessel_unlocked() -> bool: return vessel_is_unlocked(VESSEL_SHIELD) const UNLOCK_SHIELD_VESSEL: String = "unlock_shield" const UNLOCK_SHIELD_COST: int = 45 func unlock_shield_vessel() -> bool: 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) func vessel_is_fleet_eligible(vessel: String) -> bool: var definition: Resource = vessel_definition(vessel) return definition != null and definition.fleet_eligible func vessel_is_unlocked(vessel: String) -> bool: var definition: Resource = vessel_definition(vessel) if definition == null: return false 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 var costs: Dictionary = research_costs(definition.unlock_id) if not can_afford_resources(costs): return false for resource_type in costs: bank_inventory.withdraw(String(resource_type), int(costs[resource_type])) vessel_levels[definition.unlock_id] = 1 vessel_upgraded.emit(definition.unlock_id) return true func upgrade_def(id: String) -> Dictionary: 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} return {} func upgrades_for(vessel: String) -> Array: var out: Array = [] 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}) 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: return has_ship_lab() and not upgrade_maxed(id) and can_afford(upgrade_cost(id)) 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 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 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") func shield_max_charges() -> int: var definition: Resource = VESSEL_DEFINITIONS.with_capability("shield") return int(vessel_stat(definition.definition_id, "shield_charges")) func shield_recharge_time() -> float: var definition: Resource = VESSEL_DEFINITIONS.with_capability("shield") return vessel_stat(definition.definition_id, "shield_recharge") # #56: sums cargo_capacity over ships that actually declare the "cargo" # capability, so combat specialists (no capability, whatever their stat # dict says) contribute nothing - data-driven rather than a vessel-id list. # Takes the composition as an argument rather than reading it off a live # Fleet node: GameState has no fixed reference to "the" Fleet (space and a # planet surface each own a separate one), so the caller passes its own # Fleet.vessel_types(). func fleet_cargo_capacity(vessel_types: PackedStringArray) -> int: var total: int = 0 for vessel_type in vessel_types: if vessel_has_capability(vessel_type, "cargo"): total += int(vessel_stat(vessel_type, "cargo_capacity")) return total func sync_fleet_cargo_capacity(vessel_types: PackedStringArray) -> void: fleet_inventory.total_capacity = fleet_cargo_capacity(vessel_types) func transporter_load_limit() -> int: var definition: Resource = VESSEL_DEFINITIONS.with_capability("transport") return int(vessel_stat(definition.definition_id, "cargo_capacity")) func transporter_speed() -> float: var definition: Resource = VESSEL_DEFINITIONS.with_capability("transport") return vessel_stat(definition.definition_id, "transport_speed") const PRODUCT_SHIP: String = "interceptor" func product_cost(product: String) -> int: return int(product_costs(product).get(RESOURCE_ORE, 0)) func product_costs(product: String) -> Dictionary: var definition: Resource = vessel_definition(product) 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 {} func can_produce(type: String) -> bool: var definition: Resource = BUILDING_DEFINITIONS.get_definition(type) return definition != null and "production" in definition.capabilities func building_stock(id: int) -> int: return building_resource(id, RESOURCE_ORE) func pending_orders(id: int) -> int: var b: Dictionary = find_building(id) 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 String(o.product) == product: n += 1 return n 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): 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])) orders.remove_at(index) return true func outstanding_cost(id: int) -> int: 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: # 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", []): if not bool(o.started): need += int(product_costs(String(o.product)).get(resource_type, 0)) for order in b.get("research_orders", []): if not bool(order.started): need += int(research_costs(String(order.id)).get(resource_type, 0)) return maxi(need - _inventory_for_record(b).amount(resource_type), 0) func deposit_to_building(id: int, amount: int) -> int: return deposit_resource_to_building(id, RESOURCE_ORE, amount) func deposit_resource_to_building(id: int, resource_type: String, amount: int) -> int: if amount <= 0: return 0 var b: Dictionary = find_building(id) if b.is_empty(): return 0 return _inventory_for_record(b).deposit(resource_type, amount) func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool: var b: Dictionary = find_building(id) if b.is_empty() or not can_produce(b.type): return false var definition: Resource = vessel_definition(product) if definition == null or not definition.fleet_eligible \ or not vessel_is_unlocked(product): return false (b.orders as Array).append({"product": product, "elapsed": 0.0, "started": false}) return true func _advance_production(delta: float) -> void: for b in buildings: if not can_produce(b.type): continue var orders: Array = b.get("orders", []) 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) var costs: Dictionary = product_costs(product) var inventory: RefCounted = _inventory_for_record(b) var definition: Resource = vessel_definition(product) if not bool(order.started): var affordable := true for resource_type in costs: if inventory.amount(String(resource_type)) < int(costs[resource_type]): affordable = false break if not affordable: continue for resource_type in costs: inventory.withdraw(String(resource_type), int(costs[resource_type])) order.started = true order.elapsed = float(order.elapsed) + delta if float(order.elapsed) < build_time(product): continue orders.pop_front() # 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) else: vessel_deliveries.append({"type": product, "from": b.position, "to": mothership, "progress": 0.0}) order_completed.emit(int(b.id), product) func queue_lab_research(lab_id: int, upgrade_id: String) -> bool: var lab: Dictionary = find_building(lab_id) if not building_has_capability(lab, "vessel_research") or upgrade_id == "": return false var orders: Array = lab.get("research_orders", []) var unlock_definition: Resource = vessel_for_unlock(upgrade_id) if not orders.is_empty() or (unlock_definition == null and upgrade_maxed(upgrade_id)): 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] func research_cost(id: String) -> int: return int(research_costs(id).get(RESOURCE_ORE, 0)) func research_costs(id: String) -> Dictionary: var unlock_definition: Resource = vessel_for_unlock(id) 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 {} func _advance_lab_research(delta: float) -> void: for lab in buildings: if not building_has_capability(lab, "vessel_research"): continue var orders: Array = lab.get("research_orders", []) if orders.is_empty(): continue var order: Dictionary = orders[0] var id: String = String(order.id) var unlock_definition: Resource = vessel_for_unlock(id) var costs: Dictionary = research_costs(id) var inventory: RefCounted = _inventory_for_record(lab) if not bool(order.started): var affordable := true for resource_type in costs: if inventory.amount(String(resource_type)) < int(costs[resource_type]): affordable = false break if not affordable: continue for resource_type in costs: inventory.withdraw(String(resource_type), int(costs[resource_type])) order.started = true order.elapsed = float(order.elapsed) + delta if float(order.elapsed) < lab_research_time(): continue if unlock_definition != null: vessel_levels[id] = 1 else: vessel_levels[id] = upgrade_level(id) + 1 vessel_upgraded.emit(id) orders.pop_front() 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 func has_mothership() -> bool: return get_mothership_position() != null func has_building_of_type(type: String) -> bool: for building in buildings: if building.type == type: return true return false func remove_buildings_of_type(type: String) -> int: var kept: Array = [] var removed_ids: Array = [] var removed: int = 0 for b in buildings: if b.type == type: removed += 1 removed_ids.append(int(b.id)) else: kept.append(b) buildings = kept 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 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 building_has_capability(b, "bank"): 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. # 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), 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 ------------------------------------------------------------ # 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 # {"position", "planet_id", "planet_name", "resource_type", "inventory", "mining_progress"}. const EXTRACTOR_RATE: float = 2.0 # ore per second var extractors: Array = [] func has_extractor_at(planet_key: String) -> bool: for e in extractors: # Old saves have no planet_id; their name remains a migration fallback. if String(e.get("planet_id", e.get("planet_name", ""))) == planet_key: return true return false 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 signal extractor_added(planet_name: String) 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 extractors.append({ "position": at_position, "planet_id": stable_id, "planet_name": planet_name, "resource_type": resource_type, "inventory": INVENTORY.new(), "mining_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(stable_id) func extractor_stock(index: int) -> int: if index < 0 or index >= extractors.size(): return 0 var extractor: Dictionary = extractors[index] return _extractor_inventory(extractor).amount(String(extractor.get("resource_type", RESOURCE_ORE))) 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) 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 # --- 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 # 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) # 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} 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 func total_capture_points(planet_key: String) -> int: return capture_state(planet_key).get("captured", []).size() func _process(delta: float) -> void: # Runs in the autoload, so mining and transport continue while the player # is on a planet's surface or nowhere near either. _advance_production(delta) _advance_lab_research(delta) _advance_vessel_deliveries(delta) _advance_gas_burners(delta) for e in extractors: 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: _extractor_inventory(e).deposit(String(e.get("resource_type", RESOURCE_ORE)), whole) e.mining_progress = float(e.mining_progress) - whole _advance_transporters(delta) 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) 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) if float(delivery.progress) >= 1.0: stored_vessels.append(String(delivery.type)) vessel_deliveries.remove_at(i) # --- 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"}. # How many transporters each hub owns. Raised for EVERY hub - built already # or not - by research, and provisioned automatically rather than crafted. const BASE_HUB_CAPACITY: int = 2 const TRANSPORTER_IDLE: String = "idle" const TRANSPORTER_TO_MOTHERSHIP: String = "to_mothership" const TRANSPORTER_TO_BUILDING: String = "to_building" const TRANSPORTER_RETURNING: String = "returning" const TRANSPORTER_TO_EXTRACTOR: String = "to_extractor" const TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR: String = "extractor_to_mothership" # 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" var transporters: Array = [] 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: if building_has_capability(b, "transport_hub"): n += 1 return n 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, }) 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 func _sync_hub_transporters() -> void: for hub in buildings: if building_has_capability(hub, "transport_hub"): _provision_hub_transporters(hub) 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: # 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. for t in transporters: 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: return true return false 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 func _dispatch_transporters() -> void: var mothership: Variant = get_mothership_position() if mothership == null: return # nowhere to load from # 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. for b in buildings: var needed_resource := "" for resource_type in resource_types(): # 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: needed_resource = resource_type break if needed_resource == "": 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 resources 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) chosen.resource_type = needed_resource _begin_leg(chosen, transporter_position(chosen), mothership, TRANSPORTER_TO_MOTHERSHIP) # 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) # 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) func _advance_transporters(delta: float) -> void: _sync_hub_transporters() 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 resource_type: String = String(t.get("resource_type", RESOURCE_ORE)) 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) var take: int = mini(mini(want, bank_inventory.amount(resource_type)), transporter_load_limit()) 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 bank_inventory.withdraw(resource_type, take) t.carrying = take _begin_leg(t, here, target.position, TRANSPORTER_TO_BUILDING) TRANSPORTER_TO_BUILDING: # 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 # than quietly destroying it. bank_inventory.deposit(resource_type, 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_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() 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: t.extractor_index = -1 _begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING) return extractor_inventory.withdraw(resource_id, load) t.carrying = load t.resource_type = resource_id _begin_leg(t, here, get_mothership_position(), TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR) TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR: # Bank only on arrival; cargo retains the extractor's resource type. bank_inventory.deposit(String(t.get("resource_type", RESOURCE_ORE)), int(t.carrying)) t.carrying = 0 t.extractor_index = -1 _begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING) 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_RETURNING: t.state = TRANSPORTER_IDLE t.progress = 1.0 func can_afford(cost: int) -> bool: return bank_inventory.amount(RESOURCE_ORE) >= cost 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 func spend(cost: int) -> bool: if not can_afford(cost): return false bank_inventory.withdraw(RESOURCE_ORE, cost) return true