166 lines
6.1 KiB
GDScript3
166 lines
6.1 KiB
GDScript3
|
|
extends SceneTree
|
||
|
|
|
||
|
|
# Headless gameplay test: run with
|
||
|
|
# godot --headless -s res://tests/test_asteroid_mining.gd
|
||
|
|
# Asteroids used to be pure hazard - the only thing one could ever do was cost
|
||
|
|
# you a ship. They can now be shot apart for the ore inside, which is the one
|
||
|
|
# resource reachable in open space without conquering anything.
|
||
|
|
#
|
||
|
|
# Covers:
|
||
|
|
# A. an asteroid survives a hit it should survive, so "destructible" does
|
||
|
|
# not mean "evaporates on contact with anything"
|
||
|
|
# B. enough fire destroys it and leaves ore behind
|
||
|
|
# C. the ore is fetchable - flying a ship over it loads the fleet's hold,
|
||
|
|
# which is the whole point of dropping it
|
||
|
|
# D. ramming destroys the asteroid but yields NOTHING. Without that split a
|
||
|
|
# shield vessel could farm asteroids by flying into them
|
||
|
|
# E. dropped ore expires if nobody collects it, so a field that gets shot up
|
||
|
|
# and abandoned cannot leak nodes for the rest of the session; a
|
||
|
|
# hand-placed surface rock still lives forever
|
||
|
|
|
||
|
|
var game_state: Node
|
||
|
|
var main: Node
|
||
|
|
var fleet: Node
|
||
|
|
var asteroid: Node2D
|
||
|
|
var stage: int = 0
|
||
|
|
var elapsed: float = 0.0
|
||
|
|
var hold_before: int = 0
|
||
|
|
|
||
|
|
|
||
|
|
func _initialize() -> void:
|
||
|
|
game_state = root.get_node("GameState")
|
||
|
|
game_state.buildings.clear()
|
||
|
|
game_state.extractors.clear()
|
||
|
|
game_state.reset_progression()
|
||
|
|
main = load("res://Main.tscn").instantiate()
|
||
|
|
root.add_child(main)
|
||
|
|
current_scene = main
|
||
|
|
fleet = main.get_node("Fleet")
|
||
|
|
|
||
|
|
|
||
|
|
func _spawn_asteroid(at: Vector2) -> Node2D:
|
||
|
|
var rock: Node2D = load("res://Asteroid.tscn").instantiate()
|
||
|
|
rock.position = at
|
||
|
|
main.add_child(rock)
|
||
|
|
return rock
|
||
|
|
|
||
|
|
|
||
|
|
func _clear_sky() -> void:
|
||
|
|
for node in get_nodes_in_group("asteroids") + get_nodes_in_group("rocks") \
|
||
|
|
+ get_nodes_in_group("enemies") + get_nodes_in_group("player_projectiles"):
|
||
|
|
if is_instance_valid(node):
|
||
|
|
node.free()
|
||
|
|
|
||
|
|
|
||
|
|
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)
|
||
|
|
_clear_sky()
|
||
|
|
fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
|
||
|
|
|
||
|
|
# --- A/B. it takes real damage, then breaks -------------------
|
||
|
|
asteroid = _spawn_asteroid(Vector2(4000.0, 0.0))
|
||
|
|
var hit_points: int = int(asteroid.hp)
|
||
|
|
if hit_points < 2:
|
||
|
|
return _fail("an asteroid dies to a single point of damage, so there is nothing to survive")
|
||
|
|
for i in range(hit_points - 1):
|
||
|
|
asteroid.take_damage(1)
|
||
|
|
if not is_instance_valid(asteroid) or asteroid.is_queued_for_deletion():
|
||
|
|
return _fail("the asteroid broke after %d of its %d hit points" % [hit_points - 1, hit_points])
|
||
|
|
print("asteroid survived %d of %d hit points" % [hit_points - 1, hit_points])
|
||
|
|
|
||
|
|
asteroid.take_damage(1)
|
||
|
|
if is_instance_valid(asteroid) and not asteroid.is_queued_for_deletion():
|
||
|
|
return _fail("the asteroid survived its full %d hit points" % hit_points)
|
||
|
|
stage = 1
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
1:
|
||
|
|
# The drop is deferred - it is created from inside a signal, where
|
||
|
|
# the physics server refuses a direct add_child.
|
||
|
|
if elapsed < 0.2:
|
||
|
|
return false
|
||
|
|
var dropped: Array = get_nodes_in_group("rocks")
|
||
|
|
if dropped.is_empty():
|
||
|
|
return _fail("destroying an asteroid left no ore behind")
|
||
|
|
print("destroyed asteroid dropped %d ore node(s)" % dropped.size())
|
||
|
|
|
||
|
|
# --- C. the ore is fetchable ---------------------------------
|
||
|
|
hold_before = game_state.fleet_resource(GameState.RESOURCE_ORE)
|
||
|
|
var ship: Node2D = fleet.ships[0]
|
||
|
|
# Placed just clear of the ship rather than on top of it, so the
|
||
|
|
# rock's own drift has to bring it in - that pull is how a player
|
||
|
|
# actually collects one.
|
||
|
|
dropped[0].position = ship.global_position + Vector2(80.0, 0.0)
|
||
|
|
stage = 2
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
2:
|
||
|
|
if elapsed < 1.5:
|
||
|
|
return false
|
||
|
|
var gained: int = game_state.fleet_resource(GameState.RESOURCE_ORE) - hold_before
|
||
|
|
if gained <= 0:
|
||
|
|
return _fail("flying over the dropped ore put nothing in the hold")
|
||
|
|
print("dropped ore collected into the fleet hold (+%d)" % gained)
|
||
|
|
|
||
|
|
# --- D. ramming yields nothing -------------------------------
|
||
|
|
_clear_sky()
|
||
|
|
hold_before = game_state.fleet_resource(GameState.RESOURCE_ORE)
|
||
|
|
asteroid = _spawn_asteroid(fleet.ships[0].global_position)
|
||
|
|
stage = 3
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
3:
|
||
|
|
if elapsed < 0.5:
|
||
|
|
return false
|
||
|
|
if is_instance_valid(asteroid) and not asteroid.is_queued_for_deletion():
|
||
|
|
return _fail("an asteroid dropped onto a ship did not shatter against it")
|
||
|
|
if not get_nodes_in_group("rocks").is_empty():
|
||
|
|
return _fail("ramming an asteroid yielded ore - a shield vessel could farm them that way")
|
||
|
|
if game_state.fleet_resource(GameState.RESOURCE_ORE) != hold_before:
|
||
|
|
return _fail("ramming an asteroid changed the hold")
|
||
|
|
print("ramming shattered the asteroid and yielded nothing")
|
||
|
|
|
||
|
|
# --- E. abandoned ore expires, authored ore does not ----------
|
||
|
|
_clear_sky()
|
||
|
|
var abandoned: Node2D = load("res://Rock.tscn").instantiate()
|
||
|
|
abandoned.build_shape(1)
|
||
|
|
abandoned.lifetime = 0.4
|
||
|
|
# Far from every ship, or it would be collected rather than expire.
|
||
|
|
abandoned.position = fleet.get_centroid() + Vector2(9000.0, 0.0)
|
||
|
|
main.add_child(abandoned)
|
||
|
|
var authored: Node2D = load("res://Rock.tscn").instantiate()
|
||
|
|
authored.build_shape(2)
|
||
|
|
authored.position = fleet.get_centroid() + Vector2(9600.0, 0.0)
|
||
|
|
main.add_child(authored)
|
||
|
|
if authored.lifetime != 0.0:
|
||
|
|
return _fail("a plain rock has a lifetime of %.1f - surface rocks must live forever" % authored.lifetime)
|
||
|
|
stage = 4
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
4:
|
||
|
|
if elapsed < 1.0:
|
||
|
|
return false
|
||
|
|
var survivors: Array = get_nodes_in_group("rocks")
|
||
|
|
var names: Array = []
|
||
|
|
for rock in survivors:
|
||
|
|
names.append(rock.name)
|
||
|
|
if survivors.size() != 1:
|
||
|
|
return _fail("expected only the authored rock to remain, found %d: %s" % [survivors.size(), str(names)])
|
||
|
|
print("abandoned ore expired; the rock with no lifetime is still there")
|
||
|
|
|
||
|
|
print("TEST_ASTEROID_MINING: PASS")
|
||
|
|
quit(0)
|
||
|
|
return true
|
||
|
|
return false
|
||
|
|
|
||
|
|
|
||
|
|
func _fail(message: String) -> bool:
|
||
|
|
print("TEST_ASTEROID_MINING: FAIL (%s)" % message)
|
||
|
|
quit(1)
|
||
|
|
return true
|