552 lines
21 KiB
C#
552 lines
21 KiB
C#
using BumpCombat.Constants;
|
|
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;
|
|
|
|
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 void Configure(
|
|
DefensePrototypeController controller,
|
|
DefenseTowerKind kind,
|
|
int padIndex,
|
|
DefenseConstants constants)
|
|
{
|
|
owner = controller;
|
|
Kind = kind;
|
|
PadIndex = padIndex;
|
|
tuning = constants;
|
|
Tier = 1;
|
|
Branch = DefenseTowerBranch.None;
|
|
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)
|
|
{
|
|
if (Tier == 4 && Branch == DefenseTowerBranch.First)
|
|
{
|
|
foreach (DefenseSummonUnit unit in GetComponentsInChildren<DefenseSummonUnit>())
|
|
{
|
|
unit.enabled = false;
|
|
Destroy(unit.gameObject);
|
|
}
|
|
summonCount = 0;
|
|
AddSummonUnits(1);
|
|
}
|
|
else
|
|
{
|
|
AddSummonUnits(Tier == 4 ? 2 : 1);
|
|
}
|
|
}
|
|
|
|
UpdateLabel();
|
|
UpdateEffectRange();
|
|
return true;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
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 = owner.FindNearestTarget(
|
|
transform.position, range, Kind,
|
|
Kind == DefenseTowerKind.Cold || Kind == DefenseTowerKind.Seismic);
|
|
if (target == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Vector2 center = target.transform.position;
|
|
float scale = 1f + tuning.TowerDamageBonusPerTier * (Tier - 1);
|
|
float impactRadius = 0f;
|
|
bool cone = false;
|
|
bool applied;
|
|
switch (Kind)
|
|
{
|
|
case DefenseTowerKind.Direct:
|
|
if (Tier == 4 && Branch == DefenseTowerBranch.Second)
|
|
{
|
|
cone = true;
|
|
impactRadius = range;
|
|
applied = AttackCone(center - (Vector2)transform.position,
|
|
range, tuning.DirectTowerDamage * scale * 0.4f);
|
|
}
|
|
else if (Tier == 4)
|
|
{
|
|
impactRadius = 1.8f;
|
|
applied = AttackArea(center, impactRadius,
|
|
tuning.DirectTowerDamage * scale * 1.6f);
|
|
}
|
|
else
|
|
{
|
|
applied = 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 = AttackArea(center, impactRadius,
|
|
tuning.ColdDamage * scale * 0.5f,
|
|
int.MaxValue, tuning.ColdSlowSeconds * 1.5f,
|
|
tuning.ColdSlowFraction, 0.35f);
|
|
}
|
|
else
|
|
{
|
|
bool slowed = owner.TryApplyTowerSlow(this, target,
|
|
tuning.ColdSlowSeconds * (1f + 0.25f * (Tier - 1)),
|
|
tuning.ColdSlowFraction);
|
|
bool damaged = owner.TryApplyTowerDamage(this, target,
|
|
tuning.ColdDamage * scale * (Tier == 4 ? 1.8f : 1f));
|
|
if (Tier == 4)
|
|
{
|
|
owner.TryApplyTowerStun(this, target, 0.3f);
|
|
}
|
|
applied = slowed || damaged;
|
|
}
|
|
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)
|
|
{
|
|
ShowAttackEffect(center, impactRadius, cone);
|
|
}
|
|
return applied;
|
|
}
|
|
|
|
private bool AttackCone(Vector2 direction, float range, float damage)
|
|
{
|
|
bool applied = false;
|
|
Vector2 facing = direction.normalized;
|
|
foreach (DefenseRouteAgent agent in owner.GetTargetsInRange(
|
|
transform.position, range))
|
|
{
|
|
Vector2 toAgent = ((Vector2)agent.transform.position
|
|
- (Vector2)transform.position).normalized;
|
|
if (toAgent.sqrMagnitude < 0.000001f
|
|
|| Vector2.Dot(facing, toAgent) >= 0.5f)
|
|
{
|
|
applied |= owner.TryApplyTowerDamage(this, agent, damage);
|
|
}
|
|
}
|
|
|
|
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))
|
|
{
|
|
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;
|
|
}
|
|
|
|
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, Vector2 target)
|
|
{
|
|
if (IsOperational && Kind == DefenseTowerKind.Summon)
|
|
{
|
|
ShowAttackEffect(target, 0f, false, origin);
|
|
}
|
|
}
|
|
|
|
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);
|
|
effectRange = rangeObject.AddComponent<LineRenderer>();
|
|
effectRange.useWorldSpace = false;
|
|
effectRange.loop = true;
|
|
effectRange.positionCount = 48;
|
|
effectRange.sharedMaterial = GetEffectMaterial();
|
|
effectRange.sortingOrder = 3;
|
|
Color color = GetComponent<SpriteRenderer>().color;
|
|
color.a = Kind == DefenseTowerKind.Empowering ? 0.35f : 0.22f;
|
|
effectRange.startColor = color;
|
|
effectRange.endColor = color;
|
|
UpdateEffectRange();
|
|
}
|
|
|
|
private void UpdateEffectRange()
|
|
{
|
|
if (effectRange == null)
|
|
{
|
|
return;
|
|
}
|
|
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, bool cone, Vector2? source = null)
|
|
{
|
|
Vector2 origin = source ?? (Vector2)transform.position;
|
|
GameObject effect = new($"{Kind} Attack Effect");
|
|
effect.transform.SetParent(transform.parent, false);
|
|
effect.transform.position = center;
|
|
Color color = GetComponent<SpriteRenderer>().color;
|
|
color.a = 0.9f;
|
|
Vector2 direction = center - origin;
|
|
|
|
if (Kind == DefenseTowerKind.Lightning && direction.sqrMagnitude > 0.001f)
|
|
{
|
|
LineRenderer bolt = CreateEffectLine(effect.transform, "Bolt", color, 0.13f);
|
|
bolt.positionCount = 5;
|
|
Vector2 side = new(-direction.y, direction.x);
|
|
side.Normalize();
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
float fraction = i / 4f;
|
|
float offset = i == 1 ? 0.22f : i == 2 ? -0.16f
|
|
: i == 3 ? 0.18f : 0f;
|
|
bolt.SetPosition(i, origin - center
|
|
+ direction * fraction + side * offset);
|
|
}
|
|
}
|
|
else if (Kind != DefenseTowerKind.Seismic
|
|
&& direction.sqrMagnitude > 0.001f)
|
|
{
|
|
SpriteRenderer shot = CreateEffectSprite(
|
|
effect.transform, "Shot Sprite",
|
|
origin + direction * 0.5f,
|
|
new Vector2(direction.magnitude,
|
|
Kind == DefenseTowerKind.Direct ? 0.2f : 0.13f), color);
|
|
shot.transform.rotation = Quaternion.Euler(
|
|
0f, 0f, Mathf.Atan2(direction.y, direction.x)
|
|
* Mathf.Rad2Deg);
|
|
}
|
|
|
|
CreateEffectSprite(effect.transform, "Impact Sprite",
|
|
center, new Vector2(0.42f, 0.42f), color);
|
|
if (cone)
|
|
{
|
|
LineRenderer outline = CreateEffectLine(
|
|
effect.transform, "Cone Radius", color, 0.07f);
|
|
outline.positionCount = 3;
|
|
Vector2 forward = direction.normalized * radius;
|
|
outline.SetPosition(0, origin - center);
|
|
outline.SetPosition(1, origin - center
|
|
+ (Vector2)(Quaternion.Euler(0f, 0f, -60f) * forward));
|
|
outline.SetPosition(2, origin - center
|
|
+ (Vector2)(Quaternion.Euler(0f, 0f, 60f) * forward));
|
|
}
|
|
else if (radius > 0f)
|
|
{
|
|
LineRenderer area = CreateEffectLine(
|
|
effect.transform, "Impact Radius", color, 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));
|
|
}
|
|
}
|
|
|
|
Destroy(effect, 0.22f);
|
|
}
|
|
|
|
private static SpriteRenderer CreateEffectSprite(
|
|
Transform parent, string name, Vector2 position,
|
|
Vector2 size, Color color)
|
|
{
|
|
GameObject shape = new(name);
|
|
shape.transform.SetParent(parent, false);
|
|
shape.transform.position = position;
|
|
shape.transform.localScale = new Vector3(size.x, size.y, 1f);
|
|
SpriteRenderer renderer = shape.AddComponent<SpriteRenderer>();
|
|
renderer.sprite = DefensePrototypeController.WhiteSprite;
|
|
renderer.color = color;
|
|
renderer.sortingOrder = 15;
|
|
return renderer;
|
|
}
|
|
|
|
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 AddSummonUnits(int count)
|
|
{
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
int unitIndex = summonCount++;
|
|
GameObject unitObject = new($"Summon Unit {unitIndex + 1}");
|
|
unitObject.transform.SetParent(transform, false);
|
|
unitObject.transform.localPosition = new Vector3(
|
|
(unitIndex % 3 - 1) * 0.42f, -0.5f - 0.25f * (unitIndex / 3), 0f);
|
|
DefenseSummonUnit unit = unitObject.AddComponent<DefenseSummonUnit>();
|
|
unit.Configure(this, owner, unitObject.transform.localPosition);
|
|
}
|
|
}
|
|
|
|
private void CreateVisuals()
|
|
{
|
|
SpriteRenderer renderer = gameObject.GetComponent<SpriteRenderer>();
|
|
if (renderer == null)
|
|
{
|
|
renderer = gameObject.AddComponent<SpriteRenderer>();
|
|
}
|
|
renderer.sprite = DefensePrototypeController.WhiteSprite;
|
|
renderer.color = 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),
|
|
};
|
|
renderer.sortingOrder = 8;
|
|
transform.localScale = new Vector3(0.72f, 0.72f, 1f);
|
|
|
|
GameObject crown = new("Tower Crown");
|
|
crown.transform.SetParent(transform, false);
|
|
crown.transform.localPosition = new Vector3(0f, 0.42f, 0f);
|
|
crown.transform.localScale = new Vector3(0.42f, 0.22f, 1f);
|
|
SpriteRenderer crownRenderer = crown.AddComponent<SpriteRenderer>();
|
|
crownRenderer.sprite = DefensePrototypeController.WhiteSprite;
|
|
crownRenderer.color = Color.white;
|
|
crownRenderer.sortingOrder = 9;
|
|
|
|
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 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" : "GLACIAL CAGE",
|
|
DefenseTowerKind.Seismic => first ? "STONE GAZE" : "TEMPORAL",
|
|
DefenseTowerKind.Summon => first ? "SENTINEL" : "SCOUT",
|
|
_ => first ? "IMBUING" : "SMOTHERING",
|
|
};
|
|
}
|
|
}
|
|
} |