Laser shooters consume energy from the fleet hold when firing #61

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

Each laser shot costs energy from the fleet hold, the same way gas bomb shots cost gas.

Depends on: #54 (gas burner / energy resource - energy must exist) and #58 (laser shooter vessel - the weapon must exist). Land both first.

Scope

Mirror #60 (gas bomber ammunition) exactly, so the two skill shots behave identically from the player's point of view:

  1. const ENERGY_PER_SHOT: int on LaserController (suggest 8).
  2. In LaserController's fire path: require and withdraw ENERGY_PER_SHOT from GameState.fleet_inventory before consuming the firing ship's cooldown. On failure, fire nothing, consume nothing, cancel the aim.
  3. The laser button's enabled rule requires both a ready laser vessel and affordable energy.
  4. Refusal is explained through the same status-label idiom used for the gas bomber, so both weapons report shortages the same way.

Acceptance criteria

  • Firing drains exactly ENERGY_PER_SHOT from the fleet hold.
  • With insufficient fleet energy the button is disabled, pressing it explains why, and no beam is fired and no cooldown consumed.
  • Banked energy does not enable firing - only the fleet hold.
  • Behaviour is consistent with the gas bomber's ammunition rules; if the two diverge, prefer changing this one to match.

Tests

tests/test_laser_ammo.gd (bash tests/run_tests.sh laser).

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.
Each laser shot costs **energy** from the fleet hold, the same way gas bomb shots cost gas. **Depends on:** #54 (gas burner / energy resource - energy must exist) and #58 (laser shooter vessel - the weapon must exist). Land both first. ### Scope Mirror #60 (gas bomber ammunition) exactly, so the two skill shots behave identically from the player's point of view: 1. `const ENERGY_PER_SHOT: int` on `LaserController` (suggest 8). 2. In `LaserController`'s fire path: require and withdraw `ENERGY_PER_SHOT` from `GameState.fleet_inventory` before consuming the firing ship's cooldown. On failure, fire nothing, consume nothing, cancel the aim. 3. The laser button's `enabled` rule requires both a ready laser vessel and affordable energy. 4. Refusal is explained through the same status-label idiom used for the gas bomber, so both weapons report shortages the same way. ### Acceptance criteria - Firing drains exactly `ENERGY_PER_SHOT` from the fleet hold. - With insufficient fleet energy the button is disabled, pressing it explains why, and no beam is fired and no cooldown consumed. - Banked energy does not enable firing - only the fleet hold. - Behaviour is consistent with the gas bomber's ammunition rules; if the two diverge, prefer changing this one to match. ### Tests `tests/test_laser_ammo.gd` (`bash tests/run_tests.sh laser`). ### 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 facbd83.

Mirrors #60's gas bomber ammunition exactly: ENERGY_PER_SHOT (8) drawn from GameState.fleet_inventory before consuming the firing ship's cooldown, the laser button's enabled rule requires affordability too, and an unaffordable-but-otherwise-ready press still explains itself through a new LaserStatus label rather than silently doing nothing.

Added two small generic GameState helpers this needed: deposit_fleet_resource()/deposit_bank_resource(), counterparts to #60's withdraw_fleet_resource() for resources (energy) with no dedicated fleet_cargo/fleet_gas_cargo-style property of their own.

Tests: tests/test_laser_ammo.gd (bash tests/run_tests.sh laser) - banked energy does not enable firing (only the fleet hold counts), insufficient fleet energy disables the button and explains itself with no cooldown/energy spent, and firing drains exactly ENERGY_PER_SHOT with the bank left untouched. Full suite passed at the time.

Implemented and pushed in `facbd83`. Mirrors #60's gas bomber ammunition exactly: `ENERGY_PER_SHOT` (8) drawn from `GameState.fleet_inventory` before consuming the firing ship's cooldown, the laser button's `enabled` rule requires affordability too, and an unaffordable-but-otherwise-ready press still explains itself through a new `LaserStatus` label rather than silently doing nothing. Added two small generic `GameState` helpers this needed: `deposit_fleet_resource()`/`deposit_bank_resource()`, counterparts to #60's `withdraw_fleet_resource()` for resources (energy) with no dedicated `fleet_cargo`/`fleet_gas_cargo`-style property of their own. Tests: `tests/test_laser_ammo.gd` (`bash tests/run_tests.sh laser`) - banked energy does not enable firing (only the fleet hold counts), insufficient fleet energy disables the button and explains itself with no cooldown/energy spent, and firing drains exactly `ENERGY_PER_SHOT` with the bank left untouched. 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#61
No description provided.