Add gas bomber skill-shot vessel

This commit is contained in:
Loïc Wisniewski 2026-08-15 04:45:15 +02:00
commit 1d9afcbc2e
28 changed files with 882 additions and 85 deletions

View file

@ -410,6 +410,7 @@ func building_has_capability(building: Dictionary, capability: String) -> bool:
# 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"
# Compatibility constants for callers and old tests. Runtime metadata is
@ -501,8 +502,11 @@ 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):
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
@ -604,8 +608,16 @@ 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)
return definition.build_cost if definition != null else 0
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:
@ -661,34 +673,53 @@ func cancel_order(id: int, index: int) -> bool:
return false
var order: Dictionary = orders[index]
if bool(order.started):
_inventory_for_record(b).deposit(RESOURCE_ORE, product_cost(String(order.product)))
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", []):
# A started build has already paid, so it is not still owed.
if not bool(o.started):
need += product_cost(String(o.product))
need += int(product_costs(String(o.product)).get(resource_type, 0))
for order in b.get("research_orders", []):
if not bool(order.started):
need += research_cost(String(order.id))
return maxi(need - _inventory_for_record(b).amount(RESOURCE_ORE), 0)
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_ORE, amount)
return _inventory_for_record(b).deposit(resource_type, amount)
func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool:
@ -717,14 +748,20 @@ func _advance_production(delta: float) -> void:
# 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 costs: Dictionary = product_costs(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:
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
inventory.withdraw(RESOURCE_ORE, cost)
for resource_type in costs:
inventory.withdraw(String(resource_type), int(costs[resource_type]))
order.started = true
order.elapsed = float(order.elapsed) + delta
@ -764,8 +801,17 @@ func lab_research_order(lab_id: int) -> Dictionary:
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)
return unlock_definition.unlock_cost if unlock_definition != null else upgrade_cost(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:
@ -778,12 +824,18 @@ func _advance_lab_research(delta: float) -> void:
var order: Dictionary = orders[0]
var id: String = String(order.id)
var unlock_definition: Resource = vessel_for_unlock(id)
var cost: int = research_cost(id)
var costs: Dictionary = research_costs(id)
var inventory: RefCounted = _inventory_for_record(lab)
if not bool(order.started):
if inventory.amount(RESOURCE_ORE) < cost:
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
inventory.withdraw(RESOURCE_ORE, cost)
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():
@ -1129,7 +1181,13 @@ func _dispatch_transporters() -> void:
_begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position,
TRANSPORTER_TO_EXTRACTOR)
for b in buildings:
if outstanding_cost(int(b.id)) <= 0:
var needed_resource := ""
for resource_type in resource_types():
if outstanding_resource_cost(int(b.id), resource_type) > 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
@ -1149,6 +1207,7 @@ func _dispatch_transporters() -> void:
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)
@ -1175,22 +1234,24 @@ func _arrive(t: Dictionary) -> void:
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())
var resource_type: String = String(t.get("resource_type", RESOURCE_ORE))
var want: int = outstanding_resource_cost(int(t.target_id), resource_type)
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_ORE, take)
bank_inventory.withdraw(resource_type, 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
# 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_ORE, int(t.carrying))
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)
@ -1226,6 +1287,13 @@ 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