138 lines
4.9 KiB
GDScript3
138 lines
4.9 KiB
GDScript3
|
|
extends SceneTree
|
||
|
|
|
||
|
|
# Headless gameplay test: run with
|
||
|
|
# godot --headless -s res://tests/test_chaser_rams.gd
|
||
|
|
# Forgejo #82: a chaser is destroyed by the impact when it reaches a player
|
||
|
|
# vessel. It used to pass straight through the wreck untouched and line up
|
||
|
|
# another pass, which made a ramming enemy strictly better than any other.
|
||
|
|
#
|
||
|
|
# Covers:
|
||
|
|
# A. the chaser dies on contact, and dies the same way it dies to gunfire -
|
||
|
|
# through take_damage, so the explosion and the destroyed signal happen
|
||
|
|
# B. the vessel it hit still dies too. Trading one for one is the point;
|
||
|
|
# the chaser must not become harmless
|
||
|
|
# C. an enemy NOT marked dies_on_contact survives the same collision, so
|
||
|
|
# this is a per-enemy decision and not a blanket rule about touching
|
||
|
|
# D. a SHIELDED vessel survives, and still destroys the chaser
|
||
|
|
|
||
|
|
var game_state: Node
|
||
|
|
var main: Node
|
||
|
|
var fleet: Node
|
||
|
|
var stage: int = 0
|
||
|
|
var elapsed: float = 0.0
|
||
|
|
var chaser: Node2D
|
||
|
|
var sniper: Node2D
|
||
|
|
var victim: Node2D
|
||
|
|
var destroyed_fired: bool = false
|
||
|
|
|
||
|
|
|
||
|
|
func _initialize() -> void:
|
||
|
|
game_state = root.get_node("GameState")
|
||
|
|
game_state.buildings.clear()
|
||
|
|
game_state.extractors.clear()
|
||
|
|
game_state.reset_progression()
|
||
|
|
game_state.vessel_levels["unlock_shield"] = 1
|
||
|
|
main = load("res://Main.tscn").instantiate()
|
||
|
|
root.add_child(main)
|
||
|
|
current_scene = main
|
||
|
|
fleet = main.get_node("Fleet")
|
||
|
|
|
||
|
|
|
||
|
|
func _spawn_enemy(enemy_id: String, at: Vector2) -> Node2D:
|
||
|
|
var enemy: Node2D = load("res://Enemy.tscn").instantiate()
|
||
|
|
enemy.enemy_id = enemy_id
|
||
|
|
enemy.position = at
|
||
|
|
main.add_child(enemy)
|
||
|
|
enemy.speed = 0.0
|
||
|
|
return enemy
|
||
|
|
|
||
|
|
|
||
|
|
func _process(delta: float) -> bool:
|
||
|
|
elapsed += delta
|
||
|
|
match stage:
|
||
|
|
0:
|
||
|
|
if elapsed < 0.3:
|
||
|
|
return false
|
||
|
|
main.get_node("EnemySpawner").set_process(false)
|
||
|
|
main.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()
|
||
|
|
fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
|
||
|
|
|
||
|
|
var definitions: Script = load("res://EnemyDefinitions.gd")
|
||
|
|
if not bool(definitions.get_definition("chaser").dies_on_contact):
|
||
|
|
return _fail("the chaser is not marked dies_on_contact")
|
||
|
|
if bool(definitions.get_definition("sniper").dies_on_contact):
|
||
|
|
return _fail("the sniper is marked dies_on_contact, so case C proves nothing")
|
||
|
|
|
||
|
|
# Held as an instance, not read back off fleet.ships: the
|
||
|
|
# wiped-out-fleet safety net rebuilds a vessel within a frame, so
|
||
|
|
# "the fleet is empty" is never true for long enough to observe.
|
||
|
|
victim = fleet.ships[0]
|
||
|
|
chaser = _spawn_enemy("chaser", victim.global_position)
|
||
|
|
chaser.destroyed.connect(func(): destroyed_fired = true)
|
||
|
|
stage = 1
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
1:
|
||
|
|
if elapsed < 0.5:
|
||
|
|
return false
|
||
|
|
# --- A/B. one for one ------------------------------------------
|
||
|
|
if is_instance_valid(chaser) and not chaser.is_queued_for_deletion():
|
||
|
|
return _fail("the chaser survived ramming a vessel")
|
||
|
|
if not destroyed_fired:
|
||
|
|
return _fail("the chaser vanished without firing `destroyed` - it was not routed through take_damage")
|
||
|
|
if is_instance_valid(victim) and not victim.is_queued_for_deletion():
|
||
|
|
return _fail("the rammed vessel survived, so the chaser cost nothing to stop")
|
||
|
|
print("chaser and vessel destroyed each other, through the normal death path")
|
||
|
|
|
||
|
|
# --- C. not a blanket rule -------------------------------------
|
||
|
|
fleet.spawn_ships(1, Vector2(3000.0, 0.0), PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
|
||
|
|
sniper = _spawn_enemy("sniper", fleet.ships[0].global_position)
|
||
|
|
stage = 2
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
2:
|
||
|
|
if elapsed < 0.5:
|
||
|
|
return false
|
||
|
|
if not is_instance_valid(sniper) or sniper.is_queued_for_deletion():
|
||
|
|
return _fail("a sniper died on contact too - the rule is meant to be per-enemy")
|
||
|
|
print("a sniper survives the same collision")
|
||
|
|
sniper.queue_free()
|
||
|
|
|
||
|
|
# --- D. a shield saves the vessel, not the chaser --------------
|
||
|
|
fleet.spawn_ships(1, Vector2(-3000.0, 0.0), PackedStringArray([GameState.VESSEL_SHIELD]))
|
||
|
|
stage = 3
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
3:
|
||
|
|
if elapsed < 0.3:
|
||
|
|
return false
|
||
|
|
victim = fleet.ships[0]
|
||
|
|
var shielded: Node2D = victim
|
||
|
|
if not shielded.has_shield():
|
||
|
|
return _fail("the shield vessel spawned with no shield, so this case proves nothing")
|
||
|
|
chaser = _spawn_enemy("chaser", shielded.global_position)
|
||
|
|
stage = 4
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
4:
|
||
|
|
if elapsed < 0.5:
|
||
|
|
return false
|
||
|
|
if is_instance_valid(chaser) and not chaser.is_queued_for_deletion():
|
||
|
|
return _fail("the chaser survived ramming a shielded vessel")
|
||
|
|
if not is_instance_valid(victim) or victim.is_queued_for_deletion():
|
||
|
|
return _fail("the shielded vessel died anyway")
|
||
|
|
print("a shielded vessel absorbs the ram and the chaser still dies")
|
||
|
|
|
||
|
|
print("TEST_CHASER_RAMS: PASS")
|
||
|
|
quit(0)
|
||
|
|
return true
|
||
|
|
return false
|
||
|
|
|
||
|
|
|
||
|
|
func _fail(message: String) -> bool:
|
||
|
|
print("TEST_CHASER_RAMS: FAIL (%s)" % message)
|
||
|
|
quit(1)
|
||
|
|
return true
|