diff --git a/Assets/_Project/Art/Materials/M_Electrical_Glow.mat b/Assets/_Project/Art/Materials/M_Electrical_Glow.mat
index 1dd11fe..558d2db 100644
--- a/Assets/_Project/Art/Materials/M_Electrical_Glow.mat
+++ b/Assets/_Project/Art/Materials/M_Electrical_Glow.mat
@@ -105,7 +105,7 @@ Material:
- _AddPrecomputedVelocity: 0
- _AlphaClip: 0
- _AlphaToMask: 0
- - _Blend: 0
+ - _Blend: 2
- _BlendModePreserveSpecular: 1
- _BumpScale: 1
- _ClearCoatMask: 0
@@ -114,8 +114,8 @@ Material:
- _Cutoff: 0.5
- _DetailAlbedoMapScale: 1
- _DetailNormalMapScale: 1
- - _DstBlend: 10
- - _DstBlendAlpha: 10
+ - _DstBlend: 1
+ - _DstBlendAlpha: 1
- _EnvironmentReflections: 1
- _GlossMapScale: 0
- _Glossiness: 0
diff --git a/Assets/_Project/Scenes/Levels/9Player.unity b/Assets/_Project/Scenes/Levels/9Player.unity
index 76b7f7f..32942ce 100644
--- a/Assets/_Project/Scenes/Levels/9Player.unity
+++ b/Assets/_Project/Scenes/Levels/9Player.unity
@@ -12226,55 +12226,6 @@ Mesh:
- serializedVersion: 1
m_IndexStart: 0
m_IndexCount: 0
---- !u!1 &832575517
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 832575519}
- - component: {fileID: 832575518}
- m_Layer: 0
- m_Name: Global Volume
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!114 &832575518
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 832575517}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 172515602e62fb746b5d573b38a5fe58, type: 3}
- m_Name:
- m_EditorClassIdentifier:
- m_IsGlobal: 1
- priority: 0
- blendDistance: 0
- weight: 1
- sharedProfile: {fileID: 11400000, guid: 10fc4df2da32a41aaa32d77bc913491c, type: 2}
---- !u!4 &832575519
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 832575517}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 0, y: 0, z: 0}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 0}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &839198900
GameObject:
m_ObjectHideFlags: 0
@@ -28745,7 +28696,6 @@ SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 410087041}
- - {fileID: 832575519}
- {fileID: 441239881}
- {fileID: 167151709}
- {fileID: 1507514109}
diff --git a/Assets/_Project/Scripts/Combat/TeslaArcVisual.cs b/Assets/_Project/Scripts/Combat/TeslaArcVisual.cs
index 20de293..c4e7828 100644
--- a/Assets/_Project/Scripts/Combat/TeslaArcVisual.cs
+++ b/Assets/_Project/Scripts/Combat/TeslaArcVisual.cs
@@ -6,35 +6,92 @@ namespace TD.Combat
{
///
/// Local-only electrical-arc visual for
- /// towers (Tesla Coil). Subscribes to and draws a
- /// jagged line from the tower's emitter point to every enemy hit, flashed for a moment.
+ /// towers (Tesla Coil). Subscribes to and draws an
+ /// arced, jittering bolt from the tower's emitter to every enemy hit, re-randomized each
+ /// frame so it crackles, then hidden after a short flash.
///
///
/// Pure visualization — runs on every peer from the replicated fire broadcast; nothing
- /// here is networked. Arcs are pooled s reused across ticks.
+ /// here is networked. Bolts are pooled s reused across ticks.
+ /// The glow (bloom) comes from the material + an HDR color above the bloom threshold; see
+ /// and the project's post-processing Volume.
///
[RequireComponent(typeof(TowerCombat))]
public class TeslaArcVisual : MonoBehaviour
{
- [Tooltip("Point the arcs originate from — the central top of the coil. " +
+ [Tooltip("Point the bolts 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.")]
+ [Tooltip("Material for the bolts. Use an UNLIT ADDITIVE material for a convincing " +
+ "glow (a Lit material won't bloom well on a thin line). If unset, a fallback " +
+ "additive material is created at runtime.")]
[SerializeField] private Material arcMaterial;
- [SerializeField] private Color arcColor = new Color(0.6f, 0.85f, 1f, 1f);
+ [Tooltip("Base bolt color. Multiplied by Emission Intensity to push it into HDR so " +
+ "Bloom picks it up.")]
+ [ColorUsage(true, true)]
+ [SerializeField] private Color arcColor = new Color(0.55f, 0.8f, 1f, 1f);
+
+ [Tooltip("HDR multiplier on the color. Must exceed the Bloom threshold (>1) for the " +
+ "bolt to glow. Raise for a hotter, brighter arc.")]
+ [SerializeField] private float emissionIntensity = 4f;
+
[SerializeField] private float arcWidth = 0.06f;
- [Tooltip("How long each arc flash stays visible, in seconds.")]
+
+ [Tooltip("How long each 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;
+
+ [Tooltip("Points per bolt. More = smoother arc and finer crackle. ~12-20 looks good.")]
+ [SerializeField] private int segmentsPerArc = 16;
+
+ [Tooltip("Overall arc/bow as a fraction of bolt length (0 = straight line). The bolt " +
+ "lifts toward the middle for an electrical-arc curve.")]
+ [SerializeField] private float arcHeight = 0.18f;
+
+ [Tooltip("Random crackle displacement (world units) at the middle of the bolt, " +
+ "tapering to 0 at both ends so it stays anchored to the coil and enemy.")]
+ [SerializeField] private float jitter = 0.18f;
+
+ [Header("Light flash")]
+ [Tooltip("Pulse a real point light at the emitter when firing so the bolts actually " +
+ "illuminate nearby enemies (emission/bloom alone don't cast light).")]
+ [SerializeField] private bool castLight = true;
+
+ [Tooltip("Color of the flash light.")]
+ [SerializeField] private Color lightColor = new Color(0.6f, 0.85f, 1f);
+
+ [Tooltip("Peak intensity of the flash. The light flickers between this and ~70% of " +
+ "it while firing for an electrical pulse.")]
+ [SerializeField] private float lightIntensity = 8f;
+
+ [Tooltip("Range of the emitter flash light — covers enemies near the coil.")]
+ [SerializeField] private float lightRange = 12f;
+
+ [Tooltip("Also flash a small light at each bolt's IMPACT point, so enemies struck at " +
+ "the far edge of range get lit by the bolt — not just those near the coil.")]
+ [SerializeField] private bool castImpactLight = true;
+
+ [Tooltip("Peak intensity of each impact light (usually lower than the emitter flash).")]
+ [SerializeField] private float impactLightIntensity = 4f;
+
+ [Tooltip("Range of each impact light — only needs to cover the struck enemy, so keep it small.")]
+ [SerializeField] private float impactLightRange = 5f;
+
+ [Tooltip("Performance cap on simultaneous impact lights. URP limits how many lights " +
+ "affect each object, and many towers add up — targets beyond this cap rely on " +
+ "the emitter flash. Raise carefully.")]
+ [SerializeField] private int maxImpactLights = 8;
private TowerCombat combat;
private readonly List pool = new List();
+ private Light flashLight;
+ private readonly List impactLightPool = new List();
+
+ // Snapshot of the current flash so Update can re-randomize the bolts each frame.
+ private Vector3[] activeTargets;
+ private int activeCount;
+ private bool active;
private float hideAt = -1f;
private void Awake() => combat = GetComponent();
@@ -51,31 +108,120 @@ namespace TD.Combat
private void Update()
{
- if (hideAt >= 0f && Time.time >= hideAt)
+ if (!active) return;
+
+ if (Time.time >= hideAt)
{
- for (int i = 0; i < pool.Count; i++) pool[i].enabled = false;
- hideAt = -1f;
+ HideAll();
+ active = false;
+ return;
}
+
+ // Re-draw every frame with fresh jitter so the bolts crackle while visible.
+ RedrawBolts();
}
private void HandleAreaFired(Vector3[] positions)
{
- if (positions == null) return;
+ if (positions == null || positions.Length == 0) return;
+ activeTargets = positions; // RPC hands us a fresh array each call — safe to keep
+ activeCount = positions.Length;
+ active = true;
+ hideAt = Time.time + arcDuration;
+
+ RedrawBolts();
+ }
+
+ private void RedrawBolts()
+ {
Vector3 origin = emitter != null ? emitter.position : transform.position + Vector3.up;
- for (int i = 0; i < positions.Length; i++)
+ for (int i = 0; i < activeCount; i++)
{
var lr = GetLine(i);
- DrawArc(lr, origin, positions[i]);
+ DrawBolt(lr, origin, activeTargets[i]);
lr.enabled = true;
}
- // Disable any leftover lines from a previous, larger tick.
- for (int i = positions.Length; i < pool.Count; i++)
+ // Disable any leftover bolts from a previous, larger tick.
+ for (int i = activeCount; i < pool.Count; i++)
pool[i].enabled = false;
- hideAt = Time.time + arcDuration;
+ // Pulse the emitter light so the coil/source lights nearby enemies.
+ if (castLight)
+ {
+ var fl = GetFlashLight();
+ fl.intensity = lightIntensity * Random.Range(0.7f, 1f); // electrical flicker
+ fl.enabled = true;
+ }
+
+ // Pulse a light at each bolt's impact point so far-struck enemies get lit by the
+ // bolt itself. Capped for performance; extra targets rely on the emitter flash.
+ if (castImpactLight)
+ {
+ int lit = Mathf.Min(activeCount, maxImpactLights);
+ for (int i = 0; i < lit; i++)
+ {
+ var il = GetImpactLight(i);
+ il.transform.position = activeTargets[i];
+ il.intensity = impactLightIntensity * Random.Range(0.7f, 1f);
+ il.enabled = true;
+ }
+ for (int i = lit; i < impactLightPool.Count; i++)
+ impactLightPool[i].enabled = false;
+ }
+ }
+
+ private void HideAll()
+ {
+ for (int i = 0; i < pool.Count; i++)
+ pool[i].enabled = false;
+
+ if (flashLight != null) flashLight.enabled = false;
+ for (int i = 0; i < impactLightPool.Count; i++)
+ impactLightPool[i].enabled = false;
+ }
+
+ // Lazily creates a small world-space point light for a bolt's impact point.
+ private Light GetImpactLight(int index)
+ {
+ while (impactLightPool.Count <= index)
+ {
+ var go = new GameObject($"ArcImpactLight_{impactLightPool.Count}");
+ go.transform.SetParent(transform, worldPositionStays: true);
+
+ var l = go.AddComponent();
+ l.type = LightType.Point;
+ l.color = lightColor;
+ l.range = impactLightRange;
+ l.shadows = LightShadows.None;
+ l.intensity = 0f;
+ l.enabled = false;
+ impactLightPool.Add(l);
+ }
+ return impactLightPool[index];
+ }
+
+ // Lazily creates the point light at the emitter (parented so it tracks the coil).
+ private Light GetFlashLight()
+ {
+ if (flashLight == null)
+ {
+ var go = new GameObject("ArcFlashLight");
+ Transform parent = emitter != null ? emitter : transform;
+ go.transform.SetParent(parent, worldPositionStays: false);
+ go.transform.localPosition = Vector3.zero;
+
+ flashLight = go.AddComponent();
+ flashLight.type = LightType.Point;
+ flashLight.color = lightColor;
+ flashLight.range = lightRange;
+ flashLight.shadows = LightShadows.None; // perf: brief flashes don't need shadows
+ flashLight.intensity = 0f;
+ flashLight.enabled = false;
+ }
+ return flashLight;
}
private LineRenderer GetLine(int index)
@@ -84,33 +230,74 @@ namespace TD.Combat
{
var go = new GameObject($"Arc_{pool.Count}");
go.transform.SetParent(transform, worldPositionStays: false);
+
var lr = go.AddComponent();
- lr.useWorldSpace = true;
- lr.widthMultiplier = arcWidth;
- lr.numCapVertices = 2;
- lr.textureMode = LineTextureMode.Stretch;
+ lr.useWorldSpace = true;
+ lr.widthMultiplier = arcWidth;
+ lr.numCapVertices = 4;
+ lr.numCornerVertices = 4;
+ lr.alignment = LineAlignment.View; // face the camera, like a billboard
+ 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;
+ lr.material = arcMaterial != null ? arcMaterial : CreateFallbackMaterial();
+
+ // HDR color so Bloom in the post stack picks the bolt up as a glow.
+ Color hdr = arcColor * Mathf.Max(1f, emissionIntensity);
+ hdr.a = 1f;
+ lr.startColor = lr.endColor = hdr;
+
+ lr.enabled = false;
pool.Add(lr);
}
return pool[index];
}
- private void DrawArc(LineRenderer lr, Vector3 a, Vector3 b)
+ // Additive unlit fallback so bolts still glow if no material is assigned. Prefer
+ // assigning a real material in the inspector.
+ private static Material CreateFallbackMaterial()
+ {
+ var shader = Shader.Find("Universal Render Pipeline/Particles/Unlit")
+ ?? Shader.Find("Sprites/Default");
+ return new Material(shader);
+ }
+
+ // Builds an arced, jagged bolt from a to b. A smooth sine bow gives the overall arc;
+ // tapered random perpendicular offsets give the electrical crackle. Endpoints stay
+ // exactly on the coil and the enemy.
+ private void DrawBolt(LineRenderer lr, Vector3 a, Vector3 b)
{
int n = Mathf.Max(2, segmentsPerArc);
lr.positionCount = n;
+
+ Vector3 delta = b - a;
+ float len = delta.magnitude;
+ Vector3 dir = len > 1e-4f ? delta / len : Vector3.forward;
+
+ // Bow toward world-up, projected perpendicular to the bolt (any perpendicular for
+ // near-vertical bolts), plus a sideways axis for 3D crackle.
+ Vector3 bowDir = Vector3.up - dir * Vector3.Dot(Vector3.up, dir);
+ if (bowDir.sqrMagnitude < 1e-4f) bowDir = Vector3.Cross(dir, Vector3.right);
+ bowDir.Normalize();
+ Vector3 sideDir = Vector3.Cross(dir, bowDir);
+
+ float bow = arcHeight * len;
+
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.
+ Vector3 p = a + delta * t;
+
+ // Smooth arc — sine peaks at the middle, zero at both ends.
+ float curve = Mathf.Sin(t * Mathf.PI);
+ p += bowDir * (bow * curve);
+
+ // Crackle on interior points, tapered to nothing at the ends.
if (s != 0 && s != n - 1)
- p += Random.insideUnitSphere * jitter;
+ {
+ p += bowDir * (Random.Range(-jitter, jitter) * curve);
+ p += sideDir * (Random.Range(-jitter, jitter) * curve);
+ }
+
lr.SetPosition(s, p);
}
}
diff --git a/Assets/_Project/Scripts/Combat/TowerCombat.cs b/Assets/_Project/Scripts/Combat/TowerCombat.cs
index 1c37234..25006a3 100644
--- a/Assets/_Project/Scripts/Combat/TowerCombat.cs
+++ b/Assets/_Project/Scripts/Combat/TowerCombat.cs
@@ -473,8 +473,6 @@ namespace TD.Combat
{
var multiplier = GetOwnerBuffManager()?.GetMultiplier(BuffStat.Damage) ?? 1f;
float effectiveDamage = p.Damage * multiplier;
- Debug.Log("multiplier: " + multiplier);
- Debug.Log("Effective damage: " + effectiveDamage);
target.TakeDamage(effectiveDamage, p.DamageType, owner);
ApplyStatusEffect(p, target, owner);
}
diff --git a/Assets/_Project/Settings/DefaultVolumeProfile.asset b/Assets/_Project/Settings/DefaultVolumeProfile.asset
index 66d98de..1c6d468 100644
--- a/Assets/_Project/Settings/DefaultVolumeProfile.asset
+++ b/Assets/_Project/Settings/DefaultVolumeProfile.asset
@@ -37,13 +37,13 @@ MonoBehaviour:
active: 1
shadows:
m_OverrideState: 1
- m_Value: {r: 0.5, g: 0.5, b: 0.5, a: 1}
+ m_Value: {r: 0.5458793, g: 0.6841803, b: 0.8207547, a: 1}
highlights:
m_OverrideState: 1
- m_Value: {r: 0.5, g: 0.5, b: 0.5, a: 1}
+ m_Value: {r: 0.9811321, g: 0.7474893, b: 0.50445, a: 1}
balance:
m_OverrideState: 1
- m_Value: 0
+ m_Value: -25
--- !u!114 &-8104416584915340131
MonoBehaviour:
m_ObjectHideFlags: 3
@@ -161,10 +161,10 @@ MonoBehaviour:
active: 1
postExposure:
m_OverrideState: 1
- m_Value: 0
+ m_Value: -0.2
contrast:
m_OverrideState: 1
- m_Value: 0
+ m_Value: 20
colorFilter:
m_OverrideState: 1
m_Value: {r: 1, g: 1, b: 1, a: 1}
@@ -173,7 +173,7 @@ MonoBehaviour:
m_Value: 0
saturation:
m_OverrideState: 1
- m_Value: 0
+ m_Value: -25
--- !u!114 &-6288072647309666549
MonoBehaviour:
m_ObjectHideFlags: 3
@@ -192,7 +192,7 @@ MonoBehaviour:
m_Value: 0
intensity:
m_OverrideState: 1
- m_Value: 0
+ m_Value: 0.3
response:
m_OverrideState: 1
m_Value: 0.8
@@ -214,7 +214,7 @@ MonoBehaviour:
active: 1
mode:
m_OverrideState: 1
- m_Value: 0
+ m_Value: 2
neutralHDRRangeReductionMode:
m_OverrideState: 1
m_Value: 2
@@ -363,10 +363,10 @@ MonoBehaviour:
m_Value: 1
threshold:
m_OverrideState: 1
- m_Value: 0.9
+ m_Value: 0.7
intensity:
m_OverrideState: 1
- m_Value: 0
+ m_Value: 1
scatter:
m_OverrideState: 1
m_Value: 0.7
@@ -410,10 +410,10 @@ MonoBehaviour:
active: 1
temperature:
m_OverrideState: 1
- m_Value: 0
+ m_Value: -8
tint:
m_OverrideState: 1
- m_Value: 0
+ m_Value: 5
--- !u!114 &-581120513425526550
MonoBehaviour:
m_ObjectHideFlags: 3
@@ -855,7 +855,7 @@ MonoBehaviour:
active: 1
intensity:
m_OverrideState: 1
- m_Value: 0
+ m_Value: 0.1
--- !u!114 &6940869943325143175
MonoBehaviour:
m_ObjectHideFlags: 3
@@ -890,7 +890,7 @@ MonoBehaviour:
m_Value: {x: 0.5, y: 0.5}
intensity:
m_OverrideState: 1
- m_Value: 0
+ m_Value: 0.35
smoothness:
m_OverrideState: 1
m_Value: 0.2
diff --git a/Assets/_Project/Settings/Render Settings/URP-High.asset b/Assets/_Project/Settings/Render Settings/URP-High.asset
index 074ee15..330830a 100644
--- a/Assets/_Project/Settings/Render Settings/URP-High.asset
+++ b/Assets/_Project/Settings/Render Settings/URP-High.asset
@@ -45,7 +45,7 @@ MonoBehaviour:
m_MainLightShadowsSupported: 1
m_MainLightShadowmapResolution: 4096
m_AdditionalLightsRenderingMode: 1
- m_AdditionalLightsPerObjectLimit: 2
+ m_AdditionalLightsPerObjectLimit: 6
m_AdditionalLightShadowsSupported: 1
m_AdditionalLightsShadowmapResolution: 4096
m_AdditionalLightsShadowResolutionTierLow: 256
diff --git a/Assets/_Project/Settings/Render Settings/URP-Medium.asset b/Assets/_Project/Settings/Render Settings/URP-Medium.asset
index 3b9f2ab..c5c5444 100644
--- a/Assets/_Project/Settings/Render Settings/URP-Medium.asset
+++ b/Assets/_Project/Settings/Render Settings/URP-Medium.asset
@@ -45,7 +45,7 @@ MonoBehaviour:
m_MainLightShadowsSupported: 1
m_MainLightShadowmapResolution: 2048
m_AdditionalLightsRenderingMode: 1
- m_AdditionalLightsPerObjectLimit: 2
+ m_AdditionalLightsPerObjectLimit: 4
m_AdditionalLightShadowsSupported: 1
m_AdditionalLightsShadowmapResolution: 2048
m_AdditionalLightsShadowResolutionTierLow: 256
diff --git a/Assets/_Project/Settings/Render Settings/URP-Ultra.asset b/Assets/_Project/Settings/Render Settings/URP-Ultra.asset
index 9564d20..46ffed0 100644
--- a/Assets/_Project/Settings/Render Settings/URP-Ultra.asset
+++ b/Assets/_Project/Settings/Render Settings/URP-Ultra.asset
@@ -45,7 +45,7 @@ MonoBehaviour:
m_MainLightShadowsSupported: 1
m_MainLightShadowmapResolution: 8192
m_AdditionalLightsRenderingMode: 1
- m_AdditionalLightsPerObjectLimit: 2
+ m_AdditionalLightsPerObjectLimit: 8
m_AdditionalLightShadowsSupported: 1
m_AdditionalLightsShadowmapResolution: 8192
m_AdditionalLightsShadowResolutionTierLow: 256