The fleet's guns picked their own targets and fired on their own, so combat happened whether or not the player did anything - there was nothing to aim and nothing to decide, and the right half of the screen held nothing at all. Firing is now an action. VesselWeaponComponent no longer seeks a target; it shoots along a heading, and the only thing that supplies one is the new right-hand stick via FireStickController. A fleet arriving from another scene starts with its guns silent, which is the correct resting state. A subtle dashed guide runs from the fleet along the aimed heading while the stick is held, out to the guns' OWN range - read from a weapon that is actually aboard, so the line can never promise reach the fleet does not have. Layout: the two sticks take the bottom corners, the pivot pair is centred between them at the bottom edge, and the laser and gas bomb sit on an arc up and left of the fire stick where a thumb already is. Those four are now round - IconButton grew a round_frame mode, and the in-world action controls use it while panel and menu buttons stay square, so the two kinds of button read as different kinds of thing. Worth recording, because it cost a screenshot to find: a script-declared property set BEFORE the `script = ExtResource(...)` line in a .tscn is silently dropped. Godot's parser has no idea the property exists yet. The round frames were all correctly set in both scenes and none of them applied. Also fixed researtch_lab_garrison.tres, which was authored under the void profile - that is the black hole surface, so a lab garrison was spawning on black holes. Suite 110/110. test_enemy_combat's whole premise was auto-fire; it now holds the real stick toward the enemy rather than poking weapon components, so it exercises the path the player uses. test_fire_stick is new and covers the change itself: an idle stick leaves a target parked well inside range untouched, which is precisely what the old gun would never have done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
104 lines
3.6 KiB
GDScript
104 lines
3.6 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless gameplay test: run with
|
|
# godot --headless -s res://tests/test_enemy_combat.gd
|
|
# Verifies the fleet kills an enemy it is AIMED at, without the enemy ever
|
|
# having to touch a ship, and that a coherent red light-burst explosion VFX
|
|
# spawns at the moment it dies.
|
|
#
|
|
# The fleet used to fire on its own and this test only had to wait. Firing is
|
|
# now the right-hand stick, so the test holds it toward the enemy - through
|
|
# the real control, not by poking the weapon components, so what it exercises
|
|
# is the path the player uses. See tests/test_fire_stick.gd for the aiming
|
|
# behaviour itself.
|
|
|
|
const TEST_DURATION: float = 3.0
|
|
const SPAWN_OFFSET: Vector2 = Vector2(220, 0) # inside fire_range, outside collision radius
|
|
|
|
var main_scene: Node
|
|
var fleet: Node
|
|
var enemy: Node
|
|
var elapsed: float = 0.0
|
|
var started: bool = false
|
|
|
|
|
|
func _initialize() -> void:
|
|
var packed: PackedScene = load("res://Main.tscn")
|
|
main_scene = packed.instantiate()
|
|
root.add_child(main_scene)
|
|
fleet = main_scene.get_node("Fleet")
|
|
|
|
|
|
func _physics_process(delta: float) -> bool:
|
|
if not started:
|
|
# Disabling EnemySpawner in _initialize() doesn't reliably stick -
|
|
# a node's set_process(false) call made before it has fully
|
|
# finished entering the tree can get silently reset once the
|
|
# engine's own (deferred) tree-entry finalization runs, which
|
|
# happens strictly after _ready(). Here, at the point _ready() is
|
|
# already confirmed complete, it's safe.
|
|
main_scene.get_node("EnemySpawner").set_process(false)
|
|
# Crew the fleet HERE, not in _initialize(), for the same reason the
|
|
# set_process(false) call above lives here: work done in _initialize()
|
|
# can be undone by the engine's deferred tree-entry finalization. A
|
|
# new game flies one vessel into three slots now, and this test needs
|
|
# a full one.
|
|
root.get_node("GameState").unlocked.append_array(["wing", "squadron", "armada"])
|
|
fleet.spawn_ships(5, Vector2.ZERO)
|
|
var enemy_scene: PackedScene = load("res://Enemy.tscn")
|
|
enemy = enemy_scene.instantiate()
|
|
enemy.position = fleet.get_centroid() + SPAWN_OFFSET
|
|
main_scene.add_child(enemy)
|
|
_hold_fire_toward(SPAWN_OFFSET)
|
|
started = true
|
|
return false
|
|
|
|
elapsed += delta
|
|
|
|
if not is_instance_valid(enemy):
|
|
_finish(true, "enemy destroyed by aimed fleet fire after %.2fs" % elapsed)
|
|
return true
|
|
|
|
if elapsed >= TEST_DURATION:
|
|
_finish(false, "enemy still alive after %.2fs (hp=%d)" % [elapsed, enemy.hp])
|
|
return true
|
|
|
|
return false
|
|
|
|
|
|
# Presses the fire stick and holds it on the given heading. The stick keeps
|
|
# reporting that direction until it is released, so this is called once.
|
|
func _hold_fire_toward(heading: Vector2) -> void:
|
|
var stick: Control = main_scene.get_node("UI/FireStick")
|
|
var centre: Vector2 = stick.global_position + stick.size * 0.5
|
|
var press := InputEventScreenTouch.new()
|
|
press.index = 6
|
|
press.pressed = true
|
|
press.position = centre
|
|
stick.handle_touch(press)
|
|
var drag := InputEventScreenDrag.new()
|
|
drag.index = 6
|
|
drag.position = centre + heading.normalized() * stick.max_radius
|
|
stick.handle_drag(drag)
|
|
|
|
|
|
func _finish(ok: bool, message: String) -> void:
|
|
print("ships_remaining=%d" % fleet.ships.size())
|
|
print(message)
|
|
|
|
var vfx_ok: bool = false
|
|
for child in root.get_children():
|
|
if child is EnemyExplosionVFX:
|
|
vfx_ok = true
|
|
print("enemy_vfx_ok=%s" % vfx_ok)
|
|
|
|
if ok and fleet.ships.size() == 5 and vfx_ok:
|
|
print("TEST_ENEMY_COMBAT: PASS")
|
|
quit(0)
|
|
else:
|
|
if fleet.ships.size() != 5:
|
|
print("FAIL: a ship was lost - enemy must have reached melee range instead of dying to fire")
|
|
if not vfx_ok:
|
|
print("FAIL: no matching red light-burst explosion VFX found for the destroyed enemy")
|
|
print("TEST_ENEMY_COMBAT: FAIL")
|
|
quit(1)
|