From 17ffc55debfad9daddb2218807306472dd033424 Mon Sep 17 00:00:00 2001
From: OBilo <54061981+Keshash@users.noreply.github.com>
Date: Tue, 13 Jan 2026 19:45:03 +0200
Subject: [PATCH 1/4] Subsequent shot rebalance draft
---
Defs/GameSetupSteps/SubsequentShotCurve.xml | 19 ++++++++++++++++++
.../GameSetupStep_SubsequentShotCurve.cs | 20 +++++++++++++++++++
.../CombatExtended/ModSettings/Settings.cs | 8 +++++++-
.../Verbs/Verb_LaunchProjectileCE.cs | 3 ++-
.../CombatExtended/Verbs/Verb_ShootCE.cs | 14 ++++++++-----
5 files changed, 57 insertions(+), 7 deletions(-)
create mode 100644 Defs/GameSetupSteps/SubsequentShotCurve.xml
create mode 100644 Source/CombatExtended/CombatExtended/GameSetupSteps/GameSetupStep_SubsequentShotCurve.cs
diff --git a/Defs/GameSetupSteps/SubsequentShotCurve.xml b/Defs/GameSetupSteps/SubsequentShotCurve.xml
new file mode 100644
index 0000000000..2eb1f3805d
--- /dev/null
+++ b/Defs/GameSetupSteps/SubsequentShotCurve.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ CE_SubsequentShotCurve
+ 1000
+
+
+
+ (0, 0)
+ (1.5, 0.60)
+ (1.9, 0.80)
+ (13.0, 0.95)
+ (17.0, 1.0)
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/CombatExtended/CombatExtended/GameSetupSteps/GameSetupStep_SubsequentShotCurve.cs b/Source/CombatExtended/CombatExtended/GameSetupSteps/GameSetupStep_SubsequentShotCurve.cs
new file mode 100644
index 0000000000..a8e0f4184e
--- /dev/null
+++ b/Source/CombatExtended/CombatExtended/GameSetupSteps/GameSetupStep_SubsequentShotCurve.cs
@@ -0,0 +1,20 @@
+using Verse;
+
+namespace CombatExtended;
+
+public class GameSetupStep_SubsequentShotCurve : GameSetupStep
+{
+ public SimpleCurve defaultSubsequentShotCurve;
+
+ public override int SeedPart => 58224852; // unused, but required
+
+ public override void GenerateFresh()
+ {
+ Verb_ShootCE.SubsequentShotCurve = defaultSubsequentShotCurve;
+ }
+
+ public override void GenerateFromScribe()
+ {
+ Verb_ShootCE.SubsequentShotCurve = defaultSubsequentShotCurve;
+ }
+}
diff --git a/Source/CombatExtended/CombatExtended/ModSettings/Settings.cs b/Source/CombatExtended/CombatExtended/ModSettings/Settings.cs
index 287a1660c3..e74261fd95 100755
--- a/Source/CombatExtended/CombatExtended/ModSettings/Settings.cs
+++ b/Source/CombatExtended/CombatExtended/ModSettings/Settings.cs
@@ -41,6 +41,7 @@ public class Settings : ModSettings, ISettingsCE
private bool midBurstRetarget = true;
private bool fasterRepeatShots = true;
+ private float fasterRepeatShotsRecoilMult = 1.0f;
private float explosionPenMultiplier = 1.0f;
private float explosionFalloffFactor = 1.0f;
@@ -160,6 +161,7 @@ public class Settings : ModSettings, ISettingsCE
public float FragmentsFromWallsIntensity => fragmentsFromWallsIntensity;
public bool FasterRepeatShots => fasterRepeatShots;
+ public float FasterRepeatShotsRecoilMult => fasterRepeatShotsRecoilMult;
public bool MidBurstRetarget => midBurstRetarget;
public float ExplosionPenMultiplier => explosionPenMultiplier;
@@ -256,6 +258,7 @@ public override void ExposeData()
Scribe_Values.Look(ref fragmentsFromWallsReflected, "fragmentsFromWallsReflected", false);
Scribe_Values.Look(ref fragmentsFromWallsIntensity, "fragmentsFromWallsIntensity", 1.0f);
Scribe_Values.Look(ref fasterRepeatShots, "fasterRepeatShots", false);
+ Scribe_Values.Look(ref fasterRepeatShotsRecoilMult, "fasterRepeatShotsRecoilMult", 1.0f);
Scribe_Values.Look(ref midBurstRetarget, "midBurstRetarget", true);
Scribe_Values.Look(ref explosionPenMultiplier, "explosionPenMultiplier", 1.0f);
Scribe_Values.Look(ref explosionFalloffFactor, "explosionFalloffFactor", 1.0f);
@@ -305,7 +308,6 @@ private void DoSettingsWindowContents_Mechanics(Listing_Standard list)
left.CheckboxLabeled("CE_Settings_AllowMeleeHunting_Title".Translate(), ref allowMeleeHunting, "CE_Settings_AllowMeleeHunting_Desc".Translate());
left.CheckboxLabeled("CE_Settings_SmokeEffects_Title".Translate(), ref smokeEffects, "CE_Settings_SmokeEffects_Desc".Translate());
left.CheckboxLabeled("CE_Settings_TurretsBreakShields_Title".Translate(), ref turretsBreakShields, "CE_Settings_TurretsBreakShields_Desc".Translate());
- left.CheckboxLabeled("CE_Settings_FasterRepeatShots_Title".Translate(), ref fasterRepeatShots, "CE_Settings_FasterRepeatShots_Desc".Translate());
left.CheckboxLabeled("CE_Settings_MidBurstRetarget_Title".Translate(), ref midBurstRetarget, "CE_Settings_MidBurstRetarget_Desc".Translate());
left.CheckboxLabeled("CE_Settings_EnableArcOfFire_Title".Translate(), ref enableArcOfFire, "CE_Settings_EnableArcOfFire_Desc".Translate());
left.CheckboxLabeled("CE_Settings_EnableCIWS".Translate(), ref enableCIWS, "CE_Settings_EnableCIWS_Desc".Translate());
@@ -333,6 +335,9 @@ private void DoSettingsWindowContents_Mechanics(Listing_Standard list)
right.Label("CE_Settings_StabilizationSettings".Translate());
right.Gap();
medicineSearchRadius = right.SliderLabeled("CE_Settings_MedicineSearchRadius_Title".Translate() + ": " + medicineSearchRadius.ToString("F0"), medicineSearchRadius, 1f, 100f, tooltip: "CE_Settings_MedicineSearchRadius_Desc".Translate(), labelPct: 0.6f);
+ right.GapLine();
+ left.CheckboxLabeled("CE_Settings_FasterRepeatShots_Title".Translate(), ref fasterRepeatShots, "CE_Settings_FasterRepeatShots_Desc".Translate());
+ fasterRepeatShotsRecoilMult = left.SliderLabeled("CE_Settings_FasterRepeatShotsMultiplier_Title".Translate() + ": " + fasterRepeatShotsRecoilMult.ToString("F1"), fasterRepeatShotsRecoilMult, 0.1f, 3f, tooltip: "CE_Settings_FasterRepeatShotsMultiplier_Desc".Translate(), labelPct: 0.6f);
right.End();
}
@@ -499,6 +504,7 @@ private void ResetToDefault_Mechanics()
smokeEffects = true;
turretsBreakShields = true;
fasterRepeatShots = true;
+ fasterRepeatShotsRecoilMult = 1.0f;
midBurstRetarget = true;
enableCIWS = true;
fragmentsFromWalls = false;
diff --git a/Source/CombatExtended/CombatExtended/Verbs/Verb_LaunchProjectileCE.cs b/Source/CombatExtended/CombatExtended/Verbs/Verb_LaunchProjectileCE.cs
index 94cc616319..f074fa085d 100644
--- a/Source/CombatExtended/CombatExtended/Verbs/Verb_LaunchProjectileCE.cs
+++ b/Source/CombatExtended/CombatExtended/Verbs/Verb_LaunchProjectileCE.cs
@@ -620,7 +620,8 @@ private void GetRecoilVec(ref float rotation, ref float angle)
rotation += recoilMagnitude * Rand.Range(minX, maxX);
var trd = Rand.Range(minY, maxY);
angle += recoilMagnitude * Mathf.Deg2Rad * trd;
- lastRecoilDeg += nextRecoilMagnitude * trd;
+ Log.Message($"{EquipmentSource.def.label}: {lastRecoilDeg + nextRecoilMagnitude * recoil} = {lastRecoilDeg} + {nextRecoilMagnitude} * {recoil}");
+ lastRecoilDeg += nextRecoilMagnitude * recoil; //always store max recoil for subsequent shot purposes
}
///
diff --git a/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs b/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs
index c964e2486c..1f76187728 100755
--- a/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs
+++ b/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs
@@ -30,6 +30,8 @@ public class Verb_ShootCE : Verb_LaunchProjectileCE
public Vector3 drawPos;
+ internal static SimpleCurve SubsequentShotCurve = new SimpleCurve(); // Populated on game start from a SetupStepDef
+
#endregion
#region Properties
@@ -327,13 +329,15 @@ public override void RecalculateWarmupTicks()
var d = v - u;
var newShotRotation = (-90 + Mathf.Rad2Deg * Mathf.Atan2(d.z, d.x)) % 360;
- var delta = Mathf.Abs(newShotRotation - lastShotRotation) + lastRecoilDeg;
- lastRecoilDeg = 0;
+ var delta = Mathf.Abs(newShotRotation - lastShotRotation);
+
var maxReduction = storedShotReduction ?? (CompFireModes?.CurrentAimMode == AimMode.SuppressFire ?
- 0.1f :
- (_isAiming ? 0.5f : 0.25f));
- var reduction = Mathf.Max(maxReduction, delta / 45f);
+ 0.1f + RecoilAmount*RecoilAmount/10 :
+ (_isAiming ? 0.5f : 0.25f)) + RecoilAmount*RecoilAmount/10;
+ var reduction = Mathf.Max(maxReduction, delta / 45f + SubsequentShotCurve.Evaluate(lastRecoilDeg) * Controller.settings.FasterRepeatShotsRecoilMult);
+ lastRecoilDeg = 0;
storedShotReduction = reduction;
+ Log.Message($"reduction {reduction}; maxReduction {maxReduction} lastRecoilDeg {lastRecoilDeg}; storedShotReduction {storedShotReduction};");
if (reduction < 1.0f)
{
From f33b34c28c40ac20de0017c437f9d686f199b8d9 Mon Sep 17 00:00:00 2001
From: OBilo <54061981+Keshash@users.noreply.github.com>
Date: Thu, 29 Jan 2026 22:55:05 +0200
Subject: [PATCH 2/4] Add translation strings
---
Languages/English/Keyed/ModMenu.xml | 3 +++
Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/Languages/English/Keyed/ModMenu.xml b/Languages/English/Keyed/ModMenu.xml
index fa154a3ed4..455c2fa741 100644
--- a/Languages/English/Keyed/ModMenu.xml
+++ b/Languages/English/Keyed/ModMenu.xml
@@ -108,6 +108,9 @@
Controls how many wall fragments will spawn and how big they will be.
Fragments from walls intensity
+ Controls how fast subsequent shot bonuses will accumulate.
+ Subsequent shot difficulty
+
When enabled, subsequent shots at the same, or nearby, targets are faster.
Faster subsequent shots
diff --git a/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs b/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs
index 1f76187728..ca6745b338 100755
--- a/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs
+++ b/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs
@@ -336,8 +336,8 @@ public override void RecalculateWarmupTicks()
(_isAiming ? 0.5f : 0.25f)) + RecoilAmount*RecoilAmount/10;
var reduction = Mathf.Max(maxReduction, delta / 45f + SubsequentShotCurve.Evaluate(lastRecoilDeg) * Controller.settings.FasterRepeatShotsRecoilMult);
lastRecoilDeg = 0;
- storedShotReduction = reduction;
Log.Message($"reduction {reduction}; maxReduction {maxReduction} lastRecoilDeg {lastRecoilDeg}; storedShotReduction {storedShotReduction};");
+ storedShotReduction = reduction;
if (reduction < 1.0f)
{
From 897ff697bc422f3a4f03531f2e97913158c6216f Mon Sep 17 00:00:00 2001
From: OBilo <54061981+Keshash@users.noreply.github.com>
Date: Fri, 30 Jan 2026 18:31:27 +0200
Subject: [PATCH 3/4] make SS mult accumulate over several bursts, clean up
logging
---
.../CombatExtended/ModSettings/Settings.cs | 5 ++++
.../Verbs/Verb_LaunchProjectileCE.cs | 5 ++--
.../CombatExtended/Verbs/Verb_ShootCE.cs | 25 ++++++++++++++-----
3 files changed, 26 insertions(+), 9 deletions(-)
diff --git a/Source/CombatExtended/CombatExtended/ModSettings/Settings.cs b/Source/CombatExtended/CombatExtended/ModSettings/Settings.cs
index e74261fd95..999848ad2a 100755
--- a/Source/CombatExtended/CombatExtended/ModSettings/Settings.cs
+++ b/Source/CombatExtended/CombatExtended/ModSettings/Settings.cs
@@ -122,6 +122,7 @@ public class Settings : ModSettings, ISettingsCE
private bool debugDisplayCellCoverRating = false;
private bool debugDisplayAttritionInfo = false;
private bool debugWorldShellingDamageRandomness = false;
+ private bool debugSubsequentShotLogging = false;
public bool DebuggingMode => debuggingMode;
public bool DebugVerbose => debugVerbose;
@@ -137,6 +138,7 @@ public class Settings : ModSettings, ISettingsCE
public bool DebugDisplayCellCoverRating => debugDisplayCellCoverRating && debuggingMode;
public bool DebugDisplayAttritionInfo => debugDisplayAttritionInfo && debuggingMode;
public bool DebugWorldShellingDamageRandomness => debugWorldShellingDamageRandomness && debuggingMode;
+ public bool DebugSubsequentShotLogging => debugSubsequentShotLogging && debuggingMode;
#endregion
#region Autopatcher
@@ -222,6 +224,7 @@ public override void ExposeData()
Scribe_Values.Look(ref debugDisplayCellCoverRating, "debugDisplayCellCoverRating", false);
Scribe_Values.Look(ref debugDisplayAttritionInfo, "debugDisplayAttritionInfo", false);
Scribe_Values.Look(ref debugWorldShellingDamageRandomness, "debugWorldShellingDamageRandomness", false);
+ Scribe_Values.Look(ref debugSubsequentShotLogging, "debugSubsequentShotLogging", false);
Scribe_Values.Look(ref debugGenClosetPawn, "debugGenClosetPawn", false);
Scribe_Values.Look(ref debugVerbose, "debugVerbose", false);
Scribe_Values.Look(ref debugMuzzleFlash, "debugMuzzleFlash", false);
@@ -440,6 +443,7 @@ private void DoSettingsWindowContents_Misc(Listing_Standard list)
list.CheckboxLabeled("Display danger buildup within cells", ref debugDisplayDangerBuildup);
list.CheckboxLabeled("Display cover rating of cells of suppressed pawns", ref debugDisplayCellCoverRating);
list.CheckboxLabeled("Disable randomness in world shelling", ref debugWorldShellingDamageRandomness);
+ list.CheckboxLabeled("Log subsequent shot calculations", ref debugSubsequentShotLogging);
}
#endif
list.Gap();
@@ -573,6 +577,7 @@ private void ResetToDefault_Misc()
debugDisplayDangerBuildup = false;
debugDisplayCellCoverRating = false;
debugWorldShellingDamageRandomness = false;
+ debugSubsequentShotLogging = false;
#endif
}
#endregion
diff --git a/Source/CombatExtended/CombatExtended/Verbs/Verb_LaunchProjectileCE.cs b/Source/CombatExtended/CombatExtended/Verbs/Verb_LaunchProjectileCE.cs
index f074fa085d..9f99f8abda 100644
--- a/Source/CombatExtended/CombatExtended/Verbs/Verb_LaunchProjectileCE.cs
+++ b/Source/CombatExtended/CombatExtended/Verbs/Verb_LaunchProjectileCE.cs
@@ -620,8 +620,7 @@ private void GetRecoilVec(ref float rotation, ref float angle)
rotation += recoilMagnitude * Rand.Range(minX, maxX);
var trd = Rand.Range(minY, maxY);
angle += recoilMagnitude * Mathf.Deg2Rad * trd;
- Log.Message($"{EquipmentSource.def.label}: {lastRecoilDeg + nextRecoilMagnitude * recoil} = {lastRecoilDeg} + {nextRecoilMagnitude} * {recoil}");
- lastRecoilDeg += nextRecoilMagnitude * recoil; //always store max recoil for subsequent shot purposes
+ lastRecoilDeg += nextRecoilMagnitude * recoil; //always store non-randomized max recoil for subsequent shot purposes
}
///
@@ -1084,7 +1083,7 @@ public override bool TryCastShot()
didRetarget = Retarget();
repeating = true;
doRetarget = true;
- storedShotReduction = null;
+
var props = VerbPropsCE;
var midBurst = numShotsFired > 0;
var suppressing = CompFireModes?.CurrentAimMode == AimMode.SuppressFire;
diff --git a/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs b/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs
index ca6745b338..4dc1b87ab5 100755
--- a/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs
+++ b/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs
@@ -331,13 +331,26 @@ public override void RecalculateWarmupTicks()
var newShotRotation = (-90 + Mathf.Rad2Deg * Mathf.Atan2(d.z, d.x)) % 360;
var delta = Mathf.Abs(newShotRotation - lastShotRotation);
- var maxReduction = storedShotReduction ?? (CompFireModes?.CurrentAimMode == AimMode.SuppressFire ?
- 0.1f + RecoilAmount*RecoilAmount/10 :
- (_isAiming ? 0.5f : 0.25f)) + RecoilAmount*RecoilAmount/10;
- var reduction = Mathf.Max(maxReduction, delta / 45f + SubsequentShotCurve.Evaluate(lastRecoilDeg) * Controller.settings.FasterRepeatShotsRecoilMult);
+ //max possible reduction for this weapon
+ float maxReduction = CompFireModes?.CurrentAimMode == AimMode.SuppressFire ? 0.1f : _isAiming ? 0.5f : 0.25f;
+ maxReduction += RecoilAmount * RecoilAmount * 0.1f;
+
+ //current reduction after pawn stats
+ float reduction = (delta / 45f) + (SubsequentShotCurve.Evaluate(lastRecoilDeg) * Controller.settings.FasterRepeatShotsRecoilMult);
+
+ if (storedShotReduction != null)
+ {
+ reduction *= (float)storedShotReduction;
+ }
+
+ reduction = Mathf.Max(maxReduction, reduction);
+ if (Controller.settings.DebugSubsequentShotLogging)
+ {
+ Log.Message($"{caster?.LabelShort} ({EquipmentSource?.LabelShort}) subsequent shot:\nreduction {reduction}; storedShotReduction {storedShotReduction}; maxReduction {maxReduction}; lastRecoilDeg {lastRecoilDeg}; delta {delta}; delta/45 {delta/45f}");
+ }
+
lastRecoilDeg = 0;
- Log.Message($"reduction {reduction}; maxReduction {maxReduction} lastRecoilDeg {lastRecoilDeg}; storedShotReduction {storedShotReduction};");
- storedShotReduction = reduction;
+ storedShotReduction = Mathf.Clamp01(reduction);
if (reduction < 1.0f)
{
From 73f48e72963fa1e668c4426c32d691e1028402f6 Mon Sep 17 00:00:00 2001
From: OBilo <54061981+Keshash@users.noreply.github.com>
Date: Fri, 30 Jan 2026 18:48:46 +0200
Subject: [PATCH 4/4] add comments
---
Defs/GameSetupSteps/SubsequentShotCurve.xml | 1 +
Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/Defs/GameSetupSteps/SubsequentShotCurve.xml b/Defs/GameSetupSteps/SubsequentShotCurve.xml
index 2eb1f3805d..d9b6b0ebb4 100644
--- a/Defs/GameSetupSteps/SubsequentShotCurve.xml
+++ b/Defs/GameSetupSteps/SubsequentShotCurve.xml
@@ -6,6 +6,7 @@
1000
+
(0, 0)
(1.5, 0.60)
diff --git a/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs b/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs
index 4dc1b87ab5..650476f968 100755
--- a/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs
+++ b/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs
@@ -346,7 +346,7 @@ public override void RecalculateWarmupTicks()
reduction = Mathf.Max(maxReduction, reduction);
if (Controller.settings.DebugSubsequentShotLogging)
{
- Log.Message($"{caster?.LabelShort} ({EquipmentSource?.LabelShort}) subsequent shot:\nreduction {reduction}; storedShotReduction {storedShotReduction}; maxReduction {maxReduction}; lastRecoilDeg {lastRecoilDeg}; delta {delta}; delta/45 {delta/45f}");
+ Log.Message($"{caster?.LabelShort} ({EquipmentSource?.LabelShort}) subsequent shot:\nreduction {reduction}; storedShotReduction {storedShotReduction}; maxReduction {maxReduction}; lastRecoilDeg {lastRecoilDeg}; delta {delta}; delta/45 {delta / 45f}");
}
lastRecoilDeg = 0;