Hinter MatchOptions.Extended (Standard im Spiel, Schalter im Startbildschirm): Söldner-Einkommen (Angriffschampion +Preis/12 Einkommen dauerhaft), Kasernen (gefallener innerer Turm: Gegnerwellen +25 % und ein Monster mehr), Erschöpfung und Rückkauf (60 % Leben, Rückkauf 60 + 20 x Stufe), Mondsegen (Grottensieg +4 % je Stapel, Mondseele bei 4). Bots wählen mit erweiterten Regeln auch Sylvara und kaufen Helden zurück. Netz überträgt Regeln und neue Zustände. HUD: Einkommen, Kasernen-Symbol, Rückkauf, Segen im Teamkopf. Kern 531/531 (s1 31), EditMode 31/31, PlayMode 16/16, Verify 718/0, Selbsttest 30/30, Golden 0 Abweichungen, Fingerabdruck unverändert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
295 lines
10 KiB
C#
295 lines
10 KiB
C#
using System;
|
|
using Luneraria.Core;
|
|
using Luneraria.Net;
|
|
using Luneraria.View;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace Luneraria.UI
|
|
{
|
|
/// <summary>
|
|
/// Owns the session and the TCP link (port of lune_main.gd): title menu, match, return,
|
|
/// rematch. Offline is a local host with bot-filled seats. Host/Join uses LuneSession over
|
|
/// LuneTcp. Every setup from the host (first join, rejoin, rematch) relaunches the match.
|
|
/// The scene builder puts it next to the game controller, so the start screen comes first.
|
|
/// Tests and the benchmark keep the scene's own match (LuneMatchDriver.DirectStart,
|
|
/// LuneBenchmark.Running); then the shell stays out and LuneGame starts as before U4.
|
|
/// </summary>
|
|
[DefaultExecutionOrder(-50)]
|
|
public sealed class LuneNetPlay : MonoBehaviour
|
|
{
|
|
/// <summary>The start screen draws above the HUD (sorting order 10).</summary>
|
|
public const int MenuSortingOrder = 20;
|
|
|
|
public LuneGame Game;
|
|
public LuneMatchDriver Driver;
|
|
public LuneMatchView View;
|
|
public LuneCameraRig Rig;
|
|
public LuneUiAssets UiAssets;
|
|
|
|
public LuneSession Session { get; private set; }
|
|
public LuneMainMenu Menu { get; private set; }
|
|
/// <summary>
|
|
/// The start screen's own document. The HUD takes over the UIDocument of the match object
|
|
/// and clears it on every launch, so the menu cannot live there.
|
|
/// </summary>
|
|
public UIDocument MenuDocument { get; private set; }
|
|
|
|
LuneTcp _tcp;
|
|
MatchOptions _lastOptions;
|
|
bool _relaunch;
|
|
PanelSettings _panel;
|
|
|
|
void Awake()
|
|
{
|
|
if (Game == null)
|
|
Game = GetComponent<LuneGame>();
|
|
if (Driver == null)
|
|
Driver = GetComponent<LuneMatchDriver>();
|
|
if (View == null)
|
|
View = GetComponent<LuneMatchView>();
|
|
if (Rig == null)
|
|
Rig = GetComponent<LuneCameraRig>();
|
|
if (UiAssets == null && Game != null)
|
|
UiAssets = Game.UiAssets;
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
if (LuneMatchDriver.DirectStart || LuneBenchmark.Running)
|
|
{
|
|
enabled = false;
|
|
return;
|
|
}
|
|
BuildMenu();
|
|
if (Game != null)
|
|
{
|
|
Game.Net = this;
|
|
Game.enabled = false;
|
|
}
|
|
if (Driver != null)
|
|
Driver.enabled = false;
|
|
ShowMenu();
|
|
LuneSmoke.StartIfRequested(this);
|
|
}
|
|
|
|
void BuildMenu()
|
|
{
|
|
LuneUi.Assets = UiAssets;
|
|
_panel = ScriptableObject.CreateInstance<PanelSettings>();
|
|
_panel.name = "LuneMenuPanel";
|
|
_panel.themeStyleSheet = UiAssets != null ? UiAssets.Theme : null;
|
|
_panel.scaleMode = PanelScaleMode.ScaleWithScreenSize;
|
|
_panel.referenceResolution = LuneHud.Reference;
|
|
_panel.screenMatchMode = PanelScreenMatchMode.Expand;
|
|
_panel.sortingOrder = MenuSortingOrder;
|
|
var holder = new GameObject("Menu");
|
|
holder.transform.SetParent(transform, false);
|
|
MenuDocument = holder.AddComponent<UIDocument>();
|
|
MenuDocument.panelSettings = _panel;
|
|
// While a match runs the menu is hidden; its root must not take the HUD's clicks.
|
|
MenuDocument.rootVisualElement.pickingMode = PickingMode.Ignore;
|
|
Menu = new LuneMainMenu();
|
|
MenuDocument.rootVisualElement.Add(Menu);
|
|
Menu.StartOffline += StartOffline;
|
|
Menu.HostRequested += HostMatch;
|
|
Menu.JoinRequested += JoinMatch;
|
|
Menu.QuitRequested += Quit;
|
|
}
|
|
|
|
/// <summary>The start screen over the empty map; the camera rests, the fields take W, A, S, D, Q and E.</summary>
|
|
void ShowMenu()
|
|
{
|
|
Menu.Enter();
|
|
if (Rig != null)
|
|
{
|
|
Rig.Interactive = false;
|
|
Rig.SetYaw(0f);
|
|
Rig.Overview(true);
|
|
}
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (_tcp != null)
|
|
_tcp.Pump();
|
|
if (_relaunch)
|
|
{
|
|
_relaunch = false;
|
|
Launch();
|
|
}
|
|
}
|
|
|
|
void StartOffline(MatchOptions options)
|
|
{
|
|
ResetSession();
|
|
Session = new LuneSession();
|
|
Session.HostOffline(options.HumanSlot, options);
|
|
_lastOptions = options;
|
|
Menu.Leave();
|
|
Launch();
|
|
}
|
|
|
|
void HostMatch(int port, string displayName)
|
|
{
|
|
ResetSession();
|
|
_tcp = new LuneTcp();
|
|
Session = new LuneSession { Link = _tcp };
|
|
_tcp.Session = Session;
|
|
Session.SessionMessage += OnMessage;
|
|
if (!_tcp.Host(port))
|
|
{
|
|
Menu.Notify(_tcp.LastError, true);
|
|
ResetSession();
|
|
return;
|
|
}
|
|
var options = new MatchOptions { Seed = Environment.TickCount & 0xffff, HumanSlot = 0, Extended = LunePreferences.ExtendedRules };
|
|
for (int slot = 0; slot < 6; slot++)
|
|
options.Bunds.Add(slot == 0 ? Menu.Bund : DataCatalog.Factions[(slot + options.Seed) % DataCatalog.Factions.Length]);
|
|
if (!Session.HostNetwork(options, displayName))
|
|
{
|
|
Menu.Notify(Session.LastError, true);
|
|
ResetSession();
|
|
return;
|
|
}
|
|
_lastOptions = options;
|
|
Menu.Notify("Warte auf Gäste auf Port " + port + " …");
|
|
Menu.Leave();
|
|
Launch();
|
|
}
|
|
|
|
void JoinMatch(string address, int port, string displayName)
|
|
{
|
|
ResetSession();
|
|
_tcp = new LuneTcp();
|
|
Session = new LuneSession { Link = _tcp };
|
|
_tcp.Session = Session;
|
|
Session.SessionMessage += OnMessage;
|
|
Session.HostLost += () =>
|
|
{
|
|
string reason = Session != null ? Session.LastError : "";
|
|
ReturnToMenu();
|
|
if (Menu != null)
|
|
Menu.Notify(reason, true);
|
|
};
|
|
Session.MatchResetup += () =>
|
|
{
|
|
if (Session != null && Session.MatchState != null)
|
|
_relaunch = true;
|
|
};
|
|
if (!Session.JoinNetwork(displayName) || !_tcp.Join(address, port))
|
|
{
|
|
Menu.Notify(_tcp.LastError.Length > 0 ? _tcp.LastError : Session.LastError, true);
|
|
ResetSession();
|
|
return;
|
|
}
|
|
Menu.Notify("Verbinde mit " + address + ":" + port + " …");
|
|
}
|
|
|
|
void OnMessage(string text, bool isError)
|
|
{
|
|
if (Menu != null && LuneUi.Visible(Menu))
|
|
Menu.Notify(text, isError);
|
|
if (Game != null && Game.Hud != null && !isError)
|
|
Game.Hud.Notify(text, isError);
|
|
}
|
|
|
|
void Launch()
|
|
{
|
|
if (Game == null || Session == null || Session.MatchState == null)
|
|
return;
|
|
bool networked = Session.IsNetworked();
|
|
Game.Net = this;
|
|
Game.Session = networked ? Session : null;
|
|
Game.HumanSlot = Session.LocalSlot >= 0 ? Session.LocalSlot : 0;
|
|
if (Driver != null)
|
|
{
|
|
Driver.enabled = true;
|
|
if (networked)
|
|
Driver.Adopt(Session.MatchState, Session.LocalSlot, !Session.IsAuthority(), Session);
|
|
else
|
|
{
|
|
// Offline keeps the plain runner; the session already validated the setup path.
|
|
Driver.HumanSlot = Game.HumanSlot;
|
|
Driver.Bunds = _lastOptions != null && _lastOptions.Bunds != null ? _lastOptions.Bunds.ToArray() : null;
|
|
Driver.Seed = _lastOptions != null ? _lastOptions.Seed : Driver.Seed;
|
|
Driver.Extended = _lastOptions != null && _lastOptions.Extended;
|
|
Driver.Restart();
|
|
}
|
|
}
|
|
if (Rig != null)
|
|
{
|
|
// Every match starts with an unturned camera, like Godot's new game node.
|
|
Rig.SetYaw(0f, true);
|
|
Rig.Interactive = true;
|
|
}
|
|
Game.enabled = true;
|
|
Game.Launch();
|
|
}
|
|
|
|
/// <summary>Host rematch: a new match with the same seats; clients relaunch on setup.</summary>
|
|
public void Rematch()
|
|
{
|
|
if (Session == null || !Session.IsAuthority())
|
|
{
|
|
if (Game != null && Game.Hud != null)
|
|
Game.Hud.Notify("Eine Revanche startet der Host.", true);
|
|
return;
|
|
}
|
|
if (_lastOptions != null)
|
|
_lastOptions.Seed += 1;
|
|
MatchOptions options = _lastOptions ?? new MatchOptions();
|
|
Session.HostRematch(options);
|
|
_relaunch = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Back to the start screen. Godot frees the game node; here the HUD closes and the driver
|
|
/// holds a fresh match that never runs, so the map behind the menu is empty again.
|
|
/// </summary>
|
|
public void ReturnToMenu()
|
|
{
|
|
if (Game != null)
|
|
Game.Close();
|
|
if (Driver != null)
|
|
{
|
|
Driver.HumanSlot = -1;
|
|
Driver.Restart();
|
|
Driver.enabled = false;
|
|
}
|
|
ResetSession();
|
|
if (Menu != null)
|
|
ShowMenu();
|
|
}
|
|
|
|
void ResetSession()
|
|
{
|
|
if (_tcp != null)
|
|
{
|
|
_tcp.Close();
|
|
_tcp = null;
|
|
}
|
|
if (Session != null)
|
|
{
|
|
Session.Close();
|
|
Session = null;
|
|
}
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
ResetSession();
|
|
if (_panel != null)
|
|
Destroy(_panel);
|
|
}
|
|
|
|
static void Quit()
|
|
{
|
|
#if UNITY_EDITOR
|
|
UnityEditor.EditorApplication.isPlaying = false;
|
|
#else
|
|
Application.Quit();
|
|
#endif
|
|
}
|
|
}
|
|
}
|