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.
91 lines
2.6 KiB
GDScript
91 lines
2.6 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless gameplay test: run with
|
|
# godot --headless -s res://tests/test_enemy_projectile_damage.gd
|
|
# Verifies the ranged damage path #44 adds: an EnemyProjectile destroys an
|
|
# unshielded ship on contact, and is absorbed by a shielded ship, removing
|
|
# exactly one charge rather than destroying it.
|
|
|
|
var game_state: Node
|
|
var scene: Node
|
|
var fleet: Node
|
|
var interceptor: Node
|
|
var shield_ship: Node
|
|
var stage: int = 0
|
|
var elapsed: float = 0.0
|
|
|
|
|
|
func _initialize() -> void:
|
|
game_state = root.get_node("GameState")
|
|
game_state.buildings.clear()
|
|
game_state.extractors.clear()
|
|
game_state.transporters.clear()
|
|
game_state.stored_vessels.clear()
|
|
game_state.build_time_scale = 0.02
|
|
game_state.reset_progression()
|
|
game_state.rocks = 2000
|
|
scene = load("res://Main.tscn").instantiate()
|
|
root.add_child(scene)
|
|
fleet = scene.get_node("Fleet")
|
|
|
|
|
|
func _fire_at(ship: Node) -> void:
|
|
var projectile: Area2D = load("res://EnemyProjectile.tscn").instantiate()
|
|
projectile.global_position = ship.global_position
|
|
projectile.launch(Vector2.ZERO) # already on top of the target
|
|
scene.add_child(projectile)
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
elapsed += delta
|
|
match stage:
|
|
0:
|
|
scene.get_node("AsteroidField").set_process(false)
|
|
scene.get_node("EnemySpawner").set_process(false)
|
|
fleet.spawn_ships(1, Vector2.ZERO)
|
|
interceptor = fleet.ships[0]
|
|
fleet.add_ship(GameState.VESSEL_SHIELD)
|
|
shield_ship = fleet.ships[1]
|
|
shield_ship.position = Vector2(2000.0, 0.0)
|
|
elapsed = 0.0
|
|
stage = 1
|
|
return false
|
|
1:
|
|
if elapsed < 0.2:
|
|
return false
|
|
if not shield_ship.has_shield():
|
|
return _fail("a fresh shield vessel has no shield")
|
|
_fire_at(interceptor)
|
|
elapsed = 0.0
|
|
stage = 2
|
|
return false
|
|
2:
|
|
if elapsed < 0.4:
|
|
return false
|
|
if is_instance_valid(interceptor):
|
|
return _fail("an unshielded ship survived an enemy projectile")
|
|
print("unshielded interceptor destroyed by an enemy projectile")
|
|
|
|
_fire_at(shield_ship)
|
|
elapsed = 0.0
|
|
stage = 3
|
|
return false
|
|
3:
|
|
if elapsed < 0.4:
|
|
return false
|
|
if not is_instance_valid(shield_ship):
|
|
return _fail("a shielded ship was destroyed by an enemy projectile")
|
|
if shield_ship.shield_charges != 0:
|
|
return _fail("shield charges did not drop to 0 after absorbing one hit (charges=%d)" %
|
|
shield_ship.shield_charges)
|
|
print("shielded ship absorbed the hit and lost exactly one charge")
|
|
print("TEST_ENEMY_PROJECTILE_DAMAGE: PASS")
|
|
quit(0)
|
|
return true
|
|
return false
|
|
|
|
|
|
func _fail(message: String) -> bool:
|
|
print("TEST_ENEMY_PROJECTILE_DAMAGE: FAIL (%s)" % message)
|
|
quit(1)
|
|
return true
|