Das G3-HUD steht im Spiel; die Abnahme (U3D) folgt. - U3B: alle Bereiche und Komponenten aus Godot übertragen (LuneHud, Lanes, Bossgrotte, nächste Welle, Kaufkarten, Armee, Held mit Items und Talenten, Angriffschampions, Team, Ereignisse, Minimap, Tooltip, Optionen, Partieende). Porträts aus den echten Modellen, einmal gerendert. - Offene G3-Punkte umgesetzt: „1 WELLE“ als Band am Porträtrand, Schadens- und Rüstungssymbole auf den Karten, Armeewert, Heldenporträts auf der Minimap. Befehlsplatz für die Ultimates der geplanten Helden reserviert (verborgen). - U3C: LuneGame verdrahtet Befehle (gleiche Schnittstelle wie die Bots), Bereit mit 1-s-Sperre nach dem Kampf, Tempo und Pause, Tasten wie in G3, Kamera-Ziele, Auswahl im Feld, Planungsvorschau und Aufstellungsflächen. Ersetzt die U2-Debug-Anzeige. Bot-Partien und Benchmark unverändert. - Szenen- und Bibliotheksbauer idempotent: Animator-Controller und Post-Processing-Profil werden an Ort und Stelle aktualisiert. Die Szene bekommt bei jedem Bau neue Objekt-IDs, der Inhalt ist gleich. - Prüfung (Port von verify_g3, Teile A–H): 699 Prüfungen ohne Fehler. EditMode 23/23, PlayMode 12/12, Fingerabdrücke der Bot-Partien unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
154 lines
6.3 KiB
C#
154 lines
6.3 KiB
C#
using System.Collections.Generic;
|
|
using Luneraria.Core;
|
|
using UnityEngine;
|
|
|
|
namespace Luneraria.View
|
|
{
|
|
/// <summary>
|
|
/// Shows the driver's match (U2C): unit views per combat, building looks from the authoritative
|
|
/// HP, tower bolts from the towers' attack events. Runs after the driver in the same frame and
|
|
/// only reads the core.
|
|
/// </summary>
|
|
[DefaultExecutionOrder(100)]
|
|
public sealed class LuneMatchView : MonoBehaviour
|
|
{
|
|
public LuneMatchDriver Driver;
|
|
public LuneUnitLibrary Library;
|
|
public Camera ViewCamera;
|
|
/// <summary>
|
|
/// Planning only: the human's draft army in its formation (set by the game controller, U3;
|
|
/// Godot _rebuild_preview). Bot matches leave it empty and show no units while planning.
|
|
/// </summary>
|
|
public LuneCombat Preview;
|
|
|
|
readonly Dictionary<int, LuneUnitView> _units = new Dictionary<int, LuneUnitView>();
|
|
readonly Dictionary<string, LuneBuildingView> _buildings = new Dictionary<string, LuneBuildingView>();
|
|
readonly Dictionary<string, int> _towerSerials = new Dictionary<string, int>();
|
|
readonly List<Bolt> _bolts = new List<Bolt>();
|
|
LuneCombat _combat;
|
|
Transform _unitRoot;
|
|
bool _synced;
|
|
|
|
sealed class Bolt
|
|
{
|
|
public LineRenderer Line;
|
|
public float Age;
|
|
public LuneUnitView Target;
|
|
public Vector3 From;
|
|
}
|
|
|
|
public IEnumerable<LuneUnitView> Units { get { return _units.Values; } }
|
|
/// <summary>Main-thread cost of the last frame's view update (milliseconds).</summary>
|
|
public double LastSyncMs { get; private set; }
|
|
readonly System.Diagnostics.Stopwatch _clock = new System.Diagnostics.Stopwatch();
|
|
|
|
public LuneBuildingView Building(string id)
|
|
{
|
|
_buildings.TryGetValue(id, out LuneBuildingView view);
|
|
return view;
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
foreach (LuneBuildingView building in FindObjectsByType<LuneBuildingView>(FindObjectsSortMode.None))
|
|
_buildings[building.BuildingId] = building;
|
|
_unitRoot = new GameObject("Units").transform;
|
|
if (ViewCamera == null)
|
|
ViewCamera = Camera.main;
|
|
}
|
|
|
|
void LateUpdate()
|
|
{
|
|
if (Driver == null || Driver.Runner == null)
|
|
return;
|
|
_clock.Restart();
|
|
LuneMatch match = Driver.Runner.Match;
|
|
float dt = Time.deltaTime;
|
|
// Planning shows the preview when there is one (human seat); a bot match keeps the last
|
|
// combat's figures standing, unsynced, as in Godot.
|
|
bool planning = Driver.Runner.Current == LuneMatchRunner.Stage.Planning;
|
|
LuneCombat shown = planning && Preview != null ? Preview : match.Combat;
|
|
if (shown != _combat)
|
|
{
|
|
foreach (LuneUnitView view in _units.Values)
|
|
Destroy(view.gameObject);
|
|
_units.Clear();
|
|
_towerSerials.Clear();
|
|
_combat = shown;
|
|
}
|
|
bool showUnits = _combat != null && (!planning || _combat == Preview);
|
|
float alpha = Driver.Runner.Current == LuneMatchRunner.Stage.Combat ? (float)Driver.Runner.Clock.Alpha : 1f;
|
|
if (showUnits)
|
|
{
|
|
foreach (LuneUnit unit in _combat.Units)
|
|
{
|
|
if (!unit.IsMobile())
|
|
continue;
|
|
if (!_units.TryGetValue(unit.Id, out LuneUnitView view))
|
|
{
|
|
view = new GameObject().AddComponent<LuneUnitView>();
|
|
view.transform.SetParent(_unitRoot, false);
|
|
view.Bind(unit, Library);
|
|
_units[unit.Id] = view;
|
|
}
|
|
view.Sync(alpha, dt, ViewCamera != null ? ViewCamera.transform : null);
|
|
}
|
|
}
|
|
SyncBuildings(match);
|
|
SyncBolts(dt);
|
|
_synced = true;
|
|
LastSyncMs = _clock.Elapsed.TotalMilliseconds;
|
|
}
|
|
|
|
void SyncBuildings(LuneMatch match)
|
|
{
|
|
bool fighting = _combat != null && match.Phase == Phase.Combat;
|
|
foreach (BuildingDef definition in match.Map.Buildings)
|
|
{
|
|
if (!_buildings.TryGetValue(definition.Id, out LuneBuildingView view))
|
|
continue;
|
|
double hp = match.BuildingHp[definition.Id];
|
|
LuneUnit unit = null;
|
|
if (_combat != null && _combat.Buildings.TryGetValue(definition.Id, out unit) && fighting)
|
|
hp = unit.Hp;
|
|
view.SetHealth((float)hp, (float)definition.MaxHp, _synced);
|
|
if (unit != null && unit.Kind == "tower" && _combat != null)
|
|
TowerAttack(view, unit);
|
|
}
|
|
}
|
|
|
|
void TowerAttack(LuneBuildingView view, LuneUnit tower)
|
|
{
|
|
_towerSerials.TryGetValue(tower.BuildingId, out int last);
|
|
_towerSerials[tower.BuildingId] = tower.AttackSerial;
|
|
if (tower.AttackSerial == last || last == 0 && tower.AttackSerial == 0)
|
|
return;
|
|
if (tower.Target == null || !_units.TryGetValue(tower.Target.Id, out LuneUnitView target))
|
|
return;
|
|
var line = new GameObject("Bolt").AddComponent<LineRenderer>();
|
|
line.transform.SetParent(_unitRoot, false);
|
|
line.sharedMaterial = Library.Bolt[Mathf.Clamp(view.Team, 0, 2)];
|
|
line.widthMultiplier = view.Tier == "base" ? .22f : .16f;
|
|
line.positionCount = 2;
|
|
line.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
|
_bolts.Add(new Bolt { Line = line, Target = target, From = view.Muzzle });
|
|
}
|
|
|
|
void SyncBolts(float dt)
|
|
{
|
|
for (int i = _bolts.Count - 1; i >= 0; i--)
|
|
{
|
|
Bolt bolt = _bolts[i];
|
|
bolt.Age += dt;
|
|
if (bolt.Age > .25f || bolt.Target == null)
|
|
{
|
|
Destroy(bolt.Line.gameObject);
|
|
_bolts.RemoveAt(i);
|
|
continue;
|
|
}
|
|
bolt.Line.SetPosition(0, bolt.From);
|
|
bolt.Line.SetPosition(1, bolt.Target.AimPoint);
|
|
}
|
|
}
|
|
}
|
|
}
|