Mondherz-Ancients (Saumkern-Kristall als Herz, Sichelklingen, Ringe, Krone, Teamwappen, Energieadern) mit den Stufen stabil/instabil/kritisch, kurzem Zusammenbruch und bleibender Ruine; der Sieg wartet nie auf die Grafik. Turmfamilie aus einem Blender-Generator: Grenzwacht (Tower 1), Bastion (Tower 2), Kernwacht (Basisturm in der Formensprache des Ancients), mit Schadensstufen, Einsturz, Ruine und sichtbarem Angriff (Zielring, Aufladen, Bolzen, Einschlag) - der Schaden bleibt vollständig in der Simulation. Bossgrotte Mondschlund mit Tor, Treppe in die Tiefe, Altar, Säulen, Felsen und fünf Zuständen; der Generator hält Abstand zur Top-Lane, die Höhle braucht eine kleine Geländesenke (nur Darstellung). Superboss Schlundkrone mit 24 Knochen und 11 Clips (inkl. Brüllen und Sieg), Adern in Teamfarbe, dasselbe Modell für Grotte und Lane. Gemeinsame LN-Materialfamilie (Stein, Metall, Kristall, Rune, Stoff, Mondwasser, Kreaturenhaut), Occlusion-Fade ohne dithernde Schatten. Keine Regeländerung. Geprüft: verify_g2 245/245, verify_g1 443/443, Regeln 91/91 und 73/73, Stress 17/17, Balance 40 Partien identisch mit G1, verify_m1 38/38, verify_m2 14/14, Netz 43/43 und Loopback 12/12, Legacy 209 und 463. 21 Ansichten vorher/nachher (1920x1080), Videos Turmkampf und Bossbegegnung, Leistung G1/G2 auf demselben Mac (+0,5 ms in vollen Kampfansichten). Pakete 0.17.0-g2 für macOS (Universal, ad-hoc signiert), Windows x64/ARM64 und Linux; macOS nativ und unter Rosetta, beide Windows-Pakete in Windows 11 ARM (Parallels, OpenGL-Fallback) mit --lune-smoke bestanden. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
161 lines
6.2 KiB
GDScript
161 lines
6.2 KiB
GDScript
extends Node
|
|
## Entry scene of Champions of Luneraria: title menu, match, return, rematch.
|
|
## Offline is a local host with bot-filled seats. Host/Join uses LuneSession (ENet).
|
|
## Legacy QA switches open the archived Saumwacht scene unchanged.
|
|
|
|
const Game = preload("res://scripts/luneraria/lune_game.gd")
|
|
const Menu = preload("res://scripts/luneraria/ui/lune_menu.gd")
|
|
const Session = preload("res://scripts/luneraria/net/lune_session.gd")
|
|
const LEGACY_SWITCHES: Array[String] = ["--smoke-test", "--faction-test", "--expedition-test", "--ringborke", "--legacy"]
|
|
|
|
var menu: MainMenu
|
|
var game: Node3D
|
|
var settings: Control
|
|
var session: Node
|
|
var _layer: CanvasLayer
|
|
var _last_options: Dictionary = {}
|
|
|
|
func _ready() -> void:
|
|
for arg: String in OS.get_cmdline_user_args():
|
|
if arg in LEGACY_SWITCHES:
|
|
get_tree().change_scene_to_file.call_deferred("res://scenes/main.tscn")
|
|
return
|
|
DisplayServer.window_set_title("Champions of Luneraria")
|
|
get_tree().auto_accept_quit = false
|
|
get_tree().root.close_requested.connect(Soundscape.quit_game)
|
|
_layer = CanvasLayer.new()
|
|
_layer.layer = 10
|
|
add_child(_layer)
|
|
menu = Menu.new()
|
|
_layer.add_child(menu)
|
|
menu.start_requested.connect(func() -> void: start_match({"seed": int(Time.get_unix_time_from_system()) % 100000}))
|
|
menu.quit_requested.connect(func() -> void: Soundscape.quit_game())
|
|
menu.settings_requested.connect(toggle_settings)
|
|
if menu.has_signal("host_requested"):
|
|
menu.host_requested.connect(_host_match)
|
|
menu.join_requested.connect(_join_match)
|
|
settings = LuneSettings.new()
|
|
_layer.add_child(settings)
|
|
settings.visible = false
|
|
settings.closed.connect(toggle_settings)
|
|
menu.enter()
|
|
# Platform smoke test of exported builds (macOS app, Windows): plays a short match alone.
|
|
for arg: String in OS.get_cmdline_user_args():
|
|
if arg.begins_with("--lune-smoke"):
|
|
var smoke: Node = load("res://scripts/luneraria/lune_smoke.gd").new()
|
|
smoke.setup(self, arg.get_slice("=", 1) if arg.contains("=") else "")
|
|
add_child(smoke)
|
|
|
|
func start_match(options: Dictionary) -> void:
|
|
var own_team_slots: Array[int] = [0, 1, 2]
|
|
var human: int = own_team_slots[LuneMap.LANES.find(menu.lane)] if menu.lane in LuneMap.LANES else 1
|
|
var bunds: Array = []
|
|
var rng := RandomNumberGenerator.new()
|
|
rng.seed = int(options.get("seed", 1))
|
|
for slot: int in range(6):
|
|
bunds.append(menu.bund if slot == human else DataCatalog.FACTIONS[rng.randi() % DataCatalog.FACTIONS.size()])
|
|
_last_options = {"seed": int(options.get("seed", 1)), "human_slot": human, "bunds": bunds}
|
|
_launch(_last_options)
|
|
|
|
func _host_match(port: int, display_name: String) -> void:
|
|
_reset_session()
|
|
session = Session.new()
|
|
session.name = "LuneSession"
|
|
add_child(session)
|
|
var options := {"seed": int(Time.get_unix_time_from_system()) % 100000, "human_slot": 0, "bunds": []}
|
|
var rng := RandomNumberGenerator.new()
|
|
rng.seed = options.seed
|
|
for slot: int in range(6):
|
|
options.bunds.append(menu.bund if slot == 0 else DataCatalog.FACTIONS[rng.randi() % DataCatalog.FACTIONS.size()])
|
|
var err: int = session.host_enet(port, options, display_name)
|
|
if err == OK:
|
|
_last_options = options
|
|
_launch(_last_options)
|
|
else:
|
|
push_warning(session.last_error)
|
|
menu.notify_error(session.last_error)
|
|
_reset_session()
|
|
|
|
func _join_match(address: String, port: int, display_name: String) -> void:
|
|
_reset_session()
|
|
session = Session.new()
|
|
session.name = "LuneSession"
|
|
add_child(session)
|
|
session.session_message.connect(func(text: String, is_error: bool) -> void:
|
|
print(("FEHLER: " if is_error else "") + text))
|
|
session.host_lost.connect(func() -> void:
|
|
var reason: String = session.last_error if session != null else ""
|
|
return_to_menu()
|
|
menu.notify_error(reason))
|
|
if session.join_enet(address, port, display_name) != OK:
|
|
menu.notify_error(session.last_error)
|
|
_reset_session()
|
|
return
|
|
# Every setup from the host (first join, rejoin, rematch) relaunches the match view.
|
|
session.match_resetup.connect(func() -> void:
|
|
if session != null and session.match_state != null:
|
|
_last_options = {"seed": session.match_state.seed_value, "human_slot": session.local_slot, "bunds": []}
|
|
_launch(_last_options))
|
|
|
|
func _reset_session() -> void:
|
|
if session != null and is_instance_valid(session):
|
|
session.close()
|
|
session.queue_free()
|
|
session = null
|
|
|
|
func _launch(options: Dictionary) -> void:
|
|
# The rematch button lives inside the old match; detach it now, free it after the signal.
|
|
if is_instance_valid(game):
|
|
remove_child(game)
|
|
game.queue_free()
|
|
game = Game.new()
|
|
add_child(game)
|
|
game.input_blocked = func() -> bool: return menu.visible or settings.visible
|
|
if session != null and is_instance_valid(session):
|
|
game.session = session
|
|
game.start(options)
|
|
game.exit_requested.connect(return_to_menu)
|
|
game.rematch_requested.connect(_rematch)
|
|
game.hud.settings_requested.connect(toggle_settings)
|
|
menu.leave()
|
|
|
|
func _rematch() -> void:
|
|
_last_options.seed = int(_last_options.seed) + 1
|
|
if session != null and is_instance_valid(session) and session.is_networked():
|
|
if not session.is_authority():
|
|
game.hud.notify("Eine Revanche startet der Host.", true)
|
|
return
|
|
session.host_rematch(_last_options.duplicate(true))
|
|
_launch(_last_options)
|
|
|
|
func return_to_menu() -> void:
|
|
if is_instance_valid(game):
|
|
remove_child(game)
|
|
game.queue_free()
|
|
game = null
|
|
_reset_session()
|
|
menu._locked = false
|
|
menu.pending_screen = MainMenu.Screen.TITLE
|
|
menu.enter()
|
|
|
|
func toggle_settings() -> void:
|
|
settings.visible = not settings.visible
|
|
# Only an offline match pauses behind the settings; a network match keeps running.
|
|
var networked: bool = session != null and is_instance_valid(session) and session.is_networked()
|
|
if is_instance_valid(game) and not networked:
|
|
game.paused = settings.visible or game.paused
|
|
if settings.visible:
|
|
settings.sync()
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if event is InputEventKey and event.pressed and not event.echo:
|
|
if event.keycode == KEY_F1 or (event.keycode == KEY_ESCAPE and settings.visible):
|
|
toggle_settings()
|
|
get_viewport().set_input_as_handled()
|
|
elif event.keycode in [KEY_M, KEY_F11]:
|
|
if event.keycode == KEY_M:
|
|
Preferences.muted = not Preferences.muted
|
|
else:
|
|
Preferences.fullscreen = not Preferences.fullscreen
|
|
Preferences.commit()
|
|
get_viewport().set_input_as_handled()
|