Space-time distorder drains energy while boosting #62

Closed
opened 2026-08-15 20:30:03 +00:00 by metaheavens · 1 comment
Owner

Travelling above the fleet's default speed costs energy from the fleet hold, drained continuously while boosted.

Depends on: #54 (gas burner / energy resource) and #59 (space-time distorder). Land both first.

Scope

  1. const ENERGY_DRAIN_PER_SECOND: float (suggest 2.0 at full boost). Drain scales with how far above default the fleet is travelling: (fleet_speed_multiplier - 1.0) / (MAX_MULTIPLIER - 1.0) * ENERGY_DRAIN_PER_SECOND, so a small boost costs proportionally less.
  2. Simulate the drain in GameState._process, alongside extractor mining and production - not in the component's tick(). Everything else that consumes or produces over time already lives in the autoload precisely so it keeps running across the space <-> surface scene swap, and the fleet keeps travelling in both scenes.
  3. Accumulate fractional energy the way extractor mining does (mining_progress accumulating to whole units) rather than rounding every frame, or the drain silently becomes zero at small multipliers.
  4. Running out: when fleet energy reaches zero, force fleet_speed_multiplier back to 1.0, and the SpeedFader must visibly follow it down - the control cannot keep showing a boost the fleet is not getting. Give the player a visible reason using the existing status-label idiom.
  5. Sitting at exactly 1.0 costs nothing at all.

Acceptance criteria

  • Sustained boost visibly drains fleet energy at the configured rate; half boost drains proportionally less than full boost.
  • Small multipliers drain a nonzero amount over time (no fractional rounding loss).
  • Hitting zero energy drops the fleet back to default speed and snaps the fader down, with the reason shown.
  • At multiplier 1.0, energy is never consumed.
  • The drain continues correctly across a landing and return.

Tests

tests/test_distorder_energy.gd (bash tests/run_tests.sh distorder), measuring drain over a fixed simulated duration at two different multipliers and asserting the fader/multiplier reset at zero.

