Add timed craft queue and hangar

This commit is contained in:
Loïc Wisniewski 2026-08-12 22:47:46 +02:00
commit 82552464d2
14 changed files with 660 additions and 191 deletions

View file

@ -294,12 +294,15 @@ func reset_progression() -> void:
# on the same spot.
var next_building_id: int = 1
# Vessels that have been paid for and built but not yet handed to the fleet,
# as a list of VESSEL_* types - a shield vessel finishing while the player is
# on a planet must not come back as an interceptor. Production is simulated
# here, in the autoload, the same way extractor haulers are, so an order can
# complete where there is no Fleet node to hand a ship to.
var vessels_owed: Array = []
# 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 = []
signal order_completed(building_id: int, type: String)
@ -337,6 +340,24 @@ const VESSEL_NAMES: Dictionary = {
const SHIELD_COST: int = 14
# How long one vessel takes to build, once its ore is actually in the
# building. A queue you can watch and cancel needs time to exist in.
const BUILD_TIMES: Dictionary = {
VESSEL_INTERCEPTOR: 10.0,
VESSEL_SHIELD: 16.0,
VESSEL_TRANSPORTER: 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:
return float(BUILD_TIMES.get(product, 10.0)) * build_time_scale
# Researched at the ship lab, per vessel type. Separate from SKILL_TREE on
# purpose: that gates what the player may build at all and is a strategic
# spine, while these are repeatable tuning on a vessel you already have.
@ -553,11 +574,44 @@ func pending_orders_of(id: int, product: String) -> int:
return 0
var n: int = 0
for o in b.get("orders", []):
if o == product:
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):
b.stock = int(b.get("stock", 0)) + 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)
@ -565,7 +619,9 @@ func outstanding_cost(id: int) -> int:
return 0
var need: int = 0
for o in b.get("orders", []):
need += product_cost(o)
# 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 - int(b.get("stock", 0)), 0)
@ -592,40 +648,69 @@ func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool:
if product == PRODUCT_TRANSPORTER and not can_station_transporter():
# Every berth is taken or spoken for (#20).
return false
(b.orders as Array).append(product)
(b.orders as Array).append({"product": product, "elapsed": 0.0, "started": false})
return true
func _advance_production() -> void:
func _advance_production(delta: float) -> void:
for b in buildings:
if not can_produce(b.type):
continue
# `while`, not `if`: a delivery big enough to cover several queued
# orders should clear them all rather than dribbling one out per
# frame.
var orders: Array = b.get("orders", [])
while not orders.is_empty():
var product: String = String(orders[0])
var cost: int = product_cost(product)
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)
if not bool(order.started):
if int(b.get("stock", 0)) < cost:
break
continue
if product == PRODUCT_TRANSPORTER and transporters.size() >= transport_capacity():
# A hub was demolished after this was ordered, so the berth it
# was going to use is gone. Hold the order rather than
# overfilling what is left.
break
# was going to use is gone. Hold rather than overfill.
continue
if product == PRODUCT_TRANSPORTER and nearest_hub_to(b.position).is_empty():
# Every hub was demolished while this sat in the queue. Leave
# it queued and the stock untouched rather than producing a
# vessel with nowhere to live.
break
continue
b.stock = int(b.stock) - cost
orders.pop_front()
if product == PRODUCT_TRANSPORTER:
_station_transporter(b.position)
else:
vessels_owed.append(product)
order_completed.emit(int(b.id), product)
order.started = true
order.elapsed = float(order.elapsed) + delta
if float(order.elapsed) < build_time(product):
continue
orders.pop_front()
if product == PRODUCT_TRANSPORTER:
# Transporters are unchanged: they take themselves to the nearest
# hub and wait for work (#19).
_station_transporter(b.position)
else:
# Everything else is delivered to the mothership and waits there
# until the player crews it (#23).
stored_vessels.append(product)
order_completed.emit(int(b.id), product)
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:
@ -756,7 +841,7 @@ func _process(delta: float) -> void:
# Runs in the autoload, so extraction keeps earning - and queued orders
# keep completing - while the player is on a planet's surface or nowhere
# near either.
_advance_production()
_advance_production(delta)
_advance_transporters(delta)
if extractors.is_empty():
return
@ -827,7 +912,7 @@ func pending_transporter_orders() -> int:
var n: int = 0
for b in buildings:
for o in b.get("orders", []):
if o == PRODUCT_TRANSPORTER:
if String(o.product) == PRODUCT_TRANSPORTER:
n += 1
return n