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>
708 lines
28 KiB
C#
708 lines
28 KiB
C#
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
|
||
}
|
||
}
|
||
}
|