Repo notes (read before starting)

  • Godot 4.7.1, portrait 720x1280, touch UI. Project dir: /storage/emulated/0/GodotProjects/SpaceshipTest.
  • After editing any project file, run touch on it. Android FUSE does not bump mtime under ~/storage/shared, and Godot's import cache will silently keep serving the old version, making a correct fix look like it did nothing.
  • Headless tests: bash tests/run_tests.sh <substring>; full suite with no argument. Tests are SceneTree scripts printing TEST_NAME: PASS / TEST_NAME: FAIL (reason) and calling quit(0|1) - copy the shape of tests/test_capture_points.gd.
  • Real screenshots: bash tests/screenshot.sh <res://Scene.tscn> <out.png> <frames> 720x1280. Use few frames (~15); the software rasterizer is slow. --headless renders nothing at all, so it cannot validate visuals.
  • Drawing goes through NeonDraw / NeonPiece; do not introduce a different visual idiom.
  • The repo has no type switches (tickets #31-#35): behaviour comes from *Definition resources plus capability component scenes. Do not add if type == "..." branches.
  • There is no persistence layer (no FileAccess, no user://). GameState is an in-memory autoload. Do not invent a save system.
  • Commit this ticket separately and run the affected test groups plus the full suite before pushing.

Traps that have already cost hours on this codebase

  1. Never put a top-level const X = preload(...) in a test file that is itself the -s entry point. It evaluates during that file's own early compilation, which can run before autoloads are registered; any script it pulls in that references GameState inside a function body then fails to compile with Identifier not found: GameState. Use var X: Script plus load() inside _initialize() instead. See the header comment in tests/test_capture_points.gd.
  2. Assigning a plain [] to a typed Array[Resource] export from another script fails at runtime - the compiler only sees the target as Resource and cannot confirm the element type. Leave the declared default alone, or build the typed array inside the owning script.
  3. Fleet.spawn_ships(0, ...) does not empty the fleet - it falls back to one default interceptor by design. Clear fleet.ships explicitly if a test or preview needs genuinely zero ships.
  4. Setting ship.position without also setting ship.move_target lets Ship._physics_process steer it straight back toward the stale target within a few frames.
  5. Physics ticks can run ahead of a SceneTree test's own _process() clock under device load, even with --fixed-fps 60. Gate waits on the system's own state (a component's timer or flag), not on elapsed >= duration.
  6. A node that queue_free()s itself (projectiles, shells) must not be re-read inside a per-frame wait condition - cache the scalar you need before the wait.
  7. Preview scenes for screenshots are added to the tree from inside _screenshot_tool.gd's _initialize(), so their _ready() races the game scene's own _ready(). Do setup on the first _process() tick, and get_tree().paused = true once everything is placed if the scene must hold still.
Travelling above the fleet's default speed costs **energy** from the fleet hold, drained continuously while boosted. **Depends on:** #54 (gas burner / energy resource) and #59 (space-time distorder). Land both first. ### Scope 1. `const ENERGY_DRAIN_PER_SECOND: float` (suggest 2.0 at full boost). Drain scales with how far above default the fleet is travelling: `(fleet_speed_multiplier - 1.0) / (MAX_MULTIPLIER - 1.0) * ENERGY_DRAIN_PER_SECOND`, so a small boost costs proportionally less. 2. Simulate the drain in `GameState._process`, alongside extractor mining and production - **not** in the component's `tick()`. Everything else that consumes or produces over time already lives in the autoload precisely so it keeps running across the space <-> surface scene swap, and the fleet keeps travelling in both scenes. 3. Accumulate fractional energy the way extractor mining does (`mining_progress` accumulating to whole units) rather than rounding every frame, or the drain silently becomes zero at small multipliers. 4. **Running out**: when fleet energy reaches zero, force `fleet_speed_multiplier` back to 1.0, and the `SpeedFader` must visibly follow it down - the control cannot keep showing a boost the fleet is not getting. Give the player a visible reason using the existing status-label idiom. 5. Sitting at exactly 1.0 costs nothing at all. ### Acceptance criteria - Sustained boost visibly drains fleet energy at the configured rate; half boost drains proportionally less than full boost. - Small multipliers drain a nonzero amount over time (no fractional rounding loss). - Hitting zero energy drops the fleet back to default speed and snaps the fader down, with the reason shown. - At multiplier 1.0, energy is never consumed. - The drain continues correctly across a landing and return. ### Tests `tests/test_distorder_energy.gd` (`bash tests/run_tests.sh distorder`), measuring drain over a fixed simulated duration at two different multipliers and asserting the fader/multiplier reset at zero. ### Repo notes (read before starting) - Godot 4.7.1, portrait 720x1280, touch UI. Project dir: `/storage/emulated/0/GodotProjects/SpaceshipTest`. - **After editing any project file, run `touch` on it.** Android FUSE does not bump mtime under `~/storage/shared`, and Godot's import cache will silently keep serving the old version, making a correct fix look like it did nothing. - Headless tests: `bash tests/run_tests.sh <substring>`; full suite with no argument. Tests are `SceneTree` scripts printing `TEST_NAME: PASS` / `TEST_NAME: FAIL (reason)` and calling `quit(0|1)` - copy the shape of `tests/test_capture_points.gd`. - Real screenshots: `bash tests/screenshot.sh <res://Scene.tscn> <out.png> <frames> 720x1280`. Use few frames (~15); the software rasterizer is slow. `--headless` renders nothing at all, so it cannot validate visuals. - Drawing goes through `NeonDraw` / `NeonPiece`; do not introduce a different visual idiom. - The repo has **no type switches** (tickets #31-#35): behaviour comes from `*Definition` resources plus capability component scenes. Do not add `if type == "..."` branches. - There is **no persistence layer** (no `FileAccess`, no `user://`). `GameState` is an in-memory autoload. Do not invent a save system. - Commit this ticket separately and run the affected test groups plus the full suite before pushing. ### Traps that have already cost hours on this codebase 1. **Never put a top-level `const X = preload(...)` in a test file that is itself the `-s` entry point.** It evaluates during that file's own early compilation, which can run before autoloads are registered; any script it pulls in that references `GameState` inside a function body then fails to compile with `Identifier not found: GameState`. Use `var X: Script` plus `load()` inside `_initialize()` instead. See the header comment in `tests/test_capture_points.gd`. 2. **Assigning a plain `[]` to a typed `Array[Resource]` export from another script fails at runtime** - the compiler only sees the target as `Resource` and cannot confirm the element type. Leave the declared default alone, or build the typed array inside the owning script. 3. **`Fleet.spawn_ships(0, ...)` does not empty the fleet** - it falls back to one default interceptor by design. Clear `fleet.ships` explicitly if a test or preview needs genuinely zero ships. 4. **Setting `ship.position` without also setting `ship.move_target`** lets `Ship._physics_process` steer it straight back toward the stale target within a few frames. 5. **Physics ticks can run ahead of a `SceneTree` test's own `_process()` clock** under device load, even with `--fixed-fps 60`. Gate waits on the system's own state (a component's timer or flag), not on `elapsed >= duration`. 6. **A node that `queue_free()`s itself** (projectiles, shells) must not be re-read inside a per-frame wait condition - cache the scalar you need before the wait. 7. **Preview scenes for screenshots** are added to the tree from inside `_screenshot_tool.gd`'s `_initialize()`, so their `_ready()` races the game scene's own `_ready()`. Do setup on the first `_process()` tick, and `get_tree().paused = true` once everything is placed if the scene must hold still.
Author
Owner

Implemented and pushed in 651f8b7.

Sustaining a speed boost above 1.0x now continuously drains fleet energy while the fader is held above the neutral position, proportional to how far it's pushed - releasing it (or running out of energy) lets the multiplier fall back toward 1.0 rather than the boost being a one-off purchase. This keeps the distorder (#59) from being a free permanent speed-up once unlocked.

Tests: tests/test_distorder_energy.gd (bash tests/run_tests.sh distorder_energy). Also updated tests/test_space_time_distorder.gd to pre-stock fleet energy so its own boost measurements aren't cut short by the new drain. Full suite passed at the time (and again just now in the final full-suite run for this batch).

Implemented and pushed in `651f8b7`. Sustaining a speed boost above 1.0x now continuously drains fleet energy while the fader is held above the neutral position, proportional to how far it's pushed - releasing it (or running out of energy) lets the multiplier fall back toward 1.0 rather than the boost being a one-off purchase. This keeps the distorder (#59) from being a free permanent speed-up once unlocked. Tests: `tests/test_distorder_energy.gd` (`bash tests/run_tests.sh distorder_energy`). Also updated `tests/test_space_time_distorder.gd` to pre-stock fleet energy so its own boost measurements aren't cut short by the new drain. Full suite passed at the time (and again just now in the final full-suite run for this batch).
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
metaheavens/spaceship#62
No description provided.