extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_distorder_stacking.gd # Each space-time distorder vessel PAST THE FIRST raises the fleet's speed # ceiling by 1.0, so a second one is worth crewing rather than dead weight. # # Covers: # A. one vessel gives the original ceiling, so nothing changed for a fleet # that carries a single distorder # B. each additional vessel adds exactly 1.0 # C. the fader's full travel reaches the WIDENED ceiling, not the old one # D. losing a vessel mid-boost pulls the fleet back down to the new # ceiling rather than leaving it faster than its crew can sustain # E. full boost still costs the same energy per second however wide the # ceiling is - more vessels buy more speed, not cheaper speed var game_state: Node var main: Node var fleet: Node var fader: Node var stage: int = 0 var elapsed: float = 0.0 func _initialize() -> void: game_state = root.get_node("GameState") game_state.reset_progression() game_state.vessel_levels["unlock_distorder"] = 1 game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 500) main = load("res://Main.tscn").instantiate() root.add_child(main) fleet = main.get_node("Fleet") fader = main.get_node("UI/SpeedFader") func _process(delta: float) -> bool: elapsed += delta match stage: 0: if elapsed < 0.2: return false main.get_node("EnemySpawner").set_process(false) main.get_node("AsteroidField").set_process(false) # Room for a fleet of distorders. game_state.unlocked.append_array(["wing", "squadron", "armada"]) # --- A. one vessel is unchanged ------------------------------ fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([ GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_DISTORDER])) if not is_equal_approx(game_state.max_fleet_speed_multiplier, GameState.MAX_FLEET_SPEED_MULTIPLIER): return _fail("one distorder gives a ceiling of %.1f, expected the unchanged %.1f" \ % [game_state.max_fleet_speed_multiplier, GameState.MAX_FLEET_SPEED_MULTIPLIER]) print("1 distorder -> ceiling %.1fx" % game_state.max_fleet_speed_multiplier) # --- B. each additional vessel adds 1.0 ---------------------- for extra in range(1, 4): fleet.add_ship(GameState.VESSEL_DISTORDER) var expected: float = GameState.MAX_FLEET_SPEED_MULTIPLIER + float(extra) if not is_equal_approx(game_state.max_fleet_speed_multiplier, expected): return _fail("%d distorders give a ceiling of %.1f, expected %.1f" \ % [extra + 1, game_state.max_fleet_speed_multiplier, expected]) print("%d distorders -> ceiling %.1fx" % [extra + 1, game_state.max_fleet_speed_multiplier]) # --- C. the fader reaches the widened ceiling ---------------- # Dragging the track to its top must mean the CURRENT ceiling, or # the extra vessels would be unreachable through the only control # that exists for them. fader._process(0.0) if not fader.visible: return _fail("the fader is hidden with four distorder vessels crewed") var press := InputEventScreenTouch.new() press.index = 3 press.position = fader.global_position + Vector2(fader.size.x * 0.5, fader.size.y - 4.0) press.pressed = true main._handle_touch(press) var drag := InputEventScreenDrag.new() drag.index = 3 drag.position = fader.global_position + Vector2(fader.size.x * 0.5, 0.0) main._handle_drag(drag) if not is_equal_approx(game_state.fleet_speed_multiplier, game_state.max_fleet_speed_multiplier): return _fail("dragging the fader to the top reached %.2fx, not the ceiling %.2fx" \ % [game_state.fleet_speed_multiplier, game_state.max_fleet_speed_multiplier]) print("fader top reaches %.1fx" % game_state.fleet_speed_multiplier) # --- E. full boost costs the same per second ----------------- # The drain is normalized against the current ceiling, so sitting # at full boost is the same price whatever the ceiling is. var wide_energy: int = _energy_spent_over(1.0) # --- D. losing vessels pulls the boost back down ------------- var boosted_before: float = game_state.fleet_speed_multiplier for i in range(fleet.ships.size() - 1, -1, -1): if is_instance_valid(fleet.ships[i]) and fleet.ships[i].vessel_type == GameState.VESSEL_DISTORDER: fleet.store_ship(i) break if game_state.max_fleet_speed_multiplier >= boosted_before: return _fail("losing a distorder left the ceiling at %.1f, no lower than the %.1f the fleet was already doing" \ % [game_state.max_fleet_speed_multiplier, boosted_before]) if game_state.fleet_speed_multiplier > game_state.max_fleet_speed_multiplier + 0.001: return _fail("the fleet is still doing %.2fx after its ceiling dropped to %.2fx" \ % [game_state.fleet_speed_multiplier, game_state.max_fleet_speed_multiplier]) print("losing one pulled %.1fx down to the new %.1fx ceiling" \ % [boosted_before, game_state.fleet_speed_multiplier]) # Same measurement at the narrower ceiling, also at full boost. game_state.set_fleet_speed_multiplier(game_state.max_fleet_speed_multiplier) var narrow_energy: int = _energy_spent_over(1.0) if absi(wide_energy - narrow_energy) > 1: return _fail("full boost cost %d energy with a wide ceiling and %d with a narrow one - extra vessels changed the PRICE, not just the speed" \ % [wide_energy, narrow_energy]) print("full boost costs %d/%d energy per second at either ceiling" % [wide_energy, narrow_energy]) print("TEST_DISTORDER_STACKING: PASS") quit(0) return true return false # Energy actually consumed by holding the current multiplier for `seconds`, # driven directly so the measurement does not depend on frame pacing. func _energy_spent_over(seconds: float) -> int: var before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY) var step: float = 0.05 var t: float = 0.0 while t < seconds: game_state._advance_distorder_drain(step) t += step return before - game_state.fleet_resource(GameState.RESOURCE_ENERGY) func _fail(message: String) -> bool: print("TEST_DISTORDER_STACKING: FAIL (%s)" % message) quit(1) return true