extends RefCounted const BALANCE := preload("res://scripts/levels/level_balance.gd") const NAMES := preload("res://scripts/levels/celestial_names.gd") const ALIEN_SHIPS := preload("res://scripts/items/alien_ship_catalog.gd") const WORLD_SIZE := Vector2(5200, 3600) const PLAYER_START := Vector2(180, 200) const SAFE_ZONE_RADIUS := 1200.0 const ENEMY_MIN_SEPARATION := 420.0 const CELESTIAL_TEXTURES := preload("res://scripts/levels/celestial_texture_catalog.gd") const STAR_TEXTURES: Array[Texture2D] = CELESTIAL_TEXTURES.STAR_TEXTURES const PLANET_TEXTURES: Array[Texture2D] = CELESTIAL_TEXTURES.PLANET_TEXTURES static func generate(level: int, seed_value: int) -> Dictionary: var rng := RandomNumberGenerator.new() rng.seed = seed_value # Separate stream keeps names stable without changing existing system geometry. var name_rng := RandomNumberGenerator.new() name_rng.seed = seed_value ^ 0x4e414d45 var texture_rng := RandomNumberGenerator.new() texture_rng.seed = seed_value ^ 0x54455854 var planet_names := NAMES.PLANETS.duplicate() var star_names := NAMES.STARS.duplicate() var star_name := NAMES.take_name(star_names, name_rng) # Keep layout random draws independent of the expanded asset catalog. rng.randi_range(0, 7) var star_texture := STAR_TEXTURES[texture_rng.randi_range(0, STAR_TEXTURES.size() - 1)] var planet_count := rng.randi_range(1, 5) var protected_index := rng.randi_range(0, planet_count - 1) var angle_offset := rng.randf_range(0.0, TAU) var planets: Array[Dictionary] = [] for index in planet_count: var angle := angle_offset + TAU * index / planet_count + rng.randf_range(-0.12, 0.12) var orbit := 1000.0 + 110.0 * index + rng.randf_range(-40.0, 40.0) var planet_radius := rng.randf_range(70.0, 105.0) rng.randi_range(0, 17) planets.append({ "name": NAMES.take_name(planet_names, name_rng), "position": Vector2.RIGHT.rotated(angle) * orbit, "radius": planet_radius, "texture": PLANET_TEXTURES[texture_rng.randi_range(0, PLANET_TEXTURES.size() - 1)], "protected": index == protected_index, }) var enemies: Array[Dictionary] = [] var ship_rng := RandomNumberGenerator.new() ship_rng.seed = seed_value ^ (level * 0x53484950) var enemy_texture_index := ship_rng.randi_range(0, ALIEN_SHIPS.TEXTURES.size() - 1) var tiers := BALANCE.enemy_tiers(level) # Spread ships across the playable area instead of packing them around planets. for tier in tiers: var position := _find_enemy_position(planets, enemies, rng) enemies.append({ "position": position, "tier": tier, "texture_index": enemy_texture_index, }) return { "enemy_texture_index": enemy_texture_index, "star_name": star_name, "star_texture": star_texture, "star_position": Vector2.ZERO, "player_start": PLAYER_START, "planets": planets, "protected_index": protected_index, "enemies": enemies, } static func _find_enemy_position(planets: Array[Dictionary], enemies: Array[Dictionary], rng: RandomNumberGenerator) -> Vector2: var best := Vector2.ZERO var best_distance := -1.0 var valid_samples := 0 # Prefer the clearest of several random positions to avoid accidental clusters. for attempt in 3000: var candidate := Vector2(rng.randf_range(-2380, 2380), rng.randf_range(-1580, 1580)) if not _is_valid_enemy_position(candidate, planets, enemies): continue if enemies.is_empty(): return candidate var nearest := INF for enemy in enemies: nearest = minf(nearest, candidate.distance_squared_to(enemy["position"])) if nearest > best_distance: best_distance = nearest best = candidate valid_samples += 1 if valid_samples >= 16: return best if best_distance >= 0.0: return best # Deterministic fallback still respects spacing, planets and the starting area. for x in range(-2280, 2281, 130): for y in range(-1500, 1501, 130): var candidate := Vector2(x, y) if _is_valid_enemy_position(candidate, planets, enemies): return candidate push_error("Could not place an enemy outside the safe zone.") return Vector2(2380, 1580) static func _is_valid_enemy_position(candidate: Vector2, planets: Array[Dictionary], enemies: Array[Dictionary]) -> bool: if absf(candidate.x) > 2380 or absf(candidate.y) > 1580: return false if candidate.length() < SAFE_ZONE_RADIUS or candidate.distance_to(PLAYER_START) < 940.0: return false for planet in planets: var center: Vector2 = planet["position"] if candidate.distance_to(center) < float(planet["radius"]) + 75.0: return false for enemy in enemies: var center: Vector2 = enemy["position"] if candidate.distance_to(center) < ENEMY_MIN_SEPARATION: return false return true