using UnityEngine; using BumpCombat.Combat; using BumpCombat.Core; using BumpCombat.Player; namespace BumpCombat.Defense { [DisallowMultipleComponent] public sealed class DefenseSummonUnit : CharacterModel { private Sprite[] idleFrames; private Sprite[] attackFrames; private string idleKey; private string attackKey; private float spriteFrameTime; private int spriteFrame; private bool playingAttack; private bool isWalking; private string walkKey; private Sprite[] walkFrames; private DefenseTower owner; private DefensePrototypeController controller; private Vector2 anchorLocalPosition; private SpriteRenderer spriteRenderer; private float attackCooldown; private CircleCollider2D bodyCollider; private Rigidbody2D body; private static readonly RaycastHit2D[] MovementHits = new RaycastHit2D[16]; private float currentHealth; private bool dead; private bool returningToAnchor; private DefenseRouteAgent pendingAttackTarget; private bool attackDamageApplied; public int SlotIndex { get; private set; } public override float MoveSpeed => controller != null ? controller.Tuning.SummonUnitMoveSpeed : 0f; public override float MaxHealth => owner != null ? owner.SummonUnitMaximumHealth : 1f; public bool IsDead => dead; public float CurrentHealth => currentHealth; public DefenseTower Owner => owner; public Collider2D BodyCollider => bodyCollider; public bool IsOperational => isActiveAndEnabled && !dead && owner != null && owner.IsOperational; public void Configure( DefenseTower tower, DefensePrototypeController prototypeController, Vector2 localAnchor, int slotIndex) { IsMinion = true; owner = tower; controller = prototypeController; SlotIndex = slotIndex; anchorLocalPosition = localAnchor; bodyCollider = gameObject.AddComponent(); bodyCollider.radius = owner.SummonUnitCollisionRadius; body = gameObject.AddComponent(); body.bodyType = RigidbodyType2D.Kinematic; body.gravityScale = 0f; body.freezeRotation = true; spriteRenderer = gameObject.AddComponent(); spriteRenderer.color = Color.white; spriteRenderer.sortingOrder = 11; IgnorePlayerCollisionPairs(); RefreshArt(); currentHealth = MaxHealth; } public void RefreshArt() { if (spriteRenderer == null || owner == null) { return; } string family = owner.Tier == 4 && owner.Branch == DefenseTowerBranch.First ? "GiantSlime" : owner.Tier == 4 && owner.Branch == DefenseTowerBranch.Second ? "LavaSlime" : "Slime"; idleKey = $"Summons/{family}/Idle"; attackKey = $"Summons/{family}/Attack01"; walkKey = $"Summons/{family}/Walk"; idleFrames = DefenseTowerArt.Load(idleKey); attackFrames = DefenseTowerArt.Load(attackKey); walkFrames = DefenseTowerArt.Load(walkKey); spriteFrame = 0; spriteFrameTime = 0f; playingAttack = false; if (idleFrames.Length > 0) { spriteRenderer.sprite = idleFrames[0]; } float scale = family == "GiantSlime" ? 1.55f : family == "LavaSlime" ? 1.15f : 1.1f; transform.localScale = new Vector3(scale, scale, 1f); } private void FixedUpdate() { if (owner == null || !owner.isActiveAndEnabled || controller == null || !IsOperational) return; UpdateSpriteAnimation(); attackCooldown = Mathf.Max(0f, attackCooldown - Time.deltaTime); Vector2 anchor = owner.transform.TransformPoint(anchorLocalPosition); bool isBasicSlime = owner.Tier < 4; bool isScout = owner.Tier == 4 && owner.Branch == DefenseTowerBranch.Second; if (playingAttack) { if (!attackDamageApplied && (pendingAttackTarget == null || pendingAttackTarget != owner.CommandTarget || pendingAttackTarget.HasArrived || pendingAttackTarget.Enemy == null || pendingAttackTarget.Enemy.IsDead)) { playingAttack = false; attackDamageApplied = true; pendingAttackTarget = null; if (isBasicSlime) returningToAnchor = true; spriteFrame = 0; spriteFrameTime = 0f; if (idleFrames != null && idleFrames.Length > 0) spriteRenderer.sprite = idleFrames[0]; return; } Vector2 contactPosition = body.position; if (isBasicSlime && spriteFrame >= controller.Tuning.SummonBodySlamImpactFrame) { if (pendingAttackTarget != null && !pendingAttackTarget.HasArrived && pendingAttackTarget.Enemy != null && !pendingAttackTarget.Enemy.IsDead) { contactPosition = MoveBody(Vector2.MoveTowards(body.position, pendingAttackTarget.transform.position, MoveSpeed * controller.Tuning.SummonBodySlamSpeedMultiplier * Time.fixedDeltaTime)); } } if (!attackDamageApplied && spriteFrame >= controller.Tuning.SummonBodySlamImpactFrame && pendingAttackTarget != null && !pendingAttackTarget.HasArrived && pendingAttackTarget.Enemy != null && !pendingAttackTarget.Enemy.IsDead && (isScout || !isBasicSlime || IsAtContactDistance(pendingAttackTarget, contactPosition))) { bool hit = owner.TryAttack(pendingAttackTarget); attackDamageApplied = true; if (hit) owner.ShowSummonAttackEffect(body.position, pendingAttackTarget); } return; } if (returningToAnchor) { Vector2 previous = body.position; Vector2 next = MoveBody(Vector2.MoveTowards(previous, anchor, MoveSpeed * Time.fixedDeltaTime)); isWalking = (next - previous).sqrMagnitude > 0.000001f; if ((next - anchor).sqrMagnitude < 0.01f || (next - previous).sqrMagnitude < 0.000001f) { returningToAnchor = false; } return; } DefenseRouteAgent target = owner.CommandTarget; if (target == null || target.HasArrived || target.Enemy == null || target.Enemy.IsDead) { Vector2 previous = body.position; Vector2 next = MoveBody(Vector2.MoveTowards(previous, anchor, MoveSpeed * Time.fixedDeltaTime)); isWalking = (next - previous).sqrMagnitude > 0.000001f; return; } Vector2 targetPosition = target.transform.position; spriteRenderer.flipX = targetPosition.x < transform.position.x; float attackRange = controller.Tuning.SummonAttackRange * (isScout ? 2.5f : 1f); Vector2 previousPosition = body.position; Vector2 toTarget = targetPosition - previousPosition; Vector2 stopPosition = targetPosition - toTarget.normalized * attackRange * 0.8f; Vector2 nextPosition = previousPosition; if (toTarget.magnitude > attackRange * 0.8f) { nextPosition = MoveBody(Vector2.MoveTowards(previousPosition, stopPosition, MoveSpeed * Time.fixedDeltaTime)); } isWalking = (nextPosition - previousPosition).sqrMagnitude > 0.000001f; if (attackCooldown <= 0f && (targetPosition - nextPosition).sqrMagnitude <= attackRange * attackRange) { pendingAttackTarget = target; attackDamageApplied = false; returningToAnchor = isBasicSlime; PlayAttackSprite(); attackCooldown = controller.Tuning.SummonAttackInterval / (1f + 0.15f * (owner.Tier - 1)) * controller.GetTowerAttackIntervalMultiplier(owner); } } private bool IsAtContactDistance(DefenseRouteAgent target, Vector2 summonCenter) { Collider2D targetCollider = target.Enemy.GetComponent(); if (targetCollider == null) { return Vector2.Distance(summonCenter, target.transform.position) <= controller.Tuning.SummonBodySlamContactMargin; } Vector2 targetSurface = targetCollider.ClosestPoint(summonCenter); float bodyRadius = bodyCollider.bounds.extents.x; float surfaceGap = Vector2.Distance(summonCenter, targetSurface) - bodyRadius; return surfaceGap <= controller.Tuning.SummonBodySlamContactMargin; } private void IgnorePlayerCollisionPairs() { if (!IsMinion) return; PlayerController player = FindAnyObjectByType(); CharacterModel playerModel = player != null ? player.GetComponent() : null; if (player == null || playerModel == null || playerModel.IsMinion) return; Collider2D[] playerColliders = player.GetComponentsInChildren(true); for (int i = 0; i < playerColliders.Length; i++) { Collider2D playerCollider = playerColliders[i]; if (playerCollider != null && playerCollider != bodyCollider) { Physics2D.IgnoreCollision(bodyCollider, playerCollider, true); } } } private Vector2 MoveBody(Vector2 destination) { Vector2 delta = destination - body.position; float distance = delta.magnitude; if (distance <= 0.0001f) return body.position; Vector2 direction = delta / distance; ContactFilter2D filter = ContactFilter2D.noFilter; filter.useTriggers = false; int hitCount = bodyCollider.Cast(direction, filter, MovementHits, distance); for (int i = 0; i < hitCount; i++) { Collider2D hit = MovementHits[i].collider; if (hit == null || hit.transform == transform) continue; if (IsMinion && hit.GetComponentInParent() != null) continue; if (MovementHits[i].distance <= 0.001f && Vector2.Dot(direction, body.position - (Vector2)hit.bounds.center) >= 0f) continue; distance = Mathf.Min(distance, Mathf.Max(0f, MovementHits[i].distance - 0.01f)); } Vector2 next = body.position + direction * distance; body.MovePosition(next); return next; } public void RetireForReplacement() { if (bodyCollider != null) bodyCollider.enabled = false; if (spriteRenderer != null) spriteRenderer.enabled = false; if (body != null) body.simulated = false; enabled = false; owner = null; gameObject.SetActive(false); } public bool TryTakeDamage(float damage, Vector2 knockbackDirection) { if (!IsOperational || damage <= 0f) return false; float applied = DamageCalculator.FinalizeDamage(CalculateIncomingDamage(damage)); if (applied <= 0f) return false; currentHealth = Mathf.Max(0f, currentHealth - applied); if (currentHealth <= 0f) { dead = true; bodyCollider.enabled = false; spriteRenderer.enabled = false; owner.NotifySummonUnitDied(this); } return true; } private void PlayAttackSprite() { if (attackFrames == null || attackFrames.Length == 0) { return; } playingAttack = true; spriteFrameTime = 0f; spriteFrame = 0; spriteRenderer.sprite = attackFrames[0]; } private void UpdateSpriteAnimation() { Sprite[] frames = playingAttack ? attackFrames : isWalking ? walkFrames : idleFrames; string key = playingAttack ? attackKey : isWalking ? walkKey : idleKey; if (spriteRenderer == null || frames == null || frames.Length == 0) { return; } spriteFrameTime += Time.deltaTime; while (spriteFrameTime >= DefenseTowerArt.FrameSeconds(key, spriteFrame)) { spriteFrameTime -= DefenseTowerArt.FrameSeconds(key, spriteFrame); spriteFrame++; if (spriteFrame >= frames.Length) { if (playingAttack) { playingAttack = false; spriteFrame = 0; frames = idleFrames; key = idleKey; } else { spriteFrame = 0; } } spriteRenderer.sprite = frames[spriteFrame]; } } } }