Space-time distorder drains energy while boosting (#62)
GameState._advance_distorder_drain() runs in GameState._process() - not the distorder component's tick() - alongside extractor mining and production, so it (and the fleet's travel) keeps working across the space<->surface scene swap. Drain scales with how far above 1.0 the multiplier is: (fleet_speed_multiplier - 1.0) / (MAX - 1.0) * ENERGY_DRAIN_PER_SECOND (2.0 at full boost), accumulated fractionally the same way extractor mining_progress is so small multipliers still drain a nonzero amount instead of rounding to zero every frame. Running the fleet hold's energy to exactly zero force-resets fleet_speed_multiplier to 1.0 and raises a one-shot boost_depleted_notice flag, since GameState can't reach into whichever scene's SpeedFader may or may not currently exist. SpeedFader.gd consumes the flag on its next _process(), snaps its knob down (it was already reading the multiplier live, so this falls out for free) and shows "Out of energy!" - also clearing active_touch_index so a still-held drag can't immediately re-boost from the position the finger never moved away from. test_space_time_distorder.gd (#59) updated to stock fleet energy first, for the same reason #60/#61 needed it: boosting is no longer free, and that test's own multi-second boosted phases would otherwise get cut short mid-measurement by running out.
This commit is contained in:
parent
facbd83dd2
commit
651f8b787e
4 changed files with 211 additions and 1 deletions
38
GameState.gd
38
GameState.gd
|
|
@ -29,6 +29,17 @@ var planet_definition_id: String = "rocky"
|
|||
const MAX_FLEET_SPEED_MULTIPLIER: float = 3.0
|
||||
var fleet_speed_multiplier: float = 1.0
|
||||
|
||||
# #62: energy cost of sustaining a boost, scaled by how far above 1.0 the
|
||||
# multiplier is - a small boost costs proportionally less, full boost costs
|
||||
# ENERGY_DRAIN_PER_SECOND. Sitting at exactly 1.0 costs nothing.
|
||||
const ENERGY_DRAIN_PER_SECOND: float = 2.0
|
||||
var _distorder_drain_progress: float = 0.0
|
||||
# One-shot notice, consumed and cleared by SpeedFader.gd: true the instant
|
||||
# a sustained boost is force-reset for running the fleet hold's energy
|
||||
# to zero, so the control (which may not even be part of the current
|
||||
# scene tree at that exact frame) can show why on its next _process().
|
||||
var boost_depleted_notice: bool = false
|
||||
|
||||
|
||||
func set_fleet_speed_multiplier(value: float) -> void:
|
||||
fleet_speed_multiplier = clampf(value, 1.0, MAX_FLEET_SPEED_MULTIPLIER)
|
||||
|
|
@ -1165,6 +1176,32 @@ func _advance_gas_burners(delta: float) -> void:
|
|||
b.gas_burn_progress = progress
|
||||
|
||||
|
||||
func _advance_distorder_drain(delta: float) -> void:
|
||||
if fleet_speed_multiplier <= 1.0:
|
||||
# Nothing owed at 1.0x - and no stale fractional progress should
|
||||
# carry over into a LATER, unrelated boost.
|
||||
_distorder_drain_progress = 0.0
|
||||
return
|
||||
var rate: float = (fleet_speed_multiplier - 1.0) / (MAX_FLEET_SPEED_MULTIPLIER - 1.0) \
|
||||
* ENERGY_DRAIN_PER_SECOND
|
||||
_distorder_drain_progress += rate * delta
|
||||
var whole: int = int(floor(_distorder_drain_progress))
|
||||
if whole <= 0:
|
||||
return
|
||||
_distorder_drain_progress -= whole
|
||||
var available: int = fleet_inventory.amount(RESOURCE_ENERGY)
|
||||
if available >= whole:
|
||||
fleet_inventory.withdraw(RESOURCE_ENERGY, whole)
|
||||
return
|
||||
# Ran out mid-drain: take whatever is left (down to exactly zero) and
|
||||
# force the boost back to its default, rather than let the fleet keep
|
||||
# travelling fast on credit.
|
||||
fleet_inventory.withdraw(RESOURCE_ENERGY, available)
|
||||
fleet_speed_multiplier = 1.0
|
||||
_distorder_drain_progress = 0.0
|
||||
boost_depleted_notice = true
|
||||
|
||||
|
||||
# Planet conquest: a planet must have every one of its capture points held
|
||||
# before an extractor can be built there (see BuildController's placement
|
||||
# gates). Keyed by the same stable planet_id as `extractors`, entirely
|
||||
|
|
@ -1240,6 +1277,7 @@ func _process(delta: float) -> void:
|
|||
_advance_lab_research(delta)
|
||||
_advance_vessel_deliveries(delta)
|
||||
_advance_gas_burners(delta)
|
||||
_advance_distorder_drain(delta)
|
||||
for e in extractors:
|
||||
e.mining_progress = float(e.get("mining_progress", 0.0)) + delta * EXTRACTOR_RATE
|
||||
var whole: int = int(floor(float(e.mining_progress)))
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ extends Control
|
|||
var active_touch_index: int = -1
|
||||
# 0 = bottom of the track (1.0x), 1 = top (GameState.MAX_FLEET_SPEED_MULTIPLIER).
|
||||
var knob_t: float = 0.0
|
||||
var status_message_until: int = 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -44,6 +45,20 @@ func _process(_delta: float) -> void:
|
|||
return
|
||||
knob_t = (GameState.fleet_speed_multiplier - 1.0) \
|
||||
/ maxf(GameState.MAX_FLEET_SPEED_MULTIPLIER - 1.0, 0.001)
|
||||
# #62: GameState._process() (not this node's own _process()) is what
|
||||
# drains energy and force-resets the multiplier, since it has to keep
|
||||
# running across the space<->surface swap and while this control might
|
||||
# not even be in the current scene tree. It leaves a one-shot flag for
|
||||
# whichever SpeedFader picks it up next.
|
||||
if GameState.boost_depleted_notice:
|
||||
GameState.boost_depleted_notice = false
|
||||
# Still holding the drag at the moment energy ran out would
|
||||
# otherwise re-boost on the very next drag event, from a knob
|
||||
# position the finger never actually moved away from.
|
||||
active_touch_index = -1
|
||||
label.text = "Out of energy!"
|
||||
status_message_until = Time.get_ticks_msec() + 3000
|
||||
elif Time.get_ticks_msec() >= status_message_until:
|
||||
label.text = "x%.1f" % GameState.fleet_speed_multiplier
|
||||
queue_redraw()
|
||||
|
||||
|
|
|
|||
150
tests/test_distorder_energy.gd
Normal file
150
tests/test_distorder_energy.gd
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
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://Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
fader = main.get_node("UI/SpeedFader")
|
||||
fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_DISTORDER]))
|
||||
fader._process(0.0)
|
||||
if not fader.visible:
|
||||
return _fail_now("fader hidden with a distorder ship in the fleet")
|
||||
|
||||
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
|
||||
|
|
@ -37,6 +37,13 @@ func _initialize() -> void:
|
|||
game_state.stored_vessels.clear()
|
||||
game_state.reset_progression()
|
||||
game_state.vessel_levels["unlock_distorder"] = 1
|
||||
# #62: sustaining a boost now drains fleet energy continuously - stock
|
||||
# plenty so this test's own boosted phases aren't cut short by running
|
||||
# out mid-measurement. Set BEFORE the first spawn_ships() call (inside
|
||||
# Main.tscn's own _ready(), triggered by instantiate() below): the fleet
|
||||
# hold has no capacity cap yet at this point, so this can't be clamped
|
||||
# by the soon-to-exist fleet's cargo capability.
|
||||
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 500)
|
||||
main = load("res://Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
|
|
|||
Loading…
Reference in a new issue