Transporter capacity per hub, and research to raise it
Forgejo #20, part 7 of #12 and the last of it. A hub stations BASE_HUB_CAPACITY (2) transporters. Total berths are hubs x per-hub capacity, so building another hub or researching "Freight Yards" both raise it - which is the choice #12 asks the player to make. Capacity follows the same shape as max_fleet_size() (sum a bonus over `unlocked`) rather than being a second mechanism doing the same job, so research applies to hubs already built, not only to ones placed afterwards. A queued-but-unbuilt transporter holds its berth. Without that a player could order ten against two berths and only discover the problem when they refused to appear. Ordering past the cap is refused outright, and the two refusals - no hub at all, versus every berth taken - say which, because "nothing happened" is exactly the bug reported against the craft button. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d41128bc43
commit
f19807618a
3 changed files with 214 additions and 4 deletions
|
|
@ -457,12 +457,14 @@ func _refresh_craft_status() -> void:
|
|||
var queued: int = GameState.pending_orders(craft_building_id)
|
||||
var stock: int = GameState.building_stock(craft_building_id)
|
||||
var queue_text: String = "" if queued == 0 else " Queued %d" % queued
|
||||
craft_status.text = "Interceptor %d Transporter %d Here %d%s" % [
|
||||
GameState.SHIP_COST, GameState.TRANSPORTER_COST, stock, queue_text
|
||||
craft_status.text = "Interceptor %d Transporter %d Here %d%s Transporters %d/%d" % [
|
||||
GameState.SHIP_COST, GameState.TRANSPORTER_COST, stock, queue_text,
|
||||
GameState.transporters_claimed(), GameState.transport_capacity()
|
||||
]
|
||||
# Nowhere to station one means the order would be refused, so say so
|
||||
# rather than letting the button look available and do nothing.
|
||||
craft_transporter_button.enabled = GameState.has_transport_hub()
|
||||
craft_transporter_button.enabled = GameState.has_transport_hub() \
|
||||
and GameState.can_station_transporter()
|
||||
craft_transporter_button.queue_redraw()
|
||||
|
||||
craft_store_label.text = "Store ore here\nHold %d" % GameState.fleet_cargo
|
||||
|
|
@ -527,7 +529,12 @@ func _on_craft_transporter() -> void:
|
|||
if craft_building_id == 0:
|
||||
return
|
||||
if not GameState.queue_order(craft_building_id, GameState.PRODUCT_TRANSPORTER):
|
||||
craft_status.text = "Build a Transport Hub first - a transporter needs somewhere to be stationed"
|
||||
# Two different refusals, and the player needs to know WHICH.
|
||||
if not GameState.has_transport_hub():
|
||||
craft_status.text = "Build a Transport Hub first - a transporter needs somewhere to be stationed"
|
||||
else:
|
||||
craft_status.text = "All %d berths are taken - build another hub, or research Freight Yards" % \
|
||||
GameState.transport_capacity()
|
||||
return
|
||||
_refresh_craft_status()
|
||||
|
||||
|
|
|
|||
60
GameState.gd
60
GameState.gd
|
|
@ -145,6 +145,11 @@ const SKILL_TREE: Array = [
|
|||
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB,
|
||||
"desc": "Build transport hubs, which supply your other buildings with ore.",
|
||||
},
|
||||
{
|
||||
"id": "freight_yards", "name": "Freight Yards", "cost": 80, "requires": "freight",
|
||||
"x": 0.05, "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,
|
||||
|
|
@ -357,6 +362,9 @@ func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool:
|
|||
# Nowhere to station it. Refused rather than queued, so the player is
|
||||
# told now instead of watching an order sit forever.
|
||||
return false
|
||||
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)
|
||||
return true
|
||||
|
||||
|
|
@ -374,6 +382,11 @@ func _advance_production() -> void:
|
|||
var cost: int = product_cost(product)
|
||||
if int(b.get("stock", 0)) < cost:
|
||||
break
|
||||
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
|
||||
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
|
||||
|
|
@ -548,6 +561,11 @@ func _process(delta: float) -> void:
|
|||
# "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"
|
||||
|
|
@ -556,6 +574,48 @@ const TRANSPORTER_RETURNING: String = "returning"
|
|||
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 b.type == BUILDING_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 o == PRODUCT_TRANSPORTER:
|
||||
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()
|
||||
|
||||
|
|
|
|||
143
tests/test_hub_capacity.gd
Normal file
143
tests/test_hub_capacity.gd
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_hub_capacity.gd
|
||||
# Forgejo #20 (part 7 of #12): a hub stations a limited number of
|
||||
# transporters. More hubs, or research, raise it.
|
||||
#
|
||||
# Verifies:
|
||||
# 1. capacity is per hub, so a second hub raises the total
|
||||
# 2. ordering past the cap is REFUSED, not silently swallowed - the ticket
|
||||
# is explicit that the player has to be told
|
||||
# 3. a queued-but-unbuilt transporter still holds its berth, or ten could
|
||||
# be ordered against two and only fail on arrival
|
||||
# 4. researching Freight Yards raises capacity for hubs ALREADY BUILT, not
|
||||
# just ones placed afterwards
|
||||
# 5. the extra berth is immediately usable
|
||||
|
||||
var game_state: Node
|
||||
var scene: Node
|
||||
var field: Node
|
||||
var crafter_id: int = 0
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
|
||||
const CRAFTER_AT: Vector2 = Vector2(400.0, 0.0)
|
||||
const HUB_A: Vector2 = Vector2(-500.0, 0.0)
|
||||
const HUB_B: Vector2 = Vector2(-500.0, 600.0)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.transporters.clear()
|
||||
game_state.rocks = 900
|
||||
game_state.fleet_cargo = 0
|
||||
game_state.ships_owed = 0
|
||||
game_state.unlocked = ["shipyards", "logistics", "freight"]
|
||||
scene = load("res://Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
field = scene.get_node("BuildingField")
|
||||
|
||||
|
||||
func _fill_berths(n: int) -> void:
|
||||
# Queue and immediately fund n transporters.
|
||||
for i in range(n):
|
||||
if game_state.queue_order(crafter_id, GameState.PRODUCT_TRANSPORTER):
|
||||
game_state.deposit_to_building(crafter_id, GameState.TRANSPORTER_COST)
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
scene.get_node("AsteroidField").set_process(false)
|
||||
scene.get_node("EnemySpawner").set_process(false)
|
||||
field.place(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||
field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT)
|
||||
crafter_id = int(game_state.buildings[-1].id)
|
||||
field.place(GameState.BUILDING_TRANSPORT_HUB, HUB_A)
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
var per_hub: int = game_state.hub_capacity()
|
||||
if game_state.transport_capacity() != per_hub:
|
||||
print("TEST_HUB_CAPACITY: FAIL (one hub gives %d berths, per-hub capacity is %d)" % [
|
||||
game_state.transport_capacity(), per_hub
|
||||
])
|
||||
quit(1)
|
||||
return true
|
||||
print("1 hub -> %d berths (%d per hub)" % [game_state.transport_capacity(), per_hub])
|
||||
|
||||
# --- 2 + 3. fill it, then try to exceed it ------------------
|
||||
_fill_berths(per_hub)
|
||||
if game_state.transporters_claimed() != per_hub:
|
||||
print("TEST_HUB_CAPACITY: FAIL (claimed %d berths after ordering %d)" % [
|
||||
game_state.transporters_claimed(), per_hub
|
||||
])
|
||||
quit(1)
|
||||
return true
|
||||
# Still only QUEUED at this point - the berth must already count.
|
||||
if game_state.queue_order(crafter_id, GameState.PRODUCT_TRANSPORTER):
|
||||
print("TEST_HUB_CAPACITY: FAIL (queued a transporter past the %d-berth cap)" % per_hub)
|
||||
quit(1)
|
||||
return true
|
||||
print("cap enforced at %d, including orders not yet built" % per_hub)
|
||||
|
||||
# --- 1. a second hub raises the total -----------------------
|
||||
field.place(GameState.BUILDING_TRANSPORT_HUB, HUB_B)
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
2:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
if game_state.transport_capacity() != game_state.hub_capacity() * 2:
|
||||
print("TEST_HUB_CAPACITY: FAIL (2 hubs give %d berths, expected %d)" % [
|
||||
game_state.transport_capacity(), game_state.hub_capacity() * 2
|
||||
])
|
||||
quit(1)
|
||||
return true
|
||||
print("2 hubs -> %d berths" % game_state.transport_capacity())
|
||||
if not game_state.can_station_transporter():
|
||||
print("TEST_HUB_CAPACITY: FAIL (second hub did not free a berth)")
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
# Fill the second hub too, so capacity is exhausted again.
|
||||
_fill_berths(game_state.transport_capacity() - game_state.transporters_claimed())
|
||||
if game_state.can_station_transporter():
|
||||
print("TEST_HUB_CAPACITY: FAIL (still room after filling every berth)")
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
# --- 4. research raises it for hubs ALREADY built -----------
|
||||
var before: int = game_state.transport_capacity()
|
||||
game_state.unlocked.append("freight_yards")
|
||||
var after: int = game_state.transport_capacity()
|
||||
print("researched Freight Yards: %d -> %d berths across %d existing hubs" % [
|
||||
before, after, game_state.hub_count()
|
||||
])
|
||||
if after != before + game_state.hub_count():
|
||||
print("TEST_HUB_CAPACITY: FAIL (research gave %d berths, expected +1 per existing hub)" % (after - before))
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
# --- 5. and the new berth is usable straight away -----------
|
||||
if not game_state.can_station_transporter():
|
||||
print("TEST_HUB_CAPACITY: FAIL (research raised the number but left no usable berth)")
|
||||
quit(1)
|
||||
return true
|
||||
if not game_state.queue_order(crafter_id, GameState.PRODUCT_TRANSPORTER):
|
||||
print("TEST_HUB_CAPACITY: FAIL (could not order into the berth research just added)")
|
||||
quit(1)
|
||||
return true
|
||||
print("ordered into the newly researched berth")
|
||||
print("TEST_HUB_CAPACITY: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
Loading…
Reference in a new issue