extends SceneTree # Headless test: run with # godot --headless -s res://tests/test_outpost_garrison_menu.gd # Forgejo #103 (#97e): "Outposts can be occupied by ships. Only ships that # attack can be assigned. A dedicated menu on each outpost should let you # compose how you want the outpost to be filled. There's a default of 2 # vessel per outpost, upgrades can increase this number." # # Two things live on an outpost's record and conflating them is the first # thing to get wrong: the ORDER (what the player asked for) and the CREW # (who is actually there). This menu edits the order only; #104 fills the # crew. Keeping the order as a wish is what lets an outpost ask for a hull # nobody has built yet and simply wait. # # Covers: # A. only armed vessels can be assigned - a shield, a hauler or an engine # block cannot hold a position however much the player wants it there # B. two berths by default, and the order cannot exceed them # C. researching the berth upgrade raises the ceiling at outposts ALREADY # standing, not only at new ones # D. the menu opens from the outpost, drives the real order through the # real buttons, and reads back "here / ordered" rather than one number # E. an outpost with an order nobody can fill reports it as waiting, and # the order survives - it is not silently dropped var game_state: Node var scene: Node var controller: Node var menu: Control var panel: Control 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.reset_progression() game_state.reset_waves() game_state.rocks = 5000 game_state.unlocked.append("forward_bases") func _fail(message: String) -> bool: print("TEST_OUTPOST_GARRISON_MENU: FAIL (%s)" % message) quit(1) return true func _state_cases() -> bool: # --- A. only armed vessels ------------------------------------------ var eligible: PackedStringArray = game_state.garrison_vessel_types() if eligible.is_empty(): return _fail("no vessel at all may be garrisoned") if not (GameState.VESSEL_INTERCEPTOR in eligible): return _fail("the interceptor, which carries the fleet's gun, cannot be garrisoned") for unarmed in [GameState.VESSEL_SHIELD, GameState.VESSEL_TRANSPORTER, GameState.VESSEL_CARRIER, GameState.VESSEL_DISTORDER]: if game_state.vessel_can_garrison(unarmed): return _fail("%s can be garrisoned - it does not attack" % unarmed) print("garrisonable: %s" % str(eligible)) var id: int = game_state.add_building(GameState.BUILDING_OUTPOST, Vector2(500.0, 0.0)) outpost_id = id if not game_state.is_outpost(game_state.find_building(id)): return _fail("the outpost does not carry the garrison capability") if game_state.set_garrison_desired(id, GameState.VESSEL_SHIELD, 1) != 0: return _fail("a shield vessel was accepted into a garrison") # --- B. two berths, and the ceiling bites --------------------------- if GameState.GARRISON_BASE_SLOTS != 2: return _fail("the default is %d berths, not the 2 the ticket asks for" \ % GameState.GARRISON_BASE_SLOTS) if game_state.outpost_slot_capacity() != 2: return _fail("an unresearched outpost offers %d berths" % game_state.outpost_slot_capacity()) if game_state.set_garrison_desired(id, GameState.VESSEL_INTERCEPTOR, 5) != 2: return _fail("an order of 5 was not clamped to the 2 berths available") if game_state.garrison_desired_total(id) != 2: return _fail("the outpost holds an order of %d against 2 berths" \ % game_state.garrison_desired_total(id)) print("2 berths by default; an order of 5 clamps to 2") # --- C. the upgrade reaches existing outposts ----------------------- var upgrade_id: String = "" for node in GameState.SKILL_TREE: if int(node.get("outpost_bonus", 0)) > 0: upgrade_id = String(node.id) if upgrade_id == "": return _fail("no skill node raises the berth count") game_state.unlocked.append(upgrade_id) if game_state.outpost_slot_capacity() != 3: return _fail("researching \"%s\" left the capacity at %d" \ % [upgrade_id, game_state.outpost_slot_capacity()]) # The outpost was placed BEFORE the research, which is the case that # matters: a bonus that only reached new buildings would mean demolishing # and rebuilding to collect it. if game_state.set_garrison_desired(id, GameState.VESSEL_INTERCEPTOR, 3) != 3: return _fail("an outpost built before the upgrade did not gain the berth") print("\"%s\" raises an already-standing outpost to 3 berths" % upgrade_id) game_state.set_garrison_desired(id, GameState.VESSEL_INTERCEPTOR, 0) return false func _process(delta: float) -> bool: elapsed += delta match stage: 0: if _state_cases(): return true game_state.buildings.clear() scene = load("res://world/Main.tscn").instantiate() root.add_child(scene) controller = scene.get_node("BuildController") menu = scene.get_node("UI/GarrisonMenu") panel = scene.get_node("UI/GarrisonMenu/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_OUTPOST, Vector2(500.0, 0.0)) elapsed = 0.0 stage = 2 return false 2: if elapsed < 0.2: return false # --- D. the real menu, through the real buttons -------------- var outpost: Node2D = null for b in scene.get_node("BuildingField").spawned: if is_instance_valid(b) and b.building_type == GameState.BUILDING_OUTPOST: outpost = b if outpost == null: return _fail("no outpost node was placed") outpost_id = int(outpost.building_id) if outpost.primary_action() != "garrison": return _fail("the outpost's primary action is \"%s\", not \"garrison\"" \ % outpost.primary_action()) controller.current_building = outpost controller._on_action_pressed() if not menu.visible: return _fail("the outpost did not open the garrison menu") if panel.building_id != outpost_id: return _fail("the panel opened without knowing which outpost it belongs to") 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") for unarmed in [GameState.VESSEL_SHIELD, GameState.VESSEL_TRANSPORTER]: for candidate in panel._rows: if String(candidate.type) == unarmed: return _fail("the panel offers a row for the unarmed %s" % unarmed) row.plus.pressed.emit() row.plus.pressed.emit() if game_state.garrison_desired_of(outpost_id, GameState.VESSEL_INTERCEPTOR) != 2: return _fail("two presses of + ordered %d vessels" \ % game_state.garrison_desired_of(outpost_id, GameState.VESSEL_INTERCEPTOR)) if row.count.text != "0/2": return _fail("the row reads \"%s\" - it should show crew over order" % row.count.text) row.minus.pressed.emit() if game_state.garrison_desired_of(outpost_id, GameState.VESSEL_INTERCEPTOR) != 1: return _fail("- did not take the order back down") print("the menu drives the real order: + + - leaves 1, shown as \"%s\"" % row.count.text) # The ceiling is enforced through the buttons too, not only in # GameState - a + that silently did nothing would be worse than a # disabled one. row.plus.pressed.emit() row.plus.pressed.emit() row.plus.pressed.emit() var capacity: int = game_state.outpost_slot_capacity() if game_state.garrison_desired_total(outpost_id) != capacity: return _fail("pressing + past the ceiling ordered %d against %d berths" \ % [game_state.garrison_desired_total(outpost_id), capacity]) if row.plus.enabled: return _fail("+ is still offered with every berth already spoken for") print("+ stops at %d berths and greys itself out" % capacity) # --- E. an unfillable order waits --------------------------- if game_state.garrison_crew(outpost_id).size() != 0: return _fail("the outpost is crewed already, so waiting proves nothing") controller._refresh_garrison() var status: String = scene.get_node("UI/GarrisonMenu/StatusLabel").text if status.strip_edges() == "": return _fail("an outpost waiting on vessels says nothing at all") if game_state.garrison_desired_total(outpost_id) != capacity: return _fail("the order was dropped because it could not be filled") print("waiting, and saying so: \"%s\"" % status) print("TEST_OUTPOST_GARRISON_MENU: PASS") quit(0) return true return false