329 lines
14 KiB
C#
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];
|
|
}
|
|
}
|
|
}
|
|
} |