Provision transporters with hubs

This commit is contained in:
Loïc Wisniewski 2026-08-15 00:09:10 +02:00
commit d0a9f5b971
21 changed files with 309 additions and 588 deletions

View file

@ -230,7 +230,7 @@ const SKILL_TREE: Array = [
{
"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.",
"desc": "Build transport hubs. Each hub includes its own automatic transporters.",
},
{
"id": "vessel_science", "name": "Vessel Science", "cost": 35, "requires": "shipyards",
@ -313,6 +313,8 @@ func unlock(id: String) -> bool:
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
@ -381,6 +383,9 @@ func add_building(type: String, at_position: Vector2) -> int:
"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
@ -400,9 +405,9 @@ func building_has_capability(building: Dictionary, capability: String) -> bool:
# --- 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.
# 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_TRANSPORTER: String = "transporter"
@ -410,7 +415,6 @@ 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
@ -436,9 +440,6 @@ 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)
@ -477,6 +478,11 @@ func vessel_has_capability(vessel: String, capability: String) -> bool:
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:
@ -595,7 +601,6 @@ func transporter_speed() -> float:
const PRODUCT_SHIP: String = "interceptor"
const PRODUCT_TRANSPORTER: String = "transporter"
func product_cost(product: String) -> int:
@ -691,14 +696,8 @@ func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool:
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).
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
@ -725,12 +724,6 @@ func _advance_production(delta: float) -> void:
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
@ -739,20 +732,15 @@ func _advance_production(delta: float) -> void:
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)
# 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:
# 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})
vessel_deliveries.append({"type": product, "from": b.position,
"to": mothership, "progress": 0.0})
order_completed.emit(int(b.id), product)
@ -838,13 +826,21 @@ func has_building_of_type(type: String) -> bool:
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
@ -1017,11 +1013,8 @@ func _advance_vessel_deliveries(delta: float) -> void:
#
# 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.
# 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"
@ -1050,61 +1043,30 @@ func hub_count() -> int:
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
func _add_hub_transporter(hub: Dictionary) -> void:
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,
"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))
@ -1191,6 +1153,7 @@ func _dispatch_transporters() -> void:
func _advance_transporters(delta: float) -> void:
_sync_hub_transporters()
if transporters.is_empty():
return
_dispatch_transporters()