Files
tiny_tackle_heroes/Assets/_Project/Scripts/Defense/DefenseSummonUnit.cs
T
2026-09-27 21:08:05 +09:00

329 lines
14 KiB
C#

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<CircleCollider2D>();
bodyCollider.radius = owner.SummonUnitCollisionRadius;
body = gameObject.AddComponent<Rigidbody2D>();
body.bodyType = RigidbodyType2D.Kinematic;
body.gravityScale = 0f;
body.freezeRotation = true;
spriteRenderer = gameObject.AddComponent<SpriteRenderer>();
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<Collider2D>();
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<PlayerController>();
CharacterModel playerModel = player != null
? player.GetComponent<CharacterModel>() : null;
if (player == null || playerModel == null || playerModel.IsMinion) return;
Collider2D[] playerColliders = player.GetComponentsInChildren<Collider2D>(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<PlayerController>() != 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];
}
}
}
}