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 queued: int = GameState.pending_orders(craft_building_id)
|
||||||
var stock: int = GameState.building_stock(craft_building_id)
|
var stock: int = GameState.building_stock(craft_building_id)
|
||||||
var queue_text: String = "" if queued == 0 else " Queued %d" % queued
|
var queue_text: String = "" if queued == 0 else " Queued %d" % queued
|
||||||
craft_status.text = "Interceptor %d Transporter %d Here %d%s" % [
|
craft_status.text = "Interceptor %d Transporter %d Here %d%s Transporters %d/%d" % [
|
||||||
GameState.SHIP_COST, GameState.TRANSPORTER_COST, stock, queue_text
|
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
|
# Nowhere to station one means the order would be refused, so say so
|
||||||
# rather than letting the button look available and do nothing.
|
# 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_transporter_button.queue_redraw()
|
||||||
|
|
||||||
craft_store_label.text = "Store ore here\nHold %d" % GameState.fleet_cargo
|
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:
|
if craft_building_id == 0:
|
||||||
return
|
return
|
||||||
if not GameState.queue_order(craft_building_id, GameState.PRODUCT_TRANSPORTER):
|
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
|
return
|
||||||
_refresh_craft_status()
|
_refresh_craft_status()
|
||||||
|
|
||||||
|
|
|
||||||
60
GameState.gd
60
GameState.gd
|
|
@ -145,6 +145,11 @@ const SKILL_TREE: Array = [
|
||||||
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB,
|
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB,
|
||||||
"desc": "Build transport hubs, which supply your other buildings with ore.",
|
"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",
|
"id": "wing", "name": "Wing Doctrine", "cost": 30, "requires": "coordination",
|
||||||
"x": 0.82, "fleet_bonus": 1,
|
"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
|
# Nowhere to station it. Refused rather than queued, so the player is
|
||||||
# told now instead of watching an order sit forever.
|
# told now instead of watching an order sit forever.
|
||||||
return false
|
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)
|
(b.orders as Array).append(product)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
@ -374,6 +382,11 @@ func _advance_production() -> void:
|
||||||
var cost: int = product_cost(product)
|
var cost: int = product_cost(product)
|
||||||
if int(b.get("stock", 0)) < cost:
|
if int(b.get("stock", 0)) < cost:
|
||||||
break
|
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():
|
if product == PRODUCT_TRANSPORTER and nearest_hub_to(b.position).is_empty():
|
||||||
# Every hub was demolished while this sat in the queue. Leave
|
# Every hub was demolished while this sat in the queue. Leave
|
||||||
# it queued and the stock untouched rather than producing a
|
# it queued and the stock untouched rather than producing a
|
||||||
|
|
@ -548,6 +561,11 @@ func _process(delta: float) -> void:
|
||||||
# "from", "to"}.
|
# "from", "to"}.
|
||||||
const TRANSPORTER_SPEED: float = 340.0
|
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_IDLE: String = "idle"
|
||||||
const TRANSPORTER_TO_MOTHERSHIP: String = "to_mothership"
|
const TRANSPORTER_TO_MOTHERSHIP: String = "to_mothership"
|
||||||
const TRANSPORTER_TO_BUILDING: String = "to_building"
|
const TRANSPORTER_TO_BUILDING: String = "to_building"
|
||||||
|
|
@ -556,6 +574,48 @@ const TRANSPORTER_RETURNING: String = "returning"
|
||||||
var transporters: Array = []
|
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:
|
func has_transport_hub() -> bool:
|
||||||
return not nearest_hub_to(Vector2.ZERO).is_empty()
|
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