using UnityEngine; namespace BumpCombat.Enemies { /// Original Lancer pixels, posed from the same clock and tip as combat. [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(); 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(); 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("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); } } }