extends SceneTree # Headless test: run with # godot --headless -s res://tests/test_intro_scene.gd # Forgejo #108 (#122, #123, #124): the prologue. # # The director drives the REAL scene rather than being a scene of its own, so # everything below is checked against the real fleet, the real enemies and # the real asteroid field. # # Covers: # A. it runs on a NEW game and not on a resumed one # B. "no action button available, only movements" - every pressable # affordance is gone, and the joystick is not # C. the convoy: bigger than the player's own ceiling, and mostly holds # D. the guide speaks, and the banner never shows a line nobody set # E. the ambush is easy: chasers only, and clearing them advances the beat # F. the belt is dense, and it is across the lane rather than beside it # G. the breach takes the wheel: player input stops moving the fleet # H. the vortex leaves exactly one vessel, and it is the default interceptor # I. the handover: the flag clears, the buttons come back, the spawner and # the asteroid field are running again, and the fleet is a new game's # J. skipping lands in the same state as playing it through var game_state: Node var scene: Node var director: Node var fleet: Node var narration: Control var stage: int = 0 var elapsed: float = 0.0 var convoy_size: int = 0 var skipped_props: Array = [] 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.reset_new_game() game_state.rocks = 0 func _fail(message: String) -> bool: print("TEST_INTRO_SCENE: FAIL (%s)" % message) quit(1) return true func _load_scene() -> void: scene = load("res://world/Main.tscn").instantiate() root.add_child(scene) current_scene = scene fleet = scene.get_node("Fleet") director = scene.get_node_or_null("IntroDirector") narration = scene.get_node("UI/NarrationBanner") func _drop_scene() -> void: scene.queue_free() scene = null func _hidden(path: String) -> bool: var node: Control = scene.get_node_or_null(path) return node == null or not node.visible func _process(delta: float) -> bool: elapsed += delta match stage: 0: # --- A. a new game gets one ------------------------------ if not game_state.intro_active: return _fail("reset_new_game() did not arm the prologue") _load_scene() elapsed = 0.0 stage = 1 return false 1: if elapsed < 0.4: return false if director == null: return _fail("no IntroDirector in the scene") if not director.running: return _fail("the director did not start on a new game") # --- C. the convoy -------------------------------------- convoy_size = fleet.ships.size() if convoy_size <= GameState.BASE_FLEET_SIZE: return _fail("the convoy is %d vessels, no more than a new game's %d slots" \ % [convoy_size, GameState.BASE_FLEET_SIZE]) var holds: int = 0 var escorts: int = 0 for ship in fleet.ships: if String(ship.vessel_type) == GameState.VESSEL_INTERCEPTOR: escorts += 1 else: holds += 1 if holds <= escorts: return _fail("the convoy is %d holds to %d escorts - it should be mostly cargo" \ % [holds, escorts]) if escorts <= 0: return _fail("the convoy has no escorts at all") print("convoy: %d vessels, %d holds and %d escorts" % [convoy_size, holds, escorts]) # --- B. movement and nothing else ----------------------- for pressable in ["UI/BuildButton", "UI/MapButton", "UI/BuildActionButton", "UI/BuildingActionButton", "UI/EnterAtmosphereButton", "UI/TransferButton", "UI/HangarButton", "UI/LaserButton", "UI/GasBombButton"]: if not _hidden(pressable): return _fail("%s is offered during the prologue" % pressable) var joystick: Control = scene.get_node("UI/Joystick") if not joystick.visible: return _fail("the joystick is hidden - the player cannot even fly") print("every action button is away; the joystick is not") # --- D. the guide speaks -------------------------------- if not narration.is_speaking(): return _fail("the prologue opened without a word from the guide") if narration.label.text.strip_edges() == "": return _fail("the banner is up with nothing on it") print("guide: \"%s\"" % narration.label.text) # --- E. the ambush -------------------------------------- director._begin(director.Beat.AMBUSH) if director.ambush.is_empty(): return _fail("the ambush beat spawned nothing") for enemy in director.ambush: if String(enemy.enemy_id) != "chaser": return _fail("the ambush includes a %s - it is meant to be easy" % enemy.enemy_id) if director._beat_complete(): return _fail("the ambush counts as cleared while its raiders are alive") for enemy in director.ambush: enemy.queue_free() elapsed = 0.0 stage = 2 return false 2: if elapsed < 0.3: return false # The director advances itself, so the evidence that clearing the # ambush ended the beat is that it has MOVED ON - asking # _beat_complete() again would be asking about a beat it left. if director.beat <= director.Beat.AMBUSH: return _fail("killing every raider did not end the ambush beat") print("%d raiders, all chasers, and clearing them ends the beat" % director.AMBUSH_COUNT) # --- F. the belt ---------------------------------------- director._begin(director.Beat.GAUNTLET) # Only the rocks still standing: the convoy flies into them, and # a rock it has already broken is not evidence about the belt. var standing: Array = [] for rock in director.rocks: if is_instance_valid(rock) and not rock.is_queued_for_deletion(): standing.append(rock) if standing.size() < 40: return _fail("the \"pretty dense\" belt is %d rocks" % standing.size()) var ahead: int = 0 for rock in standing: if rock.global_position.y < fleet.get_centroid().y: ahead += 1 if ahead < standing.size() / 2: return _fail("only %d of %d rocks are on the lane ahead" \ % [ahead, standing.size()]) print("belt: %d rocks, %d of them across the lane ahead" % [standing.size(), ahead]) # --- G. the breach takes the wheel ---------------------- director._begin(director.Beat.BREACH) if not game_state.intro_locks_movement: return _fail("the breach beat left the player in control") # GameController.gd is the script on Main itself, not a child. var before: Vector2 = fleet.fleet_target scene._process(0.1) if fleet.fleet_target != before: return _fail("player steering still moved the fleet during the breach") print("the breach has the wheel: steering input moves nothing") # --- H. the vortex -------------------------------------- director._begin(director.Beat.VORTEX) elapsed = 0.0 stage = 3 return false 3: # Left to the director's own clock rather than ticked by hand: # the vortex is a timed animation and driving it twice would not # be the thing that ships. if elapsed < 4.2: return false var alive: int = 0 for ship in fleet.ships: if is_instance_valid(ship) and not ship.is_queued_for_deletion(): alive += 1 if alive != 1: return _fail("the vortex left %d vessels, expected 1" % alive) if String(fleet.ships[0].vessel_type) != GameState.VESSEL_INTERCEPTOR: return _fail("the survivor is a %s, not the default interceptor" \ % fleet.ships[0].vessel_type) print("the vortex took %d vessels and left one interceptor" % (convoy_size - 1)) # --- I. the handover ------------------------------------ director.finish() elapsed = 0.0 stage = 4 return false 4: if elapsed < 0.3: return false if game_state.intro_active or game_state.intro_locks_movement: return _fail("the prologue finished without clearing its own flags") if game_state.intro_fleet_size != 0: return _fail("the convoy's fleet-size override outlived the prologue") if game_state.max_fleet_size() != GameState.BASE_FLEET_SIZE: return _fail("the fleet still holds %d slots after the prologue" \ % game_state.max_fleet_size()) if fleet.ships.size() != GameState.STARTING_SHIP_COUNT: return _fail("the game starts with %d vessels, expected %d" \ % [fleet.ships.size(), GameState.STARTING_SHIP_COUNT]) if not scene.get_node("EnemySpawner").is_processing(): return _fail("waves are still switched off after the prologue") if not scene.get_node("AsteroidField").is_processing(): return _fail("the asteroid field is still switched off after the prologue") scene.get_node("BuildController")._process(0.1) if _hidden("UI/BuildButton"): return _fail("the build button never came back") if narration.is_speaking(): return _fail("the guide is still talking after the handover") print("handover: flags clear, 1 interceptor, waves and rocks running, buttons back") _drop_scene() elapsed = 0.0 stage = 5 return false 5: if elapsed < 0.3: return false # --- A (second half). a resumed game gets no prologue ---- game_state.intro_active = false _load_scene() elapsed = 0.0 stage = 6 return false 6: if elapsed < 0.4: return false if director != null and is_instance_valid(director) and director.running: return _fail("the prologue ran again on a game that is past it") if fleet.ships.size() > GameState.BASE_FLEET_SIZE: return _fail("a resumed game opened with a convoy") print("a game already past the prologue does not replay it") _drop_scene() elapsed = 0.0 stage = 7 return false 7: if elapsed < 0.3: return false # --- J. skipping lands where playing it through does ----- game_state.reset_new_game() _load_scene() elapsed = 0.0 stage = 8 return false 8: if elapsed < 0.4: return false # Skipped mid-prologue, with props on the board, so the check # below is about clearing them rather than about an empty stage. director._begin(director.Beat.AMBUSH) director._begin(director.Beat.GAUNTLET) skipped_props = director.ambush + director.rocks if skipped_props.size() < 40: return _fail("nothing was on the board to skip past") director.skip() elapsed = 0.0 stage = 9 return false 9: if elapsed < 0.3: return false if game_state.intro_active or game_state.intro_locks_movement: return _fail("skipping left the prologue's flags set") if game_state.max_fleet_size() != GameState.BASE_FLEET_SIZE: return _fail("skipping left the convoy's fleet size behind") if fleet.ships.size() != GameState.STARTING_SHIP_COUNT: return _fail("skipping left %d vessels flying" % fleet.ships.size()) for prop in skipped_props: if is_instance_valid(prop) and not prop.is_queued_for_deletion(): return _fail("skipping left the prologue's own props on the board") print("skipping clears %d props and lands where playing it through does" \ % skipped_props.size()) print("TEST_INTRO_SCENE: PASS") quit(0) return true return false