Add planet scanner and octopus biome
This commit is contained in:
parent
2ecd61de98
commit
8fbcc340ce
21 changed files with 784 additions and 30 deletions
|
|
@ -1,7 +1,7 @@
|
|||
class_name BuildingDefinitions
|
||||
extends RefCounted
|
||||
|
||||
const ORDER: PackedStringArray = ["mothership", "ship_crafter", "command_center", "outpost", "planning_center", "spacial_workshop", "extractor", "transport_hub", "ship_lab", "gas_burner"]
|
||||
const ORDER: PackedStringArray = ["mothership", "ship_crafter", "command_center", "outpost", "planning_center", "spacial_workshop", "planet_scanner", "extractor", "transport_hub", "ship_lab", "gas_burner"]
|
||||
const DEFINITIONS: Dictionary = {
|
||||
"mothership": preload("res://buildings/MothershipBuildingDefinition.tres"),
|
||||
"ship_crafter": preload("res://buildings/CrafterBuildingDefinition.tres"),
|
||||
|
|
@ -9,6 +9,7 @@ const DEFINITIONS: Dictionary = {
|
|||
"outpost": preload("res://buildings/OutpostBuildingDefinition.tres"),
|
||||
"planning_center": preload("res://buildings/PlanningBuildingDefinition.tres"),
|
||||
"spacial_workshop": preload("res://buildings/WorkshopBuildingDefinition.tres"),
|
||||
"planet_scanner": preload("res://buildings/ScannerBuildingDefinition.tres"),
|
||||
"extractor": preload("res://buildings/ExtractorBuildingDefinition.tres"),
|
||||
"transport_hub": preload("res://buildings/TransportHubBuildingDefinition.tres"),
|
||||
"ship_lab": preload("res://buildings/ShipLabBuildingDefinition.tres"),
|
||||
|
|
|
|||
25
buildings/ScannerBuildingDefinition.tres
Normal file
25
buildings/ScannerBuildingDefinition.tres
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://buildings/BuildingDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://buildings/ScannerBuildingVisual.tscn" id="2"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "planet_scanner"
|
||||
display_name = "Planet Scanner"
|
||||
description = "Searches deep space for the nearest planet of a chosen type and reveals it on the world map. Survey all four ordinary planet types to identify the tentacled world."
|
||||
cost = 45
|
||||
build_resource_costs = {"ore": 45, "gas": 15}
|
||||
action_icon = 47
|
||||
menu_icon = 47
|
||||
visual_scene = ExtResource("2")
|
||||
capabilities = PackedStringArray("planet_scanner")
|
||||
map_color = Color(0.35, 0.88, 1, 1)
|
||||
map_sides = 6
|
||||
map_radius = 8.0
|
||||
map_phase = 0.523599
|
||||
menu_button_name = "ScannerButton"
|
||||
menu_label_name = "ScannerLabel"
|
||||
menu_cost_name = "ScannerCost"
|
||||
unique = true
|
||||
max_hp = 75
|
||||
26
buildings/ScannerBuildingVisual.gd
Normal file
26
buildings/ScannerBuildingVisual.gd
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
extends Node2D
|
||||
|
||||
const COLOR: Color = Color(0.35, 0.88, 1.0)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_process(false)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
# A broad dish on an open orbital truss. Static by design: scanning is a
|
||||
# deliberate menu action, so a permanent sweep animation would promise
|
||||
# background work that the building does not perform.
|
||||
NeonDraw.draw_glowing_path(self, PackedVector2Array([
|
||||
Vector2(-58, -34), Vector2(-34, 12), Vector2(12, 30),
|
||||
]), COLOR, 0.9, 0.7)
|
||||
NeonDraw.draw_glowing_line(self, Vector2(-31, 12), Vector2(-12, 62),
|
||||
COLOR, 0.9, 0.7)
|
||||
NeonDraw.draw_glowing_line(self, Vector2(-52, 62), Vector2(30, 62),
|
||||
COLOR, 0.9, 0.7)
|
||||
NeonDraw.draw_glowing_circle(self, Vector2(-25, 4), 7.0,
|
||||
Color(0.95, 0.3, 0.72), 1.0, Color(0.06, 0.02, 0.12))
|
||||
for radius in [34.0, 52.0, 70.0]:
|
||||
draw_arc(Vector2(-30, 3), radius, -1.15, -0.18, 18,
|
||||
COLOR, 2.2, true)
|
||||
11
buildings/ScannerBuildingVisual.tscn
Normal file
11
buildings/ScannerBuildingVisual.tscn
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://buildings/ScannerBuildingVisual.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://buildings/BuildingActionProvider.gd" id="2"]
|
||||
|
||||
[node name="Visual" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="ActionProvider" type="Node" parent="."]
|
||||
script = ExtResource("2")
|
||||
actions = PackedStringArray("scanner")
|
||||
|
|
@ -32,6 +32,7 @@ const HUD_ROW_GAP := 16.0
|
|||
@onready var skill_menu: Control = get_node("../UI/SkillMenu")
|
||||
@onready var planning_menu: Control = get_node("../UI/PlanningMenu")
|
||||
@onready var garrison_menu: Control = get_node("../UI/GarrisonMenu")
|
||||
@onready var scanner_menu: Control = get_node("../UI/ScannerMenu")
|
||||
|
||||
var pending_build_type: String = ""
|
||||
var craft_building_id: int = 0
|
||||
|
|
@ -42,6 +43,7 @@ var lab_can_buy: bool = false
|
|||
var transfer_building_id: int = 0
|
||||
var transfer_extractor_index: int = -1
|
||||
var garrison_building_id: int = 0
|
||||
var scanner_building_id: int = 0
|
||||
|
||||
var current_building: Node2D = null
|
||||
var pending_building: Node2D = null
|
||||
|
|
@ -76,6 +78,8 @@ func _ready() -> void:
|
|||
formation_menu.formation_changed.connect(_on_formation_changed)
|
||||
formation_menu.reset_requested.connect(_on_reset_formation)
|
||||
skill_menu.research_requested.connect(_on_unlock_requested)
|
||||
scanner_menu.scan_requested.connect(_on_scan_requested)
|
||||
scanner_menu.closed.connect(func(): scanner_building_id = 0)
|
||||
|
||||
for control in [action_button, building_action_button, landing_button,
|
||||
transfer_button, hangar_button]:
|
||||
|
|
@ -107,7 +111,7 @@ func _seat_hud_columns() -> void:
|
|||
|
||||
func _menu_is_open() -> bool:
|
||||
for menu in [build_menu, craft_menu, lab_menu, transfer_menu, hangar_menu,
|
||||
formation_menu, skill_menu, planning_menu, garrison_menu]:
|
||||
formation_menu, skill_menu, planning_menu, garrison_menu, scanner_menu]:
|
||||
if menu.visible:
|
||||
return true
|
||||
return false
|
||||
|
|
@ -307,6 +311,24 @@ func _on_action_pressed() -> void:
|
|||
elif building_action == "garrison":
|
||||
garrison_building_id = building_id
|
||||
garrison_menu.open_for(building_id)
|
||||
elif building_action == "scanner":
|
||||
scanner_building_id = building_id
|
||||
scanner_menu.open_menu()
|
||||
|
||||
|
||||
func _on_scan_requested(definition_id: String) -> void:
|
||||
var scanner: Dictionary = GameState.find_building(scanner_building_id)
|
||||
if scanner.is_empty():
|
||||
scanner_menu.show_failure(definition_id)
|
||||
return
|
||||
var result: Dictionary = GameState.scan_for_planet(definition_id,
|
||||
Vector2(scanner.position))
|
||||
if result.is_empty():
|
||||
scanner_menu.show_failure(definition_id)
|
||||
return
|
||||
scanner_menu.close_menu()
|
||||
world_map.open_map()
|
||||
world_map.focus_world_position(Vector2(result.world_position))
|
||||
|
||||
|
||||
func _on_build_pressed() -> void:
|
||||
|
|
|
|||
|
|
@ -203,6 +203,13 @@ var dev_mode: bool = false:
|
|||
# player lands on and leaves a planet.
|
||||
var explored_chunks: Dictionary = {}
|
||||
|
||||
# The four ordinary destinations teach the scanner enough signatures to
|
||||
# identify the hostile boss world. Kept explicit so adding a sixth authored
|
||||
# planet later cannot silently move the progression gate.
|
||||
const SCANNER_SURVEY_TYPES: PackedStringArray = [PLANET_DEFINITIONS.ROCKY_ID,
|
||||
PLANET_DEFINITIONS.GAS_GIANT_ID, PLANET_DEFINITIONS.BLACK_HOLE_ID,
|
||||
PLANET_DEFINITIONS.RESEARCH_LAB_ID]
|
||||
|
||||
# --- Resources -------------------------------------------------------------
|
||||
|
||||
const RESOURCE_ORE: String = RESOURCE_DEFINITIONS.ORE_ID
|
||||
|
|
@ -430,6 +437,9 @@ const BUILDING_SHIP_LAB: String = "ship_lab"
|
|||
# Converts gas into energy, both stored/moved through the building's own
|
||||
# inventory exactly like every other building (#54).
|
||||
const BUILDING_GAS_BURNER: String = "gas_burner"
|
||||
# A map-revealing structure, not a passive sensor bonus. Its menu searches
|
||||
# the same deterministic WorldGen catalogue that PlanetField spawns from.
|
||||
const BUILDING_PLANET_SCANNER: String = "planet_scanner"
|
||||
|
||||
|
||||
var buildings: Array = []
|
||||
|
|
@ -532,6 +542,13 @@ const SKILL_TREE: Array = [
|
|||
"x": 0.82, "unlocks": BUILDING_SHIP_LAB, "research_points": 1,
|
||||
"desc": "Build ship labs, where each vessel type is upgraded and new ones unlocked.",
|
||||
},
|
||||
{
|
||||
"id": "deep_space_scanning", "name": "Deep-Space Scanning", "cost": 50,
|
||||
"costs": {"ore": 50, "gas": 15},
|
||||
"requires": "coordination", "x": 0.34,
|
||||
"unlocks": BUILDING_PLANET_SCANNER, "research_points": 1,
|
||||
"desc": "Build a planet scanner and reveal the nearest world of a chosen type.",
|
||||
},
|
||||
{
|
||||
"id": "freight_yards", "name": "Freight Yards", "cost": 80, "requires": "freight",
|
||||
"costs": {"ore": 80, "gas": 30},
|
||||
|
|
@ -791,6 +808,70 @@ func reset_new_game() -> void:
|
|||
reset_formation()
|
||||
|
||||
|
||||
func scanner_can_find_boss() -> bool:
|
||||
for definition_id in SCANNER_SURVEY_TYPES:
|
||||
if not visited_planet_kinds.has(definition_id):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func scanner_available_planet_types() -> PackedStringArray:
|
||||
var available: PackedStringArray = SCANNER_SURVEY_TYPES.duplicate()
|
||||
if scanner_can_find_boss():
|
||||
available.append(PLANET_DEFINITIONS.BOSS_ID)
|
||||
return available
|
||||
|
||||
|
||||
# Finds the nearest generated planet of this type, reveals its neighbourhood
|
||||
# on the persistent map, and returns the same identity PlanetField will spawn.
|
||||
# The search is ring-shaped, so the first hit is nearest in chunk distance and
|
||||
# results do not depend on dictionary iteration order.
|
||||
func scan_for_planet(definition_id: String, origin_world_position: Vector2) -> Dictionary:
|
||||
if not structure_exists(BUILDING_PLANET_SCANNER) \
|
||||
or definition_id not in scanner_available_planet_types():
|
||||
return {}
|
||||
var origin: Vector2i = WORLD_GEN.chunk_coords(origin_world_position)
|
||||
for radius in range(0, 257):
|
||||
if radius == 0:
|
||||
var center_match: Dictionary = _scanner_match_at(origin,
|
||||
definition_id, radius)
|
||||
if not center_match.is_empty():
|
||||
return center_match
|
||||
continue
|
||||
# Only the perimeter is new at each radius. The old square-with-a-
|
||||
# condition loop revisited every inner chunk for every radius, turning
|
||||
# a wide scan cubic precisely when the phone had the most work to do.
|
||||
for x in range(-radius, radius + 1):
|
||||
for y in [-radius, radius]:
|
||||
var horizontal: Dictionary = _scanner_match_at(
|
||||
origin + Vector2i(x, y), definition_id, radius)
|
||||
if not horizontal.is_empty():
|
||||
return horizontal
|
||||
for y in range(-radius + 1, radius):
|
||||
for x in [-radius, radius]:
|
||||
var vertical: Dictionary = _scanner_match_at(
|
||||
origin + Vector2i(x, y), definition_id, radius)
|
||||
if not vertical.is_empty():
|
||||
return vertical
|
||||
return {}
|
||||
|
||||
|
||||
func _scanner_match_at(chunk: Vector2i, definition_id: String,
|
||||
distance_chunks: int) -> Dictionary:
|
||||
if not WORLD_GEN.has_planet_at_chunk(chunk):
|
||||
return {}
|
||||
var data: Dictionary = WORLD_GEN.planet_data_at_chunk(chunk)
|
||||
if String(data.get("definition_id", "")) != definition_id:
|
||||
return {}
|
||||
for reveal_y in range(-1, 2):
|
||||
for reveal_x in range(-1, 2):
|
||||
explored_chunks[chunk + Vector2i(reveal_x, reveal_y)] = true
|
||||
data["chunk"] = chunk
|
||||
data["world_position"] = WORLD_GEN.chunk_origin(chunk) + Vector2(data.local_pos)
|
||||
data["distance_chunks"] = distance_chunks
|
||||
return data
|
||||
|
||||
|
||||
# Amounts a dev game opens with: enough of everything to reach any part of
|
||||
# the game immediately, without being so large that costs stop registering.
|
||||
const DEV_RESOURCE_AMOUNT: int = 2000
|
||||
|
|
|
|||
|
|
@ -233,13 +233,21 @@ const CHAIN: Array = [
|
|||
"brief": "An empty outpost is only a building. Open its hangar and station a ship there. It will hold that ground whether we are there or not.",
|
||||
"done": "The outpost has a crew. It watches that approach now, so we do not have to.",
|
||||
},
|
||||
{
|
||||
"id": "planet_scanner",
|
||||
"title": "LISTENING POST",
|
||||
"objective": "Build a planet scanner",
|
||||
"goal": {"kind": GOAL_BUILDING, "building": "planet_scanner"},
|
||||
"brief": "Blind searching is wasting our reach. Research Deep-Space Scanning at the mothership, then build a planet scanner. Give it a world signature and it will put the nearest match on our map.",
|
||||
"done": "The scanner is listening. Now we can choose where the search goes next.",
|
||||
},
|
||||
{
|
||||
"id": "worlds",
|
||||
"title": "THE SURVEY",
|
||||
"objective": "Land on three different kinds of world",
|
||||
"goal": {"kind": GOAL_PLANET_KINDS, "count": 3},
|
||||
"brief": "Rock, gas, and whatever else is out here. Land on three different kinds of world. If the World Ship came this way, it passed something.",
|
||||
"done": "Three kinds of world surveyed. Not one of them holds any sign of the convoy.",
|
||||
"objective": "Land on four different kinds of world",
|
||||
"goal": {"kind": GOAL_PLANET_KINDS, "count": 4},
|
||||
"brief": "Rock, gas, the abandoned labs, and the lightless wells. Use the scanner and land on all four kinds of world. If the World Ship came this way, it passed something.",
|
||||
"done": "Four kinds of world surveyed. No convoy—but their signatures resolve a fifth signal, hidden inside something alive.",
|
||||
"important": true,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
22
tests/_scanner_menu_preview.gd
Normal file
22
tests/_scanner_menu_preview.gd
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
extends Node2D
|
||||
|
||||
# Phone-sized visual fixture for the scene-owned scanner panel.
|
||||
# bash tests/screenshot.sh res://tests/_scanner_menu_preview.tscn /tmp/scanner.png 20 720x1280
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.reset_new_game()
|
||||
GameState.intro_active = false
|
||||
GameState.reset_quests(false)
|
||||
GameState.unlocked.append("deep_space_scanning")
|
||||
GameState.add_building(GameState.BUILDING_PLANET_SCANNER, Vector2.ZERO)
|
||||
for definition_id in GameState.SCANNER_SURVEY_TYPES:
|
||||
GameState.note_planet_visited("preview:%s" % definition_id, definition_id)
|
||||
# The panel owns a full-screen backdrop, so loading the entire live world
|
||||
# only adds software-renderer work and no pixels that can be seen.
|
||||
var menu: Control = load("res://ui/menus/ScannerMenu.tscn").instantiate()
|
||||
add_child(menu)
|
||||
menu.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
menu.position = Vector2.ZERO
|
||||
menu.size = get_viewport_rect().size
|
||||
menu.open_menu()
|
||||
6
tests/_scanner_menu_preview.tscn
Normal file
6
tests/_scanner_menu_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_scanner_menu_preview.gd" id="1"]
|
||||
|
||||
[node name="ScannerMenuPreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
|
@ -4,8 +4,9 @@ extends SceneTree
|
|||
# point loop. This test drives the real landed surface and encounter nodes.
|
||||
#
|
||||
# Covers:
|
||||
# A. the planet/profile are registered, dev mode plants it nearby, and its
|
||||
# scripted profile is excluded from the map editor
|
||||
# A. the planet/profile are registered, dev mode plants it nearby, normal
|
||||
# generation owns exactly one inside its octopus biome, and its scripted
|
||||
# profile is excluded from the map editor
|
||||
# B. landing composes BossEncounter instead of a PlanetSetup and does not
|
||||
# mark the world conquered for free
|
||||
# C. the off-screen approach remains controllable and reveals with its own cue
|
||||
|
|
@ -86,20 +87,20 @@ func _process(delta: float) -> bool:
|
|||
!= PlanetDefinitions.BOSS_ID:
|
||||
return _fail("dev mode does not plant the boss planet")
|
||||
state.dev_mode = false
|
||||
var normal_boss_found: bool = false
|
||||
var normal_boss_count: int = 0
|
||||
for x in range(-24, 25):
|
||||
for y in range(-24, 25):
|
||||
var chunk := Vector2i(x, y)
|
||||
if WorldGen.has_planet_at_chunk(chunk) \
|
||||
and String(WorldGen.planet_data_at_chunk(chunk).get(
|
||||
"definition_id", "")) == PlanetDefinitions.BOSS_ID:
|
||||
normal_boss_found = true
|
||||
break
|
||||
if normal_boss_found:
|
||||
break
|
||||
if not normal_boss_found:
|
||||
return _fail("normal world generation never produces the boss planet")
|
||||
print("boss planet registered, editor-hidden, and generated normally plus in dev mode")
|
||||
normal_boss_count += 1
|
||||
if normal_boss_count != 1 \
|
||||
or WorldGen.biome_at_chunk(WorldGen.OCTOPUS_BIOME_CENTER) \
|
||||
!= WorldGen.Biome.OCTOPUS:
|
||||
return _fail("normal world generation has %d bosses, or its only boss is outside the octopus biome" \
|
||||
% normal_boss_count)
|
||||
print("boss planet registered, editor-hidden, unique in its normal octopus biome, plus nearby in dev mode")
|
||||
|
||||
# --- B. generic surface composition ---------------------------
|
||||
encounter = surface.get_node_or_null("SurfaceFeatures/BossEncounter")
|
||||
|
|
|
|||
|
|
@ -21,10 +21,16 @@ func _initialize() -> void:
|
|||
])
|
||||
return
|
||||
var provider: Node = building.visual.get_node_or_null("ActionProvider")
|
||||
if definition.definition_id == GameState.BUILDING_TRANSPORT_HUB:
|
||||
if "transport_hub" in definition.capabilities:
|
||||
if provider != null or not building.available_actions().is_empty():
|
||||
_fail("transport hub composed a manual action")
|
||||
return
|
||||
elif "planet_scanner" in definition.capabilities:
|
||||
if provider == null or not provider.actions.has("scanner") \
|
||||
or provider.actions.has("transfer"):
|
||||
_fail("%s did not compose its scanner-only action" \
|
||||
% definition.definition_id)
|
||||
return
|
||||
elif provider == null or not provider.actions.has("transfer"):
|
||||
_fail("%s has no composed transfer action" % definition.definition_id)
|
||||
return
|
||||
|
|
|
|||
132
tests/test_planet_scanner.gd
Normal file
132
tests/test_planet_scanner.gd
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
extends SceneTree
|
||||
|
||||
# Focused coverage for #192-#196: the scanner progression, its scene-owned
|
||||
# menu, the one hostile region and the ambient enemies that inhabit it.
|
||||
|
||||
const QUESTS = preload("res://core/Quests.gd")
|
||||
|
||||
class FakeFleet:
|
||||
extends Node2D
|
||||
var ships: Array = [Node2D.new()]
|
||||
|
||||
func get_centroid() -> Vector2:
|
||||
return position
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var state: Node = root.get_node("GameState")
|
||||
state.reset_new_game()
|
||||
state.intro_active = false
|
||||
state.reset_quests(false)
|
||||
|
||||
# #194: exact, finite hostile geography instead of an endless rare roll.
|
||||
WorldGen.dev_planets_enabled = false
|
||||
var boss_count: int = 0
|
||||
for x in range(-40, 41):
|
||||
for y in range(-40, 41):
|
||||
var chunk := Vector2i(x, y)
|
||||
if WorldGen.has_planet_at_chunk(chunk) \
|
||||
and String(WorldGen.planet_data_at_chunk(chunk).definition_id) \
|
||||
== PlanetDefinitions.BOSS_ID:
|
||||
boss_count += 1
|
||||
if boss_count != 1:
|
||||
return _fail("normal generation contains %d boss planets, expected exactly one" % boss_count)
|
||||
if WorldGen.biome_at_chunk(WorldGen.OCTOPUS_BIOME_CENTER) \
|
||||
!= WorldGen.Biome.OCTOPUS:
|
||||
return _fail("the unique boss is not inside the octopus biome")
|
||||
if WorldGen.octopus_biome_chunks().size() < 5:
|
||||
return _fail("the octopus biome is not a multi-chunk region")
|
||||
for chunk in WorldGen.octopus_biome_chunks():
|
||||
if WorldGen.biome_at_chunk(chunk) != WorldGen.Biome.OCTOPUS:
|
||||
return _fail("declared octopus chunk %s has another biome" % chunk)
|
||||
if chunk != WorldGen.OCTOPUS_BIOME_CENTER \
|
||||
and WorldGen.has_planet_at_chunk(chunk):
|
||||
return _fail("the hostile region contains an extra planet at %s" % chunk)
|
||||
print("one finite nine-chunk octopus biome contains the one normal boss planet")
|
||||
|
||||
# #193/#195: research gate, real generated scan results and boss gating.
|
||||
if state.is_building_unlocked(GameState.BUILDING_PLANET_SCANNER):
|
||||
return _fail("planet scanner is available without research")
|
||||
state.unlocked.append("coordination")
|
||||
state.bank_inventory.deposit(GameState.RESOURCE_ORE, 50)
|
||||
state.bank_inventory.deposit(GameState.RESOURCE_GAS, 15)
|
||||
state.research_points = 1
|
||||
if not state.unlock("deep_space_scanning") \
|
||||
or not state.is_building_unlocked(GameState.BUILDING_PLANET_SCANNER):
|
||||
return _fail("Deep-Space Scanning did not unlock its building")
|
||||
state.add_building(GameState.BUILDING_PLANET_SCANNER, Vector2.ZERO)
|
||||
var rocky: Dictionary = state.scan_for_planet(PlanetDefinitions.ROCKY_ID,
|
||||
Vector2.ZERO)
|
||||
if rocky.is_empty() or String(rocky.definition_id) != PlanetDefinitions.ROCKY_ID:
|
||||
return _fail("the scanner did not find a real rocky planet")
|
||||
if not state.explored_chunks.has(Vector2i(rocky.chunk)):
|
||||
return _fail("a scan result was not revealed on the persistent map")
|
||||
if PlanetDefinitions.BOSS_ID in state.scanner_available_planet_types():
|
||||
return _fail("boss signature is available before the four-world survey")
|
||||
|
||||
var menu: Control = load("res://ui/menus/ScannerMenu.tscn").instantiate()
|
||||
root.add_child(menu)
|
||||
menu.open_menu()
|
||||
if menu.get_node("BossButton").visible:
|
||||
return _fail("scanner menu shows the boss before the survey")
|
||||
for definition_id in GameState.SCANNER_SURVEY_TYPES:
|
||||
state.note_planet_visited("scanner:test:%s" % definition_id, definition_id)
|
||||
menu.refresh()
|
||||
if not menu.get_node("BossButton").visible \
|
||||
or not menu.get_node("BossLabel").visible:
|
||||
return _fail("the fifth scanner option did not appear after four planet types")
|
||||
var boss: Dictionary = state.scan_for_planet(PlanetDefinitions.BOSS_ID,
|
||||
Vector2.ZERO)
|
||||
if boss.is_empty() or Vector2i(boss.chunk) != WorldGen.OCTOPUS_BIOME_CENTER:
|
||||
return _fail("boss scan did not resolve the unique hostile region")
|
||||
print("researched scanner reveals generated destinations; the fifth option waits for 4/4")
|
||||
|
||||
# #192: the scanner is taught immediately before the widened survey.
|
||||
var scanner_quest_at: int = -1
|
||||
var worlds_at: int = -1
|
||||
for i in range(QUESTS.count()):
|
||||
if String(QUESTS.at(i).id) == "planet_scanner":
|
||||
scanner_quest_at = i
|
||||
if String(QUESTS.at(i).id) == "worlds":
|
||||
worlds_at = i
|
||||
if scanner_quest_at < 0 or worlds_at != scanner_quest_at + 1:
|
||||
return _fail("scanner quest is not directly before the world survey")
|
||||
if int(QUESTS.at(worlds_at).goal.count) != 4:
|
||||
return _fail("world survey does not require four planet types")
|
||||
print("the quest chain teaches the scanner immediately before the four-world survey")
|
||||
|
||||
# #196: entering/leaving the biome owns a bounded roaming population.
|
||||
var host := Node2D.new()
|
||||
root.add_child(host)
|
||||
var fake_fleet := FakeFleet.new()
|
||||
fake_fleet.name = "Fleet"
|
||||
fake_fleet.position = WorldGen.chunk_origin(WorldGen.OCTOPUS_BIOME_CENTER) \
|
||||
+ Vector2(WorldGen.CHUNK_SIZE * 0.5, WorldGen.CHUNK_SIZE * 0.5)
|
||||
fake_fleet.add_to_group("player_fleet")
|
||||
host.add_child(fake_fleet)
|
||||
var field: Node2D = load("res://world/OctopusField.gd").new()
|
||||
field.name = "OctopusField"
|
||||
host.add_child(field)
|
||||
field._process(0.1)
|
||||
if field.active.size() != 1:
|
||||
return _fail("entering the octopus biome spawned %d roamers" % field.active.size())
|
||||
var roamer: Node = field.active[0]
|
||||
if roamer.max_hp < 7 or not roamer.is_in_group("roaming_octopuses"):
|
||||
return _fail("ambient octopus lacks its durable roaming composition")
|
||||
fake_fleet.position = Vector2.ZERO
|
||||
field._process(0.1)
|
||||
if not field.active.is_empty():
|
||||
return _fail("roaming octopuses followed the fleet outside their biome")
|
||||
print("octopus biome spawns bounded durable roamers and retires them at its edge")
|
||||
|
||||
print("TEST_PLANET_SCANNER: PASS")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _fail(message: String) -> void:
|
||||
print("FAIL: %s" % message)
|
||||
quit(1)
|
||||
|
|
@ -66,6 +66,9 @@ enum Icon {
|
|||
# Development tools: three sliders, used only by the editor-only sandbox
|
||||
# button. Appended to preserve every serialized icon number above.
|
||||
DEV_TOOLS,
|
||||
# #193: a directional sensor dish. Appended: scene files store these as
|
||||
# integers, so insertion would change every icon after it.
|
||||
SCANNER,
|
||||
}
|
||||
|
||||
# Size the glyphs were originally drawn against. Every glyph dimension is
|
||||
|
|
@ -328,6 +331,27 @@ func _draw() -> void:
|
|||
_draw_dash_icon(center, pulse, s)
|
||||
Icon.DEV_TOOLS:
|
||||
_draw_dev_tools_icon(center, pulse, s)
|
||||
Icon.SCANNER:
|
||||
_draw_scanner_icon(center, pulse, s)
|
||||
|
||||
|
||||
func _draw_scanner_icon(center: Vector2, pulse: float, s: float) -> void:
|
||||
# Dish, mast and three outward wavefronts: distinct from MAP (a folded
|
||||
# sheet) and RESEARCH (the mothership's archive).
|
||||
NeonDraw.draw_glowing_path(self, PackedVector2Array([
|
||||
center + Vector2(-12, -3) * s,
|
||||
center + Vector2(-5, 7) * s,
|
||||
center + Vector2(5, 4) * s,
|
||||
]), _active_color(), pulse, 0.6 * s, core_color)
|
||||
NeonDraw.draw_glowing_line(self, center + Vector2(-4, 7) * s,
|
||||
center + Vector2(-4, 14) * s, _active_color(), pulse, 0.6 * s,
|
||||
core_color)
|
||||
NeonDraw.draw_glowing_line(self, center + Vector2(-11, 14) * s,
|
||||
center + Vector2(3, 14) * s, _active_color(), pulse, 0.6 * s,
|
||||
core_color)
|
||||
for radius in [7.0, 13.0, 19.0]:
|
||||
draw_arc(center + Vector2(-7, 1) * s, radius * s, -1.1, -0.15, 10,
|
||||
_active_color(), maxf(1.0, 1.3 * s), true)
|
||||
|
||||
|
||||
func _draw_dev_tools_icon(center: Vector2, pulse: float, s: float) -> void:
|
||||
|
|
|
|||
|
|
@ -448,6 +448,46 @@ offset_top = 1554
|
|||
offset_bottom = 1606
|
||||
script = ExtResource("price_tag")
|
||||
|
||||
[node name="ScannerButton" type="Control" parent="."]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -64
|
||||
offset_right = 64
|
||||
offset_top = 1610
|
||||
offset_bottom = 1738
|
||||
script = ExtResource("10")
|
||||
icon = 47
|
||||
glow_color = Color(0.35, 0.88, 1, 1)
|
||||
|
||||
[node name="ScannerLabel" type="Label" parent="."]
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -150
|
||||
offset_right = 150
|
||||
offset_top = 1744
|
||||
offset_bottom = 1810
|
||||
theme_override_font_sizes/font_size = 32
|
||||
autowrap_mode = 2
|
||||
text = "Planet Scanner"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="ScannerCost" type="Control" parent="."]
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -60
|
||||
offset_right = 60
|
||||
offset_top = 1814
|
||||
offset_bottom = 1866
|
||||
script = ExtResource("price_tag")
|
||||
|
||||
[node name="CloseButton" type="Control" parent="."]
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
|
|
|
|||
56
ui/menus/ScannerMenu.gd
Normal file
56
ui/menus/ScannerMenu.gd
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
extends "res://ui/menus/ModalMenu.gd"
|
||||
|
||||
signal scan_requested(definition_id: String)
|
||||
|
||||
const PLANET_DEFINITIONS = preload("res://planets/PlanetDefinitions.gd")
|
||||
|
||||
@onready var status_label: Label = $StatusLabel
|
||||
@onready var progress_label: Label = $ProgressLabel
|
||||
|
||||
var entries: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
super._ready()
|
||||
entries = {
|
||||
PLANET_DEFINITIONS.ROCKY_ID: [$RockyButton, $RockyLabel],
|
||||
PLANET_DEFINITIONS.GAS_GIANT_ID: [$GasButton, $GasLabel],
|
||||
PLANET_DEFINITIONS.BLACK_HOLE_ID: [$BlackHoleButton, $BlackHoleLabel],
|
||||
PLANET_DEFINITIONS.RESEARCH_LAB_ID: [$ResearchButton, $ResearchLabel],
|
||||
PLANET_DEFINITIONS.BOSS_ID: [$BossButton, $BossLabel],
|
||||
}
|
||||
for definition_id in entries:
|
||||
var id: String = String(definition_id)
|
||||
var button: Control = entries[definition_id][0]
|
||||
button.pressed.connect(_request_scan.bind(id))
|
||||
refresh()
|
||||
|
||||
|
||||
func _request_scan(definition_id: String) -> void:
|
||||
scan_requested.emit(definition_id)
|
||||
|
||||
|
||||
func open_menu() -> void:
|
||||
super.open_menu()
|
||||
status_label.text = "Choose a signature to reveal its nearest world."
|
||||
refresh()
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var surveyed: int = 0
|
||||
for definition_id in GameState.SCANNER_SURVEY_TYPES:
|
||||
if GameState.visited_planet_kinds.has(definition_id):
|
||||
surveyed += 1
|
||||
var boss_visible: bool = GameState.scanner_can_find_boss()
|
||||
for node in entries[PLANET_DEFINITIONS.BOSS_ID]:
|
||||
node.visible = boss_visible
|
||||
progress_label.text = "PLANETARY SIGNATURES %d / %d" % [surveyed,
|
||||
GameState.SCANNER_SURVEY_TYPES.size()]
|
||||
if not boss_visible:
|
||||
progress_label.text += "\nSurvey every signature to isolate the hostile signal."
|
||||
|
||||
|
||||
func show_failure(definition_id: String) -> void:
|
||||
var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id)
|
||||
status_label.text = "No %s signature found within scanner range." \
|
||||
% definition.display_name.to_lower()
|
||||
184
ui/menus/ScannerMenu.tscn
Normal file
184
ui/menus/ScannerMenu.tscn
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/IconButton.gd" id="icon"]
|
||||
[ext_resource type="Script" path="res://ui/menus/ScannerMenu.gd" id="menu"]
|
||||
|
||||
[node name="ScannerMenu" type="Control" groups=["modal_menus"]]
|
||||
visible = false
|
||||
mouse_filter = 2
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource("menu")
|
||||
|
||||
[node name="Backdrop" type="ColorRect" parent="."]
|
||||
mouse_filter = 2
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
color = Color(0.03, 0.03, 0.08, 0.96)
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="."]
|
||||
mouse_filter = 2
|
||||
anchor_right = 1.0
|
||||
offset_left = 24.0
|
||||
offset_top = 24.0
|
||||
offset_right = -180.0
|
||||
offset_bottom = 100.0
|
||||
theme_override_font_sizes/font_size = 42
|
||||
text = "PLANET SCANNER"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="ProgressLabel" type="Label" parent="."]
|
||||
mouse_filter = 2
|
||||
anchor_right = 1.0
|
||||
offset_left = 36.0
|
||||
offset_top = 130.0
|
||||
offset_right = -36.0
|
||||
offset_bottom = 230.0
|
||||
theme_override_font_sizes/font_size = 22
|
||||
theme_override_colors/font_color = Color(0.55, 0.78, 0.86, 1)
|
||||
text = "PLANETARY SIGNATURES 0 / 4"
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="RockyButton" type="Control" parent="."]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -276.0
|
||||
offset_top = 280.0
|
||||
offset_right = -116.0
|
||||
offset_bottom = 440.0
|
||||
script = ExtResource("icon")
|
||||
icon = 47
|
||||
glow_color = Color(0.7, 0.65, 0.55, 1)
|
||||
|
||||
[node name="RockyLabel" type="Label" parent="."]
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -326.0
|
||||
offset_top = 448.0
|
||||
offset_right = -66.0
|
||||
offset_bottom = 500.0
|
||||
theme_override_font_sizes/font_size = 28
|
||||
text = "Rocky planet"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="GasButton" type="Control" parent="."]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = 116.0
|
||||
offset_top = 280.0
|
||||
offset_right = 276.0
|
||||
offset_bottom = 440.0
|
||||
script = ExtResource("icon")
|
||||
icon = 47
|
||||
glow_color = Color(0.65, 0.42, 0.9, 1)
|
||||
|
||||
[node name="GasLabel" type="Label" parent="."]
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = 66.0
|
||||
offset_top = 448.0
|
||||
offset_right = 326.0
|
||||
offset_bottom = 500.0
|
||||
theme_override_font_sizes/font_size = 28
|
||||
text = "Gas giant"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="BlackHoleButton" type="Control" parent="."]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -276.0
|
||||
offset_top = 560.0
|
||||
offset_right = -116.0
|
||||
offset_bottom = 720.0
|
||||
script = ExtResource("icon")
|
||||
icon = 47
|
||||
glow_color = Color(0.55, 0.15, 0.85, 1)
|
||||
|
||||
[node name="BlackHoleLabel" type="Label" parent="."]
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -326.0
|
||||
offset_top = 728.0
|
||||
offset_right = -66.0
|
||||
offset_bottom = 780.0
|
||||
theme_override_font_sizes/font_size = 28
|
||||
text = "Black hole"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="ResearchButton" type="Control" parent="."]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = 116.0
|
||||
offset_top = 560.0
|
||||
offset_right = 276.0
|
||||
offset_bottom = 720.0
|
||||
script = ExtResource("icon")
|
||||
icon = 47
|
||||
glow_color = Color(0.7, 0.2, 0.15, 1)
|
||||
|
||||
[node name="ResearchLabel" type="Label" parent="."]
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = 66.0
|
||||
offset_top = 728.0
|
||||
offset_right = 326.0
|
||||
offset_bottom = 810.0
|
||||
theme_override_font_sizes/font_size = 28
|
||||
text = "Research Lab"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="BossButton" type="Control" parent="."]
|
||||
visible = false
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -80.0
|
||||
offset_top = 835.0
|
||||
offset_right = 80.0
|
||||
offset_bottom = 995.0
|
||||
script = ExtResource("icon")
|
||||
icon = 47
|
||||
glow_color = Color(0.95, 0.18, 0.58, 1)
|
||||
|
||||
[node name="BossLabel" type="Label" parent="."]
|
||||
visible = false
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -180.0
|
||||
offset_top = 1003.0
|
||||
offset_right = 180.0
|
||||
offset_bottom = 1055.0
|
||||
theme_override_font_sizes/font_size = 28
|
||||
text = "Tentacled world"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="."]
|
||||
mouse_filter = 2
|
||||
anchor_right = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 32.0
|
||||
offset_top = -170.0
|
||||
offset_right = -32.0
|
||||
offset_bottom = -30.0
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "Choose a signature to reveal its nearest world."
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="CloseButton" type="Control" parent="."]
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
offset_left = -164.0
|
||||
offset_top = 24.0
|
||||
offset_right = -24.0
|
||||
offset_bottom = 164.0
|
||||
script = ExtResource("icon")
|
||||
icon = 1
|
||||
glow_color = Color(0.35, 0.88, 1, 1)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=46 format=3]
|
||||
[gd_scene load_steps=48 format=3]
|
||||
[ext_resource type="PackedScene" path="res://ui/menus/BuildMenu.tscn" id="menu_build"]
|
||||
[ext_resource type="PackedScene" path="res://ui/menus/HangarMenu.tscn" id="menu_hangar"]
|
||||
[ext_resource type="PackedScene" path="res://ui/menus/LabMenu.tscn" id="menu_lab"]
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
[ext_resource type="PackedScene" path="res://ui/menus/GarrisonMenu.tscn" id="menu_garrison"]
|
||||
[ext_resource type="PackedScene" path="res://ui/menus/SkillMenu.tscn" id="menu_skill"]
|
||||
[ext_resource type="PackedScene" path="res://ui/menus/DevMenu.tscn" id="menu_dev"]
|
||||
[ext_resource type="PackedScene" path="res://ui/menus/ScannerMenu.tscn" id="menu_scanner"]
|
||||
|
||||
|
||||
[ext_resource type="Script" path="res://world/Starfield.gd" id="1"]
|
||||
|
|
@ -45,6 +46,7 @@
|
|||
[ext_resource type="Script" path="res://ui/SpeedFader.gd" id="31"]
|
||||
[ext_resource type="Script" path="res://world/QuestDirector.gd" id="quest_dir"]
|
||||
[ext_resource type="Script" path="res://ui/QuestPanel.gd" id="quest_panel"]
|
||||
[ext_resource type="Script" path="res://world/OctopusField.gd" id="octopus_field"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="1"]
|
||||
shader = ExtResource("5")
|
||||
|
|
@ -104,6 +106,9 @@ script = ExtResource("6")
|
|||
[node name="AsteroidField" type="Node2D" parent="."]
|
||||
script = ExtResource("7")
|
||||
|
||||
[node name="OctopusField" type="Node2D" parent="."]
|
||||
script = ExtResource("octopus_field")
|
||||
|
||||
[node name="PlanetField" type="Node2D" parent="."]
|
||||
script = ExtResource("11")
|
||||
|
||||
|
|
@ -513,6 +518,7 @@ icon = 1
|
|||
[node name="GarrisonMenu" parent="UI" instance=ExtResource("menu_garrison")]
|
||||
[node name="SkillMenu" parent="UI" instance=ExtResource("menu_skill")]
|
||||
[node name="DevMenu" parent="UI" instance=ExtResource("menu_dev")]
|
||||
[node name="ScannerMenu" parent="UI" instance=ExtResource("menu_scanner")]
|
||||
[node name="QuestPanel" type="Control" parent="UI" groups=["modal_menus"]]
|
||||
visible = false
|
||||
mouse_filter = 2
|
||||
|
|
|
|||
62
world/OctopusField.gd
Normal file
62
world/OctopusField.gd
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
extends Node2D
|
||||
|
||||
# A bounded ambient threat attached to the OCTOPUS biome. Like AsteroidField,
|
||||
# this node owns only the nearby presentation: the biome itself and its one
|
||||
# boss planet remain deterministic WorldGen data.
|
||||
|
||||
@export var spawn_interval: float = 2.4
|
||||
@export var max_octopuses: int = 7
|
||||
@export var spawn_margin: float = 180.0
|
||||
@export var retire_distance: float = 2600.0
|
||||
|
||||
@onready var fleet: Node2D = get_node("../Fleet")
|
||||
|
||||
var octopus_scene: PackedScene = preload("res://boss/BossMinion.tscn")
|
||||
var active: Array = []
|
||||
var spawn_left: float = 0.0
|
||||
var spawn_count: int = 0
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
active = active.filter(func(o): return is_instance_valid(o))
|
||||
var in_biome: bool = WorldGen.biome_at_chunk(
|
||||
WorldGen.chunk_coords(fleet.get_centroid())) == WorldGen.Biome.OCTOPUS
|
||||
if not in_biome or GameState.intro_active:
|
||||
retire_all()
|
||||
spawn_left = 0.0
|
||||
return
|
||||
for octopus in active.duplicate():
|
||||
if octopus.global_position.distance_to(fleet.get_centroid()) > retire_distance:
|
||||
octopus.retire()
|
||||
active.erase(octopus)
|
||||
spawn_left -= delta
|
||||
if spawn_left <= 0.0 and active.size() < max_octopuses:
|
||||
spawn_one()
|
||||
spawn_left = spawn_interval
|
||||
|
||||
|
||||
func spawn_one() -> Node2D:
|
||||
if fleet == null or fleet.ships.is_empty():
|
||||
return null
|
||||
var visible_rect: Rect2 = ViewUtils.get_visible_world_rect(self)
|
||||
var radius: float = maxf(visible_rect.size.length() * 0.5 + spawn_margin,
|
||||
620.0)
|
||||
var angle: float = randf() * TAU
|
||||
var octopus: Node2D = octopus_scene.instantiate()
|
||||
# Ambient hatchlings are sturdy enough to harass a travelling fleet, but
|
||||
# never marked: only the boss encounter's golden hatchlings open eyes.
|
||||
octopus.max_hp = 7
|
||||
octopus.position = fleet.get_centroid() + Vector2.from_angle(angle) * radius
|
||||
get_parent().add_child(octopus)
|
||||
octopus.uses_dash_pattern = spawn_count % 3 == 2
|
||||
octopus.add_to_group("roaming_octopuses")
|
||||
active.append(octopus)
|
||||
spawn_count += 1
|
||||
return octopus
|
||||
|
||||
|
||||
func retire_all() -> void:
|
||||
for octopus in active:
|
||||
if is_instance_valid(octopus):
|
||||
octopus.retire()
|
||||
active.clear()
|
||||
|
|
@ -15,6 +15,8 @@ extends ColorRect
|
|||
|
||||
@onready var camera: Camera2D = get_node("../../Camera2D")
|
||||
|
||||
var current_chunk: Vector2i = Vector2i(2147483647, 2147483647)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
var viewport_size: Vector2 = get_viewport_rect().size
|
||||
|
|
@ -26,3 +28,8 @@ func _process(_delta: float) -> void:
|
|||
material.set_shader_parameter("camera_frac", p - cell)
|
||||
material.set_shader_parameter("camera_zoom", 1.0 / camera.zoom.x)
|
||||
material.set_shader_parameter("viewport_size", viewport_size)
|
||||
var chunk: Vector2i = WorldGen.chunk_coords(camera.global_position)
|
||||
if chunk != current_chunk:
|
||||
current_chunk = chunk
|
||||
material.set_shader_parameter("bg_color",
|
||||
WorldGen.biome_background_color(WorldGen.biome_at_chunk(chunk)))
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const PLANET_DEFINITIONS = preload("res://planets/PlanetDefinitions.gd")
|
|||
# revisiting the same chunk always yields the same biome, and biomes form
|
||||
# smooth multi-chunk regions instead of a per-chunk random checkerboard.
|
||||
|
||||
enum Biome { SPACE, ASTEROID_FIELD }
|
||||
enum Biome { SPACE, ASTEROID_FIELD, OCTOPUS }
|
||||
|
||||
const CHUNK_SIZE: float = 1200.0
|
||||
const NOISE_SEED: int = 1337
|
||||
|
|
@ -37,10 +37,18 @@ const BLACK_HOLE_CHANCE: float = 0.05
|
|||
# from 0.08 after playtesting found that rate too scarce for how central
|
||||
# research points had become to progression.
|
||||
const RESEARCH_LAB_CHANCE: float = 0.16
|
||||
# A boss world is rarer than every ordinary discovery. Its roll uses a
|
||||
# separately seeded generator below, so adding it cannot rename or re-roll
|
||||
# any planet in an existing world.
|
||||
const BOSS_PLANET_CHANCE: float = 0.025
|
||||
# The hostile region is authored into procedural space as one finite,
|
||||
# deterministic island. A probability per chunk can only say "rare"; over an
|
||||
# infinite world it inevitably creates infinitely many boss worlds. One
|
||||
# anchor and one shape make the actual rule representable: one multi-chunk
|
||||
# octopus biome, with its boss at the centre.
|
||||
const OCTOPUS_BIOME_CENTER: Vector2i = Vector2i(12, -9)
|
||||
const OCTOPUS_BIOME_OFFSETS: Array[Vector2i] = [
|
||||
Vector2i(0, 0), Vector2i(-1, 0), Vector2i(1, 0),
|
||||
Vector2i(0, -1), Vector2i(0, 1),
|
||||
Vector2i(-1, -1), Vector2i(1, -1),
|
||||
Vector2i(-1, 1), Vector2i(1, 1),
|
||||
]
|
||||
|
||||
# DEV MODE ONLY: one of every planet type, parked in the chunks immediately
|
||||
# around the spawn so all five can be reached in seconds.
|
||||
|
|
@ -113,6 +121,8 @@ static func chunk_origin(chunk: Vector2i) -> Vector2:
|
|||
|
||||
|
||||
static func biome_at_chunk(chunk: Vector2i) -> int:
|
||||
if is_octopus_chunk(chunk):
|
||||
return Biome.OCTOPUS
|
||||
var n: float = _get_noise().get_noise_2d(float(chunk.x), float(chunk.y))
|
||||
if n > ASTEROID_FIELD_THRESHOLD:
|
||||
return Biome.ASTEROID_FIELD
|
||||
|
|
@ -122,6 +132,10 @@ static func biome_at_chunk(chunk: Vector2i) -> int:
|
|||
static func has_planet_at_chunk(chunk: Vector2i) -> bool:
|
||||
if _dev_planets_active() and DEV_PLANET_CHUNKS.has(chunk):
|
||||
return true
|
||||
if chunk == OCTOPUS_BIOME_CENTER:
|
||||
return true
|
||||
if is_octopus_chunk(chunk):
|
||||
return false
|
||||
var n: float = _get_planet_noise().get_noise_2d(float(chunk.x), float(chunk.y))
|
||||
return (biome_at_chunk(chunk) == Biome.ASTEROID_FIELD and n > PLANET_THRESHOLD) or (biome_at_chunk(chunk) == Biome.SPACE and n > GAS_GIANT_THRESHOLD)
|
||||
|
||||
|
|
@ -148,17 +162,12 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary:
|
|||
var black_hole_roll: float = rng.randf()
|
||||
# Same principle, its own independent roll (#63).
|
||||
var research_lab_roll: float = rng.randf()
|
||||
# Independent from the established generation stream: existing chunks keep
|
||||
# their old names and shapes when this new rare outcome is introduced.
|
||||
var boss_rng := RandomNumberGenerator.new()
|
||||
boss_rng.seed = hash("boss:%d:%d" % [chunk.x, chunk.y])
|
||||
var boss_roll: float = boss_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 = biome_at_chunk(chunk) == Biome.SPACE
|
||||
var definition_id: String = PLANET_DEFINITIONS.GAS_GIANT_ID if gas_giant else PLANET_DEFINITIONS.ROCKY_ID
|
||||
# Rare authored outcomes, seeded deterministically like everything else.
|
||||
if boss_roll < BOSS_PLANET_CHANCE:
|
||||
if chunk == OCTOPUS_BIOME_CENTER:
|
||||
definition_id = PLANET_DEFINITIONS.BOSS_ID
|
||||
elif black_hole_roll < BLACK_HOLE_CHANCE:
|
||||
definition_id = PLANET_DEFINITIONS.BLACK_HOLE_ID
|
||||
|
|
@ -186,5 +195,26 @@ static func biome_color(biome: int) -> Color:
|
|||
match biome:
|
||||
Biome.ASTEROID_FIELD:
|
||||
return Color(0.55, 0.36, 0.18, 1.0)
|
||||
Biome.OCTOPUS:
|
||||
return Color(0.34, 0.07, 0.24, 1.0)
|
||||
_:
|
||||
return Color(0.10, 0.13, 0.28, 1.0)
|
||||
|
||||
|
||||
static func biome_background_color(biome: int) -> Color:
|
||||
match biome:
|
||||
Biome.OCTOPUS:
|
||||
return Color(0.075, 0.012, 0.065, 1.0)
|
||||
_:
|
||||
return Color(0.02, 0.02, 0.08, 1.0)
|
||||
|
||||
|
||||
static func is_octopus_chunk(chunk: Vector2i) -> bool:
|
||||
return chunk - OCTOPUS_BIOME_CENTER in OCTOPUS_BIOME_OFFSETS
|
||||
|
||||
|
||||
static func octopus_biome_chunks() -> Array[Vector2i]:
|
||||
var chunks: Array[Vector2i] = []
|
||||
for offset in OCTOPUS_BIOME_OFFSETS:
|
||||
chunks.append(OCTOPUS_BIOME_CENTER + offset)
|
||||
return chunks
|
||||
|
|
|
|||
|
|
@ -58,7 +58,11 @@ func close_map() -> void:
|
|||
|
||||
|
||||
func recenter_view() -> void:
|
||||
var fleet_local: Vector2 = fleet.get_centroid() * content.WORLD_TO_LOCAL
|
||||
focus_world_position(fleet.get_centroid())
|
||||
|
||||
|
||||
func focus_world_position(world_position: Vector2) -> void:
|
||||
var fleet_local: Vector2 = world_position * content.WORLD_TO_LOCAL
|
||||
var viewport_center: Vector2 = get_viewport_rect().size / 2.0
|
||||
content.position = viewport_center - fleet_local * content.scale.x
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue