extends SceneTree # Headless test: run with # godot --headless -s res://tests/test_planning_center.gd # Forgejo #106 and #107 (#98a, #98b): the Planning Center. # # "let the player tell the system how many ships of a give type should # always be available over the whole game (fleet + storage + outposts) ... # the planning center automatically manage the ship craft building queues # to ensure that the needed vessels are produced to fullfill the # constraints." # # Covers: # A. the census counts a vessel exactly once wherever it is - flying, # stored, stationed at an outpost, or in the air between any two # B. a shortfall is queued at a crafter, and queued vessels count, so it # does not re-order the same shortfall every tick # C. it tops up rather than rewriting: an order placed by hand survives, # and the planner appends beside it # D. it stops when the target is met, and starts again when one is lost # E. nothing happens without a planning centre, but the plan is remembered # across losing and rebuilding one # F. the panel drives the real targets through the real buttons var game_state: Node var scene: Node var controller: Node var menu: Control var panel: Control var crafter_id: int = 0 var outpost_id: int = 0 var stage: int = 0 var elapsed: float = 0.0 func _initialize() -> void: # GameState is an autoload: a bare GameState. does not # compile in a SceneTree script, so the node is fetched by name instead. game_state = root.get_node("GameState") game_state.buildings.clear() game_state.extractors.clear() game_state.transporters.clear() game_state.stored_vessels.clear() game_state.vessel_deliveries.clear() game_state.vessel_targets.clear() game_state.fleet_vessel_types = PackedStringArray() game_state.reset_progression() game_state.reset_waves() game_state.unlocked.append_array(["shipyards", "coordination", "forward_bases", "vessel_science", "logistics_command"]) func _fail(message: String) -> bool: print("TEST_PLANNING_CENTER: FAIL (%s)" % message) quit(1) return true func _queued_at_crafter() -> int: return game_state.pending_orders_of(crafter_id, GameState.VESSEL_INTERCEPTOR) func _state_cases() -> bool: game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO) crafter_id = game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(400.0, 0.0)) outpost_id = game_state.add_building(GameState.BUILDING_OUTPOST, Vector2(-900.0, 0.0)) var interceptor: String = GameState.VESSEL_INTERCEPTOR # --- A. one vessel, one count, wherever it is ------------------------ if game_state.vessel_census(interceptor) != 0: return _fail("an empty game counts %d vessels" % game_state.vessel_census(interceptor)) game_state.fleet_vessel_types = PackedStringArray([interceptor, interceptor]) game_state.stored_vessels.append(interceptor) game_state.set_garrison_desired(outpost_id, interceptor, 1) game_state.garrison_crew(outpost_id).append(interceptor) game_state.vessel_deliveries.append({"type": interceptor, "from": Vector2.ZERO, "to": Vector2(400.0, 0.0), "progress": 0.0, "to_outpost": 0}) if game_state.vessel_count_in_fleet(interceptor) != 2: return _fail("the fleet reads %d" % game_state.vessel_count_in_fleet(interceptor)) if game_state.vessel_count_in_hangar(interceptor) != 1: return _fail("the hangar reads %d" % game_state.vessel_count_in_hangar(interceptor)) if game_state.vessel_count_in_garrisons(interceptor) != 1: return _fail("the garrisons read %d" % game_state.vessel_count_in_garrisons(interceptor)) if game_state.vessel_count_in_transit(interceptor) != 1: return _fail("in transit reads %d" % game_state.vessel_count_in_transit(interceptor)) if game_state.vessel_census(interceptor) != 5: return _fail("2 flying + 1 stored + 1 stationed + 1 moving counted as %d" \ % game_state.vessel_census(interceptor)) print("census: 2 flying, 1 stored, 1 stationed, 1 moving = %d" \ % game_state.vessel_census(interceptor)) # --- E (first half). no centre, no action --------------------------- game_state.set_vessel_target(interceptor, 8) for i in range(20): game_state._advance_planning() if _queued_at_crafter() != 0: return _fail("orders were placed with no planning centre built") print("with no planning centre, a standing order does nothing") game_state.add_building(GameState.BUILDING_PLANNING_CENTER, Vector2(900.0, 0.0)) if not game_state.has_planning_center(): return _fail("the planning centre was built but is not recognised") # --- B. the shortfall is queued, once ------------------------------ if game_state.vessel_shortfall(interceptor) != 3: return _fail("a target of 8 against 5 reads a shortfall of %d" \ % game_state.vessel_shortfall(interceptor)) for i in range(20): game_state._advance_planning() if _queued_at_crafter() != 3: return _fail("20 passes at a shortfall of 3 queued %d orders" % _queued_at_crafter()) if game_state.vessel_shortfall(interceptor) != 0: return _fail("the shortfall is still %d with 3 on the slipway" \ % game_state.vessel_shortfall(interceptor)) print("a shortfall of 3 queued exactly 3, and 20 more passes added none") # --- C. hand-placed orders are left alone --------------------------- game_state.queue_order(crafter_id, GameState.VESSEL_SHIELD) var by_hand: int = game_state.pending_orders_of(crafter_id, GameState.VESSEL_SHIELD) game_state.set_vessel_target(interceptor, 10) for i in range(20): game_state._advance_planning() if game_state.pending_orders_of(crafter_id, GameState.VESSEL_SHIELD) != by_hand: return _fail("the planner cancelled a shield the player ordered by hand") if _queued_at_crafter() != 5: return _fail("raising the target to 10 left %d interceptors queued" % _queued_at_crafter()) print("a hand-placed shield survives; the planner appended beside it") # --- D. it stops, and it reacts to a loss --------------------------- game_state.set_vessel_target(interceptor, 5) var queued_before: int = _queued_at_crafter() for i in range(20): game_state._advance_planning() if _queued_at_crafter() != queued_before: return _fail("lowering the target changed the queue by %d" \ % (_queued_at_crafter() - queued_before)) # Losing one from the fleet is a real shortfall again. game_state.set_vessel_target(interceptor, 12) var before_loss: int = _queued_at_crafter() for i in range(20): game_state._advance_planning() var after_top_up: int = _queued_at_crafter() game_state.fleet_vessel_types = PackedStringArray([interceptor]) for i in range(20): game_state._advance_planning() if _queued_at_crafter() <= after_top_up: return _fail("losing a vessel did not put a replacement on the slipway") print("target met -> nothing queued; a vessel lost -> %d queued (was %d)" \ % [_queued_at_crafter() - after_top_up, before_loss]) # --- E (second half). the plan outlives the building ---------------- for b in game_state.buildings: if game_state.building_has_capability(b, "planning"): game_state.destroy_building(int(b.id)) break if game_state.has_planning_center(): return _fail("the planning centre survived being destroyed") if game_state.vessel_target(interceptor) != 12: return _fail("losing the building erased the plan") var frozen: int = _queued_at_crafter() game_state.fleet_vessel_types = PackedStringArray() for i in range(20): game_state._advance_planning() if _queued_at_crafter() != frozen: return _fail("the planner kept working after its building was destroyed") print("destroyed: the plan is remembered, and nothing acts on it") return false func _process(delta: float) -> bool: elapsed += delta match stage: 0: if _state_cases(): return true game_state.buildings.clear() game_state.vessel_targets.clear() scene = load("res://world/Main.tscn").instantiate() root.add_child(scene) controller = scene.get_node("BuildController") menu = scene.get_node("UI/PlanningMenu") panel = scene.get_node("UI/PlanningMenu/Panel") elapsed = 0.0 stage = 1 return false 1: if elapsed < 0.3: return false scene.get_node("AsteroidField").set_process(false) scene.get_node("EnemySpawner").set_process(false) scene.get_node("BuildingField").place(GameState.BUILDING_PLANNING_CENTER, Vector2(900.0, 0.0)) elapsed = 0.0 stage = 2 return false 2: if elapsed < 0.2: return false # --- F. the real panel, through the real buttons ------------- var node: Node2D = null for b in scene.get_node("BuildingField").spawned: if is_instance_valid(b) and b.building_type == GameState.BUILDING_PLANNING_CENTER: node = b if node == null: return _fail("no planning centre node was placed") if node.primary_action() != "planning": return _fail("its primary action is \"%s\"" % node.primary_action()) controller.current_building = node controller._on_action_pressed() if not menu.visible: return _fail("the planning centre did not open its menu") var row: Dictionary = {} for candidate in panel._rows: if String(candidate.type) == GameState.VESSEL_INTERCEPTOR: row = candidate if row.is_empty(): return _fail("the panel has no row for the interceptor") row.plus.pressed.emit() row.plus.pressed.emit() row.plus.pressed.emit() if game_state.vessel_target(GameState.VESSEL_INTERCEPTOR) != 3: return _fail("three presses of + set a target of %d" \ % game_state.vessel_target(GameState.VESSEL_INTERCEPTOR)) row.minus.pressed.emit() if game_state.vessel_target(GameState.VESSEL_INTERCEPTOR) != 2: return _fail("- did not lower the target") if not row.count.text.ends_with("/2"): return _fail("the row reads \"%s\" - it should show have over want" % row.count.text) # The note is what makes an unmet plan legible: it says where the # vessels actually are. if row.note.text.strip_edges() == "" and game_state.vessel_census( GameState.VESSEL_INTERCEPTOR) > 0: return _fail("vessels exist but the row says nothing about where they are") print("the panel drives the real plan: + + + - leaves \"%s\" (%s)" \ % [row.count.text, row.note.text]) var status: String = scene.get_node("UI/PlanningMenu/StatusLabel").text if status.strip_edges() == "": return _fail("the planning menu reports nothing at all") print("status: \"%s\"" % status) print("TEST_PLANNING_CENTER: PASS") quit(0) return true return false