Landed as one piece across #122, #123 and #124: the frame is useless without a script, and the script has nowhere to run without the frame. The director drives the REAL scene rather than being a scene of its own. It wants the real fleet, the real enemies and the real asteroid field, and it makes "then the game starts" cost nothing - the director stops steering, clears the flag, and what is left running is the ordinary game. The script is a list of beats, each with a deadline as well as its own completion test, so a beat whose condition is never met - a player who refuses to fly at the ambush - runs out rather than wedging the prologue. skip() ends the whole thing at any point and lands in exactly the state playing it through does, so the game has one way to start rather than two that have to be kept in step. The test pins that equivalence. "No action button available, only movements" is enforced where the buttons are decided, not by the director reaching in: the ability controllers set their own visibility every frame and would have put them straight back, and the map button belongs to the map. The joystick, the pivot and the fader stay. The convoy is ten hulls where a new game's fleet is one, so it needs an explicit fleet-size override - a plain number rather than a fake doctrine, because nothing about the player's own progression should be bent to fit a cutscene. It is carriers rather than transporters: transporters belong to hubs and are not fleet-eligible, so a convoy of them would be silently filtered down to its escorts. The ambush is chasers and only chasers. "Easy enough" is the requirement, and a sniper against a convoy the player cannot yet fight properly is neither easy nor a good first impression. The breach takes the wheel through one check in GameController, covering the stick, the keyboard and the released-again path together - three separate gates would be three chances for one of them to keep steering. The shake is written to camera.offset, never to its position, because GameController owns where the camera is and would smooth a shake away. The vessel that survives is chosen as the interceptor rather than whichever happened to be first in the list, and the prologue ends on exactly a new game's opening position. Also: PlanetAtmosphereController no longer spawns the opening fleet while the prologue is arming, and the tests that are about what the GAME starts from now say so by turning the prologue off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
120 lines
5.3 KiB
GDScript
120 lines
5.3 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless test: run with
|
|
# godot --headless -s res://tests/test_lab_research_reward.gd
|
|
# A harder enemy research lab should be worth more research points, and how
|
|
# hard a lab is depends on the layout authored for it - how many capture
|
|
# points, how many defenders - which the planet TYPE cannot know. So the
|
|
# reward is a per-map value set in the map editor, falling back to the
|
|
# planet type's flat rate when a setup does not express an opinion.
|
|
#
|
|
# Covers:
|
|
# A. a setup carries its own reward and it survives a save/load round trip
|
|
# B. a map's reward overrides the planet type's flat rate on conquest
|
|
# C. a setup that says nothing (0) still gets the type's rate, so every
|
|
# map authored before this existed keeps its old value
|
|
# D. the editor's control cycles the value and reports it
|
|
|
|
var game_state: Node
|
|
var editor: 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.
|
|
game_state.intro_active = false
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
elapsed += delta
|
|
match stage:
|
|
0:
|
|
# --- A. the field exists and round-trips ---------------------
|
|
var setup: Resource = load("res://surface/PlanetSetup.gd").new()
|
|
setup.setup_id = "reward_probe"
|
|
setup.display_name = "Reward Probe"
|
|
setup.surface_profile_id = "rocky"
|
|
setup.capture_points = PackedVector2Array([Vector2(10, 10), Vector2(-10, -10)])
|
|
setup.research_point_reward = 5
|
|
var path: String = "user://test_reward_probe.tres"
|
|
if ResourceSaver.save(setup, path) != OK:
|
|
return _fail("could not save a setup carrying a research reward")
|
|
var reloaded: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
if reloaded == null or int(reloaded.research_point_reward) != 5:
|
|
return _fail("the reward did not survive a save/load round trip")
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
|
print("a setup carries its own reward (%d) across a save/load" % reloaded.research_point_reward)
|
|
|
|
# --- B. the map's reward beats the planet type's rate --------
|
|
# A research_lab planet's own definition pays a flat 1.
|
|
var flat_rate: int = int(load("res://planets/ResearchLabPlanetDefinition.tres").conquest_research_points)
|
|
game_state.research_points = 0
|
|
game_state.register_capture_points("hard_lab", 2, "research_lab", 6)
|
|
game_state.mark_point_captured("hard_lab", 0)
|
|
game_state.mark_point_captured("hard_lab", 1)
|
|
if game_state.research_points != 6:
|
|
return _fail("a hard lab authored at 6 points awarded %d" % game_state.research_points)
|
|
if flat_rate >= 6:
|
|
return _fail("the planet type's flat rate (%d) is not below the authored one - this test proves nothing" % flat_rate)
|
|
print("a hard lab authored at 6 pays 6, over the planet type's flat %d" % flat_rate)
|
|
|
|
# --- C. silence still means the type's rate ------------------
|
|
game_state.research_points = 0
|
|
game_state.register_capture_points("plain_lab", 1, "research_lab", 0)
|
|
game_state.mark_point_captured("plain_lab", 0)
|
|
if game_state.research_points != flat_rate:
|
|
return _fail("a setup with no authored reward paid %d, expected the type's %d" \
|
|
% [game_state.research_points, flat_rate])
|
|
print("a setup with no opinion still pays the planet type's %d" % flat_rate)
|
|
|
|
# A harder map must never be worth LESS than an easier one.
|
|
if 6 <= flat_rate:
|
|
return _fail("authoring a harder lab did not raise its worth")
|
|
|
|
editor = load("res://surface/PlanetSetupEditor.tscn").instantiate()
|
|
root.add_child(editor)
|
|
current_scene = editor
|
|
elapsed = 0.0
|
|
stage = 1
|
|
return false
|
|
1:
|
|
# --- D. the editor can set it --------------------------------
|
|
if elapsed < 0.2:
|
|
return false
|
|
editor.new_setup("reward_edit", "Reward Edit", "rocky")
|
|
var seen: Array = []
|
|
for i in range(editor.REWARD_OPTIONS.size()):
|
|
seen.append(int(editor.current_setup.research_point_reward))
|
|
editor._on_reward_pressed()
|
|
# Cycling the whole list must come back round to where it began.
|
|
if int(editor.current_setup.research_point_reward) != seen[0]:
|
|
return _fail("cycling every reward option did not return to the start")
|
|
if seen.size() != editor.REWARD_OPTIONS.size() or seen.max() <= 0:
|
|
return _fail("the reward control did not offer a range of values: %s" % [seen])
|
|
editor.current_setup.research_point_reward = 3
|
|
editor._refresh_reward_label()
|
|
if not "3" in editor.reward_value_label.text:
|
|
return _fail("the editor does not report the chosen reward: '%s'" % editor.reward_value_label.text)
|
|
editor.current_setup.research_point_reward = 0
|
|
editor._refresh_reward_label()
|
|
if not "DEFAULT" in editor.reward_value_label.text:
|
|
return _fail("the editor does not distinguish 'use the planet type's rate': '%s'" \
|
|
% editor.reward_value_label.text)
|
|
print("the map editor cycles %s and reports the choice" % [seen])
|
|
|
|
print("TEST_LAB_RESEARCH_REWARD: PASS")
|
|
quit(0)
|
|
return true
|
|
return false
|
|
|
|
|
|
func _fail(message: String) -> bool:
|
|
print("TEST_LAB_RESEARCH_REWARD: FAIL (%s)" % message)
|
|
quit(1)
|
|
return true
|