Sharpen upgrades, mark the convoy's destination, and give the world sound
#204 the Beam Collimator widens the beam instead of adding range to one that already crosses the screen; beam width becomes a per-shot vessel stat. #205 the Energy Propulser gains three upgrades - dash recharge, dash reach, and boosted-cruise drain - which turns three constants into vessel stats. #206 three further doctrines carry the fleet from ten to twenty. #207 the prologue's World Ship gets a marker, through the scanner's own guidance arrow rather than a second one. #208 #210 #213 #214 four new synthesised sounds: the breach opening, cargo reaching the hold, a rock breaking, and a gas bomb blooming. #215 building health bars are larger, plated and framed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
parent
2239235993
commit
78836882af
33 changed files with 716 additions and 52 deletions
34
tests/_building_health_preview.gd
Normal file
34
tests/_building_health_preview.gd
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
extends Node2D
|
||||
|
||||
# Preview, not a test - #215's health bars, through the real game path. A bar
|
||||
# is only drawn on a DAMAGED structure, so this hurts each one by a different
|
||||
# amount: full, most, half, and nearly gone, left to right.
|
||||
#
|
||||
# bash tests/screenshot.sh res://tests/_building_health_preview.tscn shots/building_health.png 3 720x1280
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.buildings.clear()
|
||||
GameState.extractors.clear()
|
||||
GameState.intro_active = false
|
||||
GameState.reset_formation()
|
||||
var placed: Array = [
|
||||
[GameState.BUILDING_MOTHERSHIP, Vector2(-330.0, 0.0), 0.0],
|
||||
[GameState.BUILDING_SHIP_CRAFTER, Vector2(140.0, 0.0), 0.25],
|
||||
[GameState.BUILDING_TRANSPORT_HUB, Vector2(480.0, 0.0), 0.5],
|
||||
[GameState.BUILDING_SHIP_LAB, Vector2(820.0, 0.0), 0.85],
|
||||
]
|
||||
for entry in placed:
|
||||
var id: int = GameState.add_building(String(entry[0]), Vector2(entry[1]))
|
||||
var hurt: float = float(entry[2])
|
||||
if hurt > 0.0:
|
||||
GameState.damage_building(id,
|
||||
int(GameState.building_max_hp(String(entry[0])) * hurt))
|
||||
|
||||
var main: Node = load("res://world/Main.tscn").instantiate()
|
||||
add_child(main)
|
||||
main.get_node("AsteroidField").set_process(false)
|
||||
main.get_node("EnemySpawner").set_process(false)
|
||||
main.get_node("Fleet").move_to(Vector2(240.0, 260.0))
|
||||
var cam: Camera2D = main.get_node("Camera2D")
|
||||
cam.zoom = Vector2(0.78, 0.78)
|
||||
6
tests/_building_health_preview.tscn
Normal file
6
tests/_building_health_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_building_health_preview.gd" id="1"]
|
||||
|
||||
[node name="BuildingHealthPreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
21
tests/_intro_marker_preview.gd
Normal file
21
tests/_intro_marker_preview.gd
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
extends Node2D
|
||||
|
||||
# Preview, not a test - #207's destination marker during the prologue. The
|
||||
# convoy is flying north toward a World Ship 16000 units away that is never
|
||||
# on screen, so this renders the RUN beat, where the player is flying and
|
||||
# the marker is the only thing saying where.
|
||||
#
|
||||
# bash tests/screenshot.sh res://tests/_intro_marker_preview.tscn shots/intro_marker.png 3 720x1280
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.reset_new_game()
|
||||
var main: Node = load("res://world/Main.tscn").instantiate()
|
||||
add_child(main)
|
||||
main.get_node("AsteroidField").set_process(false)
|
||||
main.get_node("EnemySpawner").set_process(false)
|
||||
# BRIEF has already run inside Main's _ready(), which is what spawns the
|
||||
# convoy and names the destination; step on to the beat where the convoy
|
||||
# is actually under way.
|
||||
var director: Node = main.get_node("IntroDirector")
|
||||
director._begin(1)
|
||||
6
tests/_intro_marker_preview.tscn
Normal file
6
tests/_intro_marker_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_intro_marker_preview.gd" id="1"]
|
||||
|
||||
[node name="IntroMarkerPreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
|
@ -21,6 +21,8 @@ func _ready() -> void:
|
|||
main.get_node("EnemySpawner").set_process(false)
|
||||
main.get_node("AsteroidField").set_process(false)
|
||||
var build: Node = main.get_node("BuildController")
|
||||
# The tree lives on the menu, not on the controller - reaching through
|
||||
# build.skill_tree stopped resolving at some point and this preview had
|
||||
# been rendering an unopened menu ever since.
|
||||
build.skill_menu.visible = true
|
||||
build.skill_tree.selected_id = "shipyards"
|
||||
build._refresh_skill_menu("shipyards")
|
||||
build.skill_menu.refresh("shipyards")
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ extends SceneTree
|
|||
# E. the tree cannot promise a slot the formation has no room for - the
|
||||
# invariant that made extra doctrines silently worthless
|
||||
|
||||
const DOCTRINE_CHAIN: Array = ["wing", "squadron", "armada", "battlegroup", "grand_fleet"]
|
||||
const DOCTRINE_CHAIN: Array = ["wing", "squadron", "armada", "battlegroup",
|
||||
"grand_fleet", "task_force", "star_legion", "void_armada"]
|
||||
|
||||
var game_state: Node
|
||||
var scene: Node
|
||||
|
|
@ -98,6 +99,11 @@ func _process(delta: float) -> bool:
|
|||
if expected > GameState.DEFAULT_FORMATION.size():
|
||||
return _fail("the doctrines promise %d slots but DEFAULT_FORMATION only defines %d - the last ones buy nothing" \
|
||||
% [expected, GameState.DEFAULT_FORMATION.size()])
|
||||
# #206 named the destination, so it is pinned rather than left to
|
||||
# whatever the chain happens to add up to.
|
||||
if expected != 20:
|
||||
return _fail("the doctrine chain reaches %d vessels, not the 20 it is meant to" \
|
||||
% expected)
|
||||
print("full doctrine chain reaches %d slots, with %d formation positions defined" \
|
||||
% [expected, GameState.DEFAULT_FORMATION.size()])
|
||||
|
||||
|
|
|
|||
|
|
@ -288,6 +288,41 @@ func _process(delta: float) -> bool:
|
|||
return _fail_now("dragging further did not reach past the nearer enemy")
|
||||
print("drag depth selects between a near and a far target in the same cone")
|
||||
|
||||
# --- G. #204: the Collimator widens what one shot catches -------
|
||||
# The upgrade used to add range to a beam that already crossed the
|
||||
# screen; it adds WIDTH now, so this checks the thing it widens is
|
||||
# the hit test and not only the picture.
|
||||
for placed in [near_enemy, far_enemy, out_of_cone]:
|
||||
if is_instance_valid(placed):
|
||||
placed.free()
|
||||
var laser: Node = first_shooter.get_component("laser")
|
||||
var narrow_width: float = laser.beam_half_width
|
||||
game_state.vessel_levels["lsr_width"] = 3
|
||||
laser.apply_stats()
|
||||
if not is_equal_approx(laser.beam_half_width, narrow_width + 66.0):
|
||||
return _fail_now("three Collimators gave a half-width of %.0f, expected %.0f" \
|
||||
% [laser.beam_half_width, narrow_width + 66.0])
|
||||
|
||||
# Off the line by more than the base beam covers and less than the
|
||||
# researched one does, so the same shot answers differently.
|
||||
var flanker: Node = load("res://enemies/Enemy.tscn").instantiate()
|
||||
flanker.position = first_shooter.global_position \
|
||||
+ Vector2(600.0, narrow_width + 60.0)
|
||||
main.add_child(flanker)
|
||||
flanker.speed = 0.0
|
||||
var beam_script: Resource = load("res://vfx/LaserBeamEffect.gd")
|
||||
var narrow_shot: Node2D = beam_script.fire(root, first_shooter.global_position,
|
||||
Vector2.RIGHT, 2900.0, 1, narrow_width)
|
||||
if narrow_shot.hit_count != 0:
|
||||
return _fail_now("the unresearched beam already reached the flanking enemy")
|
||||
var wide_shot: Node2D = beam_script.fire(root, first_shooter.global_position,
|
||||
Vector2.RIGHT, 2900.0, 1, laser.beam_half_width)
|
||||
if wide_shot.hit_count != 1:
|
||||
return _fail_now("the widened beam hit %d enemies, expected the flanker" \
|
||||
% wide_shot.hit_count)
|
||||
print("Beam Collimator: %.0f -> %.0f half-width, and the wider beam catches what the narrow one missed" \
|
||||
% [narrow_width, laser.beam_half_width])
|
||||
|
||||
print("TEST_LASER_VESSEL: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
|
|
|
|||
133
tests/test_propulser_upgrades.gd
Normal file
133
tests/test_propulser_upgrades.gd
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
extends SceneTree
|
||||
|
||||
# Forgejo #205: the Energy Propulser had no upgrades at all - it was the one
|
||||
# fleet hull whose ship lab offered nothing. It now carries three, and this
|
||||
# checks each of them reaches the thing it is supposed to change rather than
|
||||
# only reading well in the menu.
|
||||
#
|
||||
# Covers:
|
||||
# A. Warp Coils lengthen the dash, and the controller aims that far
|
||||
# B. Surge Capacitor shortens the recharge, floored by minimum_stats
|
||||
# C. Flux Regulator makes boosted cruise cheaper to sustain
|
||||
# D. an unupgraded Propulser is exactly what it was before
|
||||
|
||||
const BASE_DASH_DISTANCE: float = 520.0
|
||||
const BASE_DASH_COOLDOWN: float = 5.0
|
||||
const BASE_BOOST_DRAIN: float = 2.0
|
||||
const COOLDOWN_FLOOR: float = 2.0
|
||||
# Where three Surge Capacitors actually land. Above the floor, deliberately:
|
||||
# the floor is a guard on the definition, not a target the branch reaches.
|
||||
const UPGRADED_COOLDOWN: float = 2.3
|
||||
|
||||
var state: Node
|
||||
var main: Node
|
||||
var fleet: Node
|
||||
var dash: Node
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
state = root.get_node("GameState")
|
||||
state.reset_new_game()
|
||||
state.intro_active = false
|
||||
state.reset_quests()
|
||||
state.vessel_levels["unlock_distorder"] = 1
|
||||
state.fleet_inventory.total_capacity = -1
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
current_scene = main
|
||||
fleet = main.get_node("Fleet")
|
||||
dash = main.get_node("DashController")
|
||||
|
||||
|
||||
func _stat(name: String) -> float:
|
||||
return state.vessel_stat(GameState.VESSEL_DISTORDER, name)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# --- D. nothing researched, nothing changed -------------------
|
||||
if not is_equal_approx(_stat("dash_distance"), BASE_DASH_DISTANCE) \
|
||||
or not is_equal_approx(_stat("dash_cooldown"), BASE_DASH_COOLDOWN) \
|
||||
or not is_equal_approx(_stat("boost_drain"), BASE_BOOST_DRAIN):
|
||||
return _fail("an unresearched Propulser does not match its own definition")
|
||||
print("unupgraded Propulser: %.0f dash, %.1fs recharge, %.1f energy/s" \
|
||||
% [_stat("dash_distance"), _stat("dash_cooldown"), _stat("boost_drain")])
|
||||
|
||||
# --- A. Warp Coils --------------------------------------------
|
||||
state.vessel_levels["dst_dash_range"] = 3
|
||||
if not is_equal_approx(_stat("dash_distance"), BASE_DASH_DISTANCE + 420.0):
|
||||
return _fail("three Warp Coils gave %.0f dash distance, expected %.0f" \
|
||||
% [_stat("dash_distance"), BASE_DASH_DISTANCE + 420.0])
|
||||
# The controller must aim as far as the fleet can actually surge,
|
||||
# or the guide promises reach the dash does not have.
|
||||
if not is_equal_approx(dash._aim_max_range(), _stat("dash_distance")):
|
||||
return _fail("the dash guide reaches %.0f while the dash carries %.0f" \
|
||||
% [dash._aim_max_range(), _stat("dash_distance")])
|
||||
print("Warp Coils: dash and its guide both reach %.0f" % _stat("dash_distance"))
|
||||
|
||||
# --- C. Flux Regulator ----------------------------------------
|
||||
state.vessel_levels["dst_drain"] = 3
|
||||
var expected_drain: float = BASE_BOOST_DRAIN * pow(0.75, 3)
|
||||
if not is_equal_approx(_stat("boost_drain"), expected_drain):
|
||||
return _fail("three Flux Regulators drain %.3f/s, expected %.3f" \
|
||||
% [_stat("boost_drain"), expected_drain])
|
||||
print("Flux Regulator: sustained boost costs %.2f energy/s" % _stat("boost_drain"))
|
||||
|
||||
# --- B. Surge Capacitor, and its floor ------------------------
|
||||
state.vessel_levels["dst_dash_cooldown"] = 2
|
||||
if not is_equal_approx(_stat("dash_cooldown"), BASE_DASH_COOLDOWN - 1.8):
|
||||
return _fail("two Surge Capacitors left a %.2fs recharge, expected %.2f" \
|
||||
% [_stat("dash_cooldown"), BASE_DASH_COOLDOWN - 1.8])
|
||||
state.vessel_levels["dst_dash_cooldown"] = 3
|
||||
if not is_equal_approx(_stat("dash_cooldown"), UPGRADED_COOLDOWN):
|
||||
return _fail("three Surge Capacitors left a %.2fs recharge, expected %.2f" \
|
||||
% [_stat("dash_cooldown"), UPGRADED_COOLDOWN])
|
||||
# Past the branch's own maximum, to prove minimum_stats actually
|
||||
# holds. Nothing in the game can buy this level; raising max_level
|
||||
# later would walk straight into the floor, and this says where it
|
||||
# is rather than leaving it to be discovered.
|
||||
state.vessel_levels["dst_dash_cooldown"] = 6
|
||||
if not is_equal_approx(_stat("dash_cooldown"), COOLDOWN_FLOOR):
|
||||
return _fail("the recharge passed its %.1fs floor, reaching %.2f" \
|
||||
% [COOLDOWN_FLOOR, _stat("dash_cooldown")])
|
||||
state.vessel_levels["dst_dash_cooldown"] = 3
|
||||
print("Surge Capacitor: %.1fs recharge at full research, floored at %.1f" \
|
||||
% [UPGRADED_COOLDOWN, COOLDOWN_FLOOR])
|
||||
|
||||
# A hull built AFTER the research carries the researched recharge:
|
||||
# apply_stats() is what puts a definition on a component, and a
|
||||
# stat that never reaches the component upgrades nothing.
|
||||
fleet.spawn_ships(1, Vector2.ZERO,
|
||||
PackedStringArray([GameState.VESSEL_DISTORDER]))
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
var component: Node = fleet.ships[0].get_component("distorder")
|
||||
if component == null:
|
||||
return _fail("the spawned Propulser has no distorder component")
|
||||
if not is_equal_approx(component.cooldown_duration, UPGRADED_COOLDOWN):
|
||||
return _fail("the hull recharges in %.2fs while its definition says %.2f" \
|
||||
% [component.cooldown_duration, UPGRADED_COOLDOWN])
|
||||
print("the researched recharge reaches the hull itself")
|
||||
print("TEST_PROPULSER_UPGRADES: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_PROPULSER_UPGRADES: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
|
|
@ -179,6 +179,55 @@ func _process(delta: float) -> bool:
|
|||
return _fail("a structure came down in silence")
|
||||
print("a structure coming down is heard")
|
||||
|
||||
# --- G. the things #210/#213/#214 asked to be able to hear -----
|
||||
for voice in _pool().get_children():
|
||||
(voice as AudioStreamPlayer).stop()
|
||||
var rock: Node2D = load("res://world/Asteroid.tscn").instantiate()
|
||||
scene.add_child(rock)
|
||||
rock.shatter(false)
|
||||
if _sounding() == 0:
|
||||
return _fail("an asteroid broke apart in silence")
|
||||
print("a destroyed asteroid is heard")
|
||||
|
||||
for voice in _pool().get_children():
|
||||
(voice as AudioStreamPlayer).stop()
|
||||
var bomb: Node2D = load("res://vfx/GasBombEffect.gd").launch(
|
||||
scene, Vector2.ZERO, Vector2(300.0, 0.0), 6)
|
||||
if _sounding() != 0:
|
||||
return _fail("the gas bomb made its noise on the throw, not the blast")
|
||||
bomb._explode()
|
||||
if _sounding() == 0:
|
||||
return _fail("a gas bomb bloomed in silence")
|
||||
print("a gas bomb is heard when it blooms, and not before")
|
||||
|
||||
# The hold has to be able to take the deposit, or there is no
|
||||
# arrival to hear.
|
||||
game_state.fleet_inventory.total_capacity = -1
|
||||
for voice in _pool().get_children():
|
||||
(voice as AudioStreamPlayer).stop()
|
||||
if game_state.deposit_fleet_resource(GameState.RESOURCE_ORE, 1) != 1:
|
||||
return _fail("the fleet hold refused a single rock")
|
||||
var after_first: int = _sounding()
|
||||
if after_first == 0:
|
||||
return _fail("a rock reached the fleet hold in silence")
|
||||
# Mining lands one unit at a time, so the window is the whole of
|
||||
# what stops this being a machine-gun.
|
||||
game_state.deposit_fleet_resource(GameState.RESOURCE_ORE, 1)
|
||||
game_state.deposit_fleet_resource(GameState.RESOURCE_ORE, 1)
|
||||
if _sounding() != after_first:
|
||||
return _fail("a burst of pickups opened %d voices, not the %d one blip needs" \
|
||||
% [_sounding(), after_first])
|
||||
print("a rock arriving in the hold is heard, and a burst of them only once")
|
||||
|
||||
# The bank is a number moving in a building the player is not in.
|
||||
for voice in _pool().get_children():
|
||||
(voice as AudioStreamPlayer).stop()
|
||||
game_state._pickup_sound_at = 0
|
||||
game_state.deposit_bank_resource(GameState.RESOURCE_ORE, 5)
|
||||
if _sounding() != 0:
|
||||
return _fail("a deposit into the bank made a pickup noise")
|
||||
print("banked resources stay silent")
|
||||
|
||||
print("TEST_SFX: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
|
|
|
|||
Loading…
Reference in a new issue