Files
AIGame/AIClient/Assets/HotUpdate/Scripts/Gameplay/BattleSkillPresentation.cs
T

153 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace HotUpdate.Gameplay
{
/// <summary>Authored pool and animation phase adapter. Accepts resolved events only; cannot change combat data.</summary>
public sealed class BattleSkillPresentation : MonoBehaviour
{
[Serializable] public sealed class Pool { public ParticleSystem[] instances; [NonSerialized] public int next; }
[Serializable] public sealed class Profile
{
public int pet,chargeEffect,projectileEffect,hitEffect,abilityEffect,releaseEffect;
public int chargeSound,launchSound,hitSound,abilitySound;
}
[SerializeField] Pool[] _effects;
[SerializeField] AudioClip[] _clips;
[SerializeField] AudioSource[] _voices;
[SerializeField] float[] _petImpacts, _enemyImpacts;
[SerializeField] Profile[] _profiles;
[SerializeField] bool[] _groundEffects;
[SerializeField] float[] _effectLifetimes;
sealed class Lease { public ParticleSystem ps; public int generation; public LineRenderer[] lines; }
sealed class Shot { public Lease lease; public int target; public Vector3 start; public float born,arrival; }
sealed class Charge { public Lease lease; public int owner; }
readonly Dictionary<ParticleSystem,int> _generations=new Dictionary<ParticleSystem,int>();
readonly Dictionary<int,Charge> _charges=new Dictionary<int,Charge>();
readonly Dictionary<int,Lease> _shields=new Dictionary<int,Lease>();
readonly List<Shot> _shots=new List<Shot>();
readonly List<(Lease lease,float until)> _flashes=new List<(Lease,float)>();
readonly Dictionary<int,int> _castTargets=new Dictionary<int,int>();
readonly HashSet<(int cast,int sound)> _castSounds=new HashSet<(int,int)>();
int _generation,_voice;
int[] _voiceCasts;
float _speed=1;
bool _paused;
public int PlayedSounds {get;private set;}
public int PlayedEffects {get;private set;}
public int ActiveShots=>_shots.Count;
public int ActiveCharges=>_charges.Count;
public int ActiveShields=>_shields.Count;
public int EffectKinds=>_effects?.Length??0;
public int ProfileCount=>_profiles?.Length??0;
public int ActiveBeams=>_flashes.Count;
public int[] EffectPlays {get;private set;}
public int[] SoundPlays {get;private set;}
void OnEnable(){PrototypeClientPrefs.AudioPreferencesChanged+=AudioChanged;ResetPlayback();}
void OnDisable(){PrototypeClientPrefs.AudioPreferencesChanged-=AudioChanged;ResetPlayback();}
void AudioChanged(){if(!PrototypeClientPrefs.Sfx)foreach(var voice in _voices)voice.Stop();}
public void ResetPlayback()
{
if(_effects!=null)foreach(var pool in _effects)foreach(var ps in pool.instances)ps.Stop(true,ParticleSystemStopBehavior.StopEmittingAndClear);
if(_voices!=null)foreach(var voice in _voices)voice.Stop();
_charges.Clear();_shields.Clear();_shots.Clear();_flashes.Clear();_castTargets.Clear();_castSounds.Clear();_generations.Clear();
if(_effects!=null)foreach(var pool in _effects)foreach(var ps in pool.instances)foreach(var line in ps.GetComponentsInChildren<LineRenderer>())line.enabled=false;
_voiceCasts=new int[_voices?.Length??0];EffectPlays=new int[_effects?.Length??0];SoundPlays=new int[_clips?.Length??0];_paused=false;_speed=1;PlayedSounds=PlayedEffects=0;
}
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);foreach(var line in lease.lines)line.enabled=false;_generations.Remove(lease.ps);}}
Lease Effect(int kind,Vector3 position,Color? color=null)
{
var pool=_effects[kind];var ps=pool.instances[pool.next++%pool.instances.Length];ps.Stop(true,ParticleSystemStopBehavior.StopEmittingAndClear);ps.transform.position=position;ps.transform.rotation=Quaternion.identity;
foreach(var child in ps.GetComponentsInChildren<ParticleSystem>()){var main=child.main;main.simulationSpeed=_speed;if(color.HasValue)main.startColor=color.Value;}
var lease=new Lease{ps=ps,generation=++_generation,lines=ps.GetComponentsInChildren<LineRenderer>()};foreach(var line in lease.lines)line.enabled=false;_generations[ps]=lease.generation;ps.Play(true);foreach(var child in ps.GetComponentsInChildren<ParticleSystem>())if(child.main.loop)child.Emit(1);PlayedEffects++;EffectPlays[kind]++;return lease;
}
void Sound(int id,int cast=0)
{
if(id<0||_paused||!PrototypeClientPrefs.Sfx||cast>0&&!_castSounds.Add((cast,id)))return;int index=_voice++%_voices.Length;var voice=_voices[index];voice.Stop();voice.clip=_clips[id];voice.pitch=1;voice.volume=.3f/_voices.Length;_voiceCasts[index]=cast;voice.Play();PlayedSounds++;SoundPlays[id]++;
}
void EndCharge(int cast)
{
if(_charges.TryGetValue(cast,out var charge)){Stop(charge.lease);_charges.Remove(cast);}
for(int i=0;i<_voices.Length;i++)if(_voiceCasts[i]==cast&&_voices[i].clip==_clips[0])_voices[i].Stop();
}
static Color Element(int element)=>element==1?new Color(1,.32f,.04f):element==2?new Color(.4f,1,.28f):element==3?new Color(.8f,.7f,.4f):element==4?new Color(1,.9f,.2f):element==5?new Color(.3f,.85f,1):Color.white;
Lease Burst(int kind,Vector3 position,float time,Vector3? source=null)
{
if(_groundEffects[kind])position.y=transform.position.y+.06f;
var lease=Effect(kind,position);
foreach(var line in lease.lines)
{
Vector3 start=source??position,end=position;
Vector3 origin=lease.ps.transform.InverseTransformPoint(start),delta=lease.ps.transform.InverseTransformVector(end-start);Vector3 side=Vector3.Cross(delta.normalized,Vector3.up);if(side.sqrMagnitude<.1f)side=Vector3.right;
for(int i=0;i<line.positionCount;i++){float t=i/(float)(line.positionCount-1);line.SetPosition(i,origin+delta*t+side*(i==0||i==line.positionCount-1?0:((i%2==0?1:-1)*.10f)));}line.enabled=true;
}
_flashes.Add((lease,time+_effectLifetimes[kind]));return lease;
}
public void Play(CombatEvent e,float time,Func<int,Vector3> point,PrototypeConfig config)
{
bool fresh=time-e.tick*.05f<.15f;bool pet=e.source>=0&&e.source<config.pets.Length;int element=pet?config.pets[e.source].element:0;var profile=pet?_profiles[e.source]:null;bool ability=e.skill!=null&&e.skill.EndsWith(".ability",StringComparison.Ordinal);
if(e.kind=="cast_start")
{
_castTargets[e.cast]=e.target;
if(pet&&profile.chargeEffect>=0&&fresh){_charges[e.cast]=new Charge{owner=e.source,lease=Effect(profile.chargeEffect,point(e.source))};Sound(profile.chargeSound,e.cast);}
}
else if(e.kind=="release")
{
EndCharge(e.cast);if(pet&&fresh){Sound(profile.launchSound,e.cast);if(ability&&profile.releaseEffect>=0){var lease=Burst(profile.releaseEffect,point(e.source),time);if(_castTargets.TryGetValue(e.cast,out int target)){Vector3 direction=point(target)-point(e.source);if(direction.sqrMagnitude>.001f)lease.ps.transform.rotation=Quaternion.LookRotation(direction);}}}
}
else if(e.kind=="cancel"||e.kind=="cast_end"){EndCharge(e.cast);_castTargets.Remove(e.cast);}
else if(e.kind=="projectile"&&pet&&e.arrival*.05f>time)_shots.Add(new Shot{lease=Effect(profile.projectileEffect,point(e.source),Element(element)),target=e.target,start=point(e.source),born=e.tick*.05f,arrival=e.arrival*.05f});
else if(e.kind=="hit")
{
if(pet&&fresh){Burst(profile.hitEffect,point(e.target),time,point(e.source));Sound(profile.hitSound,e.cast);}
if(e.shield<=0&&_shields.TryGetValue(e.target,out var old)){Stop(old);_shields.Remove(e.target);}
}
else if(e.kind=="shield")
{
if(_shields.TryGetValue(e.target,out var old))Stop(old);
if(pet&&e.expires*.05f>time){_shields[e.target]=Effect(profile.abilityEffect,point(e.target));if(fresh)Sound(profile.abilitySound,e.cast);}
}
else if((e.kind=="heal"||e.kind=="slow")&&pet&&fresh){Burst(profile.abilityEffect,point(e.target),time);Sound(profile.abilitySound,e.cast);}
else if(e.kind=="shield_expired"||e.kind=="death"||e.kind=="exit")
{
if(_shields.TryGetValue(e.target,out var old)){Stop(old);_shields.Remove(e.target);}
foreach(int cast in _charges.Where(x=>x.Value.owner==e.target).Select(x=>x.Key).ToArray())EndCharge(cast);
}
else if(e.kind=="battle_end")ClearLeases();
}
void ClearLeases()
{
foreach(var pool in _effects)foreach(var ps in pool.instances)ps.Stop(true,ParticleSystemStopBehavior.StopEmittingAndClear);
foreach(var voice in _voices)voice.Stop();foreach(var pool in _effects)foreach(var ps in pool.instances)foreach(var line in ps.GetComponentsInChildren<LineRenderer>())line.enabled=false;_charges.Clear();_shields.Clear();_shots.Clear();_flashes.Clear();_castTargets.Clear();_castSounds.Clear();_generations.Clear();
}
public void Render(float time,Func<int,Vector3> point,Func<int,bool> alive)
{
for(int i=_flashes.Count-1;i>=0;i--)if(time>=_flashes[i].until||!Current(_flashes[i].lease)){Stop(_flashes[i].lease);_flashes.RemoveAt(i);}
foreach(var charge in _charges.Values)if(Current(charge.lease))charge.lease.ps.transform.position=point(charge.owner);
foreach(var shield in _shields)if(Current(shield.Value))shield.Value.ps.transform.position=point(shield.Key);
for(int i=_shots.Count-1;i>=0;i--){var shot=_shots[i];if(time>=shot.arrival||!alive(shot.target)||!Current(shot.lease)){Stop(shot.lease);_shots.RemoveAt(i);continue;}float t=Mathf.InverseLerp(shot.born,shot.arrival,time);shot.lease.ps.transform.position=Vector3.Lerp(shot.start,point(shot.target),t)+Vector3.up*Mathf.Sin(t*Mathf.PI);}
}
public void SetPlayback(bool paused,float 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 voice in _voices){if(paused)voice.Pause();else voice.UnPause();}_paused=paused;_speed=speed;
}
public void Animate(Animator animator,bool pet,int art,float time,int start,int release,int end,bool moving)
{
// Dead/unspawned units and unused art variants remain in the authored pool.
// Their next visible frame is sampled from the absolute combat clock.
if(animator==null||!animator.isActiveAndEnabled)return;animator.speed=0;animator.ResetTrigger("Attack");float tick=time*20;float[] impacts=pet?_petImpacts:_enemyImpacts;
if(start>=0&&release>start&&tick<end)
{
float impact=impacts[art];float phase=tick<release?Mathf.InverseLerp(start,release,tick)*impact:Mathf.Lerp(impact,1,Mathf.InverseLerp(release,end,tick));animator.Play("Attack",0,Mathf.Clamp(phase,0,.999f));
}
else{animator.SetInteger("Motion",moving?1:0);animator.Play(moving?"Walk":"Idle",0,time/2%1);}
animator.Update(0);
}
}
}