2026-10-25 12:34:56 +01:00

297 lines
12 KiB
GDScript

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 CONTACT_RANGE := preload("res://scripts/planets/orbital_contact_range.gd")
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:
if not GameState.active_distant_map.is_empty():
var distant := Node.new()
distant.set_script(preload("res://scripts/game/distant_sector.gd"))
distant.game = self
add_child(distant)
return
GameState.system_level = clampi(GameState.system_level, 1, LEVEL_BALANCE.MAX_LEVEL)
if GameState.system_seed == 0:
GameState.system_seed = GameState.create_system_seed()
GameState.remember_system()
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)
var expeditions := Node.new()
expeditions.set_script(preload("res://scripts/planets/expedition_controller.gd"))
expeditions.game = self
expeditions.player = player
add_child(expeditions)
if GameState.return_to_freighter:
GameState.return_to_freighter = false
player.position = expeditions.freighter.position
expeditions.open_freighter.call_deferred()
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"])
planet.set("earthlike", bool(data.get("earthlike", false)))
planet.set("planet_profile", data.get("profile", {}))
bodies.add_child(planet)
if data["protected"]:
protected_planet = planet
func _spawn_enemies() -> void:
if bool(GameState.cleared_systems.get(GameState.system_level, false)):
system_cleared = true
_boss_spawned = true
protected_planet.call("set_safe", true)
return
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 is_boss_alive() -> bool:
for enemy in get_tree().get_nodes_in_group("enemies"):
if is_ancestor_of(enemy) and not enemy.is_queued_for_deletion() and int(enemy.get("loot_tier")) == 2 and float(enemy.get("hull")) > 0.0:
return true
return false
func _physics_process(_delta: float) -> void:
if not GameState.active_distant_map.is_empty(): return
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
GameState.cleared_systems[GameState.system_level] = 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 CONTACT_RANGE.at_planet(player,protected_planet)
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.seed_for_system(GameState.system_level)
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))
func travel_to_distant(map_id: String) -> bool:
if _transitioning or bool(player.get("_is_destroyed")) or not GameState.active_distant_map.is_empty(): return false
if not GameState.captain.expedition.distant_maps.has(map_id): return false
if GameState.captain.expedition.deuter < preload("res://scripts/planets/distant_expeditions.gd").fuel_cost(GameState.captain.expedition.distant_maps[map_id]): return false
_transitioning = true
_load_distant.call_deferred(map_id)
return true
func _load_distant(map_id: String) -> void:
player.get_node("ShipInventory").preserve_for_restart()
if not GameState.depart_distant(map_id):
GameState.restart_inventory.clear()
_transitioning = false
return
var cost: int = preload("res://scripts/planets/distant_expeditions.gd").fuel_cost(GameState.active_distant_map)
var map_snapshot: Dictionary = GameState.active_distant_map.duplicate(true)
get_tree().paused = false
var error := get_tree().reload_current_scene()
if error != OK:
GameState.restore_distant_home()
GameState.captain.expedition.distant_maps[map_id] = map_snapshot
GameState.captain.expedition.deuter += cost
GameState.captain.expedition.emit_changed()
GameState.restart_inventory.clear()
_transitioning = false
func travel_to_system(level: int) -> bool:
if _transitioning or bool(player.get("_is_destroyed")) or level == GameState.system_level or not GameState.can_visit_system(level): return false
_transitioning = true
_load_known_system.call_deferred(level)
return true
func _load_known_system(level: int) -> void:
var previous_level := GameState.system_level
var previous_seed := GameState.system_seed
player.get_node("ShipInventory").preserve_for_restart()
GameState.remember_system()
GameState.system_level = level
GameState.system_seed = GameState.seed_for_system(level)
get_tree().paused = false
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
push_error("Nie udało się przenieść do układu: %s" % error_string(error))