Enemy.gd now resolves stats/hull/color from EnemyDefinition resources and composes behaviour from capability components (EnemyPursuitComponent holds today's chase-the-nearest-ship logic), the same shape Ship.gd already uses for vessels. Fleet lookups go through a "player_fleet" group instead of a hardcoded ../Fleet sibling path, so an enemy parented anywhere (e.g. under a planet surface's SurfaceFeatures) still finds it. Also closes a real gap: enemies previously had no way to damage the fleet at range at all (Ship only reacted to physical contact, Projectile.gd was player-only). Ship.take_damage() is now a proper entry point - contact damage routes through it unchanged - and EnemyProjectile lets an enemy component fire at player_ships. Both ranged enemy archetypes in #45/#46 build on this. Part of the #40 conquest-mechanic breakdown.
78 lines
2.5 KiB
GDScript
78 lines
2.5 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless gameplay test: run with
|
|
# godot --headless -s res://tests/test_enemy_composition.gd
|
|
# Verifies:
|
|
# 1. an Enemy resolves its stats/hull/color from EnemyDefinitions, not
|
|
# hardcoded fields
|
|
# 2. an enemy instantiated under a node that is NOT a sibling of Fleet
|
|
# still finds the fleet and pursues it (the old `../Fleet` path would
|
|
# break here - this is exactly the planet-surface hazard #44 exists
|
|
# to close before #45/#46 land)
|
|
|
|
var main_scene: Node
|
|
var fleet: Node
|
|
var nested_host: Node2D
|
|
var enemy: Node
|
|
var elapsed: float = 0.0
|
|
var stage: int = 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
var chaser_def: Resource = EnemyDefinitions.get_definition("chaser")
|
|
if chaser_def.enemy_id != "chaser" or chaser_def.max_hp != 3:
|
|
return _fail("EnemyDefinitions did not resolve the chaser definition")
|
|
|
|
main_scene = load("res://Main.tscn").instantiate()
|
|
root.add_child(main_scene)
|
|
fleet = main_scene.get_node("Fleet")
|
|
main_scene.get_node("EnemySpawner").set_process(false)
|
|
|
|
# Deliberately NOT a sibling of Fleet - two levels deep under an
|
|
# unrelated node, the way a planet surface's SurfaceFeatures parents
|
|
# things.
|
|
nested_host = Node2D.new()
|
|
nested_host.name = "NestedHost"
|
|
main_scene.add_child(nested_host)
|
|
var inner: Node2D = Node2D.new()
|
|
inner.name = "Inner"
|
|
nested_host.add_child(inner)
|
|
|
|
enemy = load("res://Enemy.tscn").instantiate()
|
|
enemy.position = fleet.get_centroid() + Vector2(600, 0)
|
|
inner.add_child(enemy)
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
elapsed += delta
|
|
match stage:
|
|
0:
|
|
if elapsed < 0.1:
|
|
return false
|
|
if enemy.definition == null or enemy.definition.enemy_id != "chaser":
|
|
return _fail("enemy did not resolve its definition on _ready")
|
|
if enemy.hp != enemy.definition.max_hp:
|
|
return _fail("enemy hp did not come from the definition")
|
|
if not enemy.has_capability("pursuit"):
|
|
return _fail("chaser did not compose a pursuit component")
|
|
elapsed = 0.0
|
|
stage = 1
|
|
return false
|
|
1:
|
|
# Give the pursuit component a few physics ticks to close distance.
|
|
if elapsed < 1.0:
|
|
return false
|
|
var distance: float = enemy.position.distance_to(fleet.get_centroid())
|
|
print("distance_after_1s=%.1f (started at 600)" % distance)
|
|
if distance >= 550.0:
|
|
return _fail("enemy under a non-sibling host never approached the fleet")
|
|
print("TEST_ENEMY_COMPOSITION: PASS")
|
|
quit(0)
|
|
return true
|
|
return false
|
|
|
|
|
|
func _fail(message: String) -> bool:
|
|
print("TEST_ENEMY_COMPOSITION: FAIL (%s)" % message)
|
|
quit(1)
|
|
return true
|