Add black hole planet and dark matter resource (#55)
A rare, deterministically-seeded landable body yielding a fourth resource, dark matter, obtainable only through an extractor - never a surface pickup. Reuses every existing generic system (resource/planet definitions, surface profiles, extractor/transporter pipeline) with no type-switch branching. Also fixes a latent conquest-gating gap: a surface profile with no authored PlanetSetup (like the black hole's "void") never registered capture points at all, permanently blocking extractor placement. register_capture_points() now treats zero required points as vacuously conquered, and PlanetSurfaceController registers that zero-point state explicitly when no setup exists.
This commit is contained in:
parent
097e17f7aa
commit
cbe07bbb55
20 changed files with 421 additions and 2 deletions
23
tests/_black_hole_space_preview.gd
Normal file
23
tests/_black_hole_space_preview.gd
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
extends Node2D
|
||||
|
||||
# Preview, not a test - a black hole planet as seen in space, next to a
|
||||
# rocky planet for scale/contrast.
|
||||
#
|
||||
# bash tests/screenshot.sh res://tests/_black_hole_space_preview.tscn /tmp/black_hole.png 15 720x1280
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.extractors.clear()
|
||||
GameState.buildings.clear()
|
||||
|
||||
_spawn("Event Zero", "black_hole", Vector2(360.0, 420.0), 12345)
|
||||
_spawn("Dustworn", "rocky", Vector2(360.0, 950.0), 4242)
|
||||
|
||||
|
||||
func _spawn(planet_name: String, definition_id: String, at: Vector2, shape_seed: int) -> void:
|
||||
var planet: Node2D = load("res://Planet.tscn").instantiate()
|
||||
planet.planet_name = planet_name
|
||||
planet.definition_id = definition_id
|
||||
planet.shape_seed = shape_seed
|
||||
planet.position = at
|
||||
add_child(planet)
|
||||
6
tests/_black_hole_space_preview.tscn
Normal file
6
tests/_black_hole_space_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_black_hole_space_preview.gd" id="1"]
|
||||
|
||||
[node name="BlackHoleSpacePreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
15
tests/_void_surface_preview.gd
Normal file
15
tests/_void_surface_preview.gd
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
extends Node2D
|
||||
|
||||
# Preview, not a test - a black hole's landed "void" surface: no craters, no
|
||||
# resource pickups, nothing but the empty field itself.
|
||||
#
|
||||
# bash tests/screenshot.sh res://tests/_void_surface_preview.tscn /tmp/void.png 15 720x1280
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.planet_name = "Event Zero"
|
||||
GameState.planet_definition_id = "black_hole"
|
||||
GameState.surviving_ship_count = 3
|
||||
var surface: Node = load("res://PlanetSurface.tscn").instantiate()
|
||||
surface.get_node("SurfaceController").fade_duration = 0.0
|
||||
add_child(surface)
|
||||
6
tests/_void_surface_preview.tscn
Normal file
6
tests/_void_surface_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_void_surface_preview.gd" id="1"]
|
||||
|
||||
[node name="VoidSurfacePreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
200
tests/test_black_hole.gd
Normal file
200
tests/test_black_hole.gd
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_black_hole.gd
|
||||
# Forgejo #55: a rare landable body yielding dark matter, obtainable only
|
||||
# through an extractor - never a surface pickup.
|
||||
#
|
||||
# Covers:
|
||||
# A. WorldGen.planet_data_at_chunk() is a pure, deterministic function of
|
||||
# chunk coordinates, and black holes are noticeably rarer than gas
|
||||
# giants across a wide scan
|
||||
# B. dark matter is fully generic - in resource_types(), its own
|
||||
# ResourceDefinition/glyph/colour, distinct from every other resource
|
||||
# C. landing on a black hole spawns no resource provider and no capture
|
||||
# points at all (void's features_scene is null, and it has no
|
||||
# authored PlanetSetup) - yet the planet is immediately conquered
|
||||
# (register_capture_points treats zero required points as vacuous),
|
||||
# so an extractor is buildable there right away
|
||||
# D. an extractor on a black hole mines dark matter locally exactly like
|
||||
# any other extractor, and a hub transporter banks it through the
|
||||
# normal collection path with no black-hole-specific code
|
||||
|
||||
var game_state: Node
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var scene: Node
|
||||
var extractor_index: int = 0
|
||||
var load_limit: int = 0
|
||||
var baseline: Dictionary = {}
|
||||
|
||||
const BLACK_HOLE_AT := Vector2(500.0, 0.0)
|
||||
const MOTHERSHIP_AT := Vector2.ZERO
|
||||
const HUB_AT := Vector2(150.0, 0.0)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
game_state = root.get_node("GameState")
|
||||
|
||||
# --- A. deterministic and rare -----------------------------------------
|
||||
var black_hole_count: int = 0
|
||||
var gas_giant_count: int = 0
|
||||
var total_planets: int = 0
|
||||
for x in range(-40, 40):
|
||||
for y in range(-40, 40):
|
||||
var chunk: Vector2i = Vector2i(x, y)
|
||||
if not WorldGen.has_planet_at_chunk(chunk):
|
||||
continue
|
||||
total_planets += 1
|
||||
var first: Dictionary = WorldGen.planet_data_at_chunk(chunk)
|
||||
var second: Dictionary = WorldGen.planet_data_at_chunk(chunk)
|
||||
if first.definition_id != second.definition_id or first.local_pos != second.local_pos \
|
||||
or first.shape_seed != second.shape_seed or first.name != second.name:
|
||||
_fail("planet_data_at_chunk(%s) is not deterministic" % chunk)
|
||||
return
|
||||
if first.definition_id == "black_hole":
|
||||
black_hole_count += 1
|
||||
elif first.definition_id == "gas_giant":
|
||||
gas_giant_count += 1
|
||||
print("scanned %d planet chunks: %d black holes, %d gas giants" % [
|
||||
total_planets, black_hole_count, gas_giant_count
|
||||
])
|
||||
if black_hole_count == 0:
|
||||
_fail("no black holes generated across a wide scan - the rare outcome never fires")
|
||||
return
|
||||
if black_hole_count >= gas_giant_count:
|
||||
_fail("black holes (%d) are not rarer than gas giants (%d)" % [black_hole_count, gas_giant_count])
|
||||
return
|
||||
|
||||
# --- B. dark matter is fully generic -----------------------------------
|
||||
if not game_state.resource_types().has(GameState.RESOURCE_DARK_MATTER):
|
||||
_fail("dark_matter is not in resource_types() - every resource menu would miss it")
|
||||
return
|
||||
var definition: Resource = game_state.resource_definition(GameState.RESOURCE_DARK_MATTER)
|
||||
if definition == null or definition.glyph_scene == null:
|
||||
_fail("dark_matter has no ResourceDefinition or glyph_scene")
|
||||
return
|
||||
var dm_color: Color = game_state.resource_color(GameState.RESOURCE_DARK_MATTER)
|
||||
for other in [GameState.RESOURCE_ORE, GameState.RESOURCE_GAS, GameState.RESOURCE_ENERGY]:
|
||||
if dm_color == game_state.resource_color(other):
|
||||
_fail("dark_matter shares a colour with %s" % other)
|
||||
return
|
||||
print("dark matter is generic: in resource_types(), own definition, own colour %s" % dm_color)
|
||||
|
||||
# --- structural: the black hole definition itself -----------------------
|
||||
var black_hole_def: Resource = load("res://PlanetDefinitions.gd").get_definition("black_hole")
|
||||
if black_hole_def.resource_id != "dark_matter":
|
||||
_fail("black_hole planet's resource_id is '%s', expected 'dark_matter'" % black_hole_def.resource_id)
|
||||
return
|
||||
if black_hole_def.surface_profile_id != "void":
|
||||
_fail("black_hole planet's surface_profile_id is '%s', expected 'void'" % black_hole_def.surface_profile_id)
|
||||
return
|
||||
var void_profile: Resource = load("res://PlanetSurfaceProfiles.gd").get_profile("void")
|
||||
if void_profile.features_scene != null:
|
||||
_fail("void surface profile has a features_scene - dark matter must never be a surface pickup")
|
||||
return
|
||||
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.transporters.clear()
|
||||
game_state.planet_capture.clear()
|
||||
game_state.planet_id = "test_black_hole"
|
||||
game_state.planet_name = "Event Zero"
|
||||
game_state.planet_definition_id = "black_hole"
|
||||
scene = load("res://PlanetSurface.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
current_scene = scene
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
# --- C. no resource provider, no capture points, yet conquered ---
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
var feature_host: Node = scene.get_node("SurfaceFeatures")
|
||||
if feature_host.get_child_count() != 0:
|
||||
return _fail("black hole surface spawned %d children under SurfaceFeatures - expected none" % feature_host.get_child_count())
|
||||
if not game_state.is_planet_conquered(game_state.planet_id):
|
||||
return _fail("a black hole with no capture points required is not marked conquered")
|
||||
print("no resource provider, no capture points, already conquered")
|
||||
|
||||
# Snapshot rather than assert > 0: bank_inventory starts with 500
|
||||
# ore by default (a fresh game's starting stock), unrelated to
|
||||
# this planet - "nothing was picked up" has to mean "unchanged",
|
||||
# not "still zero".
|
||||
for resource_type in game_state.resource_types():
|
||||
baseline[resource_type] = game_state.banked_resource(resource_type) + game_state.fleet_resource(resource_type)
|
||||
|
||||
var fleet: Node = scene.get_node("Fleet")
|
||||
for ship in fleet.ships:
|
||||
ship.position += Vector2(300.0, 200.0)
|
||||
fleet.move_to(fleet.get_centroid())
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
# --- C continued: flying around picks up nothing, ever -----------
|
||||
if elapsed < 2.0:
|
||||
return false
|
||||
for resource_type in game_state.resource_types():
|
||||
var total: int = game_state.banked_resource(resource_type) + game_state.fleet_resource(resource_type)
|
||||
if total != baseline.get(resource_type, 0):
|
||||
return _fail("something (%s) was picked up over a black hole surface" % resource_type)
|
||||
print("flew around for %.1fs, nothing picked up" % elapsed)
|
||||
|
||||
# --- D: extractor mining + transporter banking --------------------
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.transporters.clear()
|
||||
game_state.reset_progression()
|
||||
for node in GameState.SKILL_TREE:
|
||||
game_state.unlocked.append(node.id)
|
||||
game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
||||
game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, HUB_AT)
|
||||
game_state.add_extractor("Event Zero", BLACK_HOLE_AT, GameState.RESOURCE_DARK_MATTER, "test_black_hole_extractor")
|
||||
extractor_index = game_state.extractor_index_at("test_black_hole_extractor")
|
||||
if extractor_index < 0:
|
||||
return _fail("add_extractor did not register the black hole extractor")
|
||||
load_limit = game_state.transporter_load_limit()
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
2:
|
||||
# Mining is local, exactly like ore - no black-hole-specific code.
|
||||
if elapsed < 1.2:
|
||||
return false
|
||||
var mined: int = game_state.extractor_resource(extractor_index, GameState.RESOURCE_DARK_MATTER)
|
||||
if mined <= 0:
|
||||
return _fail("extractor mined no dark matter locally after %.1fs" % elapsed)
|
||||
print("extractor mined %d dark matter locally, bank still %d" % [
|
||||
mined, game_state.banked_resource(GameState.RESOURCE_DARK_MATTER)
|
||||
])
|
||||
# Force a full load directly (bypasses waiting out the real
|
||||
# mining rate for the rest) so the transporter pickup dispatch
|
||||
# fires without a long extra wait.
|
||||
game_state.extractors[extractor_index].inventory.set_amount(GameState.RESOURCE_DARK_MATTER, load_limit)
|
||||
elapsed = 0.0
|
||||
stage = 3
|
||||
return false
|
||||
3:
|
||||
# --- a transporter collects the full load and banks it -----------
|
||||
if game_state.banked_resource(GameState.RESOURCE_DARK_MATTER) >= load_limit:
|
||||
print("transporter banked %d dark matter" % game_state.banked_resource(GameState.RESOURCE_DARK_MATTER))
|
||||
print("TEST_BLACK_HOLE: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
if elapsed > 10.0:
|
||||
return _fail("dark matter was never collected to the bank (extractor=%d bank=%d)" % [
|
||||
game_state.extractor_resource(extractor_index, GameState.RESOURCE_DARK_MATTER),
|
||||
game_state.banked_resource(GameState.RESOURCE_DARK_MATTER),
|
||||
])
|
||||
return false
|
||||
return false
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_BLACK_HOLE: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
Loading…
Reference in a new issue