champions-of-luneraria/Assets/Luneraria/UI/Game/LuneGame.cs
Martin Stickelbroeck fb5c92dd37 S3 Neutrallager und Fog of War mit Spähern (LoL, Dota 2)
Erweiterte Regeln: in jeder dritten Runde ein neutraler Wächter in jeder
Lanemitte; wer beim Fall mehr Einheiten (dann mehr Leben) in 8 m hat, nimmt
das Lager: Glutsegen (+10 % Schaden), Mondschild (Schild 10 %), Beute
(50 Gold je Spieler). Fog of War (LuneFog, nur Darstellung): Gegner sichtbar
in 9 m um eigene Einheiten, 10 m um Gebäude, 12 m um Späher; Späher (40 Gold,
3 Runden, Teamwirkung) zeigen in der Planung die gegnerische Armee ihrer Lane.
HUD: Teampanel SPÄHER und LAGER, Lager-Sechseck, Nebel in Feld, Minimap,
Namensschildern und Lane-Zählung.
Kern 597/597 (s3 31), EditMode 33/33, PlayMode 18/18, Verify 718/0,
Selbsttest 30/30, Golden 0, Fingerabdruck unverändert.

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

708 lines
28 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 System.Collections.Generic;
using System.Linq;
using Luneraria.Core;
using Luneraria.View;
using UnityEngine;
using UnityEngine.UIElements;
namespace Luneraria.UI
{
/// <summary>
/// One Champions of Luneraria match on screen with the HUD (port of the HUD side of
/// scripts/luneraria/lune_game.gd): the human player's commands go through the same command
/// interface as the bots (LuneMatch.Submit); Ready, tempo and pause go to the host loop; HUD
/// clicks and hotkeys move the camera; a left click in the field selects an own unit. During
/// planning the own draft army stands in its formation and the own deployment areas light up.
/// Offline, planning waits for the human; pause and tempo are local. Nothing here decides a rule.
/// Runs after the match view (execution order 200 after 100), so name tags follow the figures
/// of the same frame.
/// </summary>
[DefaultExecutionOrder(200)]
public sealed class LuneGame : MonoBehaviour
{
public LuneMatchDriver Driver;
public LuneMatchView View;
public LuneCameraRig Rig;
public LuneUiAssets UiAssets;
/// <summary>The human seat (Godot default 1: Blau Mid). -1 makes the match a bot match watched from seat 0.</summary>
public int HumanSlot = 1;
/// <summary>Optional render target for tests and captures (null: the screen).</summary>
public RenderTexture Target;
/// <summary>Network session (U4); null offline. Commands and Ready go through it.</summary>
public LuneSession Session;
/// <summary>Menu/shell that owns the session (U4; in the scene since the U4 addendum); null in tests and the benchmark.</summary>
public LuneNetPlay Net;
public LuneHud Hud { get; private set; }
public LuneMatch Match { get { return _runner != null ? _runner.Match : Driver.Runner.Match; } }
/// <summary>The human seat of the running match (-1: a bot match, e.g. tests and the benchmark).</summary>
int Seat { get { return _runner != null ? _runner.HumanSlot : -1; } }
bool Human { get { return Seat >= 0; } }
/// <summary>Whose view the HUD shows: the human seat, or in a bot match seat max(0, HumanSlot) as in Godot.</summary>
int Viewer { get { return Human ? Seat : Math.Max(0, HumanSlot); } }
Camera _camera;
LuneMatchRunner _runner;
readonly System.Diagnostics.Stopwatch _clock = new System.Diagnostics.Stopwatch();
double _updateMs;
double _lateMs;
LuneMatchRunner.Stage _lastStage = (LuneMatchRunner.Stage)(-1);
int _laneCycle;
bool _pausedBySettings;
VisualElement _leave;
readonly List<(Renderer renderer, int team, string lane)> _pads = new List<(Renderer, int, string)>();
readonly List<(GameObject ring, int team)> _grottoRings = new List<(GameObject, int)>();
string _deployment = "";
MaterialPropertyBlock _block;
void Start()
{
if (Net != null)
return;
Launch();
}
/// <summary>Builds the HUD for the current match (also called by the network shell).</summary>
public void Launch()
{
_camera = View != null && View.ViewCamera != null ? View.ViewCamera : Camera.main;
// The benchmark keeps its bot match; otherwise the configured seat plays.
if (!LuneBenchmark.Running && Net == null && (Driver.HumanSlot != HumanSlot || Driver.Runner == null || Driver.Runner.HumanSlot != Driver.HumanSlot))
{
Driver.HumanSlot = HumanSlot;
Driver.Restart();
}
FindPads();
Hud = gameObject.GetComponent<LuneHud>() ?? gameObject.AddComponent<LuneHud>();
Hud.enabled = true;
Bind();
// After a return to the start screen (Close) the HUD shows again.
LuneUi.Show(Hud.Root, true);
LuneSettingsPanel.Apply();
if (Rig != null)
{
Rig.PointerBlocked = () => Hud != null && Hud.IsPointerOverHud(Input.mousePosition);
Rig.KeysBlocked = () => Hud != null && Hud.Settings.Open;
Rig.ResetRequested = () => ResetCamera();
}
StartCamera(true);
// The benchmark counts the HUD scripts: its Tick plus this controller's frame work.
LuneBenchmark.UiMs = () => (Hud != null ? Hud.LastTickMs : 0.0) + _updateMs + _lateMs;
}
/// <summary>
/// Leaves the match for the start screen (LuneNetPlay.ReturnToMenu; Godot frees the game
/// node): the HUD and the deployment areas disappear and the controller stops until the
/// next Launch.
/// </summary>
public void Close()
{
if (Hud != null && Hud.Root != null)
{
LuneUi.Show(Hud.Root, false);
Hud.enabled = false;
}
foreach (var pad in _pads)
pad.renderer.gameObject.SetActive(false);
foreach (var entry in _grottoRings)
entry.ring.SetActive(false);
_deployment = "";
View.Preview = null;
enabled = false;
}
bool Networked { get { return Session != null && Session.IsNetworked(); } }
bool IsClient { get { return Networked && !Session.IsAuthority(); } }
void OnDestroy()
{
LuneBenchmark.UiMs = null;
}
/// <summary>(Re)builds the HUD for the driver's current match and wires its events.</summary>
void Bind()
{
_runner = Driver.Runner;
_lastStage = (LuneMatchRunner.Stage)(-1);
Hud.Init(_runner.Match, Viewer, View.Library, UiAssets, Target);
Hud.CommandRequested += command => Feedback(Send(command));
Hud.UndoRequested += () =>
{
if (!Human)
{
Feedback(new CommandResult(false, "Kein eigener Platz."));
return;
}
if (Networked)
{
SessionResult result = Session.Submit(new Command { Type = "undo" });
Feedback(new CommandResult(result.Ok, result.Message));
}
else
{
Feedback(Match.Undo(Seat));
}
};
Hud.ReadyRequested += OnReady;
Hud.StarterRequested += Starter;
Hud.SpeedRequested += CycleSpeed;
Hud.PauseRequested += TogglePause;
Hud.LaneFocusRequested += lane => FocusLane(lane);
Hud.MapFocusRequested += point => Rig?.LookAt(point);
Hud.CameraRequested += FocusTarget;
Hud.MenuRequested += AskLeave;
Hud.RematchRequested += Rematch;
Hud.SettingsRequested += () => Hud.Settings.Show();
Hud.Settings.Opened += () =>
{
// A running combat pauses offline while the options are open (lune_settings.gd).
_pausedBySettings = _runner.Current == LuneMatchRunner.Stage.Combat && !Driver.Paused;
if (_pausedBySettings)
Driver.Paused = true;
};
Hud.Settings.Closed += () =>
{
if (_pausedBySettings)
Driver.Paused = false;
_pausedBySettings = false;
Hud.SetSpeedLabel(Driver.Tempo, Driver.Paused);
};
Hud.SetSpeedLabel(Driver.Tempo, Driver.Paused);
BuildLeaveDialog();
}
CommandResult Send(Command command)
{
if (!Human)
return new CommandResult(false, "Zuschauer können nicht planen.");
if (Networked)
{
SessionResult result = Session.Submit(command);
return new CommandResult(result.Ok, result.Message);
}
return Match.Submit(Seat, command);
}
void Feedback(CommandResult result)
{
Hud.Notify(result.Message, !result.Ok);
RebuildPreview();
Hud.MarkDirty();
}
void Starter()
{
List<string> ids = Match.BaseIds(Match.Players[Viewer].Bund);
foreach (int index in new[] { 1, 0, 2 })
{
CommandResult result = Send(Command.Buy(ids[index]));
if (!result.Ok)
{
Feedback(result);
return;
}
}
Feedback(new CommandResult(true, "Startaufstellung: Front, Nahkampf und Fernkampf gekauft."));
}
void OnReady()
{
if (Match.Phase != Phase.Planning || !Human)
return;
if (Networked)
Session.ReadyUp();
else
_runner.Ready();
Hud.MarkDirty();
}
void CycleSpeed()
{
if (IsClient)
{
Hud.Notify("Tempo und Pause bestimmt der Host.", true);
return;
}
Driver.CycleSpeed();
Hud.SetSpeedLabel(Driver.Tempo, Driver.Paused);
}
void TogglePause()
{
if (IsClient)
{
Hud.Notify("Tempo und Pause bestimmt der Host.", true);
return;
}
if (Driver.TogglePause())
Hud.SetSpeedLabel(Driver.Tempo, Driver.Paused);
}
void Rematch()
{
if (Net != null)
{
Net.Rematch();
return;
}
Driver.Seed += 1;
Driver.Restart();
View.Preview = null;
Bind();
StartCamera(true);
}
// ---------------------------------------------------------------- camera targets
int OwnTeam { get { return Match.Players[Viewer].Team; } }
public void FocusLane(string lane, bool instant = false)
{
if (Rig == null)
return;
LanePath path = Match.Map.LanePaths[lane];
// Planning looks at the own deployment and inner tower, combat at the contested middle.
double fraction = Match.Phase == Phase.Planning ? .2 : .42;
Rig.LookAt(Match.Map.LanePoint(OwnTeam, lane, path.Length * fraction), 24f, instant);
}
public void FocusTarget(string target)
{
if (Rig == null)
return;
switch (target)
{
case "grotto":
Rig.LookAt(Match.Map.GrottoCenter(), 22f);
break;
case "base":
Rig.LookAt(Match.Map.CorePosition(OwnTeam), 26f);
break;
case "enemy_base":
Rig.LookAt(Match.Map.CorePosition(1 - OwnTeam), 26f);
break;
case "hero":
FocusHero();
break;
default:
if (Array.IndexOf(LuneMap.Lanes, target) >= 0)
FocusLane(target);
break;
}
}
/// <summary>The own hero: its planning preview or, in combat, the live unit.</summary>
LuneUnit OwnHeroUnit()
{
if (!Human)
return null;
LuneCombat source = Match.Phase == Phase.Combat ? Match.Combat : View.Preview;
return source?.Units.FirstOrDefault(u => u.Kind == "hero" && u.OwnerSlot == Seat && u.Alive);
}
public bool FocusHero()
{
LuneUnit unit = OwnHeroUnit();
if (unit == null)
{
Hud.Notify("Kein Held im Feld – im Reiter „Held“ anwerben.", true);
return false;
}
Rig?.LookAt(unit.Position, 16f);
return true;
}
/// <summary>The first view of a match (Godot _ready): the own lane at planning distance, a bot match from above.</summary>
public void StartCamera(bool instant = true)
{
if (Rig == null)
return;
if (Human)
FocusLane(Match.Players[Seat].Lane, instant);
else
Rig.Overview(instant);
}
public void ResetCamera(bool instant = false)
{
if (Rig == null)
return;
string lane = Match.Players[Viewer].Lane;
Rig.ResetView(Match.Map.LanePoint(OwnTeam, lane, Match.Map.LanePaths[lane].Length * .2), instant);
}
// ---------------------------------------------------------------- planning preview and deployment areas
/// <summary>
/// The formation areas (Deployment_team_lane) and grotto entrance rings (GrottoEntry_team) of
/// the scene. They are built inactive, like Godot's hidden pads, so the search includes
/// inactive objects.
/// </summary>
void FindPads()
{
_pads.Clear();
_grottoRings.Clear();
foreach (Renderer renderer in FindObjectsByType<Renderer>(FindObjectsInactive.Include, FindObjectsSortMode.None))
{
string[] parts = renderer.gameObject.name.Split('_');
if (parts.Length >= 3 && parts[0] == "Deployment" && int.TryParse(parts[1], out int team))
_pads.Add((renderer, team, parts[2]));
else if (parts.Length == 2 && parts[0] == "GrottoEntry" && int.TryParse(parts[1], out int side))
_grottoRings.Add((renderer.gameObject, side));
}
_deployment = "";
}
/// <summary>
/// Planning shows the own formation areas, lanes that hold own units light up, and the own
/// grotto entrance while the grotto is open (Godot show_deployment). Checked every frame,
/// applied only when something changed.
/// </summary>
void UpdateDeployment()
{
bool planning = Match.Phase == Phase.Planning && Human;
var used = new Dictionary<string, int>();
if (planning)
{
Draft draft = Match.Players[Seat].Draft;
foreach (RosterEntry entry in draft.Roster)
used[entry.Lane] = used.GetValueOrDefault(entry.Lane, 0) + 1;
if (draft.Hero != null)
used[draft.Hero.Lane] = used.GetValueOrDefault(draft.Hero.Lane, 0) + 1;
}
bool grotto = planning && Match.GrottoAvailable();
string signature = planning + "|" + OwnTeam + "|" + grotto + "|" + string.Join(",", LuneMap.Lanes.Select(lane => used.GetValueOrDefault(lane, 0) > 0 ? lane : ""));
if (signature == _deployment)
return;
_deployment = signature;
_block ??= new MaterialPropertyBlock();
foreach (var pad in _pads)
{
bool visible = planning && pad.team == OwnTeam;
if (pad.renderer.gameObject.activeSelf != visible)
pad.renderer.gameObject.SetActive(visible);
if (!visible)
continue;
pad.renderer.GetPropertyBlock(_block);
_block.SetFloat("_Highlight", used.GetValueOrDefault(pad.lane, 0) > 0 ? 1f : 0f);
pad.renderer.SetPropertyBlock(_block);
}
foreach (var entry in _grottoRings)
{
bool visible = grotto && entry.team == OwnTeam;
if (entry.ring.activeSelf != visible)
entry.ring.SetActive(visible);
}
}
/// <summary>
/// The human player's current draft army in its formation (Godot _rebuild_preview); after
/// every own command, and by fixtures that change the draft directly (g3_views).
/// </summary>
public void RebuildPreview()
{
UpdateDeployment();
if (Match.Phase != Phase.Planning || !Human)
return;
var preview = new LuneCombat(Match.Map, Match.Balance, Match.RoundNumber);
Player player = Match.Players[Seat];
var perLane = new Dictionary<string, int>();
var entries = new List<(UnitDef def, string lane, string uid, string kind)>();
foreach (RosterEntry entry in player.Draft.Roster)
entries.Add((DataCatalog.GetUnit(entry.TypeId), entry.Lane, entry.Uid, "troop"));
HeroState hero = player.Draft.Hero;
if (hero != null)
entries.Add((LuneContent.HeroDefinition(hero), hero.Lane, hero.Uid, "hero"));
foreach (var entry in entries)
{
int index = perLane.GetValueOrDefault(entry.lane, 0);
perLane[entry.lane] = index + 1;
LuneUnit unit;
if (entry.lane == LuneMatch.Grotto)
{
// Grotto orders preview at the team's grotto entrance.
Vec2 at = Match.Map.GrottoEntry(player.Team) + new Vec2(index % 3 - 1.0, index / 3) * 1.1;
unit = preview.SpawnAt(entry.kind, player.Team, "grotto", entry.def, at, Seat, entry.uid);
}
else
{
unit = preview.Spawn(entry.kind, player.Team, entry.lane, entry.def, Math.Max(.4, 7.2 - (index / 4) * 1.5), (index % 4 - 1.5) * 1.2, Seat, entry.uid);
}
unit.PreviousPosition = unit.Position;
}
// S3 (extended rules): scouted lanes show the enemy's committed army in its formation.
var enemyRows = new Dictionary<string, int>();
foreach (LuneMatch.ArmyEntry entry in LuneFog.ScoutedEnemies(Match, player.Team, player.Draft))
{
int index = enemyRows.GetValueOrDefault(entry.Lane, 0);
enemyRows[entry.Lane] = index + 1;
LuneUnit unit = preview.Spawn(entry.Kind, 1 - player.Team, entry.Lane, entry.Definition, Math.Max(.4, 7.2 - (index / 4) * 1.5), (index % 4 - 1.5) * 1.2, entry.Slot, entry.Uid);
unit.PreviousPosition = unit.Position;
}
View.Preview = preview;
}
// ---------------------------------------------------------------- frame
void Update()
{
_clock.Restart();
FrameUpdate();
_updateMs = _clock.Elapsed.TotalMilliseconds;
}
void FrameUpdate()
{
if (Driver.Runner != _runner)
{
// Somebody restarted the driver (tests, rematch from elsewhere): follow it.
View.Preview = null;
Bind();
}
TrackStage();
// S3 fog of war: only a human seat in an extended match sees through its team's eyes.
View.FogSlot = Human && Match.Extended ? Seat : -1;
if (Hud != null && Hud.Model != null)
Hud.Model.Hidden = View.Hidden;
UpdateDeployment();
HandleKeys();
HandleClicks();
}
/// <summary>After the match view moved the figures: name tags and the minimap's camera frame.</summary>
void LateUpdate()
{
if (Hud == null || _camera == null)
return;
_clock.Restart();
Hud.FieldTags.Sync(View.Units, _camera, Hud);
if (Rig != null)
Hud.Minimap.SetCamera(Rig.Focus, Rig.Zoom, Rig.Footprint(_camera));
_lateMs = _clock.Elapsed.TotalMilliseconds;
}
void TrackStage()
{
LuneMatchRunner.Stage stage = _runner.Current;
if (stage == _lastStage)
return;
LuneMatchRunner.Stage previous = _lastStage;
_lastStage = stage;
if (previous == LuneMatchRunner.Stage.Combat && Networked && Session.IsAuthority())
Session.HostAfterCombat();
if (stage == LuneMatchRunner.Stage.Planning)
{
if (Networked && Session.IsAuthority())
Session.PlanBotSeats();
RebuildPreview();
}
else if (stage == LuneMatchRunner.Stage.Combat)
{
View.Preview = null;
UpdateDeployment();
}
else if (stage == LuneMatchRunner.Stage.Over && Rig != null && Match.Combat != null)
{
Driver.Paused = false;
int fallen = Match.Combat.DestroyedCores.Contains(0) ? 0 : 1;
Rig.LookAt(Match.Map.CorePosition(fallen), 26f);
}
Hud.SetSpeedLabel(Driver.Tempo, Driver.Paused);
}
static readonly KeyCode[] GameKeys =
{
KeyCode.Escape, KeyCode.F1, KeyCode.M, KeyCode.F11, KeyCode.Space, KeyCode.P, KeyCode.Tab, KeyCode.F2, KeyCode.F3, KeyCode.F4,
KeyCode.B, KeyCode.G, KeyCode.H, KeyCode.Alpha1, KeyCode.Alpha2, KeyCode.Alpha3, KeyCode.Alpha4, KeyCode.Alpha5,
KeyCode.Keypad1, KeyCode.Keypad2, KeyCode.Keypad3, KeyCode.Keypad4, KeyCode.Keypad5,
};
/// <summary>Game keys (Godot _input); they act even while a HUD button has focus.</summary>
void HandleKeys()
{
foreach (KeyCode code in GameKeys)
{
if (Input.GetKeyDown(code))
Key(code);
}
}
bool Blocked { get { return Hud.Settings.Open || (_leave != null && LuneUi.Visible(_leave)); } }
/// <summary>One game key, as pressed by the player (also the entry point of the playtest).</summary>
public void Key(KeyCode code)
{
switch (code)
{
case KeyCode.Escape:
if (_leave != null && LuneUi.Visible(_leave))
LuneUi.Show(_leave, false);
else
Hud.HandleEscape();
return;
case KeyCode.F1:
if (Hud.Settings.Open)
Hud.Settings.Close();
else
Hud.Settings.Show();
return;
case KeyCode.M:
LunePreferences.Muted = !LunePreferences.Muted;
LunePreferences.Commit();
LuneSettingsPanel.Apply();
return;
case KeyCode.F11:
LunePreferences.Fullscreen = !LunePreferences.Fullscreen;
LunePreferences.Commit();
LuneSettingsPanel.Apply();
return;
}
if (Blocked)
return;
switch (code)
{
case KeyCode.Space:
if (Match.Phase == Phase.Planning)
{
// A Space meant as pause at the very end of a combat must not skip the planning.
if (!Hud.ReadyBlocked())
OnReady();
}
else
{
TogglePause();
}
return;
case KeyCode.P:
TogglePause();
return;
case KeyCode.Tab:
_laneCycle = (_laneCycle + 1) % 3;
FocusLane(LuneMap.Lanes[_laneCycle]);
return;
case KeyCode.F2:
case KeyCode.F3:
case KeyCode.F4:
// F1 stays the options key; F2–F4 jump to Top, Mid and Bottom.
_laneCycle = code - KeyCode.F2;
FocusLane(LuneMap.Lanes[_laneCycle]);
return;
case KeyCode.B:
FocusTarget("base");
return;
case KeyCode.G:
FocusTarget("grotto");
return;
case KeyCode.H:
FocusHero();
return;
}
int index = code >= KeyCode.Alpha1 && code <= KeyCode.Alpha5 ? code - KeyCode.Alpha1 : code >= KeyCode.Keypad1 && code <= KeyCode.Keypad5 ? code - KeyCode.Keypad1 : -1;
if (index < 0 || Match.Phase != Phase.Planning || !Human)
return;
if (Match.Players[Seat].Ready)
{
// After Ready the plan is binding (the host refuses); the keys do nothing.
Hud.Notify("Bereit gemeldet – die Planung ist verbindlich.", true);
return;
}
List<string> ids = Match.BaseIds(Match.Players[Seat].Bund);
Feedback(Send(Command.Buy(ids[index])));
}
/// <summary>Left click in the field selects an own unit (details in the HUD); it never orders anything.</summary>
void HandleClicks()
{
if (Input.GetMouseButtonDown(0))
ClickField(Input.mousePosition);
}
/// <summary>A left click at a screen point (pixels, origin bottom left); HUD areas take it themselves.</summary>
public void ClickField(Vector2 screen)
{
if (_camera == null || Blocked || Hud.IsPointerOverHud(screen))
return;
LuneUnit picked = PickOwnUnit(screen);
if (picked != null && picked.Kind == "hero")
Hud.OpenHero();
else if (picked != null)
Hud.SelectUnit(picked.PersistentUid);
else if (Hud.SelectedUid.Length > 0)
Hud.ClearSelection();
}
/// <summary>The own troop or hero whose figure is nearest to a screen point (within 34 px), or null.</summary>
public LuneUnit PickOwnUnit(Vector2 screen)
{
if (!Human)
return null;
LuneUnit best = null;
float bestDistance = 34f;
foreach (LuneUnitView view in View.Units)
{
LuneUnit unit = view.Unit;
if (unit == null || !unit.Alive || unit.OwnerSlot != Seat || (unit.Kind != "troop" && unit.Kind != "hero") || !view.gameObject.activeInHierarchy)
continue;
Vector3 at = _camera.WorldToScreenPoint(view.AimPoint);
if (at.z <= 0f)
continue;
float distance = Vector2.Distance(new Vector2(at.x, at.y), screen);
if (distance < bestDistance)
{
bestDistance = distance;
best = unit;
}
}
return best;
}
// ---------------------------------------------------------------- leaving the match
void BuildLeaveDialog()
{
_leave = new VisualElement();
_leave.style.position = Position.Absolute;
_leave.style.left = 0f;
_leave.style.top = 0f;
_leave.style.right = 0f;
_leave.style.bottom = 0f;
_leave.style.backgroundColor = new Color(.02f, .03f, .04f, .72f);
_leave.style.justifyContent = Justify.Center;
_leave.style.alignItems = Align.Center;
var panel = new LunePanel(LunePanels.Menu);
panel.style.minWidth = 520f;
_leave.Add(panel);
VisualElement column = LuneUi.Box(true, 14f, panel);
column.style.alignItems = Align.Center;
// Without the start screen (tests, benchmark) leaving ends the game.
bool menu = Net != null;
LuneUi.Label("Partie verlassen?", "HeadingLabel", column);
LuneUi.Wrap(LuneUi.Label(menu ? "Zurück zum Hauptmenü mit Offline, Hosten und Beitreten." : "Ohne Startbildschirm wird das Spiel beendet.", "MutedLabel", column)).style.unityTextAlign = TextAnchor.MiddleCenter;
VisualElement row = LuneUi.Box(false, 12f, column);
new LuneButton(menu ? "ZUM MENÜ" : "BEENDEN", LuneButtons.Primary, row).Clicked += () =>
{
LuneUi.Show(_leave, false);
if (Net != null)
Net.ReturnToMenu();
else
Quit();
};
new LuneButton("Zurück", LuneButtons.Secondary, row).Clicked += () => LuneUi.Show(_leave, false);
LuneUi.Show(_leave, false);
Hud.Root.Add(_leave);
}
void AskLeave()
{
LuneUi.Show(_leave, true);
_leave.BringToFront();
}
static void Quit()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
}
}