1364 lines
61 KiB
C#
1364 lines
61 KiB
C#
using BumpCombat.Constants;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace BumpCombat.Defense
|
|
{
|
|
[DisallowMultipleComponent]
|
|
public sealed class DefenseTower : MonoBehaviour
|
|
{
|
|
private DefensePrototypeController owner;
|
|
private DefenseConstants tuning;
|
|
private float attackCooldown;
|
|
private TextMesh label;
|
|
private LineRenderer effectRange;
|
|
private static Material effectMaterial;
|
|
private int summonCount;
|
|
private int nextSummonIndex;
|
|
private int targetSummonCount;
|
|
private float summonRespawnRemaining;
|
|
private readonly Stack<int> freeSummonSlots = new();
|
|
private readonly Dictionary<int, DefenseSummonUnit> deadSummons = new();
|
|
private readonly HashSet<GameObject> attackEffectRoots = new();
|
|
private GameObject flameBeamEffect;
|
|
private SpriteRenderer flameBeamRenderer;
|
|
private DefenseRouteAgent flameBeamTarget;
|
|
private float flameBeamStreakStartedAt;
|
|
private float flameBeamLastSuccessfulAttackAt;
|
|
private bool flameBeamStreakActive;
|
|
private SpriteRenderer bodyRenderer;
|
|
private SpriteRenderer supportAuraRenderer;
|
|
private SpriteRenderer supportRangeRenderer;
|
|
private Sprite[] supportRangeFrames;
|
|
private float supportRangeFrameTime;
|
|
private int supportRangeFrame;
|
|
private Sprite[] bodyIdleFrames;
|
|
private Sprite[] bodyAttackFrames;
|
|
private string bodyIdleKey;
|
|
private string bodyAttackKey;
|
|
private float bodyFrameTime;
|
|
private int bodyFrame;
|
|
private bool bodyAttacking;
|
|
private string supportAuraKey;
|
|
private Sprite[] supportAuraFrames;
|
|
|
|
public DefenseTowerKind Kind { get; private set; }
|
|
public DefenseTowerBranch Branch { get; private set; }
|
|
public int Tier { get; private set; } = 1;
|
|
public int PadIndex { get; private set; }
|
|
public bool IsOperational => isActiveAndEnabled
|
|
&& owner != null
|
|
&& owner.CanOperateTowers;
|
|
public bool IsDamaging => Kind != DefenseTowerKind.Empowering;
|
|
public Color HitFeedbackColor => GetTowerColor();
|
|
public float SummonUnitMaximumHealth => tuning != null ? tuning.SummonUnitMaximumHealth : 1f;
|
|
public float SummonUnitCollisionRadius => tuning != null ? tuning.SummonUnitCollisionRadius : 0.32f;
|
|
public DefenseRouteAgent CommandTarget { get; private set; }
|
|
|
|
public void Configure(
|
|
DefensePrototypeController controller,
|
|
DefenseTowerKind kind,
|
|
int padIndex,
|
|
DefenseConstants constants)
|
|
{
|
|
owner = controller;
|
|
Kind = kind;
|
|
PadIndex = padIndex;
|
|
tuning = constants;
|
|
Tier = 1;
|
|
Branch = DefenseTowerBranch.None;
|
|
summonCount = 0;
|
|
targetSummonCount = Kind == DefenseTowerKind.Summon ? tuning.SummonUnitCount : 0;
|
|
CreateVisuals();
|
|
if (Kind == DefenseTowerKind.Summon)
|
|
{
|
|
AddSummonUnits(tuning.SummonUnitCount);
|
|
}
|
|
}
|
|
|
|
public bool Upgrade(DefenseTowerBranch branch)
|
|
{
|
|
if (Tier >= 4 || (Tier < 3 && branch != DefenseTowerBranch.None)
|
|
|| (Tier == 3 && branch != DefenseTowerBranch.First
|
|
&& branch != DefenseTowerBranch.Second))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Tier++;
|
|
if (Tier == 4)
|
|
{
|
|
Branch = branch;
|
|
}
|
|
|
|
if (Kind == DefenseTowerKind.Summon)
|
|
{
|
|
int upgradedSummonCount = Tier == 4 && Branch == DefenseTowerBranch.First
|
|
? 1
|
|
: targetSummonCount + (Tier == 4 ? 2 : 1);
|
|
ReplaceSummonUnits(upgradedSummonCount);
|
|
}
|
|
|
|
UpdateLabel();
|
|
UpdateEffectRange();
|
|
UpdateBodyArt();
|
|
UpdateSupportAura();
|
|
foreach (DefenseSummonUnit unit in GetComponentsInChildren<DefenseSummonUnit>())
|
|
{
|
|
unit.RefreshArt();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
if (supportAuraRenderer != null)
|
|
{
|
|
supportAuraRenderer.enabled = false;
|
|
}
|
|
ClearAttackEffects();
|
|
EndFlamethrowerBeam(immediate: true, resetStreak: true);
|
|
CommandTarget = null;
|
|
}
|
|
|
|
private void TrackAttackEffect(GameObject effect)
|
|
{
|
|
if (effect == null) return;
|
|
attackEffectRoots.Add(effect);
|
|
DefenseSpriteSequence[] sequences =
|
|
effect.GetComponentsInChildren<DefenseSpriteSequence>(true);
|
|
if (sequences.Length == 0)
|
|
{
|
|
DestroyAttackEffectWhenComplete(effect);
|
|
return;
|
|
}
|
|
|
|
foreach (DefenseSpriteSequence sequence in sequences)
|
|
{
|
|
TrackAttackSequence(effect, sequence);
|
|
}
|
|
}
|
|
|
|
private void TrackAttackSequence(GameObject effect, DefenseSpriteSequence sequence)
|
|
{
|
|
if (effect == null || sequence == null) return;
|
|
attackEffectRoots.Add(effect);
|
|
sequence.SetCompletionCallback(() => DestroyAttackEffectWhenComplete(effect));
|
|
}
|
|
|
|
private void DestroyAttackEffectWhenComplete(GameObject effect)
|
|
{
|
|
if (effect == null || !attackEffectRoots.Contains(effect)) return;
|
|
foreach (DefenseSpriteSequence sequence
|
|
in effect.GetComponentsInChildren<DefenseSpriteSequence>(true))
|
|
{
|
|
if (!sequence.IsFinished) return;
|
|
}
|
|
|
|
attackEffectRoots.Remove(effect);
|
|
Destroy(effect);
|
|
}
|
|
|
|
private void ClearAttackEffects()
|
|
{
|
|
foreach (GameObject effect in attackEffectRoots)
|
|
{
|
|
if (effect != null) Destroy(effect);
|
|
}
|
|
attackEffectRoots.Clear();
|
|
}
|
|
|
|
private bool IsFireT4Flamethrower => Kind == DefenseTowerKind.Direct
|
|
&& Tier == 4 && Branch == DefenseTowerBranch.Second;
|
|
|
|
private bool IsFlamethrowerTargetValid(DefenseRouteAgent target)
|
|
{
|
|
return target != null && !target.HasArrived && target.Enemy != null
|
|
&& !target.Enemy.IsDead
|
|
&& Vector2.Distance(transform.position, target.transform.position)
|
|
<= GetEffectRange();
|
|
}
|
|
|
|
private void UpdateFlamethrowerBeam()
|
|
{
|
|
if (flameBeamStreakActive
|
|
&& Time.time - flameBeamLastSuccessfulAttackAt
|
|
>= tuning.FireT4BeamResetAfterSeconds)
|
|
{
|
|
EndFlamethrowerBeam();
|
|
ResetFlamethrowerStreak();
|
|
return;
|
|
}
|
|
if (flameBeamEffect == null) return;
|
|
if (!IsOperational || !IsFireT4Flamethrower
|
|
|| !IsFlamethrowerTargetValid(flameBeamTarget))
|
|
{
|
|
EndFlamethrowerBeam();
|
|
return;
|
|
}
|
|
Sprite[] frames = DefenseTowerArt.Load("Effects/Fire/Eyeball Monster_Beam-Clean");
|
|
if (Time.time - flameBeamStreakStartedAt >= DefenseTowerArtBounds.FireBeamIgnitionSeconds
|
|
&& flameBeamRenderer.sprite != frames[1])
|
|
flameBeamRenderer.sprite = frames[1];
|
|
FitFlamethrowerBeam(GetTargetHitCenter(flameBeamTarget, flameBeamTarget.transform.position));
|
|
}
|
|
|
|
private bool PulseFlamethrower(DefenseRouteAgent target, float tierScale)
|
|
{
|
|
float now = Time.time;
|
|
if (flameBeamStreakActive
|
|
&& now - flameBeamLastSuccessfulAttackAt >= tuning.FireT4BeamResetAfterSeconds)
|
|
{
|
|
EndFlamethrowerBeam();
|
|
ResetFlamethrowerStreak();
|
|
}
|
|
if (flameBeamEffect == null || flameBeamTarget != target)
|
|
{
|
|
EndFlamethrowerBeam(immediate: true);
|
|
Sprite[] frames = DefenseTowerArt.Load("Effects/Fire/Eyeball Monster_Beam-Clean");
|
|
flameBeamEffect = new GameObject("Fire T4 Sustained Beam");
|
|
flameBeamEffect.transform.SetParent(transform.parent, false);
|
|
GameObject beamObject = new("Beam Sprite");
|
|
beamObject.transform.SetParent(flameBeamEffect.transform, false);
|
|
flameBeamRenderer = beamObject.AddComponent<SpriteRenderer>();
|
|
flameBeamRenderer.sortingOrder = 14;
|
|
flameBeamRenderer.sprite = frames[0];
|
|
flameBeamTarget = target;
|
|
if (!flameBeamStreakActive) flameBeamStreakStartedAt = now;
|
|
SetHitRendererSorting(flameBeamRenderer, target);
|
|
FitFlamethrowerBeam(GetTargetHitCenter(target, target.transform.position));
|
|
}
|
|
float elapsed = Mathf.Max(0f, now - flameBeamStreakStartedAt);
|
|
float streakMultiplier = elapsed >= tuning.FireT4BeamMaximumTierSeconds
|
|
? tuning.FireT4BeamMaximumMultiplier
|
|
: elapsed >= tuning.FireT4BeamSecondTierSeconds
|
|
? tuning.FireT4BeamSecondTierMultiplier : 1f;
|
|
bool hit = owner.TryApplyTowerDamage(this, target,
|
|
tuning.DirectTowerDamage * tierScale
|
|
* tuning.FireT4BeamBaseDamageMultiplier * streakMultiplier);
|
|
if (!hit)
|
|
{
|
|
EndFlamethrowerBeam();
|
|
return false;
|
|
}
|
|
flameBeamStreakActive = true;
|
|
flameBeamLastSuccessfulAttackAt = now;
|
|
PlayBodyAttack(target.transform.position.x < transform.position.x);
|
|
FitFlamethrowerBeam(GetTargetHitCenter(target, target.transform.position));
|
|
return true;
|
|
}
|
|
|
|
private void FitFlamethrowerBeam(Vector2 end)
|
|
{
|
|
if (flameBeamRenderer == null || flameBeamRenderer.sprite == null) return;
|
|
Vector2 start = GetStaffTipWorldPosition();
|
|
Vector2 direction = end - start;
|
|
float distance = direction.magnitude;
|
|
float ppu = flameBeamRenderer.sprite.pixelsPerUnit;
|
|
float scaleX = distance * ppu
|
|
/ (DefenseTowerArtBounds.FireBeamEndX - DefenseTowerArtBounds.FireBeamStartX);
|
|
float angle = distance > 0f
|
|
? Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg : 0f;
|
|
Transform beam = flameBeamRenderer.transform;
|
|
beam.rotation = Quaternion.Euler(0f, 0f, angle);
|
|
float elapsed = flameBeamStreakActive
|
|
? Mathf.Max(0f, Time.time - flameBeamStreakStartedAt) : 0f;
|
|
float widthMultiplier = elapsed >= tuning.FireT4BeamMaximumTierSeconds
|
|
? tuning.FireT4BeamMaximumWidthMultiplier
|
|
: elapsed >= tuning.FireT4BeamSecondTierSeconds
|
|
? tuning.FireT4BeamSecondTierWidthMultiplier
|
|
: DefenseTowerArtBounds.FireT4BeamBaseWidthMultiplier;
|
|
beam.localScale = new Vector3(scaleX, widthMultiplier, 1f);
|
|
Vector2 localStart = new(
|
|
(DefenseTowerArtBounds.FireBeamStartX - flameBeamRenderer.sprite.pivot.x) / ppu,
|
|
(flameBeamRenderer.sprite.rect.height - DefenseTowerArtBounds.FireBeamCenterY
|
|
- flameBeamRenderer.sprite.pivot.y) / ppu);
|
|
beam.position = start - (Vector2)beam.right * (localStart.x * scaleX)
|
|
- (Vector2)beam.up * (localStart.y * widthMultiplier);
|
|
}
|
|
|
|
private Vector2 GetStaffTipWorldPosition()
|
|
{
|
|
if (bodyRenderer == null || bodyRenderer.sprite == null) return transform.position;
|
|
Vector2 tipPixels = DefenseTowerArtBounds.StaffTipPixels(bodyAttacking, bodyFrame);
|
|
Vector2 localPixels = tipPixels - bodyRenderer.sprite.pivot;
|
|
localPixels.y = bodyRenderer.sprite.rect.height - tipPixels.y - bodyRenderer.sprite.pivot.y;
|
|
if (bodyRenderer.flipX) localPixels.x = -localPixels.x;
|
|
return bodyRenderer.transform.TransformPoint(localPixels / bodyRenderer.sprite.pixelsPerUnit);
|
|
}
|
|
|
|
private void ResetFlamethrowerStreak()
|
|
{
|
|
flameBeamStreakActive = false;
|
|
flameBeamStreakStartedAt = 0f;
|
|
flameBeamLastSuccessfulAttackAt = 0f;
|
|
}
|
|
|
|
private void EndFlamethrowerBeam(bool immediate = false, bool resetStreak = false)
|
|
{
|
|
if (flameBeamEffect != null)
|
|
{
|
|
Sprite[] frames = DefenseTowerArt.Load("Effects/Fire/Eyeball Monster_Beam-Clean");
|
|
if (flameBeamRenderer != null && frames.Length >= 3)
|
|
flameBeamRenderer.sprite = frames[2];
|
|
if (immediate) Destroy(flameBeamEffect);
|
|
else Destroy(flameBeamEffect, DefenseTowerArtBounds.FireBeamExtinguishSeconds);
|
|
flameBeamEffect = null;
|
|
flameBeamRenderer = null;
|
|
flameBeamTarget = null;
|
|
}
|
|
if (resetStreak) ResetFlamethrowerStreak();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
BumpCombat.Core.RunManager runManager = BumpCombat.Core.RunManager.Instance;
|
|
if (runManager != null
|
|
&& (runManager.IsGameOver || runManager.IsRunCleared || runManager.IsTitleScreen))
|
|
{
|
|
ClearAttackEffects();
|
|
EndFlamethrowerBeam(immediate: true, resetStreak: true);
|
|
}
|
|
UpdateBodyAnimation();
|
|
UpdateFlamethrowerBeam();
|
|
if (Kind == DefenseTowerKind.Summon && IsOperational && summonCount < targetSummonCount)
|
|
{
|
|
summonRespawnRemaining -= Time.deltaTime;
|
|
if (summonRespawnRemaining <= 0f)
|
|
{
|
|
AddSummonUnits(1);
|
|
summonRespawnRemaining = tuning.SummonUnitRespawnSeconds;
|
|
}
|
|
}
|
|
if (Kind == DefenseTowerKind.Summon && IsOperational)
|
|
{
|
|
CommandTarget = owner.FindNearestTarget(
|
|
transform.position, tuning.SummonAreaRadius, Kind);
|
|
if (CommandTarget != null && bodyRenderer != null)
|
|
{
|
|
bodyRenderer.flipX =
|
|
CommandTarget.transform.position.x < transform.position.x;
|
|
}
|
|
}
|
|
if (Kind == DefenseTowerKind.Empowering)
|
|
UpdateEffectRange();
|
|
else if (effectRange != null)
|
|
effectRange.transform.localPosition = DefenseTowerArtBounds.CharacterShadowOffset(
|
|
Kind, bodyRenderer != null && bodyRenderer.flipX);
|
|
UpdateSupportAura();
|
|
UpdateSupportRangeAnimation();
|
|
if (!IsOperational || Kind == DefenseTowerKind.Summon
|
|
|| Kind == DefenseTowerKind.Empowering)
|
|
{
|
|
return;
|
|
}
|
|
|
|
attackCooldown -= Time.deltaTime;
|
|
if (attackCooldown > 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (owner.TryAttackFromTower(this))
|
|
{
|
|
float interval = Kind switch
|
|
{
|
|
DefenseTowerKind.Direct => tuning.DirectTowerAttackInterval,
|
|
DefenseTowerKind.Lightning => tuning.LightningInterval,
|
|
DefenseTowerKind.Cold => tuning.ColdInterval,
|
|
_ => tuning.SeismicInterval,
|
|
};
|
|
if (Tier == 4 && Kind == DefenseTowerKind.Direct
|
|
&& Branch == DefenseTowerBranch.Second)
|
|
{
|
|
interval *= 0.4f;
|
|
}
|
|
|
|
attackCooldown = Mathf.Max(0.05f,
|
|
interval / (1f + 0.15f * (Tier - 1))
|
|
* owner.GetTowerAttackIntervalMultiplier(this));
|
|
}
|
|
}
|
|
|
|
public bool TryPulse()
|
|
{
|
|
if (!IsOperational || Kind == DefenseTowerKind.Empowering
|
|
|| Kind == DefenseTowerKind.Summon)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
float range = GetEffectRange();
|
|
DefenseRouteAgent target;
|
|
if (IsFireT4Flamethrower)
|
|
{
|
|
if (IsFlamethrowerTargetValid(flameBeamTarget))
|
|
target = flameBeamTarget;
|
|
else
|
|
{
|
|
EndFlamethrowerBeam();
|
|
target = owner.FindNearestTarget(transform.position, range, Kind);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
target = owner.FindNearestTarget(
|
|
transform.position, range, Kind,
|
|
Kind == DefenseTowerKind.Cold || Kind == DefenseTowerKind.Seismic);
|
|
}
|
|
if (target == null)
|
|
{
|
|
if (IsFireT4Flamethrower) EndFlamethrowerBeam();
|
|
return false;
|
|
}
|
|
|
|
Vector2 center = target.transform.position;
|
|
Vector2 lightningVisualCenter = DefenseTowerArtBounds.EnemyShadowWorldPosition(
|
|
target.Enemy, target.transform.position);
|
|
float scale = 1f + tuning.TowerDamageBonusPerTier * (Tier - 1);
|
|
if (IsFireT4Flamethrower) return PulseFlamethrower(target, scale);
|
|
float impactRadius = 0f;
|
|
bool applied;
|
|
System.Action projectileImpact = null;
|
|
switch (Kind)
|
|
{
|
|
case DefenseTowerKind.Direct:
|
|
if (Tier == 4)
|
|
{
|
|
impactRadius = 1.8f;
|
|
applied = true;
|
|
projectileImpact = () =>
|
|
{
|
|
if (target != null && !target.HasArrived && target.Enemy != null
|
|
&& !target.Enemy.IsDead)
|
|
{
|
|
AttackArea(target.transform.position, impactRadius,
|
|
tuning.DirectTowerDamage * scale * 1.6f);
|
|
}
|
|
};
|
|
}
|
|
else
|
|
{
|
|
applied = true;
|
|
projectileImpact = () => owner.TryApplyTowerDamage(this, target,
|
|
tuning.DirectTowerDamage * scale);
|
|
}
|
|
break;
|
|
|
|
case DefenseTowerKind.Lightning:
|
|
impactRadius = Tier == 4
|
|
? (Branch == DefenseTowerBranch.First ? 3f : 2.4f)
|
|
: tuning.LightningRadius;
|
|
applied = AttackArea(center, impactRadius,
|
|
tuning.LightningDamage * scale
|
|
* (Tier == 4 && Branch == DefenseTowerBranch.Second ? 1.4f : 1f),
|
|
Tier == 4 && Branch == DefenseTowerBranch.First ? 3 : int.MaxValue);
|
|
break;
|
|
|
|
case DefenseTowerKind.Cold:
|
|
if (Tier == 4 && Branch == DefenseTowerBranch.Second)
|
|
{
|
|
impactRadius = 1.65f;
|
|
applied = true;
|
|
}
|
|
else
|
|
{
|
|
applied = true;
|
|
projectileImpact = () =>
|
|
{
|
|
if (target == null || target.HasArrived || target.Enemy == null
|
|
|| target.Enemy.IsDead) return;
|
|
owner.TryApplyTowerSlow(this, target,
|
|
tuning.ColdSlowSeconds * (1f + 0.25f * (Tier - 1)),
|
|
tuning.ColdSlowFraction);
|
|
owner.TryApplyTowerDamage(this, target,
|
|
tuning.ColdDamage * scale * (Tier == 4 ? 1.8f : 1f));
|
|
if (Tier == 4) owner.TryApplyTowerStun(this, target, 0.3f);
|
|
};
|
|
}
|
|
break;
|
|
|
|
case DefenseTowerKind.Seismic:
|
|
impactRadius = Tier == 4
|
|
? (Branch == DefenseTowerBranch.Second ? 2.2f : 1.8f)
|
|
: tuning.SeismicRadius;
|
|
applied = Tier == 4 && Branch == DefenseTowerBranch.Second
|
|
? AttackArea(center, impactRadius,
|
|
tuning.SeismicDamage * scale, int.MaxValue, 2f, 0.5f)
|
|
: AttackArea(center, impactRadius,
|
|
tuning.SeismicDamage * scale, int.MaxValue,
|
|
0f, 0f,
|
|
tuning.SeismicStunSeconds * (Tier == 4 ? 2f : 1f));
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if (applied)
|
|
{
|
|
PlayBodyAttack(center.x < transform.position.x);
|
|
bool shadowCenteredCircle = Kind == DefenseTowerKind.Seismic || IsBlizzard;
|
|
ShowAttackEffect(center, impactRadius,
|
|
visualCenter: shadowCenteredCircle ? lightningVisualCenter : null,
|
|
projectileImpact: projectileImpact,
|
|
projectileTarget: target,
|
|
damageScale: scale);
|
|
}
|
|
return applied;
|
|
}
|
|
|
|
private bool AttackArea(
|
|
Vector2 center,
|
|
float radius,
|
|
float damage,
|
|
int maxTargets = int.MaxValue,
|
|
float slowSeconds = 0f,
|
|
float slowFraction = 0f,
|
|
float stunSeconds = 0f)
|
|
{
|
|
bool applied = false;
|
|
int count = 0;
|
|
foreach (DefenseRouteAgent agent in owner.GetTargetsInRange(center, radius, Kind))
|
|
{
|
|
if (count++ >= maxTargets)
|
|
{
|
|
break;
|
|
}
|
|
|
|
bool damaged = owner.TryApplyTowerDamage(this, agent, damage);
|
|
bool slowed = slowSeconds > 0f
|
|
&& owner.TryApplyTowerSlow(this, agent, slowSeconds, slowFraction);
|
|
bool stunned = stunSeconds > 0f
|
|
&& owner.TryApplyTowerStun(this, agent, stunSeconds);
|
|
applied |= damaged || slowed || stunned;
|
|
}
|
|
|
|
return applied;
|
|
}
|
|
|
|
public bool TryAttack(DefenseRouteAgent target)
|
|
{
|
|
if (target == null || owner == null || !IsOperational)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
float damage = Kind == DefenseTowerKind.Summon
|
|
? tuning.SummonDamage : tuning.DirectTowerDamage;
|
|
damage *= 1f + tuning.TowerDamageBonusPerTier * (Tier - 1);
|
|
if (Tier == 4 && Kind == DefenseTowerKind.Summon
|
|
&& Branch == DefenseTowerBranch.First)
|
|
{
|
|
damage *= 1.8f;
|
|
}
|
|
|
|
if (Tier == 4 && Kind == DefenseTowerKind.Summon
|
|
&& Branch == DefenseTowerBranch.Second)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool hit = owner.TryApplyTowerDamage(this, target, damage);
|
|
if (hit && Tier == 4 && Kind == DefenseTowerKind.Summon
|
|
&& Branch == DefenseTowerBranch.First)
|
|
{
|
|
owner.TryApplyTowerStun(this, target, 0.3f);
|
|
}
|
|
|
|
return hit;
|
|
}
|
|
|
|
public void ShowSummonAttackEffect(Vector2 origin, DefenseRouteAgent target)
|
|
{
|
|
if (!CanApplyProjectileImpact(target) || Kind != DefenseTowerKind.Summon) return;
|
|
Vector2 center = target.transform.position;
|
|
if (Tier == 4 && Branch == DefenseTowerBranch.Second)
|
|
{
|
|
GameObject effect = new("Summon Attack Effect");
|
|
effect.transform.SetParent(transform.parent, false);
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
Vector2 position = Vector2.Lerp(origin, center, (i + 1) / 5f);
|
|
CreateAnimatedArt(effect.transform, $"Shot Sprite {i + 1}",
|
|
"Summons/LavaSlime/GroundSpike-Segment", position,
|
|
Vector2.one, 0f, 14, startDelay: i * 0.12f);
|
|
}
|
|
SpriteRenderer projectile = CreateAnimatedArt(effect.transform, "Shot Sprite 5",
|
|
"Summons/LavaSlime/GroundSpike-Segment", origin,
|
|
Vector2.one, 0f, 14, startDelay: 0.48f);
|
|
projectile.GetComponent<DefenseSpriteSequence>().Travel(
|
|
origin, center, 0.16f,
|
|
() => ApplyProjectileImpact(effect, target, null, 0f, () =>
|
|
{
|
|
float damage = tuning.SummonDamage
|
|
* (1f + tuning.TowerDamageBonusPerTier * (Tier - 1));
|
|
owner.TryApplyTowerDamage(this, target, damage);
|
|
}),
|
|
() => target != null && !target.HasArrived && target.Enemy != null
|
|
? (Vector2)target.transform.position : center,
|
|
() => IsOperational);
|
|
TrackAttackEffect(effect);
|
|
return;
|
|
}
|
|
|
|
ShowAttackEffect(center, 0f, source: origin, projectileTarget: target);
|
|
}
|
|
private float GetEffectRange()
|
|
{
|
|
if (Kind == DefenseTowerKind.Summon)
|
|
{
|
|
return tuning.SummonAreaRadius;
|
|
}
|
|
|
|
if (Kind == DefenseTowerKind.Empowering)
|
|
{
|
|
return tuning.SupportRadius;
|
|
}
|
|
return Kind == DefenseTowerKind.Direct && Tier == 4
|
|
&& Branch == DefenseTowerBranch.Second
|
|
? tuning.TowerRange * 0.5f : tuning.TowerRange;
|
|
}
|
|
|
|
private void CreateEffectRange()
|
|
{
|
|
GameObject rangeObject = new("Effect Range");
|
|
rangeObject.transform.SetParent(transform, false);
|
|
|
|
if (Kind == DefenseTowerKind.Empowering)
|
|
{
|
|
supportRangeRenderer = rangeObject.AddComponent<SpriteRenderer>();
|
|
supportRangeRenderer.sortingOrder = 3;
|
|
supportRangeFrames = DefenseTowerArt.Load("SupportRange/T1-Aura");
|
|
if (supportRangeFrames.Length > 0)
|
|
{
|
|
supportRangeRenderer.sprite = supportRangeFrames[0];
|
|
supportRangeRenderer.color = new Color(1f, 1f, 1f, tuning.SupportRangeAlpha);
|
|
}
|
|
UpdateEffectRange();
|
|
return;
|
|
}
|
|
effectRange = rangeObject.AddComponent<LineRenderer>();
|
|
effectRange.useWorldSpace = false;
|
|
effectRange.loop = true;
|
|
effectRange.positionCount = 48;
|
|
effectRange.sharedMaterial = GetEffectMaterial();
|
|
effectRange.sortingOrder = 3;
|
|
Color color = GetTowerColor();
|
|
color.a = Kind == DefenseTowerKind.Empowering ? 0.35f
|
|
: IsBlizzard ? DefenseTowerArtBounds.BlizzardRangeAlpha : 0.22f;
|
|
effectRange.startColor = color;
|
|
effectRange.endColor = color;
|
|
UpdateEffectRange();
|
|
}
|
|
|
|
private void UpdateEffectRange()
|
|
{
|
|
if (Kind == DefenseTowerKind.Empowering && supportRangeRenderer != null)
|
|
{
|
|
Sprite sprite = supportRangeRenderer.sprite;
|
|
if (sprite == null) return;
|
|
RectInt bounds = DefenseConstants.SupportRangeAlphaBounds[Mathf.Clamp(supportRangeFrame, 0, DefenseConstants.SupportRangeAlphaBounds.Length - 1)];
|
|
Vector2 lossyScale = transform.lossyScale;
|
|
Vector2 localScale = new(
|
|
tuning.SupportRadius * 2f * sprite.pixelsPerUnit / (bounds.width * Mathf.Max(0.001f, lossyScale.x)),
|
|
tuning.SupportRadius * 2f * sprite.pixelsPerUnit / (bounds.height * Mathf.Max(0.001f, lossyScale.y)));
|
|
Color[] colors = tuning.SupportTierColors;
|
|
Color sourceColor = colors != null && colors.Length > 0
|
|
? colors[Mathf.Clamp(Tier - 1, 0, colors.Length - 1)]
|
|
: new Color32(151, 169, 119, 102);
|
|
sourceColor.a *= tuning.SupportRangeAlpha;
|
|
supportRangeRenderer.color = sourceColor;
|
|
Vector2 alphaCenterBottomLeft = new(
|
|
bounds.x + bounds.width * 0.5f,
|
|
100f - bounds.y - bounds.height * 0.5f);
|
|
Vector2 correctionLocal = Vector2.Scale(
|
|
(alphaCenterBottomLeft - sprite.pivot) / sprite.pixelsPerUnit, localScale);
|
|
Vector2 shadow = DefenseTowerArtBounds.CharacterShadowOffset(
|
|
Kind, bodyRenderer != null && bodyRenderer.flipX);
|
|
supportRangeRenderer.transform.localPosition = shadow - correctionLocal;
|
|
supportRangeRenderer.transform.localScale = new Vector3(localScale.x, localScale.y, 1f);
|
|
return;
|
|
}
|
|
if (effectRange == null)
|
|
{
|
|
return;
|
|
}
|
|
Color rangeColor = GetTowerColor();
|
|
rangeColor.a = IsBlizzard
|
|
? DefenseTowerArtBounds.BlizzardRangeAlpha : 0.22f;
|
|
effectRange.startColor = rangeColor;
|
|
effectRange.endColor = rangeColor;
|
|
effectRange.transform.localPosition = DefenseTowerArtBounds.CharacterShadowOffset(
|
|
Kind, bodyRenderer != null && bodyRenderer.flipX);
|
|
float worldScale = Mathf.Max(0.001f, transform.lossyScale.x);
|
|
float radius = GetEffectRange() / worldScale;
|
|
effectRange.startWidth = 0.055f / worldScale;
|
|
effectRange.endWidth = effectRange.startWidth;
|
|
for (int i = 0; i < effectRange.positionCount; i++)
|
|
{
|
|
float angle = i * Mathf.PI * 2f / effectRange.positionCount;
|
|
effectRange.SetPosition(i, new Vector3(
|
|
Mathf.Cos(angle) * radius,
|
|
Mathf.Sin(angle) * radius, 0f));
|
|
}
|
|
}
|
|
|
|
private void ShowAttackEffect(
|
|
Vector2 center, float radius, Vector2? source = null,
|
|
Vector2? visualCenter = null, System.Action projectileImpact = null,
|
|
DefenseRouteAgent projectileTarget = null, float damageScale = 1f)
|
|
{
|
|
Vector2 origin = source ?? (Vector2)transform.position;
|
|
Vector2 direction = center - origin;
|
|
GameObject effect = new($"{Kind} Attack Effect");
|
|
effect.transform.SetParent(transform.parent, false);
|
|
bool shadowCenteredCircle = Kind == DefenseTowerKind.Seismic || IsBlizzard;
|
|
Vector2 effectCenter = shadowCenteredCircle ? visualCenter ?? center : center;
|
|
effect.transform.position = effectCenter;
|
|
Color color = GetTowerColor();
|
|
color.a = 0.9f;
|
|
|
|
switch (Kind)
|
|
{
|
|
case DefenseTowerKind.Direct:
|
|
ShowFireArt(effect.transform, origin, center, direction, projectileImpact, projectileTarget);
|
|
break;
|
|
case DefenseTowerKind.Lightning:
|
|
Vector2 impactCenter = visualCenter ?? center;
|
|
Vector2 lightningOrigin = Tier == 4 && Branch == DefenseTowerBranch.Second
|
|
? impactCenter : GetStaffTipWorldPosition();
|
|
ShowLightningArt(effect.transform, lightningOrigin, impactCenter,
|
|
impactCenter - lightningOrigin, radius, projectileTarget);
|
|
break;
|
|
case DefenseTowerKind.Cold:
|
|
ShowColdArt(effect.transform, origin, center, direction, radius,
|
|
shadowCenteredCircle ? effectCenter : (Vector2?)null,
|
|
projectileImpact, projectileTarget, damageScale);
|
|
break;
|
|
case DefenseTowerKind.Seismic:
|
|
string controlKey = Tier == 4
|
|
? (Branch == DefenseTowerBranch.First
|
|
? "Effects/Control/StoneGaze" : "Effects/Control/Temporal")
|
|
: $"Effects/Control/T{Tier}-Aura";
|
|
int visibleWidth = Tier == 4
|
|
? (Branch == DefenseTowerBranch.First ? 40 : 45)
|
|
: 32 + (Tier - 1) * 6;
|
|
float controlScale = GetSeismicGroundMarkerScale(
|
|
projectileTarget, controlKey, visibleWidth, effect.transform);
|
|
SpriteRenderer control = CreateAnimatedArt(effect.transform, "Impact Sprite", controlKey,
|
|
effectCenter, Vector2.one * controlScale, 0f, 13);
|
|
SetGroundHitRendererSorting(control, projectileTarget);
|
|
break;
|
|
case DefenseTowerKind.Summon:
|
|
if (Tier == 4 && Branch == DefenseTowerBranch.Second)
|
|
{
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
Vector2 spikePosition = Vector2.Lerp(origin, center, (i + 1) / 5f);
|
|
CreateAnimatedArt(effect.transform, $"Shot Sprite {i + 1}",
|
|
"Summons/LavaSlime/GroundSpike-Segment", spikePosition,
|
|
Vector2.one, 0f, 14, startDelay: i * 0.12f);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SpriteRenderer impact = CreateAnimatedArt(effect.transform, "Impact Sprite",
|
|
"Effects/Fire/Impact", GetTargetHitCenter(projectileTarget, center),
|
|
Vector2.one * 0.45f, 0f, 13);
|
|
SetHitRendererSorting(impact, projectileTarget);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (radius > 0f && Kind != DefenseTowerKind.Seismic)
|
|
{
|
|
Color radiusColor = color;
|
|
if (IsBlizzard) radiusColor.a = DefenseTowerArtBounds.BlizzardImpactRadiusAlpha;
|
|
LineRenderer area = CreateEffectLine(
|
|
effect.transform, "Impact Radius", radiusColor, 0.07f);
|
|
area.loop = true;
|
|
area.positionCount = 40;
|
|
for (int i = 0; i < area.positionCount; i++)
|
|
{
|
|
float angle = i * Mathf.PI * 2f / area.positionCount;
|
|
area.SetPosition(i, new Vector3(
|
|
Mathf.Cos(angle) * radius,
|
|
Mathf.Sin(angle) * radius, 0f));
|
|
}
|
|
SetHitRendererSorting(area, projectileTarget);
|
|
float radiusLifetime = Kind == DefenseTowerKind.Cold && Tier == 4
|
|
&& Branch == DefenseTowerBranch.Second
|
|
? DefenseTowerArtBounds.BlizzardLastLandingSeconds + 0.22f
|
|
: 0.22f;
|
|
Destroy(area.gameObject, radiusLifetime);
|
|
}
|
|
|
|
TrackAttackEffect(effect);
|
|
}
|
|
|
|
private static Vector2 GetTargetHitCenter(DefenseRouteAgent target, Vector2 fallback) =>
|
|
target != null && target.Enemy != null
|
|
? (Vector2)target.Enemy.transform.position : fallback;
|
|
|
|
private static float GetSeismicGroundMarkerScale(
|
|
DefenseRouteAgent target, string controlKey, int maximumVisiblePixels,
|
|
Transform effectRoot)
|
|
{
|
|
SpriteRenderer targetRenderer = target != null && target.Enemy != null
|
|
? target.Enemy.GetComponent<SpriteRenderer>() : null;
|
|
Sprite targetSprite = targetRenderer != null ? targetRenderer.sprite : null;
|
|
Sprite[] controlFrames = DefenseTowerArt.Load(controlKey);
|
|
Sprite controlSprite = controlFrames.Length > 0 ? controlFrames[0] : null;
|
|
if (targetSprite == null || controlSprite == null) return 0f;
|
|
float markerWorldWidth = DefenseTowerArtBounds.SeismicGroundMarkerWidthPixels
|
|
* Mathf.Abs(targetRenderer.transform.lossyScale.x) / targetSprite.pixelsPerUnit;
|
|
float parentScale = effectRoot != null
|
|
? Mathf.Abs(effectRoot.lossyScale.x) : 1f;
|
|
float frameWidthWorld = maximumVisiblePixels / controlSprite.pixelsPerUnit;
|
|
return markerWorldWidth / (frameWidthWorld * Mathf.Max(0.001f, parentScale));
|
|
}
|
|
|
|
private static void SetGroundHitRendererSorting(Renderer renderer, DefenseRouteAgent target)
|
|
{
|
|
if (renderer == null || target == null || target.Enemy == null) return;
|
|
SpriteRenderer targetRenderer = target.Enemy.GetComponent<SpriteRenderer>();
|
|
if (targetRenderer == null) return;
|
|
renderer.sortingLayerID = targetRenderer.sortingLayerID;
|
|
renderer.sortingOrder = targetRenderer.sortingOrder - 1;
|
|
}
|
|
|
|
private static void SetHitRendererSorting(Renderer renderer, DefenseRouteAgent target)
|
|
{
|
|
if (renderer == null || target == null || target.Enemy == null) return;
|
|
SpriteRenderer targetRenderer = target.Enemy.GetComponent<SpriteRenderer>();
|
|
if (targetRenderer == null) return;
|
|
renderer.sortingLayerID = targetRenderer.sortingLayerID;
|
|
renderer.sortingOrder = targetRenderer.sortingOrder
|
|
+ DefenseTowerArtBounds.TowerHitSortingOffset;
|
|
}
|
|
|
|
private void ApplyProjectileImpact(
|
|
GameObject effect, DefenseRouteAgent target, string impactKey,
|
|
float impactScale, System.Action damageAction)
|
|
{
|
|
if (!CanApplyProjectileImpact(target)) return;
|
|
Vector2 impactPosition = GetTargetHitCenter(target, target.transform.position);
|
|
damageAction?.Invoke();
|
|
if (string.IsNullOrEmpty(impactKey) || effect == null) return;
|
|
|
|
SpriteRenderer impact = CreateAnimatedArt(
|
|
effect.transform, "Impact Sprite", impactKey, impactPosition,
|
|
Vector2.one * impactScale, 0f, 13);
|
|
SetHitRendererSorting(impact, target);
|
|
TrackAttackSequence(effect, impact.GetComponent<DefenseSpriteSequence>());
|
|
}
|
|
|
|
private bool CanApplyProjectileImpact(DefenseRouteAgent target)
|
|
{
|
|
return IsOperational && target != null && !target.HasArrived
|
|
&& target.Enemy != null && !target.Enemy.IsDead;
|
|
}
|
|
|
|
private void ShowFireArt(
|
|
Transform parent, Vector2 origin, Vector2 center, Vector2 direction,
|
|
System.Action projectileImpact, DefenseRouteAgent projectileTarget)
|
|
{
|
|
if (Tier == 4 && Branch == DefenseTowerBranch.First)
|
|
{
|
|
Vector2 end = GetTargetHitCenter(projectileTarget, center);
|
|
Vector2 start = end + new Vector2(-0.81f, 0.81f);
|
|
SpriteRenderer meteor = CreateAnimatedArt(parent, "Impact Sprite",
|
|
"Effects/Fire/Meteor-Flight", start,
|
|
Vector2.one, 0f, 13, "Effects/Fire/Meteor-Impact");
|
|
SetHitRendererSorting(meteor, projectileTarget);
|
|
DefenseSpriteSequence sequence = meteor.GetComponent<DefenseSpriteSequence>();
|
|
sequence.Travel(start, end, 0.32f,
|
|
() => ApplyProjectileImpact(
|
|
parent.gameObject, projectileTarget, null, 0f, projectileImpact),
|
|
() => GetTargetHitCenter(projectileTarget, center),
|
|
() => IsOperational);
|
|
return;
|
|
}
|
|
|
|
if (Tier < 4)
|
|
{
|
|
origin = GetStaffTipWorldPosition();
|
|
string key = $"Effects/Fire/T{Tier}-Fireball";
|
|
Vector2 end = GetTargetHitCenter(projectileTarget, center);
|
|
direction = end - origin;
|
|
SpriteRenderer shot = CreateAnimatedArt(parent, "Shot Sprite", key,
|
|
Vector2.Lerp(origin, end, 0.5f), Vector2.one * 0.85f,
|
|
Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg, 14);
|
|
DefenseSpriteSequence sequence = shot.GetComponent<DefenseSpriteSequence>();
|
|
sequence.Travel(origin, end, 0.18f,
|
|
() => ApplyProjectileImpact(
|
|
parent.gameObject, projectileTarget, "Effects/Fire/Impact", 1f,
|
|
projectileImpact),
|
|
() => GetTargetHitCenter(projectileTarget, center),
|
|
() => IsOperational);
|
|
}
|
|
}
|
|
|
|
private void ShowLightningArt(
|
|
Transform parent, Vector2 origin, Vector2 center,
|
|
Vector2 direction, float radius, DefenseRouteAgent target)
|
|
{
|
|
if (Tier == 4 && Branch == DefenseTowerBranch.Second)
|
|
{
|
|
SpriteRenderer storm = CreateAnimatedArt(parent, "Shot Sprite",
|
|
"Effects/Lightning/LightningStorm", center, Vector2.one, 0f, 13);
|
|
storm.GetComponent<DefenseSpriteSequence>()
|
|
.FitLightningStormGround(center, radius);
|
|
SetHitRendererSorting(storm, target);
|
|
}
|
|
else
|
|
{
|
|
string key = Tier == 4
|
|
? "Effects/Lightning/Arc" : $"Effects/Lightning/T{Tier}-Bolt";
|
|
SpriteRenderer bolt = CreateAnimatedArt(parent, "Shot Sprite", key, origin,
|
|
Vector2.one, Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg, 14);
|
|
bolt.GetComponent<DefenseSpriteSequence>().FitLightningBeam(origin, center, 1f);
|
|
}
|
|
if (Tier == 4)
|
|
{
|
|
float waveScale = Mathf.Max(0.8f, radius / 1.4f);
|
|
SpriteRenderer wave = CreateAnimatedArt(parent, "Impact Sprite", "Effects/Lightning/ImpactWave",
|
|
center, Vector2.one * waveScale, 0f, 13, startDelay: 0.15f);
|
|
wave.GetComponent<DefenseSpriteSequence>().CenterImpactWave(center, waveScale);
|
|
SetHitRendererSorting(wave, target);
|
|
}
|
|
}
|
|
|
|
private void ShowColdArt(
|
|
Transform parent, Vector2 origin, Vector2 center, Vector2 direction, float radius,
|
|
Vector2? visualCenter, System.Action projectileImpact,
|
|
DefenseRouteAgent projectileTarget, float damageScale)
|
|
{
|
|
if (Tier == 4 && Branch == DefenseTowerBranch.Second)
|
|
{
|
|
ShowBlizzardSalvo(parent, visualCenter ?? center, radius,
|
|
projectileTarget,
|
|
landing => AttackBlizzardWave(
|
|
landing, radius, tuning.ColdDamage * damageScale * 0.5f));
|
|
return;
|
|
}
|
|
|
|
string key = Tier == 4
|
|
? "Effects/Ice/Freezebolt" : "Effects/Ice/T1-Volley";
|
|
origin = GetStaffTipWorldPosition();
|
|
direction = center - origin;
|
|
Vector2 end = GetTargetHitCenter(projectileTarget, center);
|
|
int projectileCount = Tier == 4 ? tuning.FreezeboltProjectileCount : Tier;
|
|
float burstInterval = tuning.ColdProjectileBurstInterval;
|
|
for (int i = 0; i < projectileCount; i++)
|
|
{
|
|
SpriteRenderer shot = CreateAnimatedArt(parent, $"Shot Sprite {i + 1}", key,
|
|
Vector2.Lerp(origin, end, 0.5f), Vector2.one,
|
|
Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg, 14,
|
|
startDelay: i * burstInterval);
|
|
shot.GetComponent<DefenseSpriteSequence>().Travel(origin, end, 0.18f,
|
|
() => ApplyProjectileImpact(
|
|
parent.gameObject, projectileTarget, "Effects/Ice/Impact", 1f,
|
|
projectileImpact),
|
|
() => GetTargetHitCenter(projectileTarget, center),
|
|
() => IsOperational, hideAtArrival: true,
|
|
startProvider: GetStaffTipWorldPosition);
|
|
}
|
|
}
|
|
|
|
private void ShowBlizzardSalvo(
|
|
Transform parent, Vector2 center, float radius,
|
|
DefenseRouteAgent target, System.Action<Vector2> landingDamageAction)
|
|
{
|
|
for (int i = 0; i < DefenseTowerArtBounds.BlizzardShardCount; i++)
|
|
{
|
|
float launchDelay = i * DefenseTowerArtBounds.BlizzardLaunchIntervalSeconds;
|
|
Vector2 landing = center
|
|
+ DefenseTowerArtBounds.BlizzardLandingOffset(i, radius);
|
|
float height = DefenseTowerArtBounds.BlizzardDropHeight
|
|
+ i % 4 * DefenseTowerArtBounds.BlizzardDropHeightStep;
|
|
float rotation = 90f + (i % 5 - 2) * 7f;
|
|
Vector2 start = landing + Vector2.up * height;
|
|
|
|
float landingDelay = launchDelay + DefenseTowerArtBounds.BlizzardLandingSeconds;
|
|
SpriteRenderer wave = CreateAnimatedArt(parent, $"Blizzard Wave {i}",
|
|
"Effects/Ice/Blizzard-Wave-v1", landing,
|
|
Vector2.one * DefenseTowerArtBounds.BlizzardWaveScale,
|
|
0f, 13, startDelay: landingDelay);
|
|
wave.GetComponent<DefenseSpriteSequence>().CenterImpactWave(
|
|
landing, DefenseTowerArtBounds.BlizzardWaveScale);
|
|
SetHitRendererSorting(wave, target);
|
|
|
|
SpriteRenderer impact = CreateAnimatedArt(parent, $"Blizzard Impact {i}",
|
|
"Effects/Ice/Impact", landing,
|
|
Vector2.one * DefenseTowerArtBounds.BlizzardImpactScale,
|
|
0f, 14, startDelay: landingDelay);
|
|
SetHitRendererSorting(impact, target);
|
|
|
|
SpriteRenderer shard = CreateAnimatedArt(parent, $"Blizzard Shard {i}",
|
|
"Effects/Ice/Shard-Blizzard-v1", start,
|
|
Vector2.one * DefenseTowerArtBounds.BlizzardShardScale,
|
|
rotation, 14, startDelay: launchDelay);
|
|
SetHitRendererSorting(shard, target);
|
|
shard.GetComponent<DefenseSpriteSequence>().Travel(
|
|
start, landing, DefenseTowerArtBounds.BlizzardLandingSeconds,
|
|
() => landingDamageAction?.Invoke(landing),
|
|
canAdvance: () => IsOperational);
|
|
|
|
Vector2 trailOffset = Vector2.up * DefenseTowerArtBounds.BlizzardTrailOffset;
|
|
SpriteRenderer trail = CreateAnimatedArt(parent, $"Blizzard Trail {i}",
|
|
"Effects/Ice/Shard-Blizzard-v1", start + trailOffset,
|
|
Vector2.one * DefenseTowerArtBounds.BlizzardTrailScale,
|
|
rotation, 13, startDelay: launchDelay);
|
|
trail.color = new Color(0.72f, 0.9f, 1f,
|
|
DefenseTowerArtBounds.BlizzardTrailAlpha);
|
|
SetHitRendererSorting(trail, target);
|
|
trail.GetComponent<DefenseSpriteSequence>().Travel(
|
|
start + trailOffset, landing + trailOffset,
|
|
DefenseTowerArtBounds.BlizzardLandingSeconds,
|
|
canAdvance: () => IsOperational);
|
|
}
|
|
}
|
|
|
|
private bool AttackBlizzardWave(Vector2 center, float castRadius, float damage)
|
|
{
|
|
float radiusX = DefenseTowerArtBounds.BlizzardWaveRadiusX;
|
|
float radiusY = DefenseTowerArtBounds.BlizzardWaveRadiusY;
|
|
// GetTargetsInRange measures from the agent root; the generous broadphase
|
|
// leaves room for the enemy sprite's shadow offset before the ellipse test.
|
|
float broadphaseRadius = castRadius + radiusX;
|
|
bool applied = false;
|
|
foreach (DefenseRouteAgent agent in owner.GetTargetsInRange(
|
|
center, broadphaseRadius, Kind))
|
|
{
|
|
if (agent == null || agent.HasArrived || agent.Enemy == null
|
|
|| agent.Enemy.IsDead)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Vector2 groundPosition = DefenseTowerArtBounds.EnemyShadowWorldPosition(
|
|
agent.Enemy, agent.transform.position);
|
|
Vector2 normalized = groundPosition - center;
|
|
float ellipseDistance = normalized.x * normalized.x / (radiusX * radiusX)
|
|
+ normalized.y * normalized.y / (radiusY * radiusY);
|
|
if (ellipseDistance > 1f) continue;
|
|
|
|
bool damaged = owner.TryApplyTowerDamage(this, agent, damage);
|
|
bool slowed = owner.TryApplyTowerSlow(this, agent,
|
|
tuning.ColdSlowSeconds * 1.5f, tuning.ColdSlowFraction);
|
|
bool stunned = owner.TryApplyTowerStun(this, agent, 0.35f);
|
|
applied |= damaged || slowed || stunned;
|
|
}
|
|
|
|
return applied;
|
|
}
|
|
|
|
private bool IsBlizzard => Kind == DefenseTowerKind.Cold && Tier == 4
|
|
&& Branch == DefenseTowerBranch.Second;
|
|
|
|
private SpriteRenderer CreateAnimatedArt(
|
|
Transform parent, string name, string key, Vector2 position,
|
|
Vector2 scale, float rotation, int sortingOrder,
|
|
string nextKey = null, float startDelay = 0f)
|
|
{
|
|
GameObject spriteObject = new(name);
|
|
spriteObject.transform.SetParent(parent, false);
|
|
spriteObject.transform.position = position;
|
|
spriteObject.transform.localScale = new Vector3(scale.x, scale.y, 1f);
|
|
spriteObject.transform.rotation = Quaternion.Euler(0f, 0f, rotation);
|
|
SpriteRenderer renderer = spriteObject.AddComponent<SpriteRenderer>();
|
|
renderer.color = Color.white;
|
|
renderer.sortingOrder = sortingOrder;
|
|
Sprite[] frames = DefenseTowerArt.Load(key);
|
|
Sprite[] next = string.IsNullOrEmpty(nextKey)
|
|
? null : DefenseTowerArt.Load(nextKey);
|
|
DefenseSpriteSequence sequence = spriteObject.AddComponent<DefenseSpriteSequence>();
|
|
sequence.Play(renderer, key, frames, true, nextKey, next, startDelay);
|
|
sequence.SetCanAdvance(() => IsOperational);
|
|
return renderer;
|
|
}
|
|
|
|
private static float ScaleToVisibleWidth(float worldWidth, float visiblePixels)
|
|
{
|
|
return worldWidth / (visiblePixels / 32f);
|
|
}
|
|
|
|
private static LineRenderer CreateEffectLine(
|
|
Transform parent, string name, Color color, float width)
|
|
{
|
|
GameObject shape = new(name);
|
|
shape.transform.SetParent(parent, false);
|
|
LineRenderer line = shape.AddComponent<LineRenderer>();
|
|
line.useWorldSpace = false;
|
|
line.sharedMaterial = GetEffectMaterial();
|
|
line.startWidth = width;
|
|
line.endWidth = width;
|
|
line.startColor = color;
|
|
line.endColor = color;
|
|
line.sortingOrder = 14;
|
|
return line;
|
|
}
|
|
|
|
private static Material GetEffectMaterial()
|
|
{
|
|
if (effectMaterial == null)
|
|
{
|
|
effectMaterial = new Material(Shader.Find("Sprites/Default"));
|
|
effectMaterial.hideFlags = HideFlags.HideAndDontSave;
|
|
}
|
|
return effectMaterial;
|
|
}
|
|
|
|
private void ReplaceSummonUnits(int count)
|
|
{
|
|
DefenseSummonUnit[] existingUnits =
|
|
GetComponentsInChildren<DefenseSummonUnit>(true);
|
|
summonCount = 0;
|
|
nextSummonIndex = 0;
|
|
targetSummonCount = count;
|
|
summonRespawnRemaining = 0f;
|
|
freeSummonSlots.Clear();
|
|
deadSummons.Clear();
|
|
foreach (DefenseSummonUnit unit in existingUnits)
|
|
{
|
|
unit.RetireForReplacement();
|
|
Destroy(unit.gameObject);
|
|
}
|
|
AddSummonUnits(count);
|
|
}
|
|
public void NotifySummonUnitDied(DefenseSummonUnit unit)
|
|
{
|
|
if (unit == null || unit.Owner != this) return;
|
|
summonCount = Mathf.Max(0, summonCount - 1);
|
|
if (!deadSummons.ContainsKey(unit.SlotIndex))
|
|
{
|
|
deadSummons.Add(unit.SlotIndex, unit);
|
|
freeSummonSlots.Push(unit.SlotIndex);
|
|
}
|
|
summonRespawnRemaining = Mathf.Max(summonRespawnRemaining, tuning.SummonUnitRespawnSeconds);
|
|
}
|
|
private void AddSummonUnits(int count)
|
|
{
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
int unitIndex = freeSummonSlots.Count > 0 ? freeSummonSlots.Pop() : nextSummonIndex++;
|
|
if (deadSummons.TryGetValue(unitIndex, out DefenseSummonUnit deadUnit))
|
|
{
|
|
deadSummons.Remove(unitIndex);
|
|
if (deadUnit != null) Destroy(deadUnit.gameObject);
|
|
}
|
|
summonCount++;
|
|
GameObject unitObject = new($"Summon Unit {unitIndex + 1}");
|
|
unitObject.transform.SetParent(transform, false);
|
|
unitObject.transform.localPosition = new Vector3(
|
|
(unitIndex % 3 - 1) * 0.8f, -0.55f - 0.8f * (unitIndex / 3), 0f);
|
|
DefenseSummonUnit unit = unitObject.AddComponent<DefenseSummonUnit>();
|
|
unit.Configure(this, owner, unitObject.transform.localPosition, unitIndex);
|
|
}
|
|
}
|
|
|
|
private void CreateVisuals()
|
|
{
|
|
SpriteRenderer rootRenderer = gameObject.GetComponent<SpriteRenderer>();
|
|
if (rootRenderer == null)
|
|
{
|
|
rootRenderer = gameObject.AddComponent<SpriteRenderer>();
|
|
}
|
|
rootRenderer.sprite = DefensePrototypeController.WhiteSprite;
|
|
rootRenderer.color = Color.white;
|
|
rootRenderer.enabled = false;
|
|
transform.localScale = new Vector3(0.72f, 0.72f, 1f);
|
|
|
|
GameObject bodyObject = new("Tower Body");
|
|
bodyObject.transform.SetParent(transform, false);
|
|
bodyObject.transform.localScale = new Vector3(2.1f, 2.1f, 1f);
|
|
bodyRenderer = bodyObject.AddComponent<SpriteRenderer>();
|
|
bodyRenderer.color = Color.white;
|
|
bodyRenderer.sortingOrder = 8;
|
|
UpdateBodyArt();
|
|
|
|
GameObject labelObject = new("Tower Label");
|
|
labelObject.transform.SetParent(transform, false);
|
|
labelObject.transform.localPosition = new Vector3(0f, 0.85f, 0f);
|
|
label = labelObject.AddComponent<TextMesh>();
|
|
label.anchor = TextAnchor.MiddleCenter;
|
|
label.alignment = TextAlignment.Center;
|
|
label.characterSize = 0.16f;
|
|
label.fontSize = 32;
|
|
label.color = Color.white;
|
|
label.GetComponent<MeshRenderer>().sortingOrder = 10;
|
|
UpdateLabel();
|
|
CreateEffectRange();
|
|
}
|
|
|
|
private void UpdateBodyArt()
|
|
{
|
|
bodyIdleKey = DefenseTowerArt.BodyKey(Kind, Tier, "Idle");
|
|
bodyIdleFrames = DefenseTowerArt.Load(bodyIdleKey);
|
|
bodyAttackKey = Kind switch
|
|
{
|
|
DefenseTowerKind.Direct or DefenseTowerKind.Lightning or DefenseTowerKind.Cold
|
|
=> DefenseTowerArt.BodyKey(Kind, Tier, "Attack01"),
|
|
DefenseTowerKind.Seismic => DefenseTowerArt.BodyKey(Kind, Tier, "Attack"),
|
|
DefenseTowerKind.Empowering => DefenseTowerArt.BodyKey(Kind, Tier, "Heal"),
|
|
_ => null,
|
|
};
|
|
bodyAttackFrames = bodyAttackKey == null
|
|
? System.Array.Empty<Sprite>() : DefenseTowerArt.Load(bodyAttackKey);
|
|
bodyAttacking = false;
|
|
bodyFrame = 0;
|
|
bodyFrameTime = 0f;
|
|
if (bodyRenderer != null && bodyIdleFrames.Length > 0)
|
|
{
|
|
bodyRenderer.sprite = bodyIdleFrames[0];
|
|
}
|
|
}
|
|
|
|
private void PlayBodyAttack(bool faceLeft)
|
|
{
|
|
if (bodyRenderer == null || bodyAttackFrames == null || bodyAttackFrames.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
bodyRenderer.flipX = faceLeft;
|
|
bodyAttacking = true;
|
|
bodyFrame = 0;
|
|
bodyFrameTime = 0f;
|
|
bodyRenderer.sprite = bodyAttackFrames[0];
|
|
}
|
|
|
|
private void UpdateBodyAnimation()
|
|
{
|
|
if (bodyRenderer == null || bodyIdleFrames == null || bodyIdleFrames.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
bodyFrameTime += Time.deltaTime;
|
|
Sprite[] frames = bodyAttacking ? bodyAttackFrames : bodyIdleFrames;
|
|
string key = bodyAttacking ? bodyAttackKey : bodyIdleKey;
|
|
if (frames == null || frames.Length == 0)
|
|
{
|
|
bodyAttacking = false;
|
|
return;
|
|
}
|
|
while (bodyFrameTime >= DefenseTowerArt.FrameSeconds(key, bodyFrame))
|
|
{
|
|
bodyFrameTime -= DefenseTowerArt.FrameSeconds(key, bodyFrame);
|
|
bodyFrame++;
|
|
if (bodyFrame >= frames.Length)
|
|
{
|
|
if (bodyAttacking)
|
|
{
|
|
bodyAttacking = false;
|
|
bodyFrame = 0;
|
|
frames = bodyIdleFrames;
|
|
key = bodyIdleKey;
|
|
}
|
|
else
|
|
{
|
|
bodyFrame = 0;
|
|
}
|
|
}
|
|
bodyRenderer.sprite = frames[bodyFrame];
|
|
}
|
|
}
|
|
|
|
private void UpdateSupportAura()
|
|
{
|
|
DefenseTower support = owner != null ? owner.GetActiveSupportFor(this) : null;
|
|
if (support == null)
|
|
{
|
|
if (supportAuraRenderer != null) supportAuraRenderer.enabled = false;
|
|
return;
|
|
}
|
|
if (supportAuraRenderer == null)
|
|
{
|
|
GameObject auraObject = new("Support Aura Art");
|
|
auraObject.transform.SetParent(transform, false);
|
|
supportAuraRenderer = auraObject.AddComponent<SpriteRenderer>();
|
|
}
|
|
string motionKey = bodyAttacking && !string.IsNullOrEmpty(bodyAttackKey) ? bodyAttackKey : bodyIdleKey;
|
|
string key = motionKey.Replace("Bodies/", "SupportMasks/");
|
|
if (supportAuraKey != key)
|
|
{
|
|
supportAuraKey = key;
|
|
supportAuraFrames = DefenseTowerArt.Load(key);
|
|
}
|
|
int maskFrame = 0;
|
|
if (bodyRenderer != null && supportAuraFrames != null)
|
|
{
|
|
for (int i = 0; i < supportAuraFrames.Length; i++)
|
|
if (supportAuraFrames[i].name == bodyRenderer.sprite.name) { maskFrame = i; break; }
|
|
}
|
|
supportAuraRenderer.sprite = supportAuraFrames != null && supportAuraFrames.Length > maskFrame
|
|
? supportAuraFrames[maskFrame] : null;
|
|
supportAuraRenderer.flipX = bodyRenderer != null && bodyRenderer.flipX;
|
|
supportAuraRenderer.transform.localPosition = bodyRenderer != null ? bodyRenderer.transform.localPosition : Vector3.zero;
|
|
supportAuraRenderer.transform.localScale = bodyRenderer != null ? bodyRenderer.transform.localScale : Vector3.one;
|
|
supportAuraRenderer.sortingOrder = bodyRenderer != null ? bodyRenderer.sortingOrder + 1 : 9;
|
|
Color[] colors = tuning.SupportTierColors;
|
|
supportAuraRenderer.color = colors != null && colors.Length > 0
|
|
? colors[Mathf.Clamp(support.Tier - 1, 0, colors.Length - 1)]
|
|
: new Color32(151, 169, 119, 102);
|
|
supportAuraRenderer.enabled = supportAuraFrames != null && supportAuraFrames.Length > 0 && supportAuraRenderer.sprite != null;
|
|
}
|
|
|
|
private void UpdateSupportRangeAnimation()
|
|
{
|
|
if (supportRangeRenderer == null || supportRangeFrames == null || supportRangeFrames.Length == 0) return;
|
|
supportRangeFrameTime += Time.deltaTime;
|
|
while (supportRangeFrameTime >= 0.14f)
|
|
{
|
|
supportRangeFrameTime -= 0.14f;
|
|
supportRangeFrame = (supportRangeFrame + 1) % supportRangeFrames.Length;
|
|
supportRangeRenderer.sprite = supportRangeFrames[supportRangeFrame];
|
|
UpdateEffectRange();
|
|
}
|
|
}
|
|
|
|
private Color GetTowerColor()
|
|
{
|
|
return Kind switch
|
|
{
|
|
DefenseTowerKind.Direct => new Color32(240, 96, 65, 255),
|
|
DefenseTowerKind.Summon => new Color32(244, 154, 74, 255),
|
|
DefenseTowerKind.Lightning => new Color32(238, 218, 83, 255),
|
|
DefenseTowerKind.Cold => new Color32(65, 211, 240, 255),
|
|
DefenseTowerKind.Seismic => new Color32(175, 141, 100, 255),
|
|
_ => new Color32(148, 228, 122, 255),
|
|
};
|
|
}
|
|
|
|
private void UpdateLabel()
|
|
{
|
|
if (label == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string family = Kind == DefenseTowerKind.Direct ? "FIRE" : Kind.ToString().ToUpperInvariant();
|
|
string branchName = Tier == 4
|
|
? GetBranchName(Kind, Branch) : string.Empty;
|
|
label.text = Tier == 4
|
|
? $"{family} IV\n{branchName}"
|
|
: $"{family} {Tier}";
|
|
}
|
|
|
|
public static string GetBranchName(DefenseTowerKind kind, DefenseTowerBranch branch)
|
|
{
|
|
bool first = branch == DefenseTowerBranch.First;
|
|
return kind switch
|
|
{
|
|
DefenseTowerKind.Direct => first ? "METEOR" : "FLAMETHROWER",
|
|
DefenseTowerKind.Lightning => first ? "ARC" : "LIGHTNING STORM",
|
|
DefenseTowerKind.Cold => first ? "FREEZEBOLT" : "BLIZZARD",
|
|
DefenseTowerKind.Seismic => first ? "STONE GAZE" : "TEMPORAL",
|
|
DefenseTowerKind.Summon => first ? "SENTINEL" : "SCOUT",
|
|
_ => first ? "IMBUING" : "SMOTHERING",
|
|
};
|
|
}
|
|
}
|
|
}
|