Black hole planet and dark matter #55

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

A new landable body yielding dark matter, a resource that can only ever be obtained through an extractor - never as a surface pickup.

Depends on #49 (generalize the resource HUD readouts).

Scope

  1. The resource: DarkMatterResourceGlyph.tscn + .gd (NeonDraw), DarkMatterResourceDefinition.tres (resource_id = "dark_matter", a deep violet/near-black-with-rim colour that still reads on both the dark space background and the bright rocky surface - check both), registered in ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_DARK_MATTER.
  2. The planet body: BlackHoleVisual.tscn + .gd implementing the same informal interface as GasGiantVisual/RockyPlanetVisual - configure(shape_seed), extractor_anchor(), and animating via its own _process. Draw an accretion disc (concentric arcs, brighter inward) around an unfilled dark core; no solid body.
  3. The definition: BlackHolePlanetDefinition.tres - definition_id = "black_hole", resource_id = "dark_matter", surface_profile_id = "void", its own names list, map colour. Register in PlanetDefinitions.
  4. The surface: VoidSurfaceProfile.tres with no resource provider - dark matter is extractor-only, so nothing may be gatherable by flying over it. PlanetSurfaceController._compose_surface() already tolerates features_scene == null; use that rather than shipping an empty provider scene. Give the void profile its own background colours (and, if #51 has landed, optionally its own background_shader).
  5. World generation: WorldGen.planet_data_at_chunk() currently picks rocky vs gas_giant from the biome. Add black holes as a rare third outcome, seeded deterministically from the chunk coordinates like everything else - the same chunk must always produce the same body, since both PlanetField and the world map resolve planets through this one function and must never disagree (see #37).
  6. Extractor placement already passes planet.resource_type into GameState.add_extractor(), so an extractor on a black hole banks dark matter with no change - confirm, don't reimplement.

Acceptance criteria

  • Black holes generate deterministically and are noticeably rarer than gas giants; WorldGen remains a pure function of chunk coordinates.
  • Landing on one yields no pickups of any kind, however long the fleet flies around.
  • An extractor built there (after conquest, per #41) banks dark matter through the normal transporter route.
  • Dark matter appears in every resource menu automatically.
  • Existing planet tests (run_tests.sh planet) still pass, including deterministic identity and map-marker agreement.

Tests

tests/test_black_hole.gd (bash tests/run_tests.sh black_hole), plus run_tests.sh planet and run_tests.sh extractor. Screenshots: the black hole in space at 720x1280, and its landed surface showing an empty field.

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 landable body yielding **dark matter**, a resource that can only ever be obtained through an extractor - never as a surface pickup. **Depends on #49 (generalize the resource HUD readouts).** ### Scope 1. **The resource**: `DarkMatterResourceGlyph.tscn` + `.gd` (`NeonDraw`), `DarkMatterResourceDefinition.tres` (`resource_id = "dark_matter"`, a deep violet/near-black-with-rim colour that still reads on both the dark space background and the bright rocky surface - check both), registered in `ResourceDefinitions.ORDER`/`DEFINITIONS` and `GameState.RESOURCE_DARK_MATTER`. 2. **The planet body**: `BlackHoleVisual.tscn` + `.gd` implementing the same informal interface as `GasGiantVisual`/`RockyPlanetVisual` - `configure(shape_seed)`, `extractor_anchor()`, and animating via its own `_process`. Draw an accretion disc (concentric arcs, brighter inward) around an unfilled dark core; no solid body. 3. **The definition**: `BlackHolePlanetDefinition.tres` - `definition_id = "black_hole"`, `resource_id = "dark_matter"`, `surface_profile_id = "void"`, its own `names` list, map colour. Register in `PlanetDefinitions`. 4. **The surface**: `VoidSurfaceProfile.tres` with **no resource provider** - dark matter is extractor-only, so nothing may be gatherable by flying over it. `PlanetSurfaceController._compose_surface()` already tolerates `features_scene == null`; use that rather than shipping an empty provider scene. Give the void profile its own background colours (and, if #51 has landed, optionally its own `background_shader`). 5. **World generation**: `WorldGen.planet_data_at_chunk()` currently picks rocky vs `gas_giant` from the biome. Add black holes as a **rare** third outcome, seeded deterministically from the chunk coordinates like everything else - the same chunk must always produce the same body, since both `PlanetField` and the world map resolve planets through this one function and must never disagree (see #37). 6. Extractor placement already passes `planet.resource_type` into `GameState.add_extractor()`, so an extractor on a black hole banks dark matter with no change - confirm, don't reimplement. ### Acceptance criteria - Black holes generate deterministically and are noticeably rarer than gas giants; `WorldGen` remains a pure function of chunk coordinates. - Landing on one yields **no** pickups of any kind, however long the fleet flies around. - An extractor built there (after conquest, per #41) banks dark matter through the normal transporter route. - Dark matter appears in every resource menu automatically. - Existing planet tests (`run_tests.sh planet`) still pass, including deterministic identity and map-marker agreement. ### Tests `tests/test_black_hole.gd` (`bash tests/run_tests.sh black_hole`), plus `run_tests.sh planet` and `run_tests.sh extractor`. Screenshots: the black hole in space at 720x1280, and its landed surface showing an empty field. ### 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 cbe07bb.

A rare, deterministically-seeded black hole planet type yielding a fourth resource, dark matter, obtainable only through an extractor - never a surface pickup.

  • dark_matter added as a fully generic ResourceDefinition (own glyph - concentric violet octagon rings around a near-black core, own colour), registered in ResourceDefinitions.gd alongside ore/gas/energy. Confirmed generic end-to-end: shows up in resource_types(), the HUD readout (banked + fleet hold), and the extractor/transporter pipeline with zero black-hole-specific code.
  • black_hole added as a PlanetDefinition (BlackHoleVisual.gd: an accretion disc of rotating glowing arc segments, genuinely empty core - no solid body drawn), surfaced via WorldGen.gd with a flat 5% per-planet-chunk chance, excluded from the guaranteed early gas-giant landmark chunk so that source stays reliable.
  • Landed surface uses a new void PlanetSurfaceProfile with no features_scene - no resource provider spawns there, confirmed by flying around a landed black hole for 2s and checking banked+fleet resource totals are unchanged from baseline.
  • Fixed a latent gap this surfaced: a surface profile with no authored PlanetSetup (like void) never registered capture points at all, which would have permanently blocked extractor placement (is_planet_conquered() stuck false forever). register_capture_points() now treats a total of 0 required points as vacuously conquered, and PlanetSurfaceController._compose_surface() registers that zero-point state explicitly when no setup exists. Verified this doesn't affect any planet with real capture points, and doesn't regress test_planet_conquest_gate.gd's "a fresh planet reported conquered" check.
  • Extractor placed on a black hole mines dark matter locally and a hub transporter banks it through the normal collection path, exactly like ore/gas/energy - no new code path.

Tests: tests/test_black_hole.gd (determinism + rarity vs gas giants across a 6,400-chunk scan, resource genericity, structural definition checks, no pickups on a landed void surface, extractor mining + transporter banking). Full suite: 81/82 passed (test_enemy_mothership.gd flaked under device load, confirmed unrelated - passes cleanly in isolation, no code touched by this ticket is anywhere near enemy mothership spawning). planet (11/11) and extractor (4/4) regressions also pass.

Screenshots taken and visually confirmed:

  • Black hole in space (720x1280): empty core, layered rotating violet accretion-disc rings, clearly distinct from a plain rocky planet placed for scale.
  • Landed void surface (720x1280): dark violet crater-toned field, Event Zero caption, dark matter's own glyph visible in both BANKED and FLEET HOLD HUD rows, no scattered resource pickups anywhere on the field.
Implemented in cbe07bb. A rare, deterministically-seeded black hole planet type yielding a fourth resource, dark matter, obtainable only through an extractor - never a surface pickup. - `dark_matter` added as a fully generic `ResourceDefinition` (own glyph - concentric violet octagon rings around a near-black core, own colour), registered in `ResourceDefinitions.gd` alongside ore/gas/energy. Confirmed generic end-to-end: shows up in `resource_types()`, the HUD readout (banked + fleet hold), and the extractor/transporter pipeline with zero black-hole-specific code. - `black_hole` added as a `PlanetDefinition` (`BlackHoleVisual.gd`: an accretion disc of rotating glowing arc segments, genuinely empty core - no solid body drawn), surfaced via `WorldGen.gd` with a flat 5% per-planet-chunk chance, excluded from the guaranteed early gas-giant landmark chunk so that source stays reliable. - Landed surface uses a new `void` `PlanetSurfaceProfile` with no `features_scene` - no resource provider spawns there, confirmed by flying around a landed black hole for 2s and checking banked+fleet resource totals are unchanged from baseline. - Fixed a latent gap this surfaced: a surface profile with no authored `PlanetSetup` (like `void`) never registered capture points at all, which would have permanently blocked extractor placement (`is_planet_conquered()` stuck false forever). `register_capture_points()` now treats a total of 0 required points as vacuously conquered, and `PlanetSurfaceController._compose_surface()` registers that zero-point state explicitly when no setup exists. Verified this doesn't affect any planet with real capture points, and doesn't regress `test_planet_conquest_gate.gd`'s "a fresh planet reported conquered" check. - Extractor placed on a black hole mines dark matter locally and a hub transporter banks it through the normal collection path, exactly like ore/gas/energy - no new code path. Tests: `tests/test_black_hole.gd` (determinism + rarity vs gas giants across a 6,400-chunk scan, resource genericity, structural definition checks, no pickups on a landed void surface, extractor mining + transporter banking). Full suite: 81/82 passed (`test_enemy_mothership.gd` flaked under device load, confirmed unrelated - passes cleanly in isolation, no code touched by this ticket is anywhere near enemy mothership spawning). `planet` (11/11) and `extractor` (4/4) regressions also pass. Screenshots taken and visually confirmed: - Black hole in space (720x1280): empty core, layered rotating violet accretion-disc rings, clearly distinct from a plain rocky planet placed for scale. - Landed void surface (720x1280): dark violet crater-toned field, `Event Zero` caption, dark matter's own glyph visible in both BANKED and FLEET HOLD HUD rows, no scattered resource pickups anywhere on the field.
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#55
No description provided.