1261 lines
43 KiB
GDScript
1261 lines
43 KiB
GDScript
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 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_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)
|
|
|
|
func collect_cargo(amount: int = 1) -> void:
|
|
fleet_inventory.deposit(RESOURCE_ORE, amount)
|
|
|
|
func collect_gas_cargo(amount: int = 1) -> void:
|
|
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)
|
|
|
|
|
|
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"
|
|
|
|
const COMMAND_CENTER_COST: int = 15
|
|
const EXTRACTOR_COST: int = 25
|
|
const TRANSPORT_HUB_COST: int = 30
|
|
const SHIP_LAB_COST: int = 35
|
|
|
|
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, which supply your other buildings with ore.",
|
|
},
|
|
{
|
|
"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": "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)
|
|
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()
|
|
|
|
|
|
# --- 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": [],
|
|
})
|
|
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) ---------------------------------
|
|
|
|
# The three things that can be flying around. The first and last double as
|
|
# production order ids, so a vessel type and the order that builds it are the
|
|
# same string rather than two that have to be kept in step.
|
|
const VESSEL_INTERCEPTOR: String = "interceptor"
|
|
const VESSEL_SHIELD: String = "shield"
|
|
const VESSEL_TRANSPORTER: String = "transporter"
|
|
|
|
# Compatibility constants for callers and old tests. Runtime metadata is
|
|
# resolved from VesselDefinition below.
|
|
const SHIELD_COST: int = 14
|
|
const TRANSPORTER_COST: int = 10
|
|
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
|
|
const TRANSPORTER_BASE_LOAD: int = 20
|
|
const TRANSPORTER_BASE_SPEED: float = 150.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_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
|
|
if not spend(definition.unlock_cost):
|
|
return false
|
|
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")
|
|
|
|
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"
|
|
const PRODUCT_TRANSPORTER: String = "transporter"
|
|
|
|
|
|
func product_cost(product: String) -> int:
|
|
var definition: Resource = vessel_definition(product)
|
|
return definition.build_cost if definition != null else 0
|
|
|
|
|
|
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):
|
|
_inventory_for_record(b).deposit(RESOURCE_ORE, product_cost(String(order.product)))
|
|
orders.remove_at(index)
|
|
return true
|
|
|
|
|
|
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", []):
|
|
# A started build has already paid, so it is not still owed.
|
|
if not bool(o.started):
|
|
need += product_cost(String(o.product))
|
|
return maxi(need - _inventory_for_record(b).amount(RESOURCE_ORE), 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
|
|
return _inventory_for_record(b).deposit(RESOURCE_ORE, 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 vessel_is_unlocked(product):
|
|
return false
|
|
if definition.has_capability("transport") 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
|
|
if definition.has_capability("transport") and not can_station_transporter():
|
|
# Every berth is taken or spoken for (#20).
|
|
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 cost: int = product_cost(product)
|
|
var inventory: RefCounted = _inventory_for_record(b)
|
|
var definition: Resource = vessel_definition(product)
|
|
|
|
if not bool(order.started):
|
|
if inventory.amount(RESOURCE_ORE) < cost:
|
|
continue
|
|
if definition.has_capability("transport") and transporters.size() >= transport_capacity():
|
|
# A hub was demolished after this was ordered, so the berth it
|
|
# was going to use is gone. Hold rather than overfill.
|
|
continue
|
|
if definition.has_capability("transport") and nearest_hub_to(b.position).is_empty():
|
|
continue
|
|
inventory.withdraw(RESOURCE_ORE, cost)
|
|
order.started = true
|
|
|
|
order.elapsed = float(order.elapsed) + delta
|
|
if float(order.elapsed) < build_time(product):
|
|
continue
|
|
|
|
orders.pop_front()
|
|
if definition.has_capability("transport"):
|
|
# Transporters are unchanged: they take themselves to the nearest
|
|
# hub and wait for work (#19).
|
|
_station_transporter(b.position)
|
|
else:
|
|
# A finished hull launches from the crafter and physically flies to
|
|
# the mothership hangar. With no mothership there is nowhere to fly,
|
|
# so preserve the old safe fallback of holding it immediately.
|
|
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 _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 cost: int = unlock_definition.unlock_cost if unlock_definition != null else upgrade_cost(id)
|
|
var inventory: RefCounted = _inventory_for_record(lab)
|
|
if not bool(order.started):
|
|
if inventory.amount(RESOURCE_ORE) < cost:
|
|
continue
|
|
inventory.withdraw(RESOURCE_ORE, cost)
|
|
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: 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 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
|
|
|
|
|
|
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)
|
|
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"}.
|
|
const TRANSPORTER_SPEED: float = 340.0
|
|
|
|
# How many transporters one hub can station. Raised for EVERY hub - built
|
|
# already or not - by research, which is the same shape as max_fleet_size()
|
|
# rather than a second mechanism doing the same job.
|
|
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"
|
|
|
|
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 transport_capacity() -> int:
|
|
# Total berths: more hubs, or more capacity per hub. Either raises it,
|
|
# which is exactly the choice #12 asks the player to make.
|
|
return hub_count() * hub_capacity()
|
|
|
|
|
|
func pending_transporter_orders() -> int:
|
|
var n: int = 0
|
|
for b in buildings:
|
|
for o in b.get("orders", []):
|
|
if vessel_has_capability(String(o.product), "transport"):
|
|
n += 1
|
|
return n
|
|
|
|
|
|
func transporters_claimed() -> int:
|
|
# Built plus already ordered: an order in the queue has a berth reserved,
|
|
# or a player could queue ten of them against two berths and only find out
|
|
# when they refused to appear.
|
|
return transporters.size() + pending_transporter_orders()
|
|
|
|
|
|
func can_station_transporter() -> bool:
|
|
return transporters_claimed() < transport_capacity()
|
|
|
|
|
|
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 not building_has_capability(b, "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, "resource_type": "ore", "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 _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
|
|
# 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)
|
|
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(mini(want, bank_inventory.amount(RESOURCE_ORE)), 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_ORE, 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.
|
|
bank_inventory.deposit(RESOURCE_ORE, 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_RETURNING:
|
|
t.state = TRANSPORTER_IDLE
|
|
t.progress = 1.0
|
|
|
|
|
|
func can_afford(cost: int) -> bool:
|
|
return bank_inventory.amount(RESOURCE_ORE) >= cost
|
|
|
|
|
|
func spend(cost: int) -> bool:
|
|
if not can_afford(cost):
|
|
return false
|
|
bank_inventory.withdraw(RESOURCE_ORE, cost)
|
|
return true
|