#204 the Beam Collimator widens the beam instead of adding range to one that already crosses the screen; beam width becomes a per-shot vessel stat. #205 the Energy Propulser gains three upgrades - dash recharge, dash reach, and boosted-cruise drain - which turns three constants into vessel stats. #206 three further doctrines carry the fleet from ten to twenty. #207 the prologue's World Ship gets a marker, through the scanner's own guidance arrow rather than a second one. #208 #210 #213 #214 four new synthesised sounds: the breach opening, cargo reaching the hold, a rock breaking, and a gas bomb blooming. #215 building health bars are larger, plated and framed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
157 lines
7 KiB
GDScript
157 lines
7 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless gameplay test: run with
|
|
# godot --headless -s res://tests/test_fleet_growth.gd
|
|
# A new game opens with a SINGLE vessel and three slots, and the fleet grows
|
|
# from there through the doctrine branch of the skill tree.
|
|
#
|
|
# BASE_FLEET_SIZE used to be both "how many slots exist" and "how many ships
|
|
# you start with", which forced a new game to open with a full fleet - there
|
|
# was nowhere to put a vessel until a doctrine was researched, so the first
|
|
# ship crafter had nothing to do. The two are now separate constants, and
|
|
# this test pins the distinction.
|
|
#
|
|
# Covers:
|
|
# A. a new game starts with one vessel and three slots
|
|
# B. the fleet really spawns one ship, and can be grown into its slots
|
|
# C. every doctrine raises the ceiling by its own stated bonus
|
|
# D. the doctrine branch is a chain, in increasing cost order
|
|
# E. the tree cannot promise a slot the formation has no room for - the
|
|
# invariant that made extra doctrines silently worthless
|
|
|
|
const DOCTRINE_CHAIN: Array = ["wing", "squadron", "armada", "battlegroup",
|
|
"grand_fleet", "task_force", "star_legion", "void_armada"]
|
|
|
|
var game_state: Node
|
|
var scene: Node
|
|
var fleet: Node
|
|
var stage: int = 0
|
|
var elapsed: float = 0.0
|
|
|
|
|
|
func _initialize() -> void:
|
|
game_state = root.get_node("GameState")
|
|
game_state.reset_new_game()
|
|
# #108: reset_new_game() arms the prologue, and the prologue opens with a
|
|
# convoy rather than the game's own starting fleet. This test is about
|
|
# what the GAME starts from, which is where the prologue hands over -
|
|
# see test_intro_scene.gd for the prologue itself. The quest chain goes
|
|
# with it: its briefings are dialogs, and a dialog stops the tree (#128).
|
|
game_state.intro_active = false
|
|
game_state.reset_quests()
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
elapsed += delta
|
|
match stage:
|
|
0:
|
|
# --- A. the opening position ---------------------------------
|
|
if GameState.STARTING_SHIP_COUNT != 1:
|
|
return _fail("a new game starts with %d vessels, expected 1" % GameState.STARTING_SHIP_COUNT)
|
|
if GameState.BASE_FLEET_SIZE != 3:
|
|
return _fail("the fleet has %d slots by default, expected 3" % GameState.BASE_FLEET_SIZE)
|
|
if game_state.surviving_ship_count != GameState.STARTING_SHIP_COUNT:
|
|
return _fail("reset_new_game left %d surviving ships" % game_state.surviving_ship_count)
|
|
if game_state.max_fleet_size() != GameState.BASE_FLEET_SIZE:
|
|
return _fail("an unresearched fleet caps at %d, expected %d" \
|
|
% [game_state.max_fleet_size(), GameState.BASE_FLEET_SIZE])
|
|
print("new game: %d vessel, %d slots" % [GameState.STARTING_SHIP_COUNT, GameState.BASE_FLEET_SIZE])
|
|
|
|
# --- C + D. the doctrine chain -------------------------------
|
|
var expected: int = GameState.BASE_FLEET_SIZE
|
|
var previous_cost: int = -1
|
|
var previous_points: int = -1
|
|
var previous_id: String = "coordination"
|
|
for id in DOCTRINE_CHAIN:
|
|
var node: Dictionary = game_state.skill_node(id)
|
|
if node.is_empty():
|
|
return _fail("no skill node '%s' - the doctrine chain is broken" % id)
|
|
var bonus: int = int(node.get("fleet_bonus", 0))
|
|
if bonus <= 0:
|
|
return _fail("'%s' is in the doctrine chain but grants no fleet_bonus" % id)
|
|
if String(node.get("requires", "")) != previous_id:
|
|
return _fail("'%s' requires '%s', expected it to follow '%s' - the branch is not one chain" \
|
|
% [id, node.get("requires", ""), previous_id])
|
|
# Later tiers must not be cheaper than earlier ones, or the
|
|
# order the player meets them stops matching what they cost.
|
|
var cost: int = int(node.get("cost", 0))
|
|
var points: int = int(node.get("research_points", 0))
|
|
if cost <= previous_cost:
|
|
return _fail("'%s' costs %d ore, not more than the tier before it (%d)" % [id, cost, previous_cost])
|
|
if points < previous_points:
|
|
return _fail("'%s' costs %d research points, fewer than the tier before it (%d)" \
|
|
% [id, points, previous_points])
|
|
previous_cost = cost
|
|
previous_points = points
|
|
previous_id = id
|
|
|
|
game_state.unlocked.append(id)
|
|
expected += bonus
|
|
if game_state.max_fleet_size() != expected:
|
|
return _fail("after researching '%s' the fleet caps at %d, expected %d" \
|
|
% [id, game_state.max_fleet_size(), expected])
|
|
print(" %-12s +%d -> %2d slots (%d ore, %d research)" % [id, bonus, expected, cost, points])
|
|
|
|
# --- E. no doctrine may be swallowed by the formation ---------
|
|
# max_fleet_size() clamps to the formation's length, so a doctrine
|
|
# added without extending DEFAULT_FORMATION would cost the player
|
|
# real resources and grant nothing.
|
|
if expected > GameState.DEFAULT_FORMATION.size():
|
|
return _fail("the doctrines promise %d slots but DEFAULT_FORMATION only defines %d - the last ones buy nothing" \
|
|
% [expected, GameState.DEFAULT_FORMATION.size()])
|
|
# #206 named the destination, so it is pinned rather than left to
|
|
# whatever the chain happens to add up to.
|
|
if expected != 20:
|
|
return _fail("the doctrine chain reaches %d vessels, not the 20 it is meant to" \
|
|
% expected)
|
|
print("full doctrine chain reaches %d slots, with %d formation positions defined" \
|
|
% [expected, GameState.DEFAULT_FORMATION.size()])
|
|
|
|
# --- B. what actually flies ----------------------------------
|
|
game_state.reset_new_game()
|
|
# ... and again here: this is the reset the scene below is built
|
|
# from, so the prologue has to be off for THIS one too - and the
|
|
# quest chain with it.
|
|
game_state.intro_active = false
|
|
game_state.reset_quests()
|
|
scene = load("res://world/Main.tscn").instantiate()
|
|
root.add_child(scene)
|
|
fleet = scene.get_node("Fleet")
|
|
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)
|
|
if fleet.ships.size() != GameState.STARTING_SHIP_COUNT:
|
|
return _fail("a fresh scene spawned %d vessels, expected %d" \
|
|
% [fleet.ships.size(), GameState.STARTING_SHIP_COUNT])
|
|
print("a fresh game really flies %d vessel" % fleet.ships.size())
|
|
|
|
# The two empty slots are what the opening is for, so they have to
|
|
# be fillable without researching anything.
|
|
for i in range(GameState.BASE_FLEET_SIZE - GameState.STARTING_SHIP_COUNT):
|
|
if not fleet.add_ship(GameState.VESSEL_INTERCEPTOR):
|
|
return _fail("could not crew slot %d of the starting fleet" % (i + 2))
|
|
if fleet.ships.size() != GameState.BASE_FLEET_SIZE:
|
|
return _fail("the fleet holds %d after filling its slots, expected %d" \
|
|
% [fleet.ships.size(), GameState.BASE_FLEET_SIZE])
|
|
# And the cap has to actually bite past that.
|
|
if fleet.add_ship(GameState.VESSEL_INTERCEPTOR):
|
|
return _fail("a %dth vessel joined an unresearched fleet of %d slots" \
|
|
% [fleet.ships.size(), GameState.BASE_FLEET_SIZE])
|
|
print("the starting slots fill to %d and refuse a %dth" \
|
|
% [GameState.BASE_FLEET_SIZE, GameState.BASE_FLEET_SIZE + 1])
|
|
|
|
print("TEST_FLEET_GROWTH: PASS")
|
|
quit(0)
|
|
return true
|
|
return false
|
|
|
|
|
|
func _fail(message: String) -> bool:
|
|
print("TEST_FLEET_GROWTH: FAIL (%s)" % message)
|
|
quit(1)
|
|
return true
|