198 lines
7.8 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|