Add rank, friends, fishing, and camp treasure with T2 studio models and skill presentation.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace HotUpdate.Gameplay
|
||||
{
|
||||
/// <summary>Independent review player for a C++ generated immutable event trace. Never computes damage or rewards.</summary>
|
||||
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<int, Lease> _charges = new Dictionary<int, Lease>();
|
||||
readonly Dictionary<int, Lease> _shields = new Dictionary<int, Lease>();
|
||||
readonly Dictionary<ParticleSystem, int> _generations = new Dictionary<ParticleSystem, int>();
|
||||
readonly List<Shot> _shots = new List<Shot>();
|
||||
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<lastTick)throw new InvalidOperationException("Unordered trace");lastTick=tick;}
|
||||
foreach(var token in (JArray)_document["initial"])
|
||||
{
|
||||
var v=Find((int)token["id"]);v.hp=(int)token["hp"];v.maxHp=(int)token["maxHp"];v.shield=(int)token["shield"];v.cast=0;v.lastMove=-100;v.casting=v.dead=v.moving=false;v.idleClock=0;
|
||||
v.root.gameObject.SetActive(true);v.root.localPosition=new Vector3(PrototypeBattle.LaneX((int)token["lane"]),0,(int)token["z"]/1000f);
|
||||
if(v.animator!=null){v.animator.enabled=true;v.animator.speed=0;v.animator.Rebind();v.animator.Update(0);}
|
||||
}
|
||||
_cursor=0;_clock=0;_paused=false;_speed=1;Finished=false;PlayedSounds=0;_voiceCasts=new int[_voices.Length];_loaded=true;
|
||||
_enemyFeedback?.ResetPlayback();Consume();Render();
|
||||
}
|
||||
void Clear()
|
||||
{
|
||||
if(_effects!=null)foreach(var pool in _effects)foreach(var ps in pool.instances)if(ps!=null)ps.Stop(true,ParticleSystemStopBehavior.StopEmittingAndClear);
|
||||
if(_voices!=null)foreach(var source in _voices)if(source!=null)source.Stop();
|
||||
_charges.Clear();_shots.Clear();_shields.Clear();_generations.Clear();_enemyFeedback?.ResetPlayback();
|
||||
}
|
||||
bool Current(Lease lease) => 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<ParticleSystem>()){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<ParticleSystem>())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<ParticleSystem>()){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); }
|
||||
/// <summary>Also permits deterministic low-frame-rate replay in verification.</summary>
|
||||
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<v.end)
|
||||
{
|
||||
normalized=tick<v.release?Mathf.InverseLerp(v.start,v.release,tick)*v.impact:Mathf.Lerp(v.impact,1,Mathf.InverseLerp(v.release,v.end,tick));
|
||||
v.animator.Play("Attack",0,Mathf.Clamp(normalized,0,.999f));
|
||||
}
|
||||
else {bool walk=v.id>=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";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user