using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace HotUpdate.Gameplay
{
/// Independent review player for a C++ generated immutable event trace. Never computes damage or rewards.
public sealed class SkillSlicePlayer : MonoBehaviour
{
[Serializable] public sealed class View
{
public int id;
public Transform root, muzzle;
public Animator animator;
public TextMesh label;
[Range(0, 1)] public float impact = .5f;
[NonSerialized] public int hp, maxHp, shield, cast, start, release, end, lastMove;
[NonSerialized] public bool casting, dead, moving;
[NonSerialized] public float idleClock;
}
[Serializable] public sealed class Pool { public ParticleSystem[] instances; [NonSerialized] public int next; }
[SerializeField] TextAsset _trace;
[SerializeField] View[] _views;
// Charge, projectile, fire impact, stone impact, shield. Authored in a prefab, no runtime construction.
[SerializeField] Pool[] _effects;
[SerializeField] AudioClip[] _clips;
[SerializeField] AudioSource[] _voices;
[SerializeField] EnemyCombatPresentation _enemyFeedback;
[SerializeField] TextMesh _status;
[SerializeField] bool _autoPlay = true;
sealed class Lease { public ParticleSystem ps; public int generation; }
sealed class Shot { public Lease lease; public View target; public Vector3 start; public float born, arrival; }
readonly Dictionary _charges = new Dictionary();
readonly Dictionary _shields = new Dictionary();
readonly Dictionary _generations = new Dictionary();
readonly List _shots = new List();
JArray _events;
JObject _document;
int _cursor, _generation, _voice;
int[] _voiceCasts;
float _clock, _speed = 1;
bool _paused, _loaded;
public bool Finished { get; private set; }
public int ConsumedEvents => _cursor;
public int EventCount => _events?.Count ?? 0;
public int PlayedSounds { get; private set; }
public int ActiveShots => _shots.Count;
public int ActiveCharges => _charges.Count;
public int ActiveShields => _shields.Count;
public float Clock => _clock;
public View[] Views => _views;
public bool Won => (bool)_document["won"];
public bool FinalStateMatches => _document != null && ((JArray)_document["final"]).All(t => { var v=Find((int)t["id"]); return v.hp==(int)t["hp"] && v.shield==(int)t["shield"] && Mathf.Abs(v.root.localPosition.z-(int)t["z"]/1000f)<.0001f && v.dead==((int)t["hp"]<=0||((bool?)t["exited"]??false)); });
View Find(int id) => Array.Find(_views, v => v.id == id) ?? throw new InvalidOperationException("Missing trace entity " + id);
void OnEnable() { PrototypeClientPrefs.AudioPreferencesChanged += AudioChanged; if (_autoPlay) Restart(); }
void OnDisable() { PrototypeClientPrefs.AudioPreferencesChanged -= AudioChanged; Clear(); _loaded=false; }
void AudioChanged() { if (!PrototypeClientPrefs.Sfx) foreach (var v in _voices) v.Stop(); }
public void Restart()
{
Clear();
_document=JObject.Parse(_trace.text);
if ((int)_document["schema"]!=1 || (string)_document["kind"]!="isolated_skill_slice" || (int)_document["tickMs"]!=50) throw new InvalidOperationException("Unsupported skill trace");
_events=(JArray)_document["events"];
int lastTick=-1;
for(int i=0;i<_events.Count;i++) {int tick=(int)_events[i]["tick"];if((int)_events[i]["seq"]!=i||tick lease!=null && _generations.TryGetValue(lease.ps,out int n) && n==lease.generation;
void Stop(Lease lease) {if(Current(lease)){lease.ps.Stop(true,ParticleSystemStopBehavior.StopEmittingAndClear);_generations.Remove(lease.ps);}}
Lease Effect(int kind,Vector3 position)
{
var pool=_effects[kind];var ps=pool.instances[pool.next++%pool.instances.Length];ps.Stop(true,ParticleSystemStopBehavior.StopEmittingAndClear);ps.transform.position=position;
foreach(var child in ps.GetComponentsInChildren()){var main=child.main;main.simulationSpeed=_speed;}
var lease=new Lease{ps=ps,generation=++_generation};_generations[ps]=lease.generation;ps.Play(true);
foreach(var child in ps.GetComponentsInChildren())if(child.main.loop)child.Emit(1);
return lease;
}
void Sound(int kind,int cast=0)
{
if(!PrototypeClientPrefs.Sfx||_paused)return;
int index=_voice++%_voices.Length;var source=_voices[index];source.Stop();source.clip=_clips[kind];source.pitch=1;source.volume=.7f/_voices.Length;_voiceCasts[index]=cast;source.Play();PlayedSounds++;
}
void EndCharge(int cast)
{
if(_charges.TryGetValue(cast,out var lease)){Stop(lease);_charges.Remove(cast);}
for(int i=0;i<_voices.Length;i++)if(_voiceCasts[i]==cast&&_voices[i].clip==_clips[0])_voices[i].Stop();
}
public void SetPlayback(bool paused,float speed)
{
if(speed!=1&&speed!=2)throw new ArgumentOutOfRangeException(nameof(speed));
if(_paused==paused&&_speed==speed)return;
foreach(var pool in _effects)foreach(var ps in pool.instances)
{
foreach(var child in ps.GetComponentsInChildren()){var main=child.main;main.simulationSpeed=speed;}
if(paused&&!_paused)ps.Pause(true);else if(!paused&&_paused&&ps.IsAlive(true))ps.Play(true);
}
if(paused!=_paused)foreach(var source in _voices){if(paused)source.Pause();else source.UnPause();}
_enemyFeedback?.SetPlayback(paused,speed);_paused=paused;_speed=speed;if(_loaded)Render();
}
void Update() { if(_loaded)Advance(Time.unscaledDeltaTime); }
/// Also permits deterministic low-frame-rate replay in verification.
public void Advance(float seconds)
{
if(seconds<0||float.IsNaN(seconds)||float.IsInfinity(seconds))throw new ArgumentOutOfRangeException(nameof(seconds));
if(!_loaded||_paused||Finished)return;
_clock+=seconds*_speed;Consume();Render();
}
void Consume()
{
while(_cursor<_events.Count&&(int)_events[_cursor]["tick"]*.05f<=_clock+.00001f)
{
var e=(JObject)_events[_cursor++];string kind=(string)e["kind"],skill=(string)e["skill"]??"";int cast=(int?)e["cast"]??0;
var source=e["source"]==null?null:Find((int)e["source"]);var target=e["target"]==null?null:Find((int)e["target"]);
float eventTime=(int)e["tick"]*.05f;bool fresh=_clock-eventTime<.15f;
if(kind=="cast_start")
{
source.cast=cast;source.start=(int)e["tick"];source.release=(int)e["release"];source.end=(int)e["end"];source.casting=true;
if(source.id==1&&fresh){_charges[cast]=Effect(0,source.muzzle.position);Sound(0,cast);}
}
else if(kind=="release") {EndCharge(cast);if(source.id==1&&fresh)Sound(1);}
else if(kind=="cancel"||kind=="cast_end") {EndCharge(cast);if(source.cast==cast)source.casting=false;}
else if(kind=="move") {var p=target.root.localPosition;p.z=(int)e["z"]/1000f;target.root.localPosition=p;target.moving=true;target.lastMove=(int)e["tick"];}
else if(kind=="projectile")
{
float arrival=(int)e["arrival"]*.05f;
if(arrival>_clock){var start=source.muzzle.position;_shots.Add(new Shot{lease=Effect(1,start),target=target,start=start,born=eventTime,arrival=arrival});}
}
else if(kind=="hit")
{
target.hp=(int)e["hp"];target.shield=(int)e["shield"];
if(fresh)
{
if(source.id==1){Effect(2,target.muzzle.position);Sound(2);}
else if(source.id==0){Effect(3,target.muzzle.position);Sound(3);}
else _enemyFeedback?.Play(new PrototypeBattle.CombatCue(PrototypeBattle.CueKind.EnemyAttack,0,transform.InverseTransformPoint(target.muzzle.position),Vector3.back,(int)e["amount"]==0,transform.InverseTransformPoint(source.muzzle.position)));
}
if(target.shield<=0&&_shields.TryGetValue(target.id,out var shield)){Stop(shield);_shields.Remove(target.id);}
}
else if(kind=="shield")
{
target.shield=(int)e["shield"];if(_shields.TryGetValue(target.id,out var old))Stop(old);
if((int)e["expires"]*.05f>_clock){_shields[target.id]=Effect(4,target.muzzle.position);if(fresh)Sound(4);}
}
else if(kind=="shield_expired") {target.shield=0;if(_shields.TryGetValue(target.id,out var shield)){Stop(shield);_shields.Remove(target.id);}}
else if(kind=="death"||kind=="exit") {target.dead=true;if(kind=="death")target.hp=0;target.casting=false;EndCharge(target.cast);if(_shields.TryGetValue(target.id,out var shield)){Stop(shield);_shields.Remove(target.id);}}
else if(kind=="battle_end") {Finished=true;Clear();}
}
}
void Render()
{
foreach(var v in _views)
{
v.root.gameObject.SetActive(!v.dead);
if(v.label!=null)v.label.text=(v.id==0?"RockBall":v.id==1?"FireCat":"Hornlet")+" "+Mathf.CeilToInt(v.hp/1000f)+"/"+Mathf.CeilToInt(v.maxHp/1000f)+(v.shield>0?" Shield "+Mathf.CeilToInt(v.shield/1000f):"");
if(v.animator==null||v.dead)continue;
float tick=_clock*20,normalized;
if(v.casting&&tick=100&&v.moving&&tick-v.lastMove<2;v.animator.SetInteger("Motion",walk?1:0);v.animator.Play(walk?"Walk":"Idle",0,_clock/2%1);}
v.animator.Update(0);
}
foreach(var pair in _charges)if(Current(pair.Value)){var v=_views.FirstOrDefault(x=>x.cast==pair.Key);if(v!=null)pair.Value.ps.transform.position=v.muzzle.position;}
foreach(var pair in _shields)if(Current(pair.Value))pair.Value.ps.transform.position=Find(pair.Key).muzzle.position;
for(int i=_shots.Count-1;i>=0;i--)
{
var shot=_shots[i];if(_clock>=shot.arrival||shot.target.dead||!Current(shot.lease)){Stop(shot.lease);_shots.RemoveAt(i);continue;}
float progress=Mathf.InverseLerp(shot.born,shot.arrival,_clock);
shot.lease.ps.transform.position=Vector3.Lerp(shot.start,shot.target.muzzle.position,progress)+Vector3.up*Mathf.Sin(progress*Mathf.PI);
}
if(_status!=null)_status.text="Skill slice "+(_paused?"Paused":Finished?Won?"Victory":"Finished":_speed+"x")+" "+_clock.ToString("F2")+"s\n"+"C++ trace / "+_cursor+" events";
}
}
}