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>
177 lines
6.8 KiB
C#
177 lines
6.8 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|