diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..b4958b48
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,7 @@
+**/bin/
+**/obj/
+**/.vs/
+**/*.user
+.git/
+.github/
+CustomNugets/
diff --git a/.editorconfig b/.editorconfig
index 1878d48f..64fe0359 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -703,6 +703,9 @@ dotnet_diagnostic.IDE0280.severity = suggestion
# Disagree about clarity of primary constructor
dotnet_diagnostic.IDE0290.severity = none
+# .ToList() is just fine
+dotnet_diagnostic.IDE0305.severity = none
+
##
## StyleCop.Analyzers
##
diff --git a/.gitattributes b/.gitattributes
index a47ecf23..3cb0752b 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,4 +1,5 @@
* text auto
+*.sh text eol=lf
*.cdr -text
*.exe -text
*.dll -text
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..8c14e2dc
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,34 @@
+name: Build and test
+
+# Minimal CI: builds the self-contained server solution and runs the test project.
+# The BlazorServer client is intentionally excluded because it consumes the Api/Common
+# packages from the local CustomNugets feed produced by the build pipeline, which is not
+# available on a clean CI runner.
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Restore
+ run: dotnet restore BtDamageResolver/BTDamageResolver.slnx
+
+ - name: Build
+ run: dotnet build BtDamageResolver/BTDamageResolver.slnx --configuration Release --no-restore
+
+ - name: Test
+ run: dotnet test BtDamageResolver/tests/Tests/Tests.csproj --configuration Release --no-build
diff --git a/.gitignore b/.gitignore
index d4fe3086..b4859aa1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@
*.userosscache
*.sln.docstates
*.ruleset
+**dpkeys
# Build results
[Dd]ebug/
@@ -42,7 +43,6 @@ dlldata.c
project.lock.json
project.fragment.lock.json
artifacts/
-**/Properties/launchSettings.json
# StyleCop
StyleCopReport.xml
@@ -161,8 +161,6 @@ __pycache__/
# Docker-Compose environment
infra/.env
-# Possible sensitive resource files
-infra/dpkeys/
-
# Debug settings containing password data
*.Debug.json
+*.Development.json
diff --git a/BtDamageResolver/data/ammo/Ammo_AC.json b/BtDamageResolver/data/ammo/Ammo_AC.json
index 5cda3b8b..772b163e 100644
--- a/BtDamageResolver/data/ammo/Ammo_AC.json
+++ b/BtDamageResolver/data/ammo/Ammo_AC.json
@@ -1,6 +1,7 @@
[
{
"Name": "AC-AP2",
+ "AmmoProxyName": "AP",
"HitModifier": 1,
"SpecialFeatures": [
{ "Data": "-4", "Type": "ArmorPiercing" },
@@ -10,6 +11,7 @@
},
{
"Name": "AC-AP5",
+ "AmmoProxyName": "AP",
"HitModifier": 1,
"SpecialFeatures": [
{ "Data": "-3", "Type": "ArmorPiercing" },
@@ -19,6 +21,7 @@
},
{
"Name": "AC-AP10",
+ "AmmoProxyName": "AP",
"HitModifier": 1,
"SpecialFeatures": [
{ "Data": "-2", "Type": "ArmorPiercing" },
@@ -28,6 +31,7 @@
},
{
"Name": "AC-AP20",
+ "AmmoProxyName": "AP",
"HitModifier": 1,
"SpecialFeatures": [
{ "Data": "-1", "Type": "ArmorPiercing" },
@@ -35,8 +39,17 @@
{ "Type": "TargetingComputerValidAimed" }
]
},
+ {
+ "Name": "AC-Slug",
+ "AmmoProxyName": "Slug",
+ "SpecialFeatures": [
+ { "Type": "TargetingComputerValid" },
+ { "Type": "TargetingComputerValidAimed" }
+ ]
+ },
{
"Name": "AC-LBXCluster",
+ "AmmoProxyName": "Cluster",
"HitModifier": -1,
"SpecialFeatures": [
{ "Type": "Cluster" },
@@ -47,6 +60,7 @@
},
{
"Name": "AC-Single",
+ "AmmoProxyName": "Normal",
"SpecialFeatures": [
{ "Type": "TargetingComputerValid" },
{ "Type": "TargetingComputerValidAimed" }
diff --git a/BtDamageResolver/data/ammo/Ammo_ATM.json b/BtDamageResolver/data/ammo/Ammo_ATM.json
index c455cc08..2df21a11 100644
--- a/BtDamageResolver/data/ammo/Ammo_ATM.json
+++ b/BtDamageResolver/data/ammo/Ammo_ATM.json
@@ -1,6 +1,7 @@
[
{
"Name": "ATM3-SRM",
+ "AmmoProxyName": "SRM",
"Damage": {
"Short": 9
},
@@ -19,6 +20,7 @@
},
{
"Name": "ATM3-MRM",
+ "AmmoProxyName": "MRM",
"Damage": {
"Short": 6
},
@@ -38,6 +40,7 @@
},
{
"Name": "ATM3-LRM",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 3
},
@@ -57,6 +60,7 @@
},
{
"Name": "ATM6-SRM",
+ "AmmoProxyName": "SRM",
"Damage": {
"Short": 18
},
@@ -75,6 +79,7 @@
},
{
"Name": "ATM6-MRM",
+ "AmmoProxyName": "MRM",
"Damage": {
"Short": 12
},
@@ -94,6 +99,7 @@
},
{
"Name": "ATM6-LRM",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 6
},
@@ -113,6 +119,7 @@
},
{
"Name": "ATM9-SRM",
+ "AmmoProxyName": "SRM",
"Damage": {
"Short": 27
},
@@ -131,6 +138,7 @@
},
{
"Name": "ATM9-MRM",
+ "AmmoProxyName": "MRM",
"Damage": {
"Short": 18
},
@@ -150,6 +158,7 @@
},
{
"Name": "ATM9-LRM",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 9
},
@@ -169,6 +178,7 @@
},
{
"Name": "ATM12-SRM",
+ "AmmoProxyName": "SRM",
"Damage": {
"Short": 36
},
@@ -187,6 +197,7 @@
},
{
"Name": "ATM12-MRM",
+ "AmmoProxyName": "MRM",
"Damage": {
"Short": 24
},
@@ -206,6 +217,7 @@
},
{
"Name": "ATM12-LRM",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 12
},
@@ -223,4 +235,4 @@
"RangeMinimum": 4,
"SpecialFeatures": [ { "Type": "Cluster" } ]
}
-]
\ No newline at end of file
+]
diff --git a/BtDamageResolver/data/ammo/Ammo_Generic.json b/BtDamageResolver/data/ammo/Ammo_Generic.json
index 64ddec29..66e0fbd2 100644
--- a/BtDamageResolver/data/ammo/Ammo_Generic.json
+++ b/BtDamageResolver/data/ammo/Ammo_Generic.json
@@ -6,7 +6,7 @@
"Name": "Missile",
"SpecialFeatures": [ { "Type": "Cluster" } ]
},
- {
+ {
"Name": "Missile+Artemis",
"SpecialFeatures": [ { "Type": "Cluster" } ],
"ClusterBonus": {
@@ -17,6 +17,20 @@
"Name": "MissileHoming",
"SpecialFeatures": [ { "Type": "Cluster" }, { "Type": "Homing" } ]
},
+ {
+ "Name": "MissileHoming+Inferno",
+ "Damage": {
+ "Short": 0
+ },
+ "DamageAerospace": {
+ "Short": 0
+ },
+ "SpecialDamage": [
+ { "Data": "3", "Type": "Burst" },
+ { "Data": "2", "Type": "Heat" }
+ ],
+ "SpecialFeatures": [ { "Type": "Cluster" }, { "Type": "Homing" } ]
+ },
{
"Name": "MissileHoming+Artemis",
"SpecialFeatures": [ { "Type": "Cluster" }, { "Type": "Homing" } ],
diff --git a/BtDamageResolver/data/ammo/Ammo_LRM.json b/BtDamageResolver/data/ammo/Ammo_LRM.json
index 7e523638..c450f989 100644
--- a/BtDamageResolver/data/ammo/Ammo_LRM.json
+++ b/BtDamageResolver/data/ammo/Ammo_LRM.json
@@ -135,4 +135,4 @@
"HitModifier": 1,
"SpecialFeatures": [ { "Type": "Cluster" }, { "Type": "Homing" }, { "Type": "IndirectFire" } ]
}
-]
\ No newline at end of file
+]
diff --git a/BtDamageResolver/data/ammo/Ammo_MML.json b/BtDamageResolver/data/ammo/Ammo_MML.json
index a97be2b8..0626501b 100644
--- a/BtDamageResolver/data/ammo/Ammo_MML.json
+++ b/BtDamageResolver/data/ammo/Ammo_MML.json
@@ -1,6 +1,7 @@
[
{
"Name": "MML3-SRM",
+ "AmmoProxyName": "SRM",
"Damage": {
"Short": 3
},
@@ -21,6 +22,7 @@
},
{
"Name": "MML3-SRM+Artemis",
+ "AmmoProxyName": "SRM",
"Damage": {
"Short": 3
},
@@ -44,6 +46,7 @@
},
{
"Name": "MML3-LRM",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 3
},
@@ -65,6 +68,7 @@
},
{
"Name": "MML3-LRM+Artemis",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 3
},
@@ -89,6 +93,7 @@
},
{
"Name": "MML3-LRM Indirect",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 3
},
@@ -111,6 +116,7 @@
},
{
"Name": "MML5-SRM",
+ "AmmoProxyName": "SRM",
"Damage": {
"Short": 5
},
@@ -131,6 +137,7 @@
},
{
"Name": "MML5-SRM+Artemis",
+ "AmmoProxyName": "SRM",
"Damage": {
"Short": 5
},
@@ -154,6 +161,7 @@
},
{
"Name": "MML5-LRM",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 5
},
@@ -175,6 +183,7 @@
},
{
"Name": "MML5-LRM+Artemis",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 5
},
@@ -199,6 +208,7 @@
},
{
"Name": "MML5-LRM Indirect",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 5
},
@@ -221,6 +231,7 @@
},
{
"Name": "MML7-SRM",
+ "AmmoProxyName": "SRM",
"Damage": {
"Short": 7
},
@@ -241,6 +252,7 @@
},
{
"Name": "MML7-SRM+Artemis",
+ "AmmoProxyName": "SRM",
"Damage": {
"Short": 7
},
@@ -264,6 +276,7 @@
},
{
"Name": "MML7-LRM",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 7
},
@@ -285,6 +298,7 @@
},
{
"Name": "MML7-LRM+Artemis",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 7
},
@@ -309,6 +323,7 @@
},
{
"Name": "MML7-LRM Indirect",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 7
},
@@ -331,6 +346,7 @@
},
{
"Name": "MML9-SRM",
+ "AmmoProxyName": "SRM",
"Damage": {
"Short": 9
},
@@ -351,6 +367,7 @@
},
{
"Name": "MML9-SRM+Artemis",
+ "AmmoProxyName": "SRM",
"Damage": {
"Short": 9
},
@@ -374,6 +391,7 @@
},
{
"Name": "MML9-LRM",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 9
},
@@ -395,6 +413,7 @@
},
{
"Name": "MML9-LRM+Artemis",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 9
},
@@ -419,6 +438,7 @@
},
{
"Name": "MML9-LRM Indirect",
+ "AmmoProxyName": "LRM",
"Damage": {
"Short": 9
},
@@ -439,4 +459,4 @@
"ClusterSize": 5,
"ClusterDamage": 1
}
-]
\ No newline at end of file
+]
diff --git a/BtDamageResolver/data/criticaldamagetable/CriticalDamageTable_MechQuad_Critical_Structure.json b/BtDamageResolver/data/criticaldamagetable/CriticalDamageTable_MechQuad_Critical_Structure.json
new file mode 100644
index 00000000..d9ac5ed9
--- /dev/null
+++ b/BtDamageResolver/data/criticaldamagetable/CriticalDamageTable_MechQuad_Critical_Structure.json
@@ -0,0 +1,18 @@
+{
+ "Type": "Critical",
+ "Location": "Structure",
+ "UnitType": "MechQuad",
+ "Mapping": {
+ "2": [ "None" ],
+ "3": [ "None" ],
+ "4": [ "None" ],
+ "5": [ "None" ],
+ "6": [ "None" ],
+ "7": [ "None" ],
+ "8": [ "Critical" ],
+ "9": [ "Critical" ],
+ "10": [ "Critical", "Critical" ],
+ "11": [ "Critical", "Critical" ],
+ "12": [ "Critical", "Critical", "Critical" ]
+ }
+}
diff --git a/BtDamageResolver/data/criticaldamagetable/CriticalDamageTable_MechTripod_Critical_Structure.json b/BtDamageResolver/data/criticaldamagetable/CriticalDamageTable_MechTripod_Critical_Structure.json
new file mode 100644
index 00000000..b6a140e6
--- /dev/null
+++ b/BtDamageResolver/data/criticaldamagetable/CriticalDamageTable_MechTripod_Critical_Structure.json
@@ -0,0 +1,18 @@
+{
+ "Type": "Critical",
+ "Location": "Structure",
+ "UnitType": "MechTripod",
+ "Mapping": {
+ "2": [ "None" ],
+ "3": [ "None" ],
+ "4": [ "None" ],
+ "5": [ "None" ],
+ "6": [ "None" ],
+ "7": [ "None" ],
+ "8": [ "Critical" ],
+ "9": [ "Critical" ],
+ "10": [ "Critical", "Critical" ],
+ "11": [ "Critical", "Critical" ],
+ "12": [ "Critical", "Critical", "Critical" ]
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Kick_Front.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Kick_Front.json
new file mode 100644
index 00000000..195de750
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Kick_Front.json
@@ -0,0 +1,13 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Kick",
+ "Direction": "Front",
+ "LocationMapping": {
+ "1": [ "FrontRightLeg" ],
+ "2": [ "FrontLeftLeg" ]
+ },
+ "CriticalDamageMapping": {
+ "1": "None",
+ "2": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Kick_Left.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Kick_Left.json
new file mode 100644
index 00000000..ae251e22
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Kick_Left.json
@@ -0,0 +1,13 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Kick",
+ "Direction": "Left",
+ "LocationMapping": {
+ "1": [ "FrontLeftLeg" ],
+ "2": [ "RearLeftLeg" ]
+ },
+ "CriticalDamageMapping": {
+ "1": "None",
+ "2": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Kick_Rear.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Kick_Rear.json
new file mode 100644
index 00000000..5492347f
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Kick_Rear.json
@@ -0,0 +1,13 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Kick",
+ "Direction": "Rear",
+ "LocationMapping": {
+ "1": [ "RearRightLeg" ],
+ "2": [ "RearLeftLeg" ]
+ },
+ "CriticalDamageMapping": {
+ "1": "None",
+ "2": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Kick_Right.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Kick_Right.json
new file mode 100644
index 00000000..c1576329
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Kick_Right.json
@@ -0,0 +1,13 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Kick",
+ "Direction": "Right",
+ "LocationMapping": {
+ "1": [ "FrontRightLeg" ],
+ "2": [ "RearRightLeg" ]
+ },
+ "CriticalDamageMapping": {
+ "1": "None",
+ "2": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Bottom.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Bottom.json
new file mode 100644
index 00000000..1e165e31
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Bottom.json
@@ -0,0 +1,31 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Normal",
+ "Direction": "Bottom",
+ "LocationMapping": {
+ "2": [ "CenterTorso", "RearCenterTorso" ],
+ "3": [ "FrontRightLeg" ],
+ "4": [ "FrontRightLeg" ],
+ "5": [ "RearRightLeg" ],
+ "6": [ "RightTorso", "RearRightTorso" ],
+ "7": [ "CenterTorso", "RearCenterTorso" ],
+ "8": [ "LeftTorso", "RearLeftTorso" ],
+ "9": [ "RearLeftLeg" ],
+ "10": [ "FrontLeftLeg" ],
+ "11": [ "FrontLeftLeg" ],
+ "12": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "2": "Critical",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None",
+ "7": "None",
+ "8": "None",
+ "9": "None",
+ "10": "None",
+ "11": "None",
+ "12": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Bottom_Floating.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Bottom_Floating.json
new file mode 100644
index 00000000..34f12b56
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Bottom_Floating.json
@@ -0,0 +1,32 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Normal",
+ "Direction": "Bottom",
+ "Rules": [ "FloatingCritical" ],
+ "LocationMapping": {
+ "2": [ "Reroll" ],
+ "3": [ "FrontRightLeg" ],
+ "4": [ "FrontRightLeg" ],
+ "5": [ "RearRightLeg" ],
+ "6": [ "RightTorso", "RearRightTorso" ],
+ "7": [ "CenterTorso", "RearCenterTorso" ],
+ "8": [ "LeftTorso", "RearLeftTorso" ],
+ "9": [ "RearLeftLeg" ],
+ "10": [ "FrontLeftLeg" ],
+ "11": [ "FrontLeftLeg" ],
+ "12": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "2": "Critical",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None",
+ "7": "None",
+ "8": "None",
+ "9": "None",
+ "10": "None",
+ "11": "None",
+ "12": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Front.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Front.json
new file mode 100644
index 00000000..9e23f524
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Front.json
@@ -0,0 +1,31 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Normal",
+ "Direction": "Front",
+ "LocationMapping": {
+ "2": [ "CenterTorso" ],
+ "3": [ "FrontRightLeg" ],
+ "4": [ "FrontRightLeg" ],
+ "5": [ "RearRightLeg" ],
+ "6": [ "RightTorso" ],
+ "7": [ "CenterTorso" ],
+ "8": [ "LeftTorso" ],
+ "9": [ "RearLeftLeg" ],
+ "10": [ "FrontLeftLeg" ],
+ "11": [ "FrontLeftLeg" ],
+ "12": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "2": "Critical",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None",
+ "7": "None",
+ "8": "None",
+ "9": "None",
+ "10": "None",
+ "11": "None",
+ "12": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Front_Floating.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Front_Floating.json
new file mode 100644
index 00000000..7973e4be
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Front_Floating.json
@@ -0,0 +1,32 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Normal",
+ "Direction": "Front",
+ "Rules": [ "FloatingCritical" ],
+ "LocationMapping": {
+ "2": [ "Reroll" ],
+ "3": [ "FrontRightLeg" ],
+ "4": [ "FrontRightLeg" ],
+ "5": [ "RearRightLeg" ],
+ "6": [ "RightTorso" ],
+ "7": [ "CenterTorso" ],
+ "8": [ "LeftTorso" ],
+ "9": [ "RearLeftLeg" ],
+ "10": [ "FrontLeftLeg" ],
+ "11": [ "FrontLeftLeg" ],
+ "12": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "2": "Critical",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None",
+ "7": "None",
+ "8": "None",
+ "9": "None",
+ "10": "None",
+ "11": "None",
+ "12": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Left.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Left.json
new file mode 100644
index 00000000..b3d1f3d3
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Left.json
@@ -0,0 +1,31 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Normal",
+ "Direction": "Left",
+ "LocationMapping": {
+ "2": [ "LeftTorso" ],
+ "3": [ "RearLeftLeg" ],
+ "4": [ "FrontLeftLeg" ],
+ "5": [ "FrontLeftLeg" ],
+ "6": [ "RearLeftLeg" ],
+ "7": [ "LeftTorso" ],
+ "8": [ "CenterTorso" ],
+ "9": [ "RightTorso" ],
+ "10": [ "FrontRightLeg" ],
+ "11": [ "RearRightLeg" ],
+ "12": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "2": "Critical",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None",
+ "7": "None",
+ "8": "None",
+ "9": "None",
+ "10": "None",
+ "11": "None",
+ "12": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Left_Floating.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Left_Floating.json
new file mode 100644
index 00000000..4c3f3184
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Left_Floating.json
@@ -0,0 +1,32 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Normal",
+ "Direction": "Left",
+ "Rules": [ "FloatingCritical" ],
+ "LocationMapping": {
+ "2": [ "Reroll" ],
+ "3": [ "RearLeftLeg" ],
+ "4": [ "FrontLeftLeg" ],
+ "5": [ "FrontLeftLeg" ],
+ "6": [ "RearLeftLeg" ],
+ "7": [ "LeftTorso" ],
+ "8": [ "CenterTorso" ],
+ "9": [ "RightTorso" ],
+ "10": [ "FrontRightLeg" ],
+ "11": [ "RearRightLeg" ],
+ "12": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "2": "Critical",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None",
+ "7": "None",
+ "8": "None",
+ "9": "None",
+ "10": "None",
+ "11": "None",
+ "12": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Rear.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Rear.json
new file mode 100644
index 00000000..4aa32b96
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Rear.json
@@ -0,0 +1,31 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Normal",
+ "Direction": "Rear",
+ "LocationMapping": {
+ "2": [ "RearCenterTorso" ],
+ "3": [ "RearRightLeg" ],
+ "4": [ "RearRightLeg" ],
+ "5": [ "FrontRightLeg" ],
+ "6": [ "RearRightTorso" ],
+ "7": [ "RearCenterTorso" ],
+ "8": [ "RearLeftTorso" ],
+ "9": [ "FrontLeftLeg" ],
+ "10": [ "RearLeftLeg" ],
+ "11": [ "RearLeftLeg" ],
+ "12": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "2": "Critical",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None",
+ "7": "None",
+ "8": "None",
+ "9": "None",
+ "10": "None",
+ "11": "None",
+ "12": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Rear_Floating.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Rear_Floating.json
new file mode 100644
index 00000000..55ace60b
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Rear_Floating.json
@@ -0,0 +1,32 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Normal",
+ "Direction": "Rear",
+ "Rules": [ "FloatingCritical" ],
+ "LocationMapping": {
+ "2": [ "Reroll" ],
+ "3": [ "RearRightLeg" ],
+ "4": [ "RearRightLeg" ],
+ "5": [ "FrontRightLeg" ],
+ "6": [ "RearRightTorso" ],
+ "7": [ "RearCenterTorso" ],
+ "8": [ "RearLeftTorso" ],
+ "9": [ "FrontLeftLeg" ],
+ "10": [ "RearLeftLeg" ],
+ "11": [ "RearLeftLeg" ],
+ "12": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "2": "Critical",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None",
+ "7": "None",
+ "8": "None",
+ "9": "None",
+ "10": "None",
+ "11": "None",
+ "12": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Right.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Right.json
new file mode 100644
index 00000000..a29be393
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Right.json
@@ -0,0 +1,31 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Normal",
+ "Direction": "Right",
+ "LocationMapping": {
+ "2": [ "RightTorso" ],
+ "3": [ "RearRightLeg" ],
+ "4": [ "FrontRightLeg" ],
+ "5": [ "FrontRightLeg" ],
+ "6": [ "RearRightLeg" ],
+ "7": [ "RightTorso" ],
+ "8": [ "CenterTorso" ],
+ "9": [ "LeftTorso" ],
+ "10": [ "FrontLeftLeg" ],
+ "11": [ "RearLeftLeg" ],
+ "12": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "2": "Critical",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None",
+ "7": "None",
+ "8": "None",
+ "9": "None",
+ "10": "None",
+ "11": "None",
+ "12": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Right_Floating.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Right_Floating.json
new file mode 100644
index 00000000..fd03ae61
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Right_Floating.json
@@ -0,0 +1,32 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Normal",
+ "Direction": "Right",
+ "Rules": [ "FloatingCritical" ],
+ "LocationMapping": {
+ "2": [ "Reroll" ],
+ "3": [ "RearRightLeg" ],
+ "4": [ "FrontRightLeg" ],
+ "5": [ "FrontRightLeg" ],
+ "6": [ "RearRightLeg" ],
+ "7": [ "RightTorso" ],
+ "8": [ "CenterTorso" ],
+ "9": [ "LeftTorso" ],
+ "10": [ "FrontLeftLeg" ],
+ "11": [ "RearLeftLeg" ],
+ "12": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "2": "Critical",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None",
+ "7": "None",
+ "8": "None",
+ "9": "None",
+ "10": "None",
+ "11": "None",
+ "12": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Top.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Top.json
new file mode 100644
index 00000000..d544569a
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Top.json
@@ -0,0 +1,31 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Normal",
+ "Direction": "Top",
+ "LocationMapping": {
+ "2": [ "CenterTorso", "RearCenterTorso" ],
+ "3": [ "FrontRightLeg" ],
+ "4": [ "FrontRightLeg" ],
+ "5": [ "RearRightLeg" ],
+ "6": [ "RightTorso", "RearRightTorso" ],
+ "7": [ "CenterTorso", "RearCenterTorso" ],
+ "8": [ "LeftTorso", "RearLeftTorso" ],
+ "9": [ "RearLeftLeg" ],
+ "10": [ "FrontLeftLeg" ],
+ "11": [ "FrontLeftLeg" ],
+ "12": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "2": "Critical",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None",
+ "7": "None",
+ "8": "None",
+ "9": "None",
+ "10": "None",
+ "11": "None",
+ "12": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Top_Floating.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Top_Floating.json
new file mode 100644
index 00000000..a76ca746
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Normal_Top_Floating.json
@@ -0,0 +1,32 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Normal",
+ "Direction": "Top",
+ "Rules": [ "FloatingCritical" ],
+ "LocationMapping": {
+ "2": [ "Reroll" ],
+ "3": [ "FrontRightLeg" ],
+ "4": [ "FrontRightLeg" ],
+ "5": [ "RearRightLeg" ],
+ "6": [ "RightTorso", "RearRightTorso" ],
+ "7": [ "CenterTorso", "RearCenterTorso" ],
+ "8": [ "LeftTorso", "RearLeftTorso" ],
+ "9": [ "RearLeftLeg" ],
+ "10": [ "FrontLeftLeg" ],
+ "11": [ "FrontLeftLeg" ],
+ "12": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "2": "Critical",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None",
+ "7": "None",
+ "8": "None",
+ "9": "None",
+ "10": "None",
+ "11": "None",
+ "12": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Punch_Front.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Punch_Front.json
new file mode 100644
index 00000000..3493563d
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Punch_Front.json
@@ -0,0 +1,21 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Punch",
+ "Direction": "Front",
+ "LocationMapping": {
+ "1": [ "FrontLeftLeg" ],
+ "2": [ "LeftTorso" ],
+ "3": [ "CenterTorso" ],
+ "4": [ "RightTorso" ],
+ "5": [ "FrontRightLeg" ],
+ "6": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "1": "None",
+ "2": "None",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Punch_Left.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Punch_Left.json
new file mode 100644
index 00000000..3f1ec0df
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Punch_Left.json
@@ -0,0 +1,21 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Punch",
+ "Direction": "Left",
+ "LocationMapping": {
+ "1": [ "LeftTorso" ],
+ "2": [ "LeftTorso" ],
+ "3": [ "CenterTorso" ],
+ "4": [ "FrontLeftLeg" ],
+ "5": [ "RearLeftLeg" ],
+ "6": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "1": "None",
+ "2": "None",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Punch_Rear.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Punch_Rear.json
new file mode 100644
index 00000000..9ec4f06d
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Punch_Rear.json
@@ -0,0 +1,21 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Punch",
+ "Direction": "Rear",
+ "LocationMapping": {
+ "1": [ "RearLeftLeg" ],
+ "2": [ "RearLeftTorso" ],
+ "3": [ "RearCenterTorso" ],
+ "4": [ "RearRightTorso" ],
+ "5": [ "RearRightLeg" ],
+ "6": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "1": "None",
+ "2": "None",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Punch_Right.json b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Punch_Right.json
new file mode 100644
index 00000000..a536290e
--- /dev/null
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_MechQuad_Punch_Right.json
@@ -0,0 +1,21 @@
+{
+ "Type": "MechQuad",
+ "AttackType": "Punch",
+ "Direction": "Right",
+ "LocationMapping": {
+ "1": [ "RightTorso" ],
+ "2": [ "RightTorso" ],
+ "3": [ "CenterTorso" ],
+ "4": [ "FrontRightLeg" ],
+ "5": [ "RearRightLeg" ],
+ "6": [ "Head" ]
+ },
+ "CriticalDamageMapping": {
+ "1": "None",
+ "2": "None",
+ "3": "None",
+ "4": "None",
+ "5": "None",
+ "6": "None"
+ }
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_Mech_Kick_Front.json b/BtDamageResolver/data/paperdoll/PaperDoll_Mech_Kick_Front.json
index 3ac35eac..97d6b9cb 100644
--- a/BtDamageResolver/data/paperdoll/PaperDoll_Mech_Kick_Front.json
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_Mech_Kick_Front.json
@@ -3,11 +3,11 @@
"AttackType": "Kick",
"Direction": "Front",
"LocationMapping": {
- "1": [ "LeftLeg" ],
- "2": [ "RightLeg" ]
+ "1": [ "RightLeg" ],
+ "2": [ "LeftLeg" ]
},
"CriticalDamageMapping": {
"1": "None",
"2": "None"
}
-}
\ No newline at end of file
+}
diff --git a/BtDamageResolver/data/paperdoll/PaperDoll_Mech_Kick_Rear.json b/BtDamageResolver/data/paperdoll/PaperDoll_Mech_Kick_Rear.json
index f3149363..0d8c524e 100644
--- a/BtDamageResolver/data/paperdoll/PaperDoll_Mech_Kick_Rear.json
+++ b/BtDamageResolver/data/paperdoll/PaperDoll_Mech_Kick_Rear.json
@@ -10,4 +10,4 @@
"1": "None",
"2": "None"
}
-}
\ No newline at end of file
+}
diff --git a/BtDamageResolver/data/paperdoll/images/Mech.cdr b/BtDamageResolver/data/paperdoll/images/Mech.cdr
index 02b5e1f4..aac65008 100644
Binary files a/BtDamageResolver/data/paperdoll/images/Mech.cdr and b/BtDamageResolver/data/paperdoll/images/Mech.cdr differ
diff --git a/BtDamageResolver/data/unit/Unit_AerospaceFighter_Samurai_OMIL-2.json b/BtDamageResolver/data/unit/Unit_AerospaceFighter_Samurai_OMIL-2.json
index 13ad8fe2..815d07be 100644
--- a/BtDamageResolver/data/unit/Unit_AerospaceFighter_Samurai_OMIL-2.json
+++ b/BtDamageResolver/data/unit/Unit_AerospaceFighter_Samurai_OMIL-2.json
@@ -8,6 +8,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_BattleArmor_BA_Knight_Serpent.json b/BtDamageResolver/data/unit/Unit_BattleArmor_BA_Knight_Serpent.json
index 1fc5df6f..39880ea6 100644
--- a/BtDamageResolver/data/unit/Unit_BattleArmor_BA_Knight_Serpent.json
+++ b/BtDamageResolver/data/unit/Unit_BattleArmor_BA_Knight_Serpent.json
@@ -8,6 +8,7 @@
"Troopers": 6,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Archangel_C-ANG-HG.json b/BtDamageResolver/data/unit/Unit_Mech_Archangel_C-ANG-HG.json
index 456b1a73..209ea58d 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Archangel_C-ANG-HG.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Archangel_C-ANG-HG.json
@@ -14,6 +14,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Archer_ARC-2R.json b/BtDamageResolver/data/unit/Unit_Mech_Archer_ARC-2R.json
new file mode 100644
index 00000000..632f3027
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Archer_ARC-2R.json
@@ -0,0 +1,54 @@
+{
+ "Features": [
+ "BattleFists",
+ "CommandUnit",
+ "Stable"
+ ],
+ "JumpJets": 0,
+ "Sinks": 10,
+ "Speed": 4,
+ "Type": "Mech",
+ "Tonnage": 70,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "LRM-20"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "LRM-20"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Archer ARC-2R"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Archer_ARC-5S.json b/BtDamageResolver/data/unit/Unit_Mech_Archer_ARC-5S.json
index f3bd5efd..572a178e 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Archer_ARC-5S.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Archer_ARC-5S.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Atlas_AS7-S.json b/BtDamageResolver/data/unit/Unit_Mech_Atlas_AS7-S.json
index b2e147a7..6276b93c 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Atlas_AS7-S.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Atlas_AS7-S.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Awesome_AWS-8Q.json b/BtDamageResolver/data/unit/Unit_Mech_Awesome_AWS-8Q.json
index 9c7c785d..a483826c 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Awesome_AWS-8Q.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Awesome_AWS-8Q.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Awesome_AWS-9M.json b/BtDamageResolver/data/unit/Unit_Mech_Awesome_AWS-9M.json
index c8c7a810..9e0b29c4 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Awesome_AWS-9M.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Awesome_AWS-9M.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Banshee_BNC-SB1.json b/BtDamageResolver/data/unit/Unit_Mech_Banshee_BNC-SB1.json
index 07d740d8..15a15a5c 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Banshee_BNC-SB1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Banshee_BNC-SB1.json
@@ -8,6 +8,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Battlemaster_BLR-3M-DC.json b/BtDamageResolver/data/unit/Unit_Mech_Battlemaster_BLR-3M-DC.json
new file mode 100644
index 00000000..01326a9e
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Battlemaster_BLR-3M-DC.json
@@ -0,0 +1,65 @@
+{
+ "Features": [
+ "CommandUnit",
+ "ImprovedCommunications",
+ "JettisonableWeapon",
+ "WeakHeadArmor"
+ ],
+ "JumpJets": 0,
+ "Sinks": 34,
+ "Speed": 4,
+ "Type": "Mech",
+ "Tonnage": 85,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "ER PPC"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "SRM-6"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Machine Gun"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Melee Kick"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Battlemaster BLR-3M-DC"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Battlemaster_BLR-3S.json b/BtDamageResolver/data/unit/Unit_Mech_Battlemaster_BLR-3S.json
index 3a06713a..56f841da 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Battlemaster_BLR-3S.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Battlemaster_BLR-3S.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Black_Knight_BL-TDHI-1-KNT.json b/BtDamageResolver/data/unit/Unit_Mech_Black_Knight_BL-TDHI-1-KNT.json
index a9ef8fe4..a7eccb42 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Black_Knight_BL-TDHI-1-KNT.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Black_Knight_BL-TDHI-1-KNT.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Blackjack_BJ-3.json b/BtDamageResolver/data/unit/Unit_Mech_Blackjack_BJ-3.json
index 047ce488..69c4f040 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Blackjack_BJ-3.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Blackjack_BJ-3.json
@@ -8,6 +8,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Catapult_CPLT-BHI1.json b/BtDamageResolver/data/unit/Unit_Mech_Catapult_CPLT-BHI1.json
index c5d796bf..0edffd52 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Catapult_CPLT-BHI1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Catapult_CPLT-BHI1.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Artemis",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Catapult_CPLT-C4C.json b/BtDamageResolver/data/unit/Unit_Mech_Catapult_CPLT-C4C.json
index be90a84d..687a9200 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Catapult_CPLT-C4C.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Catapult_CPLT-C4C.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Cauldron_Born_D.json b/BtDamageResolver/data/unit/Unit_Mech_Cauldron_Born_D.json
new file mode 100644
index 00000000..f124683c
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Cauldron_Born_D.json
@@ -0,0 +1,47 @@
+{
+ "Features": [
+ "NarrowLowProfile"
+ ],
+ "JumpJets": 0,
+ "Sinks": 26,
+ "Speed": 5,
+ "Type": "Mech",
+ "Tonnage": 65,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Rapid",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Ultra AC/10"
+ },
+ {
+ "Ammo": "Rapid",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Ultra AC/10"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan ER Large Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan ER Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan ER Medium Laser"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Cauldron Born D"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Centurion_CN9-D.json b/BtDamageResolver/data/unit/Unit_Mech_Centurion_CN9-D.json
index b318b0ec..2d733734 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Centurion_CN9-D.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Centurion_CN9-D.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Cicada_CDA-3M.json b/BtDamageResolver/data/unit/Unit_Mech_Cicada_CDA-3M.json
index 7f765285..217a9465 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Cicada_CDA-3M.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Cicada_CDA-3M.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Rapid",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Commando_CMD-2D.json b/BtDamageResolver/data/unit/Unit_Mech_Commando_CMD-2D.json
index f23ec733..d2039cfe 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Commando_CMD-2D.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Commando_CMD-2D.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Commando_COM-1B.json b/BtDamageResolver/data/unit/Unit_Mech_Commando_COM-1B.json
index cbda649d..b40515e6 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Commando_COM-1B.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Commando_COM-1B.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Commando_COM-5S.json b/BtDamageResolver/data/unit/Unit_Mech_Commando_COM-5S.json
index 9c497332..862174df 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Commando_COM-5S.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Commando_COM-5S.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Crab_CRB-BHI1.json b/BtDamageResolver/data/unit/Unit_Mech_Crab_CRB-BHI1.json
index 3c98a1a6..796684b6 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Crab_CRB-BHI1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Crab_CRB-BHI1.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Crusader_CDR-5S.json b/BtDamageResolver/data/unit/Unit_Mech_Crusader_CDR-5S.json
index 3640d24d..dd94f068 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Crusader_CDR-5S.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Crusader_CDR-5S.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Crusader_CRD-4K.json b/BtDamageResolver/data/unit/Unit_Mech_Crusader_CRD-4K.json
new file mode 100644
index 00000000..26b8152e
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Crusader_CRD-4K.json
@@ -0,0 +1,56 @@
+{
+ "Features": [
+ "EasyToMaintain",
+ "Rugged",
+ "StabilizedWeapons"
+ ],
+ "JumpJets": 0,
+ "Sinks": 14,
+ "Speed": 4,
+ "Type": "Mech",
+ "Tonnage": 65,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "LRM-10"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "LRM-10"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Pulse Laser"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "SRM-6"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "SRM-6"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Crusader CRD-4K"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Crusader_CRD-FIN1.json b/BtDamageResolver/data/unit/Unit_Mech_Crusader_CRD-FIN1.json
index 09bf617e..b9f03274 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Crusader_CRD-FIN1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Crusader_CRD-FIN1.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Artemis",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Emperor_EMP-CP1.json b/BtDamageResolver/data/unit/Unit_Mech_Emperor_EMP-CP1.json
index 5855c217..b7e4a817 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Emperor_EMP-CP1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Emperor_EMP-CP1.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Slug",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Enforcer_ENF-5D.json b/BtDamageResolver/data/unit/Unit_Mech_Enforcer_ENF-5D.json
index 0f0c3144..af46941c 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Enforcer_ENF-5D.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Enforcer_ENF-5D.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Slug",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Enforcer_ENF-CP1.json b/BtDamageResolver/data/unit/Unit_Mech_Enforcer_ENF-CP1.json
index a6cc9fc1..463133f6 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Enforcer_ENF-CP1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Enforcer_ENF-CP1.json
@@ -8,6 +8,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Slug",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Executioner_EXE-CEO1.json b/BtDamageResolver/data/unit/Unit_Mech_Executioner_EXE-CEO1.json
index de83a930..8c42d5c1 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Executioner_EXE-CEO1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Executioner_EXE-CEO1.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Fenris_D.json b/BtDamageResolver/data/unit/Unit_Mech_Fenris_D.json
index 3ce6a3fe..f96feb87 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Fenris_D.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Fenris_D.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Fenris_Prime.json b/BtDamageResolver/data/unit/Unit_Mech_Fenris_Prime.json
index 7892a294..21c87e9b 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Fenris_Prime.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Fenris_Prime.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Gargoyle_A.json b/BtDamageResolver/data/unit/Unit_Mech_Gargoyle_A.json
index cd04e3d1..edabbda7 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Gargoyle_A.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Gargoyle_A.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Grasshopper_GHR-5H.json b/BtDamageResolver/data/unit/Unit_Mech_Grasshopper_GHR-5H.json
index 0f0041bc..4bd2df4f 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Grasshopper_GHR-5H.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Grasshopper_GHR-5H.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Griffin_GRF-1DS.json b/BtDamageResolver/data/unit/Unit_Mech_Griffin_GRF-1DS.json
index 6d6450f4..8b5bc947 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Griffin_GRF-1DS.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Griffin_GRF-1DS.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Hatchetman_HCT-3F.json b/BtDamageResolver/data/unit/Unit_Mech_Hatchetman_HCT-3F.json
new file mode 100644
index 00000000..5afe15aa
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Hatchetman_HCT-3F.json
@@ -0,0 +1,41 @@
+{
+ "Features": [
+ "TargetingAntiAir"
+ ],
+ "JumpJets": 0,
+ "Sinks": 11,
+ "Speed": 4,
+ "Type": "Mech",
+ "Tonnage": 45,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "AC/10"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Melee Hatchet"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Hatchetman HCT-3F"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Hellhound.json b/BtDamageResolver/data/unit/Unit_Mech_Hellhound.json
index 25159145..dda0a049 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Hellhound.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Hellhound.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Hellspawn_HSN-CP1.json b/BtDamageResolver/data/unit/Unit_Mech_Hellspawn_HSN-CP1.json
index c78ff21e..a5f774dd 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Hellspawn_HSN-CP1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Hellspawn_HSN-CP1.json
@@ -8,6 +8,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Artemis",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Huncchback_HBK-4G.json b/BtDamageResolver/data/unit/Unit_Mech_Huncchback_HBK-4G.json
index 188e1e4d..f6c81ac5 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Huncchback_HBK-4G.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Huncchback_HBK-4G.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Hunchback_2C.json b/BtDamageResolver/data/unit/Unit_Mech_Hunchback_2C.json
index 0d938a09..e4243e7b 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Hunchback_2C.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Hunchback_2C.json
@@ -8,6 +8,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Single",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Hunchback_HBK-4G.json b/BtDamageResolver/data/unit/Unit_Mech_Hunchback_HBK-4G.json
index 273ff664..4f11ada8 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Hunchback_HBK-4G.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Hunchback_HBK-4G.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Huntsman_B.json b/BtDamageResolver/data/unit/Unit_Mech_Huntsman_B.json
new file mode 100644
index 00000000..d1aa2a97
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Huntsman_B.json
@@ -0,0 +1,52 @@
+{
+ "Features": [
+ "Stable"
+ ],
+ "JumpJets": 5,
+ "Sinks": 20,
+ "Speed": 5,
+ "Type": "Mech",
+ "Tonnage": 50,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan ER PPC"
+ },
+ {
+ "Ammo": "Slug",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan LBX AC/10"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Medium Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Medium Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Machine Gun"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Machine Gun"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Huntsman B"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Incubus_2.json b/BtDamageResolver/data/unit/Unit_Mech_Incubus_2.json
index ac288529..88adcaa6 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Incubus_2.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Incubus_2.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_JagerMech_JM6-DD.json b/BtDamageResolver/data/unit/Unit_Mech_JagerMech_JM6-DD.json
index 4e3336b7..55c7e8a5 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_JagerMech_JM6-DD.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_JagerMech_JM6-DD.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Rapid",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Javelin_JVN-10P.json b/BtDamageResolver/data/unit/Unit_Mech_Javelin_JVN-10P.json
index 4b6a901c..c0159213 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Javelin_JVN-10P.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Javelin_JVN-10P.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Jenner_2C.json b/BtDamageResolver/data/unit/Unit_Mech_Jenner_2C.json
index 09d08e02..23b2b164 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Jenner_2C.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Jenner_2C.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Jenner_JR7-D.json b/BtDamageResolver/data/unit/Unit_Mech_Jenner_JR7-D.json
index 9f0e037e..863828df 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Jenner_JR7-D.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Jenner_JR7-D.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Jiira_IIC.json b/BtDamageResolver/data/unit/Unit_Mech_Jiira_IIC.json
index adad06e9..3dccbd06 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Jiira_IIC.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Jiira_IIC.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Single",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Kit_Fox_A.json b/BtDamageResolver/data/unit/Unit_Mech_Kit_Fox_A.json
index 39c2b2c4..1bdde792 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Kit_Fox_A.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Kit_Fox_A.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Kit_Fox_Prime.json b/BtDamageResolver/data/unit/Unit_Mech_Kit_Fox_Prime.json
index b1d19e01..e3989cf3 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Kit_Fox_Prime.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Kit_Fox_Prime.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Slug",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Kraken_2.json b/BtDamageResolver/data/unit/Unit_Mech_Kraken_2.json
index 49680577..0534ca52 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Kraken_2.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Kraken_2.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Single",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Locust_LCT-3S.json b/BtDamageResolver/data/unit/Unit_Mech_Locust_LCT-3S.json
index b0114cfe..0bf471cc 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Locust_LCT-3S.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Locust_LCT-3S.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Loki_A.json b/BtDamageResolver/data/unit/Unit_Mech_Loki_A.json
index c01cb087..4a8a52cf 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Loki_A.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Loki_A.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Loki_Prime.json b/BtDamageResolver/data/unit/Unit_Mech_Loki_Prime.json
index a0606e10..d76e36fc 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Loki_Prime.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Loki_Prime.json
@@ -14,6 +14,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Marauder_2C.json b/BtDamageResolver/data/unit/Unit_Mech_Marauder_2C.json
index 83e54afb..9c7f3886 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Marauder_2C.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Marauder_2C.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Marauder_MAD-5D-DC.json b/BtDamageResolver/data/unit/Unit_Mech_Marauder_MAD-5D-DC.json
new file mode 100644
index 00000000..c31b2864
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Marauder_MAD-5D-DC.json
@@ -0,0 +1,57 @@
+{
+ "Features": [
+ "CommandUnit",
+ "DirectionalTorsoMount",
+ "ExposedWeaponLinkage",
+ "HyperExtendingActuators",
+ "ImprovedCommunications",
+ "NarrowLowProfile"
+ ],
+ "JumpJets": 4,
+ "Sinks": 30,
+ "Speed": 4,
+ "Type": "Mech",
+ "Tonnage": 75,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "ER PPC"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "ER PPC"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Large Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Small Pulse Laser"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Marauder MAD-5D-DC"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Marauder_MAD-5D.json b/BtDamageResolver/data/unit/Unit_Mech_Marauder_MAD-5D.json
index 6996807d..4ebf3f43 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Marauder_MAD-5D.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Marauder_MAD-5D.json
@@ -14,6 +14,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Marauder_MAD-BN2A.json b/BtDamageResolver/data/unit/Unit_Mech_Marauder_MAD-BN2A.json
index 5297641b..432dbfe4 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Marauder_MAD-BN2A.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Marauder_MAD-BN2A.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Cluster",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Mauler_MAL-TDHI2.json b/BtDamageResolver/data/unit/Unit_Mech_Mauler_MAL-TDHI2.json
index a0f18d32..c01f93ff 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Mauler_MAL-TDHI2.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Mauler_MAL-TDHI2.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Mist_Lynx_B.json b/BtDamageResolver/data/unit/Unit_Mech_Mist_Lynx_B.json
index 7093ac67..77ff5b3a 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Mist_Lynx_B.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Mist_Lynx_B.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Nova_A.json b/BtDamageResolver/data/unit/Unit_Mech_Nova_A.json
index 10f8cfb2..e7c76164 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Nova_A.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Nova_A.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Nova_Prime.json b/BtDamageResolver/data/unit/Unit_Mech_Nova_Prime.json
index 51c0d335..4a17fbd5 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Nova_Prime.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Nova_Prime.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Orion_ON1-K.json b/BtDamageResolver/data/unit/Unit_Mech_Orion_ON1-K.json
new file mode 100644
index 00000000..a303c481
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Orion_ON1-K.json
@@ -0,0 +1,60 @@
+{
+ "Features": [
+ "EasyToMaintain",
+ "Rugged",
+ "TargetingAntiAir"
+ ],
+ "JumpJets": 4,
+ "Sinks": 10,
+ "Speed": 4,
+ "Type": "Mech",
+ "Tonnage": 75,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "AC/10"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "LRM-15"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "SRM-4"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Melee Punch"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Melee Kick"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Orion ON1-K"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Ostroc_OSR-2D.json b/BtDamageResolver/data/unit/Unit_Mech_Ostroc_OSR-2D.json
index 60126bbc..7246b8bb 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Ostroc_OSR-2D.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Ostroc_OSR-2D.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Ostscout_OTT-7J.json b/BtDamageResolver/data/unit/Unit_Mech_Ostscout_OTT-7J.json
index 84084328..544d45b1 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Ostscout_OTT-7J.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Ostscout_OTT-7J.json
@@ -13,6 +13,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Ostsol_OTL-4D.json b/BtDamageResolver/data/unit/Unit_Mech_Ostsol_OTL-4D.json
index 715a95a6..1d412e3e 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Ostsol_OTL-4D.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Ostsol_OTL-4D.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Phoenix_Hawk_PXH-3S.json b/BtDamageResolver/data/unit/Unit_Mech_Phoenix_Hawk_PXH-3S.json
index 90c583d9..3a773b8d 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Phoenix_Hawk_PXH-3S.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Phoenix_Hawk_PXH-3S.json
@@ -14,6 +14,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Phoenix_hawk_PXH-3K.json b/BtDamageResolver/data/unit/Unit_Mech_Phoenix_hawk_PXH-3K.json
new file mode 100644
index 00000000..7457b214
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Phoenix_hawk_PXH-3K.json
@@ -0,0 +1,44 @@
+{
+ "Features": [
+ "Ams",
+ "CommandUnit",
+ "ImprovedCommunications",
+ "JettisonableWeapon",
+ "Masc"
+ ],
+ "JumpJets": 6,
+ "Sinks": 24,
+ "Speed": 6,
+ "Type": "Mech",
+ "Tonnage": 45,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "ER Large Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "ER Large Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Pulse Laser"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Phoenix hawk PXH-3K"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Preta_PRT-BN1.json b/BtDamageResolver/data/unit/Unit_Mech_Preta_PRT-BN1.json
old mode 100755
new mode 100644
index 84e8b2f1..972e1565
--- a/BtDamageResolver/data/unit/Unit_Mech_Preta_PRT-BN1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Preta_PRT-BN1.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Preta_PRT-BN2.json b/BtDamageResolver/data/unit/Unit_Mech_Preta_PRT-BN2.json
old mode 100755
new mode 100644
index 136990c8..6f0fc7e3
--- a/BtDamageResolver/data/unit/Unit_Mech_Preta_PRT-BN2.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Preta_PRT-BN2.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Puma_C.json b/BtDamageResolver/data/unit/Unit_Mech_Puma_C.json
index ecde4bf7..b7a14d12 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Puma_C.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Puma_C.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Rifleman_2C.json b/BtDamageResolver/data/unit/Unit_Mech_Rifleman_2C.json
index a138043b..1557a111 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Rifleman_2C.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Rifleman_2C.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Rifleman_RFL-5D.json b/BtDamageResolver/data/unit/Unit_Mech_Rifleman_RFL-5D.json
index 70dbd2bf..18d7ddbc 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Rifleman_RFL-5D.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Rifleman_RFL-5D.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Shadow_Hawk_SHD-2K.json b/BtDamageResolver/data/unit/Unit_Mech_Shadow_Hawk_SHD-2K.json
new file mode 100644
index 00000000..3e9f304b
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Shadow_Hawk_SHD-2K.json
@@ -0,0 +1,33 @@
+{
+ "Features": [
+ "BattleFists",
+ "Rugged"
+ ],
+ "JumpJets": 5,
+ "Sinks": 17,
+ "Speed": 5,
+ "Type": "Mech",
+ "Tonnage": 55,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "PPC"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "LRM-15"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Shadow Hawk SHD-2K"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Shadow_Hawk_SHD-5M.json b/BtDamageResolver/data/unit/Unit_Mech_Shadow_Hawk_SHD-5M.json
index 1546c14b..7fd8b655 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Shadow_Hawk_SHD-5M.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Shadow_Hawk_SHD-5M.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Single",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Shadow_Hawk_SHD-BN1.json b/BtDamageResolver/data/unit/Unit_Mech_Shadow_Hawk_SHD-BN1.json
index b6cb4602..5dff9ff9 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Shadow_Hawk_SHD-BN1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Shadow_Hawk_SHD-BN1.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Stalker_STK-5S.json b/BtDamageResolver/data/unit/Unit_Mech_Stalker_STK-5S.json
index 6136c5f0..de0135ab 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Stalker_STK-5S.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Stalker_STK-5S.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Stalker_STK-MK2.json b/BtDamageResolver/data/unit/Unit_Mech_Stalker_STK-MK2.json
index a9a29481..ec5c754d 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Stalker_STK-MK2.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Stalker_STK-MK2.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Artemis",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Stinger_STG-3G.json b/BtDamageResolver/data/unit/Unit_Mech_Stinger_STG-3G.json
index e2d3d4fd..6583e923 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Stinger_STG-3G.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Stinger_STG-3G.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Stinger_STG-5M.json b/BtDamageResolver/data/unit/Unit_Mech_Stinger_STG-5M.json
index ffdc8910..89fb741d 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Stinger_STG-5M.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Stinger_STG-5M.json
@@ -13,6 +13,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Stormcrow_A.json b/BtDamageResolver/data/unit/Unit_Mech_Stormcrow_A.json
index ac2b9117..83a01953 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Stormcrow_A.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Stormcrow_A.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Stormcrow_C.json b/BtDamageResolver/data/unit/Unit_Mech_Stormcrow_C.json
new file mode 100644
index 00000000..2787516f
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Stormcrow_C.json
@@ -0,0 +1,41 @@
+{
+ "Features": [
+ "Stable"
+ ],
+ "JumpJets": 0,
+ "Sinks": 20,
+ "Speed": 6,
+ "Type": "Mech",
+ "Tonnage": 55,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Slug",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan LBX AC/10"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Large Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Medium Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Medium Pulse Laser"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Stormcrow C"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Stormcrow_Prime.json b/BtDamageResolver/data/unit/Unit_Mech_Stormcrow_Prime.json
new file mode 100644
index 00000000..9569aa71
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Stormcrow_Prime.json
@@ -0,0 +1,45 @@
+{
+ "Features": [
+ "Stable"
+ ],
+ "JumpJets": 0,
+ "Sinks": 44,
+ "Speed": 6,
+ "Type": "Mech",
+ "Tonnage": 55,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan ER Large Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan ER Large Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan ER Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan ER Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan ER Medium Laser"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Stormcrow Prime"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Thor_A.json b/BtDamageResolver/data/unit/Unit_Mech_Thor_A.json
index e5232133..e54385dd 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Thor_A.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Thor_A.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Thor_Prime.json b/BtDamageResolver/data/unit/Unit_Mech_Thor_Prime.json
index b549c998..1fe5a997 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Thor_Prime.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Thor_Prime.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Thug_THG-ROT1.json b/BtDamageResolver/data/unit/Unit_Mech_Thug_THG-ROT1.json
index 8786160b..61e067b9 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Thug_THG-ROT1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Thug_THG-ROT1.json
@@ -8,6 +8,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Thunderbolt_TDR-5Sh.json b/BtDamageResolver/data/unit/Unit_Mech_Thunderbolt_TDR-5Sh.json
new file mode 100644
index 00000000..155e10a0
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Thunderbolt_TDR-5Sh.json
@@ -0,0 +1,63 @@
+{
+ "Features": [
+ "MultiTarget",
+ "Rugged"
+ ],
+ "JumpJets": 0,
+ "Sinks": 15,
+ "Speed": 4,
+ "Type": "Mech",
+ "Tonnage": 65,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "LRM-15"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Machine Gun"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Machine Gun"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "SRM-2"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Melee Hatchet"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Thunderbolt TDR-5Sh"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Thunderbolt_TDR-7M.json b/BtDamageResolver/data/unit/Unit_Mech_Thunderbolt_TDR-7M.json
index c866d918..7353c288 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Thunderbolt_TDR-7M.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Thunderbolt_TDR-7M.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Thunderbolt_TDR-9S.json b/BtDamageResolver/data/unit/Unit_Mech_Thunderbolt_TDR-9S.json
index 0efac7aa..159efee5 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Thunderbolt_TDR-9S.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Thunderbolt_TDR-9S.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Timber_Wolf_A.json b/BtDamageResolver/data/unit/Unit_Mech_Timber_Wolf_A.json
new file mode 100644
index 00000000..52f41706
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Timber_Wolf_A.json
@@ -0,0 +1,58 @@
+{
+ "Features": [
+ "TargetingMediumRange",
+ "WeakHeadArmor"
+ ],
+ "JumpJets": 0,
+ "Sinks": 40,
+ "Speed": 5,
+ "Type": "Mech",
+ "Tonnage": 75,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan ER PPC"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan ER PPC"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Medium Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Medium Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Medium Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Streak SRM-6"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "ER Small Laser"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Timber Wolf A"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Timber_Wolf_B.json b/BtDamageResolver/data/unit/Unit_Mech_Timber_Wolf_B.json
new file mode 100644
index 00000000..5282b37f
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Timber_Wolf_B.json
@@ -0,0 +1,48 @@
+{
+ "Features": [
+ "TargetingMediumRange",
+ "WeakHeadArmor"
+ ],
+ "JumpJets": 0,
+ "Sinks": 30,
+ "Speed": 5,
+ "Type": "Mech",
+ "Tonnage": 75,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Gauss Rifle"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Large Pulse Laser"
+ },
+ {
+ "Ammo": "Artemis",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan LRM-10"
+ },
+ {
+ "Ammo": "Artemis",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "SRM-4"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Clan Small Pulse Laser"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Timber Wolf B"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Timber_Wolf_D.json b/BtDamageResolver/data/unit/Unit_Mech_Timber_Wolf_D.json
index 42fc8938..a8779990 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Timber_Wolf_D.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Timber_Wolf_D.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Trebuchet_TBT-7M.json b/BtDamageResolver/data/unit/Unit_Mech_Trebuchet_TBT-7M.json
index 1cd143ae..5fc2bd76 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Trebuchet_TBT-7M.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Trebuchet_TBT-7M.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Uziel__UZL-MK1.json b/BtDamageResolver/data/unit/Unit_Mech_Uziel_UZL-MK1.json
similarity index 93%
rename from BtDamageResolver/data/unit/Unit_Mech_Uziel__UZL-MK1.json
rename to BtDamageResolver/data/unit/Unit_Mech_Uziel_UZL-MK1.json
index 2db3aa8a..b6dca2cd 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Uziel__UZL-MK1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Uziel_UZL-MK1.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
@@ -34,5 +35,5 @@
"Name": "Default"
}
],
- "Name": "Uziel UZL-MK1"
+ "Name": "Uziel UZL-MK1"
}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Valkyrie_VLK-QD.json b/BtDamageResolver/data/unit/Unit_Mech_Valkyrie_VLK-QD.json
index 5c36a49c..27718240 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Valkyrie_VLK-QD.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Valkyrie_VLK-QD.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Victor_VTR-9B.json b/BtDamageResolver/data/unit/Unit_Mech_Victor_VTR-9B.json
index 7d6eaaa7..0278a875 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Victor_VTR-9B.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Victor_VTR-9B.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Victor_VTR-SB2.json b/BtDamageResolver/data/unit/Unit_Mech_Victor_VTR-SB2.json
index 2cfd1dd3..db89088e 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Victor_VTR-SB2.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Victor_VTR-SB2.json
@@ -8,6 +8,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Single",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Vulture_A.json b/BtDamageResolver/data/unit/Unit_Mech_Vulture_A.json
index cae7c134..e3941fb2 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Vulture_A.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Vulture_A.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Vulture_B.json b/BtDamageResolver/data/unit/Unit_Mech_Vulture_B.json
index 8b2879f6..7f9534e7 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Vulture_B.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Vulture_B.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Vulture_MDD-BN1.json b/BtDamageResolver/data/unit/Unit_Mech_Vulture_MDD-BN1.json
index a46218c1..764c71b0 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Vulture_MDD-BN1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Vulture_MDD-BN1.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Vulture_Prime.json b/BtDamageResolver/data/unit/Unit_Mech_Vulture_Prime.json
index 03829cba..b4c969a7 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Vulture_Prime.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Vulture_Prime.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Warhammer_2C.json b/BtDamageResolver/data/unit/Unit_Mech_Warhammer_2C.json
index 581fb4a2..cccc1684 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Warhammer_2C.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Warhammer_2C.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Warhammer_WHM-7M-DC.json b/BtDamageResolver/data/unit/Unit_Mech_Warhammer_WHM-7M-DC.json
new file mode 100644
index 00000000..dce84d44
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Warhammer_WHM-7M-DC.json
@@ -0,0 +1,51 @@
+{
+ "Features": [
+ "ImprovedCommunications",
+ "Rugged",
+ "Searchlight",
+ "Stable"
+ ],
+ "JumpJets": 0,
+ "Sinks": 36,
+ "Speed": 4,
+ "Type": "Mech",
+ "Tonnage": 70,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "ER PPC"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "ER PPC"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "SRM-6"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Machine Gun"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Warhammer WHM-7M-DC"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Warhammer_WHM-7S.json b/BtDamageResolver/data/unit/Unit_Mech_Warhammer_WHM-7S.json
index a2c2d1d2..5c699953 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Warhammer_WHM-7S.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Warhammer_WHM-7S.json
@@ -12,6 +12,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Warhawk_B.json b/BtDamageResolver/data/unit/Unit_Mech_Warhawk_B.json
index a161fa03..30feb4b3 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Warhawk_B.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Warhawk_B.json
@@ -11,6 +11,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Whitworth_WTH-1.json b/BtDamageResolver/data/unit/Unit_Mech_Whitworth_WTH-1.json
index eed246eb..315760c8 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Whitworth_WTH-1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Whitworth_WTH-1.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Wolf_Trap_(Tora)_WFT-1.json b/BtDamageResolver/data/unit/Unit_Mech_Wolf_Trap_(Tora)_WFT-1.json
new file mode 100644
index 00000000..e3a79e2b
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Wolf_Trap_(Tora)_WFT-1.json
@@ -0,0 +1,40 @@
+{
+ "Features": [],
+ "JumpJets": 0,
+ "Sinks": 10,
+ "Speed": 6,
+ "Type": "Mech",
+ "Tonnage": 45,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Ammo": "Slug",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "LBX AC/10"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "LRM-10"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Laser"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Wolf Trap (Tora) WFT-1"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-7D.json b/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-7D.json
index e0e1a7f0..39a0e1a3 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-7D.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-7D.json
@@ -14,6 +14,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Single",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-7K.json b/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-7K.json
new file mode 100644
index 00000000..bd215d2b
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-7K.json
@@ -0,0 +1,51 @@
+{
+ "Features": [
+ "CommandUnit",
+ "ExtendedTorsoTwist",
+ "ImprovedCommunications",
+ "JettisonableWeapon",
+ "ProtectedActuators"
+ ],
+ "JumpJets": 5,
+ "Sinks": 26,
+ "Speed": 5,
+ "Type": "Mech",
+ "Tonnage": 55,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Large Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Small Pulse Laser"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "SRM-6"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "SRM-6"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Wolverine WVR-7K"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-7Kh.json b/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-7Kh.json
new file mode 100644
index 00000000..69a62320
--- /dev/null
+++ b/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-7Kh.json
@@ -0,0 +1,50 @@
+{
+ "Features": [
+ "CommandUnit",
+ "ExtendedTorsoTwist",
+ "ImprovedCommunications",
+ "JettisonableWeapon",
+ "ProtectedActuators"
+ ],
+ "JumpJets": 5,
+ "Sinks": 26,
+ "Speed": 5,
+ "Type": "Mech",
+ "Tonnage": 55,
+ "Troopers": 1,
+ "WeaponBays": [
+ {
+ "Arc": "Default",
+ "Weapons": [
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Large Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Medium Pulse Laser"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Small Pulse Laser"
+ },
+ {
+ "Ammo": "Normal",
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "SRM-6"
+ },
+ {
+ "Amount": 1,
+ "Modifier": 0,
+ "WeaponName": "Melee Hatchet"
+ }
+ ],
+ "Name": "Default"
+ }
+ ],
+ "Name": "Wolverine WVR-7Kh"
+}
\ No newline at end of file
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-FIN1.json b/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-FIN1.json
index 11d83106..aaffd7d0 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-FIN1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-FIN1.json
@@ -14,6 +14,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-TDHI1.json b/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-TDHI1.json
index 90367490..5adbf336 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-TDHI1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Wolverine_WVR-TDHI1.json
@@ -15,6 +15,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Zeus_ZEU-9S.json b/BtDamageResolver/data/unit/Unit_Mech_Zeus_ZEU-9S.json
index 15531036..bbb6044a 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Zeus_ZEU-9S.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Zeus_ZEU-9S.json
@@ -10,6 +10,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_Mech_Zeus_ZEU-SB1.json b/BtDamageResolver/data/unit/Unit_Mech_Zeus_ZEU-SB1.json
index 0526a6e1..7b2a4498 100644
--- a/BtDamageResolver/data/unit/Unit_Mech_Zeus_ZEU-SB1.json
+++ b/BtDamageResolver/data/unit/Unit_Mech_Zeus_ZEU-SB1.json
@@ -8,6 +8,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Artemis",
diff --git a/BtDamageResolver/data/unit/Unit_VehicleHover_Magrider_Hovertank.json b/BtDamageResolver/data/unit/Unit_VehicleHover_Magrider_Hovertank.json
index 6283c679..5e33e4a5 100644
--- a/BtDamageResolver/data/unit/Unit_VehicleHover_Magrider_Hovertank.json
+++ b/BtDamageResolver/data/unit/Unit_VehicleHover_Magrider_Hovertank.json
@@ -8,6 +8,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_VehicleHover_Maxim_Heavy_Hover_Transport.json b/BtDamageResolver/data/unit/Unit_VehicleHover_Maxim_Heavy_Hover_Transport.json
index cce1569c..f9f3e784 100644
--- a/BtDamageResolver/data/unit/Unit_VehicleHover_Maxim_Heavy_Hover_Transport.json
+++ b/BtDamageResolver/data/unit/Unit_VehicleHover_Maxim_Heavy_Hover_Transport.json
@@ -8,6 +8,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Amount": 1,
diff --git a/BtDamageResolver/data/unit/Unit_VehicleTracked_Schreck_PPC_Carrier.json b/BtDamageResolver/data/unit/Unit_VehicleTracked_Schreck_PPC_Carrier.json
index c78e7b2b..791e7346 100644
--- a/BtDamageResolver/data/unit/Unit_VehicleTracked_Schreck_PPC_Carrier.json
+++ b/BtDamageResolver/data/unit/Unit_VehicleTracked_Schreck_PPC_Carrier.json
@@ -8,6 +8,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/unit/Unit_VehicleWheeled_Striker_Light_Tank_(SRM).json b/BtDamageResolver/data/unit/Unit_VehicleWheeled_Striker_Light_Tank_(SRM).json
index 431f8ce9..a01893f8 100644
--- a/BtDamageResolver/data/unit/Unit_VehicleWheeled_Striker_Light_Tank_(SRM).json
+++ b/BtDamageResolver/data/unit/Unit_VehicleWheeled_Striker_Light_Tank_(SRM).json
@@ -8,6 +8,7 @@
"Troopers": 1,
"WeaponBays": [
{
+ "Arc": "Default",
"Weapons": [
{
"Ammo": "Normal",
diff --git a/BtDamageResolver/data/weapon/Weapons_AC-LBX.json b/BtDamageResolver/data/weapon/Weapons_AC-LBX.json
index 3e38ca80..667640fc 100644
--- a/BtDamageResolver/data/weapon/Weapons_AC-LBX.json
+++ b/BtDamageResolver/data/weapon/Weapons_AC-LBX.json
@@ -3,10 +3,11 @@
"Name": "LBX AC/2",
"Type": "Ballistic",
"Ammo": {
- "Slug": "AC-Single",
+ "Slug": "AC-Slug",
"Cluster": "AC-LBXCluster"
},
"AmmoDefault": "Slug",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 4
@@ -32,10 +33,11 @@
"Name": "LBX AC/5",
"Type": "Ballistic",
"Ammo": {
- "Slug": "AC-Single",
+ "Slug": "AC-Slug",
"Cluster": "AC-LBXCluster"
},
"AmmoDefault": "Slug",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 8
@@ -61,10 +63,11 @@
"Name": "LBX AC/10",
"Type": "Ballistic",
"Ammo": {
- "Slug": "AC-Single",
+ "Slug": "AC-Slug",
"Cluster": "AC-LBXCluster"
},
"AmmoDefault": "Slug",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 11
@@ -90,10 +93,11 @@
"Name": "LBX AC/20",
"Type": "Ballistic",
"Ammo": {
- "Slug": "AC-Single",
+ "Slug": "AC-Slug",
"Cluster": "AC-LBXCluster"
},
"AmmoDefault": "Slug",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 20
@@ -119,10 +123,11 @@
"Name": "Clan LBX AC/2",
"Type": "Ballistic",
"Ammo": {
- "Slug": "AC-Single",
+ "Slug": "AC-Slug",
"Cluster": "AC-LBXCluster"
},
"AmmoDefault": "Slug",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 4
@@ -148,10 +153,11 @@
"Name": "Clan LBX AC/5",
"Type": "Ballistic",
"Ammo": {
- "Slug": "AC-Single",
+ "Slug": "AC-Slug",
"Cluster": "AC-LBXCluster"
},
"AmmoDefault": "Slug",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 8
@@ -177,10 +183,11 @@
"Name": "Clan LBX AC/10",
"Type": "Ballistic",
"Ammo": {
- "Slug": "AC-Single",
+ "Slug": "AC-Slug",
"Cluster": "AC-LBXCluster"
},
"AmmoDefault": "Slug",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 11
@@ -206,10 +213,11 @@
"Name": "Clan LBX AC/20",
"Type": "Ballistic",
"Ammo": {
- "Slug": "AC-Single",
+ "Slug": "AC-Slug",
"Cluster": "AC-LBXCluster"
},
"AmmoDefault": "Slug",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 20
diff --git a/BtDamageResolver/data/weapon/Weapons_AC-Normal.json b/BtDamageResolver/data/weapon/Weapons_AC-Normal.json
index af8fd086..d3b34425 100644
--- a/BtDamageResolver/data/weapon/Weapons_AC-Normal.json
+++ b/BtDamageResolver/data/weapon/Weapons_AC-Normal.json
@@ -7,6 +7,7 @@
"AP": "AC-AP2"
},
"AmmoDefault": "Normal",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 4
@@ -36,6 +37,7 @@
"AP": "AC-AP5"
},
"AmmoDefault": "Normal",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 8
@@ -65,6 +67,7 @@
"AP": "AC-AP10"
},
"AmmoDefault": "Normal",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 11
@@ -94,6 +97,7 @@
"AP": "AC-AP20"
},
"AmmoDefault": "Normal",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 20
diff --git a/BtDamageResolver/data/weapon/Weapons_ATM.json b/BtDamageResolver/data/weapon/Weapons_ATM.json
index b2a2fe80..38991e52 100644
--- a/BtDamageResolver/data/weapon/Weapons_ATM.json
+++ b/BtDamageResolver/data/weapon/Weapons_ATM.json
@@ -8,6 +8,7 @@
"LRM": "ATM3-LRM"
},
"AmmoDefault": "SRM",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Heat": {
"Short": 2
@@ -48,6 +49,7 @@
"LRM": "ATM9-LRM"
},
"AmmoDefault": "SRM",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Heat": {
"Short": 6
@@ -68,6 +70,7 @@
"LRM": "ATM12-LRM"
},
"AmmoDefault": "SRM",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Heat": {
"Short": 8
diff --git a/BtDamageResolver/data/weapon/Weapons_BattleArmor-SRM.json b/BtDamageResolver/data/weapon/Weapons_BattleArmor-SRM.json
index 8d8f215c..76d7dbca 100644
--- a/BtDamageResolver/data/weapon/Weapons_BattleArmor-SRM.json
+++ b/BtDamageResolver/data/weapon/Weapons_BattleArmor-SRM.json
@@ -2,6 +2,12 @@
{
"Name": "BA SRM-1",
"Type": "Missile",
+ "Ammo": {
+ "Normal": "MissileHoming",
+ "Inferno": "MissileHoming+Inferno"
+ },
+ "AmmoDefault": "Normal",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 1
@@ -23,12 +29,17 @@
},
"UsesAmmo": true,
"ClusterSize": 2,
- "ClusterDamage": 2,
- "SpecialFeatures": [ { "Type": "Cluster" } ]
+ "ClusterDamage": 2
},
{
"Name": "BA SRM-2",
"Type": "Missile",
+ "Ammo": {
+ "Normal": "MissileHoming",
+ "Inferno": "MissileHoming+Inferno"
+ },
+ "AmmoDefault": "Normal",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 2
@@ -50,12 +61,17 @@
},
"UsesAmmo": true,
"ClusterSize": 2,
- "ClusterDamage": 2,
- "SpecialFeatures": [ { "Type": "Cluster" } ]
+ "ClusterDamage": 2
},
{
"Name": "BA SRM-3",
"Type": "Missile",
+ "Ammo": {
+ "Normal": "MissileHoming",
+ "Inferno": "MissileHoming+Inferno"
+ },
+ "AmmoDefault": "Normal",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 3
@@ -77,12 +93,17 @@
},
"UsesAmmo": true,
"ClusterSize": 2,
- "ClusterDamage": 2,
- "SpecialFeatures": [ { "Type": "Cluster" } ]
+ "ClusterDamage": 2
},
{
"Name": "BA SRM-4",
"Type": "Missile",
+ "Ammo": {
+ "Normal": "MissileHoming",
+ "Inferno": "MissileHoming+Inferno"
+ },
+ "AmmoDefault": "Normal",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 4
@@ -104,12 +125,17 @@
},
"UsesAmmo": true,
"ClusterSize": 2,
- "ClusterDamage": 2,
- "SpecialFeatures": [ { "Type": "Cluster" } ]
+ "ClusterDamage": 2
},
{
"Name": "BA SRM-5",
"Type": "Missile",
+ "Ammo": {
+ "Normal": "MissileHoming",
+ "Inferno": "MissileHoming+Inferno"
+ },
+ "AmmoDefault": "Normal",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 5
@@ -131,12 +157,17 @@
},
"UsesAmmo": true,
"ClusterSize": 2,
- "ClusterDamage": 2,
- "SpecialFeatures": [ { "Type": "Cluster" } ]
+ "ClusterDamage": 2
},
{
"Name": "BA SRM-6",
"Type": "Missile",
+ "Ammo": {
+ "Normal": "MissileHoming",
+ "Inferno": "MissileHoming+Inferno"
+ },
+ "AmmoDefault": "Normal",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 6
@@ -158,7 +189,6 @@
},
"UsesAmmo": true,
"ClusterSize": 2,
- "ClusterDamage": 2,
- "SpecialFeatures": [ { "Type": "Cluster" } ]
+ "ClusterDamage": 2
}
]
diff --git a/BtDamageResolver/data/weapon/Weapons_MML.json b/BtDamageResolver/data/weapon/Weapons_MML.json
index a0029601..c067e3ab 100644
--- a/BtDamageResolver/data/weapon/Weapons_MML.json
+++ b/BtDamageResolver/data/weapon/Weapons_MML.json
@@ -10,6 +10,7 @@
"LRM Indirect": "MML3-LRM Indirect"
},
"AmmoDefault": "SRM",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Heat": {
"Short": 2
@@ -27,6 +28,7 @@
"LRM Indirect": "MML5-LRM Indirect"
},
"AmmoDefault": "SRM",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Heat": {
"Short": 3
@@ -44,6 +46,7 @@
"LRM Indirect": "MML7-LRM Indirect"
},
"AmmoDefault": "SRM",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Heat": {
"Short": 2
@@ -61,6 +64,7 @@
"LRM Indirect": "MML9-LRM Indirect"
},
"AmmoDefault": "SRM",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Heat": {
"Short": 3
diff --git a/BtDamageResolver/data/weapon/Weapons_SRM.json b/BtDamageResolver/data/weapon/Weapons_SRM.json
index 6e6d1d40..0a2248b0 100644
--- a/BtDamageResolver/data/weapon/Weapons_SRM.json
+++ b/BtDamageResolver/data/weapon/Weapons_SRM.json
@@ -4,9 +4,11 @@
"Type": "Missile",
"Ammo": {
"Normal": "MissileHoming",
+ "Inferno": "MissileHoming+Inferno",
"Artemis": "MissileHoming+Artemis"
},
"AmmoDefault": "Normal",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 2
@@ -34,9 +36,11 @@
"Type": "Missile",
"Ammo": {
"Normal": "MissileHoming",
+ "Inferno": "MissileHoming+Inferno",
"Artemis": "MissileHoming+Artemis"
},
"AmmoDefault": "Normal",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 4
@@ -64,9 +68,11 @@
"Type": "Missile",
"Ammo": {
"Normal": "MissileHoming",
+ "Inferno": "MissileHoming+Inferno",
"Artemis": "MissileHoming+Artemis"
},
"AmmoDefault": "Normal",
+ "MultiAmmo": true,
"AttackType": "Normal",
"Damage": {
"Short": 6
diff --git a/BtDamageResolver/docs/superpowers/plans/2026-06-02-native-appsettings-config-refactor.md b/BtDamageResolver/docs/superpowers/plans/2026-06-02-native-appsettings-config-refactor.md
new file mode 100644
index 00000000..b3a354f9
--- /dev/null
+++ b/BtDamageResolver/docs/superpowers/plans/2026-06-02-native-appsettings-config-refactor.md
@@ -0,0 +1,830 @@
+# Native appsettings + ConnectionStrings Refactor — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Replace the custom `ConfigurationUtilities.GetConfiguration` scheme with the native .NET host config chain, move Redis and Postgres connection strings into native `ConnectionStrings`, delete `CommunicationOptions`, remove `RESOLVER_ENVIRONMENT`, and rename the `orleansdb` container to `resolverpostgres`.
+
+**Architecture:** The silo uses `Host.CreateDefaultBuilder()` (already default-loads `appsettings.json` → `appsettings.{DOTNET_ENVIRONMENT}.json` → env vars). The client's `Startup.ConfigureServices` becomes an instance method binding off the injected `Configuration`. The tools use `Host.CreateApplicationBuilder(args).Configuration`. Connection strings are read via `IConfiguration.GetConnectionString(...)`. `FaemiyahClusterOptions` is kept intact (published in the `Common` NuGet package) and has its `ConnectionString` hydrated via `PostConfigure`.
+
+**Tech Stack:** .NET 10, Microsoft.Orleans 10.1, Microsoft.Extensions.Hosting/Configuration, NUnit 4 + AwesomeAssertions, Docker Compose.
+
+**Repos touched (commit in the repo that contains each path):**
+- **Code repo:** `C:\Work\src\BtDamageResolver\BtDamageResolver` (Silo, Common, Services, tools, Tests) — this is the cwd/git repo.
+- **Infra:** `C:\Work\src\BtDamageResolver\infra` + `C:\Work\src\BtDamageResolver\refresh.sh|.bat` — run `git status` to confirm which repo tracks these; commit there.
+- **Client repo:** `C:\Work\src\BtDamageResolver\BtDamageResolverClient` (BlazorServer). Builds against `Common`/`Api` **NuGet** packages, so it is independent of the `Common` source edits below.
+
+**Spec:** `docs/superpowers/specs/2026-06-02-native-appsettings-config-refactor-design.md`
+
+---
+
+## Task 1: Config precedence regression test
+
+Locks in the two behaviors the original custom loader existed to guarantee: `GetConnectionString("Redis")` resolves the JSON value, and a `ConnectionStrings__Redis` environment variable overrides it. This is a characterization test of the native chain (it passes immediately); it guards against future regressions in key naming/precedence.
+
+**Files:**
+- Create: `tests/Tests/ConfigurationPrecedenceTests.cs`
+
+- [ ] **Step 1: Write the test**
+
+```csharp
+using System;
+using System.IO;
+using System.Text;
+using AwesomeAssertions;
+using Microsoft.Extensions.Configuration;
+using NUnit.Framework;
+
+namespace Faemiyah.BtDamageResolver.Tests;
+
+///
+/// Verifies the native configuration chain resolves connection strings from the
+/// ConnectionStrings block and that environment variables override JSON values,
+/// matching the source order appsettings.json -> environment variables.
+///
+[TestFixture]
+internal class ConfigurationPrecedenceTests
+{
+ private const string RedisEnvVariable = "ConnectionStrings__Redis";
+ private const string JsonConnectionString = "redis-from-json:6379";
+
+ private const string SampleJson = """
+ {
+ "ConnectionStrings": {
+ "Redis": "redis-from-json:6379"
+ }
+ }
+ """;
+
+ [TearDown]
+ public void TearDown()
+ {
+ Environment.SetEnvironmentVariable(RedisEnvVariable, null);
+ }
+
+ [Test]
+ public void GetConnectionString_ResolvesRedisFromJson()
+ {
+ // Arrange
+ var configuration = BuildConfiguration();
+
+ // Act
+ var connectionString = configuration.GetConnectionString("Redis");
+
+ // Assert
+ connectionString.Should().Be(JsonConnectionString);
+ }
+
+ [Test]
+ public void EnvironmentVariable_OverridesJsonConnectionString()
+ {
+ // Arrange
+ const string overrideValue = "redis-from-env:6379";
+ Environment.SetEnvironmentVariable(RedisEnvVariable, overrideValue);
+ var configuration = BuildConfiguration();
+
+ // Act
+ var connectionString = configuration.GetConnectionString("Redis");
+
+ // Assert
+ connectionString.Should().Be(overrideValue);
+ }
+
+ private static IConfiguration BuildConfiguration()
+ {
+ return new ConfigurationBuilder()
+ .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(SampleJson)))
+ .AddEnvironmentVariables()
+ .Build();
+ }
+}
+```
+
+- [ ] **Step 2: Run the test**
+
+Run: `dotnet test tests/Tests/Tests.csproj --filter "FullyQualifiedName~ConfigurationPrecedenceTests" --nologo`
+Expected: PASS (2 passed). This is a characterization test, so it passes on first run.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add tests/Tests/ConfigurationPrecedenceTests.cs
+git commit -m "test: lock in native ConnectionStrings precedence behavior"
+```
+
+---
+
+## Task 2: Silo — native config + ConnectionStrings + cluster hydration
+
+**Files:**
+- Modify: `src/Silo/Program.cs` (`CreateSilo` ~112-222, `GetRedisEntityRepository` ~224-231)
+- Rename: `src/Silo/SiloSettings.json` → `src/Silo/appsettings.json`; `src/Silo/SiloSettings.Release.json` → `src/Silo/appsettings.Release.json` (and `SiloSettings.Debug.json` → `appsettings.Debug.json` if a source copy exists)
+- Modify: `src/Silo/Silo.csproj`
+
+- [ ] **Step 1: Rename the settings files (preserve git history)**
+
+```bash
+git mv src/Silo/SiloSettings.json src/Silo/appsettings.json
+git mv src/Silo/SiloSettings.Release.json src/Silo/appsettings.Release.json
+# Only if a source Debug file exists (it may live only under bin/):
+git mv src/Silo/SiloSettings.Debug.json src/Silo/appsettings.Debug.json 2>/dev/null || true
+```
+
+- [ ] **Step 2: Rewrite `src/Silo/appsettings.json` to use ConnectionStrings**
+
+```json
+{
+ "LoggingOptions": {
+ "LogToConsole": true,
+ "LogToDatabase": true,
+ "LogLevel": "Information",
+ "LogLevelOrleans": "Warning",
+ "LoggingIntervalMilliseconds": 15000,
+ "SendDetailedErrorsToClient": true
+ },
+ "ClusterOptions": {
+ "Invariant": "Npgsql"
+ },
+ "ConnectionStrings": {
+ "Postgres": "User ID=USERNAME;Password=PASSWORD;Host=resolverpostgres;Port=5432;Database=BtDamageResolver;SSL Mode=Disable;",
+ "Redis": "redis:6379,password=PASSWORD"
+ },
+ "CompressionOptions": {
+ "Provider": "Brotli",
+ "Quality": 4
+ }
+}
+```
+
+- [ ] **Step 3: Update `src/Silo/Silo.csproj` copy directives**
+
+Replace the two `` blocks that reference `SiloSettings.*` with:
+
+```xml
+
+
+ Always
+
+
+ Always
+
+
+
+
+
+ Always
+
+
+```
+
+- [ ] **Step 4: Rewrite `CreateSilo()` to read native config inside `UseOrleans((context, siloBuilder) => ...)`**
+
+In `src/Silo/Program.cs`, replace the start of `CreateSilo` (the three local variables + the `Host.CreateDefaultBuilder()...ConfigureAppConfiguration(...).UseOrleans(siloBuilder =>` opening) with the version below. Keep the entire body of the Orleans delegate identical EXCEPT the four changes marked, and the closing stays the same.
+
+```csharp
+ private static IHost CreateSilo()
+ {
+ var (clientPort, siloPort) = GetSiloPortConfigurationFromEnvironment();
+
+ var siloHostBuilder = Host.CreateDefaultBuilder()
+ .UseOrleans((context, siloBuilder) =>
+ {
+ var configuration = context.Configuration;
+ var clusterOptions = new FaemiyahClusterOptions
+ {
+ Invariant = configuration[$"{Settings.ClusterOptionsBlockName}:Invariant"],
+ ConnectionString = configuration.GetConnectionString("Postgres")
+ };
+
+ siloBuilder
+ .Services.AddSerializer(serializerBuilder => serializerBuilder.AddJsonSerializer(
+ isSupported: type => type.Namespace != null && type.Namespace.StartsWith("Faemiyah.BtDamageResolver"),
+ jsonSerializerOptions: CreateJsonSerializerOptions()));
+ siloBuilder
+ .Configure(options =>
+ {
+ options.ClusterId = "faemiyah";
+ options.ServiceId = "Resolver";
+ })
+ .Configure(options => options.CollectionAge = TimeSpan.FromDays(1))
+ .Configure(options =>
+ {
+ options.DefunctSiloExpiration = TimeSpan.FromHours(1);
+ options.NumMissedProbesLimit = 2;
+ options.NumMissedTableIAmAliveLimit = 1;
+ options.NumVotesForDeathDeclaration = 1;
+ })
+ .Configure(options =>
+ {
+ options.MaxRequestProcessingTime = TimeSpan.FromSeconds(15);
+ options.SystemResponseTimeout = TimeSpan.FromSeconds(15);
+ })
+ .Configure(options =>
+ {
+ options.DropExpiredMessages = true;
+ options.ResponseTimeout = TimeSpan.FromSeconds(15);
+ options.ResponseTimeoutWithDebugger = TimeSpan.FromMinutes(15);
+ })
+ .UseAdoNetClustering(options =>
+ {
+ options.Invariant = clusterOptions.Invariant;
+ options.ConnectionString = clusterOptions.ConnectionString;
+ })
+ .AddGrainStorage(Settings.ActorStateStoreName, clusterOptions)
+ .AddGrainStorage(Settings.SessionStateStoreName, clusterOptions)
+ .Configure(options =>
+ {
+ options.AdvertisedIPAddress = GetHostIp();
+ options.GatewayListeningEndpoint = new IPEndPoint(IPAddress.Any, clientPort);
+ options.SiloListeningEndpoint = new IPEndPoint(IPAddress.Any, siloPort);
+ })
+ .AddGrainService()
+ .AddGrainService()
+ .ConfigureServices(services =>
+ {
+ services.ConfigureJsonSerializerOptions();
+ services.Configure(configuration.GetSection(Settings.CompressionOptionsBlockName));
+ services.Configure(configuration.GetSection(Settings.ClusterOptionsBlockName));
+ services.PostConfigure(options => options.ConnectionString = configuration.GetConnectionString("Postgres"));
+ services.Configure(configuration.GetSection(Settings.LoggingOptionsBlockName));
+ services.AddLogging(conf =>
+ {
+ conf.AddFaemiyahLogging();
+ conf.AddFilter("DeploymentLoadPublisher", LogLevel.Warning);
+ });
+ services.Configure(opt => opt.SuppressStatusMessages = true);
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton>(GetRedisEntityRepository);
+ services.AddSingleton>(GetRedisEntityRepository);
+ services.AddSingleton>(GetRedisEntityRepository);
+ services.AddSingleton>(GetRedisEntityRepository);
+ services.AddSingleton>(GetRedisEntityRepository);
+ services.AddSingleton>(GetRedisEntityRepository);
+ services.AddSingleton>(GetRedisEntityRepository);
+ services.AddSingleton>(GetRedisEntityRepository);
+ services.AddSingleton, CachedEntityRepository>();
+ services.AddSingleton, CachedEntityRepository>();
+ services.AddSingleton, CachedEntityRepository>();
+ services.AddSingleton, CachedEntityRepository>();
+ services.AddSingleton, CachedEntityRepository>();
+ services.AddSingleton, CachedEntityRepository>();
+ services.AddSingleton, CachedEntityRepository>();
+ services.AddSingleton, CachedEntityRepository>();
+ services.AddSingleton(serviceProvider =>
+ {
+ return new RepositoryProvider(
+ serviceProvider.GetService>(),
+ serviceProvider.GetService>(),
+ serviceProvider.GetService>(),
+ serviceProvider.GetService>(),
+ serviceProvider.GetService>(),
+ serviceProvider.GetService>(),
+ serviceProvider.GetService>()
+ );
+ });
+ services.AddSingleton();
+ });
+ });
+
+ return siloHostBuilder.Build();
+ }
+```
+
+The four changes vs. the original: (1) no top-level `GetConfiguration`/`clusterOptions`; (2) `.ConfigureAppConfiguration(...)` removed; (3) `UseOrleans((context, siloBuilder) => ...)` with `clusterOptions` built from `context.Configuration` + `GetConnectionString("Postgres")`; (4) the `services.Configure(...)` line is gone and a `FaemiyahClusterOptions` `PostConfigure` is added.
+
+- [ ] **Step 5: Update `GetRedisEntityRepository` to read the Redis connection string from config**
+
+Replace the method body:
+
+```csharp
+ private static RedisEntityRepository GetRedisEntityRepository(IServiceProvider serviceProvider)
+ where TType : class, IEntity
+ {
+ var connectionString = serviceProvider.GetRequiredService().GetConnectionString("Redis");
+ return !string.IsNullOrEmpty(connectionString)
+ ? new RedisEntityRepository(serviceProvider.GetService>>(), serviceProvider.GetService>(), connectionString)
+ : throw new InvalidOperationException($"No 'Redis' connection string configured for entity repository of type {typeof(TType)}.");
+ }
+```
+
+- [ ] **Step 6: Build the silo**
+
+Run: `dotnet build src/Silo/Silo.csproj --nologo`
+Expected: Build succeeded, 0 errors. (`CommunicationOptions` still exists in `Common`; it is simply no longer referenced by the silo.)
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add src/Silo/
+git commit -m "refactor: silo uses native appsettings + ConnectionStrings"
+```
+
+---
+
+## Task 3: Tools — native config via Host.CreateApplicationBuilder
+
+Both tools switch to `Host.CreateApplicationBuilder(args).Configuration`, read Redis from `GetConnectionString("Redis")`, and read their own `appsettings.json`. This also fixes the exporter's cross-file read (it currently loads `DataImporterSettings.json`).
+
+**Files:**
+- Modify: `tools/DataImporter/Program.cs`, `tools/DataImporter/DataImporter.cs`, `tools/DataImporter/DataImporter.csproj`
+- Modify: `tools/DataExporter/Program.cs`, `tools/DataExporter/DataExporter.cs`, `tools/DataExporter/DataExporter.csproj`
+- Rename: `DataImporterSettings.json`/`.Release.json` → `appsettings.json`/`appsettings.Release.json`; same for DataExporter
+
+- [ ] **Step 1: Rename + rewrite the importer settings file**
+
+```bash
+git mv tools/DataImporter/DataImporterSettings.json tools/DataImporter/appsettings.json
+git mv tools/DataImporter/DataImporterSettings.Release.json tools/DataImporter/appsettings.Release.json
+git mv tools/DataImporter/DataImporterSettings.Debug.json tools/DataImporter/appsettings.Debug.json 2>/dev/null || true
+```
+
+Rewrite `tools/DataImporter/appsettings.json`:
+
+```json
+{
+ "LoggingOptions": {
+ "LogToConsole": true,
+ "LogLevel": "Information",
+ "LogLevelOrleans": "Warning"
+ },
+ "ConnectionStrings": {
+ "Redis": "redis:6379,password=PASSWORD"
+ }
+}
+```
+
+- [ ] **Step 2: Rename + rewrite the exporter settings file (drop the dead Postgres block)**
+
+```bash
+git mv tools/DataExporter/DataExporterSettings.json tools/DataExporter/appsettings.json
+git mv tools/DataExporter/DataExporterSettings.Release.json tools/DataExporter/appsettings.Release.json
+git mv tools/DataExporter/DataExporterSettings.Debug.json tools/DataExporter/appsettings.Debug.json 2>/dev/null || true
+```
+
+Rewrite `tools/DataExporter/appsettings.json` (identical shape to the importer):
+
+```json
+{
+ "LoggingOptions": {
+ "LogToConsole": true,
+ "LogLevel": "Information",
+ "LogLevelOrleans": "Warning"
+ },
+ "ConnectionStrings": {
+ "Redis": "redis:6379,password=PASSWORD"
+ }
+}
+```
+
+- [ ] **Step 3: Update both `.csproj` copy directives**
+
+In `tools/DataImporter/DataImporter.csproj` and `tools/DataExporter/DataExporter.csproj`, replace the `SiloSettings`/`DataImporterSettings`/`DataExporterSettings` `None Update` entries with:
+
+```xml
+
+
+ Always
+
+
+ Always
+
+
+
+
+
+ Always
+
+
+```
+
+- [ ] **Step 4: Update `tools/DataImporter/Program.cs`**
+
+Replace the private `GetConfiguration` and its call. Change the top of `Main`:
+
+```csharp
+ var configuration = Host.CreateApplicationBuilder(args).Configuration;
+ var section = configuration.GetSection(Settings.LoggingOptionsBlockName);
+```
+
+Delete the entire `private static IConfiguration GetConfiguration(string settingsFile) { ... }` method. Add `using Microsoft.Extensions.Hosting;` and remove `using System.IO;` if no longer used. Keep `using Microsoft.Extensions.Configuration;` (needed for `GetSection`).
+
+- [ ] **Step 5: Update `tools/DataImporter/DataImporter.cs`**
+
+The `DataImporter` constructor takes `ILoggerFactory loggerFactory`. Add an `IConfiguration` parameter so it shares the host configuration, and read Redis from it:
+
+Change the constructor signature and the connection-string lines:
+
+```csharp
+ public DataImporter(ILoggerFactory loggerFactory, IConfiguration configuration)
+ {
+ _logger = loggerFactory.CreateLogger();
+ _jsonSerializerOptions = GetJsonSerializerOptions();
+
+ // Repository provider setup
+ var redisConnectionString = configuration.GetConnectionString("Redis");
+ _repositoryProvider = new RepositoryProvider(
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString),
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString),
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString),
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString),
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString),
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString),
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString));
+ }
+```
+
+KEEP `using static Faemiyah.BtDamageResolver.Common.ConfigurationUtilities;` (still used by `GetJsonSerializerOptions()`). Remove `using Faemiyah.BtDamageResolver.Common.Options;` (no longer references `CommunicationOptions`) and `using Faemiyah.BtDamageResolver.Common.Constants;` (the only `Settings.*` use — `CommunicationOptionsBlockName` — is gone). Keep `using Microsoft.Extensions.Configuration;`.
+
+- [ ] **Step 6: Thread configuration through the importer's `RunDataImport`**
+
+In `tools/DataImporter/Program.cs`, the `RunDataImport` helper constructs `new DataImporter(loggerFactory)`. Pass configuration through. Update `Main` to capture `configuration` (already done in Step 4) and change `RunDataImport`:
+
+```csharp
+ var result = Parser.Default.ParseArguments(args)
+ .MapResult(
+ initOptions => RunDataImport(loggerFactory, configuration, initOptions).Result,
+ errs => 1);
+```
+
+```csharp
+ private static async Task RunDataImport(ILoggerFactory loggerFactory, IConfiguration configuration, DataImportOptions options)
+ {
+ try
+ {
+ var dataImporter = new DataImporter(loggerFactory, configuration);
+ await dataImporter.Work(options);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ return 1;
+ }
+
+ return 0;
+ }
+```
+
+- [ ] **Step 7: Apply the identical changes to the exporter**
+
+Repeat Steps 4-6 for `tools/DataExporter/Program.cs` and `tools/DataExporter/DataExporter.cs`, substituting `DataExporter`/`DataExportOptions`/`RunDataExport` and the logger category `"DataExporter"`. The two `Program.cs` files are structurally identical apart from those names. Apply the same `using` cleanup to `DataExporter.cs` (remove `Common.Options` and `Common.Constants`, keep the `ConfigurationUtilities` static using). The exporter's `DataExporter` constructor (currently reading `DataImporterSettings.json`) becomes:
+
+```csharp
+ public DataExporter(ILoggerFactory loggerFactory, IConfiguration configuration)
+ {
+ _logger = loggerFactory.CreateLogger();
+ _jsonSerializerOptions = GetJsonSerializerOptions();
+
+ // Pretty printing options
+ _jsonSerializerOptions.IndentCharacter = ' ';
+ _jsonSerializerOptions.IndentSize = 4;
+ _jsonSerializerOptions.WriteIndented = true;
+
+ // Repository provider setup
+ var redisConnectionString = configuration.GetConnectionString("Redis");
+ _repositoryProvider = new RepositoryProvider(
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString),
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString),
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString),
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString),
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString),
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString),
+ new RedisEntityRepository(loggerFactory.CreateLogger>(), Options.Create(_jsonSerializerOptions), redisConnectionString));
+ }
+```
+
+- [ ] **Step 8: Build both tools**
+
+Run: `dotnet build tools/DataImporter/DataImporter.csproj --nologo && dotnet build tools/DataExporter/DataExporter.csproj --nologo`
+Expected: Build succeeded, 0 errors.
+
+- [ ] **Step 9: Commit**
+
+```bash
+git add tools/
+git commit -m "refactor: tools use native config + ConnectionStrings; fix exporter cross-file read"
+```
+
+---
+
+## Task 4: Services — CommunicationService reads Redis from IConfiguration
+
+**Files:**
+- Modify: `src/Services/CommunicationService.cs`
+
+- [ ] **Step 1: Swap the options dependency for `IConfiguration`**
+
+Change the constructor parameter `IOptions communicationOptions` to `IConfiguration configuration`, and line 44's `communicationOptions.Value.ConnectionString` to `configuration.GetConnectionString("Redis")`:
+
+```csharp
+ public CommunicationService(
+ ILogger logger,
+ IConfiguration configuration,
+ IOptions jsonSerializerOptions,
+ DataHelper dataHelper,
+ IGrainFactory grainFactory,
+ GrainId grainId,
+ Silo silo,
+ ILoggerFactory loggerFactory) : base(grainId, silo, loggerFactory)
+ {
+ _logger = logger;
+ _serverToClientCommunicator = new ServerToClientCommunicator(loggerFactory.CreateLogger(), jsonSerializerOptions, configuration.GetConnectionString("Redis"), dataHelper, grainFactory);
+ }
+```
+
+Update the `` doc line to `The application configuration.`. Add `using Microsoft.Extensions.Configuration;`. Remove `using Faemiyah.BtDamageResolver.Common.Options;` (keep `using Microsoft.Extensions.Options;` — still used by `IOptions`).
+
+- [ ] **Step 2: Build Services**
+
+Run: `dotnet build src/Services/Services.csproj --nologo`
+Expected: Build succeeded, 0 errors.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/Services/CommunicationService.cs
+git commit -m "refactor: CommunicationService reads Redis from IConfiguration"
+```
+
+---
+
+## Task 5: Delete CommunicationOptions, GetConfiguration, RESOLVER_ENVIRONMENT; fix Settings docs
+
+At this point nothing in the code repo references `CommunicationOptions` or `GetConfiguration`. Remove them.
+
+**Files:**
+- Delete: `src/Common/Options/CommunicationOptions.cs`
+- Modify: `src/Common/Constants/Settings.cs`
+- Modify: `src/Common/ConfigurationUtilities.cs`
+
+- [ ] **Step 1: Delete the options class**
+
+```bash
+git rm src/Common/Options/CommunicationOptions.cs
+```
+
+- [ ] **Step 2: Update `src/Common/Constants/Settings.cs`**
+
+Remove the `CommunicationOptionsBlockName` constant and its doc comment. Fix the two wrong "RabbitMQ" doc comments:
+
+```csharp
+ ///
+ /// The name of the options block which contains Orleans clustering settings (the ADO.NET invariant).
+ ///
+ public const string ClusterOptionsBlockName = "ClusterOptions";
+
+ ///
+ /// The name of the options block which configures application logging.
+ ///
+ public const string LoggingOptionsBlockName = "LoggingOptions";
+```
+
+(Leave `CompressionOptionsBlockName`, `ActorStateStoreName`, `SessionStateStoreName`, `MaximumGameEntryAgeHours` unchanged.)
+
+- [ ] **Step 3: Trim `src/Common/ConfigurationUtilities.cs`**
+
+Delete the `GetConfiguration` method (lines ~80-118), the `GetEnvironmentName` method (lines ~64-78), and the `EnvironmentName` / `DefaultDebugEnvironmentName` / `DefaultEnvironmentName` constants. Simplify `GetSiloPortConfigurationFromEnvironment` to drop the `portPad` parameter and the Debug-padding block:
+
+```csharp
+ public static (int ClientPort, int SiloPort) GetSiloPortConfigurationFromEnvironment()
+ {
+ int clientPort = DefaultOrleansClientPort, siloPort = DefaultOrleansSiloPort;
+
+ try
+ {
+ var envClientPort = Environment.GetEnvironmentVariable(EnvironmentOrleansClientPort);
+ var envSiloPort = Environment.GetEnvironmentVariable(EnvironmentOrleansSiloPort);
+
+ if (envClientPort != null && envSiloPort != null)
+ {
+ clientPort = int.Parse(envClientPort, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ siloPort = int.Parse(envSiloPort, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException("Unable to resolve silo port configuration from environment variables.", ex);
+ }
+
+ return (clientPort, siloPort);
+ }
+```
+
+Remove the now-unused `using System.IO;` and `using Microsoft.Extensions.Configuration;` directives. Keep `System`, `System.Globalization`, `System.Net`, `System.Net.Sockets`, `System.Text.Json`, `System.Text.Json.Serialization`, `Microsoft.Extensions.DependencyInjection`. (`Faemiyah...Common.Options` is no longer needed either — remove it.)
+
+- [ ] **Step 4: Full code-repo build + tests**
+
+Run: `dotnet build --nologo`
+Expected: Build succeeded, 0 errors across all projects (Common, Api, Services, Actors, Silo, tools, Tests).
+
+Run: `dotnet test tests/Tests/Tests.csproj --nologo`
+Expected: All tests pass (including `ConfigurationPrecedenceTests`).
+
+- [ ] **Step 5: Verify RESOLVER_ENVIRONMENT is gone from code**
+
+Run: `git grep -n "RESOLVER_ENVIRONMENT\|CommunicationOptions\|GetConfiguration\|GetEnvironmentName"`
+Expected: no matches in `src/`, `tools/`, or `tests/` (matches only inside `docs/superpowers/`).
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add -A
+git commit -m "refactor: remove CommunicationOptions, custom config loader, RESOLVER_ENVIRONMENT"
+```
+
+---
+
+## Task 6: Infra — rename orleansdb → resolverpostgres + ConnectionStrings env vars
+
+**Files (in the infra repo — `git status` to confirm which repo tracks these):**
+- Modify: `infra/docker-compose.yml`
+- Modify: `infra/grafana/datasources/datasource.yml`
+- Modify: `refresh.sh`, `refresh.bat`
+
+- [ ] **Step 1: Update `infra/docker-compose.yml`**
+
+Make these edits:
+- `resolver.environment`: `RESOLVER_ENVIRONMENT: Release` → `DOTNET_ENVIRONMENT: Release`; rename `ClusterOptions__ConnectionString` → `ConnectionStrings__Postgres` and change `Host=orleansdb` → `Host=resolverpostgres`; rename `CommunicationOptions__ConnectionString` → `ConnectionStrings__Redis`.
+- `resolver.depends_on`: `- orleansdb` → `- resolverpostgres`.
+- `resolverclient.environment`: remove the `RESOLVER_ENVIRONMENT: Release` line; rename `CommunicationOptions__ConnectionString` → `ConnectionStrings__Redis` (keep `ASPNETCORE_ENVIRONMENT: Release`).
+- `resolverdataimporter.environment`: `RESOLVER_ENVIRONMENT: Release` → `DOTNET_ENVIRONMENT: Release`; rename `CommunicationOptions__ConnectionString` → `ConnectionStrings__Redis`.
+- `grafana.depends_on`: `- orleansdb` → `- resolverpostgres`.
+- The `orleansdb:` service block → rename the key to `resolverpostgres:`; `container_name: orleansdb` → `resolverpostgres`; `hostname: orleansdb` → `resolverpostgres`; `image: orleansdb:latest` → `resolverpostgres:latest`; volume mount `orleansdb-data:/var/lib/postgresql/18/docker` → `resolverpostgres-data:/var/lib/postgresql/18/docker`.
+- Top-level `volumes:` block: `orleansdb-data:` → `resolverpostgres-data:`.
+
+Resulting `resolver` env block for reference:
+
+```yaml
+ environment:
+ DOTNET_ENVIRONMENT: Release
+ ConnectionStrings__Postgres: "User ID=${RESOLVER_USER:?err};Password=${RESOLVER_PASSWORD:?err};Host=resolverpostgres;Port=5432;Database=BtDamageResolver;SSL Mode=Disable;"
+ ConnectionStrings__Redis: "redis:6379,password=${RESOLVER_PASSWORD:?err}"
+```
+
+- [ ] **Step 2: Update `infra/grafana/datasources/datasource.yml`**
+
+Change `url: orleansdb:5432` → `url: resolverpostgres:5432`. Leave `name: Orleansdb` unchanged (Grafana dashboards may reference the datasource by name; renaming the display name is out of scope).
+
+- [ ] **Step 3: Update build scripts**
+
+In `refresh.sh` and `refresh.bat`, change `docker build --tag orleansdb -f ./postgresql/Dockerfile ./postgresql/` → `docker build --tag resolverpostgres -f ./postgresql/Dockerfile ./postgresql/`.
+
+- [ ] **Step 4: Verify no stray references**
+
+Run: `git grep -n "orleansdb"` (in the infra repo)
+Expected: no matches (the volume, hostname, image, datasource url, and build tag are all renamed).
+
+- [ ] **Step 5: Commit (in the infra repo)**
+
+```bash
+git add infra/docker-compose.yml infra/grafana/datasources/datasource.yml refresh.sh refresh.bat
+git commit -m "chore: rename orleansdb container to resolverpostgres; native ConnectionStrings env vars"
+```
+
+---
+
+## Task 7: Client — native config + ConnectionStrings (separate repo)
+
+**Files (in `C:\Work\src\BtDamageResolver\BtDamageResolverClient`):**
+- Modify: `src/BlazorServer/Startup.cs`
+- Modify: `src/BlazorServer/Communication/ResolverCommunicator.cs`
+- Modify: `src/BlazorServer/appsettings.json`
+- Create: `src/BlazorServer/appsettings.Release.json`
+- Delete: `src/BlazorServer/CommunicationSettings.json`, `CommunicationSettings.Release.json`, `CommunicationSettings.Debug.json`
+- Modify: `src/BlazorServer/BlazorServer.csproj`
+
+- [ ] **Step 1: Merge config into `appsettings.json`**
+
+Rewrite `src/BlazorServer/appsettings.json` to fold in the contents of `CommunicationSettings.json`:
+
+```json
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*",
+ "LoggingOptions": {
+ "LogToConsole": true,
+ "LogLevel": "Information",
+ "LogLevelOrleans": "Warning"
+ },
+ "ConnectionStrings": {
+ "Redis": "redis:6379,password=PASSWORD"
+ },
+ "CompressionOptions": {
+ "Provider": "Brotli",
+ "Quality": 4
+ }
+}
+```
+
+- [ ] **Step 2: Move the Release override and delete the old files**
+
+```bash
+git mv src/BlazorServer/CommunicationSettings.Release.json src/BlazorServer/appsettings.Release.json
+git rm src/BlazorServer/CommunicationSettings.json src/BlazorServer/CommunicationSettings.Debug.json
+```
+
+If `appsettings.Release.json` is empty `{}`, leave it as `{}`.
+
+- [ ] **Step 3: Update `BlazorServer.csproj`**
+
+Remove the two `` blocks that reference `CommunicationSettings.*`. Add an explicit copy for the Release override (the Web SDK auto-includes `appsettings.json`/`appsettings.Development.json` but not `appsettings.Release.json`):
+
+```xml
+
+
+ PreserveNewest
+
+
+```
+
+- [ ] **Step 4: Make `ConfigureServices` an instance method binding off `Configuration`**
+
+In `src/BlazorServer/Startup.cs`, change the signature from `public static void ConfigureServices(IServiceCollection services)` to `public void ConfigureServices(IServiceCollection services)`, delete the line `var configuration = GetConfiguration("CommunicationSettings.json");`, and replace every `configuration.` reference in the method with `Configuration.` (the injected property). Specifically:
+
+```csharp
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.Configure(Configuration.GetSection(Settings.CompressionOptionsBlockName));
+ services.Configure(Configuration.GetSection(Settings.LoggingOptionsBlockName));
+```
+
+Update the data-protection line that read `configuration[...]`:
+
+```csharp
+ services.AddDataProtection().SetApplicationName("BtDamageResolverClient").PersistKeysToFileSystem(new DirectoryInfo(Configuration["DataProtectionKeysPath"] ?? "/app/dpkeys/"));
+```
+
+Delete the now-removed `services.Configure(...)` line. Remove `using static Faemiyah.BtDamageResolver.Common.ConfigurationUtilities;` ONLY if `ConfigureJsonSerializerOptions()` (line ~89) is the sole remaining use — it is an extension method on `IServiceCollection` from that static class, so KEEP the static using. Leave the rest of the method unchanged.
+
+- [ ] **Step 5: Update `GetRedisEntityRepository` in `Startup.cs`**
+
+```csharp
+ private RedisEntityRepository GetRedisEntityRepository(IServiceProvider serviceProvider)
+ where TType : class, IEntity
+ {
+ var connectionString = Configuration.GetConnectionString("Redis");
+
+ if (!string.IsNullOrEmpty(connectionString))
+ {
+ return new RedisEntityRepository(serviceProvider.GetService>>(), serviceProvider.GetService>(), connectionString);
+ }
+
+ throw new InvalidOperationException($"No 'Redis' connection string configured for entity repository of type {typeof(TType)}.");
+ }
+```
+
+(`GetRedisEntityRepository` becomes a non-static instance method so it can read `Configuration`; it is referenced as a method-group `GetRedisEntityRepository` in the registrations, which still resolves on the instance.)
+
+- [ ] **Step 6: Update `ResolverCommunicator.cs`**
+
+Replace the `IOptions` dependency with `IConfiguration`:
+
+```csharp
+ private readonly string _redisConnectionString;
+```
+
+(replacing `private readonly CommunicationOptions _communicationOptions;`)
+
+```csharp
+ public ResolverCommunicator(ILogger logger, IConfiguration configuration, IOptions jsonSerializerOptions, DataHelper dataHelper, HubConnection hubConnection)
+ {
+ _logger = logger;
+ _jsonSerializerOptions = jsonSerializerOptions;
+ _redisConnectionString = configuration.GetConnectionString("Redis");
+ _dataHelper = dataHelper;
+ _hubConnection = hubConnection;
+ }
+```
+
+At line ~368, change `_communicationOptions.ConnectionString` → `_redisConnectionString`. Update the `` doc to `The application configuration.`. Add `using Microsoft.Extensions.Configuration;`. Remove `using Faemiyah.BtDamageResolver.Common.Options;` (keep `Microsoft.Extensions.Options` — still used for `IOptions`).
+
+- [ ] **Step 7: Build the client**
+
+Run (from the client repo root): `dotnet build src/BlazorServer/BlazorServer.csproj --nologo`
+Expected: Build succeeded, 0 errors. (The client compiles against `Common` NuGet 0.0.447, which still contains `CommunicationOptions`; the client simply no longer references it.)
+
+- [ ] **Step 8: Commit (in the client repo)**
+
+```bash
+git add -A
+git commit -m "refactor: client uses native appsettings + ConnectionStrings"
+```
+
+---
+
+## Final verification (manual gate — owner)
+
+- [ ] Code repo: `dotnet build --nologo` and `dotnet test tests/Tests/Tests.csproj --nologo` both green.
+- [ ] Client repo: `dotnet build src/BlazorServer/BlazorServer.csproj --nologo` green.
+- [ ] `git grep -n "orleansdb"` clean in code + infra repos.
+- [ ] Build images (`./refresh.sh` or `refresh.bat`) and `docker compose -f infra/docker-compose.yml up`. Confirm: silo connects to Redis + Postgres, client connects, DB logging writes. Note the Postgres volume is reset (empty DB; schema recreated by the custom image on first init).
+```
diff --git a/BtDamageResolver/docs/superpowers/specs/2026-06-02-native-appsettings-config-refactor-design.md b/BtDamageResolver/docs/superpowers/specs/2026-06-02-native-appsettings-config-refactor-design.md
new file mode 100644
index 00000000..349b1774
--- /dev/null
+++ b/BtDamageResolver/docs/superpowers/specs/2026-06-02-native-appsettings-config-refactor-design.md
@@ -0,0 +1,217 @@
+# Native appsettings + ConnectionStrings refactor
+
+**Date:** 2026-06-02
+**Status:** Approved (design)
+**Scope:** Silo, BlazorServer client, DataImporter, DataExporter, Common, Services, infra
+
+## Goal
+
+Replace the custom configuration-loading scheme (`ConfigurationUtilities.GetConfiguration`)
+with the native .NET host configuration chain; replace the `CommunicationOptions` wrapper class
+with the native `ConnectionStrings` block; move the Postgres connection string into
+`ConnectionStrings` as well; remove the now-dead `RESOLVER_ENVIRONMENT` / Orleans port-padding
+code; and rename the misleadingly-named `orleansdb` container to `resolverpostgres`.
+
+The custom loader was originally written to guarantee the source order
+`config file -> environment-override file -> environment variables`. This is **already** the
+default order produced by `Host.CreateDefaultBuilder()` / `Host.CreateApplicationBuilder()`, with
+environment variables taking precedence over JSON values. The custom loader is therefore redundant.
+
+## Background / current state
+
+- `ConfigurationUtilities.GetConfiguration(settingsFile)` (`src/Common/ConfigurationUtilities.cs:88-118`)
+ builds: base JSON -> `{file}.{RESOLVER_ENVIRONMENT}.json` (mandatory if the env var is set; throws
+ otherwise) -> `AddEnvironmentVariables()`.
+- **Silo** (`src/Silo/Program.cs`): `Host.CreateDefaultBuilder()` with custom config injected via
+ `.ConfigureAppConfiguration((_, c) => c.AddConfiguration(GetConfiguration("SiloSettings.json")))`.
+- **Client** (`BtDamageResolverClient/src/BlazorServer/Startup.cs`): `ConfigureServices` is `static`,
+ ignores the injected `Configuration`, and calls `GetConfiguration("CommunicationSettings.json")`.
+- **Tools** (`tools/DataImporter`, `tools/DataExporter`): each has its own private `GetConfiguration`
+ that calls only `AddJsonFile(...)` -- **no** `AddEnvironmentVariables()`.
+- `CommunicationOptions` holds a Redis `ConnectionString`. `FaemiyahClusterOptions` holds `Invariant`
+ + a Postgres `ConnectionString`.
+
+### Package topology (important)
+
+- The **client** (`BlazorServer.csproj`) consumes `Common`/`Api` via **NuGet** `PackageReference`
+ (v0.0.447), NOT project references. The **silo, services, tools** use project references.
+- Consequence: editing/deleting types in `Common` source does not break the client build until a new
+ package is published (owner's separate NuGet workflow). The client must only stop *referencing*
+ removed types in its own code.
+
+### Known issues surfaced during analysis
+
+1. **Tool env-var override is dead today.** The tools never call `AddEnvironmentVariables()`, so the
+ `CommunicationOptions__ConnectionString` env var for `resolverdataimporter` (`docker-compose.yml:45`)
+ is silently ignored; the literal `password=PASSWORD` placeholder is used. This refactor fixes it.
+2. **DataExporter reads the wrong file.** `DataExporter.cs:43` calls `GetConfiguration("DataImporterSettings.json")`
+ (the importer's file) for its Redis connection, while `Program.cs:32` reads `DataExporterSettings.json`
+ for logging. The exporter's `ClusterOptions`/Postgres block is therefore dead. Going native (each tool
+ reads its own `appsettings.json`) fixes the cross-file read; the dead Postgres block is dropped.
+3. **Dockerfiles do not reference settings filenames**; renaming settings files needs no Dockerfile changes.
+
+## Design
+
+### 1. Configuration loading
+
+Delete `ConfigurationUtilities.GetConfiguration`.
+
+- **Silo** (`src/Silo/Program.cs`): remove the `.ConfigureAppConfiguration(... AddConfiguration ...)` line
+ and the top-level `GetConfiguration` call. Read config from `context.Configuration` inside a
+ `.UseOrleans((context, siloBuilder) => ...)` delegate. Default chain:
+ `appsettings.json -> appsettings.{DOTNET_ENVIRONMENT}.json -> env vars`.
+- **Client** (`Startup.cs`): make `ConfigureServices` an **instance** method binding off the injected
+ `Configuration`. Remove the `GetConfiguration("CommunicationSettings.json")` call.
+- **Tools**: replace each private `GetConfiguration` with `Host.CreateApplicationBuilder(args).Configuration`
+ (projects already reference `Microsoft.Extensions.Hosting`). Native chain, no custom code, and each tool
+ reads its own `appsettings.json` (fixes finding #2).
+
+### 2. Remove RESOLVER_ENVIRONMENT and the dead port-padding path
+
+In `ConfigurationUtilities`:
+- Delete `GetEnvironmentName`, the `EnvironmentName` constant, and `DefaultDebugEnvironmentName`/`DefaultEnvironmentName`.
+- Simplify `GetSiloPortConfigurationFromEnvironment`: drop the `portPad` parameter and the Debug-padding block;
+ keep the `ORLEANS_CLIENTPORT`/`ORLEANS_SILOPORT` reads and defaults.
+- Remove now-unused `using` directives (`System.IO`, `Microsoft.Extensions.Configuration`).
+
+`GetSiloPortConfigurationFromEnvironment()` is already called with no argument (`Program.cs:114`).
+After this, `RESOLVER_ENVIRONMENT` no longer appears anywhere. Remaining in `ConfigurationUtilities`:
+`GetSiloPortConfigurationFromEnvironment`, `GetHostIp`, and the JSON-serializer helpers.
+
+### 3. CommunicationOptions -> ConnectionStrings:Redis
+
+- Delete `src/Common/Options/CommunicationOptions.cs` and `Settings.CommunicationOptionsBlockName`.
+- JSON: `"CommunicationOptions": { "ConnectionString": "redis:..." }` -> `"ConnectionStrings": { "Redis": "redis:..." }`.
+- Consumers switch from `IOptions` to `IConfiguration.GetConnectionString("Redis")`:
+ - `src/Silo/Program.cs:227` `GetRedisEntityRepository` -- resolve `IConfiguration` from `serviceProvider`.
+ - `src/Services/CommunicationService.cs:35,44` -- inject `IConfiguration`.
+ - `BtDamageResolverClient/src/BlazorServer/Communication/ResolverCommunicator.cs:40,368` -- inject `IConfiguration`.
+ - `tools/DataImporter/DataImporter.cs:41`, `tools/DataExporter/DataExporter.cs:44` -- read via `GetConnectionString`.
+- Remove the now-unused `services.Configure(...)` registrations and `using` directives.
+
+### 4. Postgres connection string -> ConnectionStrings:Postgres
+
+- JSON: move the Postgres string from `ClusterOptions:ConnectionString` to `ConnectionStrings:Postgres`.
+ `ClusterOptions` retains only `Invariant`.
+- **Keep `FaemiyahClusterOptions` unchanged** (still `Invariant` + `ConnectionString`) to avoid changing the
+ published `Common` package contract and to leave `Services` (`LoggingService`/`LoggingRepository`) untouched.
+ In the Silo DI, hydrate the options object:
+ ```csharp
+ services.Configure(configuration.GetSection(Settings.ClusterOptionsBlockName)); // Invariant
+ services.PostConfigure(o => o.ConnectionString = configuration.GetConnectionString("Postgres"));
+ ```
+- Silo clustering/grain storage read `Invariant` from the section and the connection string from
+ `GetConnectionString("Postgres")`. `LoggingService`/`LoggingRepository` are unchanged (still receive a
+ fully-populated `FaemiyahClusterOptions` via `IOptions<>`).
+- Connection-string key name: **`Postgres`**.
+
+### 5. Settings.cs documentation fixes
+
+`ClusterOptionsBlockName` and `LoggingOptionsBlockName` XML docs both wrongly say "RabbitMQ"; correct them.
+(`CommunicationOptionsBlockName` is deleted in section 3.)
+
+### 6. Rename orleansdb -> resolverpostgres (infra)
+
+The Postgres container is misnamed (it backs Orleans clustering, grain storage, **and** DB logging).
+Rename everywhere:
+- `infra/docker-compose.yml`: service key, `container_name`, `hostname`, `image: orleansdb:latest`
+ -> `resolverpostgres:latest`, both `depends_on: orleansdb` entries, the volume mount and the volume
+ declaration `orleansdb-data` -> `resolverpostgres-data`, and `Host=orleansdb` -> `Host=resolverpostgres`
+ in the Postgres connection string env var.
+- `infra/grafana/datasources/datasource.yml`: `url: orleansdb:5432` -> `resolverpostgres:5432`. The datasource
+ `name: Orleansdb` is left unchanged unless verified safe to rename (Grafana dashboards may reference the
+ datasource by name).
+- `refresh.sh` and `refresh.bat`: `docker build --tag orleansdb ...` -> `--tag resolverpostgres ...`.
+- `Host=orleansdb` -> `Host=resolverpostgres` in the Silo `appsettings.json` default.
+
+**Volume:** renamed to `resolverpostgres-data` (accepted). Next `docker compose up` starts with an empty
+Postgres DB; the custom Postgres image (`infra/postgresql/Dockerfile`) recreates the Orleans schema on first
+init. Persisted Orleans grain/session state and logs are lost; Redis reference data is unaffected.
+
+### 7. File renames + environment mapping
+
+| Old | New |
+|---|---|
+| `src/Silo/SiloSettings.json` / `.Debug` / `.Release` | `appsettings.json` / `.Debug` / `.Release` |
+| `tools/DataImporter/DataImporterSettings.*` | `appsettings.*` |
+| `tools/DataExporter/DataExporterSettings.*` | `appsettings.*` (dead Postgres block dropped) |
+| Client `CommunicationSettings.json` | merged into existing `appsettings.json` |
+| Client `CommunicationSettings.Release.json` | becomes `appsettings.Release.json` |
+| Client `CommunicationSettings.Debug.json` | deleted (client uses `ASPNETCORE_ENVIRONMENT`) |
+
+Environment variable per process:
+- **Silo + tools:** `RESOLVER_ENVIRONMENT` -> `DOTNET_ENVIRONMENT`.
+- **Client:** already `ASPNETCORE_ENVIRONMENT`; drop the redundant `RESOLVER_ENVIRONMENT`.
+
+`.csproj` copy directives:
+- `Silo.csproj`, `tools/*/*.csproj` (console SDK): rename `None Update` entries to `appsettings*.json`.
+- `BlazorServer.csproj` (Web SDK): `appsettings.json` auto-copies; remove the `CommunicationSettings.*`
+ entries; add an explicit copy for `appsettings.Release.json`.
+
+### 8. docker-compose.yml env vars
+
+- `resolver`: `RESOLVER_ENVIRONMENT` -> `DOTNET_ENVIRONMENT`; `CommunicationOptions__ConnectionString`
+ -> `ConnectionStrings__Redis`; `ClusterOptions__ConnectionString` -> `ConnectionStrings__Postgres`
+ (with `Host=resolverpostgres`).
+- `resolverclient`: remove `RESOLVER_ENVIRONMENT`; `CommunicationOptions__ConnectionString` -> `ConnectionStrings__Redis`.
+- `resolverdataimporter`: `RESOLVER_ENVIRONMENT` -> `DOTNET_ENVIRONMENT`; `CommunicationOptions__ConnectionString` -> `ConnectionStrings__Redis`.
+
+### Resulting JSON shapes
+
+Silo `appsettings.json`:
+```json
+{
+ "LoggingOptions": { "LogToConsole": true, "LogToDatabase": true, "LogLevel": "Information", "LogLevelOrleans": "Warning", "LoggingIntervalMilliseconds": 15000, "SendDetailedErrorsToClient": true },
+ "ClusterOptions": { "Invariant": "Npgsql" },
+ "ConnectionStrings": {
+ "Postgres": "User ID=USERNAME;Password=PASSWORD;Host=resolverpostgres;Port=5432;Database=BtDamageResolver;SSL Mode=Disable;",
+ "Redis": "redis:6379,password=PASSWORD"
+ },
+ "CompressionOptions": { "Provider": "Brotli", "Quality": 4 }
+}
+```
+
+Client `appsettings.json` (merge of existing + `CommunicationSettings.json`):
+```json
+{
+ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } },
+ "AllowedHosts": "*",
+ "LoggingOptions": { "LogToConsole": true, "LogLevel": "Information", "LogLevelOrleans": "Warning" },
+ "ConnectionStrings": { "Redis": "redis:6379,password=PASSWORD" },
+ "CompressionOptions": { "Provider": "Brotli", "Quality": 4 }
+}
+```
+
+Tools `appsettings.json` (both importer and exporter):
+```json
+{
+ "LoggingOptions": { "LogToConsole": true, "LogLevel": "Information", "LogLevelOrleans": "Warning" },
+ "ConnectionStrings": { "Redis": "redis:6379,password=PASSWORD" }
+}
+```
+
+## Testing
+
+- **New test** (`tests/Tests`): build a `ConfigurationBuilder` over a sample `appsettings.json` fixture and
+ assert (a) `GetConnectionString("Redis")` resolves the JSON value, and (b) a `ConnectionStrings__Redis`
+ environment variable **overrides** the JSON value (locks in the precedence behavior that motivated the
+ original loader).
+- **Build** Silo, BlazorServer, DataImporter, DataExporter, Common, Services, Tests.
+- **Manual gate (owner):** `docker compose up` smoke test for real Redis/Postgres connectivity (note the
+ Postgres volume reset).
+
+## Risks
+
+- Missing a compose env-var rename -> Redis/Postgres silently uses the `PASSWORD` placeholder. Mitigated by
+ the rename tables (sections 7-8) and the precedence test.
+- A missed `orleansdb` reference -> resolver/grafana cannot reach Postgres. Mitigated by the section 6 file list.
+- Web SDK not copying `appsettings.Release.json` -> empty override at runtime (harmless); mitigated by the
+ explicit copy directive.
+- `FaemiyahClusterOptions` connection-string source becomes indirect (hydrated via `PostConfigure`); mitigated
+ by an explanatory comment at the registration site.
+
+## Out of scope
+
+- Splitting the single Postgres DB into separate clustering/storage/logging databases.
+- Renaming `ConfigurationUtilities` (now purely Orleans-port/IP + JSON helpers).
+- Renaming the Grafana datasource display name `Orleansdb` (cosmetic; deferred to avoid dashboard breakage).
diff --git a/BtDamageResolver/exportdata.sh b/BtDamageResolver/exportdata.sh
deleted file mode 100644
index c3b20b97..00000000
--- a/BtDamageResolver/exportdata.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-dotnet DataExporter.dll export -f ./data/
diff --git a/BtDamageResolver/importdata.bat b/BtDamageResolver/importdata.bat
deleted file mode 100644
index 9a53aca4..00000000
--- a/BtDamageResolver/importdata.bat
+++ /dev/null
@@ -1,3 +0,0 @@
-cd tools\DataImporter\bin\Debug\net10.0
-DataImporter.exe import -s *.json -f ..\..\..\..\..\data
-cd ..\..\..\..\..
diff --git a/BtDamageResolver/importdata.sh b/BtDamageResolver/importdata.sh
deleted file mode 100644
index 20ad1726..00000000
--- a/BtDamageResolver/importdata.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-dotnet DataImporter.dll import -s '*.json' -f ./data/
diff --git a/BtDamageResolver/src/ActorInterfaces/Repositories/IAmmoRepository.cs b/BtDamageResolver/src/ActorInterfaces/Repositories/IAmmoRepository.cs
deleted file mode 100644
index 4be6da0c..00000000
--- a/BtDamageResolver/src/ActorInterfaces/Repositories/IAmmoRepository.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Faemiyah.BtDamageResolver.ActorInterfaces.Repositories.Prototypes;
-using Faemiyah.BtDamageResolver.Api.Entities.RepositoryEntities;
-using Orleans;
-
-namespace Faemiyah.BtDamageResolver.ActorInterfaces.Repositories;
-
-///
-/// Interface for an Ammo Repository Actor.
-///
-public interface IAmmoRepository : IGrainWithIntegerKey, IExternalRepositoryActorBase
-{
-}
\ No newline at end of file
diff --git a/BtDamageResolver/src/ActorInterfaces/Repositories/IArcDiagramRepository.cs b/BtDamageResolver/src/ActorInterfaces/Repositories/IArcDiagramRepository.cs
deleted file mode 100644
index 21550001..00000000
--- a/BtDamageResolver/src/ActorInterfaces/Repositories/IArcDiagramRepository.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Faemiyah.BtDamageResolver.ActorInterfaces.Repositories.Prototypes;
-using Faemiyah.BtDamageResolver.Api.Entities.RepositoryEntities;
-using Orleans;
-
-namespace Faemiyah.BtDamageResolver.ActorInterfaces.Repositories;
-
-///
-/// Interface for an Ammo Repository Actor.
-///
-public interface IArcDiagramRepository : IGrainWithIntegerKey, IExternalRepositoryActorBase
-{
-}
\ No newline at end of file
diff --git a/BtDamageResolver/src/ActorInterfaces/Repositories/IClusterTableRepository.cs b/BtDamageResolver/src/ActorInterfaces/Repositories/IClusterTableRepository.cs
deleted file mode 100644
index a49c1886..00000000
--- a/BtDamageResolver/src/ActorInterfaces/Repositories/IClusterTableRepository.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Faemiyah.BtDamageResolver.ActorInterfaces.Repositories.Prototypes;
-using Faemiyah.BtDamageResolver.Api.Entities.RepositoryEntities;
-using Orleans;
-
-namespace Faemiyah.BtDamageResolver.ActorInterfaces.Repositories;
-
-///
-/// Interface for a ClusterTable Repository Actor.
-///
-public interface IClusterTableRepository : IGrainWithIntegerKey, IExternalRepositoryActorBase
-{
-}
\ No newline at end of file
diff --git a/BtDamageResolver/src/ActorInterfaces/Repositories/ICriticalDamageTableRepository.cs b/BtDamageResolver/src/ActorInterfaces/Repositories/ICriticalDamageTableRepository.cs
deleted file mode 100644
index a5d218ec..00000000
--- a/BtDamageResolver/src/ActorInterfaces/Repositories/ICriticalDamageTableRepository.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Faemiyah.BtDamageResolver.ActorInterfaces.Repositories.Prototypes;
-using Faemiyah.BtDamageResolver.Api.Entities.RepositoryEntities;
-using Orleans;
-
-namespace Faemiyah.BtDamageResolver.ActorInterfaces.Repositories;
-
-///
-/// Interface for a CriticalDamageTable Repository Actor.
-///
-public interface ICriticalDamageTableRepository : IGrainWithIntegerKey, IExternalRepositoryActorBase
-{
-}
\ No newline at end of file
diff --git a/BtDamageResolver/src/ActorInterfaces/Repositories/IPaperDollRepository.cs b/BtDamageResolver/src/ActorInterfaces/Repositories/IPaperDollRepository.cs
deleted file mode 100644
index 8b0cdf37..00000000
--- a/BtDamageResolver/src/ActorInterfaces/Repositories/IPaperDollRepository.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Faemiyah.BtDamageResolver.ActorInterfaces.Repositories.Prototypes;
-using Faemiyah.BtDamageResolver.Api.Entities.RepositoryEntities;
-using Orleans;
-
-namespace Faemiyah.BtDamageResolver.ActorInterfaces.Repositories;
-
-///
-/// Interface for a PaperDoll Repository Actor.
-///
-public interface IPaperDollRepository : IGrainWithIntegerKey, IExternalRepositoryActorBase
-{
-}
\ No newline at end of file
diff --git a/BtDamageResolver/src/ActorInterfaces/Repositories/IUnitRepository.cs b/BtDamageResolver/src/ActorInterfaces/Repositories/IUnitRepository.cs
deleted file mode 100644
index 4dfc95af..00000000
--- a/BtDamageResolver/src/ActorInterfaces/Repositories/IUnitRepository.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Faemiyah.BtDamageResolver.ActorInterfaces.Repositories.Prototypes;
-using Faemiyah.BtDamageResolver.Api.Entities.RepositoryEntities;
-using Orleans;
-
-namespace Faemiyah.BtDamageResolver.ActorInterfaces.Repositories;
-
-///
-/// Interface for an Unit Repository Actor.
-///
-public interface IUnitRepository : IGrainWithIntegerKey, IExternalRepositoryActorBase
-{
-}
\ No newline at end of file
diff --git a/BtDamageResolver/src/ActorInterfaces/Repositories/IWeaponRepository.cs b/BtDamageResolver/src/ActorInterfaces/Repositories/IWeaponRepository.cs
deleted file mode 100644
index fc2571fe..00000000
--- a/BtDamageResolver/src/ActorInterfaces/Repositories/IWeaponRepository.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Faemiyah.BtDamageResolver.ActorInterfaces.Repositories.Prototypes;
-using Faemiyah.BtDamageResolver.Api.Entities.RepositoryEntities;
-using Orleans;
-
-namespace Faemiyah.BtDamageResolver.ActorInterfaces.Repositories;
-
-///
-/// Interface for a Weapon Repository Actor.
-///
-public interface IWeaponRepository : IGrainWithIntegerKey, IExternalRepositoryActorBase
-{
-}
\ No newline at end of file
diff --git a/BtDamageResolver/src/Actors/Cryptography/FaemiyahPasswordHasher.cs b/BtDamageResolver/src/Actors/Cryptography/FaemiyahPasswordHasher.cs
index 6396cf7b..9a7f5880 100644
--- a/BtDamageResolver/src/Actors/Cryptography/FaemiyahPasswordHasher.cs
+++ b/BtDamageResolver/src/Actors/Cryptography/FaemiyahPasswordHasher.cs
@@ -1,37 +1,37 @@
using System.Security.Cryptography;
-using System.Text;
namespace Faemiyah.BtDamageResolver.Actors.Cryptography;
///
-/// SHA-512 password hasher with a random salt.
-/// Uses the static method, which is stateless and safe for concurrent
-/// use without any locking — each call gets its own internal hash context from the runtime.
+/// PBKDF2 (Rfc2898) password hasher with a random salt.
+/// Uses the static
+/// method, which is stateless and safe for concurrent use without any locking.
/// Uses for constant-time comparison
/// to prevent timing-oracle attacks.
-/// Note: consider upgrading to PBKDF2 or Argon2 for stronger brute-force resistance in the future.
///
+///
+/// The stored hash format is not self-describing (no embedded algorithm/iteration tag), so hashes
+/// produced by a different algorithm or iteration count will simply fail to verify.
+///
public class FaemiyahPasswordHasher : IHasher
{
private const int SaltLength = 32;
+ private const int HashLength = 32;
+ private const int Iterations = 100000;
+ private static readonly HashAlgorithmName HashAlgorithm = HashAlgorithmName.SHA256;
///
public (byte[] Hash, byte[] Salt) Hash(string password)
{
var salt = RandomNumberGenerator.GetBytes(SaltLength);
- var hash = SHA512.HashData(GetSaltedBytes(password, salt));
+ var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithm, HashLength);
return (hash, salt);
}
///
public bool Verify(string password, byte[] salt, byte[] referenceHash)
{
- var hash = SHA512.HashData(GetSaltedBytes(password, salt));
+ var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithm, HashLength);
return CryptographicOperations.FixedTimeEquals(hash, referenceHash);
}
-
- private static byte[] GetSaltedBytes(string password, byte[] salt)
- {
- return [.. Encoding.UTF8.GetBytes(password), .. salt];
- }
}
\ No newline at end of file
diff --git a/BtDamageResolver/src/Actors/Extensions/TaskExtensions.cs b/BtDamageResolver/src/Actors/Extensions/TaskExtensions.cs
new file mode 100644
index 00000000..0981d2fe
--- /dev/null
+++ b/BtDamageResolver/src/Actors/Extensions/TaskExtensions.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Faemiyah.BtDamageResolver.Actors.Extensions;
+
+///
+/// Extensions for .
+///
+public static class TaskExtensions
+{
+ ///
+ /// Fire-and-forget a task, logging any exception. Use as a safer alternative to
+ /// Task.Ignore() in cases where a grain call must not be awaited (e.g. to avoid
+ /// deadlock) but failures should still be surfaced in the log.
+ ///
+ /// The task to observe.
+ /// The logger to write failures to.
+ /// The structured logging message template.
+ /// The arguments to fill the message template.
+ public static void LogAndForget(this Task task, ILogger logger, string messageTemplate, params object[] args)
+ {
+ _ = ObserveAsync(task, logger, messageTemplate, args);
+ }
+
+ private static async Task ObserveAsync(Task task, ILogger logger, string messageTemplate, object[] args)
+ {
+ try
+ {
+ await task;
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, messageTemplate, args);
+ }
+ }
+}
diff --git a/BtDamageResolver/src/Actors/GameActor.Tools.cs b/BtDamageResolver/src/Actors/GameActor.Tools.cs
index b45f8499..f7ef8446 100644
--- a/BtDamageResolver/src/Actors/GameActor.Tools.cs
+++ b/BtDamageResolver/src/Actors/GameActor.Tools.cs
@@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using Faemiyah.BtDamageResolver.ActorInterfaces;
+using Faemiyah.BtDamageResolver.Actors.Extensions;
using Microsoft.Extensions.Logging;
using Orleans;
@@ -36,8 +37,10 @@ public async Task KickPlayer(string askingPlayerId, string playerId)
// Must NOT be awaited: the player actor may currently be waiting on this grain,
// which would cause a deadlock. When it eventually processes LeaveGame it will
// call back into GameActor.LeaveGame(), which is idempotent and handles the
- // already-removed case gracefully.
- GrainFactory.GetGrain(playerId).LeaveGame().Ignore();
+ // already-removed case gracefully. Use LogAndForget so failures surface in the log
+ // instead of being silently swallowed by .Ignore().
+ GrainFactory.GetGrain(playerId).LeaveGame()
+ .LogAndForget(_logger, "Failed to notify kicked player {PlayerId} in Game {GameId}.", playerId, this.GetPrimaryKeyString());
_logger.LogInformation("In Game {GameId}, Player {PlayerId} successfully kicked player {PlayerToKickId}.", this.GetPrimaryKeyString(), askingPlayerId, playerId);
diff --git a/BtDamageResolver/src/Actors/GameActor.TurnLogic.cs b/BtDamageResolver/src/Actors/GameActor.TurnLogic.cs
index 0bdde447..8eaf249e 100644
--- a/BtDamageResolver/src/Actors/GameActor.TurnLogic.cs
+++ b/BtDamageResolver/src/Actors/GameActor.TurnLogic.cs
@@ -94,7 +94,7 @@ private static void ProcessUnitHeat(List damageReports, UnitEntry
}
}
- private async Task CheckGameStateUpdateEvents(List updatedUnits = null)
+ private async Task CheckGameStateUpdateEvents(List updatedUnits = null, bool refreshGameEntry = false)
{
try
{
@@ -126,21 +126,26 @@ private async Task CheckGameStateUpdateEvents(List updatedUnits = null)
// Log update to permanent store
await _loggingServiceClient.LogGameAction(DateTime.UtcNow, this.GetPrimaryKeyString(), GameActionType.Update, 1);
- // Remark game existence
- await GrainFactory.GetGameEntryRepository().AddOrUpdate(
- new GameEntry
- {
- Name = this.GetPrimaryKeyString(),
- PasswordProtected = _gameActorState.State.PasswordHash != null,
- Players = _gameActorState.State.PlayerStates.Count,
- TimeStamp = DateTime.UtcNow
- });
+ // Remark game existence only on events that change lobby-relevant state (join/leave/turn).
+ // Per-keystroke player-state edits do not need to refresh the lobby entry, which would otherwise
+ // trigger a broadcast storm via GameEntryRepository.AddOrUpdate -> Distribute().
+ if (refreshGameEntry || fireEventHappened)
+ {
+ await GrainFactory.GetGameEntryRepository().AddOrUpdate(
+ new GameEntry
+ {
+ Name = this.GetPrimaryKeyString(),
+ PasswordProtected = _gameActorState.State.PasswordHash != null,
+ Players = _gameActorState.State.PlayerStates.Count,
+ TimeStamp = DateTime.UtcNow
+ });
+ }
}
catch (Exception ex)
{
- // I really want to clearly log any error in turn logic so I can fix it.
_logger.LogError(ex, "Game {GameId} experienced a critical failure while running game state update events.", this.GetPrimaryKeyString());
- throw new InvalidOperationException($"Error while running game state events for game {this.GetPrimaryKeyString()}", ex);
+
+ throw new InvalidOperationException($"Game {this.GetPrimaryKeyString()} experienced a critical failure while running game state update events.", ex);
}
}
@@ -302,10 +307,10 @@ private async Task ProcessUnitTargetNumbers(UnitEntry unitEn
var attackLog = new AttackLog();
var isPrimaryTarget = weaponBay.FiringSolution.Target == primaryTarget;
- var (targetNumber, rangeBracket) = await logicUnitAttacker.ResolveHitModifier(attackLog, logicUnitDefender, primaryTargetArc, isPrimaryTarget, weaponBay, weaponEntry);
+ var (targetNumber, rangeBracket) = logicUnitAttacker.ResolveHitModifier(attackLog, logicUnitDefender, primaryTargetArc, isPrimaryTarget, weaponBay, weaponEntry);
- var (ammoEstimate, ammoMax) = await logicUnitAttacker.ProjectAmmo(targetNumber, rangeBracket, weaponEntry);
- var (heatEstimate, heatMax) = await logicUnitAttacker.ProjectHeat(targetNumber, rangeBracket, weaponEntry);
+ var (ammoEstimate, ammoMax) = logicUnitAttacker.ProjectAmmo(targetNumber, rangeBracket, weaponEntry);
+ var (heatEstimate, heatMax) = logicUnitAttacker.ProjectHeat(targetNumber, rangeBracket, weaponEntry);
var weaponAmmoCombinedString = string.IsNullOrEmpty(weaponEntry.Ammo) ? $"{weaponEntry.WeaponName}" : $"{weaponEntry.WeaponName} {weaponEntry.Ammo}";
targetNumberUpdate.AmmoEstimate.AddIfNotZero(weaponAmmoCombinedString, ammoEstimate);
diff --git a/BtDamageResolver/src/Actors/GameActor.cs b/BtDamageResolver/src/Actors/GameActor.cs
index 6659cbd1..8fc10506 100644
--- a/BtDamageResolver/src/Actors/GameActor.cs
+++ b/BtDamageResolver/src/Actors/GameActor.cs
@@ -64,6 +64,15 @@ public Task IsUnitInGame(Guid unitId)
}
///
+ ///
+ /// Stores the incoming by reference. This is safe because
+ /// Orleans serialises Faemiyah types via the JSON serializer registered in
+ /// Silo/Program.cs, so every cross-grain call (including this one) produces a fresh
+ /// deep copy at the boundary. If that serializer registration is ever changed to a by-reference
+ /// path for intra-silo calls, this method (and GetGameState, which mutates timestamps
+ /// on stored states) would need to copy defensively to avoid aliasing
+ /// state.
+ ///
public async Task SendPlayerState(string sendingPlayerId, PlayerState playerState, List unitIds)
{
// Do not accept player states from players who are not in the game.
@@ -167,7 +176,7 @@ public async Task JoinGame(string playerId, string password)
_logger.LogInformation("In Game {GameId}, Player {PlayerId} successfully connected to the game.", this.GetPrimaryKeyString(), playerId);
- await CheckGameStateUpdateEvents();
+ await CheckGameStateUpdateEvents(refreshGameEntry: true);
// Log logins to permanent store
await _loggingServiceClient.LogGameAction(DateTime.UtcNow, this.GetPrimaryKeyString(), GameActionType.Login, 0);
@@ -187,7 +196,7 @@ public async Task LeaveGame(string playerId)
if (_gameActorState.State.PlayerStates.Remove(playerId) || _gameActorState.State.PlayerIds.Remove(playerId))
{
_gameActorState.State.TimeStamp = DateTime.UtcNow;
- await CheckGameStateUpdateEvents();
+ await CheckGameStateUpdateEvents(refreshGameEntry: true);
_logger.LogInformation("In Game {GameId}, Player {PlayerId} successfully disconnected.", this.GetPrimaryKeyString(), playerId);
@@ -226,26 +235,17 @@ private void CheckForPlayerCountEvents()
private GameState GetGameState(bool markStateAsNew)
{
- var timeStampNow = DateTime.UtcNow;
-
- var gameState = new GameState
+ // markStateAsNew only affects the top-level GameState.TimeStamp. Per-player TimeStamps
+ // are already updated to TurnTimeStamp by CheckForFireEvent when a fire event happens,
+ // so this method does not mutate actor state.
+ return new GameState
{
AdminId = _gameActorState.State.AdminId,
GameId = this.GetPrimaryKeyString(),
Players = _gameActorState.State.PlayerStates,
- TimeStamp = markStateAsNew ? timeStampNow : _gameActorState.State.TimeStamp,
+ TimeStamp = markStateAsNew ? DateTime.UtcNow : _gameActorState.State.TimeStamp,
Turn = _gameActorState.State.Turn,
TurnTimeStamp = _gameActorState.State.TurnTimeStamp
};
-
- if (markStateAsNew)
- {
- foreach (var player in gameState.Players)
- {
- player.Value.TimeStamp = timeStampNow;
- }
- }
-
- return gameState;
}
}
\ No newline at end of file
diff --git a/BtDamageResolver/src/Actors/Logic/ExpressionSolver/Expression.cs b/BtDamageResolver/src/Actors/Logic/ExpressionSolver/Expression.cs
index 71d2b9fc..b8175bab 100644
--- a/BtDamageResolver/src/Actors/Logic/ExpressionSolver/Expression.cs
+++ b/BtDamageResolver/src/Actors/Logic/ExpressionSolver/Expression.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using Faemiyah.BtDamageResolver.Api;
@@ -10,6 +11,8 @@ namespace Faemiyah.BtDamageResolver.Actors.Logic.ExpressionSolver;
///
public class Expression
{
+ private static readonly HashSet _tokenChars = [.. Enum.GetValues().Select(x => (char)x)];
+ private static readonly string[] _functionNames = Enum.GetNames();
private readonly List _expressions;
private readonly ExpressionFunction _outerFunction;
private readonly IResolverRandom _random;
@@ -41,6 +44,16 @@ public Expression(decimal input)
_value = input;
}
+ ///
+ /// Is the given character a token.
+ ///
+ /// The character to test.
+ /// True if the character is a token, false otherwise.
+ public static bool IsToken(char input)
+ {
+ return _tokenChars.Contains(input);
+ }
+
///
/// Parses the expression.
///
@@ -133,7 +146,7 @@ private static (string SubExpression, ExpressionFunction FunctionType, string Re
var foundFunction = ExpressionFunction.None;
string functionScope = null;
- var functionName = Enum.GetNames().SingleOrDefault(input.StartsWith);
+ var functionName = Array.Find(_functionNames, input.StartsWith);
if (functionName != null)
{
@@ -176,7 +189,7 @@ private void CollapseOneStage(int position)
for (var diceIndex = 0; diceIndex < numDice; diceIndex++)
{
- value += _random.NextPlusOne(diceSize);
+ value += _random.DX(diceSize);
}
break;
@@ -207,7 +220,7 @@ private void CollapseOneStage(int position)
private void Construct(string input)
{
// If we start with a token, such as "-", put 0 before this so that it can be resolved properly
- if (input[0].IsToken())
+ if (Expression.IsToken(input[0]))
{
_expressions.Add(new Expression(_random, "0"));
_tokens.Add((Token)input[0]);
@@ -220,13 +233,13 @@ private void Construct(string input)
for (var ii = 0; ii < input.Length; ii++)
{
- if (input[ii].IsToken())
+ if (Expression.IsToken(input[ii]))
{
_tokens.Add((Token)input[ii]);
if (ii != 0)
{
- _expressions.Add(new Expression(decimal.Parse(input[..ii])));
+ _expressions.Add(new Expression(decimal.Parse(input[..ii], CultureInfo.InvariantCulture)));
}
input = input[(ii + 1)..];
@@ -255,7 +268,7 @@ private void Construct(string input)
if (nothingFound)
{
- _expressions.Add(new Expression(decimal.Parse(input)));
+ _expressions.Add(new Expression(decimal.Parse(input, CultureInfo.InvariantCulture)));
input = string.Empty;
}
}
diff --git a/BtDamageResolver/src/Actors/Logic/ExpressionSolver/ExpressionExtensions.cs b/BtDamageResolver/src/Actors/Logic/ExpressionSolver/ExpressionExtensions.cs
deleted file mode 100644
index 5d8d75a2..00000000
--- a/BtDamageResolver/src/Actors/Logic/ExpressionSolver/ExpressionExtensions.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System;
-using System.Linq;
-
-namespace Faemiyah.BtDamageResolver.Actors.Logic.ExpressionSolver;
-
-///
-/// Expression extensions.
-///
-public static class ExpressionExtensions
-{
- ///
- /// Is the given character a token.
- ///
- /// The character to test.
- /// True if the character is a token, false otherwise.
- public static bool IsToken(this char input)
- {
- if (Enum.GetValues().Any(t => input.Equals((char)t)))
- {
- return true;
- }
-
- return false;
- }
-}
\ No newline at end of file
diff --git a/BtDamageResolver/src/Actors/Logic/ExpressionSolver/MathExpression.cs b/BtDamageResolver/src/Actors/Logic/ExpressionSolver/MathExpression.cs
index 41d0cb8e..b6d55394 100644
--- a/BtDamageResolver/src/Actors/Logic/ExpressionSolver/MathExpression.cs
+++ b/BtDamageResolver/src/Actors/Logic/ExpressionSolver/MathExpression.cs
@@ -1,4 +1,5 @@
-using System;
+using System;
+using System.Collections.Concurrent;
using Faemiyah.BtDamageResolver.Api;
namespace Faemiyah.BtDamageResolver.Actors.Logic.ExpressionSolver;
@@ -9,6 +10,7 @@ namespace Faemiyah.BtDamageResolver.Actors.Logic.ExpressionSolver;
public class MathExpression : IMathExpression
{
private readonly IResolverRandom _random;
+ private readonly ConcurrentDictionary _resultCache = new();
///
/// Initializes a new instance of the class.
@@ -27,8 +29,19 @@ public int Parse(string expression)
return 0;
}
+ // Only deterministic expressions (no dice token) are safe to memoize.
+ if (!expression.Contains((char)Token.Dice))
+ {
+ return _resultCache.GetOrAdd(expression, Evaluate);
+ }
+
+ return Evaluate(expression);
+ }
+
+ private int Evaluate(string expression)
+ {
var expressionObject = new Expression(_random, expression);
return decimal.ToInt32(Math.Round(expressionObject.Parse(), MidpointRounding.AwayFromZero));
}
-}
\ No newline at end of file
+}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitAerospace.cs b/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitAerospace.cs
index a5f90af3..c3ebfbea 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitAerospace.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitAerospace.cs
@@ -12,7 +12,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations;
@@ -26,12 +25,11 @@ public abstract class LogicUnitAerospace : LogicUnit
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- protected LogicUnitAerospace(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
+ protected LogicUnitAerospace(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
@@ -97,7 +95,7 @@ protected override async Task ResolveCriticalHit(DamageReport damageReport, Guid
if (criticalThreatRoll > 7)
{
var aerospaceCriticalHitRoll = ResolverRandom.D26();
- damageReport.DamagePaperDoll.RecordCriticalDamage(location, damageOwnerId, inducingDamage, CriticalThreatType.DamageThreshold, criticalDamageTable.Mapping[aerospaceCriticalHitRoll]);
+ damageReport.DamagePaperDoll.RecordDamage(damageOwnerId, DamageEntry.CreateList(inducingDamage, location, CriticalThreatType.DamageThreshold, criticalDamageTable.Mapping[aerospaceCriticalHitRoll]));
damageReport.Log(new AttackLogEntry(AttackLogEntryType.DiceRoll, damageOwnerId, "Aerospace critical hit roll", aerospaceCriticalHitRoll));
damageReport.Log(new AttackLogEntry(AttackLogEntryType.Critical, damageOwnerId, string.Join(", ", criticalDamageTable.Mapping[aerospaceCriticalHitRoll].Select(c => c.ToString())), transformedDamage, location));
}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitAerospaceLarge.cs b/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitAerospaceLarge.cs
index 9ef7a910..659490e4 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitAerospaceLarge.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitAerospaceLarge.cs
@@ -11,7 +11,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations;
@@ -25,12 +24,11 @@ public abstract class LogicUnitAerospaceLarge : LogicUnitAerospace
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- protected LogicUnitAerospaceLarge(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, resolverRandom, unit)
+ protected LogicUnitAerospaceLarge(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
@@ -73,12 +71,12 @@ protected override async Task ResolveAmmo(DamageReport hitCalculationDamageRepor
{
if (!combatAction.ActionHappened)
{
- hitCalculationDamageReport.Log(new AttackLogEntry(AttackLogEntryType.Information, Unit.Id, $"Combat action was canceled for {combatAction.Weapon.Name} and no ammo will be expended"));
+ hitCalculationDamageReport.Log(new AttackLogEntry(AttackLogEntryType.Information, Unit.Id, $"Combat action was cancelled for {combatAction.Weapon.Name} and no ammo will be expended"));
return;
}
// All units may expend ammo when firing, so an unit type check is skipped
- // Bail out if ammo is not used by this weapon or the combat action was canceled
+ // Bail out if ammo is not used by this weapon or the combat action was cancelled
if (!combatAction.Weapon.UsesAmmo)
{
return;
@@ -88,7 +86,7 @@ protected override async Task ResolveAmmo(DamageReport hitCalculationDamageRepor
// I.e. when applying ammo usage, calculate it for all weapons in the bay.
foreach (var weaponEntry in combatAction.WeaponBay.Weapons)
{
- var weapon = await FormWeapon(weaponEntry);
+ var weapon = FormWeapon(weaponEntry);
SpendAmmo(hitCalculationDamageReport, weapon);
}
}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitMechBase.cs b/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitMechBase.cs
index b6bc67ff..8a996025 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitMechBase.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitMechBase.cs
@@ -4,11 +4,12 @@
using Faemiyah.BtDamageResolver.Actors.Logic.ExpressionSolver;
using Faemiyah.BtDamageResolver.Api;
using Faemiyah.BtDamageResolver.Api.ClientInterface.Repositories.Providers;
+using Faemiyah.BtDamageResolver.Api.Constants;
using Faemiyah.BtDamageResolver.Api.Entities;
+using Faemiyah.BtDamageResolver.Api.Entities.RepositoryEntities;
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations;
@@ -22,12 +23,11 @@ public abstract class LogicUnitMechBase : LogicUnit
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- protected LogicUnitMechBase(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
+ protected LogicUnitMechBase(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
@@ -59,12 +59,18 @@ public override PaperDollType GetPaperDollType()
}
///
- public override int GetStanceModifier(int distance)
+ public override int GetStanceModifier(Weapon weapon, int distance)
{
switch (Unit.Stance)
{
// Prone mechs are easier to hit at point-blank range and harder to hit at any other range
case Stance.Prone:
+ // Prone mechs cannot be punched in melee
+ if (!weapon.HasFeature(WeaponFeature.MeleeDfa, out var _) && weapon.AttackType is AttackType.Punch)
+ {
+ return LogicConstants.InvalidTargetNumber;
+ }
+
if (distance <= 1)
{
return -1;
@@ -81,6 +87,7 @@ public override bool IsBlockedByCover(Cover cover, Location location)
{
switch (cover)
{
+ case Cover.HullDown:
case Cover.Lower:
switch (location)
{
@@ -172,15 +179,22 @@ protected override async Task ResolveCriticalHit(DamageReport damageReport, Guid
// Simulate arms and legs being able to be blown off
if (criticalThreatRoll == 12 &&
- (location == Location.LeftArm || location == Location.LeftLeg ||
- location == Location.RightArm || location == Location.RightLeg))
+ (location == Location.Head ||
+ location == Location.LeftArm ||
+ location == Location.LeftLeg ||
+ location == Location.RightArm ||
+ location == Location.RightLeg ||
+ location == Location.FrontLeftLeg ||
+ location == Location.FrontRightLeg ||
+ location == Location.RearLeftLeg ||
+ location == Location.RearRightLeg))
{
- damageReport.DamagePaperDoll.RecordCriticalDamage(location, damageOwnerId, inducingDamage, CriticalThreatType.Normal, CriticalDamageType.BlownOff);
+ damageReport.DamagePaperDoll.RecordDamage(damageOwnerId, new DamageEntry(inducingDamage, location, CriticalThreatType.Normal, CriticalDamageType.BlownOff));
damageReport.Log(new AttackLogEntry(AttackLogEntryType.Critical, damageOwnerId, string.Join(", ", criticalDamageTable.Mapping[criticalThreatRoll].Select(c => c.ToString())), transformedDamage, location));
}
else if (criticalDamageTable.Mapping[criticalThreatRoll].Exists(c => c != CriticalDamageType.None))
{
- damageReport.DamagePaperDoll.RecordCriticalDamage(location, damageOwnerId, inducingDamage, CriticalThreatType.Normal, criticalDamageTable.Mapping[criticalThreatRoll]);
+ damageReport.DamagePaperDoll.RecordDamage(damageOwnerId, DamageEntry.CreateList(inducingDamage, location, CriticalThreatType.Normal, criticalDamageTable.Mapping[criticalThreatRoll]));
damageReport.Log(new AttackLogEntry(AttackLogEntryType.Critical, damageOwnerId, transformedDamage, location));
}
else
@@ -188,4 +202,10 @@ protected override async Task ResolveCriticalHit(DamageReport damageReport, Guid
damageReport.Log(new AttackLogEntry(AttackLogEntryType.Information, damageOwnerId, "Threat roll does not result in a critical hit"));
}
}
+
+ ///
+ protected override Location TransformLocation(Location location)
+ {
+ return Location.Structure;
+ }
}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitTrooper.cs b/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitTrooper.cs
index 8ef739ab..770a8261 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitTrooper.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitTrooper.cs
@@ -6,7 +6,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations;
@@ -20,12 +19,11 @@ public abstract class LogicUnitTrooper : LogicUnit
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- protected LogicUnitTrooper(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom random, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, random, unit)
+ protected LogicUnitTrooper(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom random, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, random, unit)
{
}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitVehicle.cs b/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitVehicle.cs
index dee1d16a..6df3fd04 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitVehicle.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitVehicle.cs
@@ -9,12 +9,11 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations;
///
-/// Abstract logic class for all ground vechicles and VTOLs.
+/// Abstract logic class for all ground vehicles and VTOLs.
///
public abstract class LogicUnitVehicle : LogicUnit
{
@@ -23,12 +22,11 @@ public abstract class LogicUnitVehicle : LogicUnit
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- protected LogicUnitVehicle(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
+ protected LogicUnitVehicle(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
@@ -38,6 +36,18 @@ public override bool CanTakeMotiveHits()
return true;
}
+ ///
+ public override int GetCoverModifier(Cover cover)
+ {
+ switch (cover)
+ {
+ case Cover.HullDown:
+ return 2;
+ default:
+ return 0;
+ }
+ }
+
///
public override Task TransformDamageBasedOnUnitType(DamageReport damageReport, Guid damageOwnerId, CombatAction combatAction, int damage)
{
@@ -85,7 +95,7 @@ protected override async Task ResolveCriticalHit(DamageReport damageReport, Guid
if (criticalDamageTable.Mapping[criticalThreatRoll].Exists(c => c != CriticalDamageType.None))
{
- damageReport.DamagePaperDoll.RecordCriticalDamage(location, damageOwnerId, inducingDamage, CriticalThreatType.Normal, criticalDamageTable.Mapping[criticalThreatRoll]);
+ damageReport.DamagePaperDoll.RecordDamage(damageOwnerId, DamageEntry.CreateList(inducingDamage, location, CriticalThreatType.Normal, criticalDamageTable.Mapping[criticalThreatRoll]));
damageReport.Log(new AttackLogEntry(AttackLogEntryType.Critical, damageOwnerId, string.Join(", ", criticalDamageTable.Mapping[criticalThreatRoll].Select(c => c.ToString())), transformedDamage, location));
}
else
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitVehicleGround.cs b/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitVehicleGround.cs
index cd0eb3cf..762f4c5f 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitVehicleGround.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/LogicUnitVehicleGround.cs
@@ -7,12 +7,11 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations;
///
-/// Abstract logic class for all ground vechicles and VTOLs.
+/// Abstract logic class for all ground vehicles and VTOLs.
///
public abstract class LogicUnitVehicleGround : LogicUnitVehicle
{
@@ -21,12 +20,11 @@ public abstract class LogicUnitVehicleGround : LogicUnitVehicle
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- protected LogicUnitVehicleGround(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom random, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, random, unit)
+ protected LogicUnitVehicleGround(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom random, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, random, unit)
{
}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceCapital.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceCapital.cs
index 8b50d787..13472444 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceCapital.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceCapital.cs
@@ -6,7 +6,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -20,12 +19,11 @@ public class LogicUnitAerospaceCapital : LogicUnitAerospaceLarge
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitAerospaceCapital(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, resolverRandom, unit)
+ public LogicUnitAerospaceCapital(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceDropshipAerodyne.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceDropshipAerodyne.cs
index 49a8953c..ccd3cd59 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceDropshipAerodyne.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceDropshipAerodyne.cs
@@ -5,7 +5,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -19,12 +18,11 @@ public class LogicUnitAerospaceDropshipAerodyne : LogicUnitAerospaceLarge
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitAerospaceDropshipAerodyne(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, resolverRandom, unit)
+ public LogicUnitAerospaceDropshipAerodyne(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceDropshipSpheroid.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceDropshipSpheroid.cs
index df1abd9d..5a0acb90 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceDropshipSpheroid.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceDropshipSpheroid.cs
@@ -5,7 +5,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -19,12 +18,11 @@ public class LogicUnitAerospaceDropshipSpheroid : LogicUnitAerospaceLarge
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitAerospaceDropshipSpheroid(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, resolverRandom, unit)
+ public LogicUnitAerospaceDropshipSpheroid(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceFighter.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceFighter.cs
index be340aa9..9aaa6d21 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceFighter.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitAerospaceFighter.cs
@@ -5,7 +5,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -19,12 +18,11 @@ public class LogicUnitAerospaceFighter : LogicUnitAerospace
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitAerospaceFighter(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, resolverRandom, unit)
+ public LogicUnitAerospaceFighter(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitBattleArmor.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitBattleArmor.cs
index 537a353b..c0818bf3 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitBattleArmor.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitBattleArmor.cs
@@ -7,10 +7,10 @@
using Faemiyah.BtDamageResolver.Api;
using Faemiyah.BtDamageResolver.Api.ClientInterface.Repositories.Providers;
using Faemiyah.BtDamageResolver.Api.Entities;
+using Faemiyah.BtDamageResolver.Api.Entities.RepositoryEntities;
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -24,12 +24,11 @@ public class LogicUnitBattleArmor : LogicUnitTrooper
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitBattleArmor(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, resolverRandom, unit)
+ public LogicUnitBattleArmor(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
@@ -40,7 +39,7 @@ public override PaperDollType GetPaperDollType()
}
///
- public override int GetStanceModifier(int distance)
+ public override int GetStanceModifier(Weapon weapon, int distance)
{
switch (Unit.Stance)
{
@@ -89,7 +88,7 @@ protected override List ResolveDamagePackets(DamageReport damageRe
///
protected override async Task ResolveTotalOutgoingDamage(DamageReport damageReport, ILogicUnit target, CombatAction combatAction)
{
- return await RapidFireWrapper(damageReport, target, combatAction, ResolveTotalOutgoingDamageInternalBattleArmor(damageReport, target, combatAction));
+ return await RapidFireWrapper(damageReport, target, combatAction, () => ResolveTotalOutgoingDamageInternalBattleArmor(damageReport, target, combatAction));
}
private async Task ResolveTotalOutgoingDamageInternalBattleArmor(DamageReport damageReport, ILogicUnit target, CombatAction combatAction)
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitBuilding.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitBuilding.cs
index 21be1f81..11c872dd 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitBuilding.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitBuilding.cs
@@ -8,7 +8,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -22,12 +21,11 @@ public class LogicUnitBuilding : LogicUnit
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitBuilding(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
+ public LogicUnitBuilding(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitInfantry.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitInfantry.cs
index b7004b9f..95194d6c 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitInfantry.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitInfantry.cs
@@ -7,10 +7,10 @@
using Faemiyah.BtDamageResolver.Api;
using Faemiyah.BtDamageResolver.Api.ClientInterface.Repositories.Providers;
using Faemiyah.BtDamageResolver.Api.Entities;
+using Faemiyah.BtDamageResolver.Api.Entities.RepositoryEntities;
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -24,12 +24,11 @@ public class LogicUnitInfantry : LogicUnitTrooper
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitInfantry(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom random, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, random, unit)
+ public LogicUnitInfantry(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom random, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, random, unit)
{
}
@@ -40,7 +39,7 @@ public override PaperDollType GetPaperDollType()
}
///
- public override int GetStanceModifier(int distance)
+ public override int GetStanceModifier(Weapon weapon, int distance)
{
switch (Unit.Stance)
{
@@ -149,7 +148,7 @@ protected override List ResolveDamagePackets(DamageReport damageRe
///
protected override async Task ResolveTotalOutgoingDamage(DamageReport damageReport, ILogicUnit target, CombatAction combatAction)
{
- return await RapidFireWrapper(damageReport, target, combatAction, ResolveTotalOutgoingDamageInternalInfantry(damageReport, target, combatAction));
+ return await RapidFireWrapper(damageReport, target, combatAction, () => ResolveTotalOutgoingDamageInternalInfantry(damageReport, target, combatAction));
}
private async Task ResolveTotalOutgoingDamageInternalInfantry(DamageReport damageReport, ILogicUnit target, CombatAction combatAction)
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitMech.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitMech.cs
index 88740dfd..5155cfda 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitMech.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitMech.cs
@@ -5,7 +5,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -19,12 +18,11 @@ public class LogicUnitMech : LogicUnitMechBase
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitMech(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, resolverRandom, unit)
+ public LogicUnitMech(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
@@ -33,10 +31,4 @@ public override PaperDollType GetPaperDollType()
{
return PaperDollType.Mech;
}
-
- ///
- protected override Location TransformLocation(Location location)
- {
- return Location.Structure;
- }
}
\ No newline at end of file
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitMechQuad.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitMechQuad.cs
index e13472c7..ea9bc97e 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitMechQuad.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitMechQuad.cs
@@ -5,7 +5,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -19,15 +18,31 @@ public class LogicUnitMechQuad : LogicUnitMechBase
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitMechQuad(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, resolverRandom, unit)
+ public LogicUnitMechQuad(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
+ ///
+ public override int GetCoverModifier(Cover cover)
+ {
+ switch (cover)
+ {
+ case Cover.HullDown:
+ return 3;
+ case Cover.Lower:
+ case Cover.Left:
+ case Cover.Right:
+ case Cover.Upper:
+ return 1;
+ default:
+ return 0;
+ }
+ }
+
///
public override PaperDollType GetPaperDollType()
{
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitMechTripod.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitMechTripod.cs
index f38a7ab9..6b58fe26 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitMechTripod.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitMechTripod.cs
@@ -5,7 +5,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -19,12 +18,11 @@ public class LogicUnitMechTripod : LogicUnitMechBase
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitMechTripod(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, resolverRandom, unit)
+ public LogicUnitMechTripod(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleHover.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleHover.cs
index a11d84a0..b9457afc 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleHover.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleHover.cs
@@ -5,7 +5,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -19,12 +18,11 @@ public class LogicUnitVehicleHover : LogicUnitVehicleGround
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitVehicleHover(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, resolverRandom, unit)
+ public LogicUnitVehicleHover(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleTracked.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleTracked.cs
index 9a13c53b..99c4759f 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleTracked.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleTracked.cs
@@ -5,7 +5,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -24,7 +23,7 @@ public class LogicUnitVehicleTracked : LogicUnitVehicleGround
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitVehicleTracked(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, resolverRandom, unit)
+ public LogicUnitVehicleTracked(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleVtol.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleVtol.cs
index 58208936..e4c9e905 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleVtol.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleVtol.cs
@@ -7,7 +7,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -21,15 +20,20 @@ public class LogicUnitVehicleVtol : LogicUnitVehicle
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitVehicleVtol(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, resolverRandom, unit)
+ public LogicUnitVehicleVtol(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
+ ///
+ public override int GetCoverModifier(Cover cover)
+ {
+ return 0;
+ }
+
///
public override int GetFeatureModifier(Weapon weapon)
{
@@ -65,9 +69,9 @@ protected override int TransformDamageAmountBasedOnLocation(DamageReport damageR
// Only one case for now
if (location == Location.Propulsion)
{
- var returndedDamage = decimal.ToInt32(Math.Ceiling(damage / 10m));
- damageReport.Log(new AttackLogEntry(AttackLogEntryType.Calculation, damageOwnerId, "Damage after transformation into VTOL propulsion damage", returndedDamage));
- return returndedDamage;
+ var returnedDamage = decimal.ToInt32(Math.Ceiling(damage / 10m));
+ damageReport.Log(new AttackLogEntry(AttackLogEntryType.Calculation, damageOwnerId, "Damage after transformation into VTOL propulsion damage", returnedDamage));
+ return returnedDamage;
}
return damage;
diff --git a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleWheeled.cs b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleWheeled.cs
index 4ec209cf..0f7af500 100644
--- a/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleWheeled.cs
+++ b/BtDamageResolver/src/Actors/Logic/Implementations/NonAbstract/LogicUnitVehicleWheeled.cs
@@ -5,7 +5,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic.Implementations.NonAbstract;
@@ -19,12 +18,11 @@ public class LogicUnitVehicleWheeled : LogicUnitVehicleGround
///
/// The logging interface.
/// The game options.
- /// The grain factory.
/// The math expression parser.
/// The repository provider.
/// The random number generator.
/// The unit.
- public LogicUnitVehicleWheeled(ILogger logger, GameOptions gameOptions, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, grainFactory, mathExpression, repositoryProvider, resolverRandom, unit)
+ public LogicUnitVehicleWheeled(ILogger logger, GameOptions gameOptions, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom resolverRandom, UnitEntry unit) : base(logger, gameOptions, mathExpression, repositoryProvider, resolverRandom, unit)
{
}
diff --git a/BtDamageResolver/src/Actors/Logic/Interfaces/ILogicUnit.cs b/BtDamageResolver/src/Actors/Logic/Interfaces/ILogicUnit.cs
index d6652141..a5c938d4 100644
--- a/BtDamageResolver/src/Actors/Logic/Interfaces/ILogicUnit.cs
+++ b/BtDamageResolver/src/Actors/Logic/Interfaces/ILogicUnit.cs
@@ -102,9 +102,10 @@ public interface ILogicUnit
///
/// Gets the modifier to hit resolution from unit stance.
///
+ /// The weapon being fired.
/// The distance to target.
/// The modifier to hit resolution from unit stance.
- int GetStanceModifier(int distance);
+ int GetStanceModifier(Weapon weapon, int distance);
///
/// Gets the modifier to hit resolution from unit type.
@@ -140,7 +141,7 @@ public interface ILogicUnit
/// The range bracket to calculate the projection for.
/// The weapon entry to calculate projection for.
/// A tuple containing the estimated and maximum ammo usage.
- Task<(decimal Estimate, int Max)> ProjectAmmo(int targetNumber, RangeBracket rangeBracket, WeaponEntry weaponEntry);
+ (decimal Estimate, int Max) ProjectAmmo(int targetNumber, RangeBracket rangeBracket, WeaponEntry weaponEntry);
///
/// Calculates projected heat generation for weapon.
@@ -149,7 +150,7 @@ public interface ILogicUnit
/// The range bracket to calculate the projection for.
/// The weapon entry to calculate the projection for.
/// A tuple containing the estimated and maximum heat generation.
- Task<(decimal Estimate, int Max)> ProjectHeat(int targetNumber, RangeBracket rangeBracket, WeaponEntry weaponEntry);
+ (decimal Estimate, int Max) ProjectHeat(int targetNumber, RangeBracket rangeBracket, WeaponEntry weaponEntry);
///
/// Transforms a cluster roll based on unit type and possible other properties.
@@ -243,10 +244,10 @@ public interface ILogicUnit
/// The weapon bay the weapon is in.
/// The weapon entry used.
/// A tuple with the hit modifier and the range bracket.
- Task<(int TargetNumber, RangeBracket RangeBracket)> ResolveHitModifier(AttackLog attackLog, ILogicUnit target, Arc primaryTargetArc, bool isPrimaryTarget, WeaponBay weaponBay, WeaponEntry weaponEntry);
+ (int TargetNumber, RangeBracket RangeBracket) ResolveHitModifier(AttackLog attackLog, ILogicUnit target, Arc primaryTargetArc, bool isPrimaryTarget, WeaponBay weaponBay, WeaponEntry weaponEntry);
///
- /// Calculates all heat buildup not related to weapon fire.
+ /// Calculates all heat build-up not related to weapon fire.
///
/// A damage report with non-weapon heat information.
Task ResolveNonWeaponHeat();
diff --git a/BtDamageResolver/src/Actors/Logic/LogicUnit.Ammo.cs b/BtDamageResolver/src/Actors/Logic/LogicUnit.Ammo.cs
index 76650d6e..012a88dd 100644
--- a/BtDamageResolver/src/Actors/Logic/LogicUnit.Ammo.cs
+++ b/BtDamageResolver/src/Actors/Logic/LogicUnit.Ammo.cs
@@ -12,7 +12,7 @@ namespace Faemiyah.BtDamageResolver.Actors.Logic;
public partial class LogicUnit
{
///
- public async Task<(decimal Estimate, int Max)> ProjectAmmo(int targetNumber, RangeBracket rangeBracket, WeaponEntry weaponEntry)
+ public (decimal Estimate, int Max) ProjectAmmo(int targetNumber, RangeBracket rangeBracket, WeaponEntry weaponEntry)
{
var hitChance = GetHitChanceForTargetNumber(targetNumber);
@@ -20,7 +20,7 @@ public partial class LogicUnit
// This can only be a single weapon or a multiplied weapon.
// No need to loop through different ammo usages in the bay.
- var weapon = await FormWeapon(weaponEntry);
+ var weapon = FormWeapon(weaponEntry);
if (!weapon.UsesAmmo || hitChance == 0m)
{
@@ -75,9 +75,18 @@ protected virtual Task ResolveAmmo(DamageReport hitCalculationDamageReport, Comb
/// The weapon to apply ammo usage from.
protected void SpendAmmo(DamageReport damageReport, Weapon weapon)
{
- var ammoName = $"{weapon.Name} {weapon.AppliedAmmo}";
+ string ammoName;
var ammoUsed = 1;
+ if (!weapon.MultiAmmo)
+ {
+ ammoName = weapon.Name;
+ }
+ else
+ {
+ ammoName = weapon.AmmoProxyName == null ? $"{weapon.Name} {weapon.AppliedAmmo}" : $"{weapon.Name} {weapon.AmmoProxyName}";
+ }
+
if (weapon.Instances > 1)
{
ammoUsed *= weapon.Instances;
diff --git a/BtDamageResolver/src/Actors/Logic/LogicUnit.Damage.cs b/BtDamageResolver/src/Actors/Logic/LogicUnit.Damage.cs
index d38c78bf..0ce237d0 100644
--- a/BtDamageResolver/src/Actors/Logic/LogicUnit.Damage.cs
+++ b/BtDamageResolver/src/Actors/Logic/LogicUnit.Damage.cs
@@ -7,7 +7,6 @@
using Faemiyah.BtDamageResolver.Actors.Logic.Interfaces;
using Faemiyah.BtDamageResolver.Api.Entities;
using Faemiyah.BtDamageResolver.Api.Enums;
-using Microsoft.CodeAnalysis;
namespace Faemiyah.BtDamageResolver.Actors.Logic;
@@ -63,7 +62,7 @@ public virtual Task TransformDamageBasedOnUnitType(DamageReport damageRepor
/// The combat action.
/// A task that calculates the damage of a single fire instance.
/// The total damage by rapid fire action.
- protected async Task RapidFireWrapper(DamageReport damageReport, ILogicUnit target, CombatAction combatAction, Task singleFireDamageCalculation)
+ protected async Task RapidFireWrapper(DamageReport damageReport, ILogicUnit target, CombatAction combatAction, Func> singleFireDamageCalculation)
{
if (combatAction.Weapon.HasFeature(WeaponFeature.Rapid, out var rapidFeatureEntry))
{
@@ -75,7 +74,7 @@ protected async Task RapidFireWrapper(DamageReport damageReport, ILogicUnit
var damage = 0;
for (var ii = 0; ii < hits; ii++)
{
- damage += await singleFireDamageCalculation;
+ damage += await singleFireDamageCalculation();
}
damageReport.Log(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Total damage after calculating all hits", damage));
@@ -83,7 +82,7 @@ protected async Task RapidFireWrapper(DamageReport damageReport, ILogicUnit
return damage;
}
- return await singleFireDamageCalculation;
+ return await singleFireDamageCalculation();
}
///
@@ -314,11 +313,11 @@ protected virtual async Task ResolveTotalOutgoingDamage(DamageReport damage
// Most units can charge, and mech units can make other melee attacks. Resolve their damage here.
if (combatAction.Weapon.Type == WeaponType.Melee)
{
- return await RapidFireWrapper(damageReport, target, combatAction, ResolveTotalOutgoingDamageMelee(damageReport, combatAction));
+ return await RapidFireWrapper(damageReport, target, combatAction, () => ResolveTotalOutgoingDamageMelee(damageReport, combatAction));
}
else
{
- return await RapidFireWrapper(damageReport, target, combatAction, ResolveTotalOutgoingDamageInternal(damageReport, target, combatAction));
+ return await RapidFireWrapper(damageReport, target, combatAction, () => ResolveTotalOutgoingDamageInternal(damageReport, target, combatAction));
}
}
diff --git a/BtDamageResolver/src/Actors/Logic/LogicUnit.DamageResolution.cs b/BtDamageResolver/src/Actors/Logic/LogicUnit.DamageResolution.cs
index 3ea6fed1..585c00c3 100644
--- a/BtDamageResolver/src/Actors/Logic/LogicUnit.DamageResolution.cs
+++ b/BtDamageResolver/src/Actors/Logic/LogicUnit.DamageResolution.cs
@@ -30,7 +30,7 @@ public async Task ApplyDamagePackets(DamageReport damageReport, Guid damageOwner
// Unfortunately we still have to do transformations at this point, as certain locations and armor types receive damage differently
var transformedDamage = TransformDamageAmountBasedOnLocation(damageReport, damageOwnerId, location, damagePacket.Damage);
- damageReport.DamagePaperDoll.RecordDamage(location, damageOwnerId, transformedDamage);
+ damageReport.DamagePaperDoll.RecordDamage(damageOwnerId, new DamageEntry(transformedDamage, location));
damageReport.Log(new AttackLogEntry(AttackLogEntryType.Damage, damageOwnerId, transformedDamage, location));
foreach (var specialDamageEntry in damagePacket.SpecialDamageEntries)
@@ -64,7 +64,7 @@ public async Task ApplyDamagePackets(DamageReport damageReport, Guid damageOwner
// Critical damage threats coming from special damage (AP Ammo, Retractable Blade) never do multiple critical effects. Take the first.
// Direct critical damage threats should always succeed, regardless of damage threshold. Give a very high damage amount.
var criticalDamageType = criticalDamageTable.Mapping[specialDamageEntryCriticalThreatRoll][0];
- damageReport.DamagePaperDoll.RecordCriticalDamage(location, damageOwnerId, damagePacket.Damage, CriticalThreatType.Normal, criticalDamageType);
+ damageReport.DamagePaperDoll.RecordDamage(damageOwnerId, new DamageEntry(damagePacket.Damage, location, CriticalThreatType.Normal, criticalDamageType));
damageReport.Log(new AttackLogEntry(AttackLogEntryType.Critical, damageOwnerId, criticalDamageType.ToString(), 0, location));
}
else
@@ -74,7 +74,7 @@ public async Task ApplyDamagePackets(DamageReport damageReport, Guid damageOwner
break;
default:
- damageReport.DamagePaperDoll.RecordSpecialDamage(location, damageOwnerId, specialDamageEntry);
+ damageReport.DamagePaperDoll.RecordDamage(damageOwnerId, new DamageEntry(int.Parse(specialDamageEntry.Data), location, specialDamageEntry.Type));
damageReport.Log(new AttackLogEntry(AttackLogEntryType.SpecialDamage, damageOwnerId, specialDamageEntry.Type.ToString(), int.Parse(specialDamageEntry.Data), location));
break;
}
@@ -149,12 +149,26 @@ protected virtual int TransformDamageAmountBasedOnLocation(DamageReport damageRe
int hitLocation;
var ready = false;
+ if(cover == Cover.HullDown)
+ {
+ switch(Unit.Type)
+ {
+ // Magic value for hull down cover hits - always hit turret
+ case UnitType.VehicleHover:
+ case UnitType.VehicleTracked:
+ case UnitType.VehicleWheeled:
+ return (10, Location.Turret);
+ default:
+ break;
+ }
+ }
+
do
{
hitLocation = paperDoll.LocationMapping.Keys.Count switch
{
11 => ResolverRandom.D26(),
- _ => ResolverRandom.NextPlusOne(paperDoll.LocationMapping.Keys.Count)
+ _ => ResolverRandom.DX(paperDoll.LocationMapping.Keys.Count)
};
damageReport.Log(new AttackLogEntry(AttackLogEntryType.DiceRoll, damageOwnerId, "Location", hitLocation));
diff --git a/BtDamageResolver/src/Actors/Logic/LogicUnit.Fetching.cs b/BtDamageResolver/src/Actors/Logic/LogicUnit.Fetching.cs
index 37294af3..3938e89a 100644
--- a/BtDamageResolver/src/Actors/Logic/LogicUnit.Fetching.cs
+++ b/BtDamageResolver/src/Actors/Logic/LogicUnit.Fetching.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using System.Threading.Tasks;
using Faemiyah.BtDamageResolver.Actors.Logic.Interfaces;
using Faemiyah.BtDamageResolver.Api.Entities;
using Faemiyah.BtDamageResolver.Api.Entities.RepositoryEntities;
@@ -37,7 +36,7 @@ public DamagePaperDoll GetDamagePaperDoll(ILogicUnit target, AttackType attackTy
///
/// The weapon entry to base the applied weapon on.
/// The with the chosen ammo in the weapon entry applied, if possible.
- protected async Task FormWeapon(WeaponEntry weaponEntry)
+ protected Weapon FormWeapon(WeaponEntry weaponEntry)
{
var weapon = RepositoryProvider.WeaponRepository.Get(weaponEntry.WeaponName);
@@ -63,19 +62,23 @@ protected async Task FormWeapon(WeaponEntry weaponEntry)
/// The transformed attack type.
protected virtual AttackType TransformAttackType(ILogicUnit target, AttackType attackType, List weaponFeatures)
{
- // Melee attacks that are not kicks or punches use normal attack tables
- var transformedAttackType = attackType == AttackType.Melee ? AttackType.Normal : attackType;
-
var targetType = target.Unit.Type;
+ var transformedAttackType = attackType;
// Punch and kick tables only exist for mechs, revert to normal for all other target types
+ // Special handling for mechs
switch (targetType)
{
case UnitType.Mech:
case UnitType.MechTripod:
case UnitType.MechQuad:
- // Punches and kicks to prone or crouched mechs can hit anywhere
- if (target.Unit.Stance is Stance.Prone or Stance.Crouch)
+ // Melee attacks against crouched mechs hit punch table
+ if ((attackType == AttackType.Melee || attackType == AttackType.Kick) && target.Unit.Stance is Stance.Crouch)
+ {
+ transformedAttackType = AttackType.Punch;
+ }
+
+ if (weaponFeatures.Contains(WeaponFeature.MeleeDfa) && target.Unit.Stance is Stance.Prone)
{
transformedAttackType = AttackType.Normal;
}
@@ -86,6 +89,12 @@ protected virtual AttackType TransformAttackType(ILogicUnit target, AttackType a
break;
}
+ // Melee attacks that are not kicks or punches use normal attack tables
+ if (transformedAttackType == AttackType.Melee)
+ {
+ transformedAttackType = AttackType.Normal;
+ }
+
return transformedAttackType;
}
@@ -119,13 +128,57 @@ private string GetPaperDollNameFromAttackParameters(ILogicUnit target, AttackTyp
var transformedAttackType = TransformAttackType(target, attackType, weaponFeatures);
- var transformedDirection = target.Unit.Type is UnitType.Infantry or UnitType.BattleArmor or UnitType.Building ? Direction.Front : direction;
+ var transformedDirection = TransformDirection(target, weaponFeatures, direction);
+
+ List transformedRules = TransformRules(target, gameOptions, transformedAttackType);
+
+ return PaperDoll.GetIdFromProperties(transformedTargetType, transformedAttackType, transformedDirection, transformedRules);
+ }
+
+ ///
+ /// Transform direction based on target properties and weapon features.
+ ///
+ /// The target unit.
+ /// The weapon features.
+ /// The direction of the attack.
+ /// The transformed direction.
+ private static Direction TransformDirection(ILogicUnit target, List weaponFeatures, Direction direction)
+ {
+ switch (target.Unit.Type)
+ {
+ case UnitType.BattleArmor:
+ case UnitType.Building:
+ case UnitType.Infantry:
+ direction = Direction.Front;
+ break;
+ case UnitType.Mech:
+ case UnitType.MechTripod:
+ case UnitType.MechQuad:
+ // Melee DFA against prone mechs hits the back of the mech
+ if (weaponFeatures.Contains(WeaponFeature.MeleeDfa) && target.Unit.Stance is Stance.Prone)
+ {
+ direction = Direction.Rear;
+ }
+ break;
+ }
+
+ return direction;
+ }
+ ///
+ /// Transform rules based on target properties and attack type.
+ ///
+ /// The target unit logic.
+ /// The game options.
+ /// The attack type.
+ /// The transformed rules.
+ private static List TransformRules(ILogicUnit target, GameOptions gameOptions, AttackType attackType)
+ {
// Get alterative paperdolls based on rules
var transformedRules = new List();
// Floating critical may only apply to mechs
- if (gameOptions.Rules[Rule.FloatingCritical] && transformedAttackType == AttackType.Normal)
+ if (gameOptions.Rules[Rule.FloatingCritical] && attackType == AttackType.Normal)
{
switch (target.Unit.Type)
{
@@ -151,6 +204,6 @@ private string GetPaperDollNameFromAttackParameters(ILogicUnit target, AttackTyp
}
}
- return PaperDoll.GetIdFromProperties(transformedTargetType, transformedAttackType, transformedDirection, transformedRules);
+ return transformedRules;
}
}
\ No newline at end of file
diff --git a/BtDamageResolver/src/Actors/Logic/LogicUnit.General.cs b/BtDamageResolver/src/Actors/Logic/LogicUnit.General.cs
index bd736e1f..b43781e0 100644
--- a/BtDamageResolver/src/Actors/Logic/LogicUnit.General.cs
+++ b/BtDamageResolver/src/Actors/Logic/LogicUnit.General.cs
@@ -35,7 +35,7 @@ public virtual async Task> ResolveCombatForBay(ILogicUnit tar
continue;
}
- var hitCalclulationDamageReport = new DamageReport
+ var hitCalculationDamageReport = new DamageReport
{
Phase = weapon.UsePhase,
DamagePaperDoll = GetDamagePaperDoll(target, AttackType.Normal, weaponBay.FiringSolution.Direction, weapon.SpecialFeatures.Select(w => w.Type).ToList()),
@@ -46,9 +46,9 @@ public virtual async Task> ResolveCombatForBay(ILogicUnit tar
InitialTroopers = target.Unit.Troopers
};
- var combatAction = ResolveHit(hitCalclulationDamageReport, target, primaryTargetArc, isPrimaryTarget, weapon, weaponBay, weaponEntry);
+ var combatAction = ResolveHit(hitCalculationDamageReport, target, primaryTargetArc, isPrimaryTarget, weapon, weaponBay, weaponEntry);
- damageReportCombatActionPairs.Add((hitCalclulationDamageReport, combatAction));
+ damageReportCombatActionPairs.Add((hitCalculationDamageReport, combatAction));
}
// Add damage resolution to damage reports based on combat actions
@@ -99,7 +99,7 @@ public virtual async Task> ResolveCombatForBay(ILogicUnit tar
foreach (var weaponEntry in weaponBay.Weapons.Where(w => w.State == WeaponState.Active))
{
- weapons.Add((await FormWeapon(weaponEntry), weaponEntry));
+ weapons.Add((FormWeapon(weaponEntry), weaponEntry));
}
return weapons;
diff --git a/BtDamageResolver/src/Actors/Logic/LogicUnit.Heat.cs b/BtDamageResolver/src/Actors/Logic/LogicUnit.Heat.cs
index 451bd54c..0b7812ea 100644
--- a/BtDamageResolver/src/Actors/Logic/LogicUnit.Heat.cs
+++ b/BtDamageResolver/src/Actors/Logic/LogicUnit.Heat.cs
@@ -13,7 +13,7 @@ namespace Faemiyah.BtDamageResolver.Actors.Logic;
public partial class LogicUnit
{
///
- public async Task<(decimal Estimate, int Max)> ProjectHeat(int targetNumber, RangeBracket rangeBracket, WeaponEntry weaponEntry)
+ public (decimal Estimate, int Max) ProjectHeat(int targetNumber, RangeBracket rangeBracket, WeaponEntry weaponEntry)
{
var hitChance = GetHitChanceForTargetNumber(targetNumber);
@@ -24,7 +24,7 @@ public partial class LogicUnit
int heatGenerated;
- var weapon = await FormWeapon(weaponEntry);
+ var weapon = FormWeapon(weaponEntry);
heatGenerated = weapon.HasFeature(WeaponFeature.Rapid, out var rapidFeatureEntry)
? MathExpression.Parse(rapidFeatureEntry.Data) * ResolveHeatForSingleHit(weapon, rangeBracket)
@@ -101,7 +101,7 @@ protected void ResolveHeat(DamageReport hitCalculationDamageReport, CombatAction
{
if (!combatAction.ActionHappened)
{
- hitCalculationDamageReport.Log(new AttackLogEntry(AttackLogEntryType.Information, Unit.Id, $"Combat action was canceled for {combatAction.Weapon.Name} and no heat will be generated"));
+ hitCalculationDamageReport.Log(new AttackLogEntry(AttackLogEntryType.Information, Unit.Id, $"Combat action was cancelled for {combatAction.Weapon.Name} and no heat will be generated"));
return;
}
@@ -111,18 +111,18 @@ protected void ResolveHeat(DamageReport hitCalculationDamageReport, CombatAction
return;
}
- var calculatedSingleHitheat = ResolveHeatForSingleHit(combatAction.Weapon, combatAction.RangeBracket);
+ var calculatedSingleHitHeat = ResolveHeatForSingleHit(combatAction.Weapon, combatAction.RangeBracket);
int heat;
if (combatAction.Weapon.HasFeature(WeaponFeature.Rapid, out var rapidFeatureEntry))
{
var multiplier = MathExpression.Parse(rapidFeatureEntry.Data);
- heat = calculatedSingleHitheat * multiplier;
+ heat = calculatedSingleHitHeat * multiplier;
hitCalculationDamageReport.Log(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, $"{combatAction.Weapon.Name} rate of fire multiplier for heat", multiplier));
}
else
{
- heat = calculatedSingleHitheat;
+ heat = calculatedSingleHitHeat;
}
hitCalculationDamageReport.Log(new AttackLogEntry(AttackLogEntryType.Heat, Unit.Id, combatAction.Weapon.Name, heat));
@@ -137,10 +137,13 @@ protected void ResolveHeat(DamageReport hitCalculationDamageReport, CombatAction
/// Aerospace units may have weapon bays where certain versions of weapons fire for different ranges and produce different amounts of heat.
///
/// The weapon to resolve heat for.
- /// The range bracket to resolve heat for.
+ /// The range bracket to resolve heat for. Intentionally ignored in the base implementation; see remarks.
/// The heat produced.
protected virtual int ResolveHeatForSingleHit(Weapon weapon, RangeBracket rangeBracket)
{
+ // NOTE: rangeBracket is intentionally ignored here. For non-aerospace actions, heat is range-independent.
+ // Indexing by rangeBracket here would possibly return 0 for Medium/Long/etc. -ranged hits and produce wrong
+ // heat values. Only LogicUnitAerospace (and capital-ship logic) overrides this and uses rangeBracket.
return weapon.Heat[RangeBracket.Short];
}
}
\ No newline at end of file
diff --git a/BtDamageResolver/src/Actors/Logic/LogicUnit.Hit.cs b/BtDamageResolver/src/Actors/Logic/LogicUnit.Hit.cs
index e437fcf5..c179be58 100644
--- a/BtDamageResolver/src/Actors/Logic/LogicUnit.Hit.cs
+++ b/BtDamageResolver/src/Actors/Logic/LogicUnit.Hit.cs
@@ -35,7 +35,7 @@ public void TransformCombatAction(DamageReport targetDamageReport, CombatAction
}
else
{
- var singleMissileDefenseRoll = ResolverRandom.NextPlusOne(6);
+ var singleMissileDefenseRoll = ResolverRandom.DX(6);
targetDamageReport.Log(new AttackLogEntry(AttackLogEntryType.DiceRoll, Unit.Id, "Ams defense roll against single missile", singleMissileDefenseRoll));
if (singleMissileDefenseRoll >= 4)
{
@@ -59,13 +59,9 @@ public void TransformCombatAction(DamageReport targetDamageReport, CombatAction
/// The hit chance.
protected static decimal GetHitChanceForTargetNumber(int targetNumber)
{
- if (targetNumber > 12)
- {
- return 0m;
- }
-
switch (targetNumber)
{
+ case < 3: return 1m;
case 3: return 35m / 36m;
case 4: return 33m / 36m;
case 5: return 30m / 36m;
@@ -76,7 +72,7 @@ protected static decimal GetHitChanceForTargetNumber(int targetNumber)
case 10: return 6m / 36m;
case 11: return 3m / 36m;
case 12: return 1m / 36m;
- default: return 1m;
+ default: return 0m;
}
}
diff --git a/BtDamageResolver/src/Actors/Logic/LogicUnit.HitModifier.cs b/BtDamageResolver/src/Actors/Logic/LogicUnit.HitModifier.cs
index 4786af84..07094529 100644
--- a/BtDamageResolver/src/Actors/Logic/LogicUnit.HitModifier.cs
+++ b/BtDamageResolver/src/Actors/Logic/LogicUnit.HitModifier.cs
@@ -1,5 +1,4 @@
using System;
-using System.Threading.Tasks;
using Faemiyah.BtDamageResolver.Actors.Logic.Interfaces;
using Faemiyah.BtDamageResolver.Api.Constants;
using Faemiyah.BtDamageResolver.Api.Entities;
@@ -16,126 +15,48 @@ public abstract partial class LogicUnit
private static readonly int[] MovementModifierArray = [0, 0, 0, 1, 1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 6];
///
- public async Task<(int TargetNumber, RangeBracket RangeBracket)> ResolveHitModifier(AttackLog attackLog, ILogicUnit target, Arc primaryTargetArc, bool isPrimaryTarget, WeaponBay weaponBay, WeaponEntry weaponEntry)
+ public (int TargetNumber, RangeBracket RangeBracket) ResolveHitModifier(AttackLog attackLog, ILogicUnit target, Arc primaryTargetArc, bool isPrimaryTarget, WeaponBay weaponBay, WeaponEntry weaponEntry)
{
- return ResolveHitModifier(attackLog, target, primaryTargetArc, isPrimaryTarget, await FormWeapon(weaponEntry), weaponBay, weaponEntry);
+ return ResolveHitModifier(attackLog, target, primaryTargetArc, isPrimaryTarget, FormWeapon(weaponEntry), weaponBay, weaponEntry);
}
///
public (int TargetNumber, RangeBracket RangeBracket) ResolveHitModifier(AttackLog attackLog, ILogicUnit target, Arc primaryTargetArc, bool isPrimaryTarget, Weapon weapon, WeaponBay weaponBay, WeaponEntry weaponEntry)
{
- var modifierBase = weapon.AttackType == AttackType.Normal ? Unit.Gunnery : Unit.Piloting;
-
- // NOTE: In this method, the damageReport may be null, as the target number calculation does not need a damage log. Everywhere else, it must exist.
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Base hit modifier", modifierBase));
-
- var modifierMultiTarget = GetMultiTargetModifier(primaryTargetArc, isPrimaryTarget, weapon, weaponBay);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from multiple targets", modifierMultiTarget));
-
- var modifierHeat = Unit.GetHeatAttackPenalty();
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from heat effects", modifierHeat));
-
- var modifierPenalty = GetPenaltyModifier();
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from EMP effects", modifierPenalty));
-
- var modifierFiringSolution = GetFiringSolutionModifier(weaponBay.FiringSolution);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from firing solution", modifierFiringSolution));
-
var rangeBracket = GetRangeBracket(weapon, weaponBay);
- var modifierRange = GetRangeModifier(rangeBracket);
- modifierRange += GetMinimumRangeModifier(weapon, weaponBay);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from range", modifierRange));
-
- var modifierArmor = GetArmorModifier(rangeBracket, target);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from target armor", modifierArmor));
-
- var modifierWeapon = GetWeaponModifier(weapon);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from weapon properties", modifierWeapon));
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from weapon entry properties", weaponEntry.Modifier));
-
- var modifierTargetingComputer = GetTargetingComputerModifier(weapon);
-
- attackLog.Append(new AttackLogEntry (AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from unit targeting computer", modifierTargetingComputer));
-
- var modifierUnitType = target.GetUnitTypeModifier();
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from unit type", modifierUnitType));
-
- var modifierCover = target.GetCoverModifier(weaponBay.FiringSolution.Cover);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from cover", modifierCover));
-
- var modifierStance = target.GetStanceModifier(weaponBay.FiringSolution.Distance);
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from target stance", modifierStance));
-
- var modifierMovementDirection = target.GetMovementDirectionModifier(weaponBay.FiringSolution.Direction);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from movement direction", modifierMovementDirection));
-
- var modifierMovementClass = GetMovementClassModifier(target, weapon);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from target movement class", modifierMovementClass));
-
- var modifierMovement = GetMovementModifierBase(target, weapon);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from target movement amount", modifierMovement));
-
- var modifierEvasion = GetEvasionModifier(target);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from target evasion", modifierEvasion));
-
- var modifierOwnMovement = GetOwnMovementModifier();
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from own movement", modifierOwnMovement));
-
- var modifierOwnEvasion = GetOwnEvasionModifier(weapon);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from own evasion", modifierOwnEvasion));
-
- var modifierFeatures = target.GetFeatureModifier(weapon);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from weapon features", modifierFeatures));
-
- var modifierWeather = GetWeatherModifier(weapon);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from weather effects", modifierWeather));
-
- var modifierQuirks = GetQuirkModifier(target, weapon);
-
- attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Hit modifier from unit quirks", modifierQuirks));
-
- var modifierTotal =
- modifierBase +
- modifierMultiTarget +
- modifierHeat +
- modifierPenalty +
- modifierFiringSolution +
- modifierRange +
- modifierArmor +
- modifierWeapon +
- weaponEntry.Modifier +
- modifierTargetingComputer +
- modifierUnitType +
- modifierCover +
- modifierStance +
- modifierMovementDirection +
- modifierMovementClass +
- modifierMovement +
- modifierEvasion +
- modifierOwnMovement +
- modifierOwnEvasion +
- modifierFeatures +
- modifierWeather +
- modifierQuirks;
+ var modifiers = new (string Label, int Value)[]
+ {
+ ("Base hit modifier", weapon.AttackType == AttackType.Normal ? Unit.Gunnery : Unit.Piloting),
+ ("Hit modifier from multiple targets", GetMultiTargetModifier(primaryTargetArc, isPrimaryTarget, weapon, weaponBay)),
+ ("Hit modifier from heat effects", Unit.GetHeatAttackPenalty()),
+ ("Hit modifier from EMP effects", GetPenaltyModifier()),
+ ("Hit modifier from firing solution", GetFiringSolutionModifier(weaponBay.FiringSolution)),
+ ("Hit modifier from range", GetRangeModifier(rangeBracket) + GetMinimumRangeModifier(weapon, weaponBay)),
+ ("Hit modifier from target armor", GetArmorModifier(rangeBracket, target)),
+ ("Hit modifier from weapon properties", GetWeaponModifier(weapon)),
+ ("Hit modifier from weapon entry properties", weaponEntry.Modifier),
+ ("Hit modifier from unit targeting computer", GetTargetingComputerModifier(weapon)),
+ ("Hit modifier from unit type", target.GetUnitTypeModifier()),
+ ("Hit modifier from cover", target.GetCoverModifier(weaponBay.FiringSolution.Cover)),
+ ("Hit modifier from target stance", target.GetStanceModifier(weapon, weaponBay.FiringSolution.Distance)),
+ ("Hit modifier from movement direction", target.GetMovementDirectionModifier(weaponBay.FiringSolution.Direction)),
+ ("Hit modifier from target movement class", GetMovementClassModifier(target, weapon)),
+ ("Hit modifier from target movement amount", GetMovementModifierBase(target, weapon)),
+ ("Hit modifier from target evasion", GetEvasionModifier(target)),
+ ("Hit modifier from own movement", GetOwnMovementModifier()),
+ ("Hit modifier from own evasion", GetOwnEvasionModifier(weapon)),
+ ("Hit modifier from weapon features", target.GetFeatureModifier(weapon)),
+ ("Hit modifier from weather effects", GetWeatherModifier(weapon)),
+ ("Hit modifier from unit quirks", GetQuirkModifier(target, weapon)),
+ };
+
+ var modifierTotal = 0;
+ foreach (var (label, value) in modifiers)
+ {
+ attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, label, value));
+ modifierTotal += value;
+ }
attackLog.Append(new AttackLogEntry(AttackLogEntryType.Calculation, Unit.Id, "Target number", modifierTotal));
@@ -179,7 +100,7 @@ public virtual int GetMovementModifier()
}
///
- public virtual int GetStanceModifier(int distance)
+ public virtual int GetStanceModifier(Weapon weapon, int distance)
{
return 0;
}
diff --git a/BtDamageResolver/src/Actors/Logic/LogicUnitFactory.cs b/BtDamageResolver/src/Actors/Logic/LogicUnitFactory.cs
index d7ac3f19..87548a12 100644
--- a/BtDamageResolver/src/Actors/Logic/LogicUnitFactory.cs
+++ b/BtDamageResolver/src/Actors/Logic/LogicUnitFactory.cs
@@ -8,7 +8,6 @@
using Faemiyah.BtDamageResolver.Api.Enums;
using Faemiyah.BtDamageResolver.Api.Options;
using Microsoft.Extensions.Logging;
-using Orleans;
namespace Faemiyah.BtDamageResolver.Actors.Logic;
@@ -18,7 +17,6 @@ namespace Faemiyah.BtDamageResolver.Actors.Logic;
public class LogicUnitFactory : ILogicUnitFactory
{
private readonly ILoggerFactory _loggerFactory;
- private readonly IGrainFactory _grainFactory;
private readonly IMathExpression _mathExpression;
private readonly RepositoryProvider _repositoryProvider;
private readonly IResolverRandom _resolverRandom;
@@ -27,13 +25,11 @@ public class LogicUnitFactory : ILogicUnitFactory
/// Initializes a new instance of the class.
///
/// The logger factory.
- /// The grain factory.
/// The math expression solver.
/// The random number generator.
- public LogicUnitFactory(ILoggerFactory loggerFactory, IGrainFactory grainFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom random)
+ public LogicUnitFactory(ILoggerFactory loggerFactory, IMathExpression mathExpression, RepositoryProvider repositoryProvider, IResolverRandom random)
{
_loggerFactory = loggerFactory;
- _grainFactory = grainFactory;
_mathExpression = mathExpression;
_repositoryProvider = repositoryProvider;
_resolverRandom = random;
@@ -45,33 +41,33 @@ public ILogicUnit CreateFrom(GameOptions gameOptions, UnitEntry unit)
switch (unit.Type)
{
case UnitType.Building:
- return new LogicUnitBuilding(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitBuilding(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
case UnitType.AerospaceCapital:
- return new LogicUnitAerospaceCapital(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitAerospaceCapital(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
case UnitType.AerospaceDropshipAerodyne:
- return new LogicUnitAerospaceDropshipAerodyne(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitAerospaceDropshipAerodyne(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
case UnitType.AerospaceDropshipSpheroid:
- return new LogicUnitAerospaceDropshipSpheroid(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitAerospaceDropshipSpheroid(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
case UnitType.AerospaceFighter:
- return new LogicUnitAerospaceFighter(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitAerospaceFighter(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
case UnitType.BattleArmor:
- return new LogicUnitBattleArmor(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitBattleArmor(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
case UnitType.Infantry:
- return new LogicUnitInfantry(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitInfantry(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
case UnitType.Mech:
- return new LogicUnitMech(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitMech(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
case UnitType.MechTripod:
- return new LogicUnitMechTripod(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitMechTripod(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
case UnitType.MechQuad:
- return new LogicUnitMechQuad(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitMechQuad(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
case UnitType.VehicleHover:
- return new LogicUnitVehicleHover(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitVehicleHover(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
case UnitType.VehicleTracked:
- return new LogicUnitVehicleTracked(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitVehicleTracked(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
case UnitType.VehicleVtol:
- return new LogicUnitVehicleVtol(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitVehicleVtol(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
case UnitType.VehicleWheeled:
- return new LogicUnitVehicleWheeled(_loggerFactory.CreateLogger(), gameOptions, _grainFactory, _mathExpression, _repositoryProvider, _resolverRandom, unit);
+ return new LogicUnitVehicleWheeled(_loggerFactory.CreateLogger(), gameOptions, _mathExpression, _repositoryProvider, _resolverRandom, unit);
default:
throw new NotImplementedException($"Factory for type {unit.Type} is not implemented.");
}
diff --git a/BtDamageResolver/src/Actors/PlayerActor.Connections.cs b/BtDamageResolver/src/Actors/PlayerActor.Connections.cs
index 8332721f..6966e84f 100644
--- a/BtDamageResolver/src/Actors/PlayerActor.Connections.cs
+++ b/BtDamageResolver/src/Actors/PlayerActor.Connections.cs
@@ -80,11 +80,13 @@ public async Task Disconnect(Guid authenticationToken)
await SendErrorMessageToClient($"Inconsistent state. Player {this.GetPrimaryKeyString()} Unable to sign out of the active game. {_playerActorState.State.GameId}");
}
+ // Persist before responding so the client never sees a "disconnected" response that the
+ // server later forgets due to a failed write or deactivation.
+ await _playerActorState.WriteStateAsync();
await SendDataToClient(EventNames.ConnectionResponse, GetConnectionResponse(false));
// Log the logout to permanent store
await _loggingServiceClient.LogPlayerAction(DateTime.UtcNow, this.GetPrimaryKeyString(), PlayerActionType.Logout, 0);
- await _playerActorState.WriteStateAsync();
return true;
}
@@ -183,6 +185,12 @@ private async Task MarkDisconnectedStateAndSendToClient()
{
_playerActorState.State.GameId = null;
_playerActorState.State.UpdateTimeStamp = DateTime.UtcNow;
+
+ // Persist the cleared GameId before notifying the client. Otherwise the actor could
+ // deactivate (or the silo could die) after the client has been told it disconnected,
+ // leaving stale GameId in persistent storage and resurrecting it on next activation.
+ await _playerActorState.WriteStateAsync();
+
await SendOnlyThisPlayerGameStateToClient();
await SendDataToClient(EventNames.ConnectionResponse, GetConnectionResponse(true));
}
diff --git a/BtDamageResolver/src/Actors/PlayerActor.Internal.cs b/BtDamageResolver/src/Actors/PlayerActor.Internal.cs
index 7ac15399..f59780a2 100644
--- a/BtDamageResolver/src/Actors/PlayerActor.Internal.cs
+++ b/BtDamageResolver/src/Actors/PlayerActor.Internal.cs
@@ -40,6 +40,13 @@ private ConnectionResponse GetConnectionResponse(bool isConnected)
///
/// Get the state of this player.
///
+ ///
+ /// The returned aliases the stored instances
+ /// via a shallow ToList(). This is safe only because the consumer is always reached via
+ /// a cross-grain or Redis call, both of which round-trip through the JSON serializer registered
+ /// in Silo/Program.cs and therefore produce a deep copy at the boundary. Do not call this
+ /// from same-grain code that mutates the result.
+ ///
/// The object containing the properties of this unit actor.
private Task GetPlayerState()
{
diff --git a/BtDamageResolver/src/Actors/PlayerActor.Sends.cs b/BtDamageResolver/src/Actors/PlayerActor.Sends.cs
index 45b10375..22ca3a36 100644
--- a/BtDamageResolver/src/Actors/PlayerActor.Sends.cs
+++ b/BtDamageResolver/src/Actors/PlayerActor.Sends.cs
@@ -78,7 +78,7 @@ public async Task SendPlayerState(Guid authenticationToken, PlayerState pl
if (!validationResult.IsValid)
{
- errorMessageStringBuilder.AppendLine($"Unit {unit} has the following errors: {validationResult}");
+ errorMessageStringBuilder.AppendLine($"Unit {unit.Name} ({unit.Id}) has the following errors: {validationResult}");
unitsWithErrors.Add(unit.Id);
_logger.LogWarning("Player {PlayerId} is sending invalid data for unit {UnitId}. Reason: {ValidationResult}", this.GetPrimaryKeyString(), unit.Id, validationResult);
}
diff --git a/BtDamageResolver/src/Actors/PlayerActor.cs b/BtDamageResolver/src/Actors/PlayerActor.cs
index 7ee6b0d7..ae131fde 100644
--- a/BtDamageResolver/src/Actors/PlayerActor.cs
+++ b/BtDamageResolver/src/Actors/PlayerActor.cs
@@ -4,7 +4,7 @@
using Faemiyah.BtDamageResolver.Actors.Cryptography;
using Faemiyah.BtDamageResolver.Actors.States;
using Faemiyah.BtDamageResolver.Common.Constants;
-using Faemiyah.BtDamageResolver.Common.Options;
+using Faemiyah.BtDamageResolver.Common.Logging.Options;
using Faemiyah.BtDamageResolver.Services.Interfaces;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
diff --git a/BtDamageResolver/src/Actors/Repositories/GameEntryRepositoryActor.cs b/BtDamageResolver/src/Actors/Repositories/GameEntryRepositoryActor.cs
index c8c7e3b0..a7715fb9 100644
--- a/BtDamageResolver/src/Actors/Repositories/GameEntryRepositoryActor.cs
+++ b/BtDamageResolver/src/Actors/Repositories/GameEntryRepositoryActor.cs
@@ -18,8 +18,11 @@ namespace Faemiyah.BtDamageResolver.Actors.Repositories;
///
public class GameEntryRepositoryActor : ExternalRepositoryActorBase, IGameEntryRepository
{
+ private static readonly TimeSpan ExpireInterval = TimeSpan.FromMinutes(5);
+
private readonly ICommunicationServiceClient _communicationServiceClient;
private readonly TimeSpan _maxGameAge = TimeSpan.FromHours(Settings.MaximumGameEntryAgeHours);
+ private DateTime _lastExpire = DateTime.MinValue;
///
/// Initializes a new instance of the class.
@@ -35,15 +38,13 @@ public GameEntryRepositoryActor(ILogger logger, Cached
///
public override async Task Get(string key)
{
- await CleanupOldEntries();
-
return await base.Get(key);
}
///
public override async Task> GetAll()
{
- await CleanupOldEntries();
+ await ExpireOldEntries();
return await base.GetAll();
}
@@ -83,9 +84,20 @@ private async Task Distribute()
await _communicationServiceClient.SendToAllClients(EventNames.GameEntries, await GetAll());
}
- private async Task CleanupOldEntries()
+ private async Task ExpireOldEntries()
{
- foreach (var entry in (await base.GetAll()).Where(gameEntry => gameEntry.TimeStamp < DateTime.UtcNow - _maxGameAge))
+ // Throttle expiration scans — entries age in hours so checking on every Get/GetAll
+ // (which the broadcast path also hits) is wasteful. When old entries are actually
+ // deleted, broadcast once at the end so connected lobby clients see the new list.
+ var now = DateTime.UtcNow;
+ if (now - _lastExpire < ExpireInterval)
+ {
+ return;
+ }
+
+ _lastExpire = now;
+
+ foreach (var entry in (await base.GetAll()).Where(gameEntry => gameEntry.TimeStamp < now - _maxGameAge))
{
await base.Delete(entry.GetName());
}
diff --git a/BtDamageResolver/src/Api/Api.csproj b/BtDamageResolver/src/Api/Api.csproj
index 3e82994a..3a63e761 100644
--- a/BtDamageResolver/src/Api/Api.csproj
+++ b/BtDamageResolver/src/Api/Api.csproj
@@ -5,21 +5,19 @@
Faemiyah.BtDamageResolver.Api
Faemiyah.BtDamageResolver.Api
true
- 0.0.440
- https://github.com/Warmag2/BtDamageResolver
- Warma
- Faemiyah
- BtDamageResolver
- Faemiyah 2020
- https://github.com/Warmag2/BtDamageResolver
+ 0.0.467
API classes and interfaces for BtDamageResolver. Needed to connect to and interface with a cluster.
+ README.md
-
-
-
-
+
+
+
+
+
+
+
diff --git a/BtDamageResolver/src/Api/ClientInterface/Communicators/RedisClientToServerCommunicator.cs b/BtDamageResolver/src/Api/ClientInterface/Communicators/RedisClientToServerCommunicator.cs
index 64c0d4f4..87691e45 100644
--- a/BtDamageResolver/src/Api/ClientInterface/Communicators/RedisClientToServerCommunicator.cs
+++ b/BtDamageResolver/src/Api/ClientInterface/Communicators/RedisClientToServerCommunicator.cs
@@ -96,7 +96,7 @@ protected override async Task RunProcessorMethod(Envelope envelope)
protected override void SubscribeAdditional()
{
var listenedClientQueue = RedisSubscriber.Subscribe(new RedisChannel(ClientStreamAddress, RedisChannel.PatternMode.Literal));
- listenedClientQueue.OnMessage(async channelMessage => await RunProcessorMethod(JsonSerializer.Deserialize(channelMessage.Message.ToString(), JsonSerializerOptions)).ConfigureAwait(false));
+ listenedClientQueue.OnMessage(ProcessChannelMessage);
base.SubscribeAdditional();
}
diff --git a/BtDamageResolver/src/Api/ClientInterface/Communicators/RedisCommunicator.cs b/BtDamageResolver/src/Api/ClientInterface/Communicators/RedisCommunicator.cs
index ac70512a..1a714c51 100644
--- a/BtDamageResolver/src/Api/ClientInterface/Communicators/RedisCommunicator.cs
+++ b/BtDamageResolver/src/Api/ClientInterface/Communicators/RedisCommunicator.cs
@@ -1,4 +1,6 @@
-using System.Text.Json;
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
using System.Threading.Tasks;
using Faemiyah.BtDamageResolver.Api.ClientInterface.Compression;
using Faemiyah.BtDamageResolver.Api.ClientInterface.Events;
@@ -12,7 +14,7 @@ namespace Faemiyah.BtDamageResolver.Api.ClientInterface.Communicators;
///
/// Base Redis implementation of a communicator.
///
-public abstract class RedisCommunicator
+public abstract class RedisCommunicator : IDisposable
{
///
/// The common client stream address.
@@ -107,12 +109,39 @@ protected virtual void SubscribeAdditional()
///
/// The target where to send data.
/// The data to send.
- /// The number of clients the message was delivered to.
- protected long SendEnvelope(string target, Envelope data)
+ ///
+ /// Messages are published with , so the publish does not
+ /// wait for, and cannot report, the number of subscribers that received it.
+ ///
+ protected void SendEnvelope(string target, Envelope data)
+ {
+ PublishToChannel(target, JsonSerializer.Serialize(data, JsonSerializerOptions));
+ }
+
+ ///
+ /// Sends the same envelope to many communication endpoints, serializing it only once.
+ ///
+ /// The targets where to send data.
+ /// The data to send.
+ ///
+ /// The envelope is identical for every recipient, so it is serialized a single time and the resulting
+ /// payload (which base64-encodes the already-compressed data) is reused for each channel, rather than
+ /// re-serializing it once per target.
+ ///
+ protected void SendEnvelopeToMany(IEnumerable targets, Envelope data)
+ {
+ var serializedEnvelope = JsonSerializer.Serialize(data, JsonSerializerOptions);
+ foreach (var target in targets)
+ {
+ PublishToChannel(target, serializedEnvelope);
+ }
+ }
+
+ private void PublishToChannel(string target, string serializedEnvelope)
{
var channel = new RedisChannel(target, RedisChannel.PatternMode.Literal);
CheckChannelConnection(channel);
- return RedisSubscriber.Publish(channel, JsonSerializer.Serialize(data, JsonSerializerOptions), CommandFlags.FireAndForget);
+ RedisSubscriber.Publish(channel, serializedEnvelope, CommandFlags.FireAndForget);
}
///
@@ -126,18 +155,13 @@ protected void SendErrorMessage(string target, string errorMessage)
}
///
- /// Send a message to a single client and display warnings if necessary.
+ /// Send a message to a single client.
///
/// The target where to send data.
/// The data to send.
protected void SendSingle(string clientName, Envelope envelope)
{
- var clientCount = SendEnvelope(clientName, envelope);
-
- if (clientCount != 0)
- {
- Logger.LogWarning("Number of clients receiving targeted message with target {Target} was {Count} instead of 1 as expected.", clientName, clientCount);
- }
+ SendEnvelope(clientName, envelope);
}
///
@@ -161,10 +185,33 @@ private void Subscribe()
{
RedisSubscriber = _redisConnectionMultiplexer.GetSubscriber();
_listenedMessageQueue = RedisSubscriber.Subscribe(_listenTarget);
- _listenedMessageQueue.OnMessage(async channelMessage => await RunProcessorMethod(JsonSerializer.Deserialize(channelMessage.Message.ToString(), JsonSerializerOptions)).ConfigureAwait(false));
+ _listenedMessageQueue.OnMessage(ProcessChannelMessage);
SubscribeAdditional();
}
+ ///
+ /// Deserializes and processes a single channel message, logging any unhandled exception.
+ ///
+ /// The raw channel message.
+ /// A task which completes when the message has been processed.
+ ///
+ /// observes the returned task only to
+ /// serialize processing; it does not surface faults. Without this guard any exception thrown while handling a
+ /// message would be silently swallowed (effectively async void), so we catch and log here.
+ ///
+ protected async Task ProcessChannelMessage(ChannelMessage channelMessage)
+ {
+ try
+ {
+ var envelope = JsonSerializer.Deserialize(channelMessage.Message.ToString(), JsonSerializerOptions);
+ await RunProcessorMethod(envelope).ConfigureAwait(false);
+ }
+ catch (Exception exception)
+ {
+ Logger.LogError(exception, "Unhandled exception while processing a message on channel {Channel}.", channelMessage.Channel.ToString());
+ }
+ }
+
///
/// Unsubscribe from the necessary communication endpoints.
///
@@ -175,4 +222,30 @@ private void Unsubscribe()
_listenedMessageQueue = null;
RedisSubscriber = null;
}
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Releases the Redis resources held by this communicator.
+ ///
+ /// true when called from .
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposing)
+ {
+ return;
+ }
+
+ _listenedMessageQueue?.Unsubscribe();
+ _listenedMessageQueue = null;
+ RedisSubscriber?.UnsubscribeAll();
+ RedisSubscriber = null;
+ _redisConnectionMultiplexer?.Dispose();
+ _redisConnectionMultiplexer = null;
+ }
}
\ No newline at end of file
diff --git a/BtDamageResolver/src/Api/ClientInterface/Communicators/RedisServerToClientCommunicator.cs b/BtDamageResolver/src/Api/ClientInterface/Communicators/RedisServerToClientCommunicator.cs
index 511863da..ae4fe572 100644
--- a/BtDamageResolver/src/Api/ClientInterface/Communicators/RedisServerToClientCommunicator.cs
+++ b/BtDamageResolver/src/Api/ClientInterface/Communicators/RedisServerToClientCommunicator.cs
@@ -38,12 +38,7 @@ public void Send(string clientName, string envelopeType, TType data)
public void SendToAll(string envelopeType, TType data)
where TType : class
{
- var clientCount = SendEnvelope(ClientStreamAddress, new Envelope(envelopeType, DataHelper.Pack(data)));
-
- if (clientCount == 0)
- {
- Logger.LogWarning("SendEnvelope delivered a global message of type {EnvelopeType} to zero clients.", envelopeType);
- }
+ SendEnvelope(ClientStreamAddress, new Envelope(envelopeType, DataHelper.Pack(data)));
}
///
@@ -51,10 +46,7 @@ public void SendToMany(List clientNames, string envelopeType, TTy
where TType : class
{
var envelope = new Envelope(envelopeType, DataHelper.Pack(data));
- foreach (var clientName in clientNames)
- {
- SendSingle(clientName, envelope);
- }
+ SendEnvelopeToMany(clientNames, envelope);
}
///
@@ -123,7 +115,7 @@ protected override async Task RunProcessorMethod(Envelope envelope)
await HandleGetGameStateRequest(envelope.Data, envelope.CorrelationId);
break;
case RequestNames.GetPlayerOptions:
- await HandleGetGameOptionsRequest(envelope.Data, envelope.CorrelationId);
+ await HandleGetPlayerOptionsRequest(envelope.Data, envelope.CorrelationId);
break;
case RequestNames.ForceReady:
await HandleForceReadyRequest(envelope.Data, envelope.CorrelationId);
diff --git a/BtDamageResolver/src/Api/ClientInterface/Compression/CompressionOptions.cs b/BtDamageResolver/src/Api/ClientInterface/Compression/CompressionOptions.cs
new file mode 100644
index 00000000..eb643594
--- /dev/null
+++ b/BtDamageResolver/src/Api/ClientInterface/Compression/CompressionOptions.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace Faemiyah.BtDamageResolver.Api.ClientInterface.Compression;
+
+///
+/// Defines options for on-wire compression of Redis pub/sub payloads.
+///
+[Serializable]
+public class CompressionOptions
+{
+ ///
+ /// The compression algorithm to use. Server and client must agree.
+ ///
+ public CompressionProvider Provider { get; set; } = CompressionProvider.Lzma;
+
+ ///
+ /// The compression quality (0-11). Higher = better ratio but more CPU; 4 is a balanced default.
+ /// Mapped through to BrotliEncoder quality for Brotli and NumFastBytes for LZMA.
+ ///
+ ///
+ /// Brotli accepts 0-11, where 0 is no compression and 11 is maximum compression.
+ /// LZMA accepts 5-275, where 5 is some compression 273 is more compression.
+ /// Values above or below the ranges for the provider will be clamped to the maximum.
+ ///
+ public int Quality { get; set; } = 4;
+}
diff --git a/BtDamageResolver/src/Api/ClientInterface/Compression/CompressionProvider.cs b/BtDamageResolver/src/Api/ClientInterface/Compression/CompressionProvider.cs
new file mode 100644
index 00000000..e6f8afbd
--- /dev/null
+++ b/BtDamageResolver/src/Api/ClientInterface/Compression/CompressionProvider.cs
@@ -0,0 +1,17 @@
+namespace Faemiyah.BtDamageResolver.Api.ClientInterface.Compression;
+
+///
+/// Compression algorithms supported for on-wire Redis pub/sub payloads.
+///
+public enum CompressionProvider
+{
+ ///
+ /// LZMA via the project-local CompressionLzma implementation. Highest ratio, slowest CPU.
+ ///
+ Lzma,
+
+ ///
+ /// Brotli via System.IO.Compression. Much faster than LZMA at comparable ratios for small JSON.
+ ///
+ Brotli,
+}
diff --git a/BtDamageResolver/src/Api/ClientInterface/Compression/DataHelper.cs b/BtDamageResolver/src/Api/ClientInterface/Compression/DataHelper.cs
index 70c5c247..a00813dc 100644
--- a/BtDamageResolver/src/Api/ClientInterface/Compression/DataHelper.cs
+++ b/BtDamageResolver/src/Api/ClientInterface/Compression/DataHelper.cs
@@ -1,4 +1,6 @@
-using System.Text;
+using System;
+using System.IO;
+using System.IO.Compression;
using System.Text.Json;
using Microsoft.Extensions.Options;
using SevenZip.Compression.LZMA;
@@ -11,14 +13,17 @@ namespace Faemiyah.BtDamageResolver.Api.ClientInterface.Compression;
public class DataHelper
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
+ private readonly CompressionOptions _compressionOptions;
///
/// Initializes a new instance of the class.
///
/// The JSON serializer options.
- public DataHelper(IOptions jsonSerializerOptions)
+ /// The compression options.
+ public DataHelper(IOptions jsonSerializerOptions, IOptions compressionOptions)
{
_jsonSerializerOptions = jsonSerializerOptions.Value;
+ _compressionOptions = compressionOptions.Value;
}
///
@@ -30,7 +35,7 @@ public DataHelper(IOptions jsonSerializerOptions)
public byte[] Pack(TType input)
where TType : class
{
- return CompressionHelper.Compress(Serialize(input));
+ return Compress(Serialize(input));
}
///
@@ -42,7 +47,7 @@ public byte[] Pack(TType input)
public TType Unpack(byte[] input)
where TType : class
{
- return Deserialize(CompressionHelper.Decompress(input));
+ return Deserialize(Decompress(input));
}
///
@@ -54,7 +59,7 @@ public TType Unpack(byte[] input)
private TType Deserialize(byte[] input)
where TType : class
{
- return JsonSerializer.Deserialize(Encoding.UTF8.GetString(input), _jsonSerializerOptions);
+ return JsonSerializer.Deserialize(input, _jsonSerializerOptions);
}
///
@@ -66,6 +71,50 @@ private TType Deserialize(byte[] input)
private byte[] Serialize(TType input)
where TType : class
{
- return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(input, _jsonSerializerOptions));
+ return JsonSerializer.SerializeToUtf8Bytes(input, _jsonSerializerOptions);
}
-}
\ No newline at end of file
+
+ private byte[] Compress(byte[] input)
+ {
+ return _compressionOptions.Provider switch
+ {
+ CompressionProvider.Brotli => BrotliCompress(input, _compressionOptions.Quality),
+ CompressionProvider.Lzma => CompressionHelper.Compress(input, _compressionOptions.Quality),
+ _ => throw new InvalidOperationException($"Unsupported compression provider: {_compressionOptions.Provider}")
+ };
+ }
+
+ private byte[] Decompress(byte[] input)
+ {
+ return _compressionOptions.Provider switch
+ {
+ CompressionProvider.Brotli => BrotliDecompress(input),
+ CompressionProvider.Lzma => CompressionHelper.Decompress(input),
+ _ => throw new InvalidOperationException($"Unsupported compression provider: {_compressionOptions.Provider}")
+ };
+ }
+
+ private static byte[] BrotliCompress(byte[] input, int quality)
+ {
+ var clamped = Math.Clamp(quality, 0, 11);
+ var maxLength = BrotliEncoder.GetMaxCompressedLength(input.Length);
+ var output = new byte[maxLength];
+
+ if (!BrotliEncoder.TryCompress(input, output, out var bytesWritten, clamped, window: 22))
+ {
+ throw new InvalidOperationException("Brotli compression failed: destination buffer too small.");
+ }
+
+ return output.AsSpan(0, bytesWritten).ToArray();
+ }
+
+ private static byte[] BrotliDecompress(byte[] input)
+ {
+ using var inputStream = new MemoryStream(input);
+ using var outputStream = new MemoryStream();
+ using var brotli = new BrotliStream(inputStream, CompressionMode.Decompress);
+ brotli.CopyTo(outputStream);
+
+ return outputStream.ToArray();
+ }
+}
diff --git a/BtDamageResolver/src/Api/ClientInterface/Repositories/CachedEntityRepository.cs b/BtDamageResolver/src/Api/ClientInterface/Repositories/CachedEntityRepository.cs
index 2d1f03f9..79cce80b 100644
--- a/BtDamageResolver/src/Api/ClientInterface/Repositories/CachedEntityRepository.cs
+++ b/BtDamageResolver/src/Api/ClientInterface/Repositories/CachedEntityRepository.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -20,19 +21,25 @@ public class CachedEntityRepository : IEntityRepository> _logger;
private readonly IEntityRepository _repository;
- private readonly Dictionary _cache;
+ private readonly ConcurrentDictionary _cache;
///
/// Initializes a new instance of the class.
///
/// The logging interface.
/// The repository to cache.
+ ///
+ /// This cache is a DI singleton accessed concurrently from both the owning *RepositoryActor grain
+ /// (single-threaded writes) and directly from many LogicUnit instances during fire events
+ /// (concurrent reads from arbitrary grain activations). A
+ /// is required to make this safe.
+ ///
public CachedEntityRepository(ILogger> logger, IEntityRepository repository)
{
_logger = logger;
_repository = repository;
- _cache = [];
- _logger.LogInformation("Filled entity cache for type {Type} with {Number} items.", typeof(TEntity).Name, FillCache().Result);
+ _cache = new ConcurrentDictionary();
+ _logger.LogInformation("Filled entity cache for type {Type} with {Number} items.", typeof(TEntity).Name, FillCache());
}
///
@@ -41,12 +48,15 @@ public async Task AddAsync(TEntity entity)
try
{
await _repository.AddAsync(entity);
- _cache.Add(entity.GetName(), entity);
+ if (!_cache.TryAdd(entity.GetName(), entity))
+ {
+ throw new ArgumentException($"An item with key '{entity.GetName()}' already exists in the cache.");
+ }
}
catch (Exception ex)
{
- _logger.LogError(ex, "Could not add entity {EntityName} into the repository. Unknown failure.", entity.GetName());
- throw new DataAccessException(DataAccessErrorCode.OperationFailure);
+ _logger.LogError(ex, "Could not add entity {EntityName} into the repository.", entity.GetName());
+ throw new DataAccessException(DataAccessErrorCode.OperationFailure, $"Could not add entity {entity.GetName()} into the repository.", ex);
}
}
@@ -56,19 +66,12 @@ public async Task AddOrUpdateAsync(TEntity entity)
try
{
await _repository.AddOrUpdateAsync(entity);
- if (_cache.ContainsKey(entity.GetName()))
- {
- _cache[entity.GetName()] = entity;
- }
- else
- {
- _cache.Add(entity.GetName(), entity);
- }
+ _cache[entity.GetName()] = entity;
}
catch (Exception ex)
{
- _logger.LogError(ex, "Could not add or update entity {EntityName} in the repository. Unknown failure.", entity.GetName());
- throw new DataAccessException(DataAccessErrorCode.OperationFailure);
+ _logger.LogError(ex, "Could not add or update entity {EntityName} in the repository.", entity.GetName());
+ throw new DataAccessException(DataAccessErrorCode.OperationFailure, $"Could not add or update entity {entity.GetName()} in the repository.", ex);
}
}
@@ -77,20 +80,15 @@ public async Task DeleteAsync(TKey key)
{
try
{
- if (_cache.ContainsKey(key))
- {
- await _repository.DeleteAsync(key);
- _cache.Remove(key);
+ var deleted = await _repository.DeleteAsync(key);
+ _cache.TryRemove(key, out _);
- return true;
- }
-
- return false;
+ return deleted;
}
catch (Exception ex)
{
- _logger.LogError(ex, "Could not delete entity {EntityName} from the repository. Unknown failure.", key);
- throw new DataAccessException(DataAccessErrorCode.OperationFailure);
+ _logger.LogError(ex, "Could not delete entity {EntityName} from the repository.", key);
+ throw new DataAccessException(DataAccessErrorCode.OperationFailure, $"Could not delete entity {key} from the repository.", ex);
}
}
@@ -111,11 +109,20 @@ public IReadOnlyCollection GetAllKeys()
{
var keys = _repository.GetAllKeys();
- // Update local cache in this situation
- foreach (var key in keys.Where(key => !_cache.ContainsKey(key)))
+ // Update local cache for any keys present in the backing store but missing here
+ // (typically entries added by another process — e.g. the client adding a Unit).
+ foreach (var key in keys)
{
+ if (_cache.ContainsKey(key))
+ {
+ continue;
+ }
+
var item = _repository.Get(key);
- _cache.Add(item.GetName(), item);
+ if (item != null)
+ {
+ _cache.TryAdd(key, item);
+ }
}
return keys;
@@ -131,18 +138,18 @@ public async Task UpdateAsync(TEntity entity)
}
catch (Exception ex)
{
- _logger.LogError(ex, "Could not update entity {EntityName} in repository. Unknown failure.", entity.GetName());
- throw new DataAccessException(DataAccessErrorCode.OperationFailure);
+ _logger.LogError(ex, "Could not update entity {EntityName} in repository.", entity.GetName());
+ throw new DataAccessException(DataAccessErrorCode.OperationFailure, $"Could not update entity {entity.GetName()} in the repository.", ex);
}
}
- private async Task FillCache()
+ private int FillCache()
{
var items = _repository.GetAll();
var count = 0;
foreach (var item in items)
{
- _cache.Add(item.GetName(), item);
+ _cache.TryAdd(item.GetName(), item);
count++;
}
diff --git a/BtDamageResolver/src/Api/ClientInterface/Repositories/RedisEntityRepository.cs b/BtDamageResolver/src/Api/ClientInterface/Repositories/RedisEntityRepository.cs
index 9c31df30..735ef658 100644
--- a/BtDamageResolver/src/Api/ClientInterface/Repositories/RedisEntityRepository.cs
+++ b/BtDamageResolver/src/Api/ClientInterface/Repositories/RedisEntityRepository.cs
@@ -1,6 +1,5 @@
-using System;
+using System;
using System.Collections.Generic;
-using System.Data.Common;
using System.Text.Json;
using System.Threading.Tasks;
using Faemiyah.BtDamageResolver.Api.Entities.Interfaces;
@@ -14,7 +13,7 @@
namespace Faemiyah.BtDamageResolver.Api.ClientInterface.Repositories;
///
-/// A redis-based entity repository, which directly stores all entities into database 0.
+/// A Redis-based entity repository, which directly stores all entities into database 0.
/// Entities are stored with their string-based name, and prefixed with Resolver{EntityName}.
///
/// The entity to store.
@@ -22,25 +21,23 @@ namespace Faemiyah.BtDamageResolver.Api.ClientInterface.Repositories;
public class RedisEntityRepository : IEntityRepository
where TEntity : class, IEntity
{
- private readonly string _connectionString;
private readonly string _keyPrefix;
private readonly ILogger> _logger;
private readonly JsonSerializerOptions _jsonSerializerOptions;
- private readonly ConnectionMultiplexer _redisConnectionMultiplexer;
+ private readonly ConnectionMultiplexer _connectionMultiplexer;
///
/// Initializes a new instance of the class.
///
/// The logging interface.
- /// JSON serializer options.
+ /// JSON serializer options.
/// The connection string.
- public RedisEntityRepository(ILogger> logger, IOptions jsonSerializerSettings, string connectionString)
+ public RedisEntityRepository(ILogger> logger, IOptions jsonSerializerOptions, string connectionString)
{
_logger = logger;
- _jsonSerializerOptions = jsonSerializerSettings.Value;
- _connectionString = connectionString;
+ _jsonSerializerOptions = jsonSerializerOptions.Value;
_keyPrefix = $"Resolver{typeof(TEntity).Name}";
- _redisConnectionMultiplexer = ConnectionMultiplexer.Connect(_connectionString);
+ _connectionMultiplexer = ConnectionMultiplexer.Connect(connectionString);
}
///
@@ -51,15 +48,10 @@ public async Task AddAsync(TEntity entity)
var connection = GetConnection();
await connection.StringSetAsync(GetKey(entity), JsonSerializer.Serialize(entity, _jsonSerializerOptions)).ConfigureAwait(false);
}
- catch (DbException ex)
- {
- _logger.LogError(ex, "Could not add entity {EntityName} of type {EntityType} into redis database. Database failure with error code {Code}.", entity.GetName(), typeof(TEntity), ex.ErrorCode);
- throw new DataAccessException(DataAccessErrorCode.OperationFailure);
- }
catch (Exception ex)
{
- _logger.LogError(ex, "Could not add entity {EntityName} of type {EntityType} into redis database. Unknown failure.", entity.GetName(), typeof(TEntity));
- throw new DataAccessException(DataAccessErrorCode.OperationFailure);
+ _logger.LogError(ex, "Could not add entity {EntityName} of type {EntityType} into the database.", entity.GetName(), typeof(TEntity));
+ throw new DataAccessException(DataAccessErrorCode.OperationFailure, $"Could not add entity {entity.GetName()} of type {typeof(TEntity)} into the database.", ex);
}
}
@@ -78,15 +70,10 @@ public async Task DeleteAsync(string key)
var connection = GetConnection();
return await connection.KeyDeleteAsync(GetKey(key)).ConfigureAwait(false);
}
- catch (DbException ex)
- {
- _logger.LogError(ex, "Could not delete entity {EntityName} of type {EntityType}. Database failure with error code {Code}.", key, typeof(TEntity), ex.ErrorCode);
- throw new DataAccessException(DataAccessErrorCode.OperationFailure);
- }
catch (Exception ex)
{
- _logger.LogError(ex, "Could not delete entity {EntityName} of type {EntityType}. Unknown failure.", key, typeof(TEntity));
- throw new DataAccessException(DataAccessErrorCode.OperationFailure);
+ _logger.LogError(ex, "Could not delete entity {EntityName} of type {EntityType}.", key, typeof(TEntity));
+ throw new DataAccessException(DataAccessErrorCode.OperationFailure, $"Could not delete entity {key} of type {typeof(TEntity)}.", ex);
}
}
@@ -106,15 +93,10 @@ public TEntity Get(string key)
return null;
}
- catch (DbException ex)
- {
- _logger.LogError(ex, "Could not get entity {EntityName} of type {EntityType}. Database failure with error code {Code}.", key, typeof(TEntity), ex.ErrorCode);
- throw new DataAccessException(DataAccessErrorCode.OperationFailure);
- }
catch (Exception ex)
{
- _logger.LogError(ex, "Could not get entity {EntityName} of type {EntityType}. Unknown failure.", key, typeof(TEntity));
- throw new DataAccessException(DataAccessErrorCode.OperationFailure);
+ _logger.LogError(ex, "Could not get entity {EntityName} of type {EntityType}.", key, typeof(TEntity));
+ throw new DataAccessException(DataAccessErrorCode.OperationFailure, $"Could not get entity {key} of type {typeof(TEntity)}.", ex);
}
}
@@ -136,15 +118,10 @@ public IReadOnlyCollection GetAll()
{
throw;
}
- catch (DbException ex)
- {
- _logger.LogError(ex, "Could not get all entities of type {EntityType}. Database failure with error code {Code}.", typeof(TEntity), ex.ErrorCode);
- throw new DataAccessException(DataAccessErrorCode.OperationFailure);
- }
catch (Exception ex)
{
- _logger.LogError(ex, "Could not get all entities of type {EntityType}. Unknown failure.", typeof(TEntity));
- throw new DataAccessException(DataAccessErrorCode.OperationFailure);
+ _logger.LogError(ex, "Could not get all entities of type {EntityType}.", typeof(TEntity));
+ throw new DataAccessException(DataAccessErrorCode.OperationFailure, $"Could not get all entities of type {typeof(TEntity)}.", ex);
}
}
@@ -156,15 +133,10 @@ public IReadOnlyCollection