diff --git a/BlackHolePlanetDefinition.tres b/BlackHolePlanetDefinition.tres new file mode 100644 index 0000000..9fa5589 --- /dev/null +++ b/BlackHolePlanetDefinition.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="PlanetDefinition" load_steps=3 format=3] + +[ext_resource type="Script" path="res://PlanetDefinition.gd" id="1"] +[ext_resource type="PackedScene" path="res://BlackHoleVisual.tscn" id="2"] + +[resource] +script = ExtResource("1") +definition_id = "black_hole" +display_name = "Black hole" +names = PackedStringArray("Event Zero", "Umbral Well", "The Sinkhole", "Lightless Reach", "Null Point") +visual_scene = ExtResource("2") +surface_profile_id = "void" +resource_id = "dark_matter" +map_color = Color(0.55, 0.15, 0.85, 1) diff --git a/BlackHoleVisual.gd b/BlackHoleVisual.gd new file mode 100644 index 0000000..486adee --- /dev/null +++ b/BlackHoleVisual.gd @@ -0,0 +1,49 @@ +extends Node2D + +# Same informal interface as GasGiantVisual/RockyPlanetVisual - configure(), +# extractor_anchor(), animates via its own _process(). No solid body: the +# core stays genuinely empty (nothing drawn inside CORE_RADIUS), and an +# accretion disc of rotating arc segments - not closed rings - stands in +# for a surface, brighter and thicker toward the centre. + +var disc_phase: float = 0.0 + +const CORE_RADIUS: float = 46.0 +const RING_COUNT: int = 5 +const RING_STEP: float = 16.0 +const DISC_COLOR: Color = Color(0.55, 0.15, 0.85) +const DISC_HOT_COLOR: Color = Color(0.9, 0.6, 1.0) + + +func configure(_shape_seed: int) -> void: + queue_redraw() + + +func extractor_anchor() -> Vector2: + return Vector2(0.0, -(CORE_RADIUS + float(RING_COUNT - 1) * RING_STEP + 12.0)) + + +func _process(delta: float) -> void: + disc_phase += delta + queue_redraw() + + +func _draw() -> void: + for i in range(RING_COUNT): + var radius: float = CORE_RADIUS + float(i) * RING_STEP + # 1.0 at the innermost ring, fading to 0.0 at the outermost - drives + # both colour (hotter inward) and stroke weight/brightness. + var t: float = 1.0 - float(i) / float(maxi(RING_COUNT - 1, 1)) + var ring_color: Color = DISC_COLOR.lerp(DISC_HOT_COLOR, t * 0.65) + # Alternating spin direction per ring and a speed tied to t reads as + # genuine turbulence rather than one disc rotating as a rigid whole. + var spin: float = disc_phase * (0.12 + t * 0.22) * (1.0 if i % 2 == 0 else -1.0) + for arc_i in range(2): + var start: float = spin + float(arc_i) * PI + 0.15 + var end: float = start + PI - 0.3 + var points := PackedVector2Array() + var steps: int = 24 + for j in range(steps + 1): + var angle: float = lerpf(start, end, float(j) / float(steps)) + points.append(Vector2(cos(angle), sin(angle)) * radius) + NeonDraw.draw_glowing_path(self, points, ring_color, 0.5 + t * 0.5, 0.45 + t * 0.35) diff --git a/BlackHoleVisual.gd.uid b/BlackHoleVisual.gd.uid new file mode 100644 index 0000000..8f4dc2f --- /dev/null +++ b/BlackHoleVisual.gd.uid @@ -0,0 +1 @@ +uid://c6448m0c5bses diff --git a/BlackHoleVisual.tscn b/BlackHoleVisual.tscn new file mode 100644 index 0000000..65c3d79 --- /dev/null +++ b/BlackHoleVisual.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://BlackHoleVisual.gd" id="1"] + +[node name="Visual" type="Node2D"] +script = ExtResource("1") diff --git a/DarkMatterResourceDefinition.tres b/DarkMatterResourceDefinition.tres new file mode 100644 index 0000000..07b9ee5 --- /dev/null +++ b/DarkMatterResourceDefinition.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="ResourceDefinition" load_steps=3 format=3] + +[ext_resource type="Script" path="res://ResourceDefinition.gd" id="1"] +[ext_resource type="PackedScene" path="res://DarkMatterResourceGlyph.tscn" id="2"] + +[resource] +script = ExtResource("1") +resource_id = "dark_matter" +display_name = "Dark Matter" +color = Color(0.55, 0.15, 0.85, 1) +glyph_scene = ExtResource("2") diff --git a/DarkMatterResourceGlyph.gd b/DarkMatterResourceGlyph.gd new file mode 100644 index 0000000..76da858 --- /dev/null +++ b/DarkMatterResourceGlyph.gd @@ -0,0 +1,26 @@ +extends Control + +@export var colour: Color = Color(0.55, 0.15, 0.85) + + +func _draw() -> void: + var centre: Vector2 = size * 0.5 + var radius: float = minf(size.x, size.y) * 0.34 + var points := PackedVector2Array() + for i in range(8): + var angle: float = TAU * float(i) / 8.0 - PI * 0.5 + points.append(centre + Vector2(cos(angle), sin(angle)) * radius) + # Deep violet glow around a near-black core - not colour.darkened() like + # every other glyph's core, deliberately: dark matter has to read as an + # absence even against deep space, where a merely-darker version of the + # glow colour would still show up as a faint fill instead of a void + # with a rim. + NeonDraw.draw_glowing_polygon(self, points, colour, 1.0, Color(0.04, 0.01, 0.06), maxf(0.5, radius * 0.035)) + # A tighter inner ring hints at an event horizon rather than a solid + # body - the same outline-only idiom every glyph already uses, just + # nested once more. + var inner := PackedVector2Array() + for i in range(8): + var angle: float = TAU * float(i) / 8.0 - PI * 0.5 + inner.append(centre + Vector2(cos(angle), sin(angle)) * radius * 0.5) + NeonDraw.draw_glowing_polygon(self, inner, colour.lightened(0.25), 0.75, Color(0.04, 0.01, 0.06), maxf(0.35, radius * 0.02)) diff --git a/DarkMatterResourceGlyph.gd.uid b/DarkMatterResourceGlyph.gd.uid new file mode 100644 index 0000000..5398fcb --- /dev/null +++ b/DarkMatterResourceGlyph.gd.uid @@ -0,0 +1 @@ +uid://bldtveflnyu2x diff --git a/DarkMatterResourceGlyph.tscn b/DarkMatterResourceGlyph.tscn new file mode 100644 index 0000000..21d54f0 --- /dev/null +++ b/DarkMatterResourceGlyph.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://DarkMatterResourceGlyph.gd" id="1"] + +[node name="GlyphVisual" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("1") diff --git a/GameState.gd b/GameState.gd index a1e385c..910aa6b 100644 --- a/GameState.gd +++ b/GameState.gd @@ -33,6 +33,7 @@ var explored_chunks: Dictionary = {} const RESOURCE_ORE: String = RESOURCE_DEFINITIONS.ORE_ID const RESOURCE_GAS: String = RESOURCE_DEFINITIONS.GAS_ID const RESOURCE_ENERGY: String = RESOURCE_DEFINITIONS.ENERGY_ID +const RESOURCE_DARK_MATTER: String = RESOURCE_DEFINITIONS.DARK_MATTER_ID const RESOURCE_TYPES: PackedStringArray = RESOURCE_DEFINITIONS.ORDER # All resource-bearing locations use the same container. The old public @@ -1115,7 +1116,11 @@ func register_capture_points(planet_key: String, total: int) -> void: var captured: Array = [] captured.resize(total) captured.fill(false) - planet_capture[planet_key] = {"captured": captured, "conquered": false} + # A planet demanding zero points is vacuously already held (#55: a + # black hole's "void" profile has no authored PlanetSetup at all, so + # this is the only registration it ever gets) - every planet WITH real + # points still starts unconquered, exactly as before. + planet_capture[planet_key] = {"captured": captured, "conquered": total <= 0} func capture_state(planet_key: String) -> Dictionary: diff --git a/PlanetDefinitions.gd b/PlanetDefinitions.gd index fa9dd2d..bada655 100644 --- a/PlanetDefinitions.gd +++ b/PlanetDefinitions.gd @@ -3,10 +3,12 @@ extends RefCounted const ROCKY_ID: String = "rocky" const GAS_GIANT_ID: String = "gas_giant" +const BLACK_HOLE_ID: String = "black_hole" const DEFAULT_DEFINITION_ID: String = ROCKY_ID const DEFINITIONS: Dictionary = { ROCKY_ID: preload("res://RockyPlanetDefinition.tres"), GAS_GIANT_ID: preload("res://GasGiantDefinition.tres"), + BLACK_HOLE_ID: preload("res://BlackHolePlanetDefinition.tres"), } diff --git a/PlanetSurfaceController.gd b/PlanetSurfaceController.gd index 1a74b2c..2777a61 100644 --- a/PlanetSurfaceController.gd +++ b/PlanetSurfaceController.gd @@ -65,6 +65,16 @@ func _compose_surface(profile: Resource) -> void: var setup_host: Node = SETUP_HOST_SCENE.instantiate() feature_host.add_child(setup_host) setup_host.apply_setup(setup) + else: + # No PlanetSetup exists for this profile at all (#55: "void" has + # none authored) - register zero required points directly, rather + # than leaving this planet's conquest state unregistered forever. + # capture_state()'s fallback for an unregistered planet_id defaults + # to conquered=false (see test_planet_conquest_gate.gd's own "a + # fresh planet reported conquered" check), which would otherwise + # permanently block extractor placement here - register_capture_ + # points() treats a total of 0 as vacuously already conquered. + GameState.register_capture_points(GameState.planet_id, 0) func _on_exit_pressed() -> void: diff --git a/PlanetSurfaceProfiles.gd b/PlanetSurfaceProfiles.gd index 97da418..5624fae 100644 --- a/PlanetSurfaceProfiles.gd +++ b/PlanetSurfaceProfiles.gd @@ -5,6 +5,7 @@ const DEFAULT_PROFILE_ID: String = "rocky" const PROFILES: Dictionary = { "rocky": preload("res://RockySurfaceProfile.tres"), "gas": preload("res://GasSurfaceProfile.tres"), + "void": preload("res://VoidSurfaceProfile.tres"), } diff --git a/ResourceDefinitions.gd b/ResourceDefinitions.gd index 3726aae..12ca1bd 100644 --- a/ResourceDefinitions.gd +++ b/ResourceDefinitions.gd @@ -4,11 +4,13 @@ extends RefCounted const ORE_ID: String = "ore" const GAS_ID: String = "gas" const ENERGY_ID: String = "energy" -const ORDER: PackedStringArray = [ORE_ID, GAS_ID, ENERGY_ID] +const DARK_MATTER_ID: String = "dark_matter" +const ORDER: PackedStringArray = [ORE_ID, GAS_ID, ENERGY_ID, DARK_MATTER_ID] const DEFINITIONS: Dictionary = { ORE_ID: preload("res://OreResourceDefinition.tres"), GAS_ID: preload("res://GasResourceDefinition.tres"), ENERGY_ID: preload("res://EnergyResourceDefinition.tres"), + DARK_MATTER_ID: preload("res://DarkMatterResourceDefinition.tres"), } diff --git a/VoidSurfaceProfile.tres b/VoidSurfaceProfile.tres new file mode 100644 index 0000000..da0b6f3 --- /dev/null +++ b/VoidSurfaceProfile.tres @@ -0,0 +1,12 @@ +[gd_resource type="Resource" script_class="PlanetSurfaceProfile" load_steps=2 format=3] + +[ext_resource type="Script" path="res://PlanetSurfaceProfile.gd" id="1"] + +[resource] +script = ExtResource("1") +profile_id = "void" +base_color = Color(0.04, 0.02, 0.07, 1) +crater_color = Color(0.01, 0.005, 0.02, 1) +highlight_color = Color(0.45, 0.15, 0.65, 1) +caption_color = Color(0.85, 0.7, 0.95, 1) +caption_outline_color = Color(0.03, 0.01, 0.05, 0.9) diff --git a/WorldGen.gd b/WorldGen.gd index 7cc6c6c..3238051 100644 --- a/WorldGen.gd +++ b/WorldGen.gd @@ -16,6 +16,12 @@ const NOISE_SEED: int = 1337 const ASTEROID_FIELD_THRESHOLD: float = 0.15 const PLANET_THRESHOLD: float = 0.33 const GAS_GIANT_THRESHOLD: float = 0.55 +# A flat per-planet-chunk probability rather than another noise field: gas +# giants already cover every SPACE-biome planet chunk (see GAS_GIANT_THRESHOLD +# above), so making black holes noticeably rarer just needs a small, uniform +# chance independent of biome - not a second smooth region to tune against +# the first. +const BLACK_HOLE_CHANCE: float = 0.05 # A predictable early gas source for play-testing. The origin chunk is # deterministically normal space. Its explicit in-chunk position puts it @@ -84,10 +90,20 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary: var local_pos: Vector2 = NEARBY_GAS_GIANT_LOCAL_POS if guaranteed_nearby_giant \ else Vector2(rng.randf() * CHUNK_SIZE, rng.randf() * CHUNK_SIZE) var shape_seed: int = rng.randi() + # Drawn unconditionally, in the same fixed position every call, + # regardless of biome or the gas-giant roll below - so adding this + # roll doesn't shift which random values any LATER draw (planet_name, + # below) consumes for chunks that would have existed before it. + var black_hole_roll: float = rng.randf() # A stable per-chunk roll makes gas giants persistent landmarks rather # than a different resource whenever the scene reloads. var gas_giant: bool = guaranteed_nearby_giant or biome_at_chunk(chunk) == Biome.SPACE var definition_id: String = PLANET_DEFINITIONS.GAS_GIANT_ID if gas_giant else PLANET_DEFINITIONS.ROCKY_ID + # A rare third outcome, seeded the same deterministic way as everything + # else here. Never overrides the guaranteed early gas giant landmark - + # that chunk's whole purpose is to always be the same reachable source. + if not guaranteed_nearby_giant and black_hole_roll < BLACK_HOLE_CHANCE: + definition_id = PLANET_DEFINITIONS.BLACK_HOLE_ID var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id) var names: PackedStringArray = definition.names var planet_name: String = names[rng.randi() % names.size()] diff --git a/tests/_black_hole_space_preview.gd b/tests/_black_hole_space_preview.gd new file mode 100644 index 0000000..7949634 --- /dev/null +++ b/tests/_black_hole_space_preview.gd @@ -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) diff --git a/tests/_black_hole_space_preview.tscn b/tests/_black_hole_space_preview.tscn new file mode 100644 index 0000000..7d178d5 --- /dev/null +++ b/tests/_black_hole_space_preview.tscn @@ -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") diff --git a/tests/_void_surface_preview.gd b/tests/_void_surface_preview.gd new file mode 100644 index 0000000..6ff3607 --- /dev/null +++ b/tests/_void_surface_preview.gd @@ -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) diff --git a/tests/_void_surface_preview.tscn b/tests/_void_surface_preview.tscn new file mode 100644 index 0000000..e2bda33 --- /dev/null +++ b/tests/_void_surface_preview.tscn @@ -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") diff --git a/tests/test_black_hole.gd b/tests/test_black_hole.gd new file mode 100644 index 0000000..304536f --- /dev/null +++ b/tests/test_black_hole.gd @@ -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