diff --git a/src/MadnessInteractiveReloaded/Character/Systems/CharacterPositionSystem.cs b/src/MadnessInteractiveReloaded/Character/Systems/CharacterPositionSystem.cs index 5e027c9a..d9f3be6b 100644 --- a/src/MadnessInteractiveReloaded/Character/Systems/CharacterPositionSystem.cs +++ b/src/MadnessInteractiveReloaded/Character/Systems/CharacterPositionSystem.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using Walgelijk; using Walgelijk.AssetManager; using Walgelijk.Physics; diff --git a/src/MadnessInteractiveReloaded/Character/Systems/PlayerCharacterSystem.cs b/src/MadnessInteractiveReloaded/Character/Systems/PlayerCharacterSystem.cs index 89111804..8acb0492 100644 --- a/src/MadnessInteractiveReloaded/Character/Systems/PlayerCharacterSystem.cs +++ b/src/MadnessInteractiveReloaded/Character/Systems/PlayerCharacterSystem.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using System.Runtime.CompilerServices; using Walgelijk; @@ -73,12 +73,6 @@ public override void Update() Utilities.NanFallback(Input.WorldMousePosition) : (character.Positioning.Head.GlobalPosition + new Vector2(character.Positioning.FlipScaling * 10000, 0)); - //if (equipped != null && equipped.Data.WeaponType is WeaponType.Firearm) - //{ - // var th = float.Atan2(character.AimDirection.Y, character.AimDirection.X) * character.Positioning.fli; - // character.AimTargetPosition += Vector2.TransformNormal(new Vector2(0, equipped.BarrelEndPoint.Y), Matrix3x2.CreateRotation(th)); - //} - character.RelativeAimTargetPosition = character.AimTargetPosition - character.AimOrigin; } else diff --git a/src/MadnessInteractiveReloaded/Level editor/Components/LevelEditor.LevelEditorComponent.cs b/src/MadnessInteractiveReloaded/Level editor/Components/LevelEditor.LevelEditorComponent.cs index 8f19442a..3488f095 100644 --- a/src/MadnessInteractiveReloaded/Level editor/Components/LevelEditor.LevelEditorComponent.cs +++ b/src/MadnessInteractiveReloaded/Level editor/Components/LevelEditor.LevelEditorComponent.cs @@ -1,4 +1,4 @@ -using MIR.LevelEditor.Objects; +using MIR.LevelEditor.Objects; using Newtonsoft.Json; using System; using System.Collections.Generic; diff --git a/src/MadnessInteractiveReloaded/Level/Systems/EnemySpawningSystem.cs b/src/MadnessInteractiveReloaded/Level/Systems/EnemySpawningSystem.cs index 766168d8..64c64533 100644 --- a/src/MadnessInteractiveReloaded/Level/Systems/EnemySpawningSystem.cs +++ b/src/MadnessInteractiveReloaded/Level/Systems/EnemySpawningSystem.cs @@ -1,4 +1,4 @@ -using MIR.LevelEditor.Objects; +using MIR.LevelEditor.Objects; using System; using System.Collections.Generic; using System.Linq; @@ -14,12 +14,54 @@ namespace MIR; public class EnemySpawningSystem : Walgelijk.System { private readonly List routines = []; + private readonly HashSet liveEnemies = new(); + private int cachedEnemyCount = 0; + + public override void OnActivate() + { + // Force initial count + _ = GetLiveEnemyCount(); + } public override void OnDeactivate() { foreach (var r in routines) RoutineScheduler.Stop(r); + routines.Clear(); + liveEnemies.Clear(); + cachedEnemyCount = 0; + } + + private void UpdateEnemyStatus(Entity entity, CharacterComponent character) + { + if (!MadnessUtils.FindPlayer(Scene, out _, out var player)) + { + // No player found, clear everything + if (liveEnemies.Count > 0) + { + liveEnemies.Clear(); + cachedEnemyCount = 0; + } + return; + } + + bool isEnemy = character.IsAlive + && !Scene.HasTag(entity, Tags.Player) + && character.Faction.IsEnemiesWith(player.Faction); + + bool wasEnemy = liveEnemies.Contains(entity); + + if (isEnemy && !wasEnemy) + { + liveEnemies.Add(entity); + cachedEnemyCount++; + } + else if (!isEnemy && wasEnemy) + { + liveEnemies.Remove(entity); + cachedEnemyCount--; + } } public override void Update() @@ -27,6 +69,17 @@ public override void Update() if (MadnessUtils.IsPaused(Scene) || MadnessUtils.EditingInExperimentMode(Scene) || MadnessUtils.IsCutscenePlaying(Scene)) return; + // Check for character state changes + foreach (var character in Scene.GetAllComponentsOfType()) + { + bool isCurrentlyEnemy = liveEnemies.Contains(character.Entity); + if (character.IsAlive != isCurrentlyEnemy) + { + // Character state changed (died or revived) + UpdateEnemyStatus(character.Entity, character); + } + } + if (!Scene.FindAnyComponent(out var spawningComponent) || !spawningComponent.Enabled) return; @@ -120,12 +173,21 @@ private int GetLiveEnemyCount() if (!MadnessUtils.FindPlayer(Scene, out _, out var player)) return 0; - // TODO improve speed - return Scene.GetAllComponentsOfType().Count(c => - c.IsAlive - && !Scene.HasTag(c.Entity, Tags.Player) - && c.Faction.IsEnemiesWith(player.Faction) - ); + // Perform full recount + liveEnemies.Clear(); + + foreach (var character in Scene.GetAllComponentsOfType()) + { + if (character.IsAlive + && !Scene.HasTag(character.Entity, Tags.Player) + && character.Faction.IsEnemiesWith(player.Faction)) + { + liveEnemies.Add(character.Entity); + } + } + + cachedEnemyCount = liveEnemies.Count; + return cachedEnemyCount; } private int GetMaxEnemyCount(EnemySpawningComponent spawningComponent, int liveEnemyCount, LevelProgressComponent? lvlProgress) diff --git a/src/MadnessInteractiveReloaded/MadnessInteractiveReloaded.cs b/src/MadnessInteractiveReloaded/MadnessInteractiveReloaded.cs index 76f118d7..cd00c0ee 100644 --- a/src/MadnessInteractiveReloaded/MadnessInteractiveReloaded.cs +++ b/src/MadnessInteractiveReloaded/MadnessInteractiveReloaded.cs @@ -1,4 +1,4 @@ -using MIR.Cutscenes; +using MIR.Cutscenes; using System; using System.Diagnostics; using System.Globalization; diff --git a/src/MadnessInteractiveReloaded/Program.cs b/src/MadnessInteractiveReloaded/Program.cs index 9d415cb3..e723c03c 100644 --- a/src/MadnessInteractiveReloaded/Program.cs +++ b/src/MadnessInteractiveReloaded/Program.cs @@ -1,9 +1,10 @@ -using HarmonyLib; +using HarmonyLib; using System; using System.IO; using System.Linq; using Walgelijk; using Walgelijk.AssetManager; +using System.Text; namespace MIR; @@ -138,27 +139,26 @@ private static void Main(string? mode = null, string? input = null, string? outp return; } +#endif } private static void WriteLog(string path) { - // Add a disk logger to the ILogger thing - - //var impl = Logger.Implementations.FirstOrDefault(a => a is DiskLogger); - //if (impl == null || impl is not DiskLogger diskLogger) - //{ - // File.WriteAllText(path, "Disk logger could not be found. No log was recorded. This is catastrophic."); - // return; - //} - //try - //{ - // File.Copy(diskLogger.TargetPath, path); - //} - //catch (Exception e) - //{ - // File.WriteAllText(path, "Log could not be copied to target location: " + e); - // return; - //} + try + { + var logContent = new StringBuilder(); + logContent.AppendLine($"Crash log generated at {DateTime.Now}"); + logContent.AppendLine($"Game Version: {GameVersion.Version}"); + logContent.AppendLine($"OS: {Environment.OSVersion}"); + logContent.AppendLine($"Runtime: {Environment.Version}"); + + // Write the log content to file + File.WriteAllText(path, logContent.ToString()); + } + catch (Exception e) + { + File.WriteAllText(path, $"Failed to write crash log: {e}"); + } } private static void WriteComponents(string path) @@ -219,6 +219,5 @@ private static void WriteFieldsProperties(T obj, StreamWriter w) where T : no var value = item.GetValue(obj); w.WriteLine("\t\tp_{0}: {1}", item.Name, value); } -#endif } } diff --git a/src/MadnessInteractiveReloaded/Registries/Registry.cs b/src/MadnessInteractiveReloaded/Registries/Registry.cs index 7a50ce4c..896ebf7d 100644 --- a/src/MadnessInteractiveReloaded/Registries/Registry.cs +++ b/src/MadnessInteractiveReloaded/Registries/Registry.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -8,12 +8,47 @@ namespace MIR; public class Registry : IRegistry where T : class { private readonly ConcurrentDictionary dict = new(); + private readonly bool disposeOnRemove; + + public Registry(bool disposeOnRemove = true) + { + this.disposeOnRemove = disposeOnRemove; + } public int Count => dict.Count; - public void Clear() => dict.Clear(); + public void Clear() + { + if (disposeOnRemove) + { + foreach (var item in dict.Values) + { + if (item is IDisposable disposable) + { + try + { + disposable.Dispose(); + } + catch (Exception e) + { + Logger.Error($"Error disposing item in registry: {e}"); + } + } + } + } + dict.Clear(); + } - public T Get(string key) => dict[key]; + public T Get(string key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + + if (dict.TryGetValue(key, out var value)) + return value; + + throw new KeyNotFoundException($"Key '{key}' not found in registry"); + } public IEnumerable GetAllKeys() => dict.Keys; @@ -41,17 +76,53 @@ public bool TryGetKeyFor(T value, [NotNullWhen(true)] out string? key) public void Unregister(string key) { - dict.TryRemove(key, out _); //👍👍 + if (key == null) + throw new ArgumentNullException(nameof(key)); + + if (dict.TryRemove(key, out var value) && disposeOnRemove && value is IDisposable disposable) + { + try + { + disposable.Dispose(); + } + catch (Exception e) + { + Logger.Error($"Error disposing value for key '{key}': {e}"); + } + } } public void Register(string key, T val) { - if (!dict.TryAdd(key, val)) + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (val == null) + throw new ArgumentNullException(nameof(val)); + + if (dict.TryGetValue(key, out var oldValue)) + { + if (disposeOnRemove && oldValue is IDisposable disposable) + { + try + { + disposable.Dispose(); + } + catch (Exception e) + { + Logger.Error($"Error disposing old value for key '{key}': {e}"); + } + } + + if (!dict.TryUpdate(key, val, oldValue)) + { + throw new InvalidOperationException($"Concurrent modification detected while updating key '{key}'"); + } + Logger.Log($"Registry {GetType().Name} replaced value at key '{key}'"); + } + else if (!dict.TryAdd(key, val)) { - dict[key] = val; - Logger.Log($"Registry {this} replaced {key}"); + throw new InvalidOperationException($"Failed to add value at key '{key}' - concurrent modification detected"); } - //throw new Exception($"Already registered a value at {key}"); } public T this[string key] => Get(key); diff --git a/src/MadnessInteractiveReloaded/User interface/Systems/CampaignMenuSystem.cs b/src/MadnessInteractiveReloaded/User interface/Systems/CampaignMenuSystem.cs index 82ad9b36..1ada8793 100644 --- a/src/MadnessInteractiveReloaded/User interface/Systems/CampaignMenuSystem.cs +++ b/src/MadnessInteractiveReloaded/User interface/Systems/CampaignMenuSystem.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Numerics; using Walgelijk; using Walgelijk.AssetManager; @@ -17,9 +17,15 @@ public class CampaignMenuSystem : Walgelijk.System // TODO maybe use a shared rendertexure for this? public static RenderTexture PlayerDrawTarget = new(512, 512, flags: RenderTargetFlags.None); private Rect campaignBoxRect; + private bool isLoadingCampaign = false; private static readonly MenuCharacterRenderer menuCharacterRenderer = new(); + public override void OnActivate() + { + isLoadingCampaign = false; + } + public override void Render() { if (!MadnessUtils.FindPlayer(Scene, out _, out var character)) @@ -247,10 +253,9 @@ public override void Update() "cmpgn-menu-play" : (stats.LevelIndex >= campaign.Levels.Length ? "cmpgn-menu-restart" : "cmpgn-menu-continue"); Ui.Layout.Size(130, 40).StickRight().StickBottom().Order(1); - if (Ui.Button(Localisation.Get(btnKey))) + if (Ui.Button(Localisation.Get(btnKey)) && !isLoadingCampaign) { - // TODO ensure we cant press this button while loading a campaign - + isLoadingCampaign = true; // prevent double click var i = stats.LevelIndex; if (i >= campaign.Levels.Length) // we completed the campaign, restart diff --git a/src/MadnessInteractiveReloaded/User interface/Systems/LevelSelectMenuSystem.cs b/src/MadnessInteractiveReloaded/User interface/Systems/LevelSelectMenuSystem.cs index 728bc89e..6f6ab6ac 100644 --- a/src/MadnessInteractiveReloaded/User interface/Systems/LevelSelectMenuSystem.cs +++ b/src/MadnessInteractiveReloaded/User interface/Systems/LevelSelectMenuSystem.cs @@ -1,4 +1,4 @@ -using Walgelijk.Onion; +using Walgelijk.Onion; using Walgelijk.SimpleDrawing; using Walgelijk; using Walgelijk.Localisation; @@ -232,10 +232,12 @@ private enum Screen // TODO put in component lol private string? selectedLevel; private Screen currentScreen = Screen.LevelSelect; + private bool isLoadingLevel = false; public override void OnActivate() { currentScreen = Screen.LevelSelect; + isLoadingLevel = false; } public override void Update() @@ -381,10 +383,9 @@ private void LevelSelectGrid(Campaign campaign) Ui.Theme.FontSize(24).OutlineWidth(2).Padding(12).Once(); Ui.Layout.FitContainer(0.2f, null).Scale(-12, 0).Height(60).StickBottom().StickRight(); - if (Ui.Button("Proceed")) + if (Ui.Button("Proceed") && !isLoadingLevel) { - // TODO ensure we cant press this button while loading a campaign - + isLoadingLevel = true; // prevent double click Game.Scene = LevelLoadingScene.Create(Game, Registries.Levels.Get(selectedLevel).Level, SceneCacheSettings.NoCache); MadnessUtils.Flash(Colors.Black, 0.2f); } diff --git a/src/MadnessInteractiveReloaded/Weapons/Systems/BulletTracerSystem.cs b/src/MadnessInteractiveReloaded/Weapons/Systems/BulletTracerSystem.cs index 7e0ef248..dc1df054 100644 --- a/src/MadnessInteractiveReloaded/Weapons/Systems/BulletTracerSystem.cs +++ b/src/MadnessInteractiveReloaded/Weapons/Systems/BulletTracerSystem.cs @@ -1,4 +1,4 @@ -namespace MIR; +namespace MIR; using System; using System.Numerics; @@ -15,6 +15,8 @@ private class Tracer public Vector2 To; public float Width = 1; public float Lifetime; + public float StartDelay = 0; + public int SequenceID = -1; // Track which bullet sequence this belongs to public TracerRenderTask RenderTask; @@ -34,6 +36,7 @@ private class TracerRenderTask : IRenderTask public Material Material; public float TracerLength; public float TracerWidth; + public float StartDelay = 0; public TracerRenderTask(Material material) { @@ -42,13 +45,14 @@ public TracerRenderTask(Material material) public void Execute(IGraphics graphics) { - // TODO when a bullet passes through an object, the tracers dont line up because the actual bullet impact is instant - // TODO some of this code can be calculated once the moment this tracer is created or reinitialised - const float RefDist = 5000; + // Skip if not ready to show + float adjustedTime = Time - StartDelay; + if (adjustedTime < 0) return; + var dist = Vector2.Distance(To, From); - float p = Utilities.Clamp(Time / Duration / (dist / RefDist)); + float p = Utilities.Clamp(adjustedTime / Duration / (dist / RefDist)); var a = Vector2.Lerp(From, To, p); var b = Vector2.Lerp(From, To, Utilities.Clamp(p + TracerLength)); @@ -56,7 +60,9 @@ public void Execute(IGraphics graphics) var length = delta.Length(); var angle = MathF.Atan2(delta.Y, delta.X); - var transform = Matrix3x2.CreateScale(length, float.Max(3, TracerWidth)) * Matrix3x2.CreateRotation(angle) * Matrix3x2.CreateTranslation(a.X, a.Y); + var transform = Matrix3x2.CreateScale(length, float.Max(3, TracerWidth)) * + Matrix3x2.CreateRotation(angle) * + Matrix3x2.CreateTranslation(a.X, a.Y); graphics.CurrentTarget.ModelMatrix = new Matrix4x4(transform); graphics.Draw(PrimitiveMeshes.Quad, Material); @@ -64,6 +70,9 @@ public void Execute(IGraphics graphics) } private readonly Tracer?[] tracers = new Tracer?[MaxTracerCount]; + private int currentSequenceID = 0; + private float currentSequenceDelay = 0; + private const float BaseSpeed = 15000f; // Units per second public const int MaxTracerCount = 64; public const float Duration = 0.07f; @@ -73,6 +82,14 @@ public void Execute(IGraphics graphics) public override void Initialise() { TracerMaterial.SetUniform(ShaderDefaults.MainTextureUniform, Texture.White); + ResetSequence(); + } + + private void ResetSequence() + { + currentSequenceID++; + currentSequenceDelay = 0; + if (currentSequenceID > 10000) currentSequenceID = 0; // Prevent potential overflow } /// @@ -82,22 +99,39 @@ public void ShowTracer(Vector2 from, Vector2 to, float width = 1) { var i = GetAvailable(); if (i == null) + { + ResetSequence(); // If we can't get a tracer, start fresh return; + } + float dist = Vector2.Distance(to, from); + + // Calculate delay based on distance from start + float travelTime = dist / BaseSpeed; + i.Lifetime = Duration; i.From = from; i.To = to; i.Width = width; + i.StartDelay = currentSequenceDelay; + i.SequenceID = currentSequenceID; + i.RenderTask.StartDelay = currentSequenceDelay; + + // Update sequence delay for next segment + currentSequenceDelay += travelTime; } public override void Render() { + bool hasActiveTracers = false; + for (int i = 0; i < MaxTracerCount; i++) { var tracer = tracers[i]; if (tracer?.ShouldBeRendered ?? false) { + hasActiveTracers = true; tracer.Lifetime -= Time.DeltaTime; tracer.RenderTask.To = tracer.To; tracer.RenderTask.From = tracer.From; @@ -107,6 +141,12 @@ public override void Render() RenderQueue.Add(tracer.RenderTask); } } + + // Reset sequence when all tracers are done + if (!hasActiveTracers) + { + ResetSequence(); + } } private Tracer? GetAvailable() diff --git a/src/MadnessInteractiveReloaded/Weapons/Systems/WeaponSystem.cs b/src/MadnessInteractiveReloaded/Weapons/Systems/WeaponSystem.cs index e4007cfd..7ee82c56 100644 --- a/src/MadnessInteractiveReloaded/Weapons/Systems/WeaponSystem.cs +++ b/src/MadnessInteractiveReloaded/Weapons/Systems/WeaponSystem.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using Walgelijk; using Walgelijk.AssetManager; @@ -365,12 +365,12 @@ private void CastBulletRay(Vector2 origin, Vector2 bulletDirection, WeaponCompon if (willDeflect) { if (deflectingWeapon) - victimChar.DrainDodge(ConVars.Instance.DeflectDodgeCost * weapon.Data.Damage); //minder dodge damage met zwaardiaan + victimChar.DrainDodge(ConVars.Instance.DeflectDodgeCost * weapon.Data.Damage); if ((victimChar.HasDodge() || victimChar.Stats.DodgeOversaturate)) { - var perfectDeflect = deflectingWeapon && wielder.AttacksCannotBeAutoDodged && victimChar.Positioning.MeleeBlockProgress < 1; // TODO convar - if (!wielder.AttacksCannotBeAutoDodged || perfectDeflect) // you are allowed to deflect an accurate shot event if you time it right + var perfectDeflect = deflectingWeapon && wielder.AttacksCannotBeAutoDodged && victimChar.Positioning.MeleeBlockProgress < 1; + if (!wielder.AttacksCannotBeAutoDodged || perfectDeflect) { var hitPosOnLine = hit.Position; @@ -380,6 +380,7 @@ private void CastBulletRay(Vector2 origin, Vector2 bulletDirection, WeaponCompon victimChar.Positioning.MeleeBlockImpactIntensity += Utilities.RandomFloat(-1, 1); } + Vector2 returnDir; if (perfectDeflect) { wielder.DodgeMeter = -1; @@ -389,20 +390,49 @@ private void CastBulletRay(Vector2 origin, Vector2 bulletDirection, WeaponCompon victimChar.Positioning.MeleeBlockImpactIntensity += 1; victimChar.Positioning.CurrentRecoil += 2; - // TODO should be in a system and the sound is not very nice - Audio.PlayOnce( - SoundCache.Instance.LoadSoundEffect( - Assets.Load("sounds/deflection/perfect_deflect_1.wav")), 2); - const float d = 0.5f; - MadnessUtils.RoutineForSecondsPausable(d, static (dt) => + returnDir = Vector2.Normalize(wielder.Positioning.Head.GlobalPosition - hitPosOnLine); + + var deflectSound = SoundCache.Instance.LoadSoundEffect( + Assets.Load("sounds/deflection/perfect_deflect_1.wav")); + Audio.PlayOnce(deflectSound, 2f); + + var impactSound = Utilities.PickRandom(Sounds.BulletDeflection); + Audio.PlayOnce(impactSound, 0.8f); + + // Add temporary invincibility frames during perfect deflect + victimChar.Flags |= CharacterFlags.Invincible; + MadnessUtils.DelayPausable(0.1f, () => { - Game.Main.State.Time.TimeScale = Utilities.SmoothApproach(Game.Main.State.Time.TimeScale, 1, 1, dt); + if (victimChar.IsAlive) + victimChar.Flags &= ~CharacterFlags.Invincible; }); - MadnessUtils.DelayPausable(0.05f, static () => { Game.Main.State.Time.TimeScale = 0.2f; }); - MadnessUtils.DelayPausable(d, static () => { Game.Main.State.Time.TimeScale = 1; }); + + // Visual feedback + MadnessUtils.Flash(Colors.White.WithAlpha(0.3f), 0.1f); + + Prefabs.CreateDeflectionSpark( + Scene, + hitPosOnLine, + Utilities.VectorToAngle(returnDir), + 2f + ); } else + { + returnDir = Vector2.Normalize(bulletDirection * new Vector2(-1, Utilities.RandomFloat(-12, 12))); Audio.PlayOnce(Utilities.PickRandom(Sounds.BulletDeflection), 0.5f); + Prefabs.CreateDeflectionSpark( + Scene, + hitPosOnLine, + Utilities.VectorToAngle(returnDir), + 1f + ); + } + + // Immediately set up deflected bullet without delay + wielder = victimChar; + CastBulletRay(hitPosOnLine, returnDir, weapon, data, wielder, totalDistance, iteration + 1); + return; // Skip further bullet processing if (deflectingArmour && victimChar.IsAlive && victimChar.Flags.HasFlag(CharacterFlags.StunAnimationOnNonFatalAttack)) { @@ -411,17 +441,6 @@ private void CastBulletRay(Vector2 origin, Vector2 bulletDirection, WeaponCompon else victimChar.PlayAnimation(Registries.Animations.Get("stun_light_backwards"), 1.2f); } - - var returnDir = perfectDeflect - ? Vector2.Normalize(wielder.Positioning.Head.GlobalPosition - hitPosOnLine) - : Vector2.Normalize(bulletDirection * new Vector2(-1, Utilities.RandomFloat(-12, 12))); - wielder = victimChar; - MadnessUtils.DelayPausable(0.05f, () => - { - CastBulletRay(hitPosOnLine, returnDir, weapon, data, wielder, totalDistance, iteration + 1); - }); - Prefabs.CreateDeflectionSpark(Scene, hitPosOnLine, Utilities.VectorToAngle(returnDir), 1); - return; } } } diff --git a/src/MadnessInteractiveReloaded/base/sounds/deflection/metal_ring.wav b/src/MadnessInteractiveReloaded/base/sounds/deflection/metal_ring.wav new file mode 100644 index 00000000..aae0401e Binary files /dev/null and b/src/MadnessInteractiveReloaded/base/sounds/deflection/metal_ring.wav differ