champions-of-luneraria/Assets/Luneraria/View/LuneBuildingView.cs
Martin Stickelbroeck 3871b3e2e6 U2C Simulation/View Binding
Fester Simulationstakt und Ansichten, die nur lesen; Nachweis, dass die Partie
bei jeder Bildrate dieselbe ist.

- LuneClock: Akkumulator, ganze Schritte zu 0,05 s, Alpha zum Interpolieren.
- LuneMatchRunner: Host-Ablauf einer Bot-Partie (Planung über die
  Befehlsschnittstelle, Kampf im festen Takt, Abrechnung, nächste Runde);
  LuneMatchDriver reicht in Update nur Bildzeit weiter.
- LuneTrace: Fingerabdruck der Partie (jeder Schritt, jede Abrechnung) und der
  reine Kernlauf zum Vergleich.
- Ansichten: LuneMatchView, LuneUnitView (interpolierte Position, Blickrichtung,
  Clips aus Kampfereignissen, Teamring, Lebensbalken), LuneBuildingView mit
  Einsturz und Ruine aus den autoritativen HP, Turmblitze, LuneCameraRig wie
  lune_camera.gd, vorläufige Debug-Anzeige (kein HUD; U3 portiert das G3-HUD).
- Einheitenbibliothek: Kohlenbeißer aus 131 Teilen zu einem Mesh, Animator-
  Controller mit den Blender-Clips, Ring- und Balkenmeshes, Markermaterialien.
- Tests: EditMode (30/60/120 fps, unbegrenzt, Ruckler, 8x Tempo; Seed 2 und
  Seed 9 ab Runde 21) und PlayMode in der Szene bei echten Bildraten 30, 60,
  120 und unbegrenzt: überall derselbe Fingerabdruck wie der Kern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 17:31:18 +02:00

166 lines
6.7 KiB
C#

using System.Collections.Generic;
using UnityEngine;
namespace Luneraria.View
{
/// <summary>
/// A tower (Grenzwacht, Bastion, Kernwacht) or an Ancient (Mondherz) of the G2 set. Shows the
/// authoritative building HP as one of five looks, like scripts/luneraria/view/lune_tower_view.gd
/// and lune_ancient_view.gd: intact, lightly and heavily damaged, collapsing, ruin. The view never
/// decides anything; HP comes from the core (U2C binds it).
/// </summary>
public sealed class LuneBuildingView : MonoBehaviour
{
public enum Look { Intact, Light, Heavy, Destroying, Ruin }
/// <summary>HP fractions where the look changes (the same bands for towers and Ancients).</summary>
public const float LightBelow = .605f;
public const float HeavyBelow = .305f;
public string BuildingId = "";
public int Team;
/// <summary>outer, inner, base or core.</summary>
public string Tier = "";
public Look Current = Look.Intact;
/// <summary>Length of the collapse before the ruin shows (Godot DESTROY_SECONDS).</summary>
public const float DestroySeconds = 1.7f;
readonly Dictionary<string, Transform> _parts = new Dictionary<string, Transform>();
readonly Dictionary<string, Pose> _rest = new Dictionary<string, Pose>();
float _destroyTime = -1f;
bool _destroyFromHeavy;
/// <summary>Crystal tip (where tower bolts leave).</summary>
public Vector3 Muzzle
{
get
{
Collect();
if (_parts.TryGetValue("Crystal", out Transform crystal))
return crystal.position + Vector3.up * .35f;
return transform.position + Vector3.up * 5f;
}
}
/// <summary>
/// Authoritative HP to a look. A building that falls while the view watches collapses first;
/// one that is already down when the view is built appears as a ruin at once.
/// </summary>
public void SetHealth(float hp, float maxHp, bool animate)
{
Look next = LookFor(hp, maxHp);
if (Current == Look.Destroying)
return;
if (next == Look.Ruin && Current != Look.Ruin && animate)
{
_destroyFromHeavy = Current == Look.Heavy;
Show(Look.Destroying);
_destroyTime = 0f;
return;
}
if (next != Current)
Show(next);
}
void Update()
{
if (Current != Look.Destroying)
return;
_destroyTime += Time.deltaTime;
float k = Mathf.Clamp01(_destroyTime / DestroySeconds);
// The upper part tips over and sinks; the Ancient's structure sinks into its dais.
foreach (KeyValuePair<string, Transform> part in _parts)
{
if (!_rest.TryGetValue(part.Key, out Pose rest))
continue;
bool falls = IsAncient ? !part.Key.StartsWith("Ruin") && part.Key != "Dais" && part.Key != "Pedestal"
: part.Key == "Upper" || part.Key == "Upper_Damaged" || part.Key == "Crystal" || part.Key == "Ring" || part.Key.StartsWith("Shard") || part.Key.StartsWith("Pennant") || part.Key.StartsWith("Banner");
if (!falls)
continue;
part.Value.localRotation = rest.rotation * Quaternion.Euler(0f, 0f, 70f * k * k);
part.Value.localPosition = rest.position + Vector3.down * 3f * k * k;
}
if (k >= 1f)
{
foreach (KeyValuePair<string, Transform> part in _parts)
if (_rest.TryGetValue(part.Key, out Pose rest))
part.Value.SetLocalPositionAndRotation(rest.position, rest.rotation);
Show(Look.Ruin);
}
}
public bool IsAncient { get { return Tier == "core"; } }
void Collect()
{
if (_parts.Count > 0)
return;
Transform model = transform.childCount > 0 ? transform.GetChild(0) : transform;
foreach (Transform part in model)
{
_parts[part.name] = part;
_rest[part.name] = new Pose(part.localPosition, part.localRotation);
}
}
/// <summary>Look for an HP fraction; a destroyed building shows its ruin.</summary>
public static Look LookFor(float hp, float maxHp)
{
float fraction = Mathf.Clamp01(hp / Mathf.Max(1f, maxHp));
if (hp <= 0f)
return Look.Ruin;
if (fraction < HeavyBelow)
return Look.Heavy;
if (fraction < LightBelow)
return Look.Light;
return Look.Intact;
}
/// <summary>Part visibility of a look (Godot _enter of the tower and Ancient views).</summary>
public void Show(Look look)
{
Collect();
Current = look;
bool standing = look != Look.Ruin;
if (IsAncient)
{
foreach (KeyValuePair<string, Transform> part in _parts)
{
string key = part.Key;
bool visible;
if (key.StartsWith("Ruin"))
visible = !standing;
else if (key == "Crest_A")
visible = standing && Team == 0;
else if (key == "Crest_B")
visible = standing && Team != 0;
else if (key == "Dais" || key == "Pedestal")
visible = true;
else
visible = standing;
part.Value.gameObject.SetActive(visible);
}
return;
}
bool damagedTop = look == Look.Heavy || (look == Look.Destroying && _destroyFromHeavy);
Set("Upper", standing && !damagedTop);
Set("Upper_Damaged", standing && damagedTop);
Set("Debris", look == Look.Heavy || look == Look.Destroying);
Set("Lower", standing);
Set("Ruin", !standing);
foreach (KeyValuePair<string, Transform> part in _parts)
{
string key = part.Key;
if (key == "Crystal" || key == "Ring" || key.StartsWith("Shard") || key.StartsWith("Banner") || key.StartsWith("Pennant"))
part.Value.gameObject.SetActive(standing);
}
}
void Set(string key, bool visible)
{
if (_parts.TryGetValue(key, out Transform part))
part.gameObject.SetActive(visible);
}
}
}