From 332b87bfceb40a4ced35ba4173a1b91c88a65dc6 Mon Sep 17 00:00:00 2001
From: ChloePrime <chloe_von_einzbern@qq.com>
Date: Sun, 12 May 2024 22:07:21 +0800
Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E6=8F=90=E4=BA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .gitattributes                                |  72 +++++++++++
 .gitignore                                    | 118 ++++++++++++++++++
 LICENSE                                       |  21 ++++
 Logo.png                                      |   3 +
 build.gradle                                  |  62 +++++++++
 common/build.gradle                           |  25 ++++
 .../hitfeedback/HitFeedbackMod.java           |  28 +++++
 .../hitfeedback/client/HitFeedbackAction.java |  29 +++++
 .../client/HitFeedbackActions.java            |  69 ++++++++++
 .../hitfeedback/client/HitFeedbackClient.java |  32 +++++
 .../hitfeedback/client/MinecraftHolder.java   |   7 ++
 .../client/internal/SizedTexture.java         |   8 ++
 .../client/particles/BloodParticle.java       |  21 ++++
 .../client/particles/EntityPieceParticle.java | 106 ++++++++++++++++
 .../particles/GunshotFeedbackEmitter.java     |  11 ++
 .../particles/ModParticleProviders.java       |  13 ++
 .../client/particles/ParticleEmitterBase.java | 118 ++++++++++++++++++
 .../particles/SimpleTexturedParticle.java     |  37 ++++++
 .../client/particles/SparkParticle.java       |  22 ++++
 .../particles/SwordFeedbackEmitter.java       |  42 +++++++
 .../common/CommonEventHandler.java            |  78 ++++++++++++
 .../hitfeedback/common/HitFeedbackType.java   |  89 +++++++++++++
 .../hitfeedback/common/HitFeedbackTypes.java  |  53 ++++++++
 .../hitfeedback/common/ModSoundEvents.java    |  22 ++++
 .../hitfeedback/common/PlatformMethods.java   |  11 ++
 .../common/particle/ModParticleTypes.java     |  13 ++
 .../common/particle/SimpleParticleType.java   |   7 ++
 .../mixin/LivingEntityAccessor.java           |  10 ++
 .../hitfeedback/mixin/MixinSimpleTexture.java |  45 +++++++
 .../mixin/client/ParticleEngineAccessor.java  |  13 ++
 .../mixin/client/TrackingEmitterAccessor.java |  11 ++
 .../hitfeedback/network/ModNetwork.java       |  12 ++
 .../hitfeedback/network/S2CHitFeedback.java   |  63 ++++++++++
 .../chloeprime/hitfeedback/util/Basis.java    | 111 ++++++++++++++++
 .../hitfeedback/util/ComparableSupplier.java  |  31 +++++
 .../hitfeedback/util/ImageHelper.java         |  23 ++++
 .../assets/hit_feedback/particles/blood.json  |  10 ++
 .../assets/hit_feedback/particles/spark.json  |  36 ++++++
 .../resources/assets/hit_feedback/sounds.json |  40 ++++++
 .../sounds/feedback/flesh/gunshot_1.ogg       | Bin 0 -> 9939 bytes
 .../sounds/feedback/flesh/gunshot_2.ogg       | Bin 0 -> 9750 bytes
 .../sounds/feedback/flesh/gunshot_3.ogg       |   3 +
 .../sounds/feedback/flesh/punch_1.ogg         |   3 +
 .../sounds/feedback/flesh/punch_2.ogg         |   3 +
 .../sounds/feedback/flesh/punch_3.ogg         |   3 +
 .../sounds/feedback/flesh/punch_4.ogg         |   3 +
 .../sounds/feedback/flesh/sword_1.ogg         |   3 +
 .../sounds/feedback/flesh/sword_2.ogg         |   3 +
 .../sounds/feedback/flesh/sword_3.ogg         |   3 +
 .../sounds/feedback/flesh/sword_4.ogg         |   3 +
 .../sounds/feedback/flesh/sword_5.ogg         |   3 +
 .../sounds/feedback/flesh/sword_6.ogg         |   3 +
 .../sounds/feedback/flesh/sword_7.ogg         |   3 +
 .../hit_feedback/sounds/metal/failure_1.ogg   |   3 +
 .../hit_feedback/sounds/metal/failure_2.ogg   |   3 +
 .../hit_feedback/sounds/metal/failure_3.ogg   |   3 +
 .../hit_feedback/sounds/metal/failure_4.ogg   |   3 +
 .../hit_feedback/sounds/metal/failure_5.ogg   |   3 +
 .../hit_feedback/sounds/metal/failure_6.ogg   |   3 +
 .../hit_feedback/sounds/metal/failure_7.ogg   |   3 +
 .../hit_feedback/sounds/metal/failure_8.ogg   |   3 +
 .../textures/particle/blood_0.png             |   3 +
 .../textures/particle/blood_1.png             |   3 +
 .../textures/particle/blood_2.png             |   3 +
 .../textures/particle/blood_3.png             |   3 +
 .../textures/particle/spark_0.png             |   3 +
 .../textures/particle/spark_1.png             |   3 +
 .../textures/particle/spark_2.png             |   3 +
 .../textures/particle/spark_3.png             |   3 +
 .../textures/particle/spark_4.png             |   3 +
 .../textures/particle/spark_5.png             |   3 +
 .../textures/particle/spark_6.png             |   3 +
 .../textures/particle/spark_7.png             |   3 +
 .../tags/entity_types/type/bone.json          |  38 ++++++
 .../tags/entity_types/type/metal.json         |  29 +++++
 .../tags/entity_types/type/slime.json         |   9 ++
 .../hit_feedback/tags/items/sharp_weapon.json |   6 +
 .../resources/hit_feedback-common.mixins.json |  17 +++
 common/src/main/resources/icon.png            |   3 +
 fabric/build.gradle                           |  89 +++++++++++++
 .../fabric/HitFeedbackClientFabric.java       |  15 +++
 .../common/fabric/PlatformMethodsImpl.java    |  20 +++
 .../hitfeedback/fabric/HitFeedbackFabric.java |  12 ++
 .../mixin/fabric/MixinLivingEntity.java       |  28 +++++
 fabric/src/main/resources/fabric.mod.json     |  28 +++++
 .../main/resources/hit_feedback.mixins.json   |  14 +++
 forge/build.gradle                            |  85 +++++++++++++
 forge/gradle.properties                       |   1 +
 .../common/forge/ForgeCommonEventHandler.java |  15 +++
 .../common/forge/PlatformMethodsImpl.java     |  19 +++
 .../hitfeedback/forge/HitFeedbackForge.java   |  24 ++++
 forge/src/main/resources/META-INF/mods.toml   |  71 +++++++++++
 .../main/resources/hit_feedback.mixins.json   |  13 ++
 forge/src/main/resources/pack.mcmeta          |   7 ++
 gradle.properties                             |  15 +++
 gradle/wrapper/gradle-wrapper.properties      |   1 +
 settings.gradle                               |  12 ++
 97 files changed, 2189 insertions(+)
 create mode 100644 .gitattributes
 create mode 100644 .gitignore
 create mode 100644 LICENSE
 create mode 100644 Logo.png
 create mode 100644 build.gradle
 create mode 100644 common/build.gradle
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/HitFeedbackMod.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/client/HitFeedbackAction.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/client/HitFeedbackActions.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/client/HitFeedbackClient.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/client/MinecraftHolder.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/client/internal/SizedTexture.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/client/particles/BloodParticle.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/client/particles/EntityPieceParticle.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/client/particles/GunshotFeedbackEmitter.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/client/particles/ModParticleProviders.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/client/particles/ParticleEmitterBase.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/client/particles/SimpleTexturedParticle.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/client/particles/SparkParticle.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/client/particles/SwordFeedbackEmitter.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/common/CommonEventHandler.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/common/HitFeedbackType.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/common/HitFeedbackTypes.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/common/ModSoundEvents.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/common/PlatformMethods.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/common/particle/ModParticleTypes.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/common/particle/SimpleParticleType.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/mixin/LivingEntityAccessor.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/mixin/MixinSimpleTexture.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/mixin/client/ParticleEngineAccessor.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/mixin/client/TrackingEmitterAccessor.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/network/ModNetwork.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/network/S2CHitFeedback.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/util/Basis.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/util/ComparableSupplier.java
 create mode 100644 common/src/main/java/mod/chloeprime/hitfeedback/util/ImageHelper.java
 create mode 100644 common/src/main/resources/assets/hit_feedback/particles/blood.json
 create mode 100644 common/src/main/resources/assets/hit_feedback/particles/spark.json
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds.json
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/gunshot_1.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/gunshot_2.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/gunshot_3.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_1.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_2.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_3.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_4.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_1.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_2.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_3.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_4.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_5.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_6.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_7.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/metal/failure_1.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/metal/failure_2.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/metal/failure_3.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/metal/failure_4.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/metal/failure_5.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/metal/failure_6.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/metal/failure_7.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/sounds/metal/failure_8.ogg
 create mode 100644 common/src/main/resources/assets/hit_feedback/textures/particle/blood_0.png
 create mode 100644 common/src/main/resources/assets/hit_feedback/textures/particle/blood_1.png
 create mode 100644 common/src/main/resources/assets/hit_feedback/textures/particle/blood_2.png
 create mode 100644 common/src/main/resources/assets/hit_feedback/textures/particle/blood_3.png
 create mode 100644 common/src/main/resources/assets/hit_feedback/textures/particle/spark_0.png
 create mode 100644 common/src/main/resources/assets/hit_feedback/textures/particle/spark_1.png
 create mode 100644 common/src/main/resources/assets/hit_feedback/textures/particle/spark_2.png
 create mode 100644 common/src/main/resources/assets/hit_feedback/textures/particle/spark_3.png
 create mode 100644 common/src/main/resources/assets/hit_feedback/textures/particle/spark_4.png
 create mode 100644 common/src/main/resources/assets/hit_feedback/textures/particle/spark_5.png
 create mode 100644 common/src/main/resources/assets/hit_feedback/textures/particle/spark_6.png
 create mode 100644 common/src/main/resources/assets/hit_feedback/textures/particle/spark_7.png
 create mode 100644 common/src/main/resources/data/hit_feedback/tags/entity_types/type/bone.json
 create mode 100644 common/src/main/resources/data/hit_feedback/tags/entity_types/type/metal.json
 create mode 100644 common/src/main/resources/data/hit_feedback/tags/entity_types/type/slime.json
 create mode 100644 common/src/main/resources/data/hit_feedback/tags/items/sharp_weapon.json
 create mode 100644 common/src/main/resources/hit_feedback-common.mixins.json
 create mode 100644 common/src/main/resources/icon.png
 create mode 100644 fabric/build.gradle
 create mode 100644 fabric/src/main/java/mod/chloeprime/hitfeedback/client/fabric/HitFeedbackClientFabric.java
 create mode 100644 fabric/src/main/java/mod/chloeprime/hitfeedback/common/fabric/PlatformMethodsImpl.java
 create mode 100644 fabric/src/main/java/mod/chloeprime/hitfeedback/fabric/HitFeedbackFabric.java
 create mode 100644 fabric/src/main/java/mod/chloeprime/hitfeedback/mixin/fabric/MixinLivingEntity.java
 create mode 100644 fabric/src/main/resources/fabric.mod.json
 create mode 100644 fabric/src/main/resources/hit_feedback.mixins.json
 create mode 100644 forge/build.gradle
 create mode 100644 forge/gradle.properties
 create mode 100644 forge/src/main/java/mod/chloeprime/hitfeedback/common/forge/ForgeCommonEventHandler.java
 create mode 100644 forge/src/main/java/mod/chloeprime/hitfeedback/common/forge/PlatformMethodsImpl.java
 create mode 100644 forge/src/main/java/mod/chloeprime/hitfeedback/forge/HitFeedbackForge.java
 create mode 100644 forge/src/main/resources/META-INF/mods.toml
 create mode 100644 forge/src/main/resources/hit_feedback.mixins.json
 create mode 100644 forge/src/main/resources/pack.mcmeta
 create mode 100644 gradle.properties
 create mode 100644 gradle/wrapper/gradle-wrapper.properties
 create mode 100644 settings.gradle

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..12a1765
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,72 @@
+# Normalize EOL for all files that Git considers text files.
+* text=auto eol=lf
+
+## git-lfs ##
+
+#Image
+*.ai filter=lfs diff=lfs merge=lfs -text
+*.bmp filter=lfs diff=lfs merge=lfs -text
+*.exr filter=lfs diff=lfs merge=lfs -text
+*.gif filter=lfs diff=lfs merge=lfs -text
+*.hdr filter=lfs diff=lfs merge=lfs -text
+*.iff filter=lfs diff=lfs merge=lfs -text
+*.jpeg filter=lfs diff=lfs merge=lfs -text
+*.jpg filter=lfs diff=lfs merge=lfs -text
+*.pict filter=lfs diff=lfs merge=lfs -text
+*.png filter=lfs diff=lfs merge=lfs -text
+*.psd filter=lfs diff=lfs merge=lfs -text
+*.tga filter=lfs diff=lfs merge=lfs -text
+*.tif filter=lfs diff=lfs merge=lfs -text
+*.tiff filter=lfs diff=lfs merge=lfs -text
+
+#Audio
+*.aif filter=lfs diff=lfs merge=lfs -text
+*.aiff filter=lfs diff=lfs merge=lfs -text
+*.it filter=lfs diff=lfs merge=lfs -text
+*.mod filter=lfs diff=lfs merge=lfs -text
+*.mp3 filter=lfs diff=lfs merge=lfs -text
+*.ogg filter=lfs diff=lfs merge=lfs -text
+*.s3m filter=lfs diff=lfs merge=lfs -text
+*.wav filter=lfs diff=lfs merge=lfs -text
+*.xm filter=lfs diff=lfs merge=lfs -text
+
+#Video
+*.mp4 filter=lfs diff=lfs merge=lfs -text
+*.mov filter=lfs diff=lfs merge=lfs -text
+
+# Fonts
+*.otf filter=lfs diff=lfs merge=lfs -text
+*.ttf filter=lfs diff=lfs merge=lfs -text
+
+#3D Object
+*.3dm filter=lfs diff=lfs merge=lfs -text
+*.3ds filter=lfs diff=lfs merge=lfs -text
+*.blend filter=lfs diff=lfs merge=lfs -text
+*.c4d filter=lfs diff=lfs merge=lfs -text
+*.collada filter=lfs diff=lfs merge=lfs -text
+*.dae filter=lfs diff=lfs merge=lfs -text
+*.dxf filter=lfs diff=lfs merge=lfs -text
+*.fbx filter=lfs diff=lfs merge=lfs -text
+*.jas filter=lfs diff=lfs merge=lfs -text
+*.lws filter=lfs diff=lfs merge=lfs -text
+*.lxo filter=lfs diff=lfs merge=lfs -text
+*.ma filter=lfs diff=lfs merge=lfs -text
+*.max filter=lfs diff=lfs merge=lfs -text
+*.mb filter=lfs diff=lfs merge=lfs -text
+*.obj filter=lfs diff=lfs merge=lfs -text
+*.ply filter=lfs diff=lfs merge=lfs -text
+*.skp filter=lfs diff=lfs merge=lfs -text
+*.stl filter=lfs diff=lfs merge=lfs -text
+*.ztl filter=lfs diff=lfs merge=lfs -text
+*.glb filter=lfs diff=lfs merge=lfs -text
+
+#ETC
+*.a filter=lfs diff=lfs merge=lfs -text
+*.so filter=lfs diff=lfs merge=lfs -text
+*.pdf filter=lfs diff=lfs merge=lfs -text
+*.zip filter=lfs diff=lfs merge=lfs -text
+*.dll filter=lfs diff=lfs merge=lfs -text
+*.jar filter=lfs diff=lfs merge=lfs -text
+*.rns filter=lfs diff=lfs merge=lfs -text
+*.jar filter=lfs diff=lfs merge=lfs -text
+*.reason filter=lfs diff=lfs merge=lfs -text
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c37caf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,118 @@
+# User-specific stuff
+.idea/
+
+*.iml
+*.ipr
+*.iws
+
+# IntelliJ
+out/
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+.gradle
+build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Cache of project
+.gradletasknamecache
+
+**/build/
+
+# Common working directory
+run/
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c986429
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2024 
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/Logo.png b/Logo.png
new file mode 100644
index 0000000..f1c8203
--- /dev/null
+++ b/Logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e4f5fa55144e6dfc600f77a0c39fe7cb01fa84888ad9cec5c97a36dc3c14d9d4
+size 806
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..a76c511
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,62 @@
+plugins {
+    id "architectury-plugin" version "3.4-SNAPSHOT"
+    id "dev.architectury.loom" version "1.5-SNAPSHOT" apply false
+}
+
+architectury {
+    minecraft = rootProject.minecraft_version
+}
+
+subprojects {
+    apply plugin: "dev.architectury.loom"
+
+    loom {
+        silentMojangMappingsLicense()
+    }
+
+    repositories {
+        maven { url "https://maven.parchmentmc.org" }
+        maven {
+            name = "Fuzs Mod Resources (Forge Config API Port)"
+            url = "https://raw.githubusercontent.com/Fuzss/modresources/main/maven/"
+        }
+    }
+
+    dependencies {
+        minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
+        // The following line declares the mojmap mappings, you may use other mappings as well
+        mappings loom.layered() {
+            officialMojangMappings()
+            parchment("org.parchmentmc.data:parchment-${minecraft_version}:${parchment_version}@zip")
+        }
+        // The following line declares the yarn mappings you may select this one as well.
+        // mappings "net.fabricmc:yarn:@YARN_MAPPINGS@:v2"
+    }
+}
+
+allprojects {
+    apply plugin: "java"
+    apply plugin: "architectury-plugin"
+    apply plugin: "maven-publish"
+
+    archivesBaseName = rootProject.archives_base_name
+    version = rootProject.mod_version
+    group = rootProject.maven_group
+
+    repositories {
+        // Add repositories to retrieve artifacts from in here.
+        // You should only use this when depending on other mods because
+        // Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
+        // See https://docs.gradle.org/current/userguide/declaring_repositories.html
+        // for more information about repositories.
+    }
+
+    tasks.withType(JavaCompile) {
+        options.encoding = "UTF-8"
+        options.release = 17
+    }
+
+    java {
+        withSourcesJar()
+    }
+}
diff --git a/common/build.gradle b/common/build.gradle
new file mode 100644
index 0000000..05f5ef2
--- /dev/null
+++ b/common/build.gradle
@@ -0,0 +1,25 @@
+dependencies {
+    // We depend on fabric loader here to use the fabric @Environment annotations and get the mixin dependencies
+    // Do NOT use other classes from fabric loader
+    modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}"
+    // Remove the next line if you don't want to depend on the API
+    modApi "dev.architectury:architectury:${rootProject.architectury_version}"
+}
+
+architectury {
+    common("fabric", "forge")
+}
+
+publishing {
+    publications {
+        mavenCommon(MavenPublication) {
+            artifactId = rootProject.archives_base_name
+            from components.java
+        }
+    }
+
+    // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
+    repositories {
+        // Add repositories to publish to here.
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/HitFeedbackMod.java b/common/src/main/java/mod/chloeprime/hitfeedback/HitFeedbackMod.java
new file mode 100644
index 0000000..8251428
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/HitFeedbackMod.java
@@ -0,0 +1,28 @@
+package mod.chloeprime.hitfeedback;
+
+import dev.architectury.platform.Platform;
+import dev.architectury.utils.Env;
+import mod.chloeprime.hitfeedback.client.HitFeedbackClient;
+import mod.chloeprime.hitfeedback.common.HitFeedbackTypes;
+import mod.chloeprime.hitfeedback.common.ModSoundEvents;
+import mod.chloeprime.hitfeedback.common.particle.ModParticleTypes;
+import mod.chloeprime.hitfeedback.network.ModNetwork;
+import net.minecraft.resources.ResourceLocation;
+
+public class HitFeedbackMod
+{
+	private HitFeedbackMod() {}
+
+	public static final String MOD_ID = "hit_feedback";
+
+	public static ResourceLocation loc(String path) {
+		return new ResourceLocation(MOD_ID, path);
+	}
+
+	public static void init() {
+		ModSoundEvents.DFR.register();
+		ModParticleTypes.DFR.register();
+		HitFeedbackTypes.DFR.register();
+		ModNetwork.init();
+	}
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/client/HitFeedbackAction.java b/common/src/main/java/mod/chloeprime/hitfeedback/client/HitFeedbackAction.java
new file mode 100644
index 0000000..3e9ff70
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/client/HitFeedbackAction.java
@@ -0,0 +1,29 @@
+package mod.chloeprime.hitfeedback.client;
+
+import dev.architectury.networking.NetworkManager;
+import mod.chloeprime.hitfeedback.client.particles.ParticleEmitterBase;
+import mod.chloeprime.hitfeedback.common.particle.ModParticleTypes;
+import mod.chloeprime.hitfeedback.mixin.client.ParticleEngineAccessor;
+import mod.chloeprime.hitfeedback.network.S2CHitFeedback;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.core.particles.ParticleOptions;
+
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+
+@FunctionalInterface
+public interface HitFeedbackAction extends BiConsumer<S2CHitFeedback, NetworkManager.PacketContext> {
+    static HitFeedbackAction addEmitter(ParticleEmitterBase.Builder builder, ParticleEmitterBase.Constructor constructor) {
+        return addEmitter(ModParticleTypes.BLOOD, builder, constructor);
+    }
+
+    static HitFeedbackAction addEmitter(Supplier<? extends ParticleOptions> particle, ParticleEmitterBase.Builder builder, ParticleEmitterBase.Constructor constructor) {
+        return (packet, context) -> {
+            var pos = packet.position;
+            var vel = packet.velocity;
+            var entity = packet.getEntity(context.getPlayer().getLevel());
+            var emitter = constructor.create(particle.get(), builder, entity, ((ClientLevel) entity.getLevel()), pos.x, pos.y, pos.z, vel.x, vel.y, vel.z);
+            ((ParticleEngineAccessor) MinecraftHolder.MC.particleEngine).getTrackingEmitters().add(emitter);
+        };
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/client/HitFeedbackActions.java b/common/src/main/java/mod/chloeprime/hitfeedback/client/HitFeedbackActions.java
new file mode 100644
index 0000000..0f7859c
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/client/HitFeedbackActions.java
@@ -0,0 +1,69 @@
+package mod.chloeprime.hitfeedback.client;
+
+import mod.chloeprime.hitfeedback.client.particles.GunshotFeedbackEmitter;
+import mod.chloeprime.hitfeedback.client.particles.ParticleEmitterBase;
+import mod.chloeprime.hitfeedback.client.particles.SwordFeedbackEmitter;
+import mod.chloeprime.hitfeedback.common.HitFeedbackType;
+import mod.chloeprime.hitfeedback.common.HitFeedbackTypes;
+import mod.chloeprime.hitfeedback.common.particle.ModParticleTypes;
+import mod.chloeprime.hitfeedback.util.ComparableSupplier;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+public class HitFeedbackActions {
+    private HitFeedbackActions() {}
+
+    public static void register(Supplier<HitFeedbackType> type, HitFeedbackAction action) {
+        registry.put(new ComparableSupplier<>(type), action);
+    }
+
+    public static Optional<HitFeedbackAction> get(HitFeedbackType type) {
+        var key = new ComparableSupplier<>(() -> type);
+        return Optional.ofNullable(registry.get(key));
+    }
+
+    private static final Map<ComparableSupplier<HitFeedbackType>, HitFeedbackAction> registry = new LinkedHashMap<>();
+
+    public static void init() {
+        HitFeedbackActions.register(HitFeedbackTypes.FLESH_SWORD, HitFeedbackAction.addEmitter(
+                ModParticleTypes.BLOOD,
+                swordEmitterBuilder().goreSpawnRate(0.25F),
+                SwordFeedbackEmitter::new)
+        );
+        HitFeedbackActions.register(HitFeedbackTypes.FLESH_GUNSHOT, HitFeedbackAction.addEmitter(
+                ModParticleTypes.BLOOD,
+                gunshotEmitterBuilder().goreSpawnRate(0.25F),
+                GunshotFeedbackEmitter::new)
+        );
+
+        HitFeedbackActions.register(HitFeedbackTypes.BONE, HitFeedbackAction.addEmitter(
+                new ParticleEmitterBase.Builder().life(1).emitCountPerTick(5).goreOnly(),
+                GunshotFeedbackEmitter::new)
+        );
+        HitFeedbackActions.register(HitFeedbackTypes.METAL, HitFeedbackAction.addEmitter(
+                ModParticleTypes.SPARK,
+                new ParticleEmitterBase.Builder().life(1).emitCountPerTick(10).goreSpawnRate(0),
+                GunshotFeedbackEmitter::new)
+        );
+
+        HitFeedbackActions.register(HitFeedbackTypes.SLIME_SWORD, HitFeedbackAction.addEmitter(
+                swordEmitterBuilder().goreOnly(),
+                SwordFeedbackEmitter::new)
+        );
+        HitFeedbackActions.register(HitFeedbackTypes.SLIME_GUNSHOT, HitFeedbackAction.addEmitter(
+                gunshotEmitterBuilder().goreOnly(),
+                GunshotFeedbackEmitter::new)
+        );
+    }
+
+    public static ParticleEmitterBase.Builder swordEmitterBuilder() {
+        return new ParticleEmitterBase.Builder().life(7).emitCountPerTick(5);
+    }
+
+    public static ParticleEmitterBase.Builder gunshotEmitterBuilder() {
+        return new ParticleEmitterBase.Builder().life(2).emitCountPerTick(8);
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/client/HitFeedbackClient.java b/common/src/main/java/mod/chloeprime/hitfeedback/client/HitFeedbackClient.java
new file mode 100644
index 0000000..9731e57
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/client/HitFeedbackClient.java
@@ -0,0 +1,32 @@
+package mod.chloeprime.hitfeedback.client;
+
+import dev.architectury.networking.NetworkManager;
+import mod.chloeprime.hitfeedback.client.particles.GunshotFeedbackEmitter;
+import mod.chloeprime.hitfeedback.client.particles.ModParticleProviders;
+import mod.chloeprime.hitfeedback.client.particles.ParticleEmitterBase;
+import mod.chloeprime.hitfeedback.client.particles.SwordFeedbackEmitter;
+import mod.chloeprime.hitfeedback.common.HitFeedbackTypes;
+import mod.chloeprime.hitfeedback.common.particle.ModParticleTypes;
+import mod.chloeprime.hitfeedback.network.S2CHitFeedback;
+import net.minecraft.client.particle.BreakingItemParticle;
+import net.minecraft.core.particles.ItemParticleOption;
+import net.minecraft.core.particles.ParticleOptions;
+import net.minecraft.core.particles.ParticleTypes;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+
+public class HitFeedbackClient {
+    private HitFeedbackClient() {}
+
+    public static void init() {
+        ModParticleProviders.init();
+    }
+
+    public static void setup() {
+        HitFeedbackActions.init();
+    }
+
+    public static void handleFeedbackPacket(S2CHitFeedback packet, NetworkManager.PacketContext context) {
+        HitFeedbackActions.get(packet.type).ifPresent(action -> action.accept(packet, context));
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/client/MinecraftHolder.java b/common/src/main/java/mod/chloeprime/hitfeedback/client/MinecraftHolder.java
new file mode 100644
index 0000000..ca60941
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/client/MinecraftHolder.java
@@ -0,0 +1,7 @@
+package mod.chloeprime.hitfeedback.client;
+
+import net.minecraft.client.Minecraft;
+
+public class MinecraftHolder {
+    public static final Minecraft MC = Minecraft.getInstance();
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/client/internal/SizedTexture.java b/common/src/main/java/mod/chloeprime/hitfeedback/client/internal/SizedTexture.java
new file mode 100644
index 0000000..5f80943
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/client/internal/SizedTexture.java
@@ -0,0 +1,8 @@
+package mod.chloeprime.hitfeedback.client.internal;
+
+public interface SizedTexture {
+    int hit_feedback$getWidth();
+    int hit_feedback$getHeight();
+    float hit_feedback$getFillRate();
+}
+
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/BloodParticle.java b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/BloodParticle.java
new file mode 100644
index 0000000..8575159
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/BloodParticle.java
@@ -0,0 +1,21 @@
+package mod.chloeprime.hitfeedback.client.particles;
+
+import dev.architectury.registry.client.particle.ParticleProviderRegistry;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.client.particle.ParticleProvider;
+import net.minecraft.client.particle.SpriteSet;
+import net.minecraft.core.particles.SimpleParticleType;
+
+public class BloodParticle extends SimpleTexturedParticle {
+    public BloodParticle(SpriteSet sprite, ClientLevel clientLevel, double d, double e, double f, double g, double h, double i) {
+        super(sprite, clientLevel, d, e, f, g, h, i);
+        this.lifetime += random.nextInt(40, 100);
+    }
+
+    public static final class Provider implements ParticleProviderRegistry.DeferredParticleProvider<SimpleParticleType> {
+        @Override
+        public ParticleProvider<SimpleParticleType> create(ParticleProviderRegistry.ExtendedSpriteSet spriteSet) {
+            return (options, clientLevel, d, e, f, g, h, i) -> new BloodParticle(spriteSet, clientLevel, d, e, f, g, h, i);
+        }
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/EntityPieceParticle.java b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/EntityPieceParticle.java
new file mode 100644
index 0000000..853fc24
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/EntityPieceParticle.java
@@ -0,0 +1,106 @@
+package mod.chloeprime.hitfeedback.client.particles;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.blaze3d.vertex.*;
+import mod.chloeprime.hitfeedback.client.internal.SizedTexture;
+import net.minecraft.client.Camera;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.client.particle.ParticleRenderType;
+import net.minecraft.client.particle.SingleQuadParticle;
+import net.minecraft.client.renderer.entity.EntityRenderer;
+import net.minecraft.client.renderer.texture.MissingTextureAtlasSprite;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.entity.Entity;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Optional;
+
+import static mod.chloeprime.hitfeedback.client.MinecraftHolder.MC;
+
+public class EntityPieceParticle extends SingleQuadParticle {
+    public static final int SIZE = 4;
+
+    protected EntityPieceParticle(Entity entity, ClientLevel level, double x, double y, double z, double xSpeed, double ySpeed, double zSpeed) {
+        super(level, x, y, z, xSpeed, ySpeed, zSpeed);
+        this.quadSize /= 2;
+        this.gravity = 1;
+        this.lifetime += random.nextInt(40, 100);
+
+        var tex = getEntityTexture(entity);
+        this.valid = tex.isPresent();
+        this.texture = valid ? tex.get().texture : MissingTextureAtlasSprite.getLocation();
+        var w = valid ? tex.get().width : 1;
+        var h = valid ? tex.get().height : 1;
+        this.u0 = valid ? random.nextInt(w - SIZE + 1) / (float) w : 0;
+        this.v0 = valid ? random.nextInt(h - SIZE + 1) / (float) h : 0;
+        this.u1 = u0 + SIZE / (float) w;
+        this.v1 = v0 + SIZE / (float) h;
+    }
+
+    public record EntityTextureInfo(
+            ResourceLocation texture,
+            int width,
+            int height,
+            float fillRate
+    ) {
+    }
+
+    private final ResourceLocation texture;
+    private final boolean valid;
+    private final float u0, v0;
+    private final float u1, v1;
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    public static Optional<EntityTextureInfo> getEntityTexture(Entity entity) {
+        var texture = Optional
+                .ofNullable(entity)
+                .map(MC.getEntityRenderDispatcher()::getRenderer)
+                .map((EntityRenderer d) -> d.getTextureLocation(entity));
+
+        if (texture.isEmpty()) {
+            return Optional.empty();
+        }
+
+        return texture
+                .map(tex -> MC.getTextureManager().getTexture(tex))
+                .map(tex -> tex instanceof SizedTexture simple ? simple : null)
+                .map(tex -> new EntityTextureInfo(texture.get(), tex.hit_feedback$getWidth(), tex.hit_feedback$getHeight(), tex.hit_feedback$getFillRate()));
+    }
+
+    @Override
+    public @NotNull ParticleRenderType getRenderType() {
+        return ParticleRenderType.CUSTOM;
+    }
+
+    @Override
+    public void render(VertexConsumer buffer, Camera renderInfo, float partialTicks) {
+        if (!valid || !(buffer instanceof BufferBuilder builder)) {
+            return;
+        }
+        RenderSystem.setShaderTexture(0, texture);
+        builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.PARTICLE);
+        super.render(buffer, renderInfo, partialTicks);
+        builder.end();
+        BufferUploader.end(builder);
+    }
+
+    @Override
+    protected float getU0() {
+        return u0;
+    }
+
+    @Override
+    protected float getU1() {
+        return u1;
+    }
+
+    @Override
+    protected float getV0() {
+        return v0;
+    }
+
+    @Override
+    protected float getV1() {
+        return v1;
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/GunshotFeedbackEmitter.java b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/GunshotFeedbackEmitter.java
new file mode 100644
index 0000000..20c58eb
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/GunshotFeedbackEmitter.java
@@ -0,0 +1,11 @@
+package mod.chloeprime.hitfeedback.client.particles;
+
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.core.particles.ParticleOptions;
+import net.minecraft.world.entity.Entity;
+
+public class GunshotFeedbackEmitter extends ParticleEmitterBase {
+    public GunshotFeedbackEmitter(ParticleOptions particle, Builder builder, Entity boundEntity, ClientLevel clientLevel, double d, double e, double f, double g, double h, double i) {
+        super(particle, builder, boundEntity, clientLevel, d, e, f, g, h, i);
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/ModParticleProviders.java b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/ModParticleProviders.java
new file mode 100644
index 0000000..ee22e3b
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/ModParticleProviders.java
@@ -0,0 +1,13 @@
+package mod.chloeprime.hitfeedback.client.particles;
+
+import dev.architectury.registry.client.particle.ParticleProviderRegistry;
+import mod.chloeprime.hitfeedback.common.particle.ModParticleTypes;
+
+public class ModParticleProviders {
+    private ModParticleProviders() {}
+
+    public static void init() {
+        ParticleProviderRegistry.register(ModParticleTypes.BLOOD, new BloodParticle.Provider());
+        ParticleProviderRegistry.register(ModParticleTypes.SPARK, new SparkParticle.Provider());
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/ParticleEmitterBase.java b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/ParticleEmitterBase.java
new file mode 100644
index 0000000..f498718
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/ParticleEmitterBase.java
@@ -0,0 +1,118 @@
+package mod.chloeprime.hitfeedback.client.particles;
+
+import com.google.common.base.Suppliers;
+import mod.chloeprime.hitfeedback.mixin.client.TrackingEmitterAccessor;
+import mod.chloeprime.hitfeedback.util.Basis;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.client.particle.ParticleEngine;
+import net.minecraft.client.particle.TrackingEmitter;
+import net.minecraft.core.particles.ParticleOptions;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.phys.Vec3;
+
+import java.util.function.Supplier;
+
+public class ParticleEmitterBase extends TrackingEmitter {
+    protected static final Supplier<ParticleEngine> ENGINE = Suppliers.memoize(() -> Minecraft.getInstance().particleEngine);
+    protected final ParticleOptions particle;
+    protected final int emitCountPerTick;
+    private final float goreSpawnRate;
+    private final boolean goreOnly;
+    protected Vec3 relPos;
+    protected Vec3 relMotion;
+    @SuppressWarnings("FieldMayBeFinal")
+    private boolean prepared;
+
+    public ParticleEmitterBase(ParticleOptions particle, Builder builder, Entity entity, ClientLevel clientLevel, double x, double y, double z, double g, double h, double i) {
+        super(clientLevel, entity, particle, builder.life);
+        this.particle = particle;
+        this.lifetime = builder.life;
+        this.emitCountPerTick = builder.emitCountPerTick;
+        this.goreOnly = builder.goreOnly;
+        this.goreSpawnRate = (goreOnly ? 1 : builder.goreSpawnRate) / EntityPieceParticle
+                .getEntityTexture(entity)
+                .map(EntityPieceParticle.EntityTextureInfo::fillRate)
+                .orElse(1F);
+        var basis = Basis.fromEntityBody(entity);
+        this.relPos = basis.toLocal(new Vec3(x, y, z).subtract(entity.position()));
+        this.relMotion = basis.toLocal(new Vec3(g, h, i));
+        prepared = true;
+    }
+
+    public static class Builder {
+        private int life, emitCountPerTick;
+        private float goreSpawnRate;
+        private boolean goreOnly;
+
+        public Builder life(int life) {
+            this.life = life;
+            return this;
+        }
+
+        public Builder emitCountPerTick(int emitCountPerTick) {
+            this.emitCountPerTick = emitCountPerTick;
+            return this;
+        }
+
+        public Builder goreSpawnRate(float rate) {
+            this.goreSpawnRate = rate;
+            return this;
+        }
+
+        public Builder goreOnly() {
+            this.goreOnly = true;
+            return this;
+        }
+    }
+
+    @Override
+    public void tick() {
+        if (!prepared) {
+            return;
+        }
+        if (this.age++ >= this.lifetime) {
+            this.remove();
+            return;
+        }
+        var pos = getEmitPos();
+        var vel = getEmitVelocity();
+        for (int i = 0; i < emitCountPerTick; i++) {
+            if (goreOnly) {
+                float cnt = goreSpawnRate;
+                while (cnt >= 1) {
+                    ENGINE.get().add(new EntityPieceParticle(((TrackingEmitterAccessor) this).getEntity(), level, pos.x, pos.y, pos.z, vel.x, vel.y, vel.z));
+                    cnt -= 1;
+                }
+                if (cnt > 0 && Math.random() < cnt) {
+                    ENGINE.get().add(new EntityPieceParticle(((TrackingEmitterAccessor) this).getEntity(), level, pos.x, pos.y, pos.z, vel.x, vel.y, vel.z));
+                }
+                continue;
+            }
+            level.addParticle(particle, pos.x, pos.y, pos.z, vel.x, vel.y, vel.z);
+            if (goreSpawnRate <= 0) {
+                continue;
+            }
+            for (int j = 0; j < 4; j++) {
+                if (Math.random() > goreSpawnRate / 4) {
+                    continue;
+                }
+                ENGINE.get().add(new EntityPieceParticle(((TrackingEmitterAccessor) this).getEntity(), level, pos.x, pos.y, pos.z, vel.x, vel.y, vel.z));
+            }
+        }
+    }
+
+    public Vec3 getEmitPos() {
+        Entity boundEntity = ((TrackingEmitterAccessor) this).getEntity();
+        return boundEntity.position().add(Basis.fromEntityBody(boundEntity).toGlobal(relPos));
+    }
+
+    public Vec3 getEmitVelocity() {
+        Entity boundEntity = ((TrackingEmitterAccessor) this).getEntity();
+        return Basis.fromEntityBody(boundEntity).toGlobal(relMotion);
+    }
+
+    public interface Constructor {
+        TrackingEmitter create(ParticleOptions particle, Builder builder, Entity entity, ClientLevel clientLevel, double x, double y, double z, double g, double h, double i);
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/SimpleTexturedParticle.java b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/SimpleTexturedParticle.java
new file mode 100644
index 0000000..b574dfb
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/SimpleTexturedParticle.java
@@ -0,0 +1,37 @@
+package mod.chloeprime.hitfeedback.client.particles;
+
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.client.particle.ParticleRenderType;
+import net.minecraft.client.particle.SpriteSet;
+import net.minecraft.client.particle.TextureSheetParticle;
+import net.minecraft.util.Mth;
+import org.jetbrains.annotations.NotNull;
+
+public class SimpleTexturedParticle extends TextureSheetParticle {
+    public SimpleTexturedParticle(SpriteSet sprite, ClientLevel clientLevel, double d, double e, double f, double xv, double yv, double zv) {
+        super(clientLevel, d, e, f, xv, yv, zv);
+        this.sprite = sprite;
+        this.gravity = 1;
+        setSpriteFromAge(sprite);
+
+        var oldVelocity = 1 / Mth.fastInvSqrt(xd * xd + yd * yd + zd * zd);
+        var newVelocity = 1 / Mth.fastInvSqrt(xv * xv + yv * yv + zv * zv);
+        var velScale = newVelocity / oldVelocity;
+        this.xd *= velScale;
+        this.yd *= velScale;
+        this.zd *= velScale;
+    }
+
+    @Override
+    public @NotNull ParticleRenderType getRenderType() {
+        return ParticleRenderType.PARTICLE_SHEET_OPAQUE;
+    }
+
+    @Override
+    public void tick() {
+        super.tick();
+        setSpriteFromAge(sprite);
+    }
+
+    private final SpriteSet sprite;
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/SparkParticle.java b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/SparkParticle.java
new file mode 100644
index 0000000..5e498e5
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/SparkParticle.java
@@ -0,0 +1,22 @@
+package mod.chloeprime.hitfeedback.client.particles;
+
+import dev.architectury.registry.client.particle.ParticleProviderRegistry;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.client.particle.ParticleProvider;
+import net.minecraft.client.particle.SpriteSet;
+import net.minecraft.core.particles.SimpleParticleType;
+
+public class SparkParticle extends SimpleTexturedParticle {
+    public SparkParticle(SpriteSet sprite, ClientLevel clientLevel, double d, double e, double f, double g, double h, double i) {
+        super(sprite, clientLevel, d, e, f, 1.5 * g, 1.5 * h, 1.5 * i);
+        this.lifetime = 10;
+        this.quadSize /= 8;
+    }
+
+    public static final class Provider implements ParticleProviderRegistry.DeferredParticleProvider<SimpleParticleType> {
+        @Override
+        public ParticleProvider<SimpleParticleType> create(ParticleProviderRegistry.ExtendedSpriteSet spriteSet) {
+            return (options, clientLevel, d, e, f, g, h, i) -> new SparkParticle(spriteSet, clientLevel, d, e, f, g, h, i);
+        }
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/SwordFeedbackEmitter.java b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/SwordFeedbackEmitter.java
new file mode 100644
index 0000000..14613d6
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/client/particles/SwordFeedbackEmitter.java
@@ -0,0 +1,42 @@
+package mod.chloeprime.hitfeedback.client.particles;
+
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.core.particles.ParticleOptions;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.phys.Vec3;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.WeakHashMap;
+
+public class SwordFeedbackEmitter extends ParticleEmitterBase {
+    private static final Vec3 UP = new Vec3(0, 1, 0);
+    private final Vec3 emitterMotion;
+    private boolean prepared;
+    private static final Map<Entity, Boolean> hitDirections = new WeakHashMap<>();
+
+    public SwordFeedbackEmitter(ParticleOptions particle, Builder builder, Entity boundEntity, ClientLevel clientLevel, double d, double e, double f, double g, double h, double i) {
+        super(particle, builder, boundEntity, clientLevel, d, e, f, g, h, i);
+        if (relPos.lengthSqr() <= 1e-6) {
+            this.emitterMotion = Vec3.ZERO;
+            return;
+        }
+        var direction = hitDirections.compute(boundEntity, (k, v) -> !Objects.requireNonNullElse(v, random.nextBoolean()));
+        var relX = UP.cross(relMotion).normalize();
+        var angle = random.nextDouble(-Math.PI / 12, Math.PI / 12) + (direction ? Math.PI : 0);
+        var offsetDirection = relX.scale(Math.cos(angle)).add(UP.scale(Math.sin(angle)));
+        var offsetAmount = 0.5;
+        this.relPos = this.relPos.add(offsetDirection.scale(offsetAmount));
+        this.emitterMotion = offsetDirection.scale(-2 * offsetAmount / (lifetime - 1));
+        this.prepared = true;
+    }
+
+    @Override
+    public void tick() {
+        super.tick();
+        if (prepared) {
+            relPos = relPos.add(emitterMotion);
+        }
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/common/CommonEventHandler.java b/common/src/main/java/mod/chloeprime/hitfeedback/common/CommonEventHandler.java
new file mode 100644
index 0000000..e1fb624
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/common/CommonEventHandler.java
@@ -0,0 +1,78 @@
+package mod.chloeprime.hitfeedback.common;
+
+import dev.architectury.networking.NetworkManager;
+import mod.chloeprime.hitfeedback.mixin.LivingEntityAccessor;
+import mod.chloeprime.hitfeedback.network.ModNetwork;
+import mod.chloeprime.hitfeedback.network.S2CHitFeedback;
+import net.minecraft.core.Direction;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.util.Mth;
+import net.minecraft.world.damagesource.DamageSource;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.projectile.ProjectileUtil;
+import net.minecraft.world.phys.Vec3;
+import org.jetbrains.annotations.NotNull;
+
+public class CommonEventHandler {
+    public static void onEndAttack(DamageSource source, LivingEntity victim, float amount) {
+        if (victim.getLevel().isClientSide()) {
+            return;
+        }
+        var attacker = source.getEntity();
+        var bullet = source.getDirectEntity();
+        if (attacker == null && bullet == null) {
+            return;
+        }
+        var isRangedAttack = bullet != null && bullet != attacker;
+        var position = isRangedAttack ? getBulletHitPosition(bullet, victim) : getMeleeHitPosition(attacker, victim);
+        var velocity = 0.2;
+        var normal = (isRangedAttack ? bullet.getDeltaMovement() : attacker.getLookAngle().with(Direction.Axis.Y, 0))
+                .normalize()
+                .scale(-velocity);
+        var feedback = HitFeedbackType.match(source, victim, amount > 0);
+        if (!feedback.isServerOnly()) {
+            var packet = new S2CHitFeedback(victim, feedback, position, normal);
+            ((ServerLevel) victim.level).getChunkSource().broadcast(victim, ModNetwork.CHANNEL.toPacket(NetworkManager.Side.S2C, packet));
+        }
+        feedback.getHitSound().ifPresent(sound -> {
+            var pitch = 1 + (victim.getRandom().nextFloat() - victim.getRandom().nextFloat()) * 0.2f;
+            victim.playSound(sound, ((LivingEntityAccessor) victim).invokeGetSoundVolume(), pitch);
+        });
+    }
+
+    private static Vec3 getBulletHitPosition(@NotNull Entity bullet, Entity victim) {
+        var ray = bullet.getDeltaMovement();
+        if (ray.lengthSqr() <= 1e-6) {
+            return bullet.position();
+        }
+        var start = bullet.position();
+        var end = start.add(ray);
+        var len = ray.length();
+        var aabb = bullet.getBoundingBox().expandTowards(ray).inflate(1);
+        var hit = ProjectileUtil.getEntityHitResult(bullet, start, end, aabb, entity -> entity == victim, len * len);
+        if (hit != null) {
+            return hit.getLocation();
+        }
+        var dir = ray.scale(1 / len);
+        var ratLength = ray.with(Direction.Axis.Y, 0).length();
+        var groundDistance = victim.position().subtract(bullet.position()).with(Direction.Axis.Y, 0).length();
+        if (ratLength == 0 || groundDistance == 0) {
+            return victim.getEyePosition();
+        }
+        return bullet.position().add(dir.scale(len * groundDistance / ratLength));
+    }
+
+    private static Vec3 getMeleeHitPosition(@NotNull Entity swordsman, Entity victim) {
+        var reach = PlatformMethods.getAttackReach(swordsman);
+        var ray = swordsman.getLookAngle().scale(reach);
+        var start = swordsman.getEyePosition();
+        var end = start.add(ray);
+        var aabb = swordsman.getBoundingBox().expandTowards(ray).inflate(1);
+        var hit = ProjectileUtil.getEntityHitResult(swordsman, start, end, aabb, entity -> entity == victim, reach * reach);
+        if (hit != null) {
+            return hit.getLocation();
+        }
+        return victim.position().with(Direction.Axis.Y, Mth.clamp(start.y, victim.getY(), victim.getY() + victim.getBbHeight()));
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/common/HitFeedbackType.java b/common/src/main/java/mod/chloeprime/hitfeedback/common/HitFeedbackType.java
new file mode 100644
index 0000000..0267075
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/common/HitFeedbackType.java
@@ -0,0 +1,89 @@
+package mod.chloeprime.hitfeedback.common;
+
+import dev.architectury.core.RegistryEntry;
+import net.minecraft.sounds.SoundEvent;
+import net.minecraft.tags.TagKey;
+import net.minecraft.world.damagesource.DamageSource;
+import net.minecraft.world.damagesource.IndirectEntityDamageSource;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.ai.attributes.AttributeModifier;
+import net.minecraft.world.entity.ai.attributes.Attributes;
+import net.minecraft.world.entity.monster.AbstractSkeleton;
+import net.minecraft.world.entity.monster.Slime;
+import net.minecraft.world.item.ItemStack;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.function.Supplier;
+
+import static mod.chloeprime.hitfeedback.common.HitFeedbackTypes.*;
+
+public class HitFeedbackType extends RegistryEntry<HitFeedbackType> {
+    private final @Nullable TagKey<EntityType<?>> tag;
+    private final @Nullable Supplier<SoundEvent> sound;
+    private final Set<Options> options;
+
+    public interface Options {
+        Options SERVER_ONLY = new Options() {};
+    }
+
+    public HitFeedbackType(@Nullable Supplier<SoundEvent> sound, Options... options) {
+        this(sound, null, options);
+    }
+
+    public HitFeedbackType(@Nullable Supplier<SoundEvent> sound, @Nullable TagKey<EntityType<?>> tag, Options... options) {
+        this.tag = tag;
+        this.sound = sound;
+        this.options = Collections.newSetFromMap(new IdentityHashMap<>());
+        this.options.addAll(List.of(options));
+    }
+
+    public Optional<SoundEvent> getHitSound() {
+        return Optional.ofNullable(sound).map(Supplier::get);
+    }
+
+    public boolean isServerOnly() {
+        return options.contains(Options.SERVER_ONLY);
+    }
+
+    public static HitFeedbackType match(DamageSource source, LivingEntity victim, boolean valid) {
+        return HitFeedbackTypes.REGISTRY.entrySet().stream()
+                .map(Map.Entry::getValue)
+                .filter(type -> type.tag != null && victim.getType().is(type.tag))
+                .findFirst()
+                .orElseGet(() -> matchDefault(source, victim, valid));
+    }
+
+    private static HitFeedbackType matchDefault(DamageSource source, LivingEntity victim, boolean valid) {
+        var attacker = source.getEntity();
+        var isGunshot = source instanceof IndirectEntityDamageSource;
+        var isHoldingSword = !isGunshot && isHoldingSword(attacker);
+        if (!valid) {
+            return isHoldingSword ? METAL_FAILURE.get() : PUNCH.get();
+        }
+        if (victim instanceof AbstractSkeleton) {
+            return BONE.get();
+        }
+        if (victim instanceof Slime) {
+            return isHoldingSword ? SLIME_SWORD.get() : SLIME_GUNSHOT.get();
+        }
+        return isGunshot
+                ? FLESH_GUNSHOT.get()
+                : (isHoldingSword ? FLESH_SWORD.get() : PUNCH.get());
+    }
+
+    private static boolean isHoldingSword(Entity attacker) {
+        if (!(attacker instanceof LivingEntity livingAttacker)) {
+            return false;
+        }
+        return isWeapon(livingAttacker.getMainHandItem(), EquipmentSlot.MAINHAND) || isWeapon(livingAttacker.getOffhandItem(), EquipmentSlot.OFFHAND);
+    }
+
+    private static boolean isWeapon(ItemStack stack, EquipmentSlot hand) {
+        return stack.getItem().getDefaultAttributeModifiers(hand).get(Attributes.ATTACK_DAMAGE).stream()
+                .anyMatch(mdf -> mdf.getOperation() == AttributeModifier.Operation.ADDITION && mdf.getAmount() > 0);
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/common/HitFeedbackTypes.java b/common/src/main/java/mod/chloeprime/hitfeedback/common/HitFeedbackTypes.java
new file mode 100644
index 0000000..306a657
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/common/HitFeedbackTypes.java
@@ -0,0 +1,53 @@
+package mod.chloeprime.hitfeedback.common;
+
+import dev.architectury.registry.registries.DeferredRegister;
+import dev.architectury.registry.registries.Registrar;
+import dev.architectury.registry.registries.Registries;
+import dev.architectury.registry.registries.RegistrySupplier;
+import mod.chloeprime.hitfeedback.HitFeedbackMod;
+import net.minecraft.core.Registry;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.tags.TagKey;
+
+import static mod.chloeprime.hitfeedback.HitFeedbackMod.MOD_ID;
+
+public interface HitFeedbackTypes {
+    Registrar<HitFeedbackType> REGISTRY = Registries.get(MOD_ID)
+            .<HitFeedbackType>builder(HitFeedbackMod.loc("types"))
+            .build();
+    @SuppressWarnings("unchecked")
+    DeferredRegister<HitFeedbackType> DFR = DeferredRegister.create(MOD_ID, (ResourceKey<Registry<HitFeedbackType>>) REGISTRY.key());
+
+    RegistrySupplier<HitFeedbackType> PUNCH = DFR.register("flesh_punch", () -> new HitFeedbackType(
+            ModSoundEvents.FLESH_PUNCH_HIT
+    ));
+
+    RegistrySupplier<HitFeedbackType> FLESH_SWORD = DFR.register("flesh_sword", () -> new HitFeedbackType(
+            ModSoundEvents.FLESH_SWORD_HIT
+    ));
+
+    RegistrySupplier<HitFeedbackType> FLESH_GUNSHOT = DFR.register("flesh_gunshot", () -> new HitFeedbackType(
+            ModSoundEvents.FLESH_GUNSHOT
+    ));
+
+    RegistrySupplier<HitFeedbackType> BONE = DFR.register("bone", () -> new HitFeedbackType(
+            null,
+            TagKey.create(Registry.ENTITY_TYPE_REGISTRY, HitFeedbackMod.loc("type/bone"))
+    ));
+
+    RegistrySupplier<HitFeedbackType> METAL = DFR.register("metal", () -> new HitFeedbackType(
+            null,
+            TagKey.create(Registry.ENTITY_TYPE_REGISTRY, HitFeedbackMod.loc("type/metal"))
+    ));
+
+    RegistrySupplier<HitFeedbackType> SLIME_SWORD = DFR.register("slime_sword", () -> new HitFeedbackType(null));
+
+    RegistrySupplier<HitFeedbackType> SLIME_GUNSHOT = DFR.register("slime_gunshot", () -> new HitFeedbackType(
+            null,
+            TagKey.create(Registry.ENTITY_TYPE_REGISTRY, HitFeedbackMod.loc("type/slime"))
+    ));
+
+    RegistrySupplier<HitFeedbackType> METAL_FAILURE = DFR.register("metal_failure", () -> new HitFeedbackType(
+            ModSoundEvents.METAL_FAILURE
+    ));
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/common/ModSoundEvents.java b/common/src/main/java/mod/chloeprime/hitfeedback/common/ModSoundEvents.java
new file mode 100644
index 0000000..09c6a6f
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/common/ModSoundEvents.java
@@ -0,0 +1,22 @@
+package mod.chloeprime.hitfeedback.common;
+
+import dev.architectury.registry.registries.DeferredRegister;
+import dev.architectury.registry.registries.RegistrySupplier;
+import mod.chloeprime.hitfeedback.HitFeedbackMod;
+import net.minecraft.core.Registry;
+import net.minecraft.sounds.SoundEvent;
+import org.jetbrains.annotations.ApiStatus;
+
+@ApiStatus.NonExtendable
+public interface ModSoundEvents {
+    DeferredRegister<SoundEvent> DFR = DeferredRegister.create(HitFeedbackMod.MOD_ID, Registry.SOUND_EVENT_REGISTRY);
+    RegistrySupplier<SoundEvent> FLESH_PUNCH_HIT = register("feedback.flesh.punch");
+    RegistrySupplier<SoundEvent> FLESH_SWORD_HIT = register("feedback.flesh.sword");
+    RegistrySupplier<SoundEvent> FLESH_GUNSHOT = register("feedback.flesh.gunshot");
+    RegistrySupplier<SoundEvent> METAL = register("feedback.metal");
+    RegistrySupplier<SoundEvent> METAL_FAILURE = register("feedback.metal.failure");
+
+    private static RegistrySupplier<SoundEvent> register(String id) {
+        return DFR.register(id, () -> new SoundEvent(HitFeedbackMod.loc(id)));
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/common/PlatformMethods.java b/common/src/main/java/mod/chloeprime/hitfeedback/common/PlatformMethods.java
new file mode 100644
index 0000000..3106897
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/common/PlatformMethods.java
@@ -0,0 +1,11 @@
+package mod.chloeprime.hitfeedback.common;
+
+import dev.architectury.injectables.annotations.ExpectPlatform;
+import net.minecraft.world.entity.Entity;
+
+public class PlatformMethods {
+    @ExpectPlatform
+    public static double getAttackReach(@SuppressWarnings("unused") Entity entity) {
+        throw new AssertionError("Expect Platform");
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/common/particle/ModParticleTypes.java b/common/src/main/java/mod/chloeprime/hitfeedback/common/particle/ModParticleTypes.java
new file mode 100644
index 0000000..f1b8dff
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/common/particle/ModParticleTypes.java
@@ -0,0 +1,13 @@
+package mod.chloeprime.hitfeedback.common.particle;
+
+import dev.architectury.registry.registries.DeferredRegister;
+import dev.architectury.registry.registries.RegistrySupplier;
+import mod.chloeprime.hitfeedback.HitFeedbackMod;
+import net.minecraft.core.Registry;
+import net.minecraft.core.particles.ParticleType;
+
+public interface ModParticleTypes {
+    DeferredRegister<ParticleType<?>> DFR = DeferredRegister.create(HitFeedbackMod.MOD_ID, Registry.PARTICLE_TYPE_REGISTRY);
+    RegistrySupplier<SimpleParticleType> BLOOD = DFR.register("blood", SimpleParticleType::new);
+    RegistrySupplier<SimpleParticleType> SPARK = DFR.register("spark", SimpleParticleType::new);
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/common/particle/SimpleParticleType.java b/common/src/main/java/mod/chloeprime/hitfeedback/common/particle/SimpleParticleType.java
new file mode 100644
index 0000000..8b83226
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/common/particle/SimpleParticleType.java
@@ -0,0 +1,7 @@
+package mod.chloeprime.hitfeedback.common.particle;
+
+class SimpleParticleType extends net.minecraft.core.particles.SimpleParticleType {
+    protected SimpleParticleType() {
+        super(false);
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/mixin/LivingEntityAccessor.java b/common/src/main/java/mod/chloeprime/hitfeedback/mixin/LivingEntityAccessor.java
new file mode 100644
index 0000000..a840e46
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/mixin/LivingEntityAccessor.java
@@ -0,0 +1,10 @@
+package mod.chloeprime.hitfeedback.mixin;
+
+import net.minecraft.world.entity.LivingEntity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+@Mixin(LivingEntity.class)
+public interface LivingEntityAccessor {
+    @Invoker float invokeGetSoundVolume();
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/mixin/MixinSimpleTexture.java b/common/src/main/java/mod/chloeprime/hitfeedback/mixin/MixinSimpleTexture.java
new file mode 100644
index 0000000..569d2d1
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/mixin/MixinSimpleTexture.java
@@ -0,0 +1,45 @@
+package mod.chloeprime.hitfeedback.mixin;
+
+import com.mojang.blaze3d.platform.NativeImage;
+import mod.chloeprime.hitfeedback.client.internal.SizedTexture;
+import mod.chloeprime.hitfeedback.util.ImageHelper;
+import net.minecraft.client.renderer.texture.SimpleTexture;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(SimpleTexture.class)
+public class MixinSimpleTexture implements SizedTexture {
+    @Unique
+    private int hit_feedback$w = -1, hit_feedback$h = -1;
+
+    @Unique
+    private float hit_feedback$fillRate;
+
+    @Override
+    public int hit_feedback$getWidth() {
+        return hit_feedback$w;
+    }
+
+    @Override
+    public int hit_feedback$getHeight() {
+        return hit_feedback$h;
+    }
+
+    @Override
+    public float hit_feedback$getFillRate() {
+        return hit_feedback$fillRate;
+    }
+
+    @Inject(method = "doLoad", at = @At("HEAD"))
+    private void captureTextureSize(NativeImage image, boolean blur, boolean clamp, CallbackInfo ci) {
+        hit_feedback$w = image.getWidth();
+        hit_feedback$h = image.getHeight();
+        hit_feedback$fillRate = ImageHelper.getFillRate(image);
+        if (hit_feedback$fillRate == 0) {
+            hit_feedback$fillRate = 0.01F;
+        }
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/mixin/client/ParticleEngineAccessor.java b/common/src/main/java/mod/chloeprime/hitfeedback/mixin/client/ParticleEngineAccessor.java
new file mode 100644
index 0000000..bbf0d19
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/mixin/client/ParticleEngineAccessor.java
@@ -0,0 +1,13 @@
+package mod.chloeprime.hitfeedback.mixin.client;
+
+import net.minecraft.client.particle.ParticleEngine;
+import net.minecraft.client.particle.TrackingEmitter;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import java.util.Queue;
+
+@Mixin(ParticleEngine.class)
+public interface ParticleEngineAccessor {
+    @Accessor Queue<TrackingEmitter> getTrackingEmitters();
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/mixin/client/TrackingEmitterAccessor.java b/common/src/main/java/mod/chloeprime/hitfeedback/mixin/client/TrackingEmitterAccessor.java
new file mode 100644
index 0000000..c2e1a0d
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/mixin/client/TrackingEmitterAccessor.java
@@ -0,0 +1,11 @@
+package mod.chloeprime.hitfeedback.mixin.client;
+
+import net.minecraft.client.particle.TrackingEmitter;
+import net.minecraft.world.entity.Entity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(TrackingEmitter.class)
+public interface TrackingEmitterAccessor {
+    @Accessor Entity getEntity();
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/network/ModNetwork.java b/common/src/main/java/mod/chloeprime/hitfeedback/network/ModNetwork.java
new file mode 100644
index 0000000..50d623c
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/network/ModNetwork.java
@@ -0,0 +1,12 @@
+package mod.chloeprime.hitfeedback.network;
+
+import dev.architectury.networking.NetworkChannel;
+import mod.chloeprime.hitfeedback.HitFeedbackMod;
+
+public class ModNetwork {
+    public static final NetworkChannel CHANNEL = NetworkChannel.create(HitFeedbackMod.loc("main"));
+
+    public static void init() {
+        CHANNEL.register(S2CHitFeedback.class, S2CHitFeedback::encode, S2CHitFeedback::new, S2CHitFeedback::handle);
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/network/S2CHitFeedback.java b/common/src/main/java/mod/chloeprime/hitfeedback/network/S2CHitFeedback.java
new file mode 100644
index 0000000..f6eb902
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/network/S2CHitFeedback.java
@@ -0,0 +1,63 @@
+package mod.chloeprime.hitfeedback.network;
+
+import dev.architectury.networking.NetworkManager;
+import mod.chloeprime.hitfeedback.client.HitFeedbackClient;
+import mod.chloeprime.hitfeedback.common.HitFeedbackType;
+import mod.chloeprime.hitfeedback.common.HitFeedbackTypes;
+import net.minecraft.network.FriendlyByteBuf;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.phys.Vec3;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+public class S2CHitFeedback {
+    public final int entityId;
+    public final HitFeedbackType type;
+    public final Vec3 position;
+    public final Vec3 velocity;
+
+    public S2CHitFeedback(Entity entity, HitFeedbackType type, Vec3 position, Vec3 velocity) {
+        this.entityId = entity.getId();
+        this.type = type;
+        this.position = position;
+        this.velocity = velocity;
+    }
+
+    @NotNull
+    public Entity getEntity(Level level) {
+        return Optional.ofNullable(level.getEntity(entityId)).orElseThrow(() -> new IllegalStateException("Invalid entity ID"));
+    }
+
+    public S2CHitFeedback(FriendlyByteBuf buf) {
+        this.entityId = buf.readVarInt();
+        var fbTypeId = buf.readResourceLocation();
+        var posX = buf.readDouble();
+        var posY = buf.readDouble();
+        var posZ = buf.readDouble();
+        var normalX = buf.readDouble();
+        var normalY = buf.readDouble();
+        var normalZ = buf.readDouble();
+
+        this.type = Optional.ofNullable(HitFeedbackTypes.REGISTRY.get(fbTypeId)).orElseThrow(() -> new IllegalStateException("Unknown feedback type: %s".formatted(fbTypeId)));
+        this.position = new Vec3(posX, posY, posZ);
+        this.velocity = new Vec3(normalX, normalY, normalZ);
+    }
+
+    public void encode(FriendlyByteBuf buf) {
+        buf.writeVarInt(entityId);
+        buf.writeResourceLocation(Optional.ofNullable(HitFeedbackTypes.REGISTRY.getId(type)).orElseThrow(() -> new IllegalStateException("Unregistered feedback type: %s".formatted(type))));
+        buf.writeDouble(position.x);
+        buf.writeDouble(position.y);
+        buf.writeDouble(position.z);
+        buf.writeDouble(velocity.x);
+        buf.writeDouble(velocity.y);
+        buf.writeDouble(velocity.z);
+    }
+
+    public void handle(Supplier<NetworkManager.PacketContext> ctx) {
+        ctx.get().queue(() -> HitFeedbackClient.handleFeedbackPacket(this, ctx.get()));
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/util/Basis.java b/common/src/main/java/mod/chloeprime/hitfeedback/util/Basis.java
new file mode 100644
index 0000000..65710ea
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/util/Basis.java
@@ -0,0 +1,111 @@
+package mod.chloeprime.hitfeedback.util;
+
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.phys.Vec3;
+
+public record Basis(
+        Vec3 row0,
+        Vec3 row1,
+        Vec3 row2,
+        Vec3 x,
+        Vec3 y,
+        Vec3 z
+) {
+    public static Basis factory(Vec3 row0, Vec3 row1, Vec3 row2) {
+        var x = new Vec3(row0.x, row1.x, row2.x);
+        var y = new Vec3(row0.y, row1.y, row2.y);
+        var z = new Vec3(row0.z, row1.z, row2.z);
+        return new Basis(row0, row1, row2, x, y, z);
+    }
+
+    /**
+     * Basis from x, y, z
+     * Godot's basis constructor
+     */
+    public static Basis fromVectors(Vec3 x, Vec3 y, Vec3 z) {
+        var row0 = new Vec3(x.x, y.x, z.x);
+        var row1 = new Vec3(x.y, y.y, z.y);
+        var row2 = new Vec3(x.z, y.z, z.z);
+        return new Basis(row0, row1, row2, x, y, z);
+    }
+
+    public Vec3 toLocal(Vec3 global) {
+        return new Vec3(global.dot(x), global.dot(y), global.dot(z));
+    }
+
+    public Vec3 toGlobal(Vec3 local) {
+        return new Vec3(
+                x.x * local.x + y.x * local.y + z.x * local.z,
+                x.y * local.x + y.y * local.y + z.y * local.z,
+                x.z * local.x + y.z * local.y + z.z * local.z
+        );
+    }
+
+    public static Basis fromBodyRotation(float yRot) {
+        var z = getBodyFront(yRot).reverse();
+        return fromVectors(getBodyX(z), UP, z);
+    }
+
+    public static Basis fromEntityBody(Entity entity) {
+        return fromBodyRotation(entity instanceof LivingEntity living ? living.yBodyRot : entity.getYRot());
+    }
+
+    public static Basis fromEuler(Vec3 euler) {
+        var sin = Math.sin(euler.x);
+        var cos = Math.cos(euler.x);
+        var xmat = fromVectors(
+                new Vec3(1, 0, 0),
+                new Vec3(0, cos, sin),
+                new Vec3(0, -sin, cos)
+        );
+
+        sin = Math.sin(euler.y);
+        cos = Math.cos(euler.y);
+        var ymat = fromVectors(
+                new Vec3(cos, 0, -sin),
+                new Vec3(0, 1, 0),
+                new Vec3(sin, 0, cos)
+        );
+
+        sin = Math.sin(euler.z);
+        cos = Math.cos(euler.z);
+        var zmat = fromVectors(
+                new Vec3(cos, sin, 0),
+                new Vec3(-sin, cos, 0),
+                new Vec3(0, 0, 1)
+        );
+        return mul(mul(ymat, xmat), zmat);
+    }
+
+    public static Basis mul(Basis left, Basis right) {
+        return factory(
+                new Vec3(right.tDotX(left.row0), right.tDotY(left.row0), right.tDotZ(left.row0)),
+                new Vec3(right.tDotX(left.row1), right.tDotY(left.row1), right.tDotZ(left.row1)),
+                new Vec3(right.tDotX(left.row2), right.tDotY(left.row2), right.tDotZ(left.row2))
+        );
+    }
+
+    public double tDotX(Vec3 with) {
+        return row0.x * with.x + row1.x * with.y + row2.x * with.z;
+    }
+
+    public double tDotY(Vec3 with) {
+        return row0.y * with.x + row1.y * with.y + row2.y * with.z;
+    }
+
+    public double tDotZ(Vec3 with) {
+        return row0.z * with.x + row1.z * with.y + row2.z * with.z;
+    }
+
+    private static final Vec3 UP = new Vec3(0, 1, 0);
+
+    private static Vec3 getBodyFront(float yRot) {
+        return new Vec3(-Mth.sin((float)(Math.toRadians(yRot))), 0, Mth.cos((float)(Math.toRadians(yRot))));
+    }
+
+    private static Vec3 getBodyX(Vec3 z) {
+        return new Vec3(z.z, 0, -z.x);
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/util/ComparableSupplier.java b/common/src/main/java/mod/chloeprime/hitfeedback/util/ComparableSupplier.java
new file mode 100644
index 0000000..42a4003
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/util/ComparableSupplier.java
@@ -0,0 +1,31 @@
+package mod.chloeprime.hitfeedback.util;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+public final class ComparableSupplier<T> implements Supplier<T> {
+
+    private final Supplier<T> delegate;
+
+    public ComparableSupplier(Supplier<T> delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public T get() {
+        return delegate.get();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ComparableSupplier<?> that = (ComparableSupplier<?>) o;
+        return Objects.equals(delegate.get(), that.delegate.get());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(delegate.get());
+    }
+}
diff --git a/common/src/main/java/mod/chloeprime/hitfeedback/util/ImageHelper.java b/common/src/main/java/mod/chloeprime/hitfeedback/util/ImageHelper.java
new file mode 100644
index 0000000..c09391f
--- /dev/null
+++ b/common/src/main/java/mod/chloeprime/hitfeedback/util/ImageHelper.java
@@ -0,0 +1,23 @@
+package mod.chloeprime.hitfeedback.util;
+
+import com.mojang.blaze3d.platform.NativeImage;
+
+import java.util.stream.IntStream;
+
+public class ImageHelper {
+    public static float getFillRate(NativeImage image) {
+        var width = image.getWidth();
+        var opaquePixelCount = IntStream.range(0, image.getHeight()).parallel()
+                .map(y -> {
+                    var sum = 0;
+                    for (int x = 0; x < width; x++) {
+                        if (image.getLuminanceOrAlpha(x, y) == -1) {
+                            sum += 1;
+                        }
+                    }
+                    return sum;
+                })
+                .sum();
+        return  (float)opaquePixelCount / (image.getWidth() * image.getHeight());
+    }
+}
diff --git a/common/src/main/resources/assets/hit_feedback/particles/blood.json b/common/src/main/resources/assets/hit_feedback/particles/blood.json
new file mode 100644
index 0000000..c3d021a
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/particles/blood.json
@@ -0,0 +1,10 @@
+{
+  "textures": [
+    "hit_feedback:blood_0",
+    "hit_feedback:blood_0",
+    "hit_feedback:blood_0",
+    "hit_feedback:blood_1",
+    "hit_feedback:blood_2",
+    "hit_feedback:blood_3"
+  ]
+}
\ No newline at end of file
diff --git a/common/src/main/resources/assets/hit_feedback/particles/spark.json b/common/src/main/resources/assets/hit_feedback/particles/spark.json
new file mode 100644
index 0000000..909f57a
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/particles/spark.json
@@ -0,0 +1,36 @@
+{
+  "textures": [
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_0",
+    "hit_feedback:spark_1",
+    "hit_feedback:spark_2",
+    "hit_feedback:spark_3",
+    "hit_feedback:spark_4",
+    "hit_feedback:spark_5",
+    "hit_feedback:spark_6",
+    "hit_feedback:spark_7"
+  ]
+}
\ No newline at end of file
diff --git a/common/src/main/resources/assets/hit_feedback/sounds.json b/common/src/main/resources/assets/hit_feedback/sounds.json
new file mode 100644
index 0000000..2674db4
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds.json
@@ -0,0 +1,40 @@
+{
+  "feedback.flesh.sword": {
+    "sounds": [
+      "hit_feedback:feedback/flesh/sword_1",
+      "hit_feedback:feedback/flesh/sword_2",
+      "hit_feedback:feedback/flesh/sword_3",
+      "hit_feedback:feedback/flesh/sword_4",
+      "hit_feedback:feedback/flesh/sword_5",
+      "hit_feedback:feedback/flesh/sword_6",
+      "hit_feedback:feedback/flesh/sword_7"
+    ]
+  },
+  "feedback.flesh.gunshot": {
+    "sounds": [
+      "hit_feedback:feedback/flesh/gunshot_1",
+      "hit_feedback:feedback/flesh/gunshot_2",
+      "hit_feedback:feedback/flesh/gunshot_3"
+    ]
+  },
+  "feedback.flesh.punch": {
+    "sounds": [
+      "hit_feedback:feedback/flesh/punch_1",
+      "hit_feedback:feedback/flesh/punch_2",
+      "hit_feedback:feedback/flesh/punch_3",
+      "hit_feedback:feedback/flesh/punch_4"
+    ]
+  },
+  "feedback.metal.failure": {
+    "sounds": [
+      "hit_feedback:feedback/metal/failure_1",
+      "hit_feedback:feedback/metal/failure_2",
+      "hit_feedback:feedback/metal/failure_3",
+      "hit_feedback:feedback/metal/failure_4",
+      "hit_feedback:feedback/metal/failure_5",
+      "hit_feedback:feedback/metal/failure_6",
+      "hit_feedback:feedback/metal/failure_7",
+      "hit_feedback:feedback/metal/failure_8"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/gunshot_1.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/gunshot_1.ogg
new file mode 100644
index 0000000000000000000000000000000000000000..0e1c52958c0af3bbeee95335bb7228ba2f77d915
GIT binary patch
literal 9939
zcmaiZcU%))v+xE&?^Q%ZkWhl58jw(wUIGyagdU`Y9#BG25J8%Xph%Mv6p*S^m8vwQ
zNS6|-hzQaJQJRACE%-d|d++zpJ-@Rjv(wHwb7p2|H^y#mW&joNPa75Q5<Ud;NN2Jj
zf{;L8KPL~;Ap)XZa`*!P;YyIB-&%<Aq2zzsq2wW@C^}t^?ZE2bf3%y_fAyRJ$yOeh
z-KCBF+=RS6oGgyC3mFK>%gV{i%FCV=k}xLvcwF`)kO)5ha9WW4G<a$F`FoK3h4g)i
zgv*X@1T}Ym|I5lUGFJked|d)AOZyP~6%M(u<_2cwYX4w`9K9Tc41yeeg!CMpd;>lF
zo!y153;jb>cOyD_cuPC`5*a{2Z4GlBHE;!I1^+vj&LP9Uy8g?~4sz(}T*T_AnfV6z
zxCm)^dt43#apnf*MmlQx9{&CwK5jyeLL{Q2x3>_H;P2=y<m==~aP}7p^6+;DDSvV9
zjzl8CPbkRU!<!)F?(6MxDCXfW<m>bQ3H!SrQoOGnjz`2GUtbr3kC4BklQ#iE^Vh7W
zHMJ}tfI0xcNPLpI`6S*bc79Eh<Ff*qeO{}_HH|nT1muluyG2O}!g<-6w{0bEzE8jb
zhXS`Wu5w4kYS0TuiK<y8MvAL}&}|fm^Ekw@YyLavpxi;K2K~vXSy?6K$S+E}0#Q0B
zD|QgKC=|t}S(_1Af&!Ug3i>9Iw5Uaas5c0S*oaoyTHYwAf<#=zob0>dNG8qO8IcAG
zyZljZ8drJ$&H>ZVPl%+|c*hr|s{yWn7FEj~)vZy)A0>&RKNTf}bm0LK03bV8A~sio
zk)8iJBk&FY*wszBJFW<HJ`m`v6kwufv8Dn26QBqA^keh%@6G8a!mJZTkNW(br-<(7
zM_v9%Tps{TOqq4AaMK;hxc~rtwo+`qRcyX{?85DP^ieS-R6sZYK*4E{T4t(x$=dzq
zxY26j<p1clsf6Q4;-C%L2R&SiX>ZF?AE!M!2XGnT9I>*k;r|*roDa>K%TS7%CeLPD
zvu2<xQ76gzRo4<TW9v{0WKv(}JXx*}gmTH&%o!lx0=YHYn(;3rUU}_(hU6jcO5c(L
znWG(q#7eIvWW-m25KXDay$r)j5Yn%^hRrlQ#E}kF9SzHEO9EuB!`q^Uy41|~Z6IXa
zzVwmI{vVbjz9S=p`&DO;iFf<}rypE!7t-Z$;+J32*jbci;EyD0E)Oia=OruCi>6$`
zqQ(CjoC;8<7mnt}>i;`9-4>4F^Xq~aW=EhUi;HsO<hv0sdX5I=)sh#iD$H`N>{@3^
zTd`(Dg%K;SHPk{z<Vy=1uhJ?k8lInwWjwN<5NLZ%+T$(>riYrHZo>=7evFbuf6Xa$
zcr*ZD{9Ryx<K%z)hCn}us3|AdrH14chRk$u0ff=i0PC7i+p%c;PUsZacLN-)QVG`o
ztN-0PnEZr5XaBnqcsn1d2ek-GX8(7a;DG{{x%H0x&EPrAa1hozh%o+vH0Dw><<=QO
z;s$u|rss&Jii9BryqO{%XF;?~CE8X8Og<z`?u1VNkB+B1+9D?aC?|@*6HmhvXO-^3
z4AkA<NdoG$hY=b3S>oQTQX<SCQPd;R?OJM4dd5=vBgv;nECBl<%VCQxxEfo0HMZ#L
zy~W#!?&+yT#TjS13RdgOR{pQfbmZVL0Pu(D`N8zCFdZxi99cJ<`OvxoZ{=V*eu(Zt
z<lj9GaH)X?we<g+H2ygN0ATK{;O-s}z;KFEuu?cPDGFAHVut&_8fFNI;Q|c{=3NNz
z-iFju`tY8{V#&jI5_#F97g6(hqdgK4<OOy)=d=#2oECF?szy{Y4QMndU<$^S+&j5e
z0lz+jWtdJMO7diWJ4&zk$*?K9vcN_KonLM4HA@C>yCm{$44<9MH}`s42?_yp;Kh{l
zK7j|K&J6%JnX!!U=wzop_(B4<3pFp`6pJb(JNKa^vXZ+{;#o<Y@O<zVzCcdyGngkQ
z4H`&*{?QBpFHH%giw?jp4t%;SfJs7PxJ5BsI>y}iQ7#=y3bGfeU}}!TaAPRuIBQ-E
zE>#qB7K7o&nQ`ghcyMN^7+e*hA4wcH$4&9#DAfep?NH0qfT^>X@xxn?8UWtntBF$&
z|Dy^wYL1&!1gQ$9{&-Uy!5Y*Yh_@caS&oE)nki<OaUL8l6*o>H;8FuF`QxT6@Z+O6
z>ygmOk?2b!p>K+w*86mol|6f2viDqium0;wvcZti;esgIrZ|jc711n!Xv?KD%7de%
z>X-$9T0$=c5XMKf2Lg0(DTKo*QwaDVf@KXTLc|4zPW$8W4+HR4uC_bTLjeK!6vDJ6
z!Fq>)?-&V~-Z2`g#*9(|hHwGXBLQF1iIcmaSO6|H`Vz=D8a;E(XX@A^Q(VAQGX4@M
zcFgC}xrs&})pDZq<oW02BFuV?6$t#KVtpefEnsT#gx1Wt8064PBB*c5AAjj#z?Az+
zy@@#vZ%G*6!C7vHf{U2h4xQ%1jqZRZkD;VyIqrn!uQ8?Is-wXbETj?fRySrB_Z#K?
z18hOr%AUqD)mK;iW6pyyPb&ZbR7Mii(J>|Tz-K7H7e&@G(zwkX38n&gR06h@FRBLx
zKTg7G!i%G^*t>j@2`Dj9(nYjHPQD4+Fehq66J8mOwec>cWZ2wQh#It!$geU%-!0DP
zxAjucjnjBXPD(_H=O-T#dQg&Ckzfi16P*nL-e!do7md7!rW4LjL>r0b^Jz&|rX)Lo
zgfScMy0$cuM1ET$+FLZ=%GL-~%}2U-IJx-I<oQ)ttXB#N3#uZpq9s99XtW%-Do_=x
zCK^;_h6Y#VbOuhbLVH$%stn~uOt5!Bf+1QCROM6oWkkbWDv#4yLbRRNxvaE`Ukm<s
zatg0=X~W^<qV0+3_`}KL>%eq)vGPlXO-Wtbm>02bD}(gDxXQ-NsP1r8bu`wbwT8&|
z;Lh7j&~c?zHnwr7TmMWRTLr4}s@w(%pejSOLN%Y)J%#NIn`a=w%d>JmgS0L;KW65o
zP{Zd1&S3+pnjdinYe;HVFg=JR-vr~H>M-u1!e}|#9F5y@3wcO-xp{JOtlWZ79#(E%
zAPOtLAe5YlJ%q4~`FV+0ubgBo*0U6hd#b~@KY<kGr31rO6i8qM94-rVe^)g{;>H6J
zoj<e(kYHq;Ji>s{N9_KK2?&@v;=u*r@jKe43ZMgn4z31Dff(!U&>{09Om*}nB+e4V
zq!OmB@P|Cr0k(V=ltT>B_FRnZXu#A?v?EHn#%HB|eecbNN(TUZgVF)nR3|L-l;HeK
zh@(}3g&TOb8v4SK?BJs`pB;+^b0gnnuq=_yVdmz4&I#NGeU<(J_=t>8K739dLix#@
z|EbOXTc!Okhz8g-Rswb}jAg?smDF>D&p^UKrOkZ#X@1Pg3~+@w6m3p&GH5BsI63<K
zykQ%|6cRhyptx$xse~M9f<>3M@jHV)HDXgzIg+6Q`Yo?l32ICL?IG1}Me1wVNx*u^
zjo6UZ8y=20l_)HHh-82BlH0kgZu?WnI=tG(t5<DTD9WUgVnh0NSWMoP+xZar4J<J2
z;!q2MQQunV904FGk{ZyCh(B8b%i_65d+czx^bCLyyAyc8WH1iFZG+qb;KNx$6nlhY
z)ub17`4^X;9QT)xAwN0sA3m^-kYC6KWkDPkeI!eN2rc~w2S2QAkEn3b|3LXs>}ZKY
zKG3%8n&9w{mH)*h{s)J}{)+>T>L0#jtN~bp3MhfD%BLZI9LMYUgyEQA_>upr0|0lc
zh1s1fjUG9-Vqto@qGC-nJVs8dOQ-;D#wZ*uOo|nz%4xGOYy%(c>`;InG=Z$Ti7Yk1
z%*uYAPD5P%@=LI~)pUu`M<SrN=u!X<IUWEeCNW2)9^Ixh(MIEc{8NPVnlig4djxyy
zXMj<^xrO1J>PC$ChfmO~TrAzu3mbU)Yyi+f!_F}vpc~Upt9Pq|P9Lm>!S)VlrUROV
zskF6`w;tqFwhYW{Ksdo7<(~$HLXRNO4*@9j?^~a`++X71Ti+o*%6jswgRInPk<-#r
z(r2VaVA9fOq!7{w8EGkmtmtV8*)y_I(y}r#vf>D7QJ9FRC`=q7CnF^zD<_MPmXbVk
z8YT>b$()gqm64N{7CkKjlai5!iNJ(~VWKji8hL3^X{k1-MkcG-DTi%tK7<nFvvt{>
znEqfdyCO+}*@<lz*4`4rxAk8tG<sJeJ0Cp8KYFXOuMP{p5zbkWO)cx*k3POEaqgl@
z@yZVq{oaN7*T>_nYo5`6g<JpH3^ReOxIwk<vCLBoL5rdL$|Vovo^h(Z=n$_cRkZ%F
z%RlbrTunrB8_?5=F{*Pke`Fv(^GzOj?;fY*`a<Bk|NQ(0VnYTY>p;b=#}Bx_3A2#S
zW^X==w{?1(;Cy##(Z^}x%3JqVe%lH?!A<p^!5+E_4NumzNzKX2@sk2yB09z<KQg5W
z)cMz1rEi5Pb`9+Xop8yGo3denI^tGkuVpU7L`2>)U@!Tdn+lQ7Z+9mbQpA1p>#}^*
zMz5@lHr==3fBBfvQ`^{uX|6F#5Z{qY%QO;J?9$iNYh>0#dREkE{nLA5-*@xfkM;{O
zIl&{v02)AI{^<P3YT(PsXW@;*s}ZjmYy|H6$9(IZE~`Elwi$}_vG%cODEQuUl@Vs>
z89Vk{f80@5)GA?r!%HuXRKL)vMdS#RRhtV=rSzhD=#7qhT&enW9K{0X*7J|J3NS;9
zQJ-`|>ofC;oW{@G9^;Lx?HpAYPv+t0nZZMig*-NZtMr4`fKLVd%q@eV2TF}8*R=Fb
z4Df$6H1!J3xJm=nb7d&xrv{F90lQ2N$md;@Q_4J3V+t<jbufov)N5ATlDE&Ax)t+O
zuXi4U2y{eH^4LC6RnXdDeA9BXOjf#P_cCnDjeTv?zdS6x<9k=8KoFl<-p5wmg62dR
zUrAP3G=7-Qr>I|jrx+PVQ?EbzyZh=${#+*a@1-ZcHMJfm4m$LSm7>gT+6t`a!@2TU
zn>}AJv*3eHijG<uRN27$COYjV7{x9m){Gv!soM;6U$=*FcWBNp{`rC#j7f;#f_XJP
zN>Y)+M_9@O<&P~^WajS5dx>24{j$UdkFKH?z#!glQ%g{P3ZN>y_H1`DH@Eh)T+KvN
z#EVo0gLiuMk;XwY#yo|iN^3GVE^<M&TtudBWlo62LYM}p14C5?JTJpzs=Myn`QF|8
zeb&!6uI{wO!3gVJTW2b$R^^G+rwx8?STU;v?Gs6L%ah$bFG8PJmHu=TsW*Ig;|D&?
zP$HOIP7r616i<X1^FScAne}BFqXF^q`c;>^ejfZ9RF2fYbJsECv$maAx+a^<l{Xi@
zFvnILP}yCoyz3t3_qJr~LAb`#%*NTUmY3`!=t{N=y!%&kC;x;!3g9c;S{<{>{$31d
zn-O5ct8}CxWiEt-F)s;ET{k*9FYwCWuN<`;Usb06&N29IRuG~2Rt(h0Qhjg_@)Y>S
zrLK37^5^MRN6XhMw)_+8mwGD=r`EB_lOn+;!P{@~Mh{*DD7(^_Tbi?92UH<|KV-Dj
z%s?J<_aay7;-&FSz2NJ1`ai0V2`Im!(>4`@Ts$!IQfCd{?>r^4cC4^1@NCwT<>!OC
z-eIn3T3wt-9F^YLP=FVBMw88ldpwuwGU4mkwblK~_~uD8Ew$*Z(ea&taa|)-isVg<
z3v)IsfgiC*qZDtY()Z1;$+q-lCpqJbiz?G}BD|DnQEL8B;my-hopd5M&bd4C&*(=&
zbeeB9gMJ9jU|am!#(mqcCl(hzO_RCY-mKd__u6`BTEv1FN>E^0-7ePgNrFbunDvXw
z_!_<N-dWyidl?b7e>FC<W~dDQ1krKExBT6FAuRwi^QLhN5^+wFOn6fhxR*-U442w@
zM!R79a#1zpp4U&Mw%+Mp#g9$5;$D^CFt^1qZ2SCrklP7e3bFipVnOgj?%Vq58u5?x
z1t$j$<EF0jOim_S_n=qrZ44>eS<>I%r%rB(;JiH1GSA%}!Mat6cVms;x!~i%RV<tJ
zgV|R3Z7^lks7t}z1J6x^Fsr(yv`%dmbSo;toXSNtWnGme^YNrn`<1~1miUax*ZA2D
zVyASH>YOC*`YAvabA!durV5pQB8vgbx);{kH2%AyFYH5jU}^L^FN2S2I?C%_#<()&
zv94<M6gv`EcK>W7TLKlYId#dn)WUp!_xBFT{x7mNvxV>WJ`FFuM%$YIjy5}6Y!^hS
zgR&yc{@lk6ZnB}zMp=CdM>hKEA{OOf$aEY1)NIU-GaW~W<<OEa=#{}DwJIx#$}l$U
zANvp<;)P1q28uV#slxU~LfF#hZ*OYOTDG0@y6Q)UAT4pH7oT5iYFNYPaEB`gSoaT2
z7=^4(uVDn6xS%Ec=WjpPeCW)z^d4y&P)e(6tiy@-6mdJ_nn3@lP%-X%T4+jj&G(7c
zhI<l4n73>BAHQZp5QfW2>$|{JXa#g`@Tb5@gIoP16}$3VW07~8r5ls(9Gu8xMH<*7
zzWZZ7&JacW&0r#<7o+YZT2{D~Vd42Xbr*VX*i{k&!{9vLON6T@EJKrzBL)=DMwa|)
zY;b+CK4a(4{XA**q>8~1i>j%ZJ^U9C0R%#B{aiFNJ)^!!DRR%stFLdGyjYkr@O9q9
zn3~(5ShAm=h8<F2B=nKbn8tdI^SXHZnTC_jd6vBL=NNlap}?H_xXtF^8Z|pLK!H?K
zYEmnt&2BCkgq6On@ZYMBd9vPU-~ST=2<Rv>`7|wQvt!kt3I;8retsog6^RMDcHYnB
z2c);5WcJoBb}My^>s=X!N}&HWO{qrH&paJccHe1g_6i5Rhd+!nCo9*hLJPtcMVIka
zDP2w&{jJ_>UF9#&i*i79iWQQ71BzBA;t4s0OA$&JA57k8>NJIx-gvS!JsfaHChq1_
zjVi>RneVkdA31JZ)$!JZkS-+zQX@l9r?82C<-4&+W6$Crr_qhCOoHC!TB{TGjL_c;
zjqMPGaj@p6`99zF)6f&O%@Ls)v)B6{RVx+`tiE&0t^OX}U2xsX$bMkA>rGcBehEP6
z%`osiuW=jZ#5rixFCW+YH7)b}<mB+`#LxGk^UfL@dVQO!t?i8*`dxXs_&-hvWBmrc
zT}YRM2KD=?h8<y@XWUJ`%EK}Ix6G!I1|BvSGIh^KU0k!hYQhxAbDDbBP!(rK)4xfb
zOjS%P@OUOx+v34(uevY9gGM~#-b}E4+s*#|NAY%__MP4uTn?+#hagSwq^|x+w>M$n
z-B*d9ZJJ^C5g^~l4EA}nQC0Qs+kVMHanY&kMh>;XY3$mjf0*8j69Ox&9a?<V<{mE0
zW~5|3&vo0gxOeBS;BDeJgw6)Jay{1zJsLE~YUGI1x}2+KS{RLWXfeHI%QZx$wi8{j
zO!k#_t#0WMagD6O__#JC3=QQDLX7bG#fbxAzJnYS4A=_>qA@G}yPHi<wai*Fj>$cf
zcUy9y{yMW^dXNX0U)naS3wFEU`apvxJY?-e?|RBxkEP$6QasLj<6&-;gys*MgSw+V
znZtm#sUma#+5HqpE?RnEI86GC$gftt#^3Je8=AuS+=fRDKd71o-gUhW+y(+^`S=T#
zhn$mVvTPNano3up_*gcVQ(rksjg3q>(}F&f8@E&G0H3_;=VD~1pXiy_htIX8#Q1EN
z)KwtOzV&dj0W{6+K=8rYbGvfrz%Qdi(-nfgwG;K^=7TLP20$7&(^QAejoH&j%*lOh
z%RjTnGO8grBUNrPwE4YBULdWs62gt)d(yl~y(y>{q45+5)DihQTxUSHJXv=0`%nr+
zZ_+vR)617|w`aP|g)OhR7!jCkwXvGuK7Cg|7+;=<K$(ndiuvWqG2YlNuYq70lx17P
z14WXKy`D6{+?&RfNvNyo@1>P=Xi0qijp*gmQCNF{)k4eS`eb8>%F4rY#R_82em}FG
z?0N-Z##Eka;JyLmw?ue`Gh4nQUv}kT)Y-3y+_MmIbuRFbpGe9pcFQxcE}s0Q&ij}J
zY3L2R-t=ctsCq$<otbxc)7)}Z?bzsB&soVT$;X&=IvWbv2d;<diWiG|q9Xt^GgJRY
z?Y*NfYjxS8x$nI4?oTF}n30k{mqWgN`^|rA>vu!Nu_!Ichy#WhU*|vKfVTCVQanV^
zQj<SV+JQpy$|?88Y>DTUHa`A1eSJ!%n1_cri|HGb>+$~XvJBRz5{oSj0r<$D!j9CR
zmQ)DWKQ!be<_X8Hmw)$@Jg|q)T|_&+tO+BL+Z)P3ZvJPrb~`cFk)8m?eowD%7IU9S
zxRb~Mh{+I0y7s6xBG*ag7Dx6E72icgIjPdxFhW<)S(~q&IFSXFdo*M{jx3I?0$_Rt
z@fw4;y#+o8A${6Cq^XVl&!kQ7Ddge}@|_I-)>LO&1V-q}=TqS~!Ib*NHpWycVPM#(
z*DYUU>cqwT>iZnU`=<ASP3=l5x9U^b%vcuLV`vJt7N;fh7|d&<@_j-P;-l^RQ8hL^
z9a^8`cZ+_}?zewvbuIe+mMy;TozjG`!%b*r*kjXst-pejyqP$cZ_1L&MPA>NB;lkU
z*>3pIL$xmPf2OOTDh9lLZjf#zDZ~Y}wNvO<!nQZKCgb;iqnw2>dmabv1HE(M3Yt(o
z=T?SLAO=w9QeTgJG3%$$dg8_t8d8QL^GQ4td~)K+FofGsx>F?#;D)S1Ye#~N>Tr(T
z`)AmygTrVae|U0DrMOdzid)NuJ#80YhO7enwI+eJE{bR7C#$QBTKA9&(dk$gx%BT4
z0X^3^v&MsdCk^_+mxO8IdLORNXxmViME}z`+qC1+Dcm|n559s$cS1Pe`B{tgO6X0p
z?10~i9Cv1Ecd1VKnx#2yeL3GhfJDSFtfj6F>#EXvb?=GCh7D=^&k3PqY0h&rVOE>E
zmHcks*_w5MObBbTn^3`1$zZXc20rNRHZCgfi~0=-HNKpinr#)Qt*1L?l>h{obMqa4
zhrq6(PPeFG+b_HqAx4f7kE5Hg7WdcG-Uun#K%fSCC4**^$LCLQN6>gS$e{9eq#Av{
z$}j8?wf4tDl_Y6;<?r!-mA<BY8Gve4SUewN3zF6_t;lYK)$!cl7zknhJaaC<epUMS
zd3Q$f2=UAJin0nWzA-vR4b|D!7-DmXG^Y}~m_A8V_Nw1rpNz9ND9zL#5gx2J&7-&%
zY(sM5{Qlg2u&STK)U!dI*AgM7K2bW+S$Ca$HEe1-|J#R`9BP%nTwX4%`;u0km%qyC
z`rOt(SLDI6ukO>E*$QwzNY;?#lK9HgO%{A5(-BbTcTi{2O(aI2%aHH=<(o6^sdQG6
zjSi}(7<YDw&V)Tr`cuK9?qoOg&rasYy0x4;7r#ANtmNrLObh{Y+B*CbA2R#%qioJ8
zT7CQF@g@RwFr9i+@<%E5m+zmsM=PhS3ECSrTH{|2E{UzG-{^k(flZ7*a6t{4*FsfX
zCPN%DyTqvwd?2{@fC_oy*|ZOG?e2EBWU@3cxG>91>mg=QS{cIwIFQ;jpznvep@mK;
z6IXKc1o9oe&$)Pbd8vgsIk*c)z0968W@B!8RXD$uN3pyZ)+s@zV4B_9TJBvYbZ1M9
zn$=bb7{OlowTlct(pB)9C9Sv756!`6y2*nbl^;0gMRhM2rS+EZA}kK-({`(E28eDJ
z2DB+0zl7;zr}9G|TxzL5Gf3M&l@%%h#OlF<I?vThP3O2U4=`oHBbL^8s&T0_EM!8G
z+{Vr4vReb*n|CEX9J6CM`uYJqZSmi)AJiatTj(=_7bYqqC4&$-4Su!&KWntrwzj-^
z9jnM}z!~ng0g<2@WBB%?YK`*RSz?gFb+@1__~2Y9np|1z)2qC%E1>lqLJz4RsTXTo
z7Rrk)2EV?6$|TOE#(NQknrh4KpXq}Ym_GOT)N%Mt`oXwDX^nuVf<42^rwm@b2$GS#
zE)p+O<`4OB{sx0YETlwVf-(FtU|e?Z!ZusdW`}Or`HW+wO<&Xd1zm68jaY^=6`NOq
zO-%-Q!|UNhYE^xf@RXI?BXZ8J=F5@=s=p3Sor@rMd_@}UNfy)L2Gbt~zj!3Ytg$AX
zpdz)9V-VR|*>tsO<y}>^&GEfIS|NhNBEREy+DJUhTQgN}jgD2)Y---o71D-4!B+xL
zXMVOgrHh91PiXd~cca~nHAmjkg^8SC))9jigkc1l)f2u~qMdS6^gD`n{6m^|UO0#D
zF-Co7Wn$Ljq_;T_FhW*&nxClr<Oz^|qn#Afc=G2dAF9FV#osmRo?=j)aH;}TfQtST
z!?(g&FYkancWJXdFH5xOXq?IqKe|CS2(zKnOgtNK6BuFGEd2tPl<j?Qk|8NjR?|0#
zYr>=}et$hq1vOlAScD+Ax8U4)7sJEzTCC~<7e<T*JdyoD#vLyZVM;UhRVrKePrNMn
zZS+!bRoBj8nHtfv(-yCj;MpA3Zcy{%WVsX<#pL{Tt=Bc5eqVc}e)jj(rh_65(X{Pd
zDAXYNvr!!6*JX7veN4EkYAu7p_DL4c58aGwYis_VW4=yL7CGM`$`Qt;466)=eO41=
z{z(w8K!h&!^tz4ZN?)caTawyBm2nzNJ=db>_Jrx%)8U==7AJ>#1pj=w`=i3vS7{Hm
zIKa8rGcLLlDe^+*9bZxw3)N;iMB>%jvjN#0*VLTv!QvWy{dChG+pdH-z#BbWB}*O|
z?FT$jKNE4<GkNI;mDzGjkYLIHq7bj#c<tou)8ZNT7yHY@MXL(_LI!6E-ObT@PD7s`
zv%ONT9M=sC35=>{&$Qq!x^iBpEC~7I?ggij*~z=xUw=n@9?!BZN@uaJTb;U@8EsGf
zv70-K#xF#0()Kh%wqTcuNe>mJ?olNNzN2tQ|DLazCB{avF)!C<t2t@JvsUO=&OqF$
z2*$4^qZ|{{FLa*XZ+oQ1pURr*A#-DsCr<6RtLn&;Wqs<?T<b$qB)+A0_lYOr*qL8j
zp^tJz>{c7c0`SOzZWr!bBOd3T$&MxT2Su<yJ)xjC@0<BHl_9U}hq=j8P_=+)r$*?y
z-O?OeAyLh`+)f8mtW*7T#m&Qu#{N!;8Pq?5wnU|zLpyNi^^XR7`6P5Q4Vtof_r=KG
zw96^d$hkk+YV}NSo{?v#6s!eq)RgD+DfkmEIp@VSV#+6ND2W43MY`xxoJHsq+FO`f
zk%82?YOcPiRPw%H_S)?)ZKD|Hz9pSuMol_T^qo5B`;sr3Ph)@&>EDYmih-7QlezTe
zJ?DH_irewzl9{9ky-Kz!>0!0=n6^hB(rtHJj)!rHm!ga8B=<QwK6YrVBUf*%$T&@D
z<+*r$KK1nHNyx`eJ8`xaD#oiSArbb!Zc7o9($o8H%a1v34cy;!Un!p@hwT@pd$9jJ
z5i-rVC+holYtp3~#VSc?e<V&**j9yVwOYLCpF8`_gc6uf-DYLBydU#z1qQKF5{zrL
z72l{F=l0MzUp;X9%7y7`w?AbCck$6m_Wg3;ct}n5m*shTW{&3KxoR;*hdrf?dXhrO
zUa{tZZo*tJm3)3xp*zmD&cnEr!j>7rDVZ7ibP9T>fLHdovwMtxnaofL5398k)A`-A
z2GkM~X*15=dbz|o@0Dw`)duM~OvFh8UWv&xioN9b6`Pfnj%m#4u*}tk>vdmhE{!v2
zVhE^NnEI5qx=p*@Dc-=5lTSzQn6+)(pPX>5<9R&89WP*P=ZN|mEd^g#fu9mvD>BOI
zinix?A~wJQ|1PnvRz?W4@kzaj-1yj2^U-qf%D~!K=mx_J0m~i@F3Vd{e-J@SdO9wJ
zP9~*1PNxhF&(v~!eVUs@!uPO0L~ZBd)8l@lMrq_9It@Gn>|^5OgxOEg-;Ca2X&*yn
z-?~<Ci)B-G$4KUlgh965pMJzrTo~6hd<n0)pNTj7sAL=LZQ`}{QkLRI*vep`gpGE0
z2{2JEd784|hc?Lt!Emag65qgaUPHe-4j86UgJ(upZpwSnQ@k)Qo@CzQEeo(vb4Ur?
zPG>NzQKEkJHA8uRq%qnn6>*IB=WudXMc#VPFX2>Y{f~WvOF;7ak0qFv^9_D|w<Z5n
zNUht?n?JL@d_{OL7iOJuq&gqCzcYM^6)M07xTIxab#-Jhb_p@uYRbO?COBW=KQex^
zWAuFtd-%oiLbg@P*#fcE7r|09K-^6JMVw&vw(=@j!(dF1ImESp*XB{W;(pNP6>{T1
zUr=9fE8lMY&gO2A-!zN0tipjrkXf14hIqNfqx|BF-GaOp=PT1{v;4`G71|=M=~B_O
z=TrCBOD@$@fqw`B;mlMKH`QsWbs>MWnoB-;0iHGdA*{GD2A0%bJT(9@0w_CdXh5Bs
G;r{`~8)b|D

literal 0
HcmV?d00001

diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/gunshot_2.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/gunshot_2.ogg
new file mode 100644
index 0000000000000000000000000000000000000000..9ca50bd0eb3d2f7412e2f94eca12ff1b7de33bf7
GIT binary patch
literal 9750
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
Sgbl$938xBq`Cyb53i*GQR~1zN

literal 0
HcmV?d00001

diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/gunshot_3.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/gunshot_3.ogg
new file mode 100644
index 0000000..770f9ee
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/gunshot_3.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3eacc9d1811e75c1748830c7283015153f19815c2db07ade6febad30dc8445a5
+size 10951
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_1.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_1.ogg
new file mode 100644
index 0000000..63f386f
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_1.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2bed59767bf50100ed4b17e4a013ebbaf4f6311ae6fd451c8163294b9b99914f
+size 47316
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_2.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_2.ogg
new file mode 100644
index 0000000..d046bd7
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_2.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a08bb4b6fb0037af5c3176e5c0f8271eb5f79c90fdf7b2d700a3bf50d1a61ea5
+size 33170
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_3.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_3.ogg
new file mode 100644
index 0000000..9903824
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_3.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8870678901245b30d8923d2f0b21785914dccca5858f24100b3d63acf478e471
+size 51305
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_4.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_4.ogg
new file mode 100644
index 0000000..f02ac7d
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/punch_4.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e322e4d2dc4e864fa8d67bab253e4147369d42519f621384e9dd12bee2e680fd
+size 38740
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_1.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_1.ogg
new file mode 100644
index 0000000..032c4ba
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_1.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0a9c91611e864813ec0e4c1b028f0709e43b2304c7660de052456ef17f01274e
+size 13361
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_2.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_2.ogg
new file mode 100644
index 0000000..e963dde
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_2.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b96ac7976a75924ef0ed5a7230713d26ba2e757d516743e633f9ded656ee197e
+size 17610
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_3.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_3.ogg
new file mode 100644
index 0000000..6875c25
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_3.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6a3fdadcd2e7e5682c05fe66992921725690212befb4f93e4ce94ac3a4acd84d
+size 19619
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_4.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_4.ogg
new file mode 100644
index 0000000..f9679d0
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_4.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3acf5fbbffaa594eb1ea9c59753d5698cb7085c9f30be8d8b0085213c97a939c
+size 12518
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_5.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_5.ogg
new file mode 100644
index 0000000..5a47ea7
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_5.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3f3aea256c35e960340b547c153e39a15f484d1711b54694081043815e686327
+size 12913
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_6.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_6.ogg
new file mode 100644
index 0000000..e2aaa53
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_6.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:404e7b67a256f990fe97965a6021ded5d93282c9171166f0f0644722a7412682
+size 11866
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_7.ogg b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_7.ogg
new file mode 100644
index 0000000..6e3aadf
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/feedback/flesh/sword_7.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:54349299a2a5a40103ecfc88d3f35d9118a83f0952a209038a04e7e27d3362f0
+size 11203
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_1.ogg b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_1.ogg
new file mode 100644
index 0000000..03349f0
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_1.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:60a2b0fbb9df89b33c9c15f2ad171d6b78adb83a132123c1c2f7c9631c6740b8
+size 73546
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_2.ogg b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_2.ogg
new file mode 100644
index 0000000..c78eb7c
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_2.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a255754dce0e5f4439d43a696c515d7b0e0dd265e3eb12da108fcde9ea12fc5d
+size 100691
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_3.ogg b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_3.ogg
new file mode 100644
index 0000000..c249c63
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_3.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8745bc93e3242cdc211a3f0ba59f5c5d62ca0cbb6fc1fc45c4efbebb2fc56ff2
+size 86526
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_4.ogg b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_4.ogg
new file mode 100644
index 0000000..34dcdc6
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_4.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:da410064310e9d37c0027d5b9a903df95492a9fa301337c3e6142291ca0829ea
+size 101124
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_5.ogg b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_5.ogg
new file mode 100644
index 0000000..63503fc
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_5.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e6fd5f54c057e64eba2d4ca4d8919adffae2aec7a60eef32a917ba1f87ee5c97
+size 91232
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_6.ogg b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_6.ogg
new file mode 100644
index 0000000..3760904
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_6.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5bb91f5d56e18870009f366873eaaf5401c0057a9329b771c22dfaf86d114e6b
+size 123047
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_7.ogg b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_7.ogg
new file mode 100644
index 0000000..be1a0f5
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_7.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d3d98587a19d67e482f80b4a35775bd013eaaefb53701953e189b17cc5ea6e33
+size 133011
diff --git a/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_8.ogg b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_8.ogg
new file mode 100644
index 0000000..0f240ac
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/sounds/metal/failure_8.ogg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fb0e7f800c6533deba6de236337fb83a9e498b7bb666a3491c70ded1dd9d5722
+size 134719
diff --git a/common/src/main/resources/assets/hit_feedback/textures/particle/blood_0.png b/common/src/main/resources/assets/hit_feedback/textures/particle/blood_0.png
new file mode 100644
index 0000000..c4b1b7b
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/textures/particle/blood_0.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0f3eb82a316f451ede834bf789ec56b42f05693fecad58cb20560a6026a242b3
+size 121
diff --git a/common/src/main/resources/assets/hit_feedback/textures/particle/blood_1.png b/common/src/main/resources/assets/hit_feedback/textures/particle/blood_1.png
new file mode 100644
index 0000000..65c505e
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/textures/particle/blood_1.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:10e0d910f51177904de51fbee558a3eb8675b425e32ef5d15946a463aa609a92
+size 121
diff --git a/common/src/main/resources/assets/hit_feedback/textures/particle/blood_2.png b/common/src/main/resources/assets/hit_feedback/textures/particle/blood_2.png
new file mode 100644
index 0000000..e0d0d86
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/textures/particle/blood_2.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1b62ed378d8f8ffa694b4b886d4d043c466f6c1502fda0fba8e78998a6fa734d
+size 121
diff --git a/common/src/main/resources/assets/hit_feedback/textures/particle/blood_3.png b/common/src/main/resources/assets/hit_feedback/textures/particle/blood_3.png
new file mode 100644
index 0000000..278ea0b
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/textures/particle/blood_3.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7a570d6557402a29e188e1ca5ee4c9498d5589e69eb829044dd0099a05107b95
+size 121
diff --git a/common/src/main/resources/assets/hit_feedback/textures/particle/spark_0.png b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_0.png
new file mode 100644
index 0000000..68397e3
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_0.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7cf1ac4bcb2db930ef2eb2be25ffa9760d7c48b83bff79203377e88cd1883cbf
+size 87
diff --git a/common/src/main/resources/assets/hit_feedback/textures/particle/spark_1.png b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_1.png
new file mode 100644
index 0000000..e22a412
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_1.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:977ce90c8c10a2e5a989c65ca8c276cce5b3c66a37d7bfc23f607a026ff1a8f6
+size 87
diff --git a/common/src/main/resources/assets/hit_feedback/textures/particle/spark_2.png b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_2.png
new file mode 100644
index 0000000..16f5b6a
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_2.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:801e728382bb41a05e7633770511af1714e38faf5edfeb068c97f21a339e6e54
+size 87
diff --git a/common/src/main/resources/assets/hit_feedback/textures/particle/spark_3.png b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_3.png
new file mode 100644
index 0000000..3de15a0
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_3.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:316d7a485669bb4c4b2f2228b45d167925a6a6da1585a030f52631288037131d
+size 87
diff --git a/common/src/main/resources/assets/hit_feedback/textures/particle/spark_4.png b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_4.png
new file mode 100644
index 0000000..1e9733f
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_4.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c97c18d84af870eeddbc68b00455bc5a232c2bc7a08c005f066c3cf9971cb86f
+size 87
diff --git a/common/src/main/resources/assets/hit_feedback/textures/particle/spark_5.png b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_5.png
new file mode 100644
index 0000000..8b10f18
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_5.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dcf18292493b72a3c3fe77fa5843abd44b32e8a671d88778513aeaa0e997520a
+size 87
diff --git a/common/src/main/resources/assets/hit_feedback/textures/particle/spark_6.png b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_6.png
new file mode 100644
index 0000000..da765ef
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_6.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8263abcdac95d77e825b50f580dc4195fdffc473db1f7fef8b8b699dbdfe78ec
+size 87
diff --git a/common/src/main/resources/assets/hit_feedback/textures/particle/spark_7.png b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_7.png
new file mode 100644
index 0000000..5f1d49a
--- /dev/null
+++ b/common/src/main/resources/assets/hit_feedback/textures/particle/spark_7.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f463403207e39760295324bad4d19e666c74678883465c0e69c1e86f36f75454
+size 87
diff --git a/common/src/main/resources/data/hit_feedback/tags/entity_types/type/bone.json b/common/src/main/resources/data/hit_feedback/tags/entity_types/type/bone.json
new file mode 100644
index 0000000..fa23284
--- /dev/null
+++ b/common/src/main/resources/data/hit_feedback/tags/entity_types/type/bone.json
@@ -0,0 +1,38 @@
+{
+  "replace": false,
+  "values": [
+    "minecraft:skeleton",
+    "minecraft:skeleton_horse",
+    "minecraft:wither_skeleton",
+    "minecraft:stray",
+    "minecraft:shulker",
+    {
+      "id": "alexsmobs:skelewag",
+      "required": false
+    },
+    {
+      "id": "alexsmobs:bone_serpent",
+      "required": false
+    },
+    {
+      "id": "alexsmobs:bone_serpent_part",
+      "required": false
+    },
+    {
+      "id": "quark:forgotten",
+      "required": false
+    },
+    {
+      "id": "quark:stoneling",
+      "required": false
+    },
+    {
+      "id": "lycanitesmobs:lobber",
+      "required": false
+    },
+    {
+      "id": "twilightforest:lich",
+      "required": false
+    }
+  ]
+}
\ No newline at end of file
diff --git a/common/src/main/resources/data/hit_feedback/tags/entity_types/type/metal.json b/common/src/main/resources/data/hit_feedback/tags/entity_types/type/metal.json
new file mode 100644
index 0000000..1cfb0c4
--- /dev/null
+++ b/common/src/main/resources/data/hit_feedback/tags/entity_types/type/metal.json
@@ -0,0 +1,29 @@
+{
+  "replace": false,
+  "values": [
+    "minecraft:iron_golem",
+    "minecraft:minecart",
+    "minecraft:chest_minecart",
+    "minecraft:command_block_minecart",
+    "minecraft:furnace_minecart",
+    "minecraft:hopper_minecart",
+    "minecraft:spawner_minecart",
+    "minecraft:tnt_minecart",
+    {
+      "id": "cataclysm:netherite_monstrosity",
+      "required": false
+    },
+    {
+      "id": "cataclysm:the_harbinger",
+      "required": false
+    },
+    {
+      "id": "mekanism:robit",
+      "required": false
+    },
+    {
+      "id": "twilightforest:tower_golem",
+      "required": false
+    }
+  ]
+}
\ No newline at end of file
diff --git a/common/src/main/resources/data/hit_feedback/tags/entity_types/type/slime.json b/common/src/main/resources/data/hit_feedback/tags/entity_types/type/slime.json
new file mode 100644
index 0000000..ec87aa8
--- /dev/null
+++ b/common/src/main/resources/data/hit_feedback/tags/entity_types/type/slime.json
@@ -0,0 +1,9 @@
+{
+  "replace": false,
+  "values": [
+    {
+      "id": "alexsmobs:mimicube",
+      "required": false
+    }
+  ]
+}
\ No newline at end of file
diff --git a/common/src/main/resources/data/hit_feedback/tags/items/sharp_weapon.json b/common/src/main/resources/data/hit_feedback/tags/items/sharp_weapon.json
new file mode 100644
index 0000000..8d74aef
--- /dev/null
+++ b/common/src/main/resources/data/hit_feedback/tags/items/sharp_weapon.json
@@ -0,0 +1,6 @@
+{
+  "replace": false,
+  "values": [
+    "minecraft:iron_golem",
+  ]
+}
\ No newline at end of file
diff --git a/common/src/main/resources/hit_feedback-common.mixins.json b/common/src/main/resources/hit_feedback-common.mixins.json
new file mode 100644
index 0000000..419cc59
--- /dev/null
+++ b/common/src/main/resources/hit_feedback-common.mixins.json
@@ -0,0 +1,17 @@
+{
+  "required": true,
+  "minVersion": "0.8",
+  "package": "mod.chloeprime.hitfeedback.mixin",
+  "compatibilityLevel": "JAVA_17",
+  "mixins": [
+    "LivingEntityAccessor"
+  ],
+  "client": [
+    "MixinSimpleTexture",
+    "client.ParticleEngineAccessor",
+    "client.TrackingEmitterAccessor"
+  ],
+  "injectors": {
+    "defaultRequire": 1
+  }
+}
diff --git a/common/src/main/resources/icon.png b/common/src/main/resources/icon.png
new file mode 100644
index 0000000..78932c4
--- /dev/null
+++ b/common/src/main/resources/icon.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ca7fd23b1102dc14b2a1b3ba41fc9b1f3c7f6b1d15f084d49ffc6574d44e5311
+size 284
diff --git a/fabric/build.gradle b/fabric/build.gradle
new file mode 100644
index 0000000..2e45230
--- /dev/null
+++ b/fabric/build.gradle
@@ -0,0 +1,89 @@
+plugins {
+    id "com.github.johnrengelman.shadow" version "7.1.2"
+}
+
+architectury {
+    platformSetupLoomIde()
+    fabric()
+}
+
+repositories {
+    maven {
+        url "https://maven.jamieswhiteshirt.com/libs-release"
+        content {
+            includeGroup "com.jamieswhiteshirt"
+        }
+    }
+}
+
+dependencies {
+    include modImplementation("com.jamieswhiteshirt:reach-entity-attributes:2.2.0")
+}
+
+configurations {
+    common
+    shadowCommon // Don't use shadow from the shadow plugin because we don't want IDEA to index this.
+    compileClasspath.extendsFrom common
+    runtimeClasspath.extendsFrom common
+    developmentFabric.extendsFrom common
+}
+
+dependencies {
+    modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}"
+    modApi "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}"
+    // Remove the next line if you don't want to depend on the API
+    modApi "dev.architectury:architectury-fabric:${rootProject.architectury_version}"
+
+    common(project(path: ":common", configuration: "namedElements")) { transitive false }
+    shadowCommon(project(path: ":common", configuration: "transformProductionFabric")) { transitive false }
+}
+
+processResources {
+    inputs.property "version", project.version
+
+    filesMatching("fabric.mod.json") {
+        expand "version": project.version
+    }
+}
+
+shadowJar {
+    configurations = [project.configurations.shadowCommon]
+    archiveClassifier.set("fabric-dev-shadow")
+}
+
+remapJar {
+    inputFile.set shadowJar.archiveFile
+    dependsOn shadowJar
+    archiveClassifier.set("fabric")
+}
+
+jar {
+    archiveClassifier.set("fabric-dev")
+}
+
+sourcesJar {
+    archiveClassifier.set("fabric-sources")
+    def commonSources = project(":common").sourcesJar
+    dependsOn commonSources
+    from commonSources.archiveFile.map { zipTree(it) }
+}
+
+components.java {
+    withVariantsFromConfiguration(project.configurations.shadowRuntimeElements) {
+        skip()
+    }
+}
+
+publishing {
+    publications {
+        mavenFabric(MavenPublication) {
+            artifactId = rootProject.archives_base_name + "-" + project.name
+            from components.java
+        }
+    }
+
+    // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
+    repositories {
+        // Add repositories to publish to here.
+    }
+}
\ No newline at end of file
diff --git a/fabric/src/main/java/mod/chloeprime/hitfeedback/client/fabric/HitFeedbackClientFabric.java b/fabric/src/main/java/mod/chloeprime/hitfeedback/client/fabric/HitFeedbackClientFabric.java
new file mode 100644
index 0000000..16b3275
--- /dev/null
+++ b/fabric/src/main/java/mod/chloeprime/hitfeedback/client/fabric/HitFeedbackClientFabric.java
@@ -0,0 +1,15 @@
+package mod.chloeprime.hitfeedback.client.fabric;
+
+import mod.chloeprime.hitfeedback.client.HitFeedbackClient;
+import net.fabricmc.api.ClientModInitializer;
+
+/**
+ * @see mod.chloeprime.hitfeedback.mixin.fabric.MixinLivingEntity onEndAttack
+ */
+public class HitFeedbackClientFabric implements ClientModInitializer {
+    @Override
+    public void onInitializeClient() {
+        HitFeedbackClient.init();
+        HitFeedbackClient.setup();
+    }
+}
diff --git a/fabric/src/main/java/mod/chloeprime/hitfeedback/common/fabric/PlatformMethodsImpl.java b/fabric/src/main/java/mod/chloeprime/hitfeedback/common/fabric/PlatformMethodsImpl.java
new file mode 100644
index 0000000..51da1b1
--- /dev/null
+++ b/fabric/src/main/java/mod/chloeprime/hitfeedback/common/fabric/PlatformMethodsImpl.java
@@ -0,0 +1,20 @@
+package mod.chloeprime.hitfeedback.common.fabric;
+
+import com.jamieswhiteshirt.reachentityattributes.ReachEntityAttributes;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+
+/**
+ * @see mod.chloeprime.hitfeedback.common.PlatformMethods
+ */
+@SuppressWarnings("unused")
+public class PlatformMethodsImpl {
+    public static double getAttackReach(Entity entity) {
+        var baseRange = (entity instanceof ServerPlayer player && player.isCreative()) ? 6 : 3;
+        if (entity instanceof LivingEntity living) {
+            return ReachEntityAttributes.getAttackRange(living, baseRange);
+        }
+        return baseRange;
+    }
+}
diff --git a/fabric/src/main/java/mod/chloeprime/hitfeedback/fabric/HitFeedbackFabric.java b/fabric/src/main/java/mod/chloeprime/hitfeedback/fabric/HitFeedbackFabric.java
new file mode 100644
index 0000000..4e3972f
--- /dev/null
+++ b/fabric/src/main/java/mod/chloeprime/hitfeedback/fabric/HitFeedbackFabric.java
@@ -0,0 +1,12 @@
+package mod.chloeprime.hitfeedback.fabric;
+
+import mod.chloeprime.hitfeedback.HitFeedbackMod;
+import net.fabricmc.api.ModInitializer;
+
+
+public class HitFeedbackFabric implements ModInitializer {
+    @Override
+    public void onInitialize() {
+        HitFeedbackMod.init();
+    }
+}
\ No newline at end of file
diff --git a/fabric/src/main/java/mod/chloeprime/hitfeedback/mixin/fabric/MixinLivingEntity.java b/fabric/src/main/java/mod/chloeprime/hitfeedback/mixin/fabric/MixinLivingEntity.java
new file mode 100644
index 0000000..b0b3dcc
--- /dev/null
+++ b/fabric/src/main/java/mod/chloeprime/hitfeedback/mixin/fabric/MixinLivingEntity.java
@@ -0,0 +1,28 @@
+package mod.chloeprime.hitfeedback.mixin.fabric;
+
+import mod.chloeprime.hitfeedback.common.CommonEventHandler;
+import net.minecraft.world.damagesource.DamageSource;
+import net.minecraft.world.entity.LivingEntity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.Slice;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+/**
+ * @see mod.chloeprime.hitfeedback.client.fabric.HitFeedbackClientFabric onBeginAttack
+ */
+@Mixin(LivingEntity.class)
+public class MixinLivingEntity {
+    @Inject(
+            method = "actuallyHurt",
+            at = @At(value = "CONSTANT", args = "floatValue=0.0f"),
+            slice = @Slice(
+                    from = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;awardStat(Lnet/minecraft/resources/ResourceLocation;I)V"),
+                    to = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;setHealth(F)V")
+            )
+    )
+    private void beforeActuallyHurt(DamageSource damageSource, float f, CallbackInfo ci) {
+        CommonEventHandler.onEndAttack(damageSource, (LivingEntity) (Object) this, f);
+    }
+}
diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json
new file mode 100644
index 0000000..ebdca42
--- /dev/null
+++ b/fabric/src/main/resources/fabric.mod.json
@@ -0,0 +1,28 @@
+{
+  "schemaVersion": 1,
+  "id": "hit_feedback",
+  "version": "${version}",
+
+  "name": "Hit Feedback",
+  "description": "Add blood and gore as hit feedback.",
+  "authors": ["ChloePrime"],
+  "contact": {},
+
+  "license": "MIT",
+  "icon": "icon.png",
+
+  "environment": "*",
+  "entrypoints": {
+    "main": [ "mod.chloeprime.hitfeedback.fabric.HitFeedbackFabric" ],
+    "client": [ "mod.chloeprime.hitfeedback.client.fabric.HitFeedbackClientFabric" ]
+  },
+  "mixins": [
+    "hit_feedback.mixins.json",
+    "hit_feedback-common.mixins.json"
+  ],
+  "depends": {
+    "fabricloader": ">=0.15.11",
+    "minecraft": ">=1.18.2",
+    "architectury": ">=4.0.0"
+  }
+}
diff --git a/fabric/src/main/resources/hit_feedback.mixins.json b/fabric/src/main/resources/hit_feedback.mixins.json
new file mode 100644
index 0000000..4479a88
--- /dev/null
+++ b/fabric/src/main/resources/hit_feedback.mixins.json
@@ -0,0 +1,14 @@
+{
+  "required": true,
+  "minVersion": "0.8",
+  "package": "mod.chloeprime.hitfeedback.mixin.fabric",
+  "compatibilityLevel": "JAVA_17",
+  "mixins": [
+    "MixinLivingEntity"
+  ],
+  "client": [
+  ],
+  "injectors": {
+    "defaultRequire": 1
+  }
+}
diff --git a/forge/build.gradle b/forge/build.gradle
new file mode 100644
index 0000000..50a9a1a
--- /dev/null
+++ b/forge/build.gradle
@@ -0,0 +1,85 @@
+plugins {
+    id "com.github.johnrengelman.shadow" version "7.1.2"
+}
+architectury {
+    platformSetupLoomIde()
+    forge()
+}
+
+loom {
+    forge {
+        mixinConfigs = [
+                "hit_feedback.mixins.json",
+                "hit_feedback-common.mixins.json"
+        ]
+    }
+}
+
+configurations {
+    common
+    shadowCommon // Don't use shadow from the shadow plugin because we don't want IDEA to index this.
+    compileClasspath.extendsFrom common
+    runtimeClasspath.extendsFrom common
+    developmentForge.extendsFrom common
+}
+
+dependencies {
+    forge "net.minecraftforge:forge:${rootProject.forge_version}"
+    // Remove the next line if you don't want to depend on the API
+    modApi "dev.architectury:architectury-forge:${rootProject.architectury_version}"
+
+    common(project(path: ":common", configuration: "namedElements")) { transitive false }
+    shadowCommon(project(path: ":common", configuration: "transformProductionForge")) { transitive = false }
+}
+
+processResources {
+    inputs.property "version", project.version
+
+    filesMatching("META-INF/mods.toml") {
+        expand "version": project.version
+    }
+}
+
+shadowJar {
+    exclude "fabric.mod.json"
+
+    configurations = [project.configurations.shadowCommon]
+    archiveClassifier.set("forge-dev-shadow")
+}
+
+remapJar {
+    inputFile.set shadowJar.archiveFile
+    dependsOn shadowJar
+    archiveClassifier.set("forge")
+}
+
+jar {
+    archiveClassifier.set("forge-dev")
+}
+
+sourcesJar {
+    archiveClassifier.set("forge-sources")
+    def commonSources = project(":common").sourcesJar
+    dependsOn commonSources
+    from commonSources.archiveFile.map { zipTree(it) }
+}
+
+components.java {
+    withVariantsFromConfiguration(project.configurations.shadowRuntimeElements) {
+        skip()
+    }
+}
+
+publishing {
+    publications {
+        mavenForge(MavenPublication) {
+            artifactId = rootProject.archives_base_name + "-" + project.name
+            from components.java
+        }
+    }
+
+    // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
+    repositories {
+        // Add repositories to publish to here.
+    }
+}
diff --git a/forge/gradle.properties b/forge/gradle.properties
new file mode 100644
index 0000000..32f842a
--- /dev/null
+++ b/forge/gradle.properties
@@ -0,0 +1 @@
+loom.platform=forge
\ No newline at end of file
diff --git a/forge/src/main/java/mod/chloeprime/hitfeedback/common/forge/ForgeCommonEventHandler.java b/forge/src/main/java/mod/chloeprime/hitfeedback/common/forge/ForgeCommonEventHandler.java
new file mode 100644
index 0000000..f8148be
--- /dev/null
+++ b/forge/src/main/java/mod/chloeprime/hitfeedback/common/forge/ForgeCommonEventHandler.java
@@ -0,0 +1,15 @@
+package mod.chloeprime.hitfeedback.common.forge;
+
+import mod.chloeprime.hitfeedback.common.CommonEventHandler;
+import net.minecraftforge.event.entity.living.LivingDamageEvent;
+import net.minecraftforge.eventbus.api.EventPriority;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+
+@Mod.EventBusSubscriber
+public class ForgeCommonEventHandler {
+    @SubscribeEvent(priority = EventPriority.LOWEST, receiveCanceled = true)
+    public static void onEndAttack(LivingDamageEvent event) {
+        CommonEventHandler.onEndAttack(event.getSource(), event.getEntityLiving(), event.getAmount());
+    }
+}
\ No newline at end of file
diff --git a/forge/src/main/java/mod/chloeprime/hitfeedback/common/forge/PlatformMethodsImpl.java b/forge/src/main/java/mod/chloeprime/hitfeedback/common/forge/PlatformMethodsImpl.java
new file mode 100644
index 0000000..9fff861
--- /dev/null
+++ b/forge/src/main/java/mod/chloeprime/hitfeedback/common/forge/PlatformMethodsImpl.java
@@ -0,0 +1,19 @@
+package mod.chloeprime.hitfeedback.common.forge;
+
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.player.Player;
+import net.minecraftforge.common.ForgeMod;
+
+/**
+ * @see mod.chloeprime.hitfeedback.common.PlatformMethods
+ */
+@SuppressWarnings("unused")
+public class PlatformMethodsImpl {
+    public static double getAttackReach(Entity entity) {
+        if (entity instanceof Player player) {
+            return player.getAttackRange();
+        }
+        return ForgeMod.ATTACK_RANGE.get().getDefaultValue();
+    }
+}
diff --git a/forge/src/main/java/mod/chloeprime/hitfeedback/forge/HitFeedbackForge.java b/forge/src/main/java/mod/chloeprime/hitfeedback/forge/HitFeedbackForge.java
new file mode 100644
index 0000000..f95791a
--- /dev/null
+++ b/forge/src/main/java/mod/chloeprime/hitfeedback/forge/HitFeedbackForge.java
@@ -0,0 +1,24 @@
+package mod.chloeprime.hitfeedback.forge;
+
+import dev.architectury.platform.Platform;
+import dev.architectury.platform.forge.EventBuses;
+import dev.architectury.utils.Env;
+import mod.chloeprime.hitfeedback.HitFeedbackMod;
+import mod.chloeprime.hitfeedback.client.HitFeedbackClient;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
+import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
+import net.minecraftforge.forgespi.Environment;
+
+@Mod(HitFeedbackMod.MOD_ID)
+public class HitFeedbackForge {
+    public HitFeedbackForge() {
+        var modBus = FMLJavaModLoadingContext.get().getModEventBus();
+        EventBuses.registerModEventBus(HitFeedbackMod.MOD_ID, modBus);
+        HitFeedbackMod.init();
+        if (Environment.get().getDist().isClient()) {
+            HitFeedbackClient.init();
+            modBus.addListener((FMLClientSetupEvent event) -> HitFeedbackClient.setup());
+        }
+    }
+}
\ No newline at end of file
diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml
new file mode 100644
index 0000000..dbfb675
--- /dev/null
+++ b/forge/src/main/resources/META-INF/mods.toml
@@ -0,0 +1,71 @@
+# This is an example mods.toml file. It contains the data relating to the loading mods.
+# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
+# The overall format is standard TOML format, v0.5.0.
+# Note that there are a couple of TOML lists in this file.
+# Find more information on toml format here:  https://github.com/toml-lang/toml
+# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
+modLoader="javafml" #mandatory
+# A version range to match for said mod loader - for regular FML @Mod it will be the forge version
+loaderVersion="[40,)" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions.
+# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
+# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
+license="MIT"
+# A URL to refer people to when problems occur with this mod
+issueTrackerURL="https://github.com/ChloePrime/Hit Feedback/issues"
+# A list of mods - how many allowed here is determined by the individual mod loader
+[[mods]] #mandatory
+# The modid of the mod
+modId="hit_feedback" #mandatory
+# The version number of the mod - there's a few well known ${} variables useable here or just hardcode it
+# ${version} will substitute the value of the Implementation-Version as read from the mod's JAR file metadata
+# see the associated build.gradle script for how to populate this completely automatically during a build
+version="${version}" #mandatory
+# A display name for the mod
+displayName="Hit Feedback" #mandatory
+# A URL to query for updates for this mod. See the JSON update specification <here>
+#updateJSONURL="http://myurl.me/" #optional
+# A URL for the "homepage" for this mod, displayed in the mod UI
+#displayURL="http://example.com/" #optional
+# A file name (in the root of the mod JAR) containing a logo for display
+logoFile="icon.png" #optional
+# A text field displayed in the mod UI
+#credits="Thanks for this example mod goes to Java" #optional
+# A text field displayed in the mod UI
+#authors="Love, Cheese and small house plants" #optional
+# Display Test controls the display for your mod in the server connection screen
+# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod.
+# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod.
+# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component.
+# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value.
+# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself.
+#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional)
+
+# The description text for the mod (multi line!) (#mandatory)
+description="Add blood and gore as hit feedback."
+# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
+[[dependencies.hit_feedback]] #optional
+   # the modid of the dependency
+   modId="forge" #mandatory
+   # Does this dependency have to exist - if not, ordering below must be specified
+   mandatory=true #mandatory
+   # The version range of the dependency
+   versionRange="[40,)" #mandatory
+   # An ordering relationship for the dependency - BEFORE or AFTER required if the relationship is not mandatory
+   ordering="NONE"
+   # Side this dependency is applied on - BOTH, CLIENT or SERVER
+   side="BOTH"
+# Here's another dependency
+[[dependencies.hit_feedback]]
+   modId="minecraft"
+   mandatory=true
+   # This version range declares a minimum of the current minecraft version up to but not including the next major version
+   versionRange="[1.18.2,1.19)"
+   ordering="NONE"
+   side="BOTH"
+[[dependencies.hit_feedback]]
+   modId="architectury"
+   mandatory=true
+   # This version range declares a minimum of the current minecraft version up to but not including the next major version
+   versionRange="[4,)"
+   ordering="NONE"
+   side="BOTH"
diff --git a/forge/src/main/resources/hit_feedback.mixins.json b/forge/src/main/resources/hit_feedback.mixins.json
new file mode 100644
index 0000000..9aeb6ab
--- /dev/null
+++ b/forge/src/main/resources/hit_feedback.mixins.json
@@ -0,0 +1,13 @@
+{
+  "required": true,
+  "minVersion": "0.8",
+  "package": "mod.chloeprime.hitfeedback.mixin.forge",
+  "compatibilityLevel": "JAVA_17",
+  "mixins": [
+  ],
+  "client": [
+  ],
+  "injectors": {
+    "defaultRequire": 1
+  }
+}
diff --git a/forge/src/main/resources/pack.mcmeta b/forge/src/main/resources/pack.mcmeta
new file mode 100644
index 0000000..87ea057
--- /dev/null
+++ b/forge/src/main/resources/pack.mcmeta
@@ -0,0 +1,7 @@
+{
+  "pack": {
+    "description": "HitFeedback resources",
+    "pack_format": 15,
+    "forge:server_data_pack_format": 12
+  }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..f9479fb
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,15 @@
+org.gradle.jvmargs=-Xmx1G
+
+minecraft_version=1.18.2
+parchment_version=2022.11.06
+
+archives_base_name=HitFeedback
+mod_version=1.18.2-1.0.0
+maven_group=mod.chloeprime
+
+architectury_version=4.12.94
+
+fabric_loader_version=0.15.11
+fabric_api_version=0.77.0+1.18.2
+
+forge_version=1.18.2-40.2.9
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e4b762d
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1 @@
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..06a89e4
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,12 @@
+pluginManagement {
+    repositories {
+        maven { url "https://maven.fabricmc.net/" }
+        maven { url "https://maven.architectury.dev/" }
+        maven { url "https://maven.minecraftforge.net/" }
+        gradlePluginPortal()
+    }
+}
+
+include("common")
+include("fabric")
+include("forge")
\ No newline at end of file