Space-time distorder vessel and the fleet speed fader #59

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

A researchable vessel that lets the whole fleet travel faster than its default cruise speed, controlled by a fader that appears on the right of the screen while such a vessel is in the fleet.

Scope

  1. VesselDistorderComponent.tscn + .gd, capability_id = "distorder". It carries no per-shot behaviour; its presence is what matters, plus (later ticket) energy drain.
  2. GameState.fleet_speed_multiplier: float = 1.0 (1.0 = today's behaviour), clamped to [1.0, MAX_MULTIPLIER] (suggest 3.0).
    • Apply it on top of existing stats so upgrades still compose: Ship.apply_vessel_stats() reads GameState.vessel_stat(...), and Fleet.cruise_speed drives the formation anchor. Multiply at the point of use rather than baking it into the stored stats, or researching a speed upgrade while boosted will corrupt the base value.
    • Read Fleet.gd's header comment first: the anchor advances at cruise_speed while individual ships are allowed to exceed it to hold formation. Both need scaling, or the formation visibly stretches under boost.
  3. SpeedFader.gd + a node on the right edge of the UI layer in both Main.tscn and PlanetSurface.tscn: a vertical drag control in the existing touch idiom (see TouchJoystick.gd for raw-touch handling and why these controls do not go through _gui_input), drawn with NeonDraw. Visible only when fleet.has_capability("distorder").
  4. DistorderVesselDefinition.tres + ship-lab unlock, registered in VesselDefinitions, with crafter/hangar nodes per #57's note.
  5. Losing the last distorder vessel resets fleet_speed_multiplier to 1.0 and hides the fader - a fleet must never be left permanently boosted by a vessel it no longer has.

Out of scope

Energy consumption while boosting - that is #62.

Acceptance criteria

  • The fader is absent without a distorder vessel and appears when one joins.
  • Dragging it up measurably increases how far the fleet travels per second; at 1.0 the fleet moves exactly as it does today.
  • Formation still holds together under boost - ships are not left trailing.
  • Removing the vessel resets the multiplier and hides the control.
  • The multiplier survives the space <-> surface scene swap (it lives in the autoload).

Tests

tests/test_space_time_distorder.gd (bash tests/run_tests.sh distorder) measuring actual distance travelled per second at 1.0 vs a boosted multiplier, plus a 720x1280 screenshot of the fader.

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.
A researchable vessel that lets the whole fleet travel faster than its default cruise speed, controlled by a fader that appears on the right of the screen while such a vessel is in the fleet. ### Scope 1. `VesselDistorderComponent.tscn` + `.gd`, `capability_id = "distorder"`. It carries no per-shot behaviour; its presence is what matters, plus (later ticket) energy drain. 2. `GameState.fleet_speed_multiplier: float = 1.0` (1.0 = today's behaviour), clamped to `[1.0, MAX_MULTIPLIER]` (suggest 3.0). - Apply it **on top of** existing stats so upgrades still compose: `Ship.apply_vessel_stats()` reads `GameState.vessel_stat(...)`, and `Fleet.cruise_speed` drives the formation anchor. Multiply at the point of use rather than baking it into the stored stats, or researching a speed upgrade while boosted will corrupt the base value. - Read `Fleet.gd`'s header comment first: the anchor advances at `cruise_speed` while individual ships are *allowed* to exceed it to hold formation. Both need scaling, or the formation visibly stretches under boost. 3. `SpeedFader.gd` + a node on the right edge of the `UI` layer in **both** `Main.tscn` and `PlanetSurface.tscn`: a vertical drag control in the existing touch idiom (see `TouchJoystick.gd` for raw-touch handling and why these controls do not go through `_gui_input`), drawn with `NeonDraw`. Visible only when `fleet.has_capability("distorder")`. 4. `DistorderVesselDefinition.tres` + ship-lab unlock, registered in `VesselDefinitions`, with crafter/hangar nodes per #57's note. 5. Losing the last distorder vessel resets `fleet_speed_multiplier` to 1.0 and hides the fader - a fleet must never be left permanently boosted by a vessel it no longer has. ### Out of scope Energy consumption while boosting - that is #62. ### Acceptance criteria - The fader is absent without a distorder vessel and appears when one joins. - Dragging it up measurably increases how far the fleet travels per second; at 1.0 the fleet moves exactly as it does today. - Formation still holds together under boost - ships are not left trailing. - Removing the vessel resets the multiplier and hides the control. - The multiplier survives the space <-> surface scene swap (it lives in the autoload). ### Tests `tests/test_space_time_distorder.gd` (`bash tests/run_tests.sh distorder`) measuring actual distance travelled per second at 1.0 vs a boosted multiplier, plus a 720x1280 screenshot of the fader. ### 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 116e0f9.

GameState.fleet_speed_multiplier (1.0-3.0, MAX_FLEET_SPEED_MULTIPLIER) is applied at the point of use - Fleet's cruise anchor and Ship's own max_speed/acceleration - rather than baked into stored stats, so a stat upgrade researched mid-boost can't corrupt the base value. Ship.arrive_radius is also scaled by the multiplier: at higher speeds each physics tick covers a proportionally larger step, and once that step outgrows a fixed arrive_radius the ship can overshoot its snap-to-target zone every tick while chasing a target that's also now receding faster - visible as the fleet stretching apart under boost. Scaling it preserves the same tightness 1.0x already has.

SpeedFader.gd: a self-contained vertical drag control (TouchJoystick's raw-touch idiom, not _gui_input) that writes straight to the autoload. Resets the multiplier to 1.0 the instant fleet.has_capability("distorder") goes false, so a fleet can never be left permanently boosted by a vessel it no longer has.

VesselDistorderComponent carries no per-shot behaviour (energy drain while boosted is #62) - its presence is what matters. Registered in VesselDefinitions, wired into the crafter/hangar/ship-lab menus.

Tests: tests/test_space_time_distorder.gd (bash tests/run_tests.sh distorder) - fader absent/visible with fleet composition, boosted vs baseline travel distance, formation holds together under sustained boost, losing the vessel resets the multiplier and hides the fader, and the multiplier/fader both survive a space -> surface scene swap. Full suite passed at the time.

Implemented and pushed in `116e0f9`. `GameState.fleet_speed_multiplier` (1.0-3.0, `MAX_FLEET_SPEED_MULTIPLIER`) is applied at the point of use - `Fleet`'s cruise anchor and `Ship`'s own `max_speed`/`acceleration` - rather than baked into stored stats, so a stat upgrade researched mid-boost can't corrupt the base value. `Ship.arrive_radius` is also scaled by the multiplier: at higher speeds each physics tick covers a proportionally larger step, and once that step outgrows a fixed `arrive_radius` the ship can overshoot its snap-to-target zone every tick while chasing a target that's also now receding faster - visible as the fleet stretching apart under boost. Scaling it preserves the same tightness 1.0x already has. `SpeedFader.gd`: a self-contained vertical drag control (`TouchJoystick`'s raw-touch idiom, not `_gui_input`) that writes straight to the autoload. Resets the multiplier to 1.0 the instant `fleet.has_capability("distorder")` goes false, so a fleet can never be left permanently boosted by a vessel it no longer has. `VesselDistorderComponent` carries no per-shot behaviour (energy drain while boosted is #62) - its presence is what matters. Registered in `VesselDefinitions`, wired into the crafter/hangar/ship-lab menus. Tests: `tests/test_space_time_distorder.gd` (`bash tests/run_tests.sh distorder`) - fader absent/visible with fleet composition, boosted vs baseline travel distance, formation holds together under sustained boost, losing the vessel resets the multiplier and hides the fader, and the multiplier/fader both survive a space -> surface scene swap. Full suite passed at the time.
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#59
No description provided.