diff --git a/BuildController.gd b/BuildController.gd index 6be8193..a134678 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -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() diff --git a/GameState.gd b/GameState.gd index 98ffa8b..8728c91 100644 --- a/GameState.gd +++ b/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() diff --git a/tests/test_hub_capacity.gd b/tests/test_hub_capacity.gd new file mode 100644 index 0000000..46bae11 --- /dev/null +++ b/tests/test_hub_capacity.gd @@ -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