184 lines
6.3 KiB
GDScript3
184 lines
6.3 KiB
GDScript3
|
|
extends SceneTree
|
||
|
|
|
||
|
|
# Headless gameplay test: run with
|
||
|
|
# godot --headless -s res://tests/test_fleet_agility.gd
|
||
|
|
# The fleet handled with too much inertia: flipping the stick left while
|
||
|
|
# cruising right took most of a second to take effect, and letting go coasted
|
||
|
|
# the ships several lengths further.
|
||
|
|
#
|
||
|
|
# The cause was every vessel's acceleration sitting at roughly its own top
|
||
|
|
# speed - a time constant of about a second to change velocity at all, while
|
||
|
|
# the formation anchor the ships chase turns instantly. Acceleration is now
|
||
|
|
# ~3x top speed, measured rather than guessed: past 3x the reversal time stops
|
||
|
|
# improving, because max_speed becomes the limit instead of acceleration.
|
||
|
|
#
|
||
|
|
# Feel is not directly assertable, but the three things that PRODUCE it are,
|
||
|
|
# so this measures them:
|
||
|
|
# A. how long after the stick reverses before a ship is actually travelling
|
||
|
|
# the new way
|
||
|
|
# B. how far past the turn it drifts first
|
||
|
|
# C. how far it coasts after the stick is released
|
||
|
|
#
|
||
|
|
# Plus D: the same measurement at the OLD acceleration must BREACH these
|
||
|
|
# bounds. Without that, quietly restoring the sluggish handling would still
|
||
|
|
# pass, and so would bounds so loose they mean nothing.
|
||
|
|
#
|
||
|
|
# The bounds are roughly double what the tuned values measure. This suite runs
|
||
|
|
# several Godot processes at once on a phone, and a physics-timing measurement
|
||
|
|
# under that load strays (see the note in test_space_time_distorder.gd); real
|
||
|
|
# regression here means going back towards a second, which is far outside this
|
||
|
|
# margin.
|
||
|
|
|
||
|
|
const MAX_REVERSE_TIME: float = 0.35
|
||
|
|
const MAX_OVERSHOOT: float = 45.0
|
||
|
|
const MAX_STOP_COAST: float = 60.0
|
||
|
|
# What the acceleration was before, as a fraction of what it is now.
|
||
|
|
const OLD_ACCELERATION_FRACTION: float = 1.0 / 3.0
|
||
|
|
|
||
|
|
var main: Node
|
||
|
|
var fleet: Node
|
||
|
|
var elapsed: float = 0.0
|
||
|
|
var stage: int = 0
|
||
|
|
var flip_x: float = 0.0
|
||
|
|
var reverse_time: float = -1.0
|
||
|
|
var overshoot: float = 0.0
|
||
|
|
var stop_x: float = 0.0
|
||
|
|
# Second pass re-runs the identical measurement with the old acceleration.
|
||
|
|
var measuring_old: bool = false
|
||
|
|
var tuned: Dictionary = {}
|
||
|
|
|
||
|
|
|
||
|
|
func _initialize() -> void:
|
||
|
|
var game_state: Node = root.get_node("GameState")
|
||
|
|
game_state.reset_progression()
|
||
|
|
main = load("res://Main.tscn").instantiate()
|
||
|
|
root.add_child(main)
|
||
|
|
current_scene = main
|
||
|
|
fleet = main.get_node("Fleet")
|
||
|
|
|
||
|
|
|
||
|
|
func _process(delta: float) -> bool:
|
||
|
|
elapsed += delta
|
||
|
|
match stage:
|
||
|
|
0:
|
||
|
|
if elapsed < 0.3:
|
||
|
|
return false
|
||
|
|
main.get_node("AsteroidField").set_process(false)
|
||
|
|
main.get_node("EnemySpawner").set_process(false)
|
||
|
|
# A rock or an enemy destroying the ship being measured would
|
||
|
|
# read as a handling failure. Same clean-slate idiom as
|
||
|
|
# test_space_time_distorder.gd.
|
||
|
|
for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies"):
|
||
|
|
if is_instance_valid(hazard):
|
||
|
|
hazard.queue_free()
|
||
|
|
_begin_pass()
|
||
|
|
return false
|
||
|
|
1:
|
||
|
|
# Reach steady state travelling right before disturbing anything.
|
||
|
|
if elapsed < 2.0:
|
||
|
|
return false
|
||
|
|
if fleet.ships.is_empty():
|
||
|
|
return _fail("the fleet lost its ships before anything could be measured")
|
||
|
|
flip_x = fleet.ships[0].position.x
|
||
|
|
fleet.move_in_direction(Vector2(-1.0, 0.0))
|
||
|
|
stage = 2
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
2:
|
||
|
|
var ship: Node2D = fleet.ships[0]
|
||
|
|
overshoot = maxf(overshoot, ship.position.x - flip_x)
|
||
|
|
if reverse_time < 0.0 and ship.velocity.x < 0.0:
|
||
|
|
reverse_time = elapsed
|
||
|
|
if elapsed < 2.0:
|
||
|
|
return false
|
||
|
|
if reverse_time < 0.0:
|
||
|
|
return _fail("the fleet never reversed at all within 2s of the stick flipping")
|
||
|
|
# Settle going one way again, so the coast below is measured from
|
||
|
|
# a steady cruise rather than mid-catch-up.
|
||
|
|
fleet.move_in_direction(Vector2(1.0, 0.0))
|
||
|
|
stage = 3
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
3:
|
||
|
|
if elapsed < 2.0:
|
||
|
|
return false
|
||
|
|
stop_x = fleet.ships[0].position.x
|
||
|
|
fleet.stop()
|
||
|
|
stage = 4
|
||
|
|
elapsed = 0.0
|
||
|
|
return false
|
||
|
|
4:
|
||
|
|
if elapsed < 2.5:
|
||
|
|
return false
|
||
|
|
var result: Dictionary = {
|
||
|
|
"reverse_time": reverse_time,
|
||
|
|
"overshoot": overshoot,
|
||
|
|
"coast": fleet.ships[0].position.x - stop_x,
|
||
|
|
}
|
||
|
|
if not measuring_old:
|
||
|
|
tuned = result
|
||
|
|
if not _check_tuned(result):
|
||
|
|
return true
|
||
|
|
# --- D: the same measurement, sluggish again ----------------
|
||
|
|
measuring_old = true
|
||
|
|
_begin_pass()
|
||
|
|
return false
|
||
|
|
return _check_old_breaches(result)
|
||
|
|
return false
|
||
|
|
|
||
|
|
|
||
|
|
func _begin_pass() -> void:
|
||
|
|
reverse_time = -1.0
|
||
|
|
overshoot = 0.0
|
||
|
|
for ship in fleet.ships:
|
||
|
|
# apply_vessel_stats() has already put the definition's value on the
|
||
|
|
# ship, so the old handling is reproduced by scaling that field
|
||
|
|
# directly - no need to edit a resource mid-test.
|
||
|
|
if measuring_old:
|
||
|
|
ship.acceleration *= OLD_ACCELERATION_FRACTION
|
||
|
|
ship.position = Vector2.ZERO
|
||
|
|
ship.velocity = Vector2.ZERO
|
||
|
|
fleet.move_to(Vector2.ZERO)
|
||
|
|
fleet.move_in_direction(Vector2(1.0, 0.0))
|
||
|
|
stage = 1
|
||
|
|
elapsed = 0.0
|
||
|
|
|
||
|
|
|
||
|
|
func _check_tuned(r: Dictionary) -> bool:
|
||
|
|
print("tuned (acceleration %.0f): reversed in %.3fs, overshot %.1f, coasted %.1f" \
|
||
|
|
% [fleet.ships[0].acceleration, r.reverse_time, r.overshoot, r.coast])
|
||
|
|
if r.reverse_time > MAX_REVERSE_TIME:
|
||
|
|
return _fail("the fleet took %.3fs to obey a reversed stick (allowed %.2f)" \
|
||
|
|
% [r.reverse_time, MAX_REVERSE_TIME])
|
||
|
|
if r.overshoot > MAX_OVERSHOOT:
|
||
|
|
return _fail("the fleet drifted %.1f units past the turn (allowed %.1f)" \
|
||
|
|
% [r.overshoot, MAX_OVERSHOOT])
|
||
|
|
if r.coast > MAX_STOP_COAST:
|
||
|
|
return _fail("the fleet coasted %.1f units after the stick was released (allowed %.1f)" \
|
||
|
|
% [r.coast, MAX_STOP_COAST])
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
func _check_old_breaches(r: Dictionary) -> bool:
|
||
|
|
print("old (acceleration %.0f): reversed in %.3fs, overshot %.1f, coasted %.1f" \
|
||
|
|
% [fleet.ships[0].acceleration, r.reverse_time, r.overshoot, r.coast])
|
||
|
|
var breaches: int = 0
|
||
|
|
if r.reverse_time > MAX_REVERSE_TIME:
|
||
|
|
breaches += 1
|
||
|
|
if r.overshoot > MAX_OVERSHOOT:
|
||
|
|
breaches += 1
|
||
|
|
if r.coast > MAX_STOP_COAST:
|
||
|
|
breaches += 1
|
||
|
|
if breaches == 0:
|
||
|
|
return _fail("the OLD, sluggish acceleration passed every bound too (reversed %.3fs, overshot %.1f, coasted %.1f) - these thresholds do not discriminate and would not catch a regression" \
|
||
|
|
% [r.reverse_time, r.overshoot, r.coast])
|
||
|
|
print("bounds discriminate: the old acceleration breaches %d of 3" % breaches)
|
||
|
|
print("TEST_FLEET_AGILITY: PASS")
|
||
|
|
quit(0)
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
func _fail(message: String) -> bool:
|
||
|
|
print("TEST_FLEET_AGILITY: FAIL (%s)" % message)
|
||
|
|
quit(1)
|
||
|
|
return true
|