extends Node2D const SYSTEM_GENERATOR := preload("res://scripts/levels/system_generator.gd") const LEVEL_BALANCE := preload("res://scripts/levels/level_balance.gd") const CELESTIAL_BODY := preload("res://scripts/levels/celestial_body.gd") const SERVICES_MENU := preload("res://scenes/ui/planet_services_menu.tscn") const PLANET_MARKER := preload("res://scripts/ui/protected_planet_marker.gd") const WORMHOLE_SCRIPT := preload("res://scripts/game/wormhole.gd") const WORLD_SIZE := SYSTEM_GENERATOR.WORLD_SIZE const SERVICE_APPROACH_DISTANCE := 180.0 const WORMHOLE_PLANET_DISTANCE := 620.0 const WORMHOLE_ENTRY_RADIUS := 48.0 const BOSS_SPAWN_DISTANCE := 1800.0 const BOSS_SPAWN_MARGIN := 160.0 const ENEMY_SCENE := preload("res://scenes/game/enemy_ship.tscn") const ENEMY_TEXTURES: Array[Texture2D] = preload("res://scripts/items/alien_ship_catalog.gd").TEXTURES @onready var player: CharacterBody2D = $PlayerShip var system_data: Dictionary = {} var protected_planet: Node2D var services_menu: Control var system_cleared := false var wormhole: Node2D var _transitioning := false var _pending_boss: Dictionary = {} var _boss_spawned := false func _ready() -> void: GameState.system_level = clampi(GameState.system_level, 1, LEVEL_BALANCE.MAX_LEVEL) if GameState.system_seed == 0: GameState.system_seed = GameState.create_system_seed() system_data = SYSTEM_GENERATOR.generate(GameState.system_level, GameState.system_seed) player.position = system_data["player_start"] var camera := player.get_node_or_null("Camera2D") as Camera2D if camera: camera.limit_left = int(-WORLD_SIZE.x * 0.5) camera.limit_right = int(WORLD_SIZE.x * 0.5) camera.limit_top = int(-WORLD_SIZE.y * 0.5) camera.limit_bottom = int(WORLD_SIZE.y * 0.5) _spawn_celestial_bodies() _spawn_enemies() preload("res://scripts/levels/exploration_generator.gd").spawn(self, system_data, player) var marker := Control.new() marker.set_script(PLANET_MARKER) marker.name = "ProtectedPlanetMarker" marker.mouse_filter = Control.MOUSE_FILTER_IGNORE marker.set("game", self) $HUD/HUDRoot.add_child(marker) marker.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) services_menu = SERVICES_MENU.instantiate() as Control services_menu.set("planet", protected_planet) $HUD/HUDRoot.add_child(services_menu) services_menu.connect("next_level_requested", open_wormhole) func _spawn_celestial_bodies() -> void: var bodies := Node2D.new() bodies.name = "CelestialBodies" bodies.z_index = -10 add_child(bodies) var star := Node2D.new() star.set_script(CELESTIAL_BODY) star.name = "CentralStar" star.set("display_name", system_data["star_name"]) star.set("body_texture", system_data["star_texture"]) star.set("radius", 140.0) star.set("is_star", true) bodies.add_child(star) player.set("charging_star", star) var solar_effect := Node2D.new() solar_effect.name = "SolarEnergyEffect" solar_effect.set_script(preload("res://scripts/game/solar_energy_effect.gd")) solar_effect.set("player", player) player.add_child(solar_effect) for index in system_data["planets"].size(): var data: Dictionary = system_data["planets"][index] var planet := Node2D.new() planet.set_script(CELESTIAL_BODY) planet.name = "Planet%d" % (index + 1) planet.set("display_name", data["name"]) planet.position = data["position"] planet.set("body_texture", data["texture"]) planet.set("light_source", star) planet.set("radius", data["radius"]) planet.set("is_protected", data["protected"]) bodies.add_child(planet) if data["protected"]: protected_planet = planet func _spawn_enemies() -> void: for data in system_data["enemies"]: if int(data["tier"]) == 2: _pending_boss = data.duplicate() else: _spawn_enemy(data, data["position"]) func _spawn_enemy(data: Dictionary, spawn_position: Vector2) -> CharacterBody2D: var enemy := ENEMY_SCENE.instantiate() as CharacterBody2D enemy.position = spawn_position enemy.call("apply_level_settings", GameState.system_level, int(data["tier"])) add_child(enemy) var texture: Texture2D = ENEMY_TEXTURES[int(data["texture_index"])] enemy.call("configure", player, texture) return enemy func _spawn_boss() -> void: if _boss_spawned or _pending_boss.is_empty(): return _boss_spawned = true _spawn_enemy(_pending_boss, _find_boss_spawn_position()) _pending_boss.clear() func _find_boss_spawn_position() -> Vector2: var rng := RandomNumberGenerator.new() rng.seed = GameState.system_seed ^ 0x424f5353 var angle := rng.randf_range(0.0, TAU) for step in 64: var candidate := player.global_position + Vector2.RIGHT.rotated(angle + TAU * step / 64.0) * BOSS_SPAWN_DISTANCE if absf(candidate.x) > WORLD_SIZE.x * 0.5 - BOSS_SPAWN_MARGIN or absf(candidate.y) > WORLD_SIZE.y * 0.5 - BOSS_SPAWN_MARGIN: continue if candidate.length() < 300.0: continue var clear := true for planet_data in system_data["planets"]: if candidate.distance_to(planet_data["position"]) < float(planet_data["radius"]) + 130.0: clear = false break if clear: return candidate # At the map edge, place the encounter toward the center. return player.global_position + (-player.global_position).normalized() * BOSS_SPAWN_DISTANCE func get_remaining_enemies() -> int: var remaining := 0 for enemy in get_tree().get_nodes_in_group("enemies"): if is_ancestor_of(enemy) and not enemy.is_queued_for_deletion() and float(enemy.get("hull")) > 0.0: remaining += 1 return remaining func _physics_process(_delta: float) -> void: if _transitioning or bool(player.get("_is_destroyed")): services_menu.call("hide_services") return if not system_cleared and get_remaining_enemies() == 0: if not _boss_spawned and not _pending_boss.is_empty(): _spawn_boss() else: system_cleared = true protected_planet.call("set_safe", true) if system_cleared and is_near_protected_planet(): if not services_menu.visible: services_menu.call("show_services", GameState.system_level >= LEVEL_BALANCE.MAX_LEVEL, is_instance_valid(wormhole)) else: services_menu.call("hide_services") if is_instance_valid(wormhole) and player.global_position.distance_to(wormhole.global_position) <= WORMHOLE_ENTRY_RADIUS: advance_to_next_level() func is_near_protected_planet() -> bool: return player.global_position.distance_to(protected_planet.global_position) <= float(protected_planet.get("radius")) + SERVICE_APPROACH_DISTANCE func open_wormhole() -> bool: if _transitioning or not system_cleared or not services_menu.visible or not is_near_protected_planet(): return false if bool(player.get("_is_destroyed")) or GameState.system_level >= LEVEL_BALANCE.MAX_LEVEL or is_instance_valid(wormhole): return false wormhole = Node2D.new() wormhole.name = "Wormhole" wormhole.set_script(WORMHOLE_SCRIPT) wormhole.position = _find_wormhole_position() add_child(wormhole) services_menu.call("show_services", false, true) return true func _find_wormhole_position() -> Vector2: var inward := -protected_planet.global_position.normalized() for step in 16: var candidate := protected_planet.global_position + inward.rotated(TAU * step / 16.0) * WORMHOLE_PLANET_DISTANCE if absf(candidate.x) > WORLD_SIZE.x * 0.5 - 160.0 or absf(candidate.y) > WORLD_SIZE.y * 0.5 - 160.0: continue if candidate.length() < 260.0: continue var clear := true for planet_data in system_data["planets"]: if candidate.distance_to(planet_data["position"]) < float(planet_data["radius"]) + 130.0: clear = false break if clear: return candidate return protected_planet.global_position + inward * WORMHOLE_PLANET_DISTANCE func advance_to_next_level() -> bool: if _transitioning or not system_cleared or not is_instance_valid(wormhole): return false if bool(player.get("_is_destroyed")) or GameState.system_level >= LEVEL_BALANCE.MAX_LEVEL: return false if player.global_position.distance_to(wormhole.global_position) > WORMHOLE_ENTRY_RADIUS: return false _transitioning = true services_menu.call("hide_services") _load_next_level.call_deferred() return true func _load_next_level() -> void: var previous_level := GameState.system_level var previous_seed := GameState.system_seed player.get_node("ShipInventory").call("preserve_for_restart") GameState.system_level += 1 GameState.system_seed = GameState.create_system_seed() var error := get_tree().reload_current_scene() if error != OK: GameState.system_level = previous_level GameState.system_seed = previous_seed GameState.restart_inventory.clear() _transitioning = false if is_near_protected_planet(): services_menu.call("show_services", false, true) push_error("Could not enter next system: %s" % error_string(error))