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()) { 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(); effectRange.useWorldSpace = false; effectRange.loop = true; effectRange.positionCount = 48; effectRange.sharedMaterial = GetEffectMaterial(); effectRange.sortingOrder = 3; Color color = GetComponent().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().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(); 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(); 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(); unit.Configure(this, owner, unitObject.transform.localPosition); } } private void CreateVisuals() { SpriteRenderer renderer = gameObject.GetComponent(); if (renderer == null) { renderer = gameObject.AddComponent(); } 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(); 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(); label.anchor = TextAnchor.MiddleCenter; label.alignment = TextAlignment.Center; label.characterSize = 0.16f; label.fontSize = 32; label.color = Color.white; label.GetComponent().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", }; } } }