Cover the last batch of tickets with tests
The previous ten tickets shipped with almost no coverage - only #109, #113, #115 and #117 touched a test. That was a gap, not a decision: everything before it had its behaviour pinned, and #116 in particular added a whole subsystem nothing was watching. Four new files and one extended case: test_spacial_workshop #116 - the repair economy end to end: the cost of a repair scales with the damage, the workshop asks logistics for that ore and stops at its buffer, a drone spends when it LEAVES so two cannot double-charge one wall, and a target levelled in flight sends its load home rather than swallowing it test_desktop_controls #118/#121 - both driven through the real routing, because the routing is what was missing; a test calling handle_mouse_button() itself would have passed against both reports test_enemy_bomber #114 - it flies, it faces where it flies, its bombs arrive, and it bombs structures with no fleet in sight test_menu_polish #111/#112/#119/#120 - what each panel shows test_wave_system (case G) #110 - the dev button, and that it does not exist outside dev mode One assertion worth recording: the craft caption is checked as an EXACT match against the vessel's display name rather than by hunting for resource words in it. The first version failed on "Gas Bomber" - a vessel named after a resource, and always will be. What #111 removed was the price spelled out underneath, not the name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0810c99f08
commit
ec7a8f1f17
8 changed files with 790 additions and 0 deletions
192
tests/test_enemy_bomber.gd
Normal file
192
tests/test_enemy_bomber.gd
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_enemy_bomber.gd
|
||||
# Forgejo #114: the enemy bomber - the artillery enemy, the one that lobs an
|
||||
# area shell.
|
||||
#
|
||||
# It was a siege piece that crept at 20px/s and threw a shell taking five
|
||||
# seconds to cross the gap, which read as a thing that was stuck rather than
|
||||
# a thing that was dangerous: by the time a shell landed the fleet had
|
||||
# wandered off, and the tower was still approaching.
|
||||
#
|
||||
# Covers:
|
||||
# A. it closes on a target at a speed worth calling flying, and faces
|
||||
# where it is going
|
||||
# B. its bombs travel fast enough to arrive while the target is roughly
|
||||
# where they were aimed
|
||||
# C. the bomb damages a player vessel it lands on
|
||||
# D. and a BUILDING it lands on - it used to hit only player_ships, which
|
||||
# was already out of step with #101
|
||||
# E. it bombs a structure with no fleet in sight, through the shared
|
||||
# targeting rather than asking Fleet directly
|
||||
# F. its hull is its own, not a copy of another enemy's
|
||||
|
||||
const MIN_FLYING_SPEED: float = 90.0
|
||||
const MIN_SHELL_SPEED: float = 350.0
|
||||
|
||||
var game_state: Node
|
||||
var main: Node
|
||||
var fleet: Node
|
||||
var field: Node
|
||||
var bomber: Node2D
|
||||
var target_ship: Node2D
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var start_distance: float = 0.0
|
||||
var building_id: int = 0
|
||||
var hp_before: int = 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()
|
||||
game_state.intro_active = false
|
||||
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_ENEMY_BOMBER: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
|
||||
func _spawn_bomber(at: Vector2) -> Node2D:
|
||||
var enemy: Node2D = load("res://enemies/Enemy.tscn").instantiate()
|
||||
enemy.enemy_id = "artillery"
|
||||
enemy.position = at
|
||||
main.add_child(enemy)
|
||||
return enemy
|
||||
|
||||
|
||||
func _shells() -> Array:
|
||||
var out: Array = []
|
||||
for shell in get_nodes_in_group("artillery_shells"):
|
||||
if is_instance_valid(shell) and not shell.is_queued_for_deletion():
|
||||
out.append(shell)
|
||||
return out
|
||||
|
||||
|
||||
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()
|
||||
|
||||
# --- F. its own silhouette -------------------------------
|
||||
var definitions: Script = load("res://enemies/EnemyDefinitions.gd")
|
||||
var bomber_def: Resource = definitions.get_definition("artillery")
|
||||
for other in definitions.all():
|
||||
if other.enemy_id == "artillery":
|
||||
continue
|
||||
if other.hull_points == bomber_def.hull_points:
|
||||
return _fail("the bomber shares its hull with the %s" % other.enemy_id)
|
||||
if bomber_def.speed < MIN_FLYING_SPEED:
|
||||
return _fail("the bomber's top speed is %.0f - that is still creeping" \
|
||||
% bomber_def.speed)
|
||||
print("bomber: %d hull points of its own, %.0fpx/s top speed" \
|
||||
% [bomber_def.hull_points.size() / 2, bomber_def.speed])
|
||||
|
||||
fleet.spawn_ships(1, Vector2.ZERO,
|
||||
PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
|
||||
target_ship = fleet.ships[0]
|
||||
target_ship.move_target = target_ship.position
|
||||
bomber = _spawn_bomber(Vector2(700.0, 0.0))
|
||||
start_distance = bomber.global_position.distance_to(target_ship.global_position)
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
# --- A. it flies, and faces where it is going ------------
|
||||
if elapsed < 1.0:
|
||||
return false
|
||||
if not is_instance_valid(bomber):
|
||||
return _fail("the bomber is gone before it could be measured")
|
||||
var closed: float = start_distance \
|
||||
- bomber.global_position.distance_to(target_ship.global_position)
|
||||
var speed: float = closed / elapsed
|
||||
if speed < MIN_FLYING_SPEED:
|
||||
return _fail("the bomber closed at %.0fpx/s - #114 asks for an aircraft" % speed)
|
||||
var heading: Vector2 = target_ship.global_position - bomber.global_position
|
||||
var drift: float = absf(angle_difference(bomber.rotation, heading.angle() + PI / 2.0))
|
||||
if drift > PI / 3.0:
|
||||
return _fail("it flew %.0f degrees away from where it was pointing" \
|
||||
% rad_to_deg(drift))
|
||||
print("closed %.0fpx in %.1fs (%.0fpx/s), nose within %.0f degrees" \
|
||||
% [closed, elapsed, speed, rad_to_deg(drift)])
|
||||
|
||||
# --- B/C. the bomb arrives, and it hurts -----------------
|
||||
var component: Node = bomber.get_component("artillery")
|
||||
if component.shell_speed < MIN_SHELL_SPEED:
|
||||
return _fail("its bombs travel at %.0f - too slow to arrive" % component.shell_speed)
|
||||
component.cooldown_left = 0.0
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
2:
|
||||
if _shells().is_empty():
|
||||
if elapsed > 6.0:
|
||||
return _fail("the bomber never dropped a bomb")
|
||||
return false
|
||||
print("bomb away, travelling at %.0fpx/s" % _shells()[0].shell_speed)
|
||||
elapsed = 0.0
|
||||
stage = 3
|
||||
return false
|
||||
3:
|
||||
if elapsed < 4.0:
|
||||
return false
|
||||
if is_instance_valid(target_ship) and not target_ship.is_queued_for_deletion():
|
||||
return _fail("a bomb landed on a stationary vessel and it survived")
|
||||
print("the vessel it was aimed at is gone")
|
||||
|
||||
# --- D/E. and it bombs what the player has built ---------
|
||||
for hazard in get_nodes_in_group("enemies"):
|
||||
if is_instance_valid(hazard):
|
||||
hazard.queue_free()
|
||||
# The fleet is parked far away, so nothing here is about ships.
|
||||
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.ZERO)
|
||||
building_id = int(game_state.buildings[-1].id)
|
||||
elapsed = 0.0
|
||||
stage = 4
|
||||
return false
|
||||
4:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
hp_before = game_state.building_hp(building_id)
|
||||
bomber = _spawn_bomber(Vector2(600.0, 0.0))
|
||||
bomber.speed = 0.0
|
||||
bomber.get_component("artillery").cooldown_left = 0.0
|
||||
elapsed = 0.0
|
||||
stage = 5
|
||||
return false
|
||||
5:
|
||||
if elapsed < 5.0:
|
||||
return false
|
||||
var now: int = game_state.building_hp(building_id)
|
||||
if now >= hp_before:
|
||||
return _fail("the bomber shelled a crafter with no fleet in sight and did nothing")
|
||||
print("with the fleet 9000px away it bombed the crafter: %d -> %d hp" % [hp_before, now])
|
||||
|
||||
print("TEST_ENEMY_BOMBER: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
Loading…
Reference in a new issue