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; 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 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(); UpdateBodyArt(); UpdateSupportAura(); foreach (DefenseSummonUnit unit in GetComponentsInChildren()) { unit.RefreshArt(); } return true; } private void OnDisable() { if (supportAuraRenderer != null) { supportAuraRenderer.enabled = false; } } private void Update() { UpdateBodyAnimation(); 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 = owner.FindNearestTarget( transform.position, range, Kind, Kind == DefenseTowerKind.Cold || Kind == DefenseTowerKind.Seismic); if (target == null) { return false; } Vector2 center = target.transform.position; Vector2 lightningVisualCenter = DefenseTowerArtBounds.EnemyShadowWorldPosition( target.Enemy, 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) { PlayBodyAttack(center.x < transform.position.x); bool shadowCenteredCircle = Kind == DefenseTowerKind.Seismic || (Kind == DefenseTowerKind.Cold && Tier == 4 && Branch == DefenseTowerBranch.Second); ShowAttackEffect(center, impactRadius, cone, visualCenter: Kind == DefenseTowerKind.Lightning || shadowCenteredCircle ? lightningVisualCenter : null); } 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); if (Kind == DefenseTowerKind.Empowering) { supportRangeRenderer = rangeObject.AddComponent(); 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(); 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 : 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; } 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, bool cone, Vector2? source = null, Vector2? visualCenter = null) { 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 || (Kind == DefenseTowerKind.Cold && Tier == 4 && Branch == DefenseTowerBranch.Second); 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); break; case DefenseTowerKind.Lightning: Vector2 shadowOrigin = transform.TransformPoint(DefenseTowerArtBounds.CharacterShadowOffset(Kind, bodyRenderer != null && bodyRenderer.flipX)); Vector2 impactCenter = visualCenter ?? center; ShowLightningArt(effect.transform, shadowOrigin, impactCenter, impactCenter - shadowOrigin, radius); break; case DefenseTowerKind.Cold: ShowColdArt(effect.transform, origin, center, direction, shadowCenteredCircle ? effectCenter : (Vector2?)null); 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 = ScaleToVisibleWidth(radius * 2f, visibleWidth); CreateAnimatedArt(effect.transform, "Impact Sprite", controlKey, effectCenter, Vector2.one * controlScale, 0f, 13); 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 { CreateAnimatedArt(effect.transform, "Impact Sprite", "Effects/Fire/Impact", center, Vector2.one, 0f, 13); } break; } 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)); Destroy(outline.gameObject, 0.22f); } 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(area.gameObject, 0.22f); } Destroy(effect, 1.6f); } private void ShowFireArt( Transform parent, Vector2 origin, Vector2 center, Vector2 direction) { if (Tier == 4 && Branch == DefenseTowerBranch.First) { SpriteRenderer meteor = CreateAnimatedArt(parent, "Impact Sprite", "Effects/Fire/Meteor-Flight", center + new Vector2(-0.81f, 0.81f), Vector2.one, 0f, 13, "Effects/Fire/Meteor-Impact"); DefenseSpriteSequence sequence = meteor.GetComponent(); Vector2 start = center + new Vector2(-0.81f, 0.81f); meteor.transform.position = start; sequence.Travel(start, center, 0.32f); return; } if (Tier == 4 && Branch == DefenseTowerBranch.Second) { float visibleWidth = 88f / 32f; float scaleX = Mathf.Max(0.1f, direction.magnitude / visibleWidth); CreateAnimatedArt(parent, "Shot Sprite", "Effects/Fire/Flamethrower", origin, new Vector2(scaleX, 1f), Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg, 14); } else { string key = $"Effects/Fire/T{Tier}-Fireball"; SpriteRenderer shot = CreateAnimatedArt(parent, "Shot Sprite", key, Vector2.Lerp(origin, center, 0.5f), Vector2.one * 0.85f, Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg, 14); DefenseSpriteSequence sequence = shot.GetComponent(); sequence.Travel(origin, center, 0.18f); } CreateAnimatedArt(parent, "Impact Sprite", "Effects/Fire/Impact", center, Vector2.one, 0f, 13, startDelay: 0.18f); } private void ShowLightningArt( Transform parent, Vector2 origin, Vector2 center, Vector2 direction, float radius) { if (Tier == 4 && Branch == DefenseTowerBranch.Second) { float scale = Mathf.Max(0.8f, radius / 1.2f); CreateAnimatedArt(parent, "Shot Sprite", "Effects/Lightning/LightningStorm", center, Vector2.one * scale, 0f, 13); } 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().FitLightningBeam(origin, center, 1f); } 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().CenterImpactWave(center, waveScale); } private void ShowColdArt( Transform parent, Vector2 origin, Vector2 center, Vector2 direction, Vector2? visualCenter = null) { if (Tier == 4 && Branch == DefenseTowerBranch.Second) { float cageScale = ScaleToVisibleWidth(3.3f, 48f); Vector2 cageCenter = visualCenter ?? center; CreateAnimatedArt(parent, "Shot Sprite", "Effects/Ice/GlacialCage-Back", cageCenter, Vector2.one * cageScale, 0f, 12); CreateAnimatedArt(parent, "Impact Sprite", "Effects/Ice/GlacialCage-Front", cageCenter, Vector2.one * cageScale, 0f, 14); return; } string key = Tier == 4 ? "Effects/Ice/Freezebolt" : $"Effects/Ice/T{Tier}-Volley"; SpriteRenderer shot = CreateAnimatedArt(parent, "Shot Sprite", key, Vector2.Lerp(origin, center, 0.5f), Vector2.one, Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg, 14); shot.GetComponent().Travel(origin, center, 0.18f); CreateAnimatedArt(parent, "Impact Sprite", "Effects/Ice/Impact", center, Vector2.one, 0f, 13, startDelay: 0.18f); } private static 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(); renderer.color = Color.white; renderer.sortingOrder = sortingOrder; Sprite[] frames = DefenseTowerArt.Load(key); Sprite[] next = string.IsNullOrEmpty(nextKey) ? null : DefenseTowerArt.Load(nextKey); spriteObject.AddComponent().Play( renderer, key, frames, true, nextKey, next, startDelay); 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(); 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 rootRenderer = gameObject.GetComponent(); if (rootRenderer == null) { rootRenderer = gameObject.AddComponent(); } 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(); 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(); 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 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() : 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(); } 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" : "GLACIAL CAGE", DefenseTowerKind.Seismic => first ? "STONE GAZE" : "TEMPORAL", DefenseTowerKind.Summon => first ? "SENTINEL" : "SCOUT", _ => first ? "IMBUING" : "SMOTHERING", }; } } }