champions-of-luneraria/Assets/Luneraria/View/LuneMatchRunner.cs
Martin Stickelbroeck 4dee9b18d2 U3A HUD-Grundlagen: UI Toolkit, Sichtmodell, menschlicher Sitz
Beginn des Ports des G3-HUD (Referenz luneraria/g3, 1b540f2).

- Entscheidung UI Toolkit statt uGUI nach kurzer Bewertung und einer Probe:
  Panel rendert im Batchmodus in eine 1920x1080-Textur (Skalierung wie Godot
  canvas_items/expand, 1600x960), gefaste Verlaufsplatten, alle 50 Symbole,
  Schriften aus den variablen Fonts der Referenz.
- Sichtmodell portiert (LuneHudModel, LuneLaneThreat, LuneStats) mit den
  Privatsphäre-Regeln der Referenz: Gold, Entwurf, Rückgängig und neue
  Buchungen nur für den eigenen Sitz; Lanes, Gebäude, Boss und Wellen aus
  öffentlichem Zustand.
- Menschlicher Sitz in LuneMatchRunner/LuneMatchDriver: Bots planen die
  übrigen fünf Sitze, die Planung wartet auf Bereit; Tempo 1x/2x/4x und Pause
  nur im Kampf. Bot-Partien unverändert.
- Theme-Grundlagen: Farben und Zahlenformate, Platten (forged_style.gd),
  Vektorsymbole (lune_glyphs.gd) mit Painter2D, statische SDF-Atlanten aus
  den benannten Instanzen von Cantarell und Cinzel (OFL).
- Tests: EditMode 23/23 (12 neue: gleiches öffentliches Bild für alle Sitze,
  fremde Entwürfe privat, private Boss-Lane-Wahl, Lane-Bedrohung, Bosszustände,
  nächste Welle, Bereit-Sperre, Formate, menschlicher Sitz); PlayMode 3/3,
  Fingerabdrücke der Bot-Partien unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5).
- Dokumentation: U3-UI (Bewertung, U3A), U5-VISUAL (Vorgaben des
  Projektinhabers: stilisierter Realismus für die ganze Welt, erst in U5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:57:45 +02:00

142 lines
5 KiB
C#

using System;
using Luneraria.Core;
namespace Luneraria.View
{
/// <summary>
/// Runs a match on the host (U2C/U2D, human seat since U3) against wall-clock time: planning
/// (the bots plan through the command interface), combat in fixed steps, a pause after the
/// settlement, the next round, until an Ancient falls. Only the core decides; time only paces
/// the calls, so a bot match calls the core in the order of LuneTrace.RunCore at every frame
/// rate. With a human seat (HumanSlot &gt;= 0) the bots plan the other five seats and planning
/// waits for the human's Ready, as offline in Godot (lune_game.gd); there is no pause after
/// the settlement.
/// </summary>
public sealed class LuneMatchRunner
{
public enum Stage { Planning, Combat, Settled, Over }
public readonly LuneMatch Match;
public readonly LuneClock Clock;
public readonly string Profile;
/// <summary>The seat a human plans through the HUD; -1 for a bot match.</summary>
public readonly int HumanSlot;
public LuneTrace Trace;
public double PlanningSeconds = 1.0;
public double SettledSeconds = 2.0;
public int MaxRound = 60;
public Stage Current { get; private set; }
public int StepsThisFrame { get; private set; }
public event Action<LuneCombat> CombatStarted;
public event Action<Settlement> Settled;
public event Action<int> Finished;
double _stageTime;
public LuneMatchRunner(MatchOptions options, int startRound = 1, int extraGold = 0, string profile = "")
{
Match = NewMatch(options, startRound, extraGold);
Clock = new LuneClock(Match.Balance.SimStep);
Profile = profile;
HumanSlot = options.HumanSlot >= 0 && options.HumanSlot < 6 ? options.HumanSlot : -1;
if (HumanSlot >= 0)
SettledSeconds = 0.0;
EnterPlanning();
}
/// <summary>A match with the public fixtures the golden scenarios use (start round, gold).</summary>
public static LuneMatch NewMatch(MatchOptions options, int startRound, int extraGold)
{
var match = new LuneMatch();
match.Setup(options);
if (startRound > 1)
match.RoundNumber = startRound;
if (extraGold != 0)
foreach (Player player in match.Players)
player.Draft.Gold += extraGold;
return match;
}
void EnterPlanning()
{
Current = Stage.Planning;
_stageTime = 0.0;
for (int slot = 0; slot < 6; slot++)
{
if (slot != HumanSlot)
LuneBot.Plan(Match, slot, Profile);
}
}
/// <summary>Advances by simulated seconds (frame time times match speed).</summary>
public void Advance(double seconds)
{
StepsThisFrame = 0;
switch (Current)
{
case Stage.Planning:
_stageTime += seconds;
// A bot match starts after a short look at the plans; a human starts it with Ready.
if (HumanSlot < 0 ? _stageTime >= PlanningSeconds : Match.AllReady())
StartCombat();
break;
case Stage.Combat:
StepsThisFrame = Clock.Advance(seconds, StepOnce);
if (Match.Phase != Phase.Combat)
EndCombat();
break;
case Stage.Settled:
_stageTime += seconds;
if (_stageTime >= SettledSeconds)
EnterPlanning();
break;
}
}
/// <summary>
/// The human's Ready (Godot _on_ready): the plan becomes binding; once every seat is ready
/// the combat starts at once.
/// </summary>
public bool Ready()
{
if (Current != Stage.Planning || HumanSlot < 0 || Match.Phase != Phase.Planning)
return false;
Match.SetReady(HumanSlot);
if (Match.AllReady())
StartCombat();
return true;
}
void StartCombat()
{
Match.BeginCombat();
Clock.Reset();
Current = Stage.Combat;
CombatStarted?.Invoke(Match.Combat);
}
bool StepOnce()
{
Match.StepCombat();
Trace?.RecordStep(Match.Combat);
return Match.Phase == Phase.Combat;
}
void EndCombat()
{
Trace?.RecordSettlement(Match);
Settled?.Invoke(Match.LastSettlement);
_stageTime = 0.0;
if (Match.Phase == Phase.Over || Match.RoundNumber > MaxRound)
{
Current = Stage.Over;
Finished?.Invoke(Match.Winner);
}
else
{
Current = Stage.Settled;
}
}
}
}