Buy fleet berths with Fleet Offices
#219: fleet capacity is a built, supplied thing rather than a researched one. The eight fleet-size doctrines are gone; capacity is one berth free plus one per Fleet Office standing. An office charges every 25s for the vessel in its berth, priced off that vessel's own build cost so an expensive hull is expensive to keep, and drawn from its own store the way a gas burner draws gas. An office that cannot pay goes dark, its vessel is scuttled, and the player is told through the same queue quest lines use - which reaches them on a planet surface too. An empty berth is idle rather than starved: an office is only failing while there is something in it to fail, and holding it disabled would leave a berth that could never be crewed and so could never be paid for again. Ships have no hit points and a shield absorbs a hit wholesale, so the lost vessel is scuttled rather than damaged - otherwise a shielded hull would survive its own eviction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
parent
6e13b6397c
commit
1f5fecd371
60 changed files with 695 additions and 243 deletions
21
tests/_berths.gd
Normal file
21
tests/_berths.gd
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
extends RefCounted
|
||||
|
||||
# #219 made fleet capacity a BUILT thing: one berth free, one more per Fleet
|
||||
# Office standing. A test that flies four vessels therefore needs three
|
||||
# offices, where before it unlocked three doctrines.
|
||||
#
|
||||
# Loaded with load() at runtime rather than preloaded at the top of a test -
|
||||
# these are script-mode entry points, and a top-level preload evaluates during
|
||||
# their own early compilation, which can happen before the autoloads exist.
|
||||
#
|
||||
# The offices are put a long way from the origin so they cannot wander into
|
||||
# whatever the test is actually looking at, and left unstocked: upkeep is not
|
||||
# charged until FLEET_OFFICE_UPKEEP_SECONDS has passed, and no test here runs
|
||||
# that long.
|
||||
|
||||
|
||||
static func grant(game_state: Node, vessels: int) -> void:
|
||||
var needed: int = maxi(vessels - int(game_state.BASE_FLEET_SIZE), 0)
|
||||
for i in range(needed):
|
||||
game_state.add_building(String(game_state.BUILDING_FLEET_OFFICE),
|
||||
Vector2(40000.0 + 500.0 * float(i), 40000.0))
|
||||
|
|
@ -12,7 +12,8 @@ func _ready() -> void:
|
|||
GameState.vessel_levels["unlock_laser"] = 1
|
||||
GameState.vessel_levels["unlock_gas_bomber"] = 1
|
||||
GameState.vessel_levels["unlock_distorder"] = 1
|
||||
GameState.unlocked.append("wing")
|
||||
# #219: a berth is an office now, not a doctrine.
|
||||
GameState.add_building(GameState.BUILDING_FLEET_OFFICE, Vector2(2400.0, 0.0))
|
||||
GameState.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 200)
|
||||
GameState.deposit_fleet_resource(GameState.RESOURCE_GAS, 200)
|
||||
var main: Node = load("res://world/Main.tscn").instantiate()
|
||||
|
|
|
|||
1
tests/_scanner_guidance_preview.gd.uid
Normal file
1
tests/_scanner_guidance_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bwusdnid203ma
|
||||
1
tests/_scanner_menu_preview.gd.uid
Normal file
1
tests/_scanner_menu_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cvpu8gyb5757i
|
||||
|
|
@ -20,6 +20,10 @@ func _initialize() -> void:
|
|||
state.fleet_inventory.total_capacity = -1
|
||||
state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 500)
|
||||
state.deposit_fleet_resource(GameState.RESOURCE_GAS, 500)
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
current_scene = main
|
||||
|
|
|
|||
1
tests/test_ability_button_layout.gd.uid
Normal file
1
tests/test_ability_button_layout.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cwxeg5i0betsn
|
||||
|
|
@ -47,6 +47,10 @@ func _initialize() -> void:
|
|||
game_state.reset_progression()
|
||||
game_state.intro_active = false
|
||||
game_state.reset_quests()
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
current_scene = main
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ func _initialize() -> void:
|
|||
game_state.build_time_scale = 0.02
|
||||
game_state.reset_progression()
|
||||
game_state.rocks = 2000
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ extends SceneTree
|
|||
# holes where they used to be - it is repacked from whatever is researched.
|
||||
#
|
||||
# Verifies:
|
||||
# 1. with nothing researched only the mothership is offered (it is
|
||||
# 1. with nothing researched only the mothership and the Fleet Office are
|
||||
# offered (the mothership is
|
||||
# deliberately never gated - it is where research happens)
|
||||
# 2. researching a structure adds it, and it is placed, not left at
|
||||
# whatever offsets it happened to have in the scene file
|
||||
|
|
@ -79,21 +80,36 @@ func _process(delta: float) -> bool:
|
|||
|
||||
# --- 1. nothing researched ----------------------------------
|
||||
controller._refresh_build_status()
|
||||
# #219 put the Fleet Office beside the mothership at the opening:
|
||||
# it is the only way the fleet grows now, so gating it behind
|
||||
# research would gate fleet size behind research again - which is
|
||||
# the thing that ticket removed.
|
||||
var opening: Array = [GameState.BUILDING_MOTHERSHIP,
|
||||
GameState.BUILDING_FLEET_OFFICE]
|
||||
var shown: Array = _shown()
|
||||
if shown.size() != 1 or shown[0][0] != GameState.BUILDING_MOTHERSHIP:
|
||||
var names: Array = []
|
||||
for e in shown:
|
||||
names.append(e[0])
|
||||
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (unresearched structures on offer: %s)" % [names])
|
||||
var shown_ids: Array = []
|
||||
for e in shown:
|
||||
shown_ids.append(e[0])
|
||||
shown_ids.sort()
|
||||
opening.sort()
|
||||
if shown_ids != opening:
|
||||
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (opening offer was %s, expected %s)" \
|
||||
% [shown_ids, opening])
|
||||
quit(1)
|
||||
return true
|
||||
# --- 4. and it is centred, being alone on its row -----------
|
||||
var only: Array = _slots()[0]
|
||||
if absf(only[1]) > 0.01:
|
||||
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (the only entry sits at x=%.1f instead of centred)" % only[1])
|
||||
# --- 4. and the row is centred as a whole -------------------
|
||||
# Two entries now, so "centred" is the pair straddling the middle
|
||||
# rather than one sitting on it. The rule being checked is the
|
||||
# same one: a short row is not left hanging off to one side.
|
||||
var slots: Array = _slots()
|
||||
var spread: float = 0.0
|
||||
for slot in slots:
|
||||
spread += float(slot[1])
|
||||
if absf(spread) > 0.01:
|
||||
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (the opening row is off-centre by %.1f)" % spread)
|
||||
quit(1)
|
||||
return true
|
||||
print("nothing researched -> only the mothership, centred")
|
||||
print("nothing researched -> the mothership and a Fleet Office, centred")
|
||||
|
||||
# --- 2. research three more ---------------------------------
|
||||
game_state.unlocked = ["shipyards", "logistics", "coordination"]
|
||||
|
|
@ -101,9 +117,11 @@ func _process(delta: float) -> bool:
|
|||
stage = 1
|
||||
return false
|
||||
1:
|
||||
# Three doctrines add three structures to the two the opening
|
||||
# already offers (#219's Fleet Office is one of those two).
|
||||
var shown2: Array = _shown()
|
||||
if shown2.size() != 4:
|
||||
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (researched 3, expected 4 entries, got %d)" % shown2.size())
|
||||
if shown2.size() != 5:
|
||||
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (researched 3, expected 5 entries, got %d)" % shown2.size())
|
||||
quit(1)
|
||||
return true
|
||||
for e in shown2:
|
||||
|
|
@ -117,23 +135,30 @@ func _process(delta: float) -> bool:
|
|||
var columns: int = controller.menu_columns(slots.size())
|
||||
var rows: int = controller.menu_rows(slots.size())
|
||||
if columns != 2:
|
||||
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (4 entries laid out in %d columns)" % columns)
|
||||
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (%d entries laid out in %d columns)" \
|
||||
% [slots.size(), columns])
|
||||
quit(1)
|
||||
return true
|
||||
for i in range(slots.size()):
|
||||
var want_row: float = controller.menu_row_top(i / columns, rows, columns)
|
||||
var row: int = i / columns
|
||||
var want_row: float = controller.menu_row_top(row, rows, columns)
|
||||
if absf(slots[i][0] - want_row) > 0.01:
|
||||
print("slot %d at y=%.1f, expected row %d at y=%.1f" % [i, slots[i][0], i / columns, want_row])
|
||||
print("slot %d at y=%.1f, expected row %d at y=%.1f" % [i, slots[i][0], row, want_row])
|
||||
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (a gap was left where a hidden entry used to be)")
|
||||
quit(1)
|
||||
return true
|
||||
# Each ROW is centred on however many it holds, so a final row
|
||||
# with one entry sits in the middle rather than in column 0.
|
||||
# Written the way the layout writes it, so an odd count is not
|
||||
# a special case here that is ordinary there.
|
||||
var in_row: int = mini(slots.size() - row * columns, columns)
|
||||
var spacing: float = controller.menu_column_spacing(columns)
|
||||
var want_x: float = -spacing * 0.5 if i % 2 == 0 else spacing * 0.5
|
||||
var want_x: float = (float(i % columns) - (float(in_row) - 1.0) * 0.5) * spacing
|
||||
if absf(slots[i][1] - want_x) > 0.01:
|
||||
print("TEST_BUILD_MENU_HIDES_LOCKED: FAIL (entry %d at x=%.1f, expected %.1f)" % [i, slots[i][1], want_x])
|
||||
quit(1)
|
||||
return true
|
||||
print("4 entries packed into consecutive slots, no gaps")
|
||||
print("%d entries packed into consecutive slots, no gaps" % slots.size())
|
||||
|
||||
# --- 6. #136: nothing overlaps, with EVERY structure on offer -
|
||||
# The bug the ticket reported: the row list ran out and the last
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func _initialize() -> void:
|
|||
# Real build times are 10s+; the timed path is what matters here, not the
|
||||
# wall clock. See GameState.build_time_scale.
|
||||
game_state.build_time_scale = 0.02
|
||||
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
||||
game_state.unlocked = ["shipyards", "coordination"]
|
||||
scene = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
fleet = scene.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ func _initialize() -> void:
|
|||
game_state.build_time_scale = 0.02
|
||||
game_state.rocks = 500
|
||||
game_state.research_points = 1
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
scene = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
fleet = scene.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ func _initialize() -> void:
|
|||
# real-tap checks below. A full fleet correctly DISABLES it, and a
|
||||
# disabled button not reacting to a tap is not the bug those checks are
|
||||
# looking for.
|
||||
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
||||
game_state.unlocked = ["shipyards", "coordination"]
|
||||
game_state.rocks = 100
|
||||
game_state.fleet_cargo = 0
|
||||
game_state.stored_vessels.clear()
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ func _initialize() -> void:
|
|||
game_state.reset_formation()
|
||||
game_state.intro_active = false
|
||||
game_state.vessel_levels["unlock_distorder"] = 1
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
scene = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
current_scene = scene
|
||||
|
|
|
|||
|
|
@ -45,8 +45,18 @@ func _process(delta: float) -> bool:
|
|||
for skill in GameState.SKILL_TREE:
|
||||
if not state.is_unlocked(String(skill.id)):
|
||||
return _fail("unlock-all missed %s" % skill.id)
|
||||
# #219: capacity is Fleet Offices, not research, so unlocking the
|
||||
# whole tree must NOT move it. The sandbox tool is about doctrines.
|
||||
if state.max_fleet_size() != GameState.BASE_FLEET_SIZE:
|
||||
return _fail("unlock-all moved the fleet cap to %d - capacity is not research any more" \
|
||||
% state.max_fleet_size())
|
||||
# The berths the rest of this test needs come from offices.
|
||||
for berth in range(GameState.DEFAULT_FORMATION.size() - GameState.BASE_FLEET_SIZE):
|
||||
state.add_building(GameState.BUILDING_FLEET_OFFICE,
|
||||
Vector2(3000.0 + 300.0 * float(berth), 3000.0))
|
||||
if state.max_fleet_size() != GameState.DEFAULT_FORMATION.size():
|
||||
return _fail("unlock-all did not expose the complete fleet capacity")
|
||||
return _fail("offices for every formation position gave %d berths" \
|
||||
% state.max_fleet_size())
|
||||
|
||||
for definition in VESSEL_DEFINITIONS.fleet():
|
||||
menu.fleet_add_requested.emit(String(definition.definition_id))
|
||||
|
|
|
|||
|
|
@ -109,6 +109,10 @@ func _process(delta: float) -> bool:
|
|||
# --- C continued: the SpeedFader visibly follows it down ----------
|
||||
if elapsed < 0.2:
|
||||
return false
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ func _initialize() -> void:
|
|||
game_state.reset_progression()
|
||||
game_state.vessel_levels["unlock_distorder"] = 1
|
||||
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 500)
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
|
@ -47,8 +51,6 @@ func _process(delta: float) -> bool:
|
|||
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]))
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ var started: bool = false
|
|||
|
||||
|
||||
func _initialize() -> void:
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
var packed: PackedScene = load("res://world/Main.tscn")
|
||||
main_scene = packed.instantiate()
|
||||
root.add_child(main_scene)
|
||||
|
|
@ -42,7 +46,6 @@ func _physics_process(delta: float) -> bool:
|
|||
# can be undone by the engine's deferred tree-entry finalization. A
|
||||
# new game flies one vessel into three slots now, and this test needs
|
||||
# a full one.
|
||||
root.get_node("GameState").unlocked.append_array(["wing", "squadron", "armada"])
|
||||
fleet.spawn_ships(5, Vector2.ZERO)
|
||||
target_ship = fleet.ships[0]
|
||||
var enemy_scene: PackedScene = load("res://enemies/Enemy.tscn")
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ var started: bool = false
|
|||
|
||||
|
||||
func _initialize() -> void:
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
var packed: PackedScene = load("res://world/Main.tscn")
|
||||
main_scene = packed.instantiate()
|
||||
root.add_child(main_scene)
|
||||
|
|
@ -43,7 +47,6 @@ func _physics_process(delta: float) -> bool:
|
|||
# can be undone by the engine's deferred tree-entry finalization. A
|
||||
# new game flies one vessel into three slots now, and this test needs
|
||||
# a full one.
|
||||
root.get_node("GameState").unlocked.append_array(["wing", "squadron", "armada"])
|
||||
fleet.spawn_ships(5, Vector2.ZERO)
|
||||
var enemy_scene: PackedScene = load("res://enemies/Enemy.tscn")
|
||||
enemy = enemy_scene.instantiate()
|
||||
|
|
|
|||
|
|
@ -56,7 +56,13 @@ func _initialize() -> void:
|
|||
beam_script = load("res://enemies/EnemyBigLaserBeam.gd")
|
||||
# Four vessels are crewed below, past BASE_FLEET_SIZE's 3 slots, so the
|
||||
# capacity doctrines are granted rather than assumed.
|
||||
game_state.unlocked.append_array(["wing", "squadron", "armada"])
|
||||
# #219: berths come from Fleet Offices now, not from research. Three of
|
||||
# them buy the same four-vessel fleet the doctrines used to. Left
|
||||
# unstocked deliberately - upkeep is not charged for the first
|
||||
# 25 seconds, and nothing here runs that long.
|
||||
for _berth in range(3):
|
||||
game_state.add_building(GameState.BUILDING_FLEET_OFFICE,
|
||||
Vector2(3000.0 + 400.0 * float(_berth), 3000.0))
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ func _initialize() -> void:
|
|||
game_state.build_time_scale = 0.02
|
||||
game_state.reset_progression()
|
||||
game_state.rocks = 2000
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
scene = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
fleet = scene.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ func _initialize() -> void:
|
|||
state.vessel_levels["unlock_distorder"] = 1
|
||||
state.fleet_inventory.total_capacity = -1
|
||||
state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, DASH_COST * 3)
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
current_scene = main
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ func _initialize() -> void:
|
|||
game_state.rocks = 500
|
||||
game_state.fleet_cargo = 0
|
||||
mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
scene = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
current_scene = scene
|
||||
|
|
|
|||
|
|
@ -2,31 +2,33 @@ 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.
|
||||
# A new game opens with a SINGLE vessel and a single berth, and the fleet
|
||||
# grows by building Fleet Offices - one berth each, each of which has to be
|
||||
# kept supplied.
|
||||
#
|
||||
# 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.
|
||||
# Forgejo #219 replaced the fleet-size doctrine chain with this. Researching
|
||||
# a permanent +1 asked nothing of the player afterwards; a berth that has to
|
||||
# be fed makes fleet size a standing cost against their supply lines rather
|
||||
# than a purchase they make once.
|
||||
#
|
||||
# BASE_FLEET_SIZE and STARTING_SHIP_COUNT remain separate constants - the
|
||||
# first is capacity, the second is how many hulls are actually flying - and
|
||||
# this test still 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"]
|
||||
# A. a new game starts with one vessel and one berth, and no research
|
||||
# anywhere in the tree can raise that
|
||||
# B. each supplied office adds exactly one berth, and the cap bites
|
||||
# C. an office that cannot pay loses its vessel, and says so
|
||||
# D. a restocked office goes back to work without being rebuilt
|
||||
# E. capacity can never outrun the formation's defined positions
|
||||
|
||||
var game_state: Node
|
||||
var scene: Node
|
||||
var fleet: Node
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var office_ids: Array = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
|
|
@ -41,6 +43,12 @@ func _initialize() -> void:
|
|||
game_state.reset_quests()
|
||||
|
||||
|
||||
func _stock(office_id: int, amount: int) -> void:
|
||||
var inventory: RefCounted = game_state.building_inventory(office_id)
|
||||
for resource_type in game_state.resource_types():
|
||||
inventory.set_amount(String(resource_type), amount)
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
match stage:
|
||||
|
|
@ -48,74 +56,43 @@ func _process(delta: float) -> bool:
|
|||
# --- 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])
|
||||
if GameState.BASE_FLEET_SIZE != 1:
|
||||
return _fail("a new game opens with %d berths, expected the one that needs no office" \
|
||||
% GameState.BASE_FLEET_SIZE)
|
||||
if game_state.max_fleet_size() != 1:
|
||||
return _fail("an unbuilt fleet caps at %d, expected 1" % game_state.max_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
|
||||
# The whole tree, researched, must not add a single berth: #219
|
||||
# moved fleet size out of research entirely.
|
||||
for node in GameState.SKILL_TREE:
|
||||
if int(node.get("fleet_bonus", 0)) != 0:
|
||||
return _fail("skill node '%s' still grants fleet capacity" % node.id)
|
||||
game_state.unlocked.append(String(node.id))
|
||||
if game_state.max_fleet_size() != 1:
|
||||
return _fail("researching the entire tree raised the fleet cap to %d" \
|
||||
% game_state.max_fleet_size())
|
||||
print("a new game: 1 vessel, 1 berth, and no research anywhere raises it")
|
||||
|
||||
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])
|
||||
# --- B. offices are the berths -------------------------------
|
||||
for i in range(3):
|
||||
var id: int = game_state.add_building(GameState.BUILDING_FLEET_OFFICE,
|
||||
Vector2(400.0 * float(i + 1), 0.0))
|
||||
office_ids.append(id)
|
||||
_stock(id, 500)
|
||||
if game_state.max_fleet_size() != GameState.BASE_FLEET_SIZE + i + 1:
|
||||
return _fail("%d offices gave %d berths, expected %d" % [
|
||||
i + 1, game_state.max_fleet_size(),
|
||||
GameState.BASE_FLEET_SIZE + i + 1])
|
||||
print("3 supplied offices -> %d berths" % game_state.max_fleet_size())
|
||||
|
||||
# --- 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()])
|
||||
# --- E. and never more than the formation can seat ------------
|
||||
if game_state.max_fleet_size() > GameState.DEFAULT_FORMATION.size():
|
||||
return _fail("capacity %d exceeds the %d positions the formation defines" \
|
||||
% [game_state.max_fleet_size(), 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)
|
||||
current_scene = scene
|
||||
fleet = scene.get_node("Fleet")
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
|
|
@ -123,27 +100,70 @@ func _process(delta: float) -> bool:
|
|||
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):
|
||||
# The berths the offices bought are fillable, and the cap bites
|
||||
# exactly one past them.
|
||||
for i in range(3):
|
||||
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.
|
||||
return _fail("could not crew the berth bought by office %d" % (i + 1))
|
||||
if fleet.ships.size() != 4:
|
||||
return _fail("the fleet holds %d after filling its berths, expected 4" \
|
||||
% fleet.ships.size())
|
||||
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])
|
||||
return _fail("a 5th vessel joined a fleet with 4 berths")
|
||||
print("the fleet fills to %d and refuses a 5th" % fleet.ships.size())
|
||||
|
||||
# --- C. an office that cannot pay loses its vessel ------------
|
||||
# Emptied and its clock wound past due, so the next tick bills it.
|
||||
_stock(office_ids[2], 0)
|
||||
var starved: Dictionary = game_state.find_building(int(office_ids[2]))
|
||||
starved.upkeep_progress = GameState.FLEET_OFFICE_UPKEEP_SECONDS
|
||||
game_state.take_quest_messages()
|
||||
var before: int = fleet.ships.size()
|
||||
game_state._advance_fleet_office_upkeep(0.1)
|
||||
if not bool(starved.get("starved", false)):
|
||||
return _fail("an office with an empty store kept paying")
|
||||
if game_state.max_fleet_size() != 3:
|
||||
return _fail("a starved office still counts: %d berths" % game_state.max_fleet_size())
|
||||
if game_state.take_quest_messages().is_empty():
|
||||
return _fail("the fleet lost a vessel to supply and nobody was told")
|
||||
elapsed = 0.0
|
||||
_lost_before = before
|
||||
stage = 2
|
||||
return false
|
||||
2:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
if fleet.ships.size() != _lost_before - 1:
|
||||
return _fail("a starved office left %d vessels flying, expected %d" \
|
||||
% [fleet.ships.size(), _lost_before - 1])
|
||||
print("a dry office destroyed its vessel and warned the player")
|
||||
|
||||
# --- D. the berth comes back, and a supplied office keeps it -
|
||||
# The office is idle now that its vessel is gone, so the berth is
|
||||
# offered again on the next tick - an office that could never be
|
||||
# crewed again could never pay again either.
|
||||
var revived: Dictionary = game_state.find_building(int(office_ids[2]))
|
||||
game_state._advance_fleet_office_upkeep(0.1)
|
||||
if bool(revived.get("starved", false)):
|
||||
return _fail("an office with an empty berth stayed disabled")
|
||||
if game_state.max_fleet_size() != 4:
|
||||
return _fail("an emptied office did not offer its berth again")
|
||||
|
||||
# Restocked and re-crewed, it pays and the vessel stays.
|
||||
_stock(office_ids[2], 500)
|
||||
if not fleet.add_ship(GameState.VESSEL_INTERCEPTOR):
|
||||
return _fail("could not re-crew the berth after restocking")
|
||||
revived.upkeep_progress = GameState.FLEET_OFFICE_UPKEEP_SECONDS
|
||||
game_state._advance_fleet_office_upkeep(0.1)
|
||||
if bool(revived.get("starved", false)):
|
||||
return _fail("a restocked office starved with a full store")
|
||||
if fleet.ships.size() != 4:
|
||||
return _fail("a supplied office lost its vessel anyway")
|
||||
print("restocked and re-crewed: the office pays and the vessel stays")
|
||||
|
||||
print("TEST_FLEET_GROWTH: PASS")
|
||||
quit(0)
|
||||
|
|
@ -151,6 +171,9 @@ func _process(delta: float) -> bool:
|
|||
return false
|
||||
|
||||
|
||||
var _lost_before: int = 0
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_FLEET_GROWTH: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@ func _initialize() -> void:
|
|||
return
|
||||
|
||||
game_state.fleet_gas_cargo = 20
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ func _initialize() -> void:
|
|||
game_state.rocks = 0
|
||||
game_state.gas = 0
|
||||
game_state.fleet_gas_cargo = 0
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ func _initialize() -> void:
|
|||
state.vessel_levels["unlock_gas_bomber"] = 1
|
||||
# #60: shots draw from the fleet hold now - stock enough for both fires.
|
||||
state.fleet_gas_cargo = 20
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ func _initialize() -> void:
|
|||
game_state.transporters.clear()
|
||||
game_state.stored_vessels.clear()
|
||||
game_state.rocks = 100
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
scene = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
fleet = scene.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ func _process(delta: float) -> bool:
|
|||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
scene = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
current_scene = scene
|
||||
|
|
@ -124,9 +128,17 @@ func _process(delta: float) -> bool:
|
|||
if panels.is_empty():
|
||||
return _fail("no modal panels found to be drawn over")
|
||||
for menu in panels:
|
||||
if menu.get_index() > banner_at:
|
||||
return _fail("%s is a later child than the dialog, so it draws on top of it" \
|
||||
% menu.name)
|
||||
# A panel nested inside another panel - the build menu's own
|
||||
# info popup - indexes against ITS parent, not the UI layer,
|
||||
# so comparing that number against the banner's is comparing
|
||||
# two different sibling lists. What actually has to sit under
|
||||
# the banner is the UI-level ancestor it is drawn inside.
|
||||
var top_level: Node = menu
|
||||
while top_level.get_parent() != ui:
|
||||
top_level = top_level.get_parent()
|
||||
if top_level.get_index() > banner_at:
|
||||
return _fail("%s is drawn inside %s, a later child than the dialog" \
|
||||
% [menu.name, top_level.name])
|
||||
if banner_at != ui.get_child_count() - 1:
|
||||
return _fail("the dialog is child %d of %d - something still draws over it" \
|
||||
% [banner_at, ui.get_child_count()])
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ func _initialize() -> void:
|
|||
game_state.rocks = 0
|
||||
game_state.gas = 0
|
||||
game_state.fleet_gas_cargo = 0
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ func _initialize() -> void:
|
|||
# fleet composition exists), so this deposit can't be clamped by the
|
||||
# soon-to-exist all-laser fleet having zero "cargo" capability.
|
||||
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 20)
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ func _initialize() -> void:
|
|||
game_state.fleet_cargo = 0
|
||||
game_state.stored_vessels.clear()
|
||||
game_state.build_time_scale = 0.02
|
||||
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
||||
game_state.unlocked = ["shipyards", "coordination"]
|
||||
scene = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
fleet = scene.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ func _initialize() -> void:
|
|||
game_state.reset_formation()
|
||||
game_state.intro_active = false
|
||||
game_state.rocks = 5000
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
scene = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
current_scene = scene
|
||||
|
|
|
|||
|
|
@ -51,7 +51,13 @@ func _process(delta: float) -> bool:
|
|||
# same reason the set_process(false) calls above sit at this
|
||||
# point. A new game flies one vessel now, and the checks below
|
||||
# index ships[1].
|
||||
game_state.unlocked.append_array(["wing", "squadron", "armada"])
|
||||
# #219: berths come from Fleet Offices now, not from research. Three of
|
||||
# them buy the same four-vessel fleet the doctrines used to. Left
|
||||
# unstocked deliberately - upkeep is not charged for the first
|
||||
# 25 seconds, and nothing here runs that long.
|
||||
for _berth in range(3):
|
||||
game_state.add_building(GameState.BUILDING_FLEET_OFFICE,
|
||||
Vector2(3000.0 + 400.0 * float(_berth), 3000.0))
|
||||
fleet.spawn_ships(5, Vector2.ZERO)
|
||||
# Hold the button down without releasing it.
|
||||
angle_at_hold_start = _angle()
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ func _initialize() -> void:
|
|||
return
|
||||
|
||||
game_state = root.get_node("GameState")
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
var packed: PackedScene = load("res://world/Main.tscn")
|
||||
main_scene = packed.instantiate()
|
||||
root.add_child(main_scene)
|
||||
|
|
@ -68,7 +72,6 @@ func _initialize() -> void:
|
|||
# (3) slots. This test needs a crewed fleet, so it asks for one outright
|
||||
# rather than inheriting whatever a fresh game happens to open with -
|
||||
# which is exactly what changed underneath it.
|
||||
root.get_node("GameState").unlocked.append_array(["wing", "squadron", "armada"])
|
||||
fleet.spawn_ships(5, Vector2.ZERO)
|
||||
planet_field = main_scene.get_node("PlanetField")
|
||||
landing_controller = main_scene.get_node("LandingController")
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ func _initialize() -> void:
|
|||
quit(1)
|
||||
return
|
||||
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
var packed: PackedScene = load("res://world/Main.tscn")
|
||||
main_scene = packed.instantiate()
|
||||
root.add_child(main_scene)
|
||||
|
|
@ -67,7 +71,6 @@ func _process(delta: float) -> bool:
|
|||
# there can be undone by the engine's deferred tree-entry
|
||||
# finalization, the same reason the set_process(false) calls
|
||||
# above live at this point. A new game flies one vessel now.
|
||||
root.get_node("GameState").unlocked.append_array(["wing", "squadron", "armada"])
|
||||
fleet.spawn_ships(5, Vector2.ZERO)
|
||||
# Silencing the field does not remove what it already placed, and
|
||||
# those keep drifting - a rock taking the fleet out mid-test reads
|
||||
|
|
|
|||
1
tests/test_planet_scanner.gd.uid
Normal file
1
tests/test_planet_scanner.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bthdk3qrxamks
|
||||
|
|
@ -51,7 +51,13 @@ func _initialize() -> void:
|
|||
game_state.rocks = 100
|
||||
game_state.fleet_cargo = 0
|
||||
game_state.stored_vessels.clear()
|
||||
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
||||
game_state.unlocked = ["shipyards", "coordination"]
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
# Eight, which is what every doctrine used to allow: the amounts below are
|
||||
# hand-picked to sit under a hold that size (8 interceptors x 20 = 160).
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 8)
|
||||
scene = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
fleet = scene.get_node("Fleet")
|
||||
|
|
@ -59,7 +65,6 @@ func _initialize() -> void:
|
|||
# (3) slots. This test needs a crewed fleet, so it asks for one outright
|
||||
# rather than inheriting whatever a fresh game happens to open with -
|
||||
# which is exactly what changed underneath it.
|
||||
root.get_node("GameState").unlocked.append_array(["wing", "squadron", "armada"])
|
||||
fleet.spawn_ships(5, Vector2.ZERO)
|
||||
controller = scene.get_node("BuildController")
|
||||
|
||||
|
|
@ -96,9 +101,9 @@ func _process(delta: float) -> bool:
|
|||
_clear_hazards()
|
||||
# #56 gave the hold a finite, composition-driven capacity. This
|
||||
# test is about the transfer UI, not that cap - test_fleet_cargo_
|
||||
# limit.gd owns the cap itself - so it spawns the largest fleet
|
||||
# every unlocked doctrine allows (8 interceptors x 20 = 160),
|
||||
# comfortably above every hand-picked amount below.
|
||||
# limit.gd owns the cap itself - so it spawns into every berth the
|
||||
# offices above bought (8 interceptors x 20 = 160), comfortably
|
||||
# above every hand-picked amount below.
|
||||
fleet.spawn_ships(game_state.max_fleet_size(), MOTHERSHIP_AT)
|
||||
var field: Node = scene.get_node("BuildingField")
|
||||
field.place(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
||||
|
|
|
|||
|
|
@ -107,7 +107,13 @@ func _process(delta: float) -> bool:
|
|||
# Crafting adds a FOURTH vessel below, one past the 3 slots a
|
||||
# new game has, so make room first - the subject here is the
|
||||
# crafter, not the fleet cap.
|
||||
game_state.unlocked.append_array(["wing", "squadron", "armada"])
|
||||
# #219: berths come from Fleet Offices now, not from research. Three of
|
||||
# them buy the same four-vessel fleet the doctrines used to. Left
|
||||
# unstocked deliberately - upkeep is not charged for the first
|
||||
# 25 seconds, and nothing here runs that long.
|
||||
for _berth in range(3):
|
||||
game_state.add_building(GameState.BUILDING_FLEET_OFFICE,
|
||||
Vector2(3000.0 + 400.0 * float(_berth), 3000.0))
|
||||
fleet.spawn_ships(3, Vector2.ZERO)
|
||||
var before_ships: int = fleet.ships.size()
|
||||
var before_rocks: int = game_state.rocks
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ func _initialize() -> void:
|
|||
quit(1)
|
||||
return
|
||||
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
var packed: PackedScene = load("res://world/Main.tscn")
|
||||
main_scene = packed.instantiate()
|
||||
root.add_child(main_scene)
|
||||
|
|
@ -63,7 +67,6 @@ func _process(delta: float) -> bool:
|
|||
# there can be undone by the engine's deferred tree-entry
|
||||
# finalization, the same reason the set_process(false) calls
|
||||
# above live at this point. A new game flies one vessel now.
|
||||
root.get_node("GameState").unlocked.append_array(["wing", "squadron", "armada"])
|
||||
fleet.spawn_ships(5, Vector2.ZERO)
|
||||
# Silencing the field does not remove what it already placed, and
|
||||
# those keep drifting - a rock taking the fleet out mid-test reads
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ func _initialize() -> void:
|
|||
game_state.build_time_scale = 0.02
|
||||
game_state.reset_progression()
|
||||
game_state.rocks = 2000
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
scene = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
fleet = scene.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ func _process(delta: float) -> bool:
|
|||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
scene = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
current_scene = scene
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ func _initialize() -> void:
|
|||
game_state.reset_progression()
|
||||
game_state.rocks = 1200
|
||||
game_state.fleet_cargo = 0
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
scene = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
fleet = scene.get_node("Fleet")
|
||||
|
|
@ -81,7 +85,7 @@ func _process(delta: float) -> bool:
|
|||
return true
|
||||
print("no lab -> no upgrades")
|
||||
|
||||
game_state.unlocked = ["shipyards", "vessel_science", "coordination", "wing", "squadron"]
|
||||
game_state.unlocked = ["shipyards", "vessel_science", "coordination"]
|
||||
game_state.research_points = 10
|
||||
field.place(GameState.BUILDING_SHIP_LAB, Vector2(-700.0, 0.0))
|
||||
lab_id = int(game_state.buildings[-1].id)
|
||||
|
|
|
|||
|
|
@ -126,6 +126,18 @@ func _process(delta: float) -> bool:
|
|||
print("a second click on the same node researches it")
|
||||
|
||||
# --- C. held drag scrolls, a bare hover does not --------------------
|
||||
# #219 removed the eight-deep doctrine lane and the tree now fits its own
|
||||
# view, so there is nothing left to scroll. What is under test here is the
|
||||
# INPUT rule - drag only while the button is held, wheel both ways - not
|
||||
# how tall the tree happens to be, so the view is shortened to give the
|
||||
# gesture something to move. Scrolled to the bottom first, the way the
|
||||
# menu opens when the content does overflow.
|
||||
var authored_height: float = tree_view.size.y
|
||||
tree_view.size.y = authored_height * 0.4
|
||||
tree_view.scroll_y = tree_view._max_scroll()
|
||||
if tree_view.scroll_y <= 0.0:
|
||||
return _fail("the tree still has nothing to scroll, so this proves nothing")
|
||||
|
||||
var before: float = tree_view.scroll_y
|
||||
_move(Vector2(0.0, 120.0))
|
||||
if not is_equal_approx(tree_view.scroll_y, before):
|
||||
|
|
@ -153,6 +165,10 @@ func _process(delta: float) -> bool:
|
|||
print("the wheel scrolls both ways")
|
||||
|
||||
# --- E. the panel has no dead band at the bottom --------------------
|
||||
# Back to the authored height: section C shortened the view to have
|
||||
# something to scroll, and the layout below is about the real one.
|
||||
tree_view.size.y = authored_height
|
||||
tree_view.scroll_y = 0.0
|
||||
var detail: Control = scene.get_node("UI/SkillMenu/DetailLabel")
|
||||
var status: Control = scene.get_node("UI/SkillMenu/StatusLabel")
|
||||
var tree_bottom: float = tree_view.position.y + tree_view.size.y
|
||||
|
|
|
|||
|
|
@ -131,19 +131,34 @@ func _process(delta: float) -> bool:
|
|||
print("TEST_SKILL_TREE: FAIL (freight branch does not rise vertically)")
|
||||
quit(1)
|
||||
return true
|
||||
var before_scroll: float = controller.skill_menu.tree.scroll_y
|
||||
var tree_view: Control = controller.skill_menu.tree
|
||||
var before_scroll: float = tree_view.scroll_y
|
||||
var drag: InputEventScreenDrag = InputEventScreenDrag.new()
|
||||
# Start at the root at the bottom; pull the canvas down to reveal
|
||||
# the branches rising above it.
|
||||
drag.relative = Vector2(0.0, 180.0)
|
||||
controller.skill_menu.tree.handle_drag(drag)
|
||||
if controller.skill_menu.tree.scroll_y >= before_scroll:
|
||||
print("TEST_SKILL_TREE: FAIL (tree did not scroll upward)")
|
||||
quit(1)
|
||||
return true
|
||||
tree_view.handle_drag(drag)
|
||||
# #219 removed the eight-deep doctrine lane, which was what made
|
||||
# the canvas taller than the view. A tree that FITS has nothing to
|
||||
# scroll, so what is checked depends on which it is - and the
|
||||
# clamp is worth pinning either way, since a canvas that drifts
|
||||
# off its own top edge is the bug this gesture used to expose.
|
||||
if before_scroll > 0.0:
|
||||
if tree_view.scroll_y >= before_scroll:
|
||||
print("TEST_SKILL_TREE: FAIL (tree did not scroll upward)")
|
||||
quit(1)
|
||||
return true
|
||||
print("tree scrolls: %.0f -> %.0f" % [before_scroll, tree_view.scroll_y])
|
||||
else:
|
||||
if tree_view.scroll_y != 0.0:
|
||||
print("TEST_SKILL_TREE: FAIL (a tree that fits scrolled to %.1f)" \
|
||||
% tree_view.scroll_y)
|
||||
quit(1)
|
||||
return true
|
||||
print("tree fits the view; the pan clamps at the top")
|
||||
|
||||
# --- prerequisites --------------------------------------------
|
||||
if game_state.can_unlock("wing"):
|
||||
if game_state.can_unlock("power_systems"):
|
||||
print("TEST_SKILL_TREE: FAIL (deep node unlockable with no prerequisite)")
|
||||
quit(1)
|
||||
return true
|
||||
|
|
@ -172,16 +187,21 @@ func _process(delta: float) -> bool:
|
|||
scene.get_node("UI/BuildMenu").visible = false
|
||||
|
||||
# --- fleet capacity -------------------------------------------
|
||||
# #219 took capacity out of the tree entirely: a berth is a Fleet
|
||||
# Office standing somewhere. What this checks now is that the
|
||||
# tree does NOT move the cap, and that the structure does.
|
||||
var base_cap: int = game_state.max_fleet_size()
|
||||
game_state.research_points = 2
|
||||
game_state.unlock("coordination")
|
||||
var wing_ok: bool = game_state.unlock("wing")
|
||||
if game_state.max_fleet_size() != base_cap:
|
||||
print("TEST_SKILL_TREE: FAIL (research still moves the fleet cap)")
|
||||
quit(1)
|
||||
return true
|
||||
game_state.add_building(GameState.BUILDING_FLEET_OFFICE, Vector2(2400.0, 0.0))
|
||||
var raised_cap: int = game_state.max_fleet_size()
|
||||
print("fleet cap %d -> %d after Wing Doctrine (unlocked=%s)" % [
|
||||
base_cap, raised_cap, wing_ok
|
||||
])
|
||||
if not (wing_ok and raised_cap == base_cap + 1):
|
||||
print("TEST_SKILL_TREE: FAIL (capacity upgrade did not raise the cap)")
|
||||
print("fleet cap %d -> %d after a Fleet Office" % [base_cap, raised_cap])
|
||||
if raised_cap != base_cap + 1:
|
||||
print("TEST_SKILL_TREE: FAIL (a Fleet Office did not raise the cap)")
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ func _initialize() -> void:
|
|||
game_state.build_time_scale = 0.02
|
||||
game_state.reset_progression()
|
||||
game_state.rocks = 2000
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ func _initialize() -> void:
|
|||
# hold has no capacity cap yet at this point, so this can't be clamped
|
||||
# by the soon-to-exist fleet's cargo capability.
|
||||
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 500)
|
||||
# #219: fleet capacity is built now - one berth free, one per Fleet
|
||||
# Office. This test flies more than one vessel, so it needs the offices
|
||||
# standing before the fleet spawns into them.
|
||||
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
|
|
|||
|
|
@ -126,20 +126,22 @@ func _state_cases() -> bool:
|
|||
var patient: int = game_state.wave_number
|
||||
for wave in [1, 5, 20, 60]:
|
||||
game_state.wave_number = wave
|
||||
for size in range(1, GameState.BASE_FLEET_SIZE + 1):
|
||||
for size in range(1, 4):
|
||||
for id in game_state.wave_composition(size):
|
||||
if id != "chaser":
|
||||
game_state.wave_number = patient
|
||||
return _fail("wave %d against a fleet of %d fields a %s" \
|
||||
% [wave, size, id])
|
||||
game_state.wave_number = patient
|
||||
print("a fleet of %d or fewer meets chasers only, at every wave number" \
|
||||
% GameState.BASE_FLEET_SIZE)
|
||||
print("a fleet of 3 or fewer meets chasers only, at every wave number")
|
||||
|
||||
# And each vessel past the starting three opens exactly one more kind, so
|
||||
# the ramp is progressive rather than a cliff.
|
||||
# Explicit sizes rather than BASE_FLEET_SIZE + n: #219 moved the starting
|
||||
# capacity to 1, and this is about the roster's own thresholds (4, 5, 6),
|
||||
# not about where the fleet happens to begin.
|
||||
var previous_kinds: int = 1
|
||||
for size in range(GameState.BASE_FLEET_SIZE, GameState.BASE_FLEET_SIZE + 4):
|
||||
for size in range(3, 8):
|
||||
var kinds_here: Dictionary = {}
|
||||
for id in game_state.wave_composition(size):
|
||||
kinds_here[id] = true
|
||||
|
|
|
|||
Loading…
Reference in a new issue