spaceship/tests/test_distorder_energy.gd
2026-09-04 20:01:56 +02:00

184 lines
8.5 KiB
GDScript

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_distorder_energy.gd
# Forgejo #62: sustaining a boost above the fleet's default speed drains
# energy from the fleet hold, continuously, proportional to how far above
# 1.0x the multiplier is.
#
# Most of this is pure GameState math (GameState._advance_distorder_drain,
# called directly the same way other tests call _advance_lab_research/
# _advance_production) - no Main.tscn/Fleet needed for it, which is itself
# proof the drain is scene-independent (it lives in the autoload's own
# _process(), unlike a component's tick() which only runs while its ship's
# scene is loaded) and so "continues correctly across a landing and
# return" without a scene-swap simulation to re-prove that.
#
# Covers:
# A. sustained boost drains energy at the configured rate; half boost
# drains proportionally less than full boost
# B. a small multiplier still drains a nonzero amount over time - no
# fractional rounding loss
# D. at exactly 1.0x, energy is never consumed
# C. hitting zero energy forces the multiplier back to 1.0, and the
# SpeedFader visibly follows it down with the reason shown
var game_state: Node
var main: Node
var fleet: Node
var fader: Node
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.reset_progression()
game_state.fleet_speed_multiplier = 1.0
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 1000)
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
# --- D. nothing drains at exactly 1.0x ----------------------------
var before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY)
for i in range(60):
game_state._advance_distorder_drain(1.0)
if game_state.fleet_resource(GameState.RESOURCE_ENERGY) != before:
return _fail_now("energy drained at 1.0x: %d -> %d" \
% [before, game_state.fleet_resource(GameState.RESOURCE_ENERGY)])
print("no drain at 1.0x, even over 60 simulated seconds")
# --- A. full boost drains at the configured rate ------------------
game_state.fleet_speed_multiplier = GameState.MAX_FLEET_SPEED_MULTIPLIER
var full_before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY)
for i in range(10):
game_state._advance_distorder_drain(1.0)
var full_drained: int = full_before - game_state.fleet_resource(GameState.RESOURCE_ENERGY)
if full_drained != 20:
return _fail_now("full boost drained %d over 10s, expected 20 (ENERGY_DRAIN_PER_SECOND=2.0)" \
% full_drained)
print("full boost drained %d over 10s, matches ENERGY_DRAIN_PER_SECOND" % full_drained)
# --- A continued: half boost drains proportionally less -----------
game_state.fleet_speed_multiplier = 2.0
var half_before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY)
for i in range(10):
game_state._advance_distorder_drain(1.0)
var half_drained: int = half_before - game_state.fleet_resource(GameState.RESOURCE_ENERGY)
if half_drained <= 0 or half_drained >= full_drained:
return _fail_now("half boost drained %d over 10s, expected between 0 and full's %d" \
% [half_drained, full_drained])
print("half boost drained %d over 10s - less than full's %d, still nonzero" \
% [half_drained, full_drained])
# --- B. a small multiplier still drains a nonzero amount ----------
game_state.fleet_speed_multiplier = 1.05
var small_before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY)
for i in range(600):
game_state._advance_distorder_drain(0.1)
var small_drained: int = small_before - game_state.fleet_resource(GameState.RESOURCE_ENERGY)
if small_drained <= 0:
return _fail_now("a small (1.05x) sustained boost drained nothing over 60s - fractional rounding loss")
print("small boost (1.05x) still drained %d over 60s (no rounding loss)" % small_drained)
# --- C. running out resets the multiplier and raises the notice ---
game_state.withdraw_fleet_resource(GameState.RESOURCE_ENERGY,
game_state.fleet_resource(GameState.RESOURCE_ENERGY))
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 5)
game_state.fleet_speed_multiplier = GameState.MAX_FLEET_SPEED_MULTIPLIER
game_state.boost_depleted_notice = false
for i in range(10):
game_state._advance_distorder_drain(1.0)
if game_state.fleet_resource(GameState.RESOURCE_ENERGY) != 0:
return _fail_now("fleet energy did not reach exactly 0, got %d" \
% game_state.fleet_resource(GameState.RESOURCE_ENERGY))
if not is_equal_approx(game_state.fleet_speed_multiplier, 1.0):
return _fail_now("multiplier was not forced back to 1.0 after running out: %f" \
% game_state.fleet_speed_multiplier)
if not game_state.boost_depleted_notice:
return _fail_now("no depleted notice was raised when energy ran out")
print("running out of energy dropped the multiplier to 1.0 and raised the notice")
stage = 1
elapsed = 0.0
return false
1:
# --- C continued: the SpeedFader visibly follows it down ----------
if elapsed < 0.2:
return false
main = load("res://world/Main.tscn").instantiate()
root.add_child(main)
fleet = main.get_node("Fleet")
fader = main.get_node("UI/SpeedFader")
# A carrier supplies the hold the Propulser burns from. The previous
# one-Propulser fixture had zero cargo capacity, so its depletion check
# accidentally began with no energy regardless of what it deposited.
fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([
GameState.VESSEL_CARRIER, GameState.VESSEL_DISTORDER]))
fader._process(0.0)
if not fader.visible:
return _fail_now("fader hidden with a distorder ship in the fleet")
if not bool(ProjectSettings.get_setting(
"physics/common/physics_interpolation", false)):
return _fail_now("physics interpolation is disabled for boosted flight")
if fleet.ships[0].physics_interpolation_mode \
!= Node.PHYSICS_INTERPOLATION_MODE_ON:
return _fail_now("the boosted vessel is not physics-interpolated")
if main.get_node("Camera2D").physics_interpolation_mode \
!= Node.PHYSICS_INTERPOLATION_MODE_ON:
return _fail_now("the fleet camera is not physics-interpolated")
# #199: choosing a higher cruise speed is free while parked. Once a
# heading is active, the exact same multiplier resumes its normal drain.
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 30)
game_state.set_fleet_speed_multiplier(GameState.MAX_FLEET_SPEED_MULTIPLIER)
var parked_energy: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY)
game_state._advance_distorder_drain(5.0,
game_state._distorder_boost_in_use())
if game_state.fleet_resource(GameState.RESOURCE_ENERGY) != parked_energy:
return _fail_now("a parked fleet consumed boost energy")
fleet.move_in_direction(Vector2.RIGHT)
if not fleet.is_using_speed_boost():
return _fail_now("boosted cruise did not report its energy use")
game_state._advance_distorder_drain(1.0,
game_state._distorder_boost_in_use())
if game_state.fleet_resource(GameState.RESOURCE_ENERGY) >= parked_energy:
return _fail_now("moving at boosted cruise consumed no energy")
fleet.stop()
print("parked boost was free; moving boost resumed drain")
game_state.withdraw_fleet_resource(GameState.RESOURCE_ENERGY,
game_state.fleet_resource(GameState.RESOURCE_ENERGY))
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 4)
game_state.set_fleet_speed_multiplier(GameState.MAX_FLEET_SPEED_MULTIPLIER)
fader._process(0.0)
if fader.knob_t < 0.9:
return _fail_now("fader knob did not reflect the boosted multiplier before draining: %f" \
% fader.knob_t)
for i in range(10):
game_state._advance_distorder_drain(1.0)
if not is_equal_approx(game_state.fleet_speed_multiplier, 1.0):
return _fail_now("multiplier was not forced back to 1.0 after running out (with a live fleet)")
fader._process(0.0)
if fader.knob_t > 0.05:
return _fail_now("fader knob did not snap down after the boost was depleted: %f" % fader.knob_t)
if fader.label.text != "Out of energy!":
return _fail_now("fader did not show the depleted reason, text was '%s'" % fader.label.text)
if game_state.boost_depleted_notice:
return _fail_now("boost_depleted_notice was not consumed by the fader")
print("fader visibly snapped its knob down and explained the depletion")
print("TEST_DISTORDER_ENERGY: PASS")
quit(0)
return true
return false
func _fail_now(message: String) -> bool:
print("TEST_DISTORDER_ENERGY: FAIL (%s)" % message)
quit(1)
return true