Generalize the resource HUD readouts #49

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

Foundation ticket - the black-hole and gas-burner tickets both add a resource and both need this first.

Why

Adding ore and gas to the HUD was done with four hand-placed nodes per scene and one NeonNumber subclass each:

  • Main.tscn and PlanetSurface.tscn each contain UI/ResourceCounter (banked ore), UI/GasCounter (banked gas), UI/CargoCounter (fleet ore), UI/GasCargoCounter (fleet gas).
  • Backed by ResourceCounter.gd, GasCounter.gd, CargoCounter.gd, GasCargoCounter.gd - the last two are subclasses that exist purely to re-read a different GameState field.

So a third resource currently costs two more hand-placed nodes in two scenes plus two more script files. Everything else in the game is already resource-agnostic: Inventory, ResourceBar, and ResourceTransferList all iterate GameState.resource_types() and pick up a new ResourceDefinition with no code change. The HUD is the last hardcoded surface.

Scope

  1. New ResourceReadout.gd (extends Control) that builds one row per GameState.resource_types() at _ready():
    • resource glyph from ResourceGlyph (which already resolves definition.glyph_scene),
    • a banked figure in GameState.resource_color(id),
    • a fleet-carried figure in the existing amber in-transit colour (Color(1.0, 0.78, 0.25), see CargoCounter.gd's comment on why in-transit is a different colour),
    • the fleet figure hidden when zero, matching today's behaviour.
    • Reuse NeonNumber for the digits rather than plain Labels, so the HUD keeps its current look.
  2. Replace the four nodes in both Main.tscn and PlanetSurface.tscn with a single ResourceReadout occupying the same screen region. Preserve the current arrangement as closely as possible: banked column top-left under a BANKED caption, fleet column top-right under FLEET HOLD.
  3. PlanetSurfaceController._compose_surface() currently themes ../UI/BankCaption, ../UI/FleetCaption and ../UI/CaptureLabel from the surface profile. Keep whatever captions survive on that list so the gas surface still recolours them.
  4. Delete ResourceCounter.gd, GasCounter.gd, CargoCounter.gd, GasCargoCounter.gd once nothing references them.
  5. tests/test_cargo_readout.gd reads UI/ResourceCounter and UI/CargoCounter directly (lines 47-48) - update it to the new node, keeping the same assertions about what the player can see.

Out of scope

Changing any ResourceDefinition, or adding new resources - that's the gas-burner and black-hole tickets.

Acceptance criteria

  • Adding a .tres to ResourceDefinitions.DEFINITIONS/ORDER makes a new HUD row appear in both scenes with no scene edit and no new script. Prove it with a scratch definition in the test, then remove it.
  • Ore and gas remain exactly as legible as before at 720x1280 - compare screenshots of res://Main.tscn and res://PlanetSurface.tscn against the current build.
  • Fleet figures still hide at zero.
  • bash tests/run_tests.sh cargo, run_tests.sh gas, and the full suite pass.

Tests

New tests/test_resource_readout.gd (row-per-resource, hide-at-zero, third-resource pickup), plus the updated tests/test_cargo_readout.gd. Screenshots of both scenes at 720x1280.

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.
**Foundation ticket** - the black-hole and gas-burner tickets both add a resource and both need this first. ### Why Adding ore and gas to the HUD was done with four hand-placed nodes per scene and one `NeonNumber` subclass each: - `Main.tscn` and `PlanetSurface.tscn` each contain `UI/ResourceCounter` (banked ore), `UI/GasCounter` (banked gas), `UI/CargoCounter` (fleet ore), `UI/GasCargoCounter` (fleet gas). - Backed by `ResourceCounter.gd`, `GasCounter.gd`, `CargoCounter.gd`, `GasCargoCounter.gd` - the last two are subclasses that exist purely to re-read a different `GameState` field. So a third resource currently costs two more hand-placed nodes in two scenes plus two more script files. Everything *else* in the game is already resource-agnostic: `Inventory`, `ResourceBar`, and `ResourceTransferList` all iterate `GameState.resource_types()` and pick up a new `ResourceDefinition` with no code change. The HUD is the last hardcoded surface. ### Scope 1. New `ResourceReadout.gd` (`extends Control`) that builds one row per `GameState.resource_types()` at `_ready()`: - resource glyph from `ResourceGlyph` (which already resolves `definition.glyph_scene`), - a banked figure in `GameState.resource_color(id)`, - a fleet-carried figure in the existing amber in-transit colour (`Color(1.0, 0.78, 0.25)`, see `CargoCounter.gd`'s comment on why in-transit is a different colour), - the fleet figure hidden when zero, matching today's behaviour. - Reuse `NeonNumber` for the digits rather than plain `Label`s, so the HUD keeps its current look. 2. Replace the four nodes in **both** `Main.tscn` and `PlanetSurface.tscn` with a single `ResourceReadout` occupying the same screen region. Preserve the current arrangement as closely as possible: banked column top-left under a `BANKED` caption, fleet column top-right under `FLEET HOLD`. 3. `PlanetSurfaceController._compose_surface()` currently themes `../UI/BankCaption`, `../UI/FleetCaption` and `../UI/CaptureLabel` from the surface profile. Keep whatever captions survive on that list so the gas surface still recolours them. 4. Delete `ResourceCounter.gd`, `GasCounter.gd`, `CargoCounter.gd`, `GasCargoCounter.gd` once nothing references them. 5. `tests/test_cargo_readout.gd` reads `UI/ResourceCounter` and `UI/CargoCounter` directly (lines 47-48) - update it to the new node, keeping the same assertions about what the player can see. ### Out of scope Changing any `ResourceDefinition`, or adding new resources - that's the gas-burner and black-hole tickets. ### Acceptance criteria - Adding a `.tres` to `ResourceDefinitions.DEFINITIONS`/`ORDER` makes a new HUD row appear in both scenes with **no scene edit and no new script**. Prove it with a scratch definition in the test, then remove it. - Ore and gas remain exactly as legible as before at 720x1280 - compare screenshots of `res://Main.tscn` and `res://PlanetSurface.tscn` against the current build. - Fleet figures still hide at zero. - `bash tests/run_tests.sh cargo`, `run_tests.sh gas`, and the full suite pass. ### Tests New `tests/test_resource_readout.gd` (row-per-resource, hide-at-zero, third-resource pickup), plus the updated `tests/test_cargo_readout.gd`. Screenshots of both scenes at 720x1280. ### 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

Done in commit 0582181, pushed to main.

Replaced ResourceCounter/GasCounter/CargoCounter/GasCargoCounter (one hardcoded node+script per resource, times two scenes) with a single ResourceReadout.gd that builds one banked + one fleet-hold row per GameState.resource_types() entry at _ready(). A new resource added to ResourceDefinitions now gets a HUD row automatically, no scene edit or new script needed.

Preserves the existing look exactly: BANKED column left, FLEET HOLD column right, amber fleet-in-transit color, and the ore-on-bright-surface contrast on PlanetSurface.tscn (now a bright_background export flag instead of per-node color overrides). Reuses the already-generic ResourceGlyph for icons instead of NeonNumber's hardcoded ore hexagon.

Tests:

  • tests/test_resource_readout.gd (new) - per-resource row mapping, hide-at-zero semantics, live updates, visual distinctness, a static check that the script has no hardcoded resource id, and that PlanetSurface.tscn's instance sets bright_background.
  • tests/test_cargo_readout.gd (updated) - same coverage as before, now against the generic readout.
  • Full suite: 77/77 passed.

Screenshots at 720x1280 confirm legibility against the acceptance criterion:

Along the way, found and fixed a real bug in the existing test_cargo_readout.gd: it looked up its readout nodes immediately after add_child() in _initialize(), but ResourceReadout builds its rows in _ready(), which for a -s entry-point script does not fire synchronously inside that add_child() - it's deferred to the next frame. Moved the lookup to after the test's existing elapsed-time wait in _process().

Also confirmed empirically (new repo note, worth keeping in mind for future tickets): a -s entry-point test script can reach an autoload's CONSTANTS via the bare identifier (GameState.RESOURCE_ORE) anywhere, but calling any of its METHODS that way (GameState.resource_types()) fails to compile with "Identifier not found: GameState", even from _initialize(). Route method calls through the actual instance (root.get_node("GameState")) instead, like the rest of the suite already does.

Done in commit 0582181, pushed to main. Replaced ResourceCounter/GasCounter/CargoCounter/GasCargoCounter (one hardcoded node+script per resource, times two scenes) with a single `ResourceReadout.gd` that builds one banked + one fleet-hold row per `GameState.resource_types()` entry at `_ready()`. A new resource added to `ResourceDefinitions` now gets a HUD row automatically, no scene edit or new script needed. Preserves the existing look exactly: BANKED column left, FLEET HOLD column right, amber fleet-in-transit color, and the ore-on-bright-surface contrast on `PlanetSurface.tscn` (now a `bright_background` export flag instead of per-node color overrides). Reuses the already-generic `ResourceGlyph` for icons instead of `NeonNumber`'s hardcoded ore hexagon. Tests: - `tests/test_resource_readout.gd` (new) - per-resource row mapping, hide-at-zero semantics, live updates, visual distinctness, a static check that the script has no hardcoded resource id, and that `PlanetSurface.tscn`'s instance sets `bright_background`. - `tests/test_cargo_readout.gd` (updated) - same coverage as before, now against the generic readout. - Full suite: 77/77 passed. Screenshots at 720x1280 confirm legibility against the acceptance criterion: - Main.tscn: https://metaheavens.de/forgejo/metaheavens/spaceship (see shots/resource_readout_main.png in the repo) - PlanetSurface.tscn: shots/resource_readout_surface.png Along the way, found and fixed a real bug in the existing `test_cargo_readout.gd`: it looked up its readout nodes immediately after `add_child()` in `_initialize()`, but `ResourceReadout` builds its rows in `_ready()`, which for a `-s` entry-point script does not fire synchronously inside that `add_child()` - it's deferred to the next frame. Moved the lookup to after the test's existing elapsed-time wait in `_process()`. Also confirmed empirically (new repo note, worth keeping in mind for future tickets): a `-s` entry-point test script can reach an autoload's CONSTANTS via the bare identifier (`GameState.RESOURCE_ORE`) anywhere, but calling any of its METHODS that way (`GameState.resource_types()`) fails to compile with "Identifier not found: GameState", even from `_initialize()`. Route method calls through the actual instance (`root.get_node("GameState")`) instead, like the rest of the suite already does.
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#49
No description provided.