Guide the fleet to the nearest unconquered scan target
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
parent
fd792e8b63
commit
10d19a369b
9 changed files with 311 additions and 38 deletions
|
|
@ -202,6 +202,9 @@ var dev_mode: bool = false:
|
||||||
# every scene swap) so exploration progress isn't lost every time the
|
# every scene swap) so exploration progress isn't lost every time the
|
||||||
# player lands on and leaves a planet.
|
# player lands on and leaves a planet.
|
||||||
var explored_chunks: Dictionary = {}
|
var explored_chunks: Dictionary = {}
|
||||||
|
# The scanner's latest unconquered destination. Unlike explored_chunks this
|
||||||
|
# is one intentional navigation target, shared by the map and flight HUD.
|
||||||
|
var scanner_target: Dictionary = {}
|
||||||
|
|
||||||
# The four ordinary destinations teach the scanner enough signatures to
|
# The four ordinary destinations teach the scanner enough signatures to
|
||||||
# identify the hostile boss world. Kept explicit so adding a sixth authored
|
# identify the hostile boss world. Kept explicit so adding a sixth authored
|
||||||
|
|
@ -791,6 +794,7 @@ func reset_new_game() -> void:
|
||||||
dev_unlimited_resources = false
|
dev_unlimited_resources = false
|
||||||
dev_mode = false
|
dev_mode = false
|
||||||
explored_chunks.clear()
|
explored_chunks.clear()
|
||||||
|
scanner_target.clear()
|
||||||
bank_inventory = INVENTORY.new()
|
bank_inventory = INVENTORY.new()
|
||||||
fleet_inventory = INVENTORY.new()
|
fleet_inventory = INVENTORY.new()
|
||||||
buildings.clear()
|
buildings.clear()
|
||||||
|
|
@ -822,56 +826,95 @@ func scanner_available_planet_types() -> PackedStringArray:
|
||||||
return available
|
return available
|
||||||
|
|
||||||
|
|
||||||
# Finds the nearest generated planet of this type, reveals its neighbourhood
|
# Finds the nearest unconquered generated planet of this type, reveals its
|
||||||
# on the persistent map, and returns the same identity PlanetField will spawn.
|
# neighbourhood on the persistent map, and retains it as the flight target.
|
||||||
# The search is ring-shaped, so the first hit is nearest in chunk distance and
|
# Rings bound the work, but candidates are compared by their actual generated
|
||||||
# results do not depend on dictionary iteration order.
|
# world position: the first planet in the first matching chunk ring is not
|
||||||
|
# necessarily the physically closest one when local positions differ.
|
||||||
func scan_for_planet(definition_id: String, origin_world_position: Vector2) -> Dictionary:
|
func scan_for_planet(definition_id: String, origin_world_position: Vector2) -> Dictionary:
|
||||||
if not structure_exists(BUILDING_PLANET_SCANNER) \
|
if not structure_exists(BUILDING_PLANET_SCANNER) \
|
||||||
or definition_id not in scanner_available_planet_types():
|
or definition_id not in scanner_available_planet_types():
|
||||||
return {}
|
return {}
|
||||||
var origin: Vector2i = WORLD_GEN.chunk_coords(origin_world_position)
|
var origin: Vector2i = WORLD_GEN.chunk_coords(origin_world_position)
|
||||||
|
var best: Dictionary = {}
|
||||||
|
var best_distance: float = INF
|
||||||
for radius in range(0, 257):
|
for radius in range(0, 257):
|
||||||
if radius == 0:
|
if radius == 0:
|
||||||
var center_match: Dictionary = _scanner_match_at(origin,
|
var center: Dictionary = _scanner_candidate_at(origin,
|
||||||
definition_id, radius)
|
definition_id, radius)
|
||||||
if not center_match.is_empty():
|
if not center.is_empty():
|
||||||
return center_match
|
best = center
|
||||||
continue
|
best_distance = origin_world_position.distance_to(
|
||||||
# Only the perimeter is new at each radius. The old square-with-a-
|
Vector2(center.world_position))
|
||||||
# condition loop revisited every inner chunk for every radius, turning
|
else:
|
||||||
# a wide scan cubic precisely when the phone had the most work to do.
|
# Only the perimeter is new at each radius. Every matching candidate
|
||||||
for x in range(-radius, radius + 1):
|
# on it is considered before deciding whether the search can stop.
|
||||||
for y in [-radius, radius]:
|
for x in range(-radius, radius + 1):
|
||||||
var horizontal: Dictionary = _scanner_match_at(
|
for y in [-radius, radius]:
|
||||||
origin + Vector2i(x, y), definition_id, radius)
|
var horizontal: Dictionary = _scanner_candidate_at(
|
||||||
if not horizontal.is_empty():
|
origin + Vector2i(x, y), definition_id, radius)
|
||||||
return horizontal
|
var horizontal_distance: float = INF if horizontal.is_empty() else \
|
||||||
for y in range(-radius + 1, radius):
|
origin_world_position.distance_to(Vector2(horizontal.world_position))
|
||||||
for x in [-radius, radius]:
|
if horizontal_distance < best_distance:
|
||||||
var vertical: Dictionary = _scanner_match_at(
|
best = horizontal
|
||||||
origin + Vector2i(x, y), definition_id, radius)
|
best_distance = horizontal_distance
|
||||||
if not vertical.is_empty():
|
for y in range(-radius + 1, radius):
|
||||||
return vertical
|
for x in [-radius, radius]:
|
||||||
return {}
|
var vertical: Dictionary = _scanner_candidate_at(
|
||||||
|
origin + Vector2i(x, y), definition_id, radius)
|
||||||
|
var vertical_distance: float = INF if vertical.is_empty() else \
|
||||||
|
origin_world_position.distance_to(Vector2(vertical.world_position))
|
||||||
|
if vertical_distance < best_distance:
|
||||||
|
best = vertical
|
||||||
|
best_distance = vertical_distance
|
||||||
|
if not best.is_empty() and best_distance <= _scanner_unsearched_distance(
|
||||||
|
origin, origin_world_position, radius):
|
||||||
|
return _accept_scanner_target(best)
|
||||||
|
return _accept_scanner_target(best) if not best.is_empty() else {}
|
||||||
|
|
||||||
|
|
||||||
func _scanner_match_at(chunk: Vector2i, definition_id: String,
|
func _scanner_candidate_at(chunk: Vector2i, definition_id: String,
|
||||||
distance_chunks: int) -> Dictionary:
|
distance_chunks: int) -> Dictionary:
|
||||||
if not WORLD_GEN.has_planet_at_chunk(chunk):
|
if not WORLD_GEN.has_planet_at_chunk(chunk):
|
||||||
return {}
|
return {}
|
||||||
var data: Dictionary = WORLD_GEN.planet_data_at_chunk(chunk)
|
var data: Dictionary = WORLD_GEN.planet_data_at_chunk(chunk)
|
||||||
if String(data.get("definition_id", "")) != definition_id:
|
if String(data.get("definition_id", "")) != definition_id \
|
||||||
|
or is_planet_conquered(String(data.get("planet_id", ""))):
|
||||||
return {}
|
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["chunk"] = chunk
|
||||||
data["world_position"] = WORLD_GEN.chunk_origin(chunk) + Vector2(data.local_pos)
|
data["world_position"] = WORLD_GEN.chunk_origin(chunk) + Vector2(data.local_pos)
|
||||||
data["distance_chunks"] = distance_chunks
|
data["distance_chunks"] = distance_chunks
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
func _scanner_unsearched_distance(origin_chunk: Vector2i,
|
||||||
|
origin_world_position: Vector2, radius: int) -> float:
|
||||||
|
var top_left: Vector2 = WORLD_GEN.chunk_origin(
|
||||||
|
origin_chunk - Vector2i(radius, radius))
|
||||||
|
var bottom_right: Vector2 = WORLD_GEN.chunk_origin(
|
||||||
|
origin_chunk + Vector2i(radius + 1, radius + 1))
|
||||||
|
return minf(minf(origin_world_position.x - top_left.x,
|
||||||
|
bottom_right.x - origin_world_position.x), minf(
|
||||||
|
origin_world_position.y - top_left.y,
|
||||||
|
bottom_right.y - origin_world_position.y))
|
||||||
|
|
||||||
|
|
||||||
|
func _accept_scanner_target(data: Dictionary) -> Dictionary:
|
||||||
|
var chunk := Vector2i(data.chunk)
|
||||||
|
for reveal_y in range(-1, 2):
|
||||||
|
for reveal_x in range(-1, 2):
|
||||||
|
explored_chunks[chunk + Vector2i(reveal_x, reveal_y)] = true
|
||||||
|
scanner_target = data.duplicate(true)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
func active_scanner_target() -> Dictionary:
|
||||||
|
if scanner_target.is_empty() \
|
||||||
|
or is_planet_conquered(String(scanner_target.get("planet_id", ""))):
|
||||||
|
return {}
|
||||||
|
return scanner_target
|
||||||
|
|
||||||
|
|
||||||
# Amounts a dev game opens with: enough of everything to reach any part of
|
# 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.
|
# the game immediately, without being so large that costs stop registering.
|
||||||
const DEV_RESOURCE_AMOUNT: int = 2000
|
const DEV_RESOURCE_AMOUNT: int = 2000
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,17 @@ extends Node2D
|
||||||
|
|
||||||
# Preview, not a test (underscore prefix keeps it out of run_tests.sh).
|
# Preview, not a test (underscore prefix keeps it out of run_tests.sh).
|
||||||
# Renders the world map with buildings placed and an extractor built, so the
|
# Renders the world map with buildings placed and an extractor built, so the
|
||||||
# issue #11 building markers and the issue #9 extractor badge can actually be
|
# issue #11 building markers, issue #9 extractor badge, and issue #200 scan
|
||||||
# LOOKED at. --headless draws no pixels, so no headless test can tell whether
|
# reticle can actually be LOOKED at. --headless draws no pixels, so no
|
||||||
# a marker is visible - see tests/screenshot.sh.
|
# headless test can tell whether a marker is visible - see tests/screenshot.sh.
|
||||||
#
|
#
|
||||||
# bash tests/screenshot.sh res://tests/_marker_preview.tscn /tmp/map.png 20 720x1280
|
# bash tests/screenshot.sh res://tests/_marker_preview.tscn /tmp/map.png 20 720x1280
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
GameState.reset_new_game()
|
||||||
|
GameState.intro_active = false
|
||||||
|
GameState.reset_quests(false)
|
||||||
GameState.buildings.clear()
|
GameState.buildings.clear()
|
||||||
GameState.extractors.clear()
|
GameState.extractors.clear()
|
||||||
GameState.explored_chunks.clear()
|
GameState.explored_chunks.clear()
|
||||||
|
|
@ -18,6 +21,8 @@ func _ready() -> void:
|
||||||
GameState.add_building(GameState.BUILDING_MOTHERSHIP, Vector2(900.0, -700.0))
|
GameState.add_building(GameState.BUILDING_MOTHERSHIP, Vector2(900.0, -700.0))
|
||||||
GameState.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(-1100.0, 300.0))
|
GameState.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(-1100.0, 300.0))
|
||||||
GameState.add_building(GameState.BUILDING_COMMAND_CENTER, Vector2(400.0, 1200.0))
|
GameState.add_building(GameState.BUILDING_COMMAND_CENTER, Vector2(400.0, 1200.0))
|
||||||
|
GameState.add_building(GameState.BUILDING_PLANET_SCANNER, Vector2(-350.0, -500.0))
|
||||||
|
GameState.scan_for_planet(PlanetDefinitions.GAS_GIANT_ID, Vector2.ZERO)
|
||||||
|
|
||||||
# Reveal a patch of map, and put an extractor on whichever planet is
|
# Reveal a patch of map, and put an extractor on whichever planet is
|
||||||
# nearest the origin so its badge is in frame too.
|
# nearest the origin so its badge is in frame too.
|
||||||
|
|
@ -42,4 +47,8 @@ func _ready() -> void:
|
||||||
# open_map(), not `visible = true` - opening also recentres the view on
|
# open_map(), not `visible = true` - opening also recentres the view on
|
||||||
# the fleet, and without that the map is drawn from its own origin in the
|
# the fleet, and without that the map is drawn from its own origin in the
|
||||||
# corner of the screen with everything near the fleet cropped off.
|
# corner of the screen with everything near the fleet cropped off.
|
||||||
main.get_node("UI/WorldMap").open_map()
|
var world_map: Control = main.get_node("UI/WorldMap")
|
||||||
|
world_map.open_map()
|
||||||
|
var target: Dictionary = GameState.active_scanner_target()
|
||||||
|
if not target.is_empty():
|
||||||
|
world_map.focus_world_position(Vector2(target.world_position))
|
||||||
|
|
|
||||||
16
tests/_scanner_guidance_preview.gd
Normal file
16
tests/_scanner_guidance_preview.gd
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Phone-sized visual fixture for the scanner's persistent flight direction.
|
||||||
|
# bash tests/screenshot.sh res://tests/_scanner_guidance_preview.tscn /tmp/scanner_guidance.png 45 720x1280
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.reset_new_game()
|
||||||
|
GameState.intro_active = false
|
||||||
|
GameState.reset_quests(false)
|
||||||
|
GameState.add_building(GameState.BUILDING_PLANET_SCANNER, Vector2.ZERO)
|
||||||
|
GameState.scan_for_planet(PlanetDefinitions.GAS_GIANT_ID, Vector2.ZERO)
|
||||||
|
var main: Node = load("res://world/Main.tscn").instantiate()
|
||||||
|
add_child(main)
|
||||||
|
main.get_node("EnemySpawner").set_process(false)
|
||||||
|
main.get_node("AsteroidField").set_process(false)
|
||||||
6
tests/_scanner_guidance_preview.tscn
Normal file
6
tests/_scanner_guidance_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/_scanner_guidance_preview.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="ScannerGuidancePreview" type="Node2D"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
@ -59,12 +59,29 @@ func _run() -> void:
|
||||||
or not state.is_building_unlocked(GameState.BUILDING_PLANET_SCANNER):
|
or not state.is_building_unlocked(GameState.BUILDING_PLANET_SCANNER):
|
||||||
return _fail("Deep-Space Scanning did not unlock its building")
|
return _fail("Deep-Space Scanning did not unlock its building")
|
||||||
state.add_building(GameState.BUILDING_PLANET_SCANNER, Vector2.ZERO)
|
state.add_building(GameState.BUILDING_PLANET_SCANNER, Vector2.ZERO)
|
||||||
|
var explored_before_scan: Dictionary = state.explored_chunks.duplicate()
|
||||||
var rocky: Dictionary = state.scan_for_planet(PlanetDefinitions.ROCKY_ID,
|
var rocky: Dictionary = state.scan_for_planet(PlanetDefinitions.ROCKY_ID,
|
||||||
Vector2.ZERO)
|
Vector2.ZERO)
|
||||||
if rocky.is_empty() or String(rocky.definition_id) != PlanetDefinitions.ROCKY_ID:
|
if rocky.is_empty() or String(rocky.definition_id) != PlanetDefinitions.ROCKY_ID:
|
||||||
return _fail("the scanner did not find a real rocky planet")
|
return _fail("the scanner did not find a real rocky planet")
|
||||||
|
if explored_before_scan.has(Vector2i(rocky.chunk)):
|
||||||
|
return _fail("the scanner test's first target was already explored")
|
||||||
if not state.explored_chunks.has(Vector2i(rocky.chunk)):
|
if not state.explored_chunks.has(Vector2i(rocky.chunk)):
|
||||||
return _fail("a scan result was not revealed on the persistent map")
|
return _fail("a scan result was not revealed on the persistent map")
|
||||||
|
if String(state.active_scanner_target().planet_id) != String(rocky.planet_id):
|
||||||
|
return _fail("the revealed planet was not retained as navigation target")
|
||||||
|
if not _is_nearest_unconquered(state, rocky, PlanetDefinitions.ROCKY_ID):
|
||||||
|
return _fail("the scanner did not select the physically closest rocky planet")
|
||||||
|
# A conquered nearest match must be skipped on the next scan.
|
||||||
|
state.planet_capture[String(rocky.planet_id)] = {
|
||||||
|
"captured": [], "conquered": true,
|
||||||
|
}
|
||||||
|
var next_rocky: Dictionary = state.scan_for_planet(PlanetDefinitions.ROCKY_ID,
|
||||||
|
Vector2.ZERO)
|
||||||
|
if next_rocky.is_empty() or String(next_rocky.planet_id) == String(rocky.planet_id) \
|
||||||
|
or state.is_planet_conquered(String(next_rocky.planet_id)):
|
||||||
|
return _fail("scanner reused the nearest conquered rocky planet")
|
||||||
|
print("scanner found the nearest unvisited world and skipped it after conquest")
|
||||||
if PlanetDefinitions.BOSS_ID in state.scanner_available_planet_types():
|
if PlanetDefinitions.BOSS_ID in state.scanner_available_planet_types():
|
||||||
return _fail("boss signature is available before the four-world survey")
|
return _fail("boss signature is available before the four-world survey")
|
||||||
|
|
||||||
|
|
@ -83,6 +100,13 @@ func _run() -> void:
|
||||||
Vector2.ZERO)
|
Vector2.ZERO)
|
||||||
if boss.is_empty() or Vector2i(boss.chunk) != WorldGen.OCTOPUS_BIOME_CENTER:
|
if boss.is_empty() or Vector2i(boss.chunk) != WorldGen.OCTOPUS_BIOME_CENTER:
|
||||||
return _fail("boss scan did not resolve the unique hostile region")
|
return _fail("boss scan did not resolve the unique hostile region")
|
||||||
|
var map_content: Node2D = load("res://world/WorldMapContent.gd").new()
|
||||||
|
var marker: Dictionary = map_content.scanner_target_marker()
|
||||||
|
if marker.is_empty() or String(marker.planet_id) != String(boss.planet_id) \
|
||||||
|
or Vector2(marker.position).distance_to(
|
||||||
|
Vector2(boss.world_position) * map_content.WORLD_TO_LOCAL) > 0.01:
|
||||||
|
return _fail("the active scan has no matching destination point on the map")
|
||||||
|
map_content.free()
|
||||||
print("researched scanner reveals generated destinations; the fifth option waits for 4/4")
|
print("researched scanner reveals generated destinations; the fifth option waits for 4/4")
|
||||||
|
|
||||||
# #192: the scanner is taught immediately before the widened survey.
|
# #192: the scanner is taught immediately before the widened survey.
|
||||||
|
|
@ -104,10 +128,27 @@ func _run() -> void:
|
||||||
root.add_child(host)
|
root.add_child(host)
|
||||||
var fake_fleet := FakeFleet.new()
|
var fake_fleet := FakeFleet.new()
|
||||||
fake_fleet.name = "Fleet"
|
fake_fleet.name = "Fleet"
|
||||||
fake_fleet.position = WorldGen.chunk_origin(WorldGen.OCTOPUS_BIOME_CENTER) \
|
fake_fleet.position = Vector2.ZERO
|
||||||
+ Vector2(WorldGen.CHUNK_SIZE * 0.5, WorldGen.CHUNK_SIZE * 0.5)
|
|
||||||
fake_fleet.add_to_group("player_fleet")
|
fake_fleet.add_to_group("player_fleet")
|
||||||
host.add_child(fake_fleet)
|
host.add_child(fake_fleet)
|
||||||
|
menu.close_menu()
|
||||||
|
var guidance: Control = load("res://ui/ScannerGuidance.tscn").instantiate()
|
||||||
|
root.add_child(guidance)
|
||||||
|
guidance._process(0.0)
|
||||||
|
if not guidance.visible or not guidance.get_node("TargetLabel").text.contains(
|
||||||
|
String(boss.name).to_upper()):
|
||||||
|
return _fail("the flight HUD did not point toward the scanned destination")
|
||||||
|
var indicator: Control = guidance.get_node("Indicator")
|
||||||
|
var indicator_center: Vector2 = indicator.position + indicator.size * 0.5
|
||||||
|
var viewport_size: Vector2 = guidance.get_viewport_rect().size
|
||||||
|
if indicator_center.x < 20.0 or indicator_center.y < 20.0 \
|
||||||
|
or indicator_center.x > viewport_size.x - 20.0 \
|
||||||
|
or indicator_center.y > viewport_size.y - 20.0:
|
||||||
|
return _fail("scanner direction indicator was placed outside the viewport")
|
||||||
|
guidance.queue_free()
|
||||||
|
|
||||||
|
fake_fleet.position = WorldGen.chunk_origin(WorldGen.OCTOPUS_BIOME_CENTER) \
|
||||||
|
+ Vector2(WorldGen.CHUNK_SIZE * 0.5, WorldGen.CHUNK_SIZE * 0.5)
|
||||||
var field: Node2D = load("res://world/OctopusField.gd").new()
|
var field: Node2D = load("res://world/OctopusField.gd").new()
|
||||||
field.name = "OctopusField"
|
field.name = "OctopusField"
|
||||||
host.add_child(field)
|
host.add_child(field)
|
||||||
|
|
@ -130,3 +171,24 @@ func _run() -> void:
|
||||||
func _fail(message: String) -> void:
|
func _fail(message: String) -> void:
|
||||||
print("FAIL: %s" % message)
|
print("FAIL: %s" % message)
|
||||||
quit(1)
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _is_nearest_unconquered(state: Node, result: Dictionary,
|
||||||
|
definition_id: String) -> bool:
|
||||||
|
var origin := Vector2.ZERO
|
||||||
|
var result_distance: float = origin.distance_to(Vector2(result.world_position))
|
||||||
|
var radius: int = int(result.distance_chunks) + 2
|
||||||
|
for x in range(-radius, radius + 1):
|
||||||
|
for y in range(-radius, radius + 1):
|
||||||
|
var chunk := Vector2i(x, y)
|
||||||
|
if not WorldGen.has_planet_at_chunk(chunk):
|
||||||
|
continue
|
||||||
|
var data: Dictionary = WorldGen.planet_data_at_chunk(chunk)
|
||||||
|
if String(data.definition_id) != definition_id \
|
||||||
|
or state.is_planet_conquered(String(data.planet_id)):
|
||||||
|
continue
|
||||||
|
var world_position: Vector2 = WorldGen.chunk_origin(chunk) \
|
||||||
|
+ Vector2(data.local_pos)
|
||||||
|
if origin.distance_to(world_position) < result_distance - 0.01:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
|
||||||
78
ui/ScannerGuidance.gd
Normal file
78
ui/ScannerGuidance.gd
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
class_name ScannerGuidance
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
const PLANET_DEFINITIONS = preload("res://planets/PlanetDefinitions.gd")
|
||||||
|
const EDGE_MARGIN: float = 34.0
|
||||||
|
|
||||||
|
@onready var indicator: Control = $Indicator
|
||||||
|
@onready var target_label: Label = $TargetLabel
|
||||||
|
|
||||||
|
var fleet: Node2D
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
fleet = get_tree().get_first_node_in_group("player_fleet") as Node2D
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
var target: Dictionary = GameState.active_scanner_target()
|
||||||
|
if target.is_empty() or GameState.intro_active or _modal_is_open():
|
||||||
|
visible = false
|
||||||
|
return
|
||||||
|
if fleet == null or not is_instance_valid(fleet):
|
||||||
|
fleet = get_tree().get_first_node_in_group("player_fleet") as Node2D
|
||||||
|
if fleet == null:
|
||||||
|
visible = false
|
||||||
|
return
|
||||||
|
|
||||||
|
visible = true
|
||||||
|
var target_world := Vector2(target.world_position)
|
||||||
|
var viewport_size: Vector2 = get_viewport_rect().size
|
||||||
|
var screen_position: Vector2 = get_viewport().get_canvas_transform() * target_world
|
||||||
|
var safe_margin: Vector2 = Vector2.ONE * EDGE_MARGIN + indicator.size * 0.5
|
||||||
|
var safe_rect := Rect2(safe_margin, viewport_size - safe_margin * 2.0)
|
||||||
|
var at: Vector2 = screen_position if safe_rect.has_point(screen_position) \
|
||||||
|
else edge_position(viewport_size, screen_position - viewport_size * 0.5)
|
||||||
|
indicator.position = at - indicator.size * 0.5
|
||||||
|
var toward_target: Vector2 = target_world - fleet.get_centroid()
|
||||||
|
if toward_target.length_squared() > 0.01:
|
||||||
|
indicator.rotation = toward_target.angle() + PI / 2.0
|
||||||
|
|
||||||
|
var definition: Resource = PLANET_DEFINITIONS.get_definition(
|
||||||
|
String(target.definition_id))
|
||||||
|
if definition != null:
|
||||||
|
indicator.glow_color = definition.map_color
|
||||||
|
indicator.queue_redraw()
|
||||||
|
var sectors: float = toward_target.length() / WorldGen.CHUNK_SIZE
|
||||||
|
target_label.text = "%s · %.1f SECTORS" % [
|
||||||
|
String(target.get("name", "SCANNER TARGET")).to_upper(), sectors]
|
||||||
|
_place_label(at, viewport_size)
|
||||||
|
|
||||||
|
|
||||||
|
func edge_position(viewport_size: Vector2, direction: Vector2) -> Vector2:
|
||||||
|
var center: Vector2 = viewport_size * 0.5
|
||||||
|
if direction.length_squared() <= 0.01:
|
||||||
|
return center
|
||||||
|
var half: Vector2 = center - Vector2.ONE * EDGE_MARGIN - indicator.size * 0.5
|
||||||
|
var scale_to_edge: float = minf(half.x / maxf(absf(direction.x), 0.001),
|
||||||
|
half.y / maxf(absf(direction.y), 0.001))
|
||||||
|
return center + direction * scale_to_edge
|
||||||
|
|
||||||
|
|
||||||
|
func _place_label(indicator_center: Vector2, viewport_size: Vector2) -> void:
|
||||||
|
var toward_center: Vector2 = viewport_size * 0.5 - indicator_center
|
||||||
|
if toward_center.length_squared() < 0.01:
|
||||||
|
toward_center = Vector2.DOWN
|
||||||
|
var wanted: Vector2 = indicator_center + toward_center.normalized() * 105.0 \
|
||||||
|
- target_label.size * 0.5
|
||||||
|
target_label.position = Vector2(
|
||||||
|
clampf(wanted.x, EDGE_MARGIN, viewport_size.x - target_label.size.x - EDGE_MARGIN),
|
||||||
|
clampf(wanted.y, EDGE_MARGIN, viewport_size.y - target_label.size.y - EDGE_MARGIN))
|
||||||
|
|
||||||
|
|
||||||
|
func _modal_is_open() -> bool:
|
||||||
|
for menu in get_tree().get_nodes_in_group("modal_menus"):
|
||||||
|
if menu.is_visible_in_tree():
|
||||||
|
return true
|
||||||
|
return false
|
||||||
32
ui/ScannerGuidance.tscn
Normal file
32
ui/ScannerGuidance.tscn
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
[gd_scene load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://ui/ScannerGuidance.gd" id="1"]
|
||||||
|
[ext_resource type="Script" path="res://ui/IconButton.gd" id="2"]
|
||||||
|
|
||||||
|
[node name="ScannerGuidance" type="Control"]
|
||||||
|
visible = false
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="Indicator" type="Control" parent="."]
|
||||||
|
mouse_filter = 2
|
||||||
|
offset_right = 92.0
|
||||||
|
offset_bottom = 92.0
|
||||||
|
script = ExtResource("2")
|
||||||
|
icon = 4
|
||||||
|
glow_color = Color(0.3, 0.95, 1, 1)
|
||||||
|
round_frame = true
|
||||||
|
|
||||||
|
[node name="TargetLabel" type="Label" parent="."]
|
||||||
|
mouse_filter = 2
|
||||||
|
offset_right = 420.0
|
||||||
|
offset_bottom = 76.0
|
||||||
|
theme_override_colors/font_color = Color(0.55, 0.95, 1, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0.02, 0.02, 0.08, 0.95)
|
||||||
|
theme_override_constants/outline_size = 4
|
||||||
|
theme_override_font_sizes/font_size = 27
|
||||||
|
text = "SCANNER TARGET"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=48 format=3]
|
[gd_scene load_steps=49 format=3]
|
||||||
[ext_resource type="PackedScene" path="res://ui/menus/BuildMenu.tscn" id="menu_build"]
|
[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/HangarMenu.tscn" id="menu_hangar"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/menus/LabMenu.tscn" id="menu_lab"]
|
[ext_resource type="PackedScene" path="res://ui/menus/LabMenu.tscn" id="menu_lab"]
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
[ext_resource type="PackedScene" path="res://ui/menus/SkillMenu.tscn" id="menu_skill"]
|
[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/DevMenu.tscn" id="menu_dev"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/menus/ScannerMenu.tscn" id="menu_scanner"]
|
[ext_resource type="PackedScene" path="res://ui/menus/ScannerMenu.tscn" id="menu_scanner"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ui/ScannerGuidance.tscn" id="scanner_guidance"]
|
||||||
|
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://world/Starfield.gd" id="1"]
|
[ext_resource type="Script" path="res://world/Starfield.gd" id="1"]
|
||||||
|
|
@ -296,6 +297,8 @@ offset_top = 188.0
|
||||||
offset_bottom = 252.0
|
offset_bottom = 252.0
|
||||||
script = ExtResource("32")
|
script = ExtResource("32")
|
||||||
|
|
||||||
|
[node name="ScannerGuidance" parent="UI" instance=ExtResource("scanner_guidance")]
|
||||||
|
|
||||||
[node name="QuestButton" type="Control" parent="UI"]
|
[node name="QuestButton" type="Control" parent="UI"]
|
||||||
anchor_left = 1.0
|
anchor_left = 1.0
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const WORLD_TO_LOCAL: float = 0.04
|
||||||
const EXTRACTOR_BADGE_COLOR: Color = Color(1.0, 0.85, 0.3)
|
const EXTRACTOR_BADGE_COLOR: Color = Color(1.0, 0.85, 0.3)
|
||||||
const EXTRACTOR_BADGE_OFFSET: Vector2 = Vector2(8.0, -8.0)
|
const EXTRACTOR_BADGE_OFFSET: Vector2 = Vector2(8.0, -8.0)
|
||||||
const EXTRACTOR_BADGE_RADIUS: float = 5.0
|
const EXTRACTOR_BADGE_RADIUS: float = 5.0
|
||||||
|
const SCANNER_TARGET_COLOR: Color = Color(0.3, 0.95, 1.0)
|
||||||
|
|
||||||
@onready var tracker: Node2D = get_node("../../../ExplorationTracker")
|
@onready var tracker: Node2D = get_node("../../../ExplorationTracker")
|
||||||
@onready var fleet: Node2D = get_node("../../../Fleet")
|
@onready var fleet: Node2D = get_node("../../../Fleet")
|
||||||
|
|
@ -53,6 +54,17 @@ func chunk_has_extractor_badge(chunk: Vector2i) -> bool:
|
||||||
return GameState.has_extractor_at(WorldGen.planet_data_at_chunk(chunk).planet_id)
|
return GameState.has_extractor_at(WorldGen.planet_data_at_chunk(chunk).planet_id)
|
||||||
|
|
||||||
|
|
||||||
|
func scanner_target_marker() -> Dictionary:
|
||||||
|
var target: Dictionary = GameState.active_scanner_target()
|
||||||
|
if target.is_empty():
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"position": Vector2(target.world_position) * WORLD_TO_LOCAL,
|
||||||
|
"planet_id": String(target.planet_id),
|
||||||
|
"definition_id": String(target.definition_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func _regular_polygon(center: Vector2, radius: float, sides: int, phase: float = 0.0) -> PackedVector2Array:
|
func _regular_polygon(center: Vector2, radius: float, sides: int, phase: float = 0.0) -> PackedVector2Array:
|
||||||
var pts: PackedVector2Array = PackedVector2Array()
|
var pts: PackedVector2Array = PackedVector2Array()
|
||||||
for i in range(sides):
|
for i in range(sides):
|
||||||
|
|
@ -103,6 +115,18 @@ func _draw() -> void:
|
||||||
for marker in building_markers():
|
for marker in building_markers():
|
||||||
_draw_marker_polygon(_regular_polygon(marker.position, marker.radius, marker.sides, marker.phase), marker.color)
|
_draw_marker_polygon(_regular_polygon(marker.position, marker.radius, marker.sides, marker.phase), marker.color)
|
||||||
|
|
||||||
|
# A scan is an intentional destination, distinct from passive exploration.
|
||||||
|
# Draw its reticle independently of the explored-chunk pass so the point is
|
||||||
|
# guaranteed to remain visible even when it lies beyond travelled space.
|
||||||
|
var scan_marker: Dictionary = scanner_target_marker()
|
||||||
|
if not scan_marker.is_empty():
|
||||||
|
var target_position := Vector2(scan_marker.position)
|
||||||
|
draw_arc(target_position, 11.0, 0.0, TAU, 24,
|
||||||
|
SCANNER_TARGET_COLOR, 2.5, true)
|
||||||
|
for direction in [Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN]:
|
||||||
|
draw_line(target_position + direction * 13.0,
|
||||||
|
target_position + direction * 19.0, SCANNER_TARGET_COLOR, 2.0, true)
|
||||||
|
|
||||||
var fleet_local: Vector2 = fleet.get_centroid() * WORLD_TO_LOCAL
|
var fleet_local: Vector2 = fleet.get_centroid() * WORLD_TO_LOCAL
|
||||||
var marker_points: PackedVector2Array = PackedVector2Array([
|
var marker_points: PackedVector2Array = PackedVector2Array([
|
||||||
fleet_local + Vector2(0, -10),
|
fleet_local + Vector2(0, -10),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue