Laser shooter vessel and its skill shot #58

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

A new fleet vessel granting a manually aimed laser beam attack. Model it closely on the gas bomber, which is the established pattern for a vessel-granted skill shot.

Reference implementation

Read these together before starting - the gas bomber is the template and its per-ship-cooldown design was hard-won in #45-era work:

  • VesselGasBomberComponent.gd - per-ship cooldown plus the ready/cooling hull-fill visual (_filled_hull)
  • GasBombController.gd - press-drag-release aiming, the MAX_RANGE/DRAG_RADIUS mapping, camera zoom-out while aiming and restoration afterward, and _next_ready_bomber() selecting whichever ship still has a shot
  • GasBombButton in Main.tscn / PlanetSurface.tscn - the button appears only when the fleet has the capability

Scope

  1. VesselLaserComponent.tscn + .gd, capability_id = "laser": its own cooldown, is_ready(), consume_shot(duration), and a ready/cooling hull fill in the laser's colour. Per ship, not per fleet - two laser vessels give two shots (this is the exact bug fixed in eb79efd for bombers; do not reintroduce it).
  2. LaserController.gd + a node in both Main.tscn and PlanetSurface.tscn, plus a UI/LaserButton IconButton beside the rotation controls, visible = fleet.has_capability("laser"), enabled only when some laser vessel is ready.
  3. Aiming: press-drag-release, same input plumbing as the gas bomb (it must cooperate with GameController._handle_touch's existing first-refusal chain - see how gas_bomb_controller.handle_touch is offered the event first). The shot is a straight beam from the firing ship along the drag direction out to MAX_RANGE, damaging every node in group enemies within BEAM_HALF_WIDTH of the segment. Reuse the aim-time camera zoom-out and restoration.
  4. LaserVesselDefinition.tres with capabilities = ["laser"], its own hull and colour, build costs, and ship-lab unlock_* fields. Append any new IconButton.Icon enum entries - inserting one renumbers every icon int already saved in scenes.
  5. Register in VesselDefinitions; add crafter/hangar nodes as described in #57 (same hand-placed constraint).
  6. Ship-lab upgrades for damage/range/cooldown, consistent with other vessels.

Out of scope

Energy consumption per shot - that is #61, which depends on this ticket and on #54 (energy).

Acceptance criteria

  • The button is absent with no laser vessel in the fleet, and appears as soon as one joins.
  • Cooldown is per ship: two laser vessels fire two shots before the control disables.
  • The beam damages every enemy along its line, and nothing off it.
  • Camera zoom-out during aim, and restoration afterward.
  • Works identically in space and on a landed surface.

Tests

tests/test_laser_vessel.gd (bash tests/run_tests.sh laser), plus a 720x1280 screenshot of the aim state with the beam preview drawn.

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 new fleet vessel granting a manually aimed laser beam attack. Model it closely on the gas bomber, which is the established pattern for a vessel-granted skill shot. ### Reference implementation Read these together before starting - the gas bomber is the template and its per-ship-cooldown design was hard-won in #45-era work: - `VesselGasBomberComponent.gd` - per-ship cooldown plus the ready/cooling hull-fill visual (`_filled_hull`) - `GasBombController.gd` - press-drag-release aiming, the `MAX_RANGE`/`DRAG_RADIUS` mapping, camera zoom-out while aiming and restoration afterward, and `_next_ready_bomber()` selecting whichever ship still has a shot - `GasBombButton` in `Main.tscn` / `PlanetSurface.tscn` - the button appears only when the fleet has the capability ### Scope 1. `VesselLaserComponent.tscn` + `.gd`, `capability_id = "laser"`: its own cooldown, `is_ready()`, `consume_shot(duration)`, and a ready/cooling hull fill in the laser's colour. Per **ship**, not per fleet - two laser vessels give two shots (this is the exact bug fixed in `eb79efd` for bombers; do not reintroduce it). 2. `LaserController.gd` + a node in **both** `Main.tscn` and `PlanetSurface.tscn`, plus a `UI/LaserButton` `IconButton` beside the rotation controls, `visible = fleet.has_capability("laser")`, `enabled` only when some laser vessel is ready. 3. Aiming: press-drag-release, same input plumbing as the gas bomb (it must cooperate with `GameController._handle_touch`'s existing first-refusal chain - see how `gas_bomb_controller.handle_touch` is offered the event first). The shot is a straight beam from the firing ship along the drag direction out to `MAX_RANGE`, damaging every node in group `enemies` within `BEAM_HALF_WIDTH` of the segment. Reuse the aim-time camera zoom-out and restoration. 4. `LaserVesselDefinition.tres` with `capabilities = ["laser"]`, its own hull and colour, build costs, and ship-lab `unlock_*` fields. **Append** any new `IconButton.Icon` enum entries - inserting one renumbers every icon int already saved in scenes. 5. Register in `VesselDefinitions`; add crafter/hangar nodes as described in #57 (same hand-placed constraint). 6. Ship-lab upgrades for damage/range/cooldown, consistent with other vessels. ### Out of scope Energy consumption per shot - that is #61, which depends on this ticket and on #54 (energy). ### Acceptance criteria - The button is absent with no laser vessel in the fleet, and appears as soon as one joins. - Cooldown is per ship: two laser vessels fire two shots before the control disables. - The beam damages every enemy along its line, and nothing off it. - Camera zoom-out during aim, and restoration afterward. - Works identically in space and on a landed surface. ### Tests `tests/test_laser_vessel.gd` (`bash tests/run_tests.sh laser`), plus a 720x1280 screenshot of the aim state with the beam preview drawn. ### 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 in ed46cd7.

A manually aimed instant beam, modelled closely on the gas bomber's press-drag-release aiming as instructed.

  • VesselLaserComponent.gd: per-ship cooldown plus the ready/cooling hull-fill visual, same shape as VesselGasBomberComponent. One deliberate difference from the bomber: damage/beam_range/cooldown_duration are resolved from the vessel's own base_stats/upgrades via apply_stats() (the same pattern VesselWeaponComponent uses), not hardcoded in the controller - the ticket's damage/range/cooldown ship-lab upgrades need something real to modify. Verified two laser ships give two shots (cooldown lives on the component, per ship) - the exact bug already fixed for bombers in eb79efd.
  • LaserController.gd: same press-drag-release plumbing as GasBombController (aim-time camera zoom-out via the same fit/restore math, _next_ready_laser_ship() mirroring _next_ready_bomber()), adapted for an instant beam rather than a travelling, timed projectile - the shot lands the moment the touch releases, only a brief flash lingers for feedback. The beam is a straight segment from the firing ship's own position (not the fleet centroid - matches the bomber's identical discrepancy) out to the ship's own beam_range, damaging every node in group enemies within BEAM_HALF_WIDTH of the segment, using the same perpendicular/along-segment math as EnemyBigLaserBeam's mirror-image (enemies -> player) case.
  • One LaserController node reused unmodified by both Main.tscn and PlanetSurface.tscn (identical relative ../Fleet/../Camera2D/../UI/LaserButton paths, matching GasBombController), wired into GameController's touch/drag first-refusal chain right after the bomb controller - works identically in space and on a landed surface with zero branching.
  • LaserVesselDefinition.tres: capabilities = ["laser"], component_scenes = [VesselLaserComponent.tscn] only, its own narrow-dart hull and cyan glow, ship-lab unlock_* fields, and 3 upgrades (LaserDamageUpgrade/LaserRangeUpgrade/LaserCooldownUpgrade, the last with a minimum_stats floor so cooldown can't be upgraded to zero). Two new appended IconButton.Icon entries (LASER_VESSEL ship glyph, LASER_SHOT action-button glyph - verified their actual enum ints empirically rather than hand-counted, same as #57).
  • Registered in VesselDefinitions, appended after carrier (pure append, no existing vessel's index moves). The crafter/hangar/ship-lab menus were already fully generic over VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes are hand-placed, so this repacked those three rows to 5/5/6 columns and extended BuildController's matching @onready arrays. No new signal handlers were needed anywhere.

A genuine regression, same class as #57's: test_vessel_composition.gd's hardcoded fleet-catalog size needed bumping again, 4 -> 5.

A real, non-obvious trap surfaced and documented in GameController.gd: a brand-new class_name script resolves fine as a static type when its own file is the godot -s entry point (proven - test_laser_vessel.gd uses var skill: LaserController successfully), but the identical type hint fails to parse ("Could not find type") when the script holding it is only reached indirectly - e.g. Main.tscn loaded from inside a _screenshot_tool.gd preview, which is exactly how the required screenshot loads it. Fixed by typing GameController.laser_controller as Node with duck-typed calls instead of the strict LaserController type; noted in a comment so it doesn't cost the next brand-new controller class an hour of debugging.

Tests: tests/test_laser_vessel.gd (bash tests/run_tests.sh laser) covers the control appearing/disappearing with fleet composition, the beam hitting exactly the enemies on its line (within beam_half_width, within range) and nothing else, per-ship cooldown (two ships -> two shots -> then disabled), and camera zoom-out/restore. run_tests.sh vessel (5/5, after the fleet-catalog fix), gas_bomber/fleet/cargo/hangar/lab/build/skill/icon/enemy_combat regressions all pass. Full suite: 85/85.

Screenshot taken and visually confirmed (720x1280): aim state with one laser ship ready (fully lit hull) and one rearming (partially filled), the beam-preview line drawn from the fleet to the drag target, and the LaserButton visibly pressed/highlighted in the action cluster.

Implemented in ed46cd7. A manually aimed instant beam, modelled closely on the gas bomber's press-drag-release aiming as instructed. - `VesselLaserComponent.gd`: per-**ship** cooldown plus the ready/cooling hull-fill visual, same shape as `VesselGasBomberComponent`. One deliberate difference from the bomber: `damage`/`beam_range`/`cooldown_duration` are resolved from the vessel's own `base_stats`/upgrades via `apply_stats()` (the same pattern `VesselWeaponComponent` uses), not hardcoded in the controller - the ticket's damage/range/cooldown ship-lab upgrades need something real to modify. Verified two laser ships give two shots (cooldown lives on the component, per ship) - the exact bug already fixed for bombers in `eb79efd`. - `LaserController.gd`: same press-drag-release plumbing as `GasBombController` (aim-time camera zoom-out via the same fit/restore math, `_next_ready_laser_ship()` mirroring `_next_ready_bomber()`), adapted for an **instant** beam rather than a travelling, timed projectile - the shot lands the moment the touch releases, only a brief flash lingers for feedback. The beam is a straight segment from the firing ship's own position (not the fleet centroid - matches the bomber's identical discrepancy) out to the ship's own `beam_range`, damaging every node in group `enemies` within `BEAM_HALF_WIDTH` of the segment, using the same perpendicular/along-segment math as `EnemyBigLaserBeam`'s mirror-image (enemies -> player) case. - One `LaserController` node reused unmodified by both `Main.tscn` and `PlanetSurface.tscn` (identical relative `../Fleet`/`../Camera2D`/`../UI/LaserButton` paths, matching `GasBombController`), wired into `GameController`'s touch/drag first-refusal chain right after the bomb controller - works identically in space and on a landed surface with zero branching. - `LaserVesselDefinition.tres`: `capabilities = ["laser"]`, `component_scenes = [VesselLaserComponent.tscn]` only, its own narrow-dart hull and cyan glow, ship-lab `unlock_*` fields, and 3 upgrades (`LaserDamageUpgrade`/`LaserRangeUpgrade`/`LaserCooldownUpgrade`, the last with a `minimum_stats` floor so cooldown can't be upgraded to zero). Two new appended `IconButton.Icon` entries (`LASER_VESSEL` ship glyph, `LASER_SHOT` action-button glyph - verified their actual enum ints empirically rather than hand-counted, same as #57). - Registered in `VesselDefinitions`, appended after `carrier` (pure append, no existing vessel's index moves). The crafter/hangar/ship-lab menus were already fully generic over `VesselDefinitions.fleet()`/`.all()` - only the per-vessel `Main.tscn` nodes are hand-placed, so this repacked those three rows to 5/5/6 columns and extended `BuildController`'s matching `@onready` arrays. No new signal handlers were needed anywhere. A genuine regression, same class as #57's: `test_vessel_composition.gd`'s hardcoded fleet-catalog size needed bumping again, 4 -> 5. A real, non-obvious trap surfaced and documented in `GameController.gd`: a **brand-new** `class_name` script resolves fine as a static type when its own file is the `godot -s` entry point (proven - `test_laser_vessel.gd` uses `var skill: LaserController` successfully), but the identical type hint fails to parse ("Could not find type") when the script holding it is only reached *indirectly* - e.g. `Main.tscn` loaded from inside a `_screenshot_tool.gd` preview, which is exactly how the required screenshot loads it. Fixed by typing `GameController.laser_controller` as `Node` with duck-typed calls instead of the strict `LaserController` type; noted in a comment so it doesn't cost the next brand-new controller class an hour of debugging. Tests: `tests/test_laser_vessel.gd` (`bash tests/run_tests.sh laser`) covers the control appearing/disappearing with fleet composition, the beam hitting exactly the enemies on its line (within `beam_half_width`, within range) and nothing else, per-ship cooldown (two ships -> two shots -> then disabled), and camera zoom-out/restore. `run_tests.sh vessel` (5/5, after the fleet-catalog fix), `gas_bomber`/`fleet`/`cargo`/`hangar`/`lab`/`build`/`skill`/`icon`/`enemy_combat` regressions all pass. Full suite: 85/85. Screenshot taken and visually confirmed (720x1280): aim state with one laser ship ready (fully lit hull) and one rearming (partially filled), the beam-preview line drawn from the fleet to the drag target, and the `LaserButton` visibly pressed/highlighted in the action cluster.
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#58
No description provided.