champions-of-luneraria/Assets/Luneraria/UI/Game/LuneMainMenu.cs
Martin Stickelbroeck 769e622952 U4 Netzwerk: LuneSession, TCP, Host/Join, 43+12 Prüfungen
Der M4-Umfang ist als Protokoll 4 portiert, serverautoritativ. Der Host
besitzt LuneMatch und rechnet jeden Kampf; Clients senden nur Absichten
und zeigen den Kampf aus Host-Momentaufnahmen.

- LuneSession (Core/Net): Rollen offline/Host/Gast, sechs Sitze, Lobby,
  Hallo mit Protokollprüfung und Namenskürzung, Beitritt und Wieder-
  einstieg über den Anzeigenamen, private Planung mit Spoof-Schutz,
  Bereit ist verbindlich, Bots planen einmal je Runde, Disconnect gibt
  den Platz an einen Bot (nie Kernverlust), Übernahme verwirft den
  unbestätigten Bot-Entwurf, Revanche hält die Plätze, Hostverlust
  meldet und führt ins Menü zurück.
- Kampfsynchronisation: Beginn mit Einheitenätzen und Gebäude-HP, alle
  0,05 s neue Einheiten und ein Frame (13 Werte je Einheit), späte
  Frames über die Serial verworfen, Gast interpoliert (frame_alpha).
- Transport: LuneLoopback (In-Prozess) für die Tests, LuneTcp + LuneWire
  (Cursor-Codec) für Echtverbindungen statt ENet; ein Socket je Gast.
- Oberfläche: LuneMainMenu (Offline/Hosten/Beitreten, Bund, Lane, Name,
  Adresse, Port) als Startbildschirm, LuneNetPlay als Schale wie
  lune_main.gd. LuneGame leitet Befehle, Rückgängig und Bereit im
  Netzspiel über die Sitzung; Tempo und Pause bestimmt der Host.
- Tests: NetSuite 43/43 (Port von test_lune_net.gd), NetRoundsSuite
  12/12 (Port von net_rounds.py: 5 Runden, Prüfsummen gleich, Gast
  rechnet 0 Schritte, Drop und Wiedereinstieg auf denselben Sitz),
  LuneWireTests Round-trip. EditMode 29/29, PlayMode 13/13, Kerntests
  457/457. Fingerabdrücke der Bot-Partien unverändert.
- Dokumentation: U4-NETWORK (Abnahme, Grenzen), PORT-QA (U4), PORT-STATUS
  (U4 fertig, U5 nächster Schritt), KNOWN-BEHAVIOR-DIFFERENCES.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:14:42 +02:00

