Add artillery enemy with telegraphed AOE (#46)
EnemyArtilleryComponent is a stationary siege piece (slow drift toward the fleet, deliberately no pursuit component) that fires at the fleet centroid on a long cooldown via a standalone ArtilleryShell - telegraph ring (~1.8s, growing radius plus a filling countdown arc) -> slow visible travel -> AOE explosion via Ship.take_damage() on every player_ships node inside, same all-or-nothing shield absorption as every other damage source. The shell is independent of the enemy that fired it (added to the tree root, like EnemyProjectile/Projectile) so the shooter dying mid-flight doesn't cancel an already-telegraphed shot, and the impact point is fixed at fire time - never re-aimed at where the fleet moves to afterward. Part of the #40 conquest-mechanic breakdown.
This commit is contained in:
parent
fc2b5ca74a
commit
def224667d
16 changed files with 455 additions and 1 deletions
16
ArtilleryEnemyDefinition.tres
Normal file
16
ArtilleryEnemyDefinition.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://EnemyArtilleryComponent.tscn" id="2"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
enemy_id = "artillery"
|
||||||
|
display_name = "Artillery"
|
||||||
|
max_hp = 8
|
||||||
|
speed = 60.0
|
||||||
|
acceleration = 120.0
|
||||||
|
rotation_speed = 3.0
|
||||||
|
hull_points = PackedVector2Array(0, -26, 18.4, -18.4, 26, 0, 18.4, 18.4, 0, 26, -18.4, 18.4, -26, 0, -18.4, -18.4)
|
||||||
|
glow_color = Color(1.0, 0.5, 0.2, 1)
|
||||||
|
component_scenes = Array[PackedScene]([ExtResource("2")])
|
||||||
112
ArtilleryShell.gd
Normal file
112
ArtilleryShell.gd
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
class_name ArtilleryShell
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Self-contained telegraph -> travel -> explode sequence for the artillery
|
||||||
|
# enemy's attack (see EnemyArtilleryComponent). Lives independently of the
|
||||||
|
# enemy that fired it once launched - added to the tree root, exactly like
|
||||||
|
# EnemyProjectile/Projectile - so the shooter dying mid-flight doesn't
|
||||||
|
# cancel a shot already telegraphed to the player.
|
||||||
|
#
|
||||||
|
# Shield interaction: a shield charge absorbs this hit entirely, same
|
||||||
|
# all-or-nothing semantics as every other damage source in this game
|
||||||
|
# (VesselShieldComponent.absorb_hit) - _explode() just calls
|
||||||
|
# Ship.take_damage() like everything else and lets that logic decide.
|
||||||
|
|
||||||
|
signal finished
|
||||||
|
|
||||||
|
@export var telegraph_time: float = 1.8
|
||||||
|
@export var shell_speed: float = 180.0
|
||||||
|
@export var aoe_radius: float = 220.0
|
||||||
|
@export var damage: int = 3
|
||||||
|
@export var color: Color = Color(1.0, 0.5, 0.2)
|
||||||
|
|
||||||
|
enum Phase { TELEGRAPHING, TRAVELING, EXPLODING }
|
||||||
|
|
||||||
|
const EXPLOSION_DURATION: float = 0.8
|
||||||
|
|
||||||
|
var phase: int = Phase.TELEGRAPHING
|
||||||
|
var target: Vector2 = Vector2.ZERO
|
||||||
|
var shell_pos: Vector2 = Vector2.ZERO
|
||||||
|
var telegraph_left: float = 0.0
|
||||||
|
var explosion_time: float = 0.0
|
||||||
|
var trail: PackedVector2Array = PackedVector2Array()
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
add_to_group("artillery_shells")
|
||||||
|
|
||||||
|
|
||||||
|
func launch(from: Vector2, at: Vector2) -> void:
|
||||||
|
target = at
|
||||||
|
shell_pos = from
|
||||||
|
telegraph_left = telegraph_time
|
||||||
|
phase = Phase.TELEGRAPHING
|
||||||
|
position = target
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
match phase:
|
||||||
|
Phase.TELEGRAPHING:
|
||||||
|
telegraph_left -= delta
|
||||||
|
if telegraph_left <= 0.0:
|
||||||
|
phase = Phase.TRAVELING
|
||||||
|
trail = PackedVector2Array([shell_pos])
|
||||||
|
queue_redraw()
|
||||||
|
Phase.TRAVELING:
|
||||||
|
var to_target: Vector2 = target - shell_pos
|
||||||
|
var step: float = shell_speed * delta
|
||||||
|
if to_target.length() <= step:
|
||||||
|
shell_pos = target
|
||||||
|
_explode()
|
||||||
|
else:
|
||||||
|
shell_pos += to_target.normalized() * step
|
||||||
|
trail.append(shell_pos)
|
||||||
|
while trail.size() > 14:
|
||||||
|
trail.remove_at(0)
|
||||||
|
position = shell_pos
|
||||||
|
queue_redraw()
|
||||||
|
Phase.EXPLODING:
|
||||||
|
explosion_time -= delta
|
||||||
|
queue_redraw()
|
||||||
|
if explosion_time <= 0.0:
|
||||||
|
finished.emit()
|
||||||
|
queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _explode() -> void:
|
||||||
|
phase = Phase.EXPLODING
|
||||||
|
explosion_time = EXPLOSION_DURATION
|
||||||
|
position = target
|
||||||
|
for ship in get_tree().get_nodes_in_group("player_ships"):
|
||||||
|
if is_instance_valid(ship) and ship.global_position.distance_to(target) <= aoe_radius:
|
||||||
|
ship.take_damage(damage)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
match phase:
|
||||||
|
Phase.TELEGRAPHING:
|
||||||
|
var progress: float = clampf(1.0 - telegraph_left / maxf(telegraph_time, 0.001), 0.0, 1.0)
|
||||||
|
# Growing ring plus a filling countdown arc: the warning reads
|
||||||
|
# clearly without needing a number on screen.
|
||||||
|
var radius: float = lerpf(24.0, aoe_radius, progress)
|
||||||
|
_draw_ring(radius, 0.7)
|
||||||
|
draw_arc(Vector2.ZERO, aoe_radius + 14.0, -PI / 2.0, -PI / 2.0 + TAU * progress, 48,
|
||||||
|
Color(color, 0.85), 8.0, true)
|
||||||
|
Phase.TRAVELING:
|
||||||
|
for i in range(1, trail.size()):
|
||||||
|
var alpha: float = float(i) / trail.size()
|
||||||
|
draw_line(trail[i - 1] - shell_pos, trail[i] - shell_pos, Color(color, alpha * 0.6), 5.0, true)
|
||||||
|
draw_circle(Vector2.ZERO, 14.0, Color(color, 0.35))
|
||||||
|
NeonDraw.draw_glowing_circle(self, Vector2.ZERO, 8.0, color, 1.0)
|
||||||
|
Phase.EXPLODING:
|
||||||
|
var eprogress: float = 1.0 - explosion_time / EXPLOSION_DURATION
|
||||||
|
var eradius: float = lerpf(30.0, aoe_radius, minf(eprogress * 1.5, 1.0))
|
||||||
|
draw_circle(Vector2.ZERO, eradius, Color(color, (1.0 - eprogress) * 0.18))
|
||||||
|
_draw_ring(eradius, 1.0 - eprogress)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_ring(radius: float, alpha: float) -> void:
|
||||||
|
draw_arc(Vector2.ZERO, radius, 0.0, TAU, 64, Color(color, 0.16 * alpha), 16.0, true)
|
||||||
|
draw_arc(Vector2.ZERO, radius, 0.0, TAU, 64, Color(color, 0.5 * alpha), 6.0, true)
|
||||||
|
draw_arc(Vector2.ZERO, radius, 0.0, TAU, 64, Color(1.0, 0.92, 0.86, 0.9 * alpha), 2.0, true)
|
||||||
1
ArtilleryShell.gd.uid
Normal file
1
ArtilleryShell.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cvxao0mjslo5x
|
||||||
6
ArtilleryShell.tscn
Normal file
6
ArtilleryShell.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://ArtilleryShell.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="ArtilleryShell" type="Node2D"]
|
||||||
|
script = ExtResource("1")
|
||||||
61
EnemyArtilleryComponent.gd
Normal file
61
EnemyArtilleryComponent.gd
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
class_name EnemyArtilleryComponent
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# Stationary siege piece: drifts slowly toward the fleet (deliberately no
|
||||||
|
# pursuit component - closing distance is incidental, not its purpose) and
|
||||||
|
# fires a slow, heavily telegraphed AOE shell on a long cooldown via a
|
||||||
|
# standalone ArtilleryShell, so an already-launched shot survives this
|
||||||
|
# enemy dying mid-flight, same as EnemyProjectile/Projectile already do.
|
||||||
|
|
||||||
|
var capability_id: String = "artillery"
|
||||||
|
|
||||||
|
@export var fire_cooldown: float = 6.0
|
||||||
|
@export var fire_range: float = 900.0
|
||||||
|
@export var telegraph_time: float = 1.8
|
||||||
|
@export var shell_speed: float = 180.0
|
||||||
|
@export var aoe_radius: float = 220.0
|
||||||
|
@export var damage: int = 3
|
||||||
|
@export var drift_speed: float = 20.0
|
||||||
|
@export var color: Color = Color(1.0, 0.5, 0.2)
|
||||||
|
|
||||||
|
var cooldown_left: float = 0.0
|
||||||
|
var shell_scene: PackedScene = preload("res://ArtilleryShell.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
func tick(delta: float) -> void:
|
||||||
|
var enemy: Node2D = get_parent()
|
||||||
|
_drift(enemy, delta)
|
||||||
|
|
||||||
|
cooldown_left -= delta
|
||||||
|
if cooldown_left > 0.0:
|
||||||
|
return
|
||||||
|
|
||||||
|
var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet")
|
||||||
|
if fleet == null or fleet.ships.is_empty():
|
||||||
|
return
|
||||||
|
var centroid: Vector2 = fleet.get_centroid()
|
||||||
|
if enemy.global_position.distance_to(centroid) > fire_range:
|
||||||
|
return
|
||||||
|
|
||||||
|
var shell: Node2D = shell_scene.instantiate()
|
||||||
|
shell.telegraph_time = telegraph_time
|
||||||
|
shell.shell_speed = shell_speed
|
||||||
|
shell.aoe_radius = aoe_radius
|
||||||
|
shell.damage = damage
|
||||||
|
shell.color = color
|
||||||
|
enemy.get_tree().root.add_child(shell)
|
||||||
|
shell.launch(enemy.global_position, centroid)
|
||||||
|
cooldown_left = fire_cooldown
|
||||||
|
|
||||||
|
|
||||||
|
func _drift(enemy: Node2D, delta: float) -> void:
|
||||||
|
# Local position throughout, same convention as EnemyPursuitComponent/
|
||||||
|
# EnemyStandoffComponent: valid as long as the enemy's container and
|
||||||
|
# Fleet share a common origin, which every scene in this project does.
|
||||||
|
var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet")
|
||||||
|
if fleet == null or fleet.ships.is_empty():
|
||||||
|
return
|
||||||
|
var to_fleet: Vector2 = fleet.get_centroid() - enemy.position
|
||||||
|
if to_fleet.length() < 1.0:
|
||||||
|
return
|
||||||
|
enemy.position += to_fleet.normalized() * drift_speed * delta
|
||||||
1
EnemyArtilleryComponent.gd.uid
Normal file
1
EnemyArtilleryComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b0woyjce8kl6x
|
||||||
6
EnemyArtilleryComponent.tscn
Normal file
6
EnemyArtilleryComponent.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://EnemyArtilleryComponent.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="EnemyArtilleryComponent" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
@ -2,10 +2,11 @@ class_name EnemyDefinitions
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const DEFAULT_ID: String = "chaser"
|
const DEFAULT_ID: String = "chaser"
|
||||||
const ORDER: PackedStringArray = ["chaser", "sniper"]
|
const ORDER: PackedStringArray = ["chaser", "sniper", "artillery"]
|
||||||
const DEFINITIONS: Dictionary = {
|
const DEFINITIONS: Dictionary = {
|
||||||
"chaser": preload("res://ChaserEnemyDefinition.tres"),
|
"chaser": preload("res://ChaserEnemyDefinition.tres"),
|
||||||
"sniper": preload("res://SniperEnemyDefinition.tres"),
|
"sniper": preload("res://SniperEnemyDefinition.tres"),
|
||||||
|
"artillery": preload("res://ArtilleryEnemyDefinition.tres"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
20
tests/_artillery_telegraph_gas_preview.gd
Normal file
20
tests/_artillery_telegraph_gas_preview.gd
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.planet_name = "Violet Tempest"
|
||||||
|
GameState.planet_id = "artillery_preview_gas"
|
||||||
|
GameState.planet_definition_id = "gas_giant"
|
||||||
|
GameState.surviving_ship_count = 3
|
||||||
|
var surface: Node = load("res://PlanetSurface.tscn").instantiate()
|
||||||
|
surface.get_node("SurfaceController").fade_duration = 0.0
|
||||||
|
add_child(surface)
|
||||||
|
|
||||||
|
var shell: Node = load("res://ArtilleryShell.tscn").instantiate()
|
||||||
|
shell.telegraph_time = 10.0 # frozen mid-countdown for the screenshot
|
||||||
|
shell.aoe_radius = 220.0
|
||||||
|
surface.add_child(shell)
|
||||||
|
shell.launch(Vector2(-600, -600), Vector2.ZERO)
|
||||||
|
shell.telegraph_left = shell.telegraph_time * 0.45
|
||||||
|
shell.set_process(false)
|
||||||
|
shell.queue_redraw()
|
||||||
1
tests/_artillery_telegraph_gas_preview.gd.uid
Normal file
1
tests/_artillery_telegraph_gas_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://be13xddivhtr
|
||||||
6
tests/_artillery_telegraph_gas_preview.tscn
Normal file
6
tests/_artillery_telegraph_gas_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/_artillery_telegraph_gas_preview.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="ArtilleryTelegraphGasPreview" type="Node2D"]
|
||||||
|
script = ExtResource("1")
|
||||||
20
tests/_artillery_telegraph_rocky_preview.gd
Normal file
20
tests/_artillery_telegraph_rocky_preview.gd
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.planet_name = "Kepler's Rest"
|
||||||
|
GameState.planet_id = "artillery_preview_rocky"
|
||||||
|
GameState.planet_definition_id = "rocky"
|
||||||
|
GameState.surviving_ship_count = 3
|
||||||
|
var surface: Node = load("res://PlanetSurface.tscn").instantiate()
|
||||||
|
surface.get_node("SurfaceController").fade_duration = 0.0
|
||||||
|
add_child(surface)
|
||||||
|
|
||||||
|
var shell: Node = load("res://ArtilleryShell.tscn").instantiate()
|
||||||
|
shell.telegraph_time = 10.0 # frozen mid-countdown for the screenshot
|
||||||
|
shell.aoe_radius = 220.0
|
||||||
|
surface.add_child(shell)
|
||||||
|
shell.launch(Vector2(-600, -600), Vector2.ZERO)
|
||||||
|
shell.telegraph_left = shell.telegraph_time * 0.45
|
||||||
|
shell.set_process(false)
|
||||||
|
shell.queue_redraw()
|
||||||
1
tests/_artillery_telegraph_rocky_preview.gd.uid
Normal file
1
tests/_artillery_telegraph_rocky_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dlaxr7it3582h
|
||||||
6
tests/_artillery_telegraph_rocky_preview.tscn
Normal file
6
tests/_artillery_telegraph_rocky_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/_artillery_telegraph_rocky_preview.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="ArtilleryTelegraphRockyPreview" type="Node2D"]
|
||||||
|
script = ExtResource("1")
|
||||||
195
tests/test_artillery_enemy.gd
Normal file
195
tests/test_artillery_enemy.gd
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# Headless gameplay test: run with
|
||||||
|
# godot --headless -s res://tests/test_artillery_enemy.gd
|
||||||
|
# Verifies:
|
||||||
|
# 1. the telegraph is visible (phase == TELEGRAPHING) for the whole
|
||||||
|
# window before any damage lands
|
||||||
|
# 2. a ship that leaves the blast radius before impact takes no damage;
|
||||||
|
# one that stays takes the full hit
|
||||||
|
# 3. a shielded ship inside the blast survives, losing exactly one charge
|
||||||
|
# 4. the impact point is fixed at fire time - moving the fleet after the
|
||||||
|
# shot is launched does not redirect it (tested through the real
|
||||||
|
# EnemyArtilleryComponent, since that's what could tempt "aim at the
|
||||||
|
# fleet's live position" instead of its position at fire time)
|
||||||
|
|
||||||
|
# Resolved via load() inside _initialize(), NOT a top-level `const ...
|
||||||
|
# preload(...)` - a top-level preload in a script-mode ENTRY point (-s
|
||||||
|
# this_file.gd) evaluates during this file's own early compilation, which
|
||||||
|
# can happen before autoloads are registered. See test_capture_points.gd
|
||||||
|
# for the full explanation.
|
||||||
|
var ARTILLERY_SHELL_SCRIPT: Script
|
||||||
|
|
||||||
|
var game_state: Node
|
||||||
|
var main: Node
|
||||||
|
var fleet: Node
|
||||||
|
var shell: Node
|
||||||
|
var ship_inside: Node
|
||||||
|
var ship_leaves: Node
|
||||||
|
var shield_ship: Node
|
||||||
|
var far_ship: Node
|
||||||
|
var artillery: Node
|
||||||
|
var art_component: Node
|
||||||
|
var live_shell: Node
|
||||||
|
var recorded_target: Vector2 = Vector2.ZERO
|
||||||
|
var impact_point: Vector2 = Vector2.ZERO
|
||||||
|
var travel_time: float = 0.0
|
||||||
|
var stage2_wait: float = 0.0
|
||||||
|
var stage5_wait: float = 0.0
|
||||||
|
var stage: int = 0
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
ARTILLERY_SHELL_SCRIPT = load("res://ArtilleryShell.gd")
|
||||||
|
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
|
||||||
|
main = load("res://Main.tscn").instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
fleet = main.get_node("Fleet")
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> bool:
|
||||||
|
elapsed += delta
|
||||||
|
match stage:
|
||||||
|
0:
|
||||||
|
if elapsed < 0.1:
|
||||||
|
return false
|
||||||
|
main.get_node("EnemySpawner").set_process(false)
|
||||||
|
main.get_node("AsteroidField").set_process(false)
|
||||||
|
for a in get_nodes_in_group("asteroids"):
|
||||||
|
a.queue_free()
|
||||||
|
|
||||||
|
impact_point = Vector2(5000, 0)
|
||||||
|
fleet.spawn_ships(1, impact_point)
|
||||||
|
ship_inside = fleet.ships[0]
|
||||||
|
ship_inside.position = impact_point
|
||||||
|
|
||||||
|
fleet.add_ship(GameState.VESSEL_INTERCEPTOR)
|
||||||
|
ship_leaves = fleet.ships[fleet.ships.size() - 1]
|
||||||
|
ship_leaves.position = impact_point + Vector2(100, 0)
|
||||||
|
|
||||||
|
fleet.add_ship(GameState.VESSEL_SHIELD)
|
||||||
|
shield_ship = fleet.ships[fleet.ships.size() - 1]
|
||||||
|
shield_ship.position = impact_point + Vector2(-100, 0)
|
||||||
|
|
||||||
|
var origin: Vector2 = impact_point + Vector2(400, 400)
|
||||||
|
travel_time = origin.distance_to(impact_point) / 300.0
|
||||||
|
shell = load("res://ArtilleryShell.tscn").instantiate()
|
||||||
|
shell.telegraph_time = 1.0
|
||||||
|
shell.shell_speed = 300.0
|
||||||
|
shell.aoe_radius = 220.0
|
||||||
|
shell.damage = 3
|
||||||
|
main.add_child(shell)
|
||||||
|
shell.launch(origin, impact_point)
|
||||||
|
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 1
|
||||||
|
return false
|
||||||
|
1:
|
||||||
|
# Mid-telegraph: nobody hurt yet, still visibly telegraphing.
|
||||||
|
if elapsed < shell.telegraph_time * 0.8:
|
||||||
|
return false
|
||||||
|
if shell.phase != ARTILLERY_SHELL_SCRIPT.Phase.TELEGRAPHING:
|
||||||
|
return _fail("shell left the telegraph phase too early")
|
||||||
|
if not is_instance_valid(ship_inside) or not is_instance_valid(ship_leaves) \
|
||||||
|
or not is_instance_valid(shield_ship):
|
||||||
|
return _fail("damage landed before the telegraph window finished")
|
||||||
|
|
||||||
|
# Ship B clears the blast radius now, well before impact.
|
||||||
|
ship_leaves.position = impact_point + Vector2(2000, 0)
|
||||||
|
# Captured once, as a plain float: the shell frees itself once
|
||||||
|
# it resolves, so re-reading shell.telegraph_time from inside
|
||||||
|
# the wait condition below (evaluated every frame) would touch
|
||||||
|
# a freed object on any frame after that.
|
||||||
|
stage2_wait = shell.telegraph_time * 0.2 + travel_time + 1.5
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 2
|
||||||
|
return false
|
||||||
|
2:
|
||||||
|
# Enough time for the remaining telegraph, travel, and the full
|
||||||
|
# explosion visual to resolve.
|
||||||
|
if elapsed < stage2_wait:
|
||||||
|
return false
|
||||||
|
if is_instance_valid(ship_inside):
|
||||||
|
return _fail("a ship that stayed inside the blast radius survived")
|
||||||
|
if not is_instance_valid(ship_leaves):
|
||||||
|
return _fail("a ship that left the blast radius before impact was still hit")
|
||||||
|
if not is_instance_valid(shield_ship):
|
||||||
|
return _fail("a shielded ship inside the blast was destroyed instead of absorbing the hit")
|
||||||
|
if shield_ship.shield_charges != 0:
|
||||||
|
return _fail("shield charges did not drop to 0 after absorbing the artillery hit (charges=%d)" %
|
||||||
|
shield_ship.shield_charges)
|
||||||
|
print("stayed=destroyed left=survived shielded=absorbed-one-charge")
|
||||||
|
|
||||||
|
# --- Part B: the impact point does not home onto a fleet that
|
||||||
|
# moves after the shot is fired ---------------------------------
|
||||||
|
fleet.spawn_ships(1, Vector2(9000, 0))
|
||||||
|
far_ship = fleet.ships[0]
|
||||||
|
artillery = load("res://Enemy.tscn").instantiate()
|
||||||
|
artillery.enemy_id = "artillery"
|
||||||
|
artillery.position = far_ship.position + Vector2(300, 0)
|
||||||
|
main.add_child(artillery)
|
||||||
|
art_component = artillery.get_component("artillery")
|
||||||
|
art_component.cooldown_left = 0.0
|
||||||
|
art_component.telegraph_time = 0.3
|
||||||
|
art_component.fire_cooldown = 999.0 # only the one shot for this check
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 3
|
||||||
|
return false
|
||||||
|
3:
|
||||||
|
if elapsed < 0.15:
|
||||||
|
return false
|
||||||
|
# The shell is added to the tree root (a sibling of main, not
|
||||||
|
# its child - see EnemyArtilleryComponent.tick()), so it has to
|
||||||
|
# be found via its group, not main.get_children().
|
||||||
|
var shells: Array = get_nodes_in_group("artillery_shells")
|
||||||
|
if shells.is_empty():
|
||||||
|
return _fail("artillery enemy did not fire when in range and off cooldown")
|
||||||
|
live_shell = shells[0]
|
||||||
|
recorded_target = live_shell.target
|
||||||
|
|
||||||
|
far_ship.position += Vector2(4000, 0) # flee well clear of the recorded target
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 4
|
||||||
|
return false
|
||||||
|
4:
|
||||||
|
# Check the target field WHILE the shell is still alive - it
|
||||||
|
# frees itself right after exploding, so this has to happen
|
||||||
|
# before that, not after.
|
||||||
|
if elapsed < 0.3:
|
||||||
|
return false
|
||||||
|
if not is_instance_valid(live_shell):
|
||||||
|
return _fail("shell resolved implausibly fast - timing assumption broke")
|
||||||
|
if live_shell.target != recorded_target:
|
||||||
|
return _fail("shell target changed after the fleet moved - it homed instead of staying fixed")
|
||||||
|
# Captured once, as a plain float: far_ship's own weapon can
|
||||||
|
# destroy the artillery enemy (and with it art_component) well
|
||||||
|
# before this wait ends, so the condition below must not keep
|
||||||
|
# re-reading a property off a node that might already be freed.
|
||||||
|
stage5_wait = art_component.telegraph_time + (300.0 / 180.0) + 1.5
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 5
|
||||||
|
return false
|
||||||
|
5:
|
||||||
|
if elapsed < stage5_wait:
|
||||||
|
return false
|
||||||
|
if not is_instance_valid(far_ship):
|
||||||
|
return _fail("fleeing ship was hit by a shot aimed at its old position - homing occurred")
|
||||||
|
print("shell held its fire-time target and did not home onto the fleeing ship")
|
||||||
|
print("TEST_ARTILLERY_ENEMY: PASS")
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _fail(message: String) -> bool:
|
||||||
|
print("TEST_ARTILLERY_ENEMY: FAIL (%s)" % message)
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
1
tests/test_artillery_enemy.gd.uid
Normal file
1
tests/test_artillery_enemy.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cv3lvd01j4kp
|
||||||
Loading…
Reference in a new issue