204 lines
7.5 KiB
GDScript3
204 lines
7.5 KiB
GDScript3
|
|
extends SceneTree
|
||
|
|
|
||
|
|
# Headless gameplay test: run with
|
||
|
|
# godot --headless -s res://tests/test_waves_attack_buildings.gd
|
||
|
|
# Forgejo #101 (#97c): "Waves tries to attack built buildings."
|
||
|
|
#
|
||
|
|
# Before this every enemy component asked one question - which player SHIP
|
||
|
|
# is nearest - so a structure was scenery they flew straight through. A
|
||
|
|
# Building is also a plain Node2D, so there was nothing to collide with even
|
||
|
|
# if one had tried.
|
||
|
|
#
|
||
|
|
# Covers:
|
||
|
|
# A. an enemy with no ship anywhere near it goes for a building
|
||
|
|
# B. a ship in its face still wins: the fleet must be able to defend a
|
||
|
|
# structure by interposing itself, and an enemy that ignored the
|
||
|
|
# vessels shooting it to reach a depot slightly closer reads as broken
|
||
|
|
# C. a chaser that reaches a building damages it, and spends itself doing
|
||
|
|
# so - the same bargain it makes ramming a vessel
|
||
|
|
# D. an enemy parked on a structure keeps hurting it, rather than landing
|
||
|
|
# one hit on entry and then sitting there harmlessly
|
||
|
|
# E. an enemy projectile damages a structure
|
||
|
|
# F. enough of it levels the building, and the node goes with the record
|
||
|
|
|
||
|
|
var game_state: Node
|
||
|
|
var main: Node
|
||
|
|
var fleet: Node
|
||
|
|
var field: Node
|
||
|
|
var chaser: Node2D
|
||
|
|
var sniper: Node2D
|
||
|
|
var target_building: Node2D
|
||
|
|
# Held separately: the node is freed when the building comes down, and the
|
||
|
|
# last stage still has to ask GameState about the record it left behind.
|
||
|
|
var target_id: int = 0
|
||
|
|
var stage: int = 0
|
||
|
|
var elapsed: float = 0.0
|
||
|
|
var hp_watermark: int = 0
|
||
|
|
var destroyed_fired: bool = false
|
||
|
|
|
||
|
|
|
||
|
|
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()
|
||
|
|
game_state.rocks = 5000
|
||
|
|
main = load("res://world/Main.tscn").instantiate()
|
||
|
|
root.add_child(main)
|
||
|
|
current_scene = main
|
||
|
|
fleet = main.get_node("Fleet")
|
||
|
|
field = main.get_node("BuildingField")
|
||
|
|
|
||
|
|
|
||
|
|
func _fail(message: String) -> bool:
|
||
|
|
print("TEST_WAVES_ATTACK_BUILDINGS: FAIL (%s)" % message)
|
||
|
|
quit(1)
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
func _spawn_enemy(enemy_id: String, at: Vector2) -> Node2D:
|
||
|
|
var enemy: Node2D = load("res://enemies/Enemy.tscn").instantiate()
|
||
|
|
enemy.enemy_id = enemy_id
|
||
|
|
enemy.position = at
|
||
|
|
main.add_child(enemy)
|
||
|
|
return enemy
|
||
|
|
|
||
|
|
|
||
|
|
func _building_of(type: String) -> Node2D:
|
||
|
|
for b in field.spawned:
|
||
|
|
if is_instance_valid(b) and not b.is_queued_for_deletion() and b.building_type == type:
|
||
|
|
return b
|
||
|
|
return null
|
||
|
|
|
||
|
|
|
||
|
|
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()
|
||
|
|
# The fleet is parked far away for A, C and D. It comes back for B.
|
||
|
|
fleet.spawn_ships(1, Vector2(-9000.0, 0.0),
|
||
|
|
PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
|
||
|
|
fleet.ships[0].move_target = fleet.ships[0].position
|
||
|
|
field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2(0.0, 0.0))
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 1
|
||
|
|
return false
|
||
|
|
1:
|
||
|
|
if elapsed < 0.2:
|
||
|
|
return false
|
||
|
|
target_building = _building_of(GameState.BUILDING_SHIP_CRAFTER)
|
||
|
|
if target_building == null:
|
||
|
|
return _fail("no crafter was placed")
|
||
|
|
target_id = int(target_building.building_id)
|
||
|
|
|
||
|
|
# --- A. with no ship near, a building is the target ----------
|
||
|
|
var targets: Script = load("res://enemies/EnemyTargets.gd")
|
||
|
|
var from: Vector2 = Vector2(600.0, 0.0)
|
||
|
|
var picked: Node2D = targets.nearest_target(main, from)
|
||
|
|
if picked != target_building:
|
||
|
|
return _fail("an enemy 600px from a crafter and 9600px from the fleet picked %s" \
|
||
|
|
% ("nothing" if picked == null else str(picked.name)))
|
||
|
|
print("with the fleet 9600px away, the crafter is the target")
|
||
|
|
|
||
|
|
# --- B. a ship in its face still wins ------------------------
|
||
|
|
# Deliberately FURTHER from the enemy than the building is: the
|
||
|
|
# rule is not "nearest thing", it is "the fleet you are fighting".
|
||
|
|
var guard: Node2D = fleet.ships[0]
|
||
|
|
guard.position = from + Vector2(0.0, 500.0)
|
||
|
|
guard.move_target = guard.position
|
||
|
|
var defended: Node2D = targets.nearest_target(main, from)
|
||
|
|
if defended != guard:
|
||
|
|
return _fail("a vessel 500px away lost priority to a building 600px away - the fleet cannot screen anything")
|
||
|
|
guard.position = Vector2(-9000.0, 0.0)
|
||
|
|
guard.move_target = guard.position
|
||
|
|
print("a vessel inside %.0fpx outranks a nearer building" % targets.SHIP_PRIORITY_RANGE)
|
||
|
|
|
||
|
|
# --- C. a chaser spends itself on the structure --------------
|
||
|
|
hp_watermark = game_state.building_hp(target_id)
|
||
|
|
chaser = _spawn_enemy("chaser", target_building.global_position)
|
||
|
|
chaser.destroyed.connect(func(): destroyed_fired = true)
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 2
|
||
|
|
return false
|
||
|
|
2:
|
||
|
|
if elapsed < 1.2:
|
||
|
|
return false
|
||
|
|
var now: int = game_state.building_hp(target_id)
|
||
|
|
if now >= hp_watermark:
|
||
|
|
return _fail("a chaser sat on the crafter and it took no damage")
|
||
|
|
if is_instance_valid(chaser) and not chaser.is_queued_for_deletion():
|
||
|
|
return _fail("the chaser survived ramming a structure")
|
||
|
|
if not destroyed_fired:
|
||
|
|
return _fail("the chaser vanished without firing `destroyed`")
|
||
|
|
print("a chaser took the crafter from %d to %d hp and died doing it" % [hp_watermark, now])
|
||
|
|
|
||
|
|
# --- D. a besieger keeps working ----------------------------
|
||
|
|
sniper = _spawn_enemy("sniper", target_building.global_position)
|
||
|
|
sniper.speed = 0.0
|
||
|
|
hp_watermark = game_state.building_hp(target_id)
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 3
|
||
|
|
return false
|
||
|
|
3:
|
||
|
|
if elapsed < 2.5:
|
||
|
|
return false
|
||
|
|
var now: int = game_state.building_hp(target_id)
|
||
|
|
var strikes: int = hp_watermark - now
|
||
|
|
if not is_instance_valid(sniper):
|
||
|
|
return _fail("the sniper died on contact - it is not marked dies_on_contact")
|
||
|
|
if strikes <= sniper.definition.contact_damage:
|
||
|
|
return _fail("%.1fs of siege did %d damage - that is one hit on entry, not a siege" \
|
||
|
|
% [elapsed, strikes])
|
||
|
|
print("%.1fs parked on the crafter cost it %d hp" % [elapsed, strikes])
|
||
|
|
sniper.queue_free()
|
||
|
|
|
||
|
|
# --- E. a projectile hits a structure ------------------------
|
||
|
|
hp_watermark = game_state.building_hp(target_id)
|
||
|
|
var projectile: Area2D = load("res://enemies/EnemyProjectile.tscn").instantiate()
|
||
|
|
projectile.position = target_building.global_position + Vector2(0.0, -140.0)
|
||
|
|
projectile.damage = 3
|
||
|
|
root.add_child(projectile)
|
||
|
|
projectile.launch(Vector2(0.0, 1.0))
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 4
|
||
|
|
return false
|
||
|
|
4:
|
||
|
|
if elapsed < 1.2:
|
||
|
|
return false
|
||
|
|
var now: int = game_state.building_hp(target_id)
|
||
|
|
if now >= hp_watermark:
|
||
|
|
return _fail("an enemy bolt passed straight through the crafter")
|
||
|
|
print("an enemy bolt took %d hp off the crafter" % (hp_watermark - now))
|
||
|
|
|
||
|
|
# --- F. enough of it levels the place ------------------------
|
||
|
|
game_state.damage_building(target_id,
|
||
|
|
game_state.building_hp(target_id) - 1)
|
||
|
|
var last: Node2D = _spawn_enemy("chaser", target_building.global_position)
|
||
|
|
last.speed = 0.0
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 5
|
||
|
|
return false
|
||
|
|
5:
|
||
|
|
if elapsed < 1.2:
|
||
|
|
return false
|
||
|
|
if not game_state.find_building(target_id).is_empty():
|
||
|
|
return _fail("the crafter survived on its last hit point")
|
||
|
|
if _building_of(GameState.BUILDING_SHIP_CRAFTER) != null:
|
||
|
|
return _fail("the crafter node outlived the record")
|
||
|
|
print("the crafter came down, and its node went with it")
|
||
|
|
|
||
|
|
print("TEST_WAVES_ATTACK_BUILDINGS: PASS")
|
||
|
|
quit(0)
|
||
|
|
return true
|
||
|
|
return false
|