#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
188 lines
8.8 KiB
GDScript
188 lines
8.8 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
|
|
# #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)
|
|
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
|