177 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using Luneraria.Core;
using UnityEngine;
using UnityEngine.UIElements;
namespace Luneraria.UI
{
/// <summary>
/// Start screen of Champions of Luneraria (port of the network part of lune_menu.gd):
/// offline (local host with bot seats), host a direct connection, or join one. The menu is
/// the start screen of the game; the HUD's "Menü" returns here. Peer 1 is the host; free
/// seats stay bots. The host is authoritative.
/// </summary>
public sealed class LuneMainMenu : VisualElement
{
public event Action<MatchOptions> StartOffline;
public event Action<int, string> HostRequested;
public event Action<string, int, string> JoinRequested;
public event Action QuitRequested;
public string Bund = "wurzelbund";
public string Lane = "mid";
readonly TextField _name;
readonly TextField _address;
readonly TextField _port;
readonly Label _status;
LuneButton _laneTop;
LuneButton _laneMid;
LuneButton _laneBot;
public LuneMainMenu()
{
style.position = Position.Absolute;
style.left = 0f;
style.top = 0f;
style.right = 0f;
style.bottom = 0f;
style.justifyContent = Justify.Center;
style.alignItems = Align.Center;
style.backgroundColor = new Color(.02f, .03f, .05f, .92f);
var panel = new LunePanel(LunePanels.Menu);
panel.style.minWidth = 560f;
panel.style.maxWidth = 640f;
Add(panel);
VisualElement column = LuneUi.Box(true, 14f, panel);
column.style.alignItems = Align.Center;
LuneUi.Label("CHAMPIONS", "TitleLabel", column).style.fontSize = 56f;
LuneUi.Label("OF LUNERARIA", "TitleLabel", column).style.fontSize = 40f;
LuneUi.Label("DREI LANES · 3 GEGEN 3 · AUTOBATTLER", "EyebrowLabel", column);
LuneUi.Wrap(LuneUi.Label("Team Blau (Südwest) mit zwei Bot-Verbündeten gegen drei Bots in Team Rot. Kein Rundenlimit: Es gewinnt, wer den gegnerische Ancient zerstört. Für sechs Menschen hosten oder beitreten; freie Plätze bleiben Bots. Der Host ist autoritativ.", "MutedLabel", column));
LuneUi.Label("DEIN BUND", "SectionLabel", column);
VisualElement bundRow = LuneUi.Box(false, 10f, column);
new LuneButton("Glutpakt", LuneButtons.HudToggle, bundRow).Clicked += () => SetBund("glutpakt");
new LuneButton("Wurzelbund", LuneButtons.HudToggle, bundRow).Clicked += () => SetBund("wurzelbund");
LuneUi.Label("DEINE LANE", "SectionLabel", column);
VisualElement laneRow = LuneUi.Box(false, 10f, column);
_laneTop = new LuneButton("Top", LuneButtons.HudToggle, laneRow);
_laneMid = new LuneButton("Mid", LuneButtons.HudToggle, laneRow);
_laneBot = new LuneButton("Bottom", LuneButtons.HudToggle, laneRow);
_laneTop.ToggleMode = true;
_laneMid.ToggleMode = true;
_laneBot.ToggleMode = true;
_laneTop.Clicked += () => SetLane("top");
_laneMid.Clicked += () => SetLane("mid");
_laneBot.Clicked += () => SetLane("bot");
SetLane("mid");
LuneUi.Label("NETZWERK · DIREKTVERBINDUNG", "SectionLabel", column);
_name = Field(column, "Anzeigename", "Spieler");
VisualElement netRow = LuneUi.Box(false, 10f, column);
_address = Field(netRow, "Host-Adresse", "127.0.0.1");
_address.style.flexGrow = 1f;
_port = Field(netRow, "Port", LuneNet.DefaultPort.ToString());
_port.style.width = 100f;
VisualElement actions = LuneUi.Box(false, 12f, column);
new LuneButton("ALLEIN SPIELEN", LuneButtons.Primary, actions).Clicked += StartAlone;
new LuneButton("HOSTEN", LuneButtons.Secondary, actions).Clicked += StartHost;
new LuneButton("BEITRETEN", LuneButtons.Secondary, actions).Clicked += StartJoin;
new LuneButton("BEENDEN", LuneButtons.Secondary, actions).Clicked += () => QuitRequested?.Invoke();
_status = LuneUi.Label("", "MutedLabel", column);
_status.style.unityTextAlign = TextAnchor.MiddleCenter;
}
static TextField Field(VisualElement parent, string label, string value)
{
VisualElement box = LuneUi.Box(true, 4f, parent);
box.style.flexGrow = 1f;
LuneUi.Label(label, "EyebrowLabel", box);
var field = new TextField { value = value };
field.style.flexGrow = 1f;
field.style.minHeight = 32f;
field.style.fontSize = 16f;
box.Add(field);
return field;
}
void SetBund(string bund)
{
Bund = bund;
}
void SetLane(string lane)
{
Lane = lane;
_laneTop.On = lane == "top";
_laneMid.On = lane == "mid";
_laneBot.On = lane == "bot";
}
/// <summary>Own seat on team Blau: slots 0–2 map to top, mid, bot.</summary>
public MatchOptions OfflineOptions()
{
int seat = Array.IndexOf(LuneMap.Lanes, Lane);
if (seat < 0)
seat = 1;
var options = new MatchOptions { Seed = Environment.TickCount & 0xffff, HumanSlot = seat };
for (int slot = 0; slot < 6; slot++)
options.Bunds.Add(slot == seat ? Bund : DataCatalog.Factions[(slot + options.Seed) % DataCatalog.Factions.Length]);
return options;
}
void StartAlone()
{
StartOffline?.Invoke(OfflineOptions());
}
void StartHost()
{
int port = ParsePort();
HostRequested?.Invoke(port, CleanName());
}
void StartJoin()
{
int port = ParsePort();
JoinRequested?.Invoke(_address.value.Trim(), port, CleanName());
}
int ParsePort()
{
int port;
return int.TryParse(_port.value.Trim(), out port) && port > 0 ? port : LuneNet.DefaultPort;
}
string CleanName()
{
string name = (_name.value ?? "").Trim();
if (name.Length == 0)
name = "Spieler";
return name.Length > 24 ? name.Substring(0, 24) : name;
}
public void Notify(string message, bool error = false)
{
_status.text = message ?? "";
_status.style.color = error ? LuneUiTheme.Hex("ff8a70") : LuneUiTheme.Muted;
}
public void Enter()
{
LuneUi.Show(this, true);
Notify("");
}
public void Leave()
{
LuneUi.Show(this, false);
}
}
}