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.
This commit is contained in:
parent
5326852807
commit
f14ee656ea
20 changed files with 393 additions and 42 deletions
91
tests/test_enemy_projectile_damage.gd
Normal file
91
tests/test_enemy_projectile_damage.gd
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
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
|
||||
Loading…
Reference in a new issue