Enemy research lab planet and research points #63

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

A new planet type that produces no resources, but grants a research point when conquered. Research points become a required currency for progressing the mothership skill tree and researching at the ship lab.

This changes the whole progression economy, so read the balance note at the end before starting.

Depends on the capture/conquest loop (#43, already merged) - the award hooks GameState.planet_conquered.

Scope

  1. The planet: ResearchLabPlanetVisual.tscn + .gd (same informal interface as GasGiantVisual: configure(shape_seed), extractor_anchor()), and ResearchLabPlanetDefinition.tres with definition_id = "research_lab", an empty resource_id (it yields nothing), its own names, map colour, and a surface profile with no resource provider (features_scene = null, as #55 also uses). Register in PlanetDefinitions.
    • It should visually read as occupied/hostile rather than natural - it is an enemy lab. Pair it with authored setups placing real defenders (this is exactly what the #48 map editor is for); ship at least one such setup.
  2. Generation: add it to WorldGen.planet_data_at_chunk() as a rare deterministic outcome, same constraints as every other body - a pure function of chunk coordinates, since PlanetField and the world map both resolve through it and must never disagree (#37).
  3. The currency:
    • GameState.research_points: int = 0 and signal research_points_changed(total: int).
    • Connect planet_conquered and award +1 when the conquered planet's definition is research_lab. Conquest is permanent for the run so the signal fires once per planet, but guard against double-award anyway and assert it in the test.
  4. The gate - both progression paths:
    • SKILL_TREE entries gain an optional research_points cost. GameState.can_unlock() requires it in addition to the ore cost, and unlock() spends it.
    • VesselDefinition and VesselUpgradeDefinition gain a research_point_cost. GameState.unlock_vessel() and the lab upgrade path require and spend it alongside unlock_resource_costs.
  5. Legibility - a blocked player must never be left guessing:
    • Show the research-point balance in SkillTreeView and the lab menu.
    • When research points are the blocker, say so explicitly, in the same "explain the refusal" idiom BuildController uses for unaffordable builds.

Balance note (decide deliberately)

A fresh game has zero research points and no way to get one until the player finds, fights and conquers a research lab. If every skill-tree node costs points, the opening is hard-blocked. Give the early nodes (shipyards, and enough of its immediate children to reach a ship crafter and an extractor) a cost of 0 research points, and put the cost on mid/late nodes only. State the chosen split in the ticket's closing comment so the next balance pass knows what was intended.

Acceptance criteria

  • Research labs generate deterministically and are rare.
  • Landing on one yields no resources of any kind.
  • Conquering one awards exactly one research point, once, and emits the signal.
  • A skill-tree node or lab research whose research-point cost is unmet is refused and explains that the research points are what is missing - not "not enough ore".
  • Unlocking spends the points, and the displayed balance updates.
  • A fresh game can still reach a ship crafter and an extractor with zero research points.

Tests

tests/test_research_points.gd (bash tests/run_tests.sh research), plus run_tests.sh skill and run_tests.sh lab for regressions, and a 720x1280 screenshot of the skill tree showing the balance and a points-blocked node.

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 planet type that produces no resources, but grants a **research point** when conquered. Research points become a required currency for progressing the mothership skill tree and researching at the ship lab. This changes the whole progression economy, so read the balance note at the end before starting. **Depends on** the capture/conquest loop (#43, already merged) - the award hooks `GameState.planet_conquered`. ### Scope 1. **The planet**: `ResearchLabPlanetVisual.tscn` + `.gd` (same informal interface as `GasGiantVisual`: `configure(shape_seed)`, `extractor_anchor()`), and `ResearchLabPlanetDefinition.tres` with `definition_id = "research_lab"`, an **empty** `resource_id` (it yields nothing), its own `names`, map colour, and a surface profile with no resource provider (`features_scene = null`, as #55 also uses). Register in `PlanetDefinitions`. - It should visually read as occupied/hostile rather than natural - it is an *enemy* lab. Pair it with authored setups placing real defenders (this is exactly what the #48 map editor is for); ship at least one such setup. 2. **Generation**: add it to `WorldGen.planet_data_at_chunk()` as a rare deterministic outcome, same constraints as every other body - a pure function of chunk coordinates, since `PlanetField` and the world map both resolve through it and must never disagree (#37). 3. **The currency**: - `GameState.research_points: int = 0` and `signal research_points_changed(total: int)`. - Connect `planet_conquered` and award **+1** when the conquered planet's definition is `research_lab`. Conquest is permanent for the run so the signal fires once per planet, but guard against double-award anyway and assert it in the test. 4. **The gate** - both progression paths: - `SKILL_TREE` entries gain an optional `research_points` cost. `GameState.can_unlock()` requires it in addition to the ore cost, and `unlock()` spends it. - `VesselDefinition` and `VesselUpgradeDefinition` gain a `research_point_cost`. `GameState.unlock_vessel()` and the lab upgrade path require and spend it alongside `unlock_resource_costs`. 5. **Legibility** - a blocked player must never be left guessing: - Show the research-point balance in `SkillTreeView` and the lab menu. - When research points are the blocker, say so explicitly, in the same "explain the refusal" idiom `BuildController` uses for unaffordable builds. ### Balance note (decide deliberately) A fresh game has zero research points and no way to get one until the player finds, fights and conquers a research lab. If every skill-tree node costs points, the opening is hard-blocked. Give the early nodes (`shipyards`, and enough of its immediate children to reach a ship crafter and an extractor) a cost of **0** research points, and put the cost on mid/late nodes only. State the chosen split in the ticket's closing comment so the next balance pass knows what was intended. ### Acceptance criteria - Research labs generate deterministically and are rare. - Landing on one yields no resources of any kind. - Conquering one awards exactly one research point, once, and emits the signal. - A skill-tree node or lab research whose research-point cost is unmet is refused and explains that the research points are what is missing - not "not enough ore". - Unlocking spends the points, and the displayed balance updates. - A fresh game can still reach a ship crafter and an extractor with zero research points. ### Tests `tests/test_research_points.gd` (`bash tests/run_tests.sh research`), plus `run_tests.sh skill` and `run_tests.sh lab` for regressions, and a 720x1280 screenshot of the skill tree showing the balance and a points-blocked node. ### 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 and pushed in 3f3a222, with the two-day code-quality review/refactor in d68f4d4.

What shipped:

  • A deterministic, rare enemy Research Lab planet with its own hostile visual, resource-less surface profile, map identity, and an authored defended setup.
  • Definition-authored conquest rewards: the lab grants exactly +1 research point once. The conquest engine has no research-lab ID special case.
  • Research points gate and are spent by both mothership skill-tree nodes and ship-lab vessel/upgrade research.
  • Skill-tree and lab UI show the balance, costs, and an explicit research-point shortage message.
  • Fresh-game reset includes the new currency/state.

Balance split: shipyards and logistics cost 0 research points, so a fresh run can still reach the crafter and extractor. Mid/late skills cost points: coordination 1, and later logistics/power/combat nodes 1-2. Vessel unlocks and upgrades cost 1 point each.

The follow-up review removed resource-specific gas-burner branches from transport by composing logistics/conversion data into building definitions; extracted shared aimed-ability and per-vessel cooldown/fill controllers; made skill research-point lookup explicit; and fixed a scene-transition race where the outgoing distorder UI could reset the boost before the surface fleet restored.

Validation:

  • Godot full project import: clean.
  • 720x1280 skill-tree screenshot checked: balance, point cost, ore cost, and blocked explanation render without overlap.
  • Focused research, skill, lab, gas-burner, gas-bomber, laser, hub, vessel-composition, and distorder checks passed.
  • Final full suite: 90/90 passed (JOBS=2).

No tickets after #63 were implemented in this pass.

Implemented and pushed in `3f3a222`, with the two-day code-quality review/refactor in `d68f4d4`. What shipped: - A deterministic, rare enemy Research Lab planet with its own hostile visual, resource-less surface profile, map identity, and an authored defended setup. - Definition-authored conquest rewards: the lab grants exactly +1 research point once. The conquest engine has no research-lab ID special case. - Research points gate and are spent by both mothership skill-tree nodes and ship-lab vessel/upgrade research. - Skill-tree and lab UI show the balance, costs, and an explicit research-point shortage message. - Fresh-game reset includes the new currency/state. Balance split: `shipyards` and `logistics` cost 0 research points, so a fresh run can still reach the crafter and extractor. Mid/late skills cost points: `coordination` 1, and later logistics/power/combat nodes 1-2. Vessel unlocks and upgrades cost 1 point each. The follow-up review removed resource-specific gas-burner branches from transport by composing logistics/conversion data into building definitions; extracted shared aimed-ability and per-vessel cooldown/fill controllers; made skill research-point lookup explicit; and fixed a scene-transition race where the outgoing distorder UI could reset the boost before the surface fleet restored. Validation: - Godot full project import: clean. - 720x1280 skill-tree screenshot checked: balance, point cost, ore cost, and blocked explanation render without overlap. - Focused research, skill, lab, gas-burner, gas-bomber, laser, hub, vessel-composition, and distorder checks passed. - Final full suite: **90/90 passed** (`JOBS=2`). No tickets after #63 were implemented in this pass.
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#63
No description provided.