228 lines
8.8 KiB
GDScript3
228 lines
8.8 KiB
GDScript3
|
|
extends SceneTree
|
||
|
|
|
||
|
|
# Headless test: run with
|
||
|
|
# godot --headless -s res://tests/test_wave_system.gd
|
||
|
|
# Forgejo #100 (#97b): waves replace the roaming enemy spawner.
|
||
|
|
#
|
||
|
|
# The old EnemySpawner put one enemy on the map every 3-6 seconds for ever.
|
||
|
|
# There was no rhythm to it and nothing to prepare for. Enemies now arrive
|
||
|
|
# in waves: a long quiet, a warning, an attack, and quiet again once the
|
||
|
|
# wave is dead.
|
||
|
|
#
|
||
|
|
# The clock lives in GameState because Main.tscn is destroyed on every trip
|
||
|
|
# to a planet surface - a wave must not be cancelled, nor restarted from
|
||
|
|
# zero, by a landing.
|
||
|
|
#
|
||
|
|
# Covers:
|
||
|
|
# A. the phase machine runs peace -> warning -> active, and a wave that is
|
||
|
|
# killed puts it back to peace
|
||
|
|
# B. no timer is exposed and none is drawn. #97 asks for an alert with no
|
||
|
|
# visible countdown, so the alert's own text must carry no digits and
|
||
|
|
# GameState must not offer a remaining-seconds accessor at all
|
||
|
|
# C. waves get stronger with the FLEET, and with the wave number
|
||
|
|
# D. the trickle is gone: a live director with no wave running leaves the
|
||
|
|
# map empty, where the old spawner would have filled it
|
||
|
|
# E. a launched wave really arrives, around the fleet, and clearing it
|
||
|
|
# returns the world to peace
|
||
|
|
# F. the alert is up during the warning and at no other time
|
||
|
|
|
||
|
|
var game_state: Node
|
||
|
|
var scene: Node
|
||
|
|
var spawner: Node2D
|
||
|
|
var fleet: Node2D
|
||
|
|
var alert: Control
|
||
|
|
var stage: int = 0
|
||
|
|
var elapsed: float = 0.0
|
||
|
|
|
||
|
|
|
||
|
|
func _initialize() -> void:
|
||
|
|
# GameState is an autoload: a bare GameState.<instance member> does not
|
||
|
|
# compile in a SceneTree script, so the node is fetched by name instead.
|
||
|
|
game_state = root.get_node("GameState")
|
||
|
|
game_state.buildings.clear()
|
||
|
|
game_state.extractors.clear()
|
||
|
|
game_state.transporters.clear()
|
||
|
|
game_state.reset_progression()
|
||
|
|
game_state.reset_waves()
|
||
|
|
|
||
|
|
|
||
|
|
func _fail(message: String) -> bool:
|
||
|
|
print("TEST_WAVE_SYSTEM: FAIL (%s)" % message)
|
||
|
|
quit(1)
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
func _state_cases() -> bool:
|
||
|
|
# --- A. the phase machine --------------------------------------------
|
||
|
|
if game_state.wave_phase != GameState.WAVE_PHASE_PEACE:
|
||
|
|
return _fail("a reset game does not open in peace")
|
||
|
|
game_state.advance_waves(GameState.WAVE_PEACE_SECONDS - 0.1)
|
||
|
|
if game_state.wave_phase != GameState.WAVE_PHASE_PEACE:
|
||
|
|
return _fail("the peace ended early")
|
||
|
|
game_state.advance_waves(0.2)
|
||
|
|
if not game_state.wave_is_incoming():
|
||
|
|
return _fail("the peace ran out without a warning")
|
||
|
|
if game_state.wave_number != 0:
|
||
|
|
return _fail("the wave was counted while it was still only a warning")
|
||
|
|
game_state.advance_waves(GameState.WAVE_WARNING_SECONDS - 0.1)
|
||
|
|
if not game_state.wave_is_incoming():
|
||
|
|
return _fail("the warning ended early")
|
||
|
|
game_state.advance_waves(0.2)
|
||
|
|
if game_state.wave_phase != GameState.WAVE_PHASE_ACTIVE:
|
||
|
|
return _fail("the warning did not turn into an attack")
|
||
|
|
if game_state.wave_number != 1 or not game_state.wave_pending:
|
||
|
|
return _fail("wave %d turned active without being owed to anyone" % game_state.wave_number)
|
||
|
|
game_state.wave_cleared()
|
||
|
|
if game_state.wave_phase != GameState.WAVE_PHASE_PEACE or game_state.wave_pending:
|
||
|
|
return _fail("clearing the wave did not return the world to peace")
|
||
|
|
print("peace -> warning -> wave 1 -> peace")
|
||
|
|
|
||
|
|
# --- B. nothing that could be read as a countdown --------------------
|
||
|
|
for forbidden in ["wave_seconds_left", "wave_time_left", "wave_countdown",
|
||
|
|
"seconds_until_wave", "wave_remaining"]:
|
||
|
|
if game_state.has_method(forbidden):
|
||
|
|
return _fail("GameState.%s() exists - the alert could become a timer" % forbidden)
|
||
|
|
game_state.advance_waves(GameState.WAVE_PEACE_SECONDS + 0.1)
|
||
|
|
if not game_state.wave_is_incoming():
|
||
|
|
return _fail("could not get back into a warning for the pulse check")
|
||
|
|
var pulse: float = game_state.wave_warning_pulse()
|
||
|
|
if pulse < 0.0 or pulse > 1.0:
|
||
|
|
return _fail("the warning pulse is %f, outside 0..1" % pulse)
|
||
|
|
print("no remaining-seconds accessor exists; urgency is a %.2f pulse" % pulse)
|
||
|
|
|
||
|
|
# --- C. strength follows the fleet -----------------------------------
|
||
|
|
var small: PackedStringArray = game_state.wave_composition(1)
|
||
|
|
var large: PackedStringArray = game_state.wave_composition(9)
|
||
|
|
if small.is_empty():
|
||
|
|
return _fail("a wave against a single vessel is empty")
|
||
|
|
if large.size() <= small.size():
|
||
|
|
return _fail("a fleet of 9 draws %d enemies, no more than the %d a single vessel draws" \
|
||
|
|
% [large.size(), small.size()])
|
||
|
|
if game_state.wave_strength(9) <= game_state.wave_strength(1):
|
||
|
|
return _fail("wave strength does not rise with fleet size")
|
||
|
|
var before_number: int = game_state.wave_number
|
||
|
|
game_state.wave_number = before_number + 20
|
||
|
|
var later: PackedStringArray = game_state.wave_composition(1)
|
||
|
|
game_state.wave_number = before_number
|
||
|
|
if later.size() <= small.size():
|
||
|
|
return _fail("wave 21 against one vessel is no bigger than wave 1 - there is no escalation")
|
||
|
|
# A big wave must not be the same swarm at greater length.
|
||
|
|
var heavy_kinds: Dictionary = {}
|
||
|
|
for id in large:
|
||
|
|
heavy_kinds[id] = true
|
||
|
|
if heavy_kinds.size() < 2:
|
||
|
|
return _fail("a large wave is %d copies of one enemy" % large.size())
|
||
|
|
if large.size() > GameState.WAVE_MAX_ENEMIES:
|
||
|
|
return _fail("a wave of %d exceeds the %d cap" % [large.size(), GameState.WAVE_MAX_ENEMIES])
|
||
|
|
print("fleet 1 -> %d enemies, fleet 9 -> %d enemies of %d kinds" \
|
||
|
|
% [small.size(), large.size(), heavy_kinds.size()])
|
||
|
|
game_state.reset_waves()
|
||
|
|
return false
|
||
|
|
|
||
|
|
|
||
|
|
func _enemy_count() -> int:
|
||
|
|
var n: int = 0
|
||
|
|
for enemy in get_nodes_in_group("enemies"):
|
||
|
|
if is_instance_valid(enemy) and not enemy.is_queued_for_deletion():
|
||
|
|
n += 1
|
||
|
|
return n
|
||
|
|
|
||
|
|
|
||
|
|
func _process(delta: float) -> bool:
|
||
|
|
elapsed += delta
|
||
|
|
match stage:
|
||
|
|
0:
|
||
|
|
if _state_cases():
|
||
|
|
return true
|
||
|
|
scene = load("res://world/Main.tscn").instantiate()
|
||
|
|
root.add_child(scene)
|
||
|
|
spawner = scene.get_node("EnemySpawner")
|
||
|
|
fleet = scene.get_node("Fleet")
|
||
|
|
alert = scene.get_node("UI/WaveAlert")
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 1
|
||
|
|
return false
|
||
|
|
1:
|
||
|
|
if elapsed < 0.3:
|
||
|
|
return false
|
||
|
|
scene.get_node("AsteroidField").set_process(false)
|
||
|
|
for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies"):
|
||
|
|
if is_instance_valid(hazard):
|
||
|
|
hazard.queue_free()
|
||
|
|
# The director stays ENABLED for this test - it is the thing
|
||
|
|
# under test.
|
||
|
|
game_state.reset_waves()
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 2
|
||
|
|
return false
|
||
|
|
2:
|
||
|
|
# --- D. no trickle ------------------------------------------
|
||
|
|
# Comfortably longer than the old 3-6s spawn interval, so the
|
||
|
|
# removed behaviour would have shown up several times over.
|
||
|
|
if elapsed < 8.0:
|
||
|
|
if _enemy_count() > 0:
|
||
|
|
return _fail("an enemy appeared with no wave running - the trickle is still there")
|
||
|
|
return false
|
||
|
|
if game_state.wave_phase != GameState.WAVE_PHASE_PEACE:
|
||
|
|
return _fail("the peace should still be running after %.0fs" % elapsed)
|
||
|
|
print("%.0fs of peace and not one enemy - the old trickle is gone" % elapsed)
|
||
|
|
|
||
|
|
# --- F. the alert is up only during the warning --------------
|
||
|
|
if alert.visible:
|
||
|
|
return _fail("the wave alert is showing during peacetime")
|
||
|
|
game_state.wave_phase = GameState.WAVE_PHASE_WARNING
|
||
|
|
game_state.wave_timer = 0.0
|
||
|
|
alert._process(0.0)
|
||
|
|
if not alert.visible:
|
||
|
|
return _fail("the wave alert did not appear for an incoming wave")
|
||
|
|
var text: String = alert.label.text
|
||
|
|
for c in text:
|
||
|
|
if c >= "0" and c <= "9":
|
||
|
|
return _fail("the alert reads \"%s\" - that is a timer" % text)
|
||
|
|
print("the alert reads \"%s\" during the warning, with no digits in it" % text)
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 3
|
||
|
|
return false
|
||
|
|
3:
|
||
|
|
# --- E. the wave really arrives -----------------------------
|
||
|
|
game_state.wave_phase = GameState.WAVE_PHASE_ACTIVE
|
||
|
|
game_state.wave_number += 1
|
||
|
|
game_state.wave_pending = true
|
||
|
|
spawner._process(0.0)
|
||
|
|
var arrived: int = _enemy_count()
|
||
|
|
if arrived <= 0:
|
||
|
|
return _fail("the wave turned active and nothing arrived")
|
||
|
|
if game_state.wave_pending:
|
||
|
|
return _fail("the wave was launched but is still owed")
|
||
|
|
if alert.visible:
|
||
|
|
alert._process(0.0)
|
||
|
|
if alert.visible:
|
||
|
|
return _fail("the alert is still up after the wave arrived")
|
||
|
|
var centre: Vector2 = fleet.get_centroid()
|
||
|
|
var nearest: float = INF
|
||
|
|
for enemy in get_nodes_in_group("enemies"):
|
||
|
|
if is_instance_valid(enemy):
|
||
|
|
nearest = minf(nearest, centre.distance_to(enemy.global_position))
|
||
|
|
if nearest < 200.0:
|
||
|
|
return _fail("a wave enemy arrived %.0fpx from the fleet - that is on top of them" % nearest)
|
||
|
|
print("wave %d arrived: %d enemies, nearest %.0fpx away" \
|
||
|
|
% [game_state.wave_number, arrived, nearest])
|
||
|
|
|
||
|
|
for enemy in get_nodes_in_group("enemies"):
|
||
|
|
if is_instance_valid(enemy):
|
||
|
|
enemy.queue_free()
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 4
|
||
|
|
return false
|
||
|
|
4:
|
||
|
|
if elapsed < 0.2:
|
||
|
|
return false
|
||
|
|
spawner._process(0.0)
|
||
|
|
if game_state.wave_phase != GameState.WAVE_PHASE_PEACE:
|
||
|
|
return _fail("killing the whole wave did not bring the quiet back")
|
||
|
|
print("the wave died and the quiet came back")
|
||
|
|
|
||
|
|
print("TEST_WAVE_SYSTEM: PASS")
|
||
|
|
quit(0)
|
||
|
|
return true
|
||
|
|
return false
|