Gas burner building and the energy resource #54

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

A new building that consumes gas to produce energy, a new resource stored and moved exactly like ore and gas.

Depends on #49 (generalize the resource HUD readouts) - without it, energy needs two more hand-placed nodes in two scenes.

Scope

  1. The resource:
    • EnergyResourceGlyph.tscn + .gd drawn with NeonDraw (a cell/bolt mark; see GasResourceGlyph.gd for the shape of one).
    • EnergyResourceDefinition.tres: resource_id = "energy", display name, its own colour (suggest a warm yellow-green, clearly distinct from ore teal, gas pink, and the amber in-transit colour).
    • Add ENERGY_ID to ResourceDefinitions and append it to ORDER and DEFINITIONS.
    • Add const RESOURCE_ENERGY to GameState beside RESOURCE_ORE/RESOURCE_GAS.
    • Inventory, ResourceBar and ResourceTransferList are already resource-agnostic and need no edit - confirm that in the test rather than touching them.
  2. The building:
    • GasBurnerBuildingVisual.tscn + .gd following TransportHubBuildingVisual etc., composed through BuildingVisualBase.
    • GasBurnerBuildingDefinition.tres: definition_id = "gas_burner", capabilities = ["gas_burner"], placement_mode = "space", cost, action/menu icons (append new IconButton.Icon enum entries - inserting one silently renumbers every icon int already stored in scenes), map colour/sides, and menu_button_name/menu_label_name/menu_cost_name.
    • Add the matching UI/BuildMenu/GasBurnerButton / GasBurnerLabel / GasBurnerCost nodes to Main.tscn. BuildController._layout_build_menu() already positions whatever is researched, so no layout code changes.
  3. Conversion, simulated in the GameState autoload alongside _advance_production/extractor mining, so it keeps running while the player is landed or far away:
    • const GAS_BURN_RATE: float (gas/second) and const ENERGY_PER_GAS: int.
    • Per gas-burner building: withdraw gas from its own building inventory and deposit energy into the same inventory. It must stall when it has no gas, exactly like a lab waiting on materials - never conjure energy from the bank.
  4. Logistics: transporters must both deliver gas to a burner and collect energy from it. Follow the extractor pickup path and the queued-building-supply path already in _advance_transporters (the priority rule fixed in f5c60f8 - queued deliveries outrank extractor collection - must keep holding).
  5. Research gate: a SKILL_TREE node unlocking gas_burner, mirroring how freight unlocks BUILDING_TRANSPORT_HUB.

Acceptance criteria

  • Energy appears in the HUD, ResourceBar and the transfer menu with no per-resource code anywhere.
  • A burner with gas produces energy into its local stock at the configured rate; with no gas it produces nothing.
  • A transporter delivers gas to a burner, and another collects its energy to the bank.
  • The burner is unbuildable until its skill-tree node is researched.
  • Ore and gas behaviour is untouched.

Tests

