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
16
ChaserEnemyDefinition.tres
Normal file
16
ChaserEnemyDefinition.tres
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[gd_resource type="Resource" script_class="EnemyDefinition" load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://EnemyDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://EnemyPursuitComponent.tscn" id="2"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
enemy_id = "chaser"
|
||||
display_name = "Chaser"
|
||||
max_hp = 3
|
||||
speed = 140.0
|
||||
acceleration = 260.0
|
||||
rotation_speed = 6.0
|
||||
hull_points = PackedVector2Array(0, -16, 12, 0, 0, 16, -12, 0)
|
||||
glow_color = Color(1, 0.15, 0.2, 1)
|
||||
component_scenes = Array[PackedScene]([ExtResource("2")])
|
||||
76
Enemy.gd
76
Enemy.gd
|
|
@ -2,50 +2,53 @@ extends Area2D
|
|||
|
||||
signal destroyed
|
||||
|
||||
@export var speed: float = 140.0
|
||||
@export var acceleration: float = 260.0
|
||||
@export var rotation_speed: float = 6.0
|
||||
@export var max_hp: int = 3
|
||||
const ENEMY_DEFINITIONS = preload("res://EnemyDefinitions.gd")
|
||||
|
||||
var velocity: Vector2 = Vector2.ZERO
|
||||
@export var enemy_id: String = "chaser"
|
||||
|
||||
var definition: Resource
|
||||
var hp: int
|
||||
|
||||
@onready var fleet: Node2D = get_node("../Fleet")
|
||||
|
||||
# Mirrors Ship.gd's own max_speed/acceleration: copied from the definition
|
||||
# once, then mutable, so a test/preview can pin an enemy in place (e.g.
|
||||
# `enemy.speed = 0.0`) without needing to know about definitions at all.
|
||||
var speed: float
|
||||
var acceleration: float
|
||||
var rotation_speed: float
|
||||
var max_hp: int
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
hp = max_hp
|
||||
add_to_group("enemies")
|
||||
definition = ENEMY_DEFINITIONS.get_definition(enemy_id)
|
||||
speed = definition.speed
|
||||
acceleration = definition.acceleration
|
||||
rotation_speed = definition.rotation_speed
|
||||
max_hp = definition.max_hp
|
||||
hp = max_hp
|
||||
for component_scene in definition.component_scenes:
|
||||
var component: Node = component_scene.instantiate()
|
||||
add_child(component)
|
||||
if component.has_method("setup"):
|
||||
component.setup()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func get_component(capability: String) -> Node:
|
||||
for child in get_children():
|
||||
if child.get("capability_id") == capability:
|
||||
return child
|
||||
return null
|
||||
|
||||
|
||||
func has_capability(capability: String) -> bool:
|
||||
return get_component(capability) != null
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
var target_pos: Vector2 = _nearest_ship_position()
|
||||
var to_target: Vector2 = target_pos - position
|
||||
|
||||
if to_target.length() > 1.0:
|
||||
var desired_velocity: Vector2 = to_target.normalized() * speed
|
||||
velocity = velocity.move_toward(desired_velocity, acceleration * delta)
|
||||
position += velocity * delta
|
||||
|
||||
if velocity.length() > 5.0:
|
||||
var target_angle: float = velocity.angle() + PI / 2.0
|
||||
rotation = lerp_angle(rotation, target_angle, rotation_speed * delta)
|
||||
|
||||
|
||||
func _nearest_ship_position() -> Vector2:
|
||||
if fleet.ships.is_empty():
|
||||
return position
|
||||
|
||||
var nearest: Node2D = fleet.ships[0]
|
||||
var best_dist: float = position.distance_to(nearest.position)
|
||||
for ship in fleet.ships:
|
||||
var d: float = position.distance_to(ship.position)
|
||||
if d < best_dist:
|
||||
best_dist = d
|
||||
nearest = ship
|
||||
return nearest.position
|
||||
for child in get_children():
|
||||
if child.has_method("tick"):
|
||||
child.tick(delta)
|
||||
|
||||
|
||||
func take_damage(amount: int) -> void:
|
||||
|
|
@ -57,8 +60,9 @@ func take_damage(amount: int) -> void:
|
|||
|
||||
|
||||
func _draw() -> void:
|
||||
var points: PackedVector2Array = PackedVector2Array([
|
||||
Vector2(0, -16), Vector2(12, 0), Vector2(0, 16), Vector2(-12, 0)
|
||||
var points: PackedVector2Array = definition.hull_points if definition != null else PackedVector2Array([
|
||||
Vector2(0, -16), Vector2(12, 0), Vector2(0, 16), Vector2(-12, 0),
|
||||
])
|
||||
var glow: Color = definition.glow_color if definition != null else Color(1.0, 0.15, 0.2)
|
||||
# Constant glow - see Ship.gd. Nothing animates, so no per-frame redraw.
|
||||
NeonDraw.draw_glowing_polygon(self, points, Color(1.0, 0.15, 0.2), 1.0)
|
||||
NeonDraw.draw_glowing_polygon(self, points, glow, 1.0)
|
||||
|
|
|
|||
19
EnemyDefinition.gd
Normal file
19
EnemyDefinition.gd
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
class_name EnemyDefinition
|
||||
extends Resource
|
||||
|
||||
# Data-driven enemy stats and appearance, mirroring VesselDefinition's role
|
||||
# for player ships. Enemy.gd instantiates component_scenes exactly like
|
||||
# Ship.gd does, so behaviour (pursuit, ranged fire, ...) lives in capability
|
||||
# components rather than in a type switch here.
|
||||
|
||||
@export var enemy_id: String = "chaser"
|
||||
@export var display_name: String = "Chaser"
|
||||
@export var max_hp: int = 3
|
||||
@export var speed: float = 140.0
|
||||
@export var acceleration: float = 260.0
|
||||
@export var rotation_speed: float = 6.0
|
||||
@export var hull_points: PackedVector2Array = PackedVector2Array([
|
||||
Vector2(0, -16), Vector2(12, 0), Vector2(0, 16), Vector2(-12, 0),
|
||||
])
|
||||
@export var glow_color: Color = Color(1.0, 0.15, 0.2)
|
||||
@export var component_scenes: Array[PackedScene] = []
|
||||
1
EnemyDefinition.gd.uid
Normal file
1
EnemyDefinition.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bkgt6o1m12l0g
|
||||
19
EnemyDefinitions.gd
Normal file
19
EnemyDefinitions.gd
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
class_name EnemyDefinitions
|
||||
extends RefCounted
|
||||
|
||||
const DEFAULT_ID: String = "chaser"
|
||||
const ORDER: PackedStringArray = ["chaser"]
|
||||
const DEFINITIONS: Dictionary = {
|
||||
"chaser": preload("res://ChaserEnemyDefinition.tres"),
|
||||
}
|
||||
|
||||
|
||||
static func get_definition(enemy_id: String) -> Resource:
|
||||
return DEFINITIONS.get(enemy_id, DEFINITIONS[DEFAULT_ID])
|
||||
|
||||
|
||||
static func all() -> Array:
|
||||
var result: Array = []
|
||||
for enemy_id in ORDER:
|
||||
result.append(DEFINITIONS[enemy_id])
|
||||
return result
|
||||
1
EnemyDefinitions.gd.uid
Normal file
1
EnemyDefinitions.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cfc4w3atpf8yj
|
||||
45
EnemyProjectile.gd
Normal file
45
EnemyProjectile.gd
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
extends Area2D
|
||||
|
||||
# Mirrors Projectile.gd, but travels the other direction: fired by an enemy
|
||||
# component (see EnemyRangedWeaponComponent, #45) and damages player_ships.
|
||||
# This is the half of ranged combat that never existed before #44 - enemies
|
||||
# could previously only hurt the fleet by physical contact.
|
||||
|
||||
@export var speed: float = 260.0
|
||||
@export var lifetime: float = 3.0
|
||||
@export var damage: int = 1
|
||||
@export var bolt_length: float = 20.0
|
||||
@export var color: Color = Color(1.0, 0.4, 0.35)
|
||||
|
||||
var velocity: Vector2 = Vector2.ZERO
|
||||
var age: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("enemy_projectiles")
|
||||
area_entered.connect(_on_area_entered)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func launch(direction: Vector2) -> void:
|
||||
velocity = direction.normalized() * speed
|
||||
rotation = direction.angle() + PI / 2.0
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
position += velocity * delta
|
||||
age += delta
|
||||
if age >= lifetime:
|
||||
queue_free()
|
||||
|
||||
|
||||
func _on_area_entered(area: Area2D) -> void:
|
||||
if area.is_in_group("player_ships"):
|
||||
area.take_damage(damage)
|
||||
queue_free()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var tip: Vector2 = Vector2(0, -bolt_length * 0.6)
|
||||
var tail: Vector2 = Vector2(0, bolt_length * 0.4)
|
||||
NeonDraw.draw_glowing_line(self, tail, tip, color, 1.0, 0.5)
|
||||
1
EnemyProjectile.gd.uid
Normal file
1
EnemyProjectile.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b4hja3oj3rdrc
|
||||
12
EnemyProjectile.tscn
Normal file
12
EnemyProjectile.tscn
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://EnemyProjectile.gd" id="1"]
|
||||
|
||||
[sub_resource type="CircleShape2D" id="1"]
|
||||
radius = 4.0
|
||||
|
||||
[node name="EnemyProjectile" type="Area2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
shape = SubResource("1")
|
||||
38
EnemyPursuitComponent.gd
Normal file
38
EnemyPursuitComponent.gd
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
class_name EnemyPursuitComponent
|
||||
extends Node
|
||||
|
||||
# Chases the nearest player ship. Moved out of Enemy.gd verbatim so the
|
||||
# behaviour is opt-in per definition rather than hardcoded into every enemy.
|
||||
|
||||
var capability_id: String = "pursuit"
|
||||
var velocity: Vector2 = Vector2.ZERO
|
||||
|
||||
|
||||
func tick(delta: float) -> void:
|
||||
var enemy: Node2D = get_parent()
|
||||
var target_pos: Vector2 = _nearest_ship_position(enemy)
|
||||
var to_target: Vector2 = target_pos - enemy.position
|
||||
|
||||
if to_target.length() > 1.0:
|
||||
var desired_velocity: Vector2 = to_target.normalized() * enemy.speed
|
||||
velocity = velocity.move_toward(desired_velocity, enemy.acceleration * delta)
|
||||
enemy.position += velocity * delta
|
||||
|
||||
if velocity.length() > 5.0:
|
||||
var target_angle: float = velocity.angle() + PI / 2.0
|
||||
enemy.rotation = lerp_angle(enemy.rotation, target_angle, enemy.rotation_speed * delta)
|
||||
|
||||
|
||||
func _nearest_ship_position(enemy: Node2D) -> Vector2:
|
||||
var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet")
|
||||
if fleet == null or fleet.ships.is_empty():
|
||||
return enemy.position
|
||||
|
||||
var nearest: Node2D = fleet.ships[0]
|
||||
var best_dist: float = enemy.position.distance_to(nearest.position)
|
||||
for ship in fleet.ships:
|
||||
var d: float = enemy.position.distance_to(ship.position)
|
||||
if d < best_dist:
|
||||
best_dist = d
|
||||
nearest = ship
|
||||
return nearest.position
|
||||
1
EnemyPursuitComponent.gd.uid
Normal file
1
EnemyPursuitComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://n8wohkrnl42e
|
||||
6
EnemyPursuitComponent.tscn
Normal file
6
EnemyPursuitComponent.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://EnemyPursuitComponent.gd" id="1"]
|
||||
|
||||
[node name="EnemyPursuitComponent" type="Node"]
|
||||
script = ExtResource("1")
|
||||
4
Fleet.gd
4
Fleet.gd
|
|
@ -32,6 +32,10 @@ var steer_direction: Vector2 = Vector2.ZERO
|
|||
|
||||
|
||||
func _ready() -> void:
|
||||
# Lets anything resolve "the fleet" without a hardcoded ../Fleet sibling
|
||||
# path - enemies parented under different hosts (e.g. a planet surface's
|
||||
# SurfaceFeatures) need this to find their target.
|
||||
add_to_group("player_fleet")
|
||||
# Researching a vessel upgrade improves the ships already flying, not
|
||||
# just the next one built (#5).
|
||||
GameState.vessel_upgraded.connect(_on_vessel_upgraded)
|
||||
|
|
|
|||
22
Ship.gd
22
Ship.gd
|
|
@ -115,17 +115,27 @@ func _physics_process(delta: float) -> void:
|
|||
rotation = lerp_angle(rotation, target_angle, rotation_speed * delta)
|
||||
|
||||
|
||||
func _on_area_entered(area: Area2D) -> void:
|
||||
if not (area.is_in_group("enemies") or area.is_in_group("asteroids")):
|
||||
return
|
||||
func take_damage(_amount: int) -> bool:
|
||||
# Ships have no hit points - any unabsorbed hit is lethal. A shield
|
||||
# charge absorbs a hit entirely regardless of its amount, matching
|
||||
# VesselShieldComponent's existing all-or-nothing semantics; `_amount`
|
||||
# is accepted (not `int`-clamped away) so callers like EnemyProjectile
|
||||
# have a normal damage parameter to pass even though it isn't graded here.
|
||||
for child in get_children():
|
||||
if child.has_method("absorb_hit") and child.absorb_hit():
|
||||
if area.is_in_group("asteroids") and is_instance_valid(area):
|
||||
area.queue_free()
|
||||
return
|
||||
return true
|
||||
ShipExplosionVFX.spawn(get_tree().root, position)
|
||||
destroyed.emit()
|
||||
queue_free()
|
||||
return false
|
||||
|
||||
|
||||
func _on_area_entered(area: Area2D) -> void:
|
||||
if not (area.is_in_group("enemies") or area.is_in_group("asteroids")):
|
||||
return
|
||||
var absorbed: bool = take_damage(1)
|
||||
if absorbed and area.is_in_group("asteroids") and is_instance_valid(area):
|
||||
area.queue_free()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
|
|
|
|||
|
|
@ -39,5 +39,5 @@ func _ready() -> void:
|
|||
for offset in [Vector2(-65, 0), Vector2(45, -45), Vector2(55, 65)]:
|
||||
var enemy: Node = load("res://Enemy.tscn").instantiate()
|
||||
enemy.position = skill.aim_target + offset
|
||||
enemy.speed = 0.0
|
||||
main.add_child(enemy)
|
||||
enemy.speed = 0.0 # after add_child - _ready() sets it from the definition
|
||||
|
|
|
|||
78
tests/test_enemy_composition.gd
Normal file
78
tests/test_enemy_composition.gd
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
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
|
||||
1
tests/test_enemy_composition.gd.uid
Normal file
1
tests/test_enemy_composition.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dltu3a0n318lo
|
||||
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
|
||||
1
tests/test_enemy_projectile_damage.gd.uid
Normal file
1
tests/test_enemy_projectile_damage.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cx7f3e82jkumb
|
||||
|
|
@ -107,16 +107,19 @@ func _process(delta: float) -> bool:
|
|||
if main.get_node("Camera2D").zoom.x >= original_zoom:
|
||||
return _fail_now("long aim did not zoom out")
|
||||
|
||||
# speed is set AFTER add_child(): _ready() copies it from the
|
||||
# enemy's definition, which would otherwise clobber a
|
||||
# pre-add_child override the instant the enemy entered the tree.
|
||||
for offset in [Vector2(-60, 0), Vector2(70, 35)]:
|
||||
var enemy: Node = load("res://Enemy.tscn").instantiate()
|
||||
enemy.position = skill.aim_target + offset
|
||||
enemy.speed = 0.0
|
||||
main.add_child(enemy)
|
||||
enemy.speed = 0.0
|
||||
inside_enemies.append(enemy)
|
||||
outside_enemy = load("res://Enemy.tscn").instantiate()
|
||||
outside_enemy.position = skill.aim_target + Vector2(GasBombController.AOE_RADIUS + 80.0, 0)
|
||||
outside_enemy.speed = 0.0
|
||||
main.add_child(outside_enemy)
|
||||
outside_enemy.speed = 0.0
|
||||
|
||||
var release := InputEventScreenTouch.new()
|
||||
release.index = 7
|
||||
|
|
|
|||
Loading…
Reference in a new issue