#101 (#97c): waves attack buildings
Every enemy component used to ask the same question - which player SHIP is nearest - so a structure was scenery they flew straight through. A Building is also a plain Node2D, so there was nothing to collide with even if one had tried. EnemyTargets answers the question once, for pursuit, standoff, the lancer's lunge and the ranged weapon together, so they cannot drift into disagreeing about it. It keeps one thumb on the scale: a vessel inside 520px always wins, even against a nearer building, or the fleet could not defend anything by interposing itself and an enemy would fly past the ships shooting it. Buildings get a hitbox - an Area2D in a group, the same shape as a Ship or an asteroid, so EnemyProjectile needed one extra branch rather than a new concept. It applies damage on a tick rather than on area_entered: entry fires once, which is right for a rammer that dies on the first strike, but would let a sniper that drifted inside sit there scratch-free for ever. On a tick, an enemy parked on a structure is besieging it. What one strike costs is data on the enemy definition, for the same reason dies_on_contact is - a lancer arriving at 950px/s is not a sniper drifting into a wall. Also clears the asteroids in test_enemy_projectile_damage. set_process(false) only stops NEW asteroids; the ones already placed keep drifting, and one wandering into the shield vessel parked at (2000, 0) spent its single charge so the projectile fired next killed it. That failed about one parallel suite run in thirty and never in isolation. Clearing them is what the other combat tests already do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
26f3728546
commit
3debcd7edd
18 changed files with 403 additions and 54 deletions
|
|
@ -1,8 +1,18 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://buildings/Building.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://buildings/BuildingHitbox.gd" id="2"]
|
||||
|
||||
[sub_resource type="CircleShape2D" id="1"]
|
||||
radius = 72.0
|
||||
|
||||
[node name="Building" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="VisualHost" type="Node2D" parent="."]
|
||||
|
||||
[node name="Hitbox" type="Area2D" parent="."]
|
||||
script = ExtResource("2")
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="Hitbox"]
|
||||
shape = SubResource("1")
|
||||
|
|
|
|||
54
buildings/BuildingHitbox.gd
Normal file
54
buildings/BuildingHitbox.gd
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
extends Area2D
|
||||
|
||||
# #101 (#97c): the part of a structure a wave can actually hit.
|
||||
#
|
||||
# A Building is a plain Node2D, so nothing could collide with it - enemies
|
||||
# flew through the mothership without noticing. This gives it a body in the
|
||||
# same shape the rest of the game already understands: an Area2D in a group,
|
||||
# exactly like a Ship or an asteroid, so EnemyProjectile needed one extra
|
||||
# branch rather than a new concept.
|
||||
#
|
||||
# Damage is applied on a TICK rather than on area_entered. Entry fires once,
|
||||
# which is right for a rammer - it dies on the first strike - but would let
|
||||
# a sniper that drifted inside sit there scratch-free for ever. On a tick,
|
||||
# an enemy parked on a structure is besieging it, which is what #97 asks
|
||||
# for.
|
||||
|
||||
const STRIKE_INTERVAL: float = 0.7
|
||||
|
||||
var _strike_timer: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("player_buildings")
|
||||
|
||||
|
||||
func building_id() -> int:
|
||||
var building: Node = get_parent()
|
||||
return 0 if building == null else int(building.building_id)
|
||||
|
||||
|
||||
# Called by EnemyProjectile, which knows nothing about buildings beyond this
|
||||
# name - the same call it makes on a Ship.
|
||||
func take_damage(amount: int) -> void:
|
||||
GameState.damage_building(building_id(), amount)
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_strike_timer -= delta
|
||||
if _strike_timer > 0.0:
|
||||
return
|
||||
_strike_timer = STRIKE_INTERVAL
|
||||
var id: int = building_id()
|
||||
if id == 0:
|
||||
return
|
||||
for area in get_overlapping_areas():
|
||||
if not is_instance_valid(area) or not area.is_in_group("enemies"):
|
||||
continue
|
||||
var damage: int = area.contact_damage() if area.has_method("contact_damage") else 6
|
||||
# The structure is told first, for the same reason Ship tells the
|
||||
# enemy first: resolving the enemy's own death can free it, and a
|
||||
# rammer must still land its hit on the way out.
|
||||
GameState.damage_building(id, damage)
|
||||
if area.has_method("collide_with_player_building"):
|
||||
area.collide_with_player_building()
|
||||
|
|
@ -16,3 +16,4 @@ glow_color = Color(1.0, 0.5, 0.2, 1)
|
|||
detail_lines = PackedVector2Array(-14, -20, 14, -20, -12, 6, -20, 16, 12, 6, 20, 16, 0, -22, 0, 6)
|
||||
detail_rings = PackedVector2Array(0, -20, 8, 0)
|
||||
component_scenes = Array[PackedScene]([ExtResource("2")])
|
||||
contact_damage = 6
|
||||
|
|
|
|||
|
|
@ -15,3 +15,4 @@ hull_points = PackedVector2Array(0, -16, 12, 0, 0, 16, -12, 0)
|
|||
glow_color = Color(1, 0.15, 0.2, 1)
|
||||
dies_on_contact = true
|
||||
component_scenes = Array[PackedScene]([ExtResource("2")])
|
||||
contact_damage = 16
|
||||
|
|
|
|||
|
|
@ -57,10 +57,27 @@ func _physics_process(delta: float) -> void:
|
|||
# explosion, same `destroyed` signal, same bookkeeping for whatever is
|
||||
# listening.
|
||||
func collide_with_player_vessel() -> void:
|
||||
_resolve_ram()
|
||||
|
||||
|
||||
# #101: a structure resolves an impact by the same rule - whether it costs
|
||||
# the enemy its life is the definition's call, not the target's type.
|
||||
func collide_with_player_building() -> void:
|
||||
_resolve_ram()
|
||||
|
||||
|
||||
func _resolve_ram() -> void:
|
||||
if definition != null and bool(definition.dies_on_contact):
|
||||
take_damage(hp)
|
||||
|
||||
|
||||
# What one strike against a structure takes off it. Data on the definition,
|
||||
# for the same reason dies_on_contact is: a lancer arriving at 950px/s is not
|
||||
# a sniper drifting into a wall.
|
||||
func contact_damage() -> int:
|
||||
return 6 if definition == null else int(definition.contact_damage)
|
||||
|
||||
|
||||
func take_damage(amount: int) -> void:
|
||||
hp -= amount
|
||||
if hp <= 0:
|
||||
|
|
|
|||
|
|
@ -31,5 +31,9 @@ extends Resource
|
|||
# check: the chaser rams because ramming is its whole behaviour, while a
|
||||
# sniper that happens to drift into a ship has not spent itself doing it.
|
||||
@export var dies_on_contact: bool = false
|
||||
# #101: what one strike against a player structure takes off it. A rammer
|
||||
# spends itself on the hit and so hits hardest; a stand-off enemy that drifts
|
||||
# into a wall barely scratches it, its real threat being its weapon.
|
||||
@export var contact_damage: int = 6
|
||||
@export var core_color: Color = Color(0.06, 0.02, 0.03)
|
||||
@export var component_scenes: Array[PackedScene] = []
|
||||
|
|
|
|||
|
|
@ -162,18 +162,11 @@ func _face(enemy: Node2D, direction: Vector2, delta: float) -> void:
|
|||
enemy.rotation = lerp_angle(enemy.rotation, target_angle, enemy.rotation_speed * delta)
|
||||
|
||||
|
||||
# #101: a lancer will lock a structure exactly as it locks a vessel - the
|
||||
# wind-up, the line and the charge are the same, and so is what happens when
|
||||
# it arrives.
|
||||
func _nearest_ship(enemy: Node2D) -> Node2D:
|
||||
var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet")
|
||||
if fleet == null or fleet.ships.is_empty():
|
||||
return null
|
||||
var nearest: Node2D = fleet.ships[0]
|
||||
var best_dist: float = enemy.global_position.distance_to(nearest.global_position)
|
||||
for ship in fleet.ships:
|
||||
var d: float = enemy.global_position.distance_to(ship.global_position)
|
||||
if d < best_dist:
|
||||
best_dist = d
|
||||
nearest = ship
|
||||
return nearest
|
||||
return EnemyTargets.nearest_target(self, enemy.global_position)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
|
|
|
|||
|
|
@ -37,6 +37,12 @@ func _on_area_entered(area: Area2D) -> void:
|
|||
if area.is_in_group("player_ships"):
|
||||
area.take_damage(damage)
|
||||
queue_free()
|
||||
return
|
||||
# #101: structures are targets too. The hitbox is a child Area2D of the
|
||||
# Building, which is a plain Node2D and could not be hit otherwise.
|
||||
if area.is_in_group("player_buildings"):
|
||||
area.take_damage(damage)
|
||||
queue_free()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
|
|
|
|||
|
|
@ -17,15 +17,18 @@ var velocity: Vector2 = Vector2.ZERO
|
|||
|
||||
func tick(delta: float) -> void:
|
||||
var enemy: Node2D = get_parent()
|
||||
var target: Node2D = _nearest_ship(enemy)
|
||||
if target == null or enemy.position.distance_to(target.position) > detection_range:
|
||||
# #101: a structure is a target now, not scenery. EnemyTargets keeps a
|
||||
# ship in the enemy's face ahead of a building further off.
|
||||
var target: Node2D = EnemyTargets.nearest_target(self, enemy.global_position)
|
||||
if target == null \
|
||||
or enemy.global_position.distance_to(target.global_position) > detection_range:
|
||||
return
|
||||
var to_target: Vector2 = target.position - enemy.position
|
||||
var to_target: Vector2 = target.global_position - enemy.global_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
|
||||
enemy.global_position += velocity * delta
|
||||
|
||||
if velocity.length() > 5.0:
|
||||
var target_angle: float = velocity.angle() + PI / 2.0
|
||||
|
|
@ -33,15 +36,4 @@ func tick(delta: float) -> void:
|
|||
|
||||
|
||||
func _nearest_ship(enemy: Node2D) -> Node2D:
|
||||
var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet")
|
||||
if fleet == null or fleet.ships.is_empty():
|
||||
return null
|
||||
|
||||
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
|
||||
return EnemyTargets.nearest_ship(self, enemy.global_position)
|
||||
|
|
|
|||
|
|
@ -38,17 +38,12 @@ func tick(delta: float) -> void:
|
|||
fire_timer = fire_cooldown
|
||||
|
||||
|
||||
# #101: shoots at whatever EnemyTargets says it is coming for, so a sniper
|
||||
# shells a depot rather than sitting in range of one doing nothing.
|
||||
func _nearest_ship_in_range(enemy: Node2D) -> Node2D:
|
||||
var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet")
|
||||
if fleet == null:
|
||||
var target: Node2D = EnemyTargets.nearest_target(self, enemy.global_position)
|
||||
if target == null:
|
||||
return null
|
||||
var nearest: Node2D = null
|
||||
var best_distance: float = fire_range
|
||||
for ship in fleet.ships:
|
||||
if not is_instance_valid(ship):
|
||||
continue
|
||||
var distance: float = enemy.global_position.distance_to(ship.global_position)
|
||||
if distance <= best_distance:
|
||||
best_distance = distance
|
||||
nearest = ship
|
||||
return nearest
|
||||
if enemy.global_position.distance_to(target.global_position) > fire_range:
|
||||
return null
|
||||
return target
|
||||
|
|
|
|||
|
|
@ -21,10 +21,11 @@ var velocity: Vector2 = Vector2.ZERO
|
|||
|
||||
func tick(delta: float) -> void:
|
||||
var enemy: Node2D = get_parent()
|
||||
var target: Node2D = _nearest_ship(enemy)
|
||||
# #101: holds its preferred range off a building just as off a ship.
|
||||
var target: Node2D = EnemyTargets.nearest_target(self, enemy.global_position)
|
||||
if target == null:
|
||||
return
|
||||
var to_target: Vector2 = target.position - enemy.position
|
||||
var to_target: Vector2 = target.global_position - enemy.global_position
|
||||
var distance: float = to_target.length()
|
||||
if distance < 1.0 or distance > detection_range:
|
||||
return
|
||||
|
|
@ -41,7 +42,7 @@ func tick(delta: float) -> void:
|
|||
desired_velocity = lateral * enemy.speed * 0.5
|
||||
|
||||
velocity = velocity.move_toward(desired_velocity, enemy.acceleration * delta)
|
||||
enemy.position += velocity * delta
|
||||
enemy.global_position += velocity * delta
|
||||
|
||||
if velocity.length() > 5.0:
|
||||
var target_angle: float = velocity.angle() + PI / 2.0
|
||||
|
|
@ -49,14 +50,4 @@ func tick(delta: float) -> void:
|
|||
|
||||
|
||||
func _nearest_ship(enemy: Node2D) -> Node2D:
|
||||
var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet")
|
||||
if fleet == null or fleet.ships.is_empty():
|
||||
return null
|
||||
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
|
||||
return EnemyTargets.nearest_ship(self, enemy.global_position)
|
||||
|
|
|
|||
65
enemies/EnemyTargets.gd
Normal file
65
enemies/EnemyTargets.gd
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
class_name EnemyTargets
|
||||
extends RefCounted
|
||||
|
||||
# #101 (#97c): who a wave enemy is coming for.
|
||||
#
|
||||
# Before this, every enemy component asked the same question - "which player
|
||||
# SHIP is nearest?" - and a structure was scenery they flew straight past.
|
||||
# #97 makes waves go after what the player has built, so the question
|
||||
# becomes "which player thing is nearest", asked in one place so pursuit,
|
||||
# standoff, the lancer's lunge and the ranged weapon cannot drift into
|
||||
# disagreeing about it.
|
||||
#
|
||||
# There is one thumb on the scale. A ship inside SHIP_PRIORITY_RANGE always
|
||||
# wins, even if a building is nearer: an enemy that ignored the vessels
|
||||
# shooting it in order to reach a depot two hundred pixels closer would read
|
||||
# as broken, and would make the fleet unable to defend anything by
|
||||
# interposing itself.
|
||||
|
||||
const SHIP_PRIORITY_RANGE: float = 520.0
|
||||
|
||||
|
||||
static func nearest_ship(node: Node, from: Vector2) -> Node2D:
|
||||
var fleet: Node2D = node.get_tree().get_first_node_in_group("player_fleet")
|
||||
if fleet == null:
|
||||
return null
|
||||
var nearest: Node2D = null
|
||||
var best: float = INF
|
||||
for ship in fleet.ships:
|
||||
if not is_instance_valid(ship):
|
||||
continue
|
||||
var d: float = from.distance_to(ship.global_position)
|
||||
if d < best:
|
||||
best = d
|
||||
nearest = ship
|
||||
return nearest
|
||||
|
||||
|
||||
static func nearest_building(node: Node, from: Vector2) -> Node2D:
|
||||
var nearest: Node2D = null
|
||||
var best: float = INF
|
||||
for building in node.get_tree().get_nodes_in_group("buildings"):
|
||||
if not is_instance_valid(building) or building.is_queued_for_deletion():
|
||||
continue
|
||||
var d: float = from.distance_to(building.global_position)
|
||||
if d < best:
|
||||
best = d
|
||||
nearest = building
|
||||
return nearest
|
||||
|
||||
|
||||
static func nearest_target(node: Node, from: Vector2) -> Node2D:
|
||||
var ship: Node2D = nearest_ship(node, from)
|
||||
if ship != null and from.distance_to(ship.global_position) <= SHIP_PRIORITY_RANGE:
|
||||
return ship
|
||||
var building: Node2D = nearest_building(node, from)
|
||||
if building == null:
|
||||
return ship
|
||||
if ship == null:
|
||||
return building
|
||||
return ship if from.distance_to(ship.global_position) \
|
||||
<= from.distance_to(building.global_position) else building
|
||||
|
||||
|
||||
static func is_building(target: Node) -> bool:
|
||||
return target != null and target.is_in_group("buildings")
|
||||
|
|
@ -17,3 +17,4 @@ detail_lines = PackedVector2Array(0, -28, 0, 6, -7, -2, 7, -2)
|
|||
detail_rings = PackedVector2Array(0, 8, 4, 0)
|
||||
dies_on_contact = true
|
||||
component_scenes = Array[PackedScene]([ExtResource("2")])
|
||||
contact_damage = 22
|
||||
|
|
|
|||
|
|
@ -19,3 +19,4 @@ glow_color = Color(0.55, 0.05, 0.85, 1)
|
|||
detail_lines = PackedVector2Array(0, -44, 0, 40, -28, -18, 28, -18, -34, 4, 34, 4, -20, 28, 20, 28, -12, -40, 12, -40)
|
||||
detail_rings = PackedVector2Array(0, -28, 9, 0, 0, 18, 6, 0)
|
||||
component_scenes = Array[PackedScene]([ExtResource("2"), ExtResource("3"), ExtResource("4"), ExtResource("5")])
|
||||
contact_damage = 34
|
||||
|
|
|
|||
|
|
@ -17,3 +17,4 @@ glow_color = Color(0.42, 0.72, 1.0, 1)
|
|||
detail_lines = PackedVector2Array(0, -30, 0, 10, -6, 12, 6, 12)
|
||||
detail_rings = PackedVector2Array(0, -2, 5, 0)
|
||||
component_scenes = Array[PackedScene]([ExtResource("2"), ExtResource("3")])
|
||||
contact_damage = 4
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ glow_color = Color(0.68, 0.92, 0.18, 1)
|
|||
detail_lines = PackedVector2Array(-16, -16, 16, 16, 16, -16, -16, 16)
|
||||
detail_rings = PackedVector2Array(0, 0, 13, 0, 0, -22, 5, 0, 0, 22, 5, 0, -22, 0, 5, 0, 22, 0, 5, 0)
|
||||
component_scenes = Array[PackedScene]([ExtResource("2")])
|
||||
contact_damage = 5
|
||||
|
|
|
|||
|
|
@ -42,6 +42,18 @@ func _process(delta: float) -> bool:
|
|||
0:
|
||||
scene.get_node("AsteroidField").set_process(false)
|
||||
scene.get_node("EnemySpawner").set_process(false)
|
||||
# set_process(false) only stops NEW asteroids; the ones already
|
||||
# placed keep drifting under their own _physics_process, and they
|
||||
# are placed at random. One of them wandering into the shield
|
||||
# vessel parked out at (2000, 0) spends its single charge, and
|
||||
# the projectile fired at it a moment later then kills it - which
|
||||
# presented as "a shielded ship was destroyed by an enemy
|
||||
# projectile" on roughly one parallel suite run in thirty, and
|
||||
# never in isolation. Clearing them is what the other combat
|
||||
# tests already do (see test_sniper_enemy.gd).
|
||||
for asteroid in get_nodes_in_group("asteroids"):
|
||||
if is_instance_valid(asteroid):
|
||||
asteroid.queue_free()
|
||||
fleet.spawn_ships(1, Vector2.ZERO)
|
||||
interceptor = fleet.ships[0]
|
||||
fleet.add_ship(GameState.VESSEL_SHIELD)
|
||||
|
|
|
|||
204
tests/test_waves_attack_buildings.gd
Normal file
204
tests/test_waves_attack_buildings.gd
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_waves_attack_buildings.gd
|
||||
# Forgejo #101 (#97c): "Waves tries to attack built buildings."
|
||||
#
|
||||
# Before this every enemy component asked one question - which player SHIP
|
||||
# is nearest - so a structure was scenery they flew straight through. A
|
||||
# Building is also a plain Node2D, so there was nothing to collide with even
|
||||
# if one had tried.
|
||||
#
|
||||
# Covers:
|
||||
# A. an enemy with no ship anywhere near it goes for a building
|
||||
# B. a ship in its face still wins: the fleet must be able to defend a
|
||||
# structure by interposing itself, and an enemy that ignored the
|
||||
# vessels shooting it to reach a depot slightly closer reads as broken
|
||||
# C. a chaser that reaches a building damages it, and spends itself doing
|
||||
# so - the same bargain it makes ramming a vessel
|
||||
# D. an enemy parked on a structure keeps hurting it, rather than landing
|
||||
# one hit on entry and then sitting there harmlessly
|
||||
# E. an enemy projectile damages a structure
|
||||
# F. enough of it levels the building, and the node goes with the record
|
||||
|
||||
var game_state: Node
|
||||
var main: Node
|
||||
var fleet: Node
|
||||
var field: Node
|
||||
var chaser: Node2D
|
||||
var sniper: Node2D
|
||||
var target_building: Node2D
|
||||
# Held separately: the node is freed when the building comes down, and the
|
||||
# last stage still has to ask GameState about the record it left behind.
|
||||
var target_id: int = 0
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var hp_watermark: int = 0
|
||||
var destroyed_fired: bool = false
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
# GameState is an autoload: a bare GameState.<instance member> does not
|
||||
# compile in a SceneTree script, so the node is fetched by name instead.
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.transporters.clear()
|
||||
game_state.reset_progression()
|
||||
game_state.reset_waves()
|
||||
game_state.rocks = 5000
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
current_scene = main
|
||||
fleet = main.get_node("Fleet")
|
||||
field = main.get_node("BuildingField")
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_WAVES_ATTACK_BUILDINGS: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
|
||||
func _spawn_enemy(enemy_id: String, at: Vector2) -> Node2D:
|
||||
var enemy: Node2D = load("res://enemies/Enemy.tscn").instantiate()
|
||||
enemy.enemy_id = enemy_id
|
||||
enemy.position = at
|
||||
main.add_child(enemy)
|
||||
return enemy
|
||||
|
||||
|
||||
func _building_of(type: String) -> Node2D:
|
||||
for b in field.spawned:
|
||||
if is_instance_valid(b) and not b.is_queued_for_deletion() and b.building_type == type:
|
||||
return b
|
||||
return null
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
main.get_node("EnemySpawner").set_process(false)
|
||||
main.get_node("AsteroidField").set_process(false)
|
||||
for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies"):
|
||||
if is_instance_valid(hazard):
|
||||
hazard.queue_free()
|
||||
# The fleet is parked far away for A, C and D. It comes back for B.
|
||||
fleet.spawn_ships(1, Vector2(-9000.0, 0.0),
|
||||
PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
|
||||
fleet.ships[0].move_target = fleet.ships[0].position
|
||||
field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2(0.0, 0.0))
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
if elapsed < 0.2:
|
||||
return false
|
||||
target_building = _building_of(GameState.BUILDING_SHIP_CRAFTER)
|
||||
if target_building == null:
|
||||
return _fail("no crafter was placed")
|
||||
target_id = int(target_building.building_id)
|
||||
|
||||
# --- A. with no ship near, a building is the target ----------
|
||||
var targets: Script = load("res://enemies/EnemyTargets.gd")
|
||||
var from: Vector2 = Vector2(600.0, 0.0)
|
||||
var picked: Node2D = targets.nearest_target(main, from)
|
||||
if picked != target_building:
|
||||
return _fail("an enemy 600px from a crafter and 9600px from the fleet picked %s" \
|
||||
% ("nothing" if picked == null else str(picked.name)))
|
||||
print("with the fleet 9600px away, the crafter is the target")
|
||||
|
||||
# --- B. a ship in its face still wins ------------------------
|
||||
# Deliberately FURTHER from the enemy than the building is: the
|
||||
# rule is not "nearest thing", it is "the fleet you are fighting".
|
||||
var guard: Node2D = fleet.ships[0]
|
||||
guard.position = from + Vector2(0.0, 500.0)
|
||||
guard.move_target = guard.position
|
||||
var defended: Node2D = targets.nearest_target(main, from)
|
||||
if defended != guard:
|
||||
return _fail("a vessel 500px away lost priority to a building 600px away - the fleet cannot screen anything")
|
||||
guard.position = Vector2(-9000.0, 0.0)
|
||||
guard.move_target = guard.position
|
||||
print("a vessel inside %.0fpx outranks a nearer building" % targets.SHIP_PRIORITY_RANGE)
|
||||
|
||||
# --- C. a chaser spends itself on the structure --------------
|
||||
hp_watermark = game_state.building_hp(target_id)
|
||||
chaser = _spawn_enemy("chaser", target_building.global_position)
|
||||
chaser.destroyed.connect(func(): destroyed_fired = true)
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
2:
|
||||
if elapsed < 1.2:
|
||||
return false
|
||||
var now: int = game_state.building_hp(target_id)
|
||||
if now >= hp_watermark:
|
||||
return _fail("a chaser sat on the crafter and it took no damage")
|
||||
if is_instance_valid(chaser) and not chaser.is_queued_for_deletion():
|
||||
return _fail("the chaser survived ramming a structure")
|
||||
if not destroyed_fired:
|
||||
return _fail("the chaser vanished without firing `destroyed`")
|
||||
print("a chaser took the crafter from %d to %d hp and died doing it" % [hp_watermark, now])
|
||||
|
||||
# --- D. a besieger keeps working ----------------------------
|
||||
sniper = _spawn_enemy("sniper", target_building.global_position)
|
||||
sniper.speed = 0.0
|
||||
hp_watermark = game_state.building_hp(target_id)
|
||||
elapsed = 0.0
|
||||
stage = 3
|
||||
return false
|
||||
3:
|
||||
if elapsed < 2.5:
|
||||
return false
|
||||
var now: int = game_state.building_hp(target_id)
|
||||
var strikes: int = hp_watermark - now
|
||||
if not is_instance_valid(sniper):
|
||||
return _fail("the sniper died on contact - it is not marked dies_on_contact")
|
||||
if strikes <= sniper.definition.contact_damage:
|
||||
return _fail("%.1fs of siege did %d damage - that is one hit on entry, not a siege" \
|
||||
% [elapsed, strikes])
|
||||
print("%.1fs parked on the crafter cost it %d hp" % [elapsed, strikes])
|
||||
sniper.queue_free()
|
||||
|
||||
# --- E. a projectile hits a structure ------------------------
|
||||
hp_watermark = game_state.building_hp(target_id)
|
||||
var projectile: Area2D = load("res://enemies/EnemyProjectile.tscn").instantiate()
|
||||
projectile.position = target_building.global_position + Vector2(0.0, -140.0)
|
||||
projectile.damage = 3
|
||||
root.add_child(projectile)
|
||||
projectile.launch(Vector2(0.0, 1.0))
|
||||
elapsed = 0.0
|
||||
stage = 4
|
||||
return false
|
||||
4:
|
||||
if elapsed < 1.2:
|
||||
return false
|
||||
var now: int = game_state.building_hp(target_id)
|
||||
if now >= hp_watermark:
|
||||
return _fail("an enemy bolt passed straight through the crafter")
|
||||
print("an enemy bolt took %d hp off the crafter" % (hp_watermark - now))
|
||||
|
||||
# --- F. enough of it levels the place ------------------------
|
||||
game_state.damage_building(target_id,
|
||||
game_state.building_hp(target_id) - 1)
|
||||
var last: Node2D = _spawn_enemy("chaser", target_building.global_position)
|
||||
last.speed = 0.0
|
||||
elapsed = 0.0
|
||||
stage = 5
|
||||
return false
|
||||
5:
|
||||
if elapsed < 1.2:
|
||||
return false
|
||||
if not game_state.find_building(target_id).is_empty():
|
||||
return _fail("the crafter survived on its last hit point")
|
||||
if _building_of(GameState.BUILDING_SHIP_CRAFTER) != null:
|
||||
return _fail("the crafter node outlived the record")
|
||||
print("the crafter came down, and its node went with it")
|
||||
|
||||
print("TEST_WAVES_ATTACK_BUILDINGS: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
Loading…
Reference in a new issue