tests/test_gas_burner.gd (bash tests/run_tests.sh burner), plus run_tests.sh transporter and run_tests.sh resource for regressions, and a 720x1280 screenshot of the build menu and transfer menu showing three resources laid out cleanly.

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 building that consumes gas to produce **energy**, a new resource stored and moved exactly like ore and gas. **Depends on #49 (generalize the resource HUD readouts)** - without it, energy needs two more hand-placed nodes in two scenes. ### Scope 1. **The resource**: - `EnergyResourceGlyph.tscn` + `.gd` drawn with `NeonDraw` (a cell/bolt mark; see `GasResourceGlyph.gd` for the shape of one). - `EnergyResourceDefinition.tres`: `resource_id = "energy"`, display name, its own colour (suggest a warm yellow-green, clearly distinct from ore teal, gas pink, and the amber in-transit colour). - Add `ENERGY_ID` to `ResourceDefinitions` and append it to `ORDER` and `DEFINITIONS`. - Add `const RESOURCE_ENERGY` to `GameState` beside `RESOURCE_ORE`/`RESOURCE_GAS`. - `Inventory`, `ResourceBar` and `ResourceTransferList` are already resource-agnostic and need no edit - confirm that in the test rather than touching them. 2. **The building**: - `GasBurnerBuildingVisual.tscn` + `.gd` following `TransportHubBuildingVisual` etc., composed through `BuildingVisualBase`. - `GasBurnerBuildingDefinition.tres`: `definition_id = "gas_burner"`, `capabilities = ["gas_burner"]`, `placement_mode = "space"`, cost, action/menu icons (**append** new `IconButton.Icon` enum entries - inserting one silently renumbers every icon int already stored in scenes), map colour/sides, and `menu_button_name`/`menu_label_name`/`menu_cost_name`. - Add the matching `UI/BuildMenu/GasBurnerButton` / `GasBurnerLabel` / `GasBurnerCost` nodes to `Main.tscn`. `BuildController._layout_build_menu()` already positions whatever is researched, so no layout code changes. 3. **Conversion**, simulated in the `GameState` autoload alongside `_advance_production`/extractor mining, so it keeps running while the player is landed or far away: - `const GAS_BURN_RATE: float` (gas/second) and `const ENERGY_PER_GAS: int`. - Per gas-burner building: withdraw gas from **its own** building inventory and deposit energy into the same inventory. It must stall when it has no gas, exactly like a lab waiting on materials - never conjure energy from the bank. 4. **Logistics**: transporters must both deliver gas to a burner and collect energy from it. Follow the extractor pickup path and the queued-building-supply path already in `_advance_transporters` (the priority rule fixed in `f5c60f8` - queued deliveries outrank extractor collection - must keep holding). 5. **Research gate**: a `SKILL_TREE` node unlocking `gas_burner`, mirroring how `freight` unlocks `BUILDING_TRANSPORT_HUB`. ### Acceptance criteria - Energy appears in the HUD, `ResourceBar` and the transfer menu with **no per-resource code** anywhere. - A burner with gas produces energy into its local stock at the configured rate; with no gas it produces nothing. - A transporter delivers gas to a burner, and another collects its energy to the bank. - The burner is unbuildable until its skill-tree node is researched. - Ore and gas behaviour is untouched. ### Tests `tests/test_gas_burner.gd` (`bash tests/run_tests.sh burner`), plus `run_tests.sh transporter` and `run_tests.sh resource` for regressions, and a 720x1280 screenshot of the build menu and transfer menu showing three resources laid out cleanly. ### 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 097e17f, pushed to main.

The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own warm yellow-green ResourceDefinition, registered in ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY. Inventory/ResourceBar/ResourceTransferList needed no changes - #49 already made them resource-agnostic, confirmed structurally in the test rather than touched.

The building: GasBurnerBuildingVisual (a hexagonal chamber with intake vents in gas's colour and output ports in energy's, so the silhouette itself reads "gas in, energy out"), GasBurnerBuildingDefinition (capabilities=["gas_burner"], its own build-menu icon appended to IconButton.Icon rather than inserted, per the ticket's own warning about renumbering stored scene values), and the matching UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new "Power Systems" skill-tree node, mirroring how "freight" unlocks the transport hub.

Conversion: _advance_gas_burners(), simulated in GameState alongside _advance_production/extractor mining so it keeps running while the player is elsewhere. Withdraws gas and deposits energy in the building's OWN inventory only - never the bank - so it stalls with no local gas exactly like a lab waiting on materials.

Logistics: a gas burner's ongoing gas deficit is folded into the existing queued-building-supply dispatch path (same "needs resource X" loop production/research orders already use, extended with gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy to the bank, mirroring the extractor pickup path. The existing f5c60f8 priority (queued deliveries outrank collection) holds: the pickup dispatch loop runs last.

Tests: new tests/test_gas_burner.gd (resource genericity, stall semantics, research gate, and full delivery+pickup logistics via a real hub), run_tests transporter/resource regressions, and screenshots of the build menu (all 7 buildings, shots/gas_burner_build_menu.png) and transfer menu (all 3 resources, shots/gas_burner_transfer.png). Full suite: 81/81.

Two real bugs found and fixed along the way, neither gas-burner-specific:

  1. _transporter_inbound_to() matched ANY non-idle transporter with a matching target_id, regardless of purpose. Once pickup states shared target_id with delivery states (both address a building, not an extractor), a transporter delivering gas made the SAME building look "busy" to the unrelated energy-pickup dispatch, starving it even with a second, genuinely free transporter sitting idle at the hub. Split into _transporter_inbound_to() (delivery states only) and a new _transporter_inbound_for_pickup() (pickup states only).
  2. The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content sizing) had no room for a 7th building - despite the ticket's own note that no layout changes should be needed. A naive 4th row ran off the bottom of a 720x1280 viewport; fitting one meant both adding a 4th row top AND shrinking the shared button/label/cost sizing so each row's content is no longer taller than the compressed row spacing. Caught only by actually screenshotting it - a real build menu, not headless, since --headless renders nothing.
Done in commit 097e17f, pushed to main. The resource: `EnergyResourceGlyph` (a cell/bolt mark) + energy's own warm yellow-green `ResourceDefinition`, registered in `ResourceDefinitions.ORDER`/`DEFINITIONS` and `GameState.RESOURCE_ENERGY`. `Inventory`/`ResourceBar`/`ResourceTransferList` needed no changes - #49 already made them resource-agnostic, confirmed structurally in the test rather than touched. The building: `GasBurnerBuildingVisual` (a hexagonal chamber with intake vents in gas's colour and output ports in energy's, so the silhouette itself reads "gas in, energy out"), `GasBurnerBuildingDefinition` (`capabilities=["gas_burner"]`, its own build-menu icon appended to `IconButton.Icon` rather than inserted, per the ticket's own warning about renumbering stored scene values), and the matching `UI/BuildMenu/GasBurner*` nodes in `Main.tscn`. Gated behind a new "Power Systems" skill-tree node, mirroring how "freight" unlocks the transport hub. Conversion: `_advance_gas_burners()`, simulated in `GameState` alongside `_advance_production`/extractor mining so it keeps running while the player is elsewhere. Withdraws gas and deposits energy in the building's OWN inventory only - never the bank - so it stalls with no local gas exactly like a lab waiting on materials. Logistics: a gas burner's ongoing gas deficit is folded into the existing queued-building-supply dispatch path (same "needs resource X" loop production/research orders already use, extended with `gas_burner_gas_deficit()`), and a new `TO_BUILDING_PICKUP`/`TO_MOTHERSHIP_FROM_BUILDING` transporter state pair collects its energy to the bank, mirroring the extractor pickup path. The existing f5c60f8 priority (queued deliveries outrank collection) holds: the pickup dispatch loop runs last. Tests: new `tests/test_gas_burner.gd` (resource genericity, stall semantics, research gate, and full delivery+pickup logistics via a real hub), `run_tests transporter`/`resource` regressions, and screenshots of the build menu (all 7 buildings, `shots/gas_burner_build_menu.png`) and transfer menu (all 3 resources, `shots/gas_burner_transfer.png`). Full suite: 81/81. Two real bugs found and fixed along the way, neither gas-burner-specific: 1. `_transporter_inbound_to()` matched ANY non-idle transporter with a matching `target_id`, regardless of purpose. Once pickup states shared `target_id` with delivery states (both address a building, not an extractor), a transporter delivering gas made the SAME building look "busy" to the unrelated energy-pickup dispatch, starving it even with a second, genuinely free transporter sitting idle at the hub. Split into `_transporter_inbound_to()` (delivery states only) and a new `_transporter_inbound_for_pickup()` (pickup states only). 2. The build menu's 3-row/6-slot layout (`MENU_ROW_TOPS` + row content sizing) had no room for a 7th building - despite the ticket's own note that no layout changes should be needed. A naive 4th row ran off the bottom of a 720x1280 viewport; fitting one meant both adding a 4th row top AND shrinking the shared button/label/cost sizing so each row's content is no longer taller than the compressed row spacing. Caught only by actually screenshotting it - a real build menu, not headless, since `--headless` renders nothing.
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#54
No description provided.