spaceship/tests/test_enemy_composition.gd
Loïc Wisniewski f14ee656ea Add enemy foundation: definitions, components, ranged damage (#44)
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.
2026-08-15 14:43:29 +02:00

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