#219: fleet capacity is a built, supplied thing rather than a researched one. The eight fleet-size doctrines are gone; capacity is one berth free plus one per Fleet Office standing. An office charges every 25s for the vessel in its berth, priced off that vessel's own build cost so an expensive hull is expensive to keep, and drawn from its own store the way a gas burner draws gas. An office that cannot pay goes dark, its vessel is scuttled, and the player is told through the same queue quest lines use - which reaches them on a planet surface too. An empty berth is idle rather than starved: an office is only failing while there is something in it to fail, and holding it disabled would leave a berth that could never be crewed and so could never be paid for again. Ships have no hit points and a shield absorbs a hit wholesale, so the lost vessel is scuttled rather than damaged - otherwise a shielded hull would survive its own eviction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
107 lines
3.5 KiB
GDScript
107 lines
3.5 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
|
|
# #219: fleet capacity is built now - one berth free, one per Fleet
|
|
# Office. This test flies more than one vessel, so it needs the offices
|
|
# standing before the fleet spawns into them.
|
|
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
|
|
scene = load("res://world/Main.tscn").instantiate()
|
|
root.add_child(scene)
|
|
fleet = scene.get_node("Fleet")
|
|
|
|
|
|
func _fire_at(ship: Node) -> void:
|
|
var projectile: Area2D = load("res://enemies/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)
|
|
# 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)
|
|
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
|