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 := 110.0 const STAR_TEXTURES: Array[Texture2D] = [ preload("res://assets/stars/s1.png"), preload("res://assets/stars/s2.png"), preload("res://assets/stars/s3.png"), preload("res://assets/stars/s4.png"), preload("res://assets/stars/s5.png"), preload("res://assets/stars/s6.png"), preload("res://assets/stars/s7.png"), preload("res://assets/stars/s8.png"), ] const PLANET_TEXTURES: Array[Texture2D] = [ preload("res://assets/planets/p1.png"), preload("res://assets/planets/p2.png"), preload("res://assets/planets/p3.png"), preload("res://assets/planets/p4.png"), preload("res://assets/planets/p5.png"), preload("res://assets/planets/p6.png"), preload("res://assets/planets/p7.png"), preload("res://assets/planets/p8.png"), preload("res://assets/planets/p9.png"), preload("res://assets/planets/p10.png"), preload("res://assets/planets/p11.png"), preload("res://assets/planets/p12.png"), preload("res://assets/planets/p13.png"), preload("res://assets/planets/p14.png"), preload("res://assets/planets/p15.png"), preload("res://assets/planets/p16.png"), preload("res://assets/planets/p17.png"), preload("res://assets/planets/p18.png"), ] 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 planet_names := NAMES.PLANETS.duplicate() var star_names := NAMES.STARS.duplicate() var star_name := NAMES.take_name(star_names, name_rng) var star_texture := STAR_TEXTURES[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) planets.append({ "name": NAMES.take_name(planet_names, name_rng), "position": Vector2.RIGHT.rotated(angle) * orbit, "radius": rng.randf_range(70.0, 105.0), "texture": PLANET_TEXTURES[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) var cursor := 0 while cursor < tiers.size(): var cluster_size := 3 if tiers[cursor] == 2 else rng.randi_range(2, 5) cluster_size = mini(cluster_size, tiers.size() - cursor) var planet: Dictionary = planets[rng.randi_range(0, planets.size() - 1)] var anchor: Vector2 = planet["position"] anchor += anchor.normalized() * rng.randf_range(250.0, 450.0) for member in cluster_size: var position := _find_enemy_position(anchor, planets, enemies, rng) # Preserve the geometry stream formerly used to choose each ship. rng.randi_range(0, 10) enemies.append({ "position": position, "tier": tiers[cursor], "texture_index": enemy_texture_index, }) cursor += 1 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(anchor: Vector2, planets: Array[Dictionary], enemies: Array[Dictionary], rng: RandomNumberGenerator) -> Vector2: for attempt in 3000: var candidate: Vector2 if attempt < 500: candidate = anchor + Vector2.RIGHT.rotated(rng.randf_range(0, TAU)) * rng.randf_range(130, 420) else: candidate = Vector2(rng.randf_range(-2380, 2380), rng.randf_range(-1580, 1580)) if _is_valid_enemy_position(candidate, planets, enemies): return candidate # Deterministic fallback for crowded clusters, still respecting the safe 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