109 lines
4.6 KiB
GDScript
109 lines
4.6 KiB
GDScript
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 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)
|
|
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
|