champions-of-luneraria/Assets/Luneraria/UI/Game/LuneNetPlay.cs
Martin Stickelbroeck d590fdebeb U4-Nachtrag: Startbildschirm in der Szene, Rückkehr aus der Partie
Befund aus dem Spieltest des U3-Nachtrags: Nichts legte LuneNetPlay in die
Szene. Das Spiel begann direkt in einer Partie, der Startbildschirm aus U4
war unerreichbar, und der Verlassen-Dialog beendete das Spiel statt zum
Hauptmenü zu führen.

- LuneWorldBuilder legt LuneNetPlay auf das Match-Objekt; Szene neu gebaut.
- Das Menü bekommt ein eigenes UIDocument über dem HUD. Vorher hing es an
  keinem Panel, und das HUD hätte es beim ersten Partiestart geleert.
- Rückkehr über „Menü“ → „Zum Menü“: HUD und Aufstellungsflächen weg,
  Sitzung beendet, leere Karte. Die Kamera ruht hinter dem Menü und beginnt
  jede Partie ungedreht. Die Bund-Wahl ist markiert, die Knopfreihe passt
  in die Tafel.
- Tests und Benchmark ohne Startbildschirm (LuneMatchDriver.DirectStart,
  -luneBenchmark); Bot-Partien und Fingerabdrücke unverändert.
- Neu: LuneSmoke (-luneSmoke <Ordner>), Selbsttest eines Players nach
  lune_smoke.gd. StartScreenTests führt ihn im Editor aus.
- Tests: EditMode 29/29, PlayMode 14/14 (StartScreenTests 26/26), Verify
  718/0, Fingerabdrücke DCE95C96556F3A1A und 7B8E43AEFC0D60C5 unverändert.
  macOS-Player: Selbsttest 26/26, Bilder in
  docs/port/screenshots/u4/startbildschirm/. Ein zusammenhängender
  PlayMode-Lauf auf dem Endstand und ein Player-Benchmark stehen aus, weil
  der Mac zugeklappt schlief (siehe U4-NETWORK.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 05:37:51 +02:00

294 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 };
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.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
}
}
}