Carrier vessel #57

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

A fleet vessel that cannot shoot but substantially raises how much the fleet can carry.

Depends on #56 (limit how much the fleet can carry) - the carrier is meaningless until a cap exists.

Scope

  1. CarrierVesselDefinition.tres:
    • definition_id = "carrier", fleet_eligible = true
    • capabilities = ["cargo"] and component_scenes = [VesselCargoComponent.tscn] only - no VesselWeaponComponent, so it structurally cannot fire. This is exactly how the shield vessel ended up weaponless after #35; do not add an if anywhere to suppress firing.
    • base_stats: a large cargo_capacity (suggest 120, i.e. six interceptors' worth), slightly lower max_speed/acceleration than an interceptor, plus the delivery_speed every vessel carries.
    • A wide, distinctly non-combat hull silhouette and its own glow colour.
    • unlock_id / unlock_name / unlock_description / unlock_resource_costs so it is researched at a ship lab like the shield vessel and gas bomber.
  2. Register in VesselDefinitions.ORDER and DEFINITIONS.
  3. Menu wiring - note this is still hand-placed per vessel. BuildController resolves crafter and hangar entries by fixed node path (../UI/CraftMenu/ShieldButton, ../UI/HangarMenu/AvailableGasBomber, FleetGasBomber, and their *Label siblings). Add the matching CarrierButton/CarrierLabel, AvailableCarrier/AvailableCarrierLabel, FleetCarrier/FleetCarrierLabel nodes to Main.tscn and extend those arrays.
    • Worth flagging for a future ticket rather than doing here: those two menus are the last per-vessel hardcoded surfaces left after #35, and would be better generated from VesselDefinitions.fleet().
  4. A ship-lab upgrade raising carrier capacity, consistent with every other vessel having upgrades (VesselUpgradeDefinition, additive_modifiers on cargo_capacity).

Acceptance criteria

  • Crafting a carrier and crewing it raises GameState.fleet_cargo_capacity() by its stat.
  • A carrier never fires: it has no weapon component and no projectile ever originates from it, verified with an enemy parked inside interceptor fire range.
  • It appears in the crafter, the hangar, and the ship lab with correct name, cost, icon and colour.
  • It is unbuildable until researched.

Tests

tests/test_carrier_vessel.gd (bash tests/run_tests.sh carrier), plus a 720x1280 screenshot of the crafter and hangar showing the new entry laid out cleanly alongside the existing vessels.

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 fleet vessel that cannot shoot but substantially raises how much the fleet can carry. **Depends on #56 (limit how much the fleet can carry)** - the carrier is meaningless until a cap exists. ### Scope 1. `CarrierVesselDefinition.tres`: - `definition_id = "carrier"`, `fleet_eligible = true` - `capabilities = ["cargo"]` and `component_scenes = [VesselCargoComponent.tscn]` **only** - no `VesselWeaponComponent`, so it structurally cannot fire. This is exactly how the shield vessel ended up weaponless after #35; do not add an `if` anywhere to suppress firing. - `base_stats`: a large `cargo_capacity` (suggest 120, i.e. six interceptors' worth), slightly lower `max_speed`/`acceleration` than an interceptor, plus the `delivery_speed` every vessel carries. - A wide, distinctly non-combat hull silhouette and its own glow colour. - `unlock_id` / `unlock_name` / `unlock_description` / `unlock_resource_costs` so it is researched at a ship lab like the shield vessel and gas bomber. 2. Register in `VesselDefinitions.ORDER` and `DEFINITIONS`. 3. **Menu wiring - note this is still hand-placed per vessel.** `BuildController` resolves crafter and hangar entries by fixed node path (`../UI/CraftMenu/ShieldButton`, `../UI/HangarMenu/AvailableGasBomber`, `FleetGasBomber`, and their `*Label` siblings). Add the matching `CarrierButton`/`CarrierLabel`, `AvailableCarrier`/`AvailableCarrierLabel`, `FleetCarrier`/`FleetCarrierLabel` nodes to `Main.tscn` and extend those arrays. - Worth flagging for a future ticket rather than doing here: those two menus are the last per-vessel hardcoded surfaces left after #35, and would be better generated from `VesselDefinitions.fleet()`. 4. A ship-lab upgrade raising carrier capacity, consistent with every other vessel having upgrades (`VesselUpgradeDefinition`, `additive_modifiers` on `cargo_capacity`). ### Acceptance criteria - Crafting a carrier and crewing it raises `GameState.fleet_cargo_capacity()` by its stat. - A carrier never fires: it has no weapon component and no projectile ever originates from it, verified with an enemy parked inside interceptor fire range. - It appears in the crafter, the hangar, and the ship lab with correct name, cost, icon and colour. - It is unbuildable until researched. ### Tests `tests/test_carrier_vessel.gd` (`bash tests/run_tests.sh carrier`), plus a 720x1280 screenshot of the crafter and hangar showing the new entry laid out cleanly alongside the existing vessels. ### 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 bbb1d4b.

An unarmed hauler that trades weapons for a much larger cargo hold - the first real payoff for #56's fleet capacity cap.

  • CarrierVesselDefinition.tres: capabilities = ["cargo"], component_scenes = [VesselCargoComponent.tscn] only - no VesselWeaponComponent, so it structurally cannot fire (same pattern that already made the shield vessel weaponless after #35 - no if anywhere suppresses firing, there is simply nothing to call tick() on). cargo_capacity = 120 (six interceptors' worth), max_speed/acceleration a bit below an interceptor's, a wide flat-bottomed hull silhouette and its own amber glow colour, and a new CarrierCapacityUpgrade.tres (+30 cargo per level, 3 levels) researchable at the ship lab like every other vessel upgrade.
  • Registered in VesselDefinitions.ORDER/DEFINITIONS, appended after transporter - a pure append, so every existing vessel keeps its current index in both .all() and .fleet().
  • Menu wiring: the crafter, hangar and ship lab screens were already fully generic over VesselDefinitions.fleet()/.all() (BuildController._refresh_craft_status()/_refresh_hangar()/_refresh_lab(), connected via index-bound generic handlers) - only the per-vessel UI nodes are hand-placed in Main.tscn, not the logic. Added CarrierButton/CarrierLabel, AvailableCarrier/AvailableCarrierLabel, FleetCarrier/FleetCarrierLabel and a 5th ship-lab slot (VesselE/VesselELabel), repacking each row from 3/3/4 to 4/4/5 columns to fit. Extended BuildController's matching @onready arrays - no new signal handlers were needed anywhere.
  • New CARRIER_VESSEL icon glyph, appended to IconButton.Icon (never inserted - ints are baked into scenes).

A genuine regression surfaced and fixed: test_vessel_composition.gd hardcoded the combat-fleet catalog at exactly 3 vessels; now that carrier is fleet-eligible it's 4, so that assertion is now != 4.

Tests: tests/test_carrier_vessel.gd (bash tests/run_tests.sh carrier) covers: structurally unarmed + cargo-capable; capacity math directly (fleet_cargo_capacity(["carrier"]) == 120); unbuildable before research, buildable and correctly priced (24 ore) after; correct icon/colour/label in the crafter, hangar and ship lab; crewing one raises fleet_cargo_total_capacity() by exactly 120; and - with an enemy parked well inside interceptor fire range and the enemy frozen so it can't close in and wipe the (unarmed, unescorted) fleet first - zero player_projectiles ever appear from a fleet made only of a carrier. run_tests.sh vessel (4/4, after the fix above), fleet/cargo/hangar/lab/build/skill/icon regressions all pass. Full suite: 84/84.

Screenshots taken and visually confirmed (720x1280): the crafter menu with all 4 vessels researched, Carrier's wide amber hull glyph clearly distinct from interceptor/shield/gas bomber, correctly labelled "Carrier / 24 ore"; the hangar with one of each vessel available and an interceptor+carrier crewed, both AVAILABLE and FLEET rows laid out cleanly across 4 columns with no overlap, and the HUD's "FLEET HOLD 0/140" caption independently confirming the 20+120 capacity math.

Implemented in bbb1d4b. An unarmed hauler that trades weapons for a much larger cargo hold - the first real payoff for #56's fleet capacity cap. - `CarrierVesselDefinition.tres`: `capabilities = ["cargo"]`, `component_scenes = [VesselCargoComponent.tscn]` only - no `VesselWeaponComponent`, so it structurally cannot fire (same pattern that already made the shield vessel weaponless after #35 - no `if` anywhere suppresses firing, there is simply nothing to call `tick()` on). `cargo_capacity = 120` (six interceptors' worth), `max_speed`/`acceleration` a bit below an interceptor's, a wide flat-bottomed hull silhouette and its own amber glow colour, and a new `CarrierCapacityUpgrade.tres` (+30 cargo per level, 3 levels) researchable at the ship lab like every other vessel upgrade. - Registered in `VesselDefinitions.ORDER`/`DEFINITIONS`, appended after `transporter` - a pure append, so every existing vessel keeps its current index in both `.all()` and `.fleet()`. - Menu wiring: the crafter, hangar and ship lab screens were already fully generic over `VesselDefinitions.fleet()`/`.all()` (`BuildController._refresh_craft_status()`/`_refresh_hangar()`/`_refresh_lab()`, connected via index-bound generic handlers) - only the per-vessel UI *nodes* are hand-placed in `Main.tscn`, not the logic. Added `CarrierButton`/`CarrierLabel`, `AvailableCarrier`/`AvailableCarrierLabel`, `FleetCarrier`/`FleetCarrierLabel` and a 5th ship-lab slot (`VesselE`/`VesselELabel`), repacking each row from 3/3/4 to 4/4/5 columns to fit. Extended `BuildController`'s matching `@onready` arrays - no new signal handlers were needed anywhere. - New `CARRIER_VESSEL` icon glyph, appended to `IconButton.Icon` (never inserted - ints are baked into scenes). A genuine regression surfaced and fixed: `test_vessel_composition.gd` hardcoded the combat-fleet catalog at exactly 3 vessels; now that carrier is fleet-eligible it's 4, so that assertion is now `!= 4`. Tests: `tests/test_carrier_vessel.gd` (`bash tests/run_tests.sh carrier`) covers: structurally unarmed + cargo-capable; capacity math directly (`fleet_cargo_capacity(["carrier"]) == 120`); unbuildable before research, buildable and correctly priced (24 ore) after; correct icon/colour/label in the crafter, hangar and ship lab; crewing one raises `fleet_cargo_total_capacity()` by exactly 120; and - with an enemy parked well inside interceptor fire range and the enemy frozen so it can't close in and wipe the (unarmed, unescorted) fleet first - zero `player_projectiles` ever appear from a fleet made only of a carrier. `run_tests.sh vessel` (4/4, after the fix above), `fleet`/`cargo`/`hangar`/`lab`/`build`/`skill`/`icon` regressions all pass. Full suite: 84/84. Screenshots taken and visually confirmed (720x1280): the crafter menu with all 4 vessels researched, Carrier's wide amber hull glyph clearly distinct from interceptor/shield/gas bomber, correctly labelled "Carrier / 24 ore"; the hangar with one of each vessel available and an interceptor+carrier crewed, both AVAILABLE and FLEET rows laid out cleanly across 4 columns with no overlap, and the HUD's "FLEET HOLD 0/140" caption independently confirming the 20+120 capacity math.
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#57
No description provided.