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/Definitions/Towers/BasicArrowTower.asset b/Assets/_Project/Definitions/Towers/BasicArrowTower.asset index 7709fe3..3a1c5c4 100644 --- a/Assets/_Project/Definitions/Towers/BasicArrowTower.asset +++ b/Assets/_Project/Definitions/Towers/BasicArrowTower.asset @@ -25,7 +25,7 @@ MonoBehaviour: GroundedOnly: 0 Damage: 10 Range: 20 - FireRate: 5 + AttacksPerSecond: 5 SplashRadius: 0 ChainCount: 0 ChainRange: 0 diff --git a/Assets/_Project/Definitions/Towers/SiegeCannonTower.asset b/Assets/_Project/Definitions/Towers/SiegeCannonTower.asset index f8b6521..7a6ead6 100644 --- a/Assets/_Project/Definitions/Towers/SiegeCannonTower.asset +++ b/Assets/_Project/Definitions/Towers/SiegeCannonTower.asset @@ -27,7 +27,7 @@ MonoBehaviour: GroundedOnly: 1 Damage: 25 Range: 16 - AttacksPerSecond: 0 + AttacksPerSecond: 0.8 SplashRadius: 3 ChainCount: 0 ChainRange: 0 diff --git a/Assets/_Project/Definitions/Towers/TeslaCoilTower.asset b/Assets/_Project/Definitions/Towers/TeslaCoilTower.asset index c122a53..aa5b2b6 100644 --- a/Assets/_Project/Definitions/Towers/TeslaCoilTower.asset +++ b/Assets/_Project/Definitions/Towers/TeslaCoilTower.asset @@ -27,7 +27,7 @@ MonoBehaviour: GroundedOnly: 0 Damage: 5 Range: 10 - AttacksPerSecond: 0 + AttacksPerSecond: 1.5 SplashRadius: 0 ChainCount: 0 ChainRange: 0 diff --git a/Assets/_Project/Definitions/Towers/Wall.asset b/Assets/_Project/Definitions/Towers/Wall.asset index 8ee63b7..ead40ed 100644 --- a/Assets/_Project/Definitions/Towers/Wall.asset +++ b/Assets/_Project/Definitions/Towers/Wall.asset @@ -24,7 +24,7 @@ MonoBehaviour: GroundedOnly: 0 Damage: 0 Range: 0 - FireRate: 0 + AttacksPerSecond: 0 SplashRadius: 0 ChainCount: 0 ChainRange: 0 diff --git a/Assets/_Project/Prefabs/Towers/Tower_TeslaCoil.prefab b/Assets/_Project/Prefabs/Towers/Tower_TeslaCoil.prefab index 959f827..cff0c5b 100644 --- a/Assets/_Project/Prefabs/Towers/Tower_TeslaCoil.prefab +++ b/Assets/_Project/Prefabs/Towers/Tower_TeslaCoil.prefab @@ -99,7 +99,6 @@ GameObject: - component: {fileID: 805962841523123163} - component: {fileID: 8853488620519990682} - component: {fileID: 4524938955752569428} - - component: {fileID: 42814861218671054} - component: {fileID: -2590971686077483651} m_Layer: 0 m_Name: Tower_TeslaCoil @@ -147,7 +146,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} m_Name: m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject - GlobalObjectIdHash: 280718094 + GlobalObjectIdHash: 1363936954 InScenePlacedSourceGlobalObjectIdHash: 0 DeferredDespawnTick: 0 Ownership: 1 @@ -224,103 +223,6 @@ MonoBehaviour: arcDuration: 0.12 segmentsPerArc: 6 jitter: 0.2 ---- !u!82 &42814861218671054 -AudioSource: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 6482414459531823157} - m_Enabled: 1 - serializedVersion: 4 - OutputAudioMixerGroup: {fileID: 0} - m_audioClip: {fileID: 0} - m_Resource: {fileID: 0} - m_PlayOnAwake: 0 - m_Volume: 0.5 - m_Pitch: 1 - Loop: 0 - Mute: 0 - Spatialize: 0 - SpatializePostEffects: 0 - Priority: 128 - DopplerLevel: 1 - MinDistance: 1 - MaxDistance: 500 - Pan2D: 0 - rolloffMode: 0 - BypassEffects: 0 - BypassListenerEffects: 0 - BypassReverbZones: 0 - rolloffCustomCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 1 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - panLevelCustomCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - spreadCustomCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - reverbZoneMixCustomCurve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 0 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 --- !u!114 &-2590971686077483651 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/_Project/Scenes/Levels/9Player.unity b/Assets/_Project/Scenes/Levels/9Player.unity index 76b7f7f..ad268e9 100644 --- a/Assets/_Project/Scenes/Levels/9Player.unity +++ b/Assets/_Project/Scenes/Levels/9Player.unity @@ -2222,6 +2222,63 @@ Transform: m_Children: [] m_Father: {fileID: 1097152085} m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0} +--- !u!1 &176580460 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 176580462} + - component: {fileID: 176580461} + m_Layer: 0 + m_Name: AudioManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &176580461 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 176580460} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9ec21c01769ef58d1ac26ba89668eeaa, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::TD.Audio.AudioManager + categories: + - category: 0 + maxVoices: 4 + volume: 0.5 + - category: 1 + maxVoices: 4 + volume: 0.5 + - category: 2 + maxVoices: 4 + volume: 0.5 + - category: 3 + maxVoices: 4 + volume: 0.5 +--- !u!4 &176580462 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 176580460} + 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!43 &203092944 Mesh: m_ObjectHideFlags: 0 @@ -12226,55 +12283,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 +28753,6 @@ SceneRoots: m_ObjectHideFlags: 0 m_Roots: - {fileID: 410087041} - - {fileID: 832575519} - {fileID: 441239881} - {fileID: 167151709} - {fileID: 1507514109} @@ -28764,3 +28771,4 @@ SceneRoots: - {fileID: 1097152085} - {fileID: 1889390964} - {fileID: 2139601601} + - {fileID: 176580462} diff --git a/Assets/_Project/Scripts/Audio.meta b/Assets/_Project/Scripts/Audio.meta new file mode 100644 index 0000000..d399f53 --- /dev/null +++ b/Assets/_Project/Scripts/Audio.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d11cb1aeddfee3009879738a45665945 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Scripts/Audio/AudioCategory.cs b/Assets/_Project/Scripts/Audio/AudioCategory.cs new file mode 100644 index 0000000..ef63198 --- /dev/null +++ b/Assets/_Project/Scripts/Audio/AudioCategory.cs @@ -0,0 +1,5 @@ +// Assets/_Project/Scripts/Audio/AudioCategory.cs +namespace TD.Audio +{ + public enum AudioCategory { TowerFire, EnemyHitDeath, UI, Music } +} diff --git a/Assets/_Project/Scripts/Audio/AudioCategory.cs.meta b/Assets/_Project/Scripts/Audio/AudioCategory.cs.meta new file mode 100644 index 0000000..3947300 --- /dev/null +++ b/Assets/_Project/Scripts/Audio/AudioCategory.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2f652b06e3d150798895893892820cc7 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Audio/AudioManager.cs b/Assets/_Project/Scripts/Audio/AudioManager.cs new file mode 100644 index 0000000..961a820 --- /dev/null +++ b/Assets/_Project/Scripts/Audio/AudioManager.cs @@ -0,0 +1,74 @@ +// Assets/_Project/Scripts/Audio/AudioManager.cs +using System.Collections.Generic; +using UnityEngine; + +namespace TD.Audio +{ + public class AudioManager : MonoBehaviour + { + [System.Serializable] + public struct CategoryConfig + { + public AudioCategory category; + public int maxVoices; + [Range(0f, 1f)] + public float volume; + } + + public static AudioManager Instance { get; private set; } + + [SerializeField] private CategoryConfig[] categories; + + private Dictionary pools; + private Dictionary indices; + + private void Awake() + { + if (Instance != null) + { + Destroy(gameObject); + return; + } + Instance = this; + InitializePools(); + } + + private void OnDestroy() + { + if (Instance == this) Instance = null; + } + + private void InitializePools() + { + pools = new Dictionary(); + indices = new Dictionary(); + + foreach (var config in categories) + { + var pool = new AudioSource[config.maxVoices]; + for (int i = 0; i < config.maxVoices; i++) + { + var src = gameObject.AddComponent(); + src.playOnAwake = false; + src.volume = config.volume; + pool[i] = src; + } + pools[config.category] = pool; + indices[config.category] = 0; + } + } + + public void Play(AudioClip clip, AudioCategory category, float pitch = 1f) + { + if (clip == null) return; + if (!pools.TryGetValue(category, out var pool)) return; + + int idx = indices[category]; + var src = pool[idx % pool.Length]; + src.pitch = pitch; + src.clip = clip; + src.Play(); + indices[category] = idx + 1; + } + } +} diff --git a/Assets/_Project/Scripts/Audio/AudioManager.cs.meta b/Assets/_Project/Scripts/Audio/AudioManager.cs.meta new file mode 100644 index 0000000..855be40 --- /dev/null +++ b/Assets/_Project/Scripts/Audio/AudioManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9ec21c01769ef58d1ac26ba89668eeaa \ No newline at end of file diff --git a/Assets/_Project/Scripts/Audio/README.md b/Assets/_Project/Scripts/Audio/README.md new file mode 100644 index 0000000..2f7d81c --- /dev/null +++ b/Assets/_Project/Scripts/Audio/README.md @@ -0,0 +1,36 @@ +# Audio System + +Centralised voice-limited audio for the game. Lives in `Assets/_Project/Scripts/Audio/`. + +## How it works + +`AudioManager` is a per-scene MonoBehaviour singleton. On `Awake` it pre-allocates a pool of `AudioSource` components (on its own GameObject) for each sound category. Callers request playback via `AudioManager.Instance.Play(clip, category, pitch)` — the manager picks the next source in that category's pool using round-robin, interrupting the oldest sound if all voices are busy. + +## Categories (`AudioCategory` enum) + +| Category | Typical use | +|---|---| +| `TowerFire` | Tower attack sounds | +| `EnemyHitDeath` | Enemy damage and death sounds | +| `UI` | Button clicks, placement confirmation, etc. | +| `Music` | Background music | + +Voice limits and volume per category are configured in the Inspector on the `AudioManager` scene GameObject. + +## Scene setup + +An empty GameObject named `AudioManager` must exist in each gameplay scene with the `AudioManager` component attached. Configure the `Categories` array in the Inspector — one entry per `AudioCategory` value. + +Suggested defaults: +- TowerFire: 4 voices, volume 1.0 +- EnemyHitDeath: 8 voices, volume 1.0 +- UI: 4 voices, volume 1.0 +- Music: 2 voices, volume 0.8 + +## Adding a new sound-emitting component + +1. Get a reference to an `AudioClip` via `[SerializeField]` +2. Call `AudioManager.Instance?.Play(clip, AudioCategory.YourCategory)` at the point the sound should trigger +3. Pitch randomization (if desired) is the caller's responsibility: `Random.Range(minPitch, maxPitch)` passed as the third argument + +See `TeslaTowerFireSound.cs` in `Scripts/Combat/` as a reference implementation. diff --git a/Assets/_Project/Scripts/Audio/README.md.meta b/Assets/_Project/Scripts/Audio/README.md.meta new file mode 100644 index 0000000..284147a --- /dev/null +++ b/Assets/_Project/Scripts/Audio/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4acf87ebbb79a6ce482e6e4767d27b41 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: 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/TeslaTowerFireSound.cs b/Assets/_Project/Scripts/Combat/TeslaTowerFireSound.cs index 69de453..9294b85 100644 --- a/Assets/_Project/Scripts/Combat/TeslaTowerFireSound.cs +++ b/Assets/_Project/Scripts/Combat/TeslaTowerFireSound.cs @@ -1,4 +1,5 @@ // Assets/_Project/Scripts/Combat/TeslaTowerFireSound.cs +using TD.Audio; using UnityEngine; namespace TD.Combat @@ -14,7 +15,6 @@ namespace TD.Combat /// as the Tesla Coil gains more audio (idle hum, charge-up, etc.). /// [RequireComponent(typeof(TowerCombat))] - [RequireComponent(typeof(AudioSource))] public class TeslaTowerFireSound : MonoBehaviour { [Tooltip("Sound played each time the coil discharges.")] @@ -25,12 +25,10 @@ namespace TD.Combat [SerializeField] private float maxPitch = 1.07f; private TowerCombat combat; - private AudioSource audioSource; private void Awake() { - combat = GetComponent(); - audioSource = GetComponent(); + combat = GetComponent(); } private void OnEnable() @@ -45,12 +43,7 @@ namespace TD.Combat private void HandleAreaFired(Vector3[] positions) { - if (fireClip != null) - { - audioSource.pitch = Random.Range(minPitch, maxPitch); - audioSource.clip = fireClip; - audioSource.Play(); - } + AudioManager.Instance?.Play(fireClip, AudioCategory.TowerFire, Random.Range(minPitch, maxPitch)); } } } diff --git a/Assets/_Project/Scripts/Combat/TowerCombat.cs b/Assets/_Project/Scripts/Combat/TowerCombat.cs index 657d5f8..9c9ac7c 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 diff --git a/Assets/_Recovery.meta b/Assets/_Recovery.meta new file mode 100644 index 0000000..701aa9b --- /dev/null +++ b/Assets/_Recovery.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4da4faa336600407eba8a8b842ddebe1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json index 5343bc8..6269d02 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -4,7 +4,7 @@ "com.unity.cinemachine": "3.1.6", "com.unity.collab-proxy": "2.12.4", "com.unity.entities.graphics": "6.4.0", - "com.unity.ide.rider": "3.0.39", + "com.unity.ide.rider": "3.0.40", "com.unity.ide.visualstudio": "2.0.27", "com.unity.inputsystem": "1.19.0", "com.unity.multiplayer.center": "1.0.1", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index e619138..a89bbf9 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -86,7 +86,7 @@ "dependencies": {} }, "com.unity.ide.rider": { - "version": "3.0.39", + "version": "3.0.40", "depth": 0, "source": "registry", "dependencies": { diff --git a/ProjectSettings/Packages/com.unity.probuilder/Settings.json b/ProjectSettings/Packages/com.unity.probuilder/Settings.json index c46a6fa..f70e4ce 100644 --- a/ProjectSettings/Packages/com.unity.probuilder/Settings.json +++ b/ProjectSettings/Packages/com.unity.probuilder/Settings.json @@ -155,11 +155,6 @@ "type": "UnityEngine.ProBuilder.RectSelectMode, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", "key": "editor.dragSelectRectMode", "value": "{\"m_Value\":0}" - }, - { - "type": "UnityEditor.ProBuilder.Actions.ExportAssetOptions, Unity.ProBuilder.Editor, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "key": "export.assetOptions", - "value": "{\"m_Value\":{\"makePrefab\":false,\"replaceOriginal\":false}}" } ] }