Skip to content

Commit 5475c70

Browse files
committed
Bring melee stat displays closer to reality
Our current melee stat displays leave much to be desired: * The "Melee DPS" stat shown prominently for pawns is an unpatched vanilla stat, which doesn't take CE's damage scaling into account. * The stuff a weapon is made of isn't factored into stats, which impacts not just damage and cooldown but also the chance to pick a given tool relative to other tools. * The armor penetration stat shown for humans doesn't consider the wielded weapon. So: * Patch the MeleeDPS stat to use our StatWorker. * In StatWorker_MeleeDamageAverage and StatWorker_MeleeArmorPenetration, use the pawn's list of melee verbs when computing the stat for a pawn, since the available verbs impact their weighting relative to each other. For instance, a pawn wielding a club may still choose to fight with fists some % of the time, but won't do so if wielding a masterwork longsword. * Always obtain melee verbs from `VerbUtility.GetAllVerbProperties` when computing stats for a single weapon (which may be wielded by a pawn), so that damage, cooldown and tool use chance can be computed via `VerbProperties` helpers that take stuffing into account. * Show the chance to use a given tool in the armor penetration stat explainer for clarity.
1 parent 818ac48 commit 5475c70

File tree

6 files changed

+379
-197
lines changed

6 files changed

+379
-197
lines changed

Patches/Core/Stats/Stats.xml

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -485,13 +485,28 @@
485485
</value>
486486
</Operation>
487487

488-
<Operation Class="PatchOperationReplace">
488+
<Operation Class="PatchOperationReplace">
489+
<xpath>Defs/StatDef[defName="MeleeWeapon_AverageDPS"]/category</xpath>
490+
<value>
491+
<category>Weapon</category>
492+
</value>
493+
</Operation>
494+
495+
496+
<Operation Class="PatchOperationReplace">
489497
<xpath>Defs/StatDef[defName="MeleeWeapon_AverageDPS"]/displayPriorityInCategory</xpath>
490498
<value>
491-
<displayPriorityInCategory>6</displayPriorityInCategory>
499+
<displayPriorityInCategory>7</displayPriorityInCategory>
492500
</value>
493501
</Operation>
494502

503+
<Operation Class="PatchOperationReplace">
504+
<xpath>Defs/StatDef[defName="MeleeDPS"]/workerClass</xpath>
505+
<value>
506+
<workerClass>CombatExtended.StatWorker_MeleeDamageAverage</workerClass>
507+
</value>
508+
</Operation>
509+
495510
<!-- Melee AP -->
496511

497512
<Operation Class="PatchOperationReplace">
@@ -508,4 +523,19 @@
508523
</value>
509524
</Operation>
510525

526+
<Operation Class="PatchOperationReplace">
527+
<xpath>Defs/StatDef[defName="MeleeWeapon_AverageArmorPenetration"]/category</xpath>
528+
<value>
529+
<category>Weapon</category>
530+
</value>
531+
</Operation>
532+
533+
534+
<Operation Class="PatchOperationReplace">
535+
<xpath>Defs/StatDef[defName="MeleeWeapon_AverageArmorPenetration"]/displayPriorityInCategory</xpath>
536+
<value>
537+
<displayPriorityInCategory>8</displayPriorityInCategory>
538+
</value>
539+
</Operation>
540+
511541
</Patch>

Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeArmorPenetration.cs

Lines changed: 146 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -17,114 +17,183 @@ public override string GetStatDrawEntryLabel(StatDef stat, float value, ToString
1717

1818
public override string GetExplanationUnfinalized(StatRequest req, ToStringNumberSense numberSense)
1919
{
20-
var tools = (req.Def as ThingDef)?.tools;
21-
22-
if (tools.NullOrEmpty())
20+
if (req.Def is not ThingDef thingDef)
2321
{
2422
return base.GetExplanationUnfinalized(req, numberSense);
2523
}
24+
25+
var pawn = GetCurrentWielder(req);
26+
27+
var skillFactor = GetSkillFactor(pawn);
28+
var otherFactors = GetOtherFactors(pawn);
29+
2630
var stringBuilder = new StringBuilder();
27-
var penetrationFactor = GetPenetrationFactor(req);
28-
var skillFactor = GetSkillFactor(req);
29-
stringBuilder.AppendLine("CE_WeaponPenetrationFactor".Translate() + ": " + penetrationFactor.ToStringByStyle(ToStringStyle.PercentZero));
31+
3032
if (Mathf.Abs(skillFactor - 1f) > 0.001f)
3133
{
3234
stringBuilder.AppendLine("CE_WeaponPenetrationSkillFactor".Translate() + ": " + skillFactor.ToStringByStyle(ToStringStyle.PercentZero));
3335
}
3436

3537
stringBuilder.AppendLine();
3638

37-
foreach (ToolCE tool in tools)
39+
if (req.Thing is Pawn)
3840
{
39-
var maneuvers = DefDatabase<ManeuverDef>.AllDefsListForReading.Where(d => tool.capacities.Contains(d.requiredCapacity));
40-
var maneuverString = "(";
41-
foreach (var maneuver in maneuvers)
42-
{
43-
maneuverString += maneuver.ToString() + "/";
44-
}
45-
maneuverString = maneuverString.TrimmedToLength(maneuverString.Length - 1) + ")";
46-
stringBuilder.AppendLine(" " + "Tool".Translate() + ": " + tool.ToString() + " " + maneuverString);
47-
var otherFactors = GetOtherFactors(req).Aggregate(1f, (x, y) => x * y);
48-
if (Mathf.Abs(otherFactors - 1f) > 0.001f)
41+
var meleeVerbs = pawn.meleeVerbs.GetUpdatedAvailableVerbsList(terrainTools: false);
42+
var cumulativeWeights = meleeVerbs.Sum(verbEntry => verbEntry.GetSelectionWeight(null));
43+
foreach (var verbEntry in meleeVerbs)
4944
{
50-
stringBuilder.AppendLine(" " + "CE_WeaponPenetrationOtherFactors".Translate() + ": " + otherFactors.ToStringByStyle(ToStringStyle.PercentZero));
51-
}
45+
var penetrationFactor =
46+
verbEntry.verb.EquipmentSource?.GetStatValue(CE_StatDefOf.MeleePenetrationFactor) ?? 1f;
47+
var chance = verbEntry.GetSelectionWeight(null) / cumulativeWeights;
5248

53-
stringBuilder.Append(string.Format(" {0}: {1} x {2}",
54-
"CE_DescSharpPenetration".Translate(),
55-
tool.armorPenetrationSharp.ToStringByStyle(ToStringStyle.FloatMaxTwo),
56-
penetrationFactor.ToStringByStyle(ToStringStyle.FloatMaxThree)));
57-
if (Mathf.Abs(skillFactor - 1f) > 0.001f)
58-
{
59-
stringBuilder.Append(string.Format(" x {0}", skillFactor.ToStringByStyle(ToStringStyle.FloatMaxTwo)));
49+
if (chance > 0)
50+
{
51+
ShowExplanationForVerb(
52+
stringBuilder,
53+
verbEntry.verb.tool,
54+
verbEntry.verb.maneuver,
55+
skillFactor,
56+
otherFactors,
57+
penetrationFactor,
58+
chance
59+
);
60+
}
6061
}
61-
if (Mathf.Abs(otherFactors - 1f) > 0.001f)
62+
}
63+
else
64+
{
65+
var penetrationFactor = GetPenetrationFactor(req);
66+
var meleeVerbPropsWithSource = AllMeleeVerbPropsWithSource(thingDef);
67+
var cumulativeWeights = meleeVerbPropsWithSource.Sum(vps => AdjustedMeleeSelectionWeight(vps, pawn, req));
68+
foreach (var vps in meleeVerbPropsWithSource)
6269
{
63-
stringBuilder.Append(string.Format(" x {0}", otherFactors.ToStringByStyle(ToStringStyle.FloatMaxTwo)));
70+
var chance = AdjustedMeleeSelectionWeight(vps, pawn, req) / cumulativeWeights;
71+
ShowExplanationForVerb(
72+
stringBuilder,
73+
vps.tool,
74+
vps.maneuver,
75+
skillFactor,
76+
otherFactors,
77+
penetrationFactor,
78+
chance
79+
);
6480
}
65-
stringBuilder.AppendLine(string.Format(" = {0} {1}",
66-
(tool.armorPenetrationSharp * penetrationFactor * skillFactor * otherFactors).ToStringByStyle(ToStringStyle.FloatMaxTwo),
67-
"CE_mmRHA".Translate()));
81+
}
6882

83+
return stringBuilder.ToString();
84+
}
6985

70-
stringBuilder.Append(string.Format(" {0}: {1} x {2}",
71-
"CE_DescBluntPenetration".Translate(),
72-
tool.armorPenetrationBlunt.ToStringByStyle(ToStringStyle.FloatMaxTwo),
73-
penetrationFactor.ToStringByStyle(ToStringStyle.FloatMaxThree)));
74-
if (Mathf.Abs(skillFactor - 1f) > 0.001f)
75-
{
76-
stringBuilder.Append(string.Format(" x {0}", skillFactor.ToStringByStyle(ToStringStyle.FloatMaxTwo)));
77-
}
78-
if (Mathf.Abs(otherFactors - 1f) > 0.001f)
79-
{
80-
stringBuilder.Append(string.Format(" x {0}", otherFactors.ToStringByStyle(ToStringStyle.FloatMaxTwo)));
81-
}
82-
stringBuilder.AppendLine(string.Format(" = {0} {1}",
83-
(tool.armorPenetrationBlunt * penetrationFactor * skillFactor * otherFactors).ToStringByStyle(ToStringStyle.FloatMaxTwo),
84-
"CE_MPa".Translate()));
85-
stringBuilder.AppendLine();
86+
private void ShowExplanationForVerb(StringBuilder stringBuilder, Tool verbTool, ManeuverDef maneuver,
87+
float skillFactor,
88+
float otherFactors,
89+
float penetrationFactor,
90+
float chance)
91+
{
92+
if (verbTool is not ToolCE tool)
93+
{
94+
return;
8695
}
87-
return stringBuilder.ToString();
96+
97+
var maneuverString = "(" + maneuver + ")";
98+
stringBuilder.AppendLine(" " + "Tool".Translate() + ": " + tool.ToString() + " " + maneuverString);
99+
100+
101+
stringBuilder.AppendLine(" " + "CE_WeaponPenetrationFactor".Translate() + ": " + penetrationFactor.ToStringByStyle(ToStringStyle.PercentZero));
102+
103+
if (Mathf.Abs(otherFactors - 1f) > 0.001f)
104+
{
105+
stringBuilder.AppendLine(" " + "CE_WeaponPenetrationOtherFactors".Translate() + ": " + otherFactors.ToStringByStyle(ToStringStyle.PercentZero));
106+
}
107+
108+
stringBuilder.Append(string.Format(" {0}: {1} x {2}",
109+
"CE_DescSharpPenetration".Translate(),
110+
tool.armorPenetrationSharp.ToStringByStyle(ToStringStyle.FloatMaxTwo),
111+
penetrationFactor.ToStringByStyle(ToStringStyle.FloatMaxThree)));
112+
if (Mathf.Abs(skillFactor - 1f) > 0.001f)
113+
{
114+
stringBuilder.Append(string.Format(" x {0}", skillFactor.ToStringByStyle(ToStringStyle.FloatMaxTwo)));
115+
}
116+
if (Mathf.Abs(otherFactors - 1f) > 0.001f)
117+
{
118+
stringBuilder.Append(string.Format(" x {0}", otherFactors.ToStringByStyle(ToStringStyle.FloatMaxTwo)));
119+
}
120+
stringBuilder.AppendLine(string.Format(" = {0} {1}",
121+
(tool.armorPenetrationSharp * penetrationFactor * skillFactor * otherFactors).ToStringByStyle(ToStringStyle.FloatMaxTwo),
122+
"CE_mmRHA".Translate()));
123+
124+
125+
stringBuilder.Append(string.Format(" {0}: {1} x {2}",
126+
"CE_DescBluntPenetration".Translate(),
127+
tool.armorPenetrationBlunt.ToStringByStyle(ToStringStyle.FloatMaxTwo),
128+
penetrationFactor.ToStringByStyle(ToStringStyle.FloatMaxThree)));
129+
if (Mathf.Abs(skillFactor - 1f) > 0.001f)
130+
{
131+
stringBuilder.Append(string.Format(" x {0}", skillFactor.ToStringByStyle(ToStringStyle.FloatMaxTwo)));
132+
}
133+
if (Mathf.Abs(otherFactors - 1f) > 0.001f)
134+
{
135+
stringBuilder.Append(string.Format(" x {0}", otherFactors.ToStringByStyle(ToStringStyle.FloatMaxTwo)));
136+
}
137+
stringBuilder.AppendLine(string.Format(" = {0} {1}",
138+
(tool.armorPenetrationBlunt * penetrationFactor * skillFactor * otherFactors).ToStringByStyle(ToStringStyle.FloatMaxTwo),
139+
"CE_MPa".Translate()));
140+
stringBuilder.AppendLine(" " + "CE_ChanceFactor".Translate() + ": " + chance.ToStringByStyle(ToStringStyle.FloatMaxTwo));
141+
stringBuilder.AppendLine();
88142
}
89143

90144
public override string GetExplanationFinalizePart(StatRequest req, ToStringNumberSense numberSense, float finalVal)
91145
{
92146
return "StatsReport_FinalValue".Translate() + ": " + GetFinalDisplayValue(req);
93147
}
94148

95-
private string GetFinalDisplayValue(StatRequest optionalReq)
149+
private string GetFinalDisplayValue(StatRequest req)
96150
{
97-
var tools = (optionalReq.Def as ThingDef)?.tools;
98-
if (tools.NullOrEmpty())
99-
{
100-
return "";
101-
}
102-
if (tools.Any(x => !(x is ToolCE)))
151+
if (req.Def is not ThingDef thingDef)
103152
{
104-
Log.Error($"Trying to get stat MeleeArmorPenetration from {optionalReq.Def.defName} which has no support for Combat Extended.");
105153
return "";
106154
}
107155

108-
float totalSelectionWeight = 0f;
109-
foreach (Tool tool in tools)
156+
var pawn = GetCurrentWielder(req);
157+
var otherFactors = GetOtherFactors(pawn);
158+
var skillFactor = GetSkillFactor(pawn);
159+
160+
float totalAveragePenSharp;
161+
float totalAveragePenBlunt;
162+
163+
if (req.Thing is Pawn)
110164
{
111-
totalSelectionWeight += tool.chanceFactor;
165+
var meleeVerbs = pawn.meleeVerbs.GetUpdatedAvailableVerbsList(terrainTools: false);
166+
167+
totalAveragePenSharp = meleeVerbs.AverageWeighted(
168+
verbEntry => verbEntry.GetSelectionWeight(null),
169+
verbEntry => verbEntry.verb.tool is ToolCE tool ? tool.armorPenetrationSharp * otherFactors * verbEntry.verb.EquipmentSource?.GetStatValue(CE_StatDefOf.MeleePenetrationFactor) ?? 1f : 0f
170+
);
171+
totalAveragePenBlunt = meleeVerbs.AverageWeighted(
172+
verbEntry => verbEntry.GetSelectionWeight(null),
173+
verbEntry => verbEntry.verb.tool is ToolCE tool ? tool.armorPenetrationBlunt * otherFactors * verbEntry.verb.EquipmentSource?.GetStatValue(CE_StatDefOf.MeleePenetrationFactor) ?? 1f : 0f
174+
);
112175
}
113-
float totalAveragePenSharp = 0f;
114-
float totalAveragePenBlunt = 0f;
115-
foreach (ToolCE tool in tools)
176+
else
116177
{
117-
var weightFactor = tool.chanceFactor / totalSelectionWeight;
118-
var otherFactors = GetOtherFactors(optionalReq).Aggregate(1f, (x, y) => x * y);
119-
totalAveragePenSharp += weightFactor * tool.armorPenetrationSharp * otherFactors;
120-
totalAveragePenBlunt += weightFactor * tool.armorPenetrationBlunt * otherFactors;
178+
var verbPropsWithSource = AllMeleeVerbPropsWithSource(thingDef);
179+
totalAveragePenSharp = verbPropsWithSource.AverageWeighted(
180+
vps => AdjustedMeleeSelectionWeight(vps, pawn, req),
181+
vps => vps.tool is ToolCE tool ? tool.armorPenetrationSharp * otherFactors : 0f
182+
);
183+
totalAveragePenBlunt = verbPropsWithSource.AverageWeighted(
184+
vps => AdjustedMeleeSelectionWeight(vps, pawn, req),
185+
vps => vps.tool is ToolCE tool ? tool.armorPenetrationBlunt * otherFactors : 0f
186+
);
187+
188+
var penetrationFactor = GetPenetrationFactor(req);
189+
190+
totalAveragePenSharp *= penetrationFactor;
191+
totalAveragePenBlunt *= penetrationFactor;
121192
}
122-
var penetrationFactor = GetPenetrationFactor(optionalReq);
123-
var skillFactor = GetSkillFactor(optionalReq);
124193

125-
return (totalAveragePenSharp * penetrationFactor * skillFactor).ToStringByStyle(ToStringStyle.FloatMaxTwo) + " " + "CE_mmRHA".Translate()
194+
return (totalAveragePenSharp * skillFactor).ToStringByStyle(ToStringStyle.FloatMaxTwo) + " " + "CE_mmRHA".Translate()
126195
+ ", "
127-
+ (totalAveragePenBlunt * penetrationFactor * skillFactor).ToStringByStyle(ToStringStyle.FloatMaxTwo) + " " + "CE_MPa".Translate();
196+
+ (totalAveragePenBlunt * skillFactor).ToStringByStyle(ToStringStyle.FloatMaxTwo) + " " + "CE_MPa".Translate();
128197
}
129198

130199
private float GetPenetrationFactor(StatRequest req)
@@ -144,30 +213,23 @@ private float GetPenetrationFactor(StatRequest req)
144213
}
145214
public const float skillFactorPerLevel = (25f / 19f) / 100f;
146215
public const float powerForOtherFactors = 0.75f;
147-
private float GetSkillFactor(StatRequest req)
216+
private float GetSkillFactor(Pawn pawn)
148217
{
149218
var skillFactor = 1f;
150-
if (req.Thing is Pawn pawn && pawn.skills != null)
219+
if (pawn?.skills != null)
151220
{
152221
skillFactor += skillFactorPerLevel * (pawn.skills.GetSkill(SkillDefOf.Melee).Level - 1);
153222
}
154-
else
155-
{
156-
var thingHolder = (req.Thing?.ParentHolder as Pawn_EquipmentTracker)?.pawn;
157-
if (thingHolder != null && thingHolder.skills != null)
158-
{
159-
skillFactor += skillFactorPerLevel * (thingHolder.skills.GetSkill(SkillDefOf.Melee).Level - 1);
160-
}
161-
}
162223
return skillFactor;
163224
}
164-
private IEnumerable<float> GetOtherFactors(StatRequest req)
225+
private float GetOtherFactors(Pawn pawn)
165226
{
166-
var pawn = req.Thing as Pawn ?? (req.Thing?.ParentHolder as Pawn_EquipmentTracker)?.pawn;
167227
if (pawn != null)
168228
{
169-
yield return Mathf.Pow(pawn.ageTracker.CurLifeStage.meleeDamageFactor, powerForOtherFactors);
170-
yield return Mathf.Pow(pawn.GetStatValue(StatDefOf.MeleeDamageFactor, true, -1), powerForOtherFactors);
229+
return Mathf.Pow(pawn.ageTracker.CurLifeStage.meleeDamageFactor, powerForOtherFactors) *
230+
Mathf.Pow(pawn.GetStatValue(StatDefOf.MeleeDamageFactor, true, -1), powerForOtherFactors);
171231
}
232+
233+
return 1f;
172234
}
173235
}

0 commit comments

Comments
 (0)