diff --git a/BuildingField.gd b/BuildingField.gd index cd27afe..ae0ea7d 100644 --- a/BuildingField.gd +++ b/BuildingField.gd @@ -17,6 +17,19 @@ func _ready() -> void: rebuild_all() +func _process(_delta: float) -> void: + # Unloading the fleet's hold (#14). Done here because this is the node + # that already knows where the buildings are and already answers "is the + # fleet standing on one?" - and it uses the fleet CENTROID, the same + # stable single-number test Planet and Building already use, rather than + # per-ship collision that flickers as ships jostle in formation. + if GameState.fleet_cargo <= 0: + return + var under: Node2D = get_building_fleet_is_on() + if under != null and under.building_type == GameState.BUILDING_MOTHERSHIP: + GameState.deliver_cargo() + + func rebuild_all() -> void: for b in spawned: if is_instance_valid(b): diff --git a/GameState.gd b/GameState.gd index 28cb105..b4f6d23 100644 --- a/GameState.gd +++ b/GameState.gd @@ -22,11 +22,41 @@ var explored_chunks: Dictionary = {} # --- Resources ------------------------------------------------------------- -# Rocks mined from asteroid-planet surfaces, spent on buildings and ships. +# Ore that has been BANKED at the mothership. This is the only pool anything +# can be bought with. # Starts stocked for play-testing so buildings can be tried without mining # first - drop this back to 0 for a real playthrough. var rocks: int = 500 +# Ore the fleet is physically carrying and has not delivered yet (#14). Mining +# fills this, not `rocks`: ore is cargo until the fleet brings it home, so a +# long mining run is a real trip with something to lose rather than instant +# income from anywhere on the map. +# +# Lives here, like everything else that has to survive Main.tscn being torn +# down and rebuilt on every trip to a planet's surface - and cargo mined ON a +# surface would otherwise be dropped the moment the player took off. +var fleet_cargo: int = 0 + +signal cargo_delivered(amount: int) + + +func collect_cargo(amount: int = 1) -> void: + fleet_cargo += amount + + +func deliver_cargo() -> int: + # Everything the fleet is carrying, banked at once. Returns how much moved + # so a caller can react to a delivery actually having happened rather than + # to merely being in range. + if fleet_cargo <= 0: + return 0 + var delivered: int = fleet_cargo + rocks += delivered + fleet_cargo = 0 + cargo_delivered.emit(delivered) + return delivered + const MOTHERSHIP_COST: int = 20 const SHIP_CRAFTER_COST: int = 12 const SHIP_COST: int = 6 diff --git a/Rock.gd b/Rock.gd index bc8ffac..a5a85bd 100644 --- a/Rock.gd +++ b/Rock.gd @@ -80,7 +80,9 @@ func _nearest_ship() -> Node2D: func _on_area_entered(area: Area2D) -> void: if area.is_in_group("player_ships"): - GameState.rocks += 1 + # Into the fleet's hold, not the bank (#14). It only counts as + # spendable once the fleet has flown it back to the mothership. + GameState.collect_cargo(1) collected.emit() queue_free() diff --git a/tests/test_fleet_cargo.gd b/tests/test_fleet_cargo.gd new file mode 100644 index 0000000..3b395ec --- /dev/null +++ b/tests/test_fleet_cargo.gd @@ -0,0 +1,179 @@ +extends SceneTree + +# Headless gameplay test: run with +# godot --headless -s res://tests/test_fleet_cargo.gd +# Forgejo #14 (part 1 of #12): mined ore is CARGO the fleet is carrying, not +# instant global income. It only becomes spendable once the fleet flies it +# back to the mothership. +# +# Before this, Rock.gd did `GameState.rocks += 1` on pickup, so ore was +# banked instantly from anywhere on the map and a mining run had nothing to +# lose. +# +# Verifies: +# 1. mining fills the hold and leaves the bank alone +# 2. reaching the mothership banks the lot, and empties the hold +# 3. with no mothership there is nowhere to deliver, so cargo just +# accumulates - it is never quietly banked anyway +# 4. sitting on a DIFFERENT building is not a delivery +# 5. ore is conserved: nothing is created or destroyed in the transfer +# 6. cargo survives the trip to a planet's surface and back, which is +# exactly the trip a mining run is + +var game_state: Node +var scene: Node +var fleet: Node +var field: Node +var stage: int = 0 +var elapsed: float = 0.0 +var banked_before: int = 0 + +const MOTHERSHIP_AT: Vector2 = Vector2(1200.0, 0.0) +const CRAFTER_AT: Vector2 = Vector2(-4000.0, 0.0) + + +func _initialize() -> void: + game_state = root.get_node("GameState") + game_state.buildings.clear() + game_state.extractors.clear() + game_state.rocks = 100 + game_state.fleet_cargo = 0 + scene = load("res://Main.tscn").instantiate() + root.add_child(scene) + fleet = scene.get_node("Fleet") + field = scene.get_node("BuildingField") + + +func _park_fleet_on(at: Vector2) -> void: + for ship in fleet.ships: + ship.position = at + fleet.move_to(at) + + +func _process(delta: float) -> bool: + elapsed += delta + match stage: + 0: + scene.get_node("AsteroidField").set_process(false) + scene.get_node("EnemySpawner").set_process(false) + + # --- 1. mining fills the hold, not the bank ----------------- + # Through a REAL Rock, flown over by a real ship, rather than by + # calling collect_cargo() directly: the whole change is which + # pool Rock.gd credits on pickup, and a test that calls the new + # API by hand passes just as happily with Rock.gd left untouched. + banked_before = game_state.rocks + var rock: Area2D = load("res://Rock.tscn").instantiate() + rock.build_shape(1) + rock.global_position = fleet.ships[0].global_position + scene.add_child(rock) + elapsed = 0.0 + stage = 5 + return false + 5: + # A tick for the Area2D overlap to register. + if elapsed < 0.25: + return false + if game_state.fleet_cargo != 1: + print("TEST_FLEET_CARGO: FAIL (flying over a rock put %d in the hold)" % game_state.fleet_cargo) + quit(1) + return true + if game_state.rocks != banked_before: + print("TEST_FLEET_CARGO: FAIL (a mined rock was banked instantly: %d -> %d)" % [ + banked_before, game_state.rocks + ]) + quit(1) + return true + print("flew over a real rock -> hold=%d bank=%d (unchanged)" % [ + game_state.fleet_cargo, game_state.rocks + ]) + + game_state.collect_cargo(1) + game_state.collect_cargo(1) + if game_state.fleet_cargo != 3: + print("TEST_FLEET_CARGO: FAIL (mining did not fill the hold: %d)" % game_state.fleet_cargo) + quit(1) + return true + if game_state.rocks != banked_before: + print("TEST_FLEET_CARGO: FAIL (mining banked %d straight away)" % (game_state.rocks - banked_before)) + quit(1) + return true + print("mined 3 -> hold=%d bank=%d (unchanged)" % [game_state.fleet_cargo, game_state.rocks]) + + # --- 3. no mothership means nowhere to deliver -------------- + _park_fleet_on(Vector2.ZERO) + elapsed = 0.0 + stage = 1 + return false + 1: + if elapsed < 0.4: + return false + if game_state.fleet_cargo != 3 or game_state.rocks != banked_before: + print("TEST_FLEET_CARGO: FAIL (delivered with no mothership built)") + quit(1) + return true + print("no mothership -> hold still %d, bank still %d" % [game_state.fleet_cargo, game_state.rocks]) + + # --- 4. a different building is not a delivery point -------- + field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT) + field.place(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT) + _park_fleet_on(CRAFTER_AT) + elapsed = 0.0 + stage = 2 + return false + 2: + if elapsed < 0.4: + return false + if game_state.fleet_cargo != 3: + print("TEST_FLEET_CARGO: FAIL (the ship crafter accepted a delivery)") + quit(1) + return true + print("parked on the crafter -> hold still %d" % game_state.fleet_cargo) + + # --- 2. the mothership banks it ----------------------------- + _park_fleet_on(MOTHERSHIP_AT) + elapsed = 0.0 + stage = 3 + return false + 3: + if elapsed < 0.4: + return false + var total_before: int = banked_before + 3 + print("delivered: hold=%d bank=%d (expected hold=0 bank=%d)" % [ + game_state.fleet_cargo, game_state.rocks, total_before + ]) + if game_state.fleet_cargo != 0: + print("TEST_FLEET_CARGO: FAIL (hold not emptied on delivery)") + quit(1) + return true + # --- 5. conservation ---------------------------------------- + if game_state.rocks != total_before: + print("TEST_FLEET_CARGO: FAIL (ore was created or destroyed in the transfer)") + quit(1) + return true + + # --- 6. cargo survives the scene swap ----------------------- + # Not simulated by loading PlanetSurface.tscn here: the point is + # that cargo lives in the autoload rather than in Main.tscn, so + # tearing Main.tscn down must not touch it. + # Fly off the mothership first, or this new cargo is (correctly) + # banked the moment BuildingField next runs, and the teardown + # check below would be measuring a delivery instead. + _park_fleet_on(CRAFTER_AT) + game_state.collect_cargo(5) + scene.queue_free() + elapsed = 0.0 + stage = 4 + return false + 4: + if elapsed < 0.3: + return false + if game_state.fleet_cargo != 5: + print("TEST_FLEET_CARGO: FAIL (cargo lost when the space scene was torn down: %d)" % game_state.fleet_cargo) + quit(1) + return true + print("cargo survived the scene teardown: hold=%d" % game_state.fleet_cargo) + print("TEST_FLEET_CARGO: PASS") + quit(0) + return true + return false diff --git a/tests/test_resources_and_buildings.gd b/tests/test_resources_and_buildings.gd index a78200c..6069a1b 100644 --- a/tests/test_resources_and_buildings.gd +++ b/tests/test_resources_and_buildings.gd @@ -3,7 +3,9 @@ extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_resources_and_buildings.gd # Covers the resource/building loop end to end: -# 1. flying a ship over a surface rock banks it in GameState +# 1. flying a ship over a surface rock loads it into the fleet's hold +# (banking it needs a trip to the mothership - see #14 and +# tests/test_fleet_cargo.gd) # 2. buildings are paid for, persist in GameState, and respawn with the # space scene (they must survive the trip to a planet and back) # 3. a ship crafter adds a vessel to the fleet for rocks @@ -21,6 +23,7 @@ var mothership_pos: Vector2 = Vector2(900.0, -400.0) func _initialize() -> void: game_state = root.get_node("GameState") game_state.rocks = 0 + game_state.fleet_cargo = 0 game_state.buildings.clear() game_state.has_return_position = false @@ -49,12 +52,14 @@ func _process(delta: float) -> bool: 1: if elapsed < 1.5: return false - if game_state.rocks != 1: - print("rocks=%d after flying a ship onto one" % game_state.rocks) + # Into the hold, not the bank: since #14 ore is cargo until the + # fleet delivers it to the mothership. + if game_state.fleet_cargo != 1: + print("fleet_cargo=%d after flying a ship onto one" % game_state.fleet_cargo) print("TEST_RESOURCES_AND_BUILDINGS: FAIL (rock not gathered)") quit(1) return true - print("gathered rock -> rocks=%d" % game_state.rocks) + print("gathered rock -> hold=%d bank=%d" % [game_state.fleet_cargo, game_state.rocks]) # Bank enough to build with, then move to the space scene. game_state.rocks = 60