Adding Tesla Coil Tower and Arc VFX
New tower!
This commit is contained in:
parent
7b93137216
commit
699ea34268
20 changed files with 1317 additions and 2 deletions
118
Assets/_Project/Scripts/Combat/TeslaArcVisual.cs
Normal file
118
Assets/_Project/Scripts/Combat/TeslaArcVisual.cs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
// Assets/_Project/Scripts/Combat/TeslaArcVisual.cs
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Local-only electrical-arc visual for <see cref="TD.Core.TargetType.AllInRange"/>
|
||||
/// towers (Tesla Coil). Subscribes to <see cref="TowerCombat.OnAreaFired"/> and draws a
|
||||
/// jagged line from the tower's emitter point to every enemy hit, flashed for a moment.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pure visualization — runs on every peer from the replicated fire broadcast; nothing
|
||||
/// here is networked. Arcs are pooled <see cref="LineRenderer"/>s reused across ticks.
|
||||
/// </remarks>
|
||||
[RequireComponent(typeof(TowerCombat))]
|
||||
public class TeslaArcVisual : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Point the arcs originate from — the central top of the coil. " +
|
||||
"If unset, falls back to one unit above the tower root.")]
|
||||
[SerializeField] private Transform emitter;
|
||||
|
||||
[Tooltip("Material for the arc lines. Assign an unlit/additive material for a glow. " +
|
||||
"If unset, a fallback Sprites/Default material is created at runtime.")]
|
||||
[SerializeField] private Material arcMaterial;
|
||||
|
||||
[SerializeField] private Color arcColor = new Color(0.6f, 0.85f, 1f, 1f);
|
||||
[SerializeField] private float arcWidth = 0.06f;
|
||||
[Tooltip("How long each arc flash stays visible, in seconds.")]
|
||||
[SerializeField] private float arcDuration = 0.12f;
|
||||
[Tooltip("Number of points per arc. More = more jagged detail.")]
|
||||
[SerializeField] private int segmentsPerArc = 6;
|
||||
[Tooltip("Random offset applied to interior arc points for the lightning look.")]
|
||||
[SerializeField] private float jitter = 0.2f;
|
||||
|
||||
private TowerCombat combat;
|
||||
private readonly List<LineRenderer> pool = new List<LineRenderer>();
|
||||
private float hideAt = -1f;
|
||||
|
||||
private void Awake() => combat = GetComponent<TowerCombat>();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (combat != null) combat.OnAreaFired += HandleAreaFired;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (combat != null) combat.OnAreaFired -= HandleAreaFired;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (hideAt >= 0f && Time.time >= hideAt)
|
||||
{
|
||||
for (int i = 0; i < pool.Count; i++) pool[i].enabled = false;
|
||||
hideAt = -1f;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleAreaFired(Vector3[] positions)
|
||||
{
|
||||
if (positions == null) return;
|
||||
|
||||
Vector3 origin = emitter != null ? emitter.position : transform.position + Vector3.up;
|
||||
|
||||
for (int i = 0; i < positions.Length; i++)
|
||||
{
|
||||
var lr = GetLine(i);
|
||||
DrawArc(lr, origin, positions[i]);
|
||||
lr.enabled = true;
|
||||
}
|
||||
|
||||
// Disable any leftover lines from a previous, larger tick.
|
||||
for (int i = positions.Length; i < pool.Count; i++)
|
||||
pool[i].enabled = false;
|
||||
|
||||
hideAt = Time.time + arcDuration;
|
||||
}
|
||||
|
||||
private LineRenderer GetLine(int index)
|
||||
{
|
||||
while (pool.Count <= index)
|
||||
{
|
||||
var go = new GameObject($"Arc_{pool.Count}");
|
||||
go.transform.SetParent(transform, worldPositionStays: false);
|
||||
var lr = go.AddComponent<LineRenderer>();
|
||||
lr.useWorldSpace = true;
|
||||
lr.widthMultiplier = arcWidth;
|
||||
lr.numCapVertices = 2;
|
||||
lr.textureMode = LineTextureMode.Stretch;
|
||||
lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
lr.material = arcMaterial != null
|
||||
? arcMaterial
|
||||
: new Material(Shader.Find("Sprites/Default"));
|
||||
lr.startColor = lr.endColor = arcColor;
|
||||
lr.enabled = false;
|
||||
pool.Add(lr);
|
||||
}
|
||||
return pool[index];
|
||||
}
|
||||
|
||||
private void DrawArc(LineRenderer lr, Vector3 a, Vector3 b)
|
||||
{
|
||||
int n = Mathf.Max(2, segmentsPerArc);
|
||||
lr.positionCount = n;
|
||||
for (int s = 0; s < n; s++)
|
||||
{
|
||||
float t = s / (float)(n - 1);
|
||||
Vector3 p = Vector3.Lerp(a, b, t);
|
||||
// Jitter interior points only — endpoints stay anchored to the coil and enemy.
|
||||
if (s != 0 && s != n - 1)
|
||||
p += Random.insideUnitSphere * jitter;
|
||||
lr.SetPosition(s, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Project/Scripts/Combat/TeslaArcVisual.cs.meta
Normal file
11
Assets/_Project/Scripts/Combat/TeslaArcVisual.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a7c3e1f95b2d4068b1a9c4e7f2d50b83
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -90,6 +90,13 @@ namespace TD.Combat
|
|||
/// </summary>
|
||||
public event Action<Vector3> OnFire;
|
||||
|
||||
/// <summary>
|
||||
/// Fired locally on ALL peers when an <see cref="TargetType.AllInRange"/> tower
|
||||
/// attacks, carrying the world positions of every enemy hit this tick. A visual
|
||||
/// component (e.g. <c>TeslaArcVisual</c>) subscribes to draw an arc to each.
|
||||
/// </summary>
|
||||
public event Action<Vector3[]> OnAreaFired;
|
||||
|
||||
// ----- NGO lifecycle -----------------------------------------------
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
|
|
@ -300,10 +307,14 @@ namespace TD.Combat
|
|||
public readonly float ProjectileSpeed;
|
||||
public readonly int ChainCount;
|
||||
public readonly float ChainRange;
|
||||
// The tower's attack range. Carried here so the AllInRange path knows how far
|
||||
// to sweep without re-reading the TowerDefinition.
|
||||
public readonly float Range;
|
||||
|
||||
public CombatProfile(float damage, DamageType damageType, TargetType targetType,
|
||||
float splashRadius, float slowFactor, float dotDamagePerSecond,
|
||||
float effectDuration, float projectileSpeed, int chainCount, float chainRange)
|
||||
float effectDuration, float projectileSpeed, int chainCount, float chainRange,
|
||||
float range)
|
||||
{
|
||||
Damage = damage;
|
||||
DamageType = damageType;
|
||||
|
|
@ -315,6 +326,7 @@ namespace TD.Combat
|
|||
ProjectileSpeed = projectileSpeed;
|
||||
ChainCount = chainCount;
|
||||
ChainRange = chainRange;
|
||||
Range = range;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -354,7 +366,8 @@ namespace TD.Combat
|
|||
}
|
||||
|
||||
return new CombatProfile(damage, damageType, targetType, splash, slow, dot,
|
||||
duration, def.ProjectileSpeed, def.ChainCount, def.ChainRange);
|
||||
duration, def.ProjectileSpeed, def.ChainCount, def.ChainRange,
|
||||
def.Range);
|
||||
}
|
||||
|
||||
// ----- Damage application ------------------------------------------
|
||||
|
|
@ -377,9 +390,33 @@ namespace TD.Combat
|
|||
case TargetType.Chain:
|
||||
ApplyChain(p, primary, owner);
|
||||
break;
|
||||
|
||||
case TargetType.AllInRange:
|
||||
ApplyAllInRange(p, owner);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Hits every enemy within the tower's range this tick (Tesla Coil). Broadcasts the
|
||||
// hit positions so a visual component can draw an arc to each.
|
||||
private void ApplyAllInRange(in CombatProfile p, PlayerSlot owner)
|
||||
{
|
||||
int count = Physics.OverlapSphereNonAlloc(
|
||||
transform.position, p.Range, s_overlapBuffer, enemyLayerMask);
|
||||
|
||||
var hitPositions = new List<Vector3>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var eh = s_overlapBuffer[i].GetComponent<EnemyHealth>();
|
||||
if (eh == null || eh.IsDead) continue;
|
||||
HitEnemy(p, eh, owner);
|
||||
hitPositions.Add(eh.transform.position);
|
||||
}
|
||||
|
||||
if (hitPositions.Count > 0)
|
||||
AreaFiredClientRpc(hitPositions.ToArray());
|
||||
}
|
||||
|
||||
private void ApplySplash(in CombatProfile p, EnemyHealth primary,
|
||||
Vector3 origin, PlayerSlot owner)
|
||||
{
|
||||
|
|
@ -513,6 +550,14 @@ namespace TD.Combat
|
|||
// and draw a line renderer through these world positions.
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void AreaFiredClientRpc(Vector3[] hitPositions)
|
||||
{
|
||||
// Visual consumers (TeslaArcVisual) subscribe to OnAreaFired to draw an
|
||||
// electrical arc from the tower's emitter to each hit enemy.
|
||||
OnAreaFired?.Invoke(hitPositions);
|
||||
}
|
||||
|
||||
// ----- NV callback (fires on all peers) ----------------------------
|
||||
|
||||
private void HandleReplicatedTargetChanged(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ namespace TD.Core
|
|||
Fire = 3,
|
||||
Poison = 4,
|
||||
Holy = 5,
|
||||
Electric = 6,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -40,6 +41,9 @@ namespace TD.Core
|
|||
Splash = 1,
|
||||
/// <summary>Chains damage to up to ChainCount additional nearby enemies.</summary>
|
||||
Chain = 2,
|
||||
/// <summary>Hits EVERY enemy within the tower's Range each attack (e.g. Tesla Coil).
|
||||
/// Hitscan only — pairs with a null ProjectilePrefab.</summary>
|
||||
AllInRange = 3,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue