Files

198 lines
7.8 KiB
C#

using UnityEngine;
namespace BumpCombat.Enemies
{
/// <summary>Original Lancer pixels, posed from the same clock and tip as combat.</summary>
[DisallowMultipleComponent]
[DefaultExecutionOrder(1000)]
public sealed class LancerThrustVisual : MonoBehaviour
{
private const string Lease = "LancerThrust";
private const float RecoveryPoseDuration = 0.08f;
private static readonly float[] TipPixels = { 22f, 20f, 19f, 30f, 28f, 26f };
private static Sprite[] bodyFrames;
private static Sprite[] spearFrames;
private EnemyController controller;
private EnemyAttack attack;
private SpriteRenderer source;
private SpriteRenderer bodyVisual;
private SpriteRenderer spearVisual;
private bool showing;
private bool ownsLease;
private float recoveryRemaining;
public SpriteRenderer BodyRenderer => bodyVisual;
public SpriteRenderer SpearRenderer => spearVisual;
public bool IsVisible => showing && bodyVisual != null && bodyVisual.enabled;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetFrames()
{
ReleaseFrames(bodyFrames);
ReleaseFrames(spearFrames);
bodyFrames = null;
spearFrames = null;
}
private static void ReleaseFrames(Sprite[] frames)
{
if (frames == null) return;
foreach (Sprite frame in frames)
{
if (frame != null) Destroy(frame);
}
}
public void Begin(EnemyAttack owner, EnemyController enemy)
{
attack = owner;
controller = enemy;
source = GetComponent<SpriteRenderer>();
bodyFrames ??= LoadFrames("Body", new Vector2(0.5f, 0.5f));
spearFrames ??= LoadFrames("Spear", new Vector2(0.6f, 0.56f));
if (source == null || bodyFrames == null || spearFrames == null) return;
if (bodyVisual == null) bodyVisual = CreateRenderer("Lancer Thrust Body");
if (spearVisual == null) spearVisual = CreateRenderer("Lancer Thrust Spear");
recoveryRemaining = 0f;
showing = true;
if (!ownsLease)
{
controller.AcquireSpriteVisualLease(Lease);
ownsLease = true;
}
ApplyPose(0, LancerAttackMotion.WarningStartReachFraction);
}
public void Stop(bool cancelled)
{
if (!showing) return;
if (cancelled || !isActiveAndEnabled || controller == null
|| controller.IsDead || controller.IsStunned)
{
Hide();
return;
}
recoveryRemaining = RecoveryPoseDuration;
ApplyPose(5, LancerAttackMotion.WarningEndReachFraction);
}
private void LateUpdate()
{
if (!showing) return;
if (controller == null || controller.IsDead || controller.IsStunned
|| !attack.isActiveAndEnabled)
{
Hide();
return;
}
if (controller.IsKnockbackActive || controller.IsLaunchVisualActive)
{
// Knockback can pause an uncancelled attack. Let Hurt/Launch show,
// then resume this same state clock when the controller resumes.
SuspendRenderers();
return;
}
if (recoveryRemaining > 0f)
{
recoveryRemaining -= Time.deltaTime;
if (recoveryRemaining <= 0f) Hide();
else ApplyPose(5, LancerAttackMotion.WarningEndReachFraction);
return;
}
float progress = controller.StateNormalizedTime;
if (controller.State == EnemyState.Warning)
{
ApplyPose(Mathf.Min(2, Mathf.FloorToInt(progress * 3f)),
LancerAttackMotion.EvaluateWarningReachFraction(progress));
}
else if (controller.State == EnemyState.Active)
{
ApplyPose(progress <= LancerAttackMotion.ActiveExtensionEndFraction ? 3 : 4,
LancerAttackMotion.EvaluateActiveReachFraction(progress));
}
else Hide();
}
private void ApplyPose(int frame, float reach)
{
if (!ownsLease)
{
controller.AcquireSpriteVisualLease(Lease);
ownsLease = true;
}
Vector2 direction = attack.LockedDirection.sqrMagnitude > 0.0001f
? attack.LockedDirection.normalized : Vector2.right;
float scale = Mathf.Max(0.0001f, Mathf.Abs(transform.lossyScale.x));
Vector2 hand = (Vector2)transform.position
+ LancerAttackMotion.GetHandAnchorOffset(direction, scale);
Vector2 tip = LancerAttackMotion.GetTipPosition(
transform.position, direction, controller.AttackLength, reach, scale);
bodyVisual.sprite = bodyFrames[frame];
bodyVisual.flipX = direction.x < 0f;
bodyVisual.color = source.color;
bodyVisual.sortingLayerID = source.sortingLayerID;
bodyVisual.sortingOrder = source.sortingOrder;
bodyVisual.enabled = true;
spearVisual.sprite = spearFrames[frame];
spearVisual.color = source.color;
spearVisual.sortingLayerID = source.sortingLayerID;
spearVisual.sortingOrder = source.sortingOrder + 1;
spearVisual.enabled = true;
spearVisual.transform.SetPositionAndRotation(hand,
Quaternion.Euler(0f, 0f, Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg));
// Spear has no inherited scale: elite reach/width are already combat values.
spearVisual.transform.localScale = new Vector3(
Vector2.Distance(hand, tip) * 32f / TipPixels[frame],
controller.AttackWidth * 32f / 8f, 1f);
}
private SpriteRenderer CreateRenderer(string objectName)
{
GameObject child = new(objectName);
SpriteRenderer renderer = child.AddComponent<SpriteRenderer>();
if (objectName.EndsWith("Body")) child.transform.SetParent(transform, false);
// An independent transform preserves world rotation/width under scaled elites.
renderer.sharedMaterial = source.sharedMaterial;
return renderer;
}
private static Sprite[] LoadFrames(string part, Vector2 pivot)
{
Texture2D texture = Resources.Load<Texture2D>("Enemies/Lancer/Lancer-Thrust-" + part + "-v1");
if (texture == null || texture.width != 600 || texture.height != 100) return null;
Sprite[] frames = new Sprite[6];
for (int i = 0; i < frames.Length; i++)
{
frames[i] = Sprite.Create(texture, new Rect(i * 100, 0, 100, 100), pivot, 32f,
0, SpriteMeshType.FullRect);
frames[i].name = "Lancer-Thrust-" + part + "-" + i;
}
return frames;
}
private void Hide()
{
showing = false;
recoveryRemaining = 0f;
SuspendRenderers();
}
private void SuspendRenderers()
{
if (bodyVisual != null) bodyVisual.enabled = false;
if (spearVisual != null) spearVisual.enabled = false;
if (ownsLease && controller != null) controller.ReleaseSpriteVisualLease(Lease);
ownsLease = false;
}
private void OnDisable() => Hide();
private void OnDestroy()
{
Hide();
if (bodyVisual != null) Destroy(bodyVisual.gameObject);
if (spearVisual != null) Destroy(spearVisual.gameObject);
}
}
}