Files
tiny_tackle_heroes/Assets/_Project/Scripts/Defense/DefenseSpriteSequence.cs
T
2026-09-28 11:07:17 +09:00

282 lines
11 KiB
C#

using System;
using UnityEngine;
using BumpCombat.Constants;
namespace BumpCombat.Defense
{
[DisallowMultipleComponent]
public sealed class DefenseSpriteSequence : MonoBehaviour
{
private SpriteRenderer target;
private Sprite[] frames;
private string key;
private float durationScale = 1f;
private bool useUnscaledTime;
private float frameRemaining;
private int frame;
private Sprite[] nextFrames;
private string nextKey;
private bool destroyWhenFinished;
private float delayRemaining;
private bool animationFinished;
private Vector2 travelStart;
private System.Func<Vector2> travelStartProvider;
private Vector2 travelEnd;
private float travelDuration;
private float travelElapsed;
private Action travelArrival;
private System.Func<Vector2> travelEndProvider;
private System.Func<bool> travelCanAdvance;
private bool travelArrivalInvoked;
private bool hideRendererAtArrival;
private System.Action completionCallback;
private bool finished;
private bool fitsLightningBeam;
private Vector2 beamStart;
private Vector2 beamEnd;
private float beamThickness;
private bool centersImpactWave;
private Vector2 impactWaveCenter;
private float impactWaveScale;
private bool fitsLightningStormGround;
private Vector2 lightningStormGroundCenter;
private float lightningStormGroundRadius;
public void Play(SpriteRenderer renderer, string sequenceKey, Sprite[] sequenceFrames,
bool destroyOnFinish, string followingKey = null,
Sprite[] followingFrames = null, float startDelay = 0f)
{
enabled = true;
completionCallback = null;
finished = false;
animationFinished = false;
durationScale = 1f;
useUnscaledTime = false;
target = renderer;
key = sequenceKey;
frames = sequenceFrames;
destroyWhenFinished = destroyOnFinish;
nextKey = followingKey;
nextFrames = followingFrames;
delayRemaining = Mathf.Max(0f, startDelay);
travelStartProvider = null;
travelEndProvider = null;
travelCanAdvance = null;
travelArrival = null;
travelDuration = 0f;
travelElapsed = 0f;
travelArrivalInvoked = false;
hideRendererAtArrival = false;
if (target == null || frames == null || frames.Length == 0)
{
FinishSequence();
return;
}
frame = 0;
target.sprite = frames[frame];
target.enabled = delayRemaining <= 0f;
frameRemaining = DefenseTowerArt.FrameSeconds(key, frame) * durationScale;
UpdateVisualAlignment();
}
public void FitLightningBeam(Vector2 start, Vector2 end, float thickness)
{
fitsLightningBeam = true;
beamStart = start;
beamEnd = end;
beamThickness = thickness;
UpdateVisualAlignment();
}
public void CenterImpactWave(Vector2 center, float uniformScale)
{
centersImpactWave = true;
impactWaveCenter = center;
impactWaveScale = uniformScale;
UpdateVisualAlignment();
}
public void FitLightningStormGround(Vector2 center, float radius)
{
fitsLightningStormGround = true;
lightningStormGroundCenter = center;
lightningStormGroundRadius = Mathf.Max(0f, radius);
UpdateVisualAlignment();
}
public bool IsFinished => finished;
public void SetPlaybackTiming(float newDurationScale, bool useUnscaledTime)
{
if (float.IsNaN(newDurationScale) || float.IsInfinity(newDurationScale) || newDurationScale <= 0f)
throw new ArgumentOutOfRangeException(nameof(newDurationScale), "Playback duration scale must be positive and finite.");
float previousScale = durationScale;
durationScale = newDurationScale;
this.useUnscaledTime = useUnscaledTime;
if (!finished && frames != null && frames.Length > 0)
frameRemaining *= durationScale / previousScale;
}
public void SetCompletionCallback(System.Action callback)
{
if (callback == null) return;
if (finished) callback();
else completionCallback += callback;
}
public void SetCanAdvance(System.Func<bool> canAdvance)
{
travelCanAdvance = canAdvance;
}
public void Travel(Vector2 start, Vector2 end, float duration,
Action onArrival = null, System.Func<Vector2> endProvider = null,
System.Func<bool> canAdvance = null, bool hideAtArrival = false,
System.Func<Vector2> startProvider = null)
{
travelStart = start;
travelStartProvider = startProvider;
travelEnd = end;
travelDuration = Mathf.Max(0.001f, duration);
travelElapsed = 0f;
travelArrival = onArrival;
travelEndProvider = endProvider;
travelCanAdvance = canAdvance ?? travelCanAdvance;
travelArrivalInvoked = false;
hideRendererAtArrival = hideAtArrival;
if (delayRemaining <= 0f) ResolveTravelStart();
else transform.position = start;
}
private void ResolveTravelStart()
{
if (travelStartProvider != null)
{
travelStart = travelStartProvider();
travelStartProvider = null;
}
transform.position = travelStart;
}
private void Update()
{
if (target == null || frames == null || frames.Length == 0)
{
FinishSequence();
return;
}
if (travelCanAdvance != null && !travelCanAdvance()) return;
float deltaTime = useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
if (delayRemaining > 0f)
{
delayRemaining -= deltaTime;
if (delayRemaining > 0f) return;
if (travelDuration > 0f || travelStartProvider != null)
ResolveTravelStart();
target.enabled = true;
}
if (travelDuration > 0f && travelElapsed < travelDuration)
{
travelElapsed += deltaTime;
Vector2 currentEnd = travelEndProvider != null
? travelEndProvider() : travelEnd;
transform.position = Vector2.Lerp(
travelStart, currentEnd, Mathf.Clamp01(travelElapsed / travelDuration));
if (travelElapsed >= travelDuration && !travelArrivalInvoked)
{
travelArrivalInvoked = true;
if (hideRendererAtArrival && target != null) target.enabled = false;
travelArrival?.Invoke();
}
}
if (animationFinished)
{
if (travelDuration <= 0f || travelElapsed >= travelDuration) FinishSequence();
return;
}
frameRemaining -= deltaTime;
while (frameRemaining <= 0f)
{
frame++;
if (frame >= frames.Length)
{
if (nextFrames != null && nextFrames.Length > 0)
{
frames = nextFrames;
key = nextKey;
nextFrames = null;
frame = 0;
}
else
{
animationFinished = true;
if (travelDuration <= 0f || travelElapsed >= travelDuration) FinishSequence();
return;
}
}
target.sprite = frames[frame];
UpdateVisualAlignment();
frameRemaining += DefenseTowerArt.FrameSeconds(key, frame) * durationScale;
}
}
private void UpdateVisualAlignment()
{
if (target == null || target.sprite == null) return;
if (fitsLightningBeam)
{
Vector2 direction = beamEnd - beamStart;
float distance = direction.magnitude;
Vector2Int bounds = DefenseTowerArtBounds.LightningBeamXBounds(frame);
float pixelsPerUnit = target.sprite.pixelsPerUnit;
float visibleWidth = (bounds.y - bounds.x) / pixelsPerUnit;
float scaleX = distance <= 0f || visibleWidth <= 0f
? 0f : distance / visibleWidth;
float localStart = (bounds.x - target.sprite.pivot.x) / pixelsPerUnit * scaleX;
transform.rotation = Quaternion.Euler(0f, 0f,
distance > 0f ? Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg : 0f);
transform.position = beamStart - (Vector2)(transform.right * localStart);
transform.localScale = new Vector3(scaleX, beamThickness, 1f);
}
if (fitsLightningStormGround)
{
float scale = lightningStormGroundRadius * target.sprite.pixelsPerUnit
/ DefenseTowerArtBounds.LightningStormGroundRadiusPixels;
transform.localScale = Vector3.one * scale;
Vector2 localCenter = (DefenseTowerArtBounds.LightningStormGroundCenterPixels
- target.sprite.pivot) / target.sprite.pixelsPerUnit;
transform.position = lightningStormGroundCenter
- (Vector2)transform.TransformVector(localCenter);
}
if (centersImpactWave)
{
transform.localScale = Vector3.one * impactWaveScale;
Vector2 localCenter = (DefenseTowerArtBounds.ImpactWaveCenterPixels(frame, target.sprite)
- target.sprite.pivot) / target.sprite.pixelsPerUnit;
transform.position = impactWaveCenter - (Vector2)transform.TransformVector(localCenter);
}
}
private void FinishSequence()
{
if (finished) return;
finished = true;
if (destroyWhenFinished)
{
Destroy(gameObject);
}
else
{
if (target != null) target.enabled = false;
enabled = false;
}
System.Action callback = completionCallback;
completionCallback = null;
callback?.Invoke();
}
}
}