diff --git a/Patches/Core/Stats/Stats.xml b/Patches/Core/Stats/Stats.xml index 9e2ec48e50..04d689dc48 100644 --- a/Patches/Core/Stats/Stats.xml +++ b/Patches/Core/Stats/Stats.xml @@ -485,13 +485,28 @@ - + + Defs/StatDef[defName="MeleeWeapon_AverageDPS"]/category + + Weapon + + + + + Defs/StatDef[defName="MeleeWeapon_AverageDPS"]/displayPriorityInCategory - 6 + 7 + + Defs/StatDef[defName="MeleeDPS"]/workerClass + + CombatExtended.StatWorker_MeleeDamageAverage + + + @@ -508,4 +523,19 @@ + + Defs/StatDef[defName="MeleeWeapon_AverageArmorPenetration"]/category + + Weapon + + + + + + Defs/StatDef[defName="MeleeWeapon_AverageArmorPenetration"]/displayPriorityInCategory + + 8 + + + \ No newline at end of file diff --git a/Source/CombatExtended/CombatExtended/CE_Utility.cs b/Source/CombatExtended/CombatExtended/CE_Utility.cs index 50426a1031..f07399b79d 100755 --- a/Source/CombatExtended/CombatExtended/CE_Utility.cs +++ b/Source/CombatExtended/CombatExtended/CE_Utility.cs @@ -1680,38 +1680,4 @@ internal static T ElementAtOrLast(this IEnumerable enumerable, int index) } return current; } - - internal static List GetThingDefTools(ThingDef thingDef) - { - List tools = new List(); - if (thingDef.isTechHediff) - { - tools = GetTechHediffTools(thingDef); - } - else if (thingDef.IsWeapon || thingDef.category == ThingCategory.Pawn) - { - tools = thingDef.tools?.ToList(); - } - - return tools; - } - - internal static List GetTechHediffTools(ThingDef thingDef) - { - List techHediffTools = new List(); - List allDefsListForReading = DefDatabase.AllDefsListForReading; - for (int i = 0; i < allDefsListForReading.Count; i++) - { - if (allDefsListForReading[i].IsIngredient(thingDef)) - { - HediffDef hediffDef = allDefsListForReading[i].addsHediff; - HediffCompProperties_VerbGiver hediffCompProperties_VerbGiver = hediffDef?.comps?.FirstOrDefault((HediffCompProperties x) => x is HediffCompProperties_VerbGiver) as HediffCompProperties_VerbGiver; - if (hediffCompProperties_VerbGiver != null && !hediffCompProperties_VerbGiver.tools.NullOrEmpty() && hediffCompProperties_VerbGiver.tools.All(t => t is ToolCE)) - { - techHediffTools = hediffCompProperties_VerbGiver.tools.ToList(); - } - } - } - return techHediffTools; - } } diff --git a/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeArmorPenetration.cs b/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeArmorPenetration.cs index 94176ad57d..9172b18b6f 100755 --- a/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeArmorPenetration.cs +++ b/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeArmorPenetration.cs @@ -10,24 +10,6 @@ namespace CombatExtended; public class StatWorker_MeleeArmorPenetration : StatWorker_MeleeStats { - public override bool ShouldShowFor(StatRequest req) - { - if (!(req.Def is ThingDef thingDef)) - { - return false; - } - - if (stat.category == StatCategoryDefOf.PawnCombat) - { - return req.Thing is Pawn; - } - else if (stat.category == StatCategoryDefOf.Weapon_Melee) - { - return !(req.Thing is Pawn) && !CE_Utility.GetThingDefTools(thingDef).NullOrEmpty(); - } - - return false; - } public override string GetStatDrawEntryLabel(StatDef stat, float value, ToStringNumberSense numberSense, StatRequest optionalReq, bool finalized = true) { @@ -36,16 +18,18 @@ public override string GetStatDrawEntryLabel(StatDef stat, float value, ToString public override string GetExplanationUnfinalized(StatRequest req, ToStringNumberSense numberSense) { - List tools = CE_Utility.GetThingDefTools(req.Def as ThingDef); - - if (tools.NullOrEmpty()) + if (req.Def is not ThingDef thingDef) { return base.GetExplanationUnfinalized(req, numberSense); } + + var pawn = GetCurrentWielder(req); + + var skillFactor = GetSkillFactor(pawn); + var otherFactors = GetOtherFactors(pawn); + var stringBuilder = new StringBuilder(); - var penetrationFactor = GetPenetrationFactor(req); - var skillFactor = GetSkillFactor(req); - stringBuilder.AppendLine("CE_WeaponPenetrationFactor".Translate() + ": " + penetrationFactor.ToStringByStyle(ToStringStyle.PercentZero)); + if (Mathf.Abs(skillFactor - 1f) > 0.001f) { stringBuilder.AppendLine("CE_WeaponPenetrationSkillFactor".Translate() + ": " + skillFactor.ToStringByStyle(ToStringStyle.PercentZero)); @@ -53,57 +37,109 @@ public override string GetExplanationUnfinalized(StatRequest req, ToStringNumber stringBuilder.AppendLine(); - foreach (ToolCE tool in tools) + if (req.Thing is Pawn) { - var maneuvers = DefDatabase.AllDefsListForReading.Where(d => tool.capacities.Contains(d.requiredCapacity)); - var maneuverString = "("; - foreach (var maneuver in maneuvers) + var meleeVerbs = pawn.meleeVerbs.GetUpdatedAvailableVerbsList(terrainTools: false); + var cumulativeWeights = meleeVerbs.Sum(verbEntry => verbEntry.GetSelectionWeight(null)); + foreach (var verbEntry in meleeVerbs) { - maneuverString += maneuver.ToString() + "/"; + var penetrationFactor = + verbEntry.verb.EquipmentSource?.GetStatValue(CE_StatDefOf.MeleePenetrationFactor) ?? 1f; + var chance = verbEntry.GetSelectionWeight(null) / cumulativeWeights; + + if (chance > 0) + { + ShowExplanationForVerb( + stringBuilder, + verbEntry.verb.tool, + verbEntry.verb.maneuver, + skillFactor, + otherFactors, + penetrationFactor, + chance + ); + } } - maneuverString = maneuverString.TrimmedToLength(maneuverString.Length - 1) + ")"; - stringBuilder.AppendLine(" " + "Tool".Translate() + ": " + tool.ToString() + " " + maneuverString); - var otherFactors = GetOtherFactors(req).Aggregate(1f, (x, y) => x * y); - if (Mathf.Abs(otherFactors - 1f) > 0.001f) + } + else + { + var penetrationFactor = GetPenetrationFactor(req); + var meleeVerbPropsWithSource = AllMeleeVerbPropsWithSource(thingDef); + var cumulativeWeights = meleeVerbPropsWithSource.Sum(vps => AdjustedMeleeSelectionWeight(vps, pawn, req)); + foreach (var vps in meleeVerbPropsWithSource) { - stringBuilder.AppendLine(" " + "CE_WeaponPenetrationOtherFactors".Translate() + ": " + otherFactors.ToStringByStyle(ToStringStyle.PercentZero)); + var chance = AdjustedMeleeSelectionWeight(vps, pawn, req) / cumulativeWeights; + ShowExplanationForVerb( + stringBuilder, + vps.tool, + vps.maneuver, + skillFactor, + otherFactors, + penetrationFactor, + chance + ); } + } - stringBuilder.Append(string.Format(" {0}: {1} x {2}", - "CE_DescSharpPenetration".Translate(), - tool.armorPenetrationSharp.ToStringByStyle(ToStringStyle.FloatMaxTwo), - penetrationFactor.ToStringByStyle(ToStringStyle.FloatMaxThree))); - if (Mathf.Abs(skillFactor - 1f) > 0.001f) - { - stringBuilder.Append(string.Format(" x {0}", skillFactor.ToStringByStyle(ToStringStyle.FloatMaxTwo))); - } - if (Mathf.Abs(otherFactors - 1f) > 0.001f) - { - stringBuilder.Append(string.Format(" x {0}", otherFactors.ToStringByStyle(ToStringStyle.FloatMaxTwo))); - } - stringBuilder.AppendLine(string.Format(" = {0} {1}", - (tool.armorPenetrationSharp * penetrationFactor * skillFactor * otherFactors).ToStringByStyle(ToStringStyle.FloatMaxTwo), - "CE_mmRHA".Translate())); + return stringBuilder.ToString(); + } + private void ShowExplanationForVerb(StringBuilder stringBuilder, Tool verbTool, ManeuverDef maneuver, + float skillFactor, + float otherFactors, + float penetrationFactor, + float chance) + { + if (verbTool is not ToolCE tool) + { + return; + } - stringBuilder.Append(string.Format(" {0}: {1} x {2}", - "CE_DescBluntPenetration".Translate(), - tool.armorPenetrationBlunt.ToStringByStyle(ToStringStyle.FloatMaxTwo), - penetrationFactor.ToStringByStyle(ToStringStyle.FloatMaxThree))); - if (Mathf.Abs(skillFactor - 1f) > 0.001f) - { - stringBuilder.Append(string.Format(" x {0}", skillFactor.ToStringByStyle(ToStringStyle.FloatMaxTwo))); - } - if (Mathf.Abs(otherFactors - 1f) > 0.001f) - { - stringBuilder.Append(string.Format(" x {0}", otherFactors.ToStringByStyle(ToStringStyle.FloatMaxTwo))); - } - stringBuilder.AppendLine(string.Format(" = {0} {1}", - (tool.armorPenetrationBlunt * penetrationFactor * skillFactor * otherFactors).ToStringByStyle(ToStringStyle.FloatMaxTwo), - "CE_MPa".Translate())); - stringBuilder.AppendLine(); + var maneuverString = "(" + maneuver + ")"; + stringBuilder.AppendLine(" " + "Tool".Translate() + ": " + tool.ToString() + " " + maneuverString); + + + stringBuilder.AppendLine(" " + "CE_WeaponPenetrationFactor".Translate() + ": " + penetrationFactor.ToStringByStyle(ToStringStyle.PercentZero)); + + if (Mathf.Abs(otherFactors - 1f) > 0.001f) + { + stringBuilder.AppendLine(" " + "CE_WeaponPenetrationOtherFactors".Translate() + ": " + otherFactors.ToStringByStyle(ToStringStyle.PercentZero)); } - return stringBuilder.ToString(); + + stringBuilder.Append(string.Format(" {0}: {1} x {2}", + "CE_DescSharpPenetration".Translate(), + tool.armorPenetrationSharp.ToStringByStyle(ToStringStyle.FloatMaxTwo), + penetrationFactor.ToStringByStyle(ToStringStyle.FloatMaxThree))); + if (Mathf.Abs(skillFactor - 1f) > 0.001f) + { + stringBuilder.Append(string.Format(" x {0}", skillFactor.ToStringByStyle(ToStringStyle.FloatMaxTwo))); + } + if (Mathf.Abs(otherFactors - 1f) > 0.001f) + { + stringBuilder.Append(string.Format(" x {0}", otherFactors.ToStringByStyle(ToStringStyle.FloatMaxTwo))); + } + stringBuilder.AppendLine(string.Format(" = {0} {1}", + (tool.armorPenetrationSharp * penetrationFactor * skillFactor * otherFactors).ToStringByStyle(ToStringStyle.FloatMaxTwo), + "CE_mmRHA".Translate())); + + + stringBuilder.Append(string.Format(" {0}: {1} x {2}", + "CE_DescBluntPenetration".Translate(), + tool.armorPenetrationBlunt.ToStringByStyle(ToStringStyle.FloatMaxTwo), + penetrationFactor.ToStringByStyle(ToStringStyle.FloatMaxThree))); + if (Mathf.Abs(skillFactor - 1f) > 0.001f) + { + stringBuilder.Append(string.Format(" x {0}", skillFactor.ToStringByStyle(ToStringStyle.FloatMaxTwo))); + } + if (Mathf.Abs(otherFactors - 1f) > 0.001f) + { + stringBuilder.Append(string.Format(" x {0}", otherFactors.ToStringByStyle(ToStringStyle.FloatMaxTwo))); + } + stringBuilder.AppendLine(string.Format(" = {0} {1}", + (tool.armorPenetrationBlunt * penetrationFactor * skillFactor * otherFactors).ToStringByStyle(ToStringStyle.FloatMaxTwo), + "CE_MPa".Translate())); + stringBuilder.AppendLine(" " + "CE_ChanceFactor".Translate() + ": " + chance.ToStringByStyle(ToStringStyle.FloatMaxTwo)); + stringBuilder.AppendLine(); } public override string GetExplanationFinalizePart(StatRequest req, ToStringNumberSense numberSense, float finalVal) @@ -111,39 +147,58 @@ public override string GetExplanationFinalizePart(StatRequest req, ToStringNumbe return "StatsReport_FinalValue".Translate() + ": " + GetFinalDisplayValue(req); } - private string GetFinalDisplayValue(StatRequest optionalReq) + private string GetFinalDisplayValue(StatRequest req) { - List tools = CE_Utility.GetThingDefTools(optionalReq.Def as ThingDef); - if (tools.NullOrEmpty()) - { - return ""; - } - if (tools.Any(x => !(x is ToolCE))) + if (req.Def is not ThingDef thingDef) { - Log.Error($"Trying to get stat MeleeArmorPenetration from {optionalReq.Def.defName} which has no support for Combat Extended."); return ""; } - float totalSelectionWeight = 0f; - foreach (Tool tool in tools) + var pawn = GetCurrentWielder(req); + var otherFactors = GetOtherFactors(pawn); + var skillFactor = GetSkillFactor(pawn); + + float totalAveragePenSharp; + float totalAveragePenBlunt; + + if (req.Thing is Pawn) { - totalSelectionWeight += tool.chanceFactor; + // When computing average armor penetration for a pawn, consider the weighted average of the armor penetration of all their available melee verbs. + // The penetration factor in this case depends on the weapon that provides a given verb. + var meleeVerbs = pawn.meleeVerbs.GetUpdatedAvailableVerbsList(terrainTools: false); + + totalAveragePenSharp = meleeVerbs.AverageWeighted( + verbEntry => verbEntry.GetSelectionWeight(null), + verbEntry => verbEntry.verb.tool is ToolCE tool ? tool.armorPenetrationSharp * otherFactors * verbEntry.verb.EquipmentSource?.GetStatValue(CE_StatDefOf.MeleePenetrationFactor) ?? 1f : 0f + ); + totalAveragePenBlunt = meleeVerbs.AverageWeighted( + verbEntry => verbEntry.GetSelectionWeight(null), + verbEntry => verbEntry.verb.tool is ToolCE tool ? tool.armorPenetrationBlunt * otherFactors * verbEntry.verb.EquipmentSource?.GetStatValue(CE_StatDefOf.MeleePenetrationFactor) ?? 1f : 0f + ); } - float totalAveragePenSharp = 0f; - float totalAveragePenBlunt = 0f; - foreach (ToolCE tool in tools) + else { - var weightFactor = tool.chanceFactor / totalSelectionWeight; - var otherFactors = GetOtherFactors(optionalReq).Aggregate(1f, (x, y) => x * y); - totalAveragePenSharp += weightFactor * tool.armorPenetrationSharp * otherFactors; - totalAveragePenBlunt += weightFactor * tool.armorPenetrationBlunt * otherFactors; + // Otherwise, when calculating average armor penetration for a single weapon (or def), be it wielded or unwielded, + // consider the weighted average of each verb and derive the penetration factor from the weapon/def. + var verbPropsWithSource = AllMeleeVerbPropsWithSource(thingDef); + totalAveragePenSharp = verbPropsWithSource.AverageWeighted( + vps => AdjustedMeleeSelectionWeight(vps, pawn, req), + vps => vps.tool is ToolCE tool ? tool.armorPenetrationSharp * otherFactors : 0f + ); + totalAveragePenBlunt = verbPropsWithSource.AverageWeighted( + vps => AdjustedMeleeSelectionWeight(vps, pawn, req), + vps => vps.tool is ToolCE tool ? tool.armorPenetrationBlunt * otherFactors : 0f + ); + + var penetrationFactor = GetPenetrationFactor(req); + + totalAveragePenSharp *= penetrationFactor; + totalAveragePenBlunt *= penetrationFactor; } - var penetrationFactor = GetPenetrationFactor(optionalReq); - var skillFactor = GetSkillFactor(optionalReq); - return (totalAveragePenSharp * penetrationFactor * skillFactor).ToStringByStyle(ToStringStyle.FloatMaxTwo) + " " + "CE_mmRHA".Translate() + return (totalAveragePenSharp * skillFactor).ToStringByStyle(ToStringStyle.FloatMaxTwo) + " " + "CE_mmRHA".Translate() + ", " - + (totalAveragePenBlunt * penetrationFactor * skillFactor).ToStringByStyle(ToStringStyle.FloatMaxTwo) + " " + "CE_MPa".Translate(); + + (totalAveragePenBlunt * skillFactor).ToStringByStyle(ToStringStyle.FloatMaxTwo) + " " + "CE_MPa".Translate(); } private float GetPenetrationFactor(StatRequest req) @@ -163,30 +218,23 @@ private float GetPenetrationFactor(StatRequest req) } public const float skillFactorPerLevel = (25f / 19f) / 100f; public const float powerForOtherFactors = 0.75f; - private float GetSkillFactor(StatRequest req) + private float GetSkillFactor(Pawn pawn) { var skillFactor = 1f; - if (req.Thing is Pawn pawn && pawn.skills != null) + if (pawn?.skills != null) { skillFactor += skillFactorPerLevel * (pawn.skills.GetSkill(SkillDefOf.Melee).Level - 1); } - else - { - var thingHolder = (req.Thing?.ParentHolder as Pawn_EquipmentTracker)?.pawn; - if (thingHolder != null && thingHolder.skills != null) - { - skillFactor += skillFactorPerLevel * (thingHolder.skills.GetSkill(SkillDefOf.Melee).Level - 1); - } - } return skillFactor; } - private IEnumerable GetOtherFactors(StatRequest req) + private float GetOtherFactors(Pawn pawn) { - var pawn = req.Thing as Pawn ?? (req.Thing?.ParentHolder as Pawn_EquipmentTracker)?.pawn; if (pawn != null) { - yield return Mathf.Pow(pawn.ageTracker.CurLifeStage.meleeDamageFactor, powerForOtherFactors); - yield return Mathf.Pow(pawn.GetStatValue(StatDefOf.MeleeDamageFactor, true, -1), powerForOtherFactors); + return Mathf.Pow(pawn.ageTracker.CurLifeStage.meleeDamageFactor, powerForOtherFactors) * + Mathf.Pow(pawn.GetStatValue(StatDefOf.MeleeDamageFactor, true, -1), powerForOtherFactors); } + + return 1f; } } diff --git a/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeDamage.cs b/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeDamage.cs index 5a41d4e650..fcc84fd9d1 100755 --- a/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeDamage.cs +++ b/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeDamage.cs @@ -17,25 +17,14 @@ public override string GetStatDrawEntryLabel(StatDef stat, float value, ToString public override string GetExplanationUnfinalized(StatRequest req, ToStringNumberSense numberSense) { - var skilledDamageVariationMin = damageVariationMin; - var skilledDamageVariationMax = damageVariationMax; - var meleeSkillLevel = -1; - if (req.Thing?.ParentHolder is Pawn_EquipmentTracker tracker && tracker.pawn != null) - { - var pawnHolder = tracker.pawn; - skilledDamageVariationMin = GetDamageVariationMin(pawnHolder); - skilledDamageVariationMax = GetDamageVariationMax(pawnHolder); + var pawn = GetCurrentWielder(req); - if (pawnHolder.skills != null) - { - meleeSkillLevel = pawnHolder.skills.GetSkill(SkillDefOf.Melee).Level; - } - } + var skilledDamageVariationMin = GetDamageVariationMin(pawn); + var skilledDamageVariationMax = GetDamageVariationMax(pawn); + var meleeSkillLevel = pawn?.skills?.GetSkill(SkillDefOf.Melee).Level ?? -1; - var tools = (req.Def as ThingDef)?.tools; - - if (tools.NullOrEmpty()) + if (req.Def is not ThingDef thingDef) { return base.GetExplanationUnfinalized(req, numberSense); } @@ -50,18 +39,13 @@ public override string GetExplanationUnfinalized(StatRequest req, ToStringNumber (100 * skilledDamageVariationMin).ToStringByStyle(ToStringStyle.FloatMaxTwo), (100 * skilledDamageVariationMax).ToStringByStyle(ToStringStyle.FloatMaxTwo))); stringBuilder.AppendLine(""); - foreach (Tool tool in tools) + + foreach (var vps in AllMeleeVerbPropsWithSource(thingDef)) { - if (tool is ToolCE toolCE) + if (vps.tool is ToolCE toolCE) { - var adjustedToolDamage = GetAdjustedDamage(toolCE, req.Thing); - var maneuvers = DefDatabase.AllDefsListForReading.Where(d => toolCE.capacities.Contains(d.requiredCapacity)); - var maneuverString = "("; - foreach (var maneuver in maneuvers) - { - maneuverString += maneuver.ToString() + "/"; - } - maneuverString = maneuverString.TrimmedToLength(maneuverString.Length - 1) + ")"; + var adjustedToolDamage = AdjustedMeleeDamageAmount(vps, pawn, req); + var maneuverString = "(" + vps.maneuver + ")"; stringBuilder.AppendLine(" " + "Tool".Translate() + ": " + toolCE.ToString() + " " + maneuverString); stringBuilder.AppendLine(" " + "CE_DescBaseDamage".Translate() + ": " + toolCE.power.ToStringByStyle(ToStringStyle.FloatMaxTwo)); stringBuilder.AppendLine(" " + "CE_AdjustedForWeapon".Translate() + ": " + adjustedToolDamage.ToStringByStyle(ToStringStyle.FloatMaxTwo)); @@ -87,36 +71,23 @@ public override string GetExplanationFinalizePart(StatRequest req, ToStringNumbe return "StatsReport_FinalValue".Translate() + ": " + GetFinalDisplayValue(req); } - private string GetFinalDisplayValue(StatRequest optionalReq) + private string GetFinalDisplayValue(StatRequest req) { - var skilledDamageVariationMin = damageVariationMin; - var skilledDamageVariationMax = damageVariationMax; + var pawn = GetCurrentWielder(req); - if (optionalReq.Thing?.ParentHolder is Pawn_EquipmentTracker tracker) - { - skilledDamageVariationMin = GetDamageVariationMin(tracker.pawn); - skilledDamageVariationMax = GetDamageVariationMax(tracker.pawn); - } + var skilledDamageVariationMin = GetDamageVariationMin(pawn); + var skilledDamageVariationMax = GetDamageVariationMax(pawn); - var tools = (optionalReq.Def as ThingDef)?.tools; - if (tools.NullOrEmpty()) + if (req.Def is not ThingDef thingDef) { return ""; } - if (tools.Any(x => !(x is ToolCE))) - { - if (DebugSettings.godMode) - { - Log.Error($"Trying to get stat MeleeDamage from {optionalReq.Def.defName} which has no support for Combat Extended."); - } - return "CE_UnpatchedWeaponShort".Translate(); - } float lowestDamage = Int32.MaxValue; float highestDamage = 0f; - foreach (ToolCE tool in tools) + foreach (var vps in AllMeleeVerbPropsWithSource(thingDef)) { - var toolDamage = GetAdjustedDamage(tool, optionalReq.Thing); + var toolDamage = AdjustedMeleeDamageAmount(vps, pawn, req); if (toolDamage > highestDamage) { highestDamage = toolDamage; diff --git a/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeDamageAverage.cs b/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeDamageAverage.cs index 3393596055..164692608d 100755 --- a/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeDamageAverage.cs +++ b/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeDamageAverage.cs @@ -9,85 +9,74 @@ namespace CombatExtended; public class StatWorker_MeleeDamageAverage : StatWorker_MeleeDamageBase { - public override bool ShouldShowFor(StatRequest req) + + public override float GetValueUnfinalized(StatRequest req, bool applyPostProcess = true) { - if (!(req.Def is ThingDef thingDef)) - { - return false; - } + var wielder = GetCurrentWielder(req); - if (stat.category == StatCategoryDefOf.PawnCombat) + var skilledDamageVariationMin = GetDamageVariationMin(wielder); + var skilledDamageVariationMax = GetDamageVariationMax(wielder); + + if (req.Def is not ThingDef thingDef) { - return req.Thing is Pawn; + return 0; } - else if (stat.category == StatCategoryDefOf.Weapon_Melee) + + float AverageDamageWithVariation(float baseDamage) { - return !(req.Thing is Pawn) && !CE_Utility.GetThingDefTools(thingDef).NullOrEmpty(); + var minDamage = baseDamage * skilledDamageVariationMin; + var maxDamage = baseDamage * skilledDamageVariationMax; + return (minDamage + maxDamage) / 2f; } - return false; - } - - public override float GetValueUnfinalized(StatRequest req, bool applyPostProcess) - { - var skilledDamageVariationMin = damageVariationMin; - var skilledDamageVariationMax = damageVariationMax; + float averageDamage; + float averageCooldown; - if (req.Thing?.ParentHolder is Pawn_EquipmentTracker tracker) + if (req.Thing is Pawn pawn) { - skilledDamageVariationMin = GetDamageVariationMin(tracker.pawn); - skilledDamageVariationMax = GetDamageVariationMax(tracker.pawn); - } + var meleeVerbs = pawn.meleeVerbs.GetUpdatedAvailableVerbsList(terrainTools: false); + averageDamage = meleeVerbs.AverageWeighted( + verbEntry => verbEntry.GetSelectionWeight(null), + verbEntry => AverageDamageWithVariation(verbEntry.verb.verbProps.AdjustedMeleeDamageAmount(verbEntry.verb, pawn)) + ); + averageCooldown = meleeVerbs.AverageWeighted( + verbEntry => verbEntry.GetSelectionWeight(null), + verbEntry => verbEntry.verb.verbProps.AdjustedCooldown(verbEntry.verb, pawn) + ); - List tools = CE_Utility.GetThingDefTools(req.Def as ThingDef); - if (tools.NullOrEmpty()) - { - return 0; } - if (tools.Any(x => !(x is ToolCE))) + else { - Log.Error($"Trying to get stat MeleeDamageAverage from {req.Def.defName} which has no support for Combat Extended."); - return 0; + var verbPropsWithSource = AllMeleeVerbPropsWithSource(thingDef); + averageDamage = verbPropsWithSource.AverageWeighted( + vps => AdjustedMeleeSelectionWeight(vps, wielder, req), + vps => AverageDamageWithVariation(AdjustedMeleeDamageAmount(vps, wielder, req)) + ); + averageCooldown = verbPropsWithSource.AverageWeighted( + vps => AdjustedMeleeSelectionWeight(vps, wielder, req), + vps => AdjustedCooldown(vps, wielder, req) + ); } - var totalSelectionWeight = 0f; - foreach (var tool in tools) - { - totalSelectionWeight += tool.chanceFactor; - } - var totalDPS = 0f; - foreach (var tool in tools) + float dps = averageDamage / averageCooldown; + + if (wielder != null) { - var toolDamage = GetAdjustedDamage((ToolCE)tool, req.Thing); - var minDPS = toolDamage / tool.cooldownTime * skilledDamageVariationMin; - var maxDPS = toolDamage / tool.cooldownTime * skilledDamageVariationMax; - var weightFactor = tool.chanceFactor / totalSelectionWeight; - totalDPS += weightFactor * ((minDPS + maxDPS) / 2f); + dps *= wielder.GetStatValue(StatDefOf.MeleeHitChance); } - return totalDPS; + + return dps; } public override string GetExplanationUnfinalized(StatRequest req, ToStringNumberSense numberSense) { - var skilledDamageVariationMin = damageVariationMin; - var skilledDamageVariationMax = damageVariationMax; - var meleeSkillLevel = -1; + var wielder = GetCurrentWielder(req); - if (req.Thing?.ParentHolder is Pawn_EquipmentTracker tracker && tracker.pawn != null) - { - var pawnHolder = tracker.pawn; - skilledDamageVariationMin = GetDamageVariationMin(pawnHolder); - skilledDamageVariationMax = GetDamageVariationMax(pawnHolder); + var skilledDamageVariationMin = GetDamageVariationMin(wielder); + var skilledDamageVariationMax = GetDamageVariationMax(wielder); + var meleeSkillLevel = wielder?.skills?.GetSkill(SkillDefOf.Melee).Level ?? -1; - if (pawnHolder.skills != null) - { - meleeSkillLevel = pawnHolder.skills.GetSkill(SkillDefOf.Melee).Level; - } - } - - List tools = CE_Utility.GetThingDefTools(req.Def as ThingDef); - - if (tools.NullOrEmpty()) + if (req.Def is not ThingDef thingDef) { return base.GetExplanationUnfinalized(req, numberSense); } @@ -104,33 +93,90 @@ public override string GetExplanationUnfinalized(StatRequest req, ToStringNumber (100 * skilledDamageVariationMax).ToStringByStyle(ToStringStyle.FloatMaxTwo))); stringBuilder.AppendLine(""); - foreach (ToolCE tool in tools) + if (req.Thing is Pawn pawn) + { + var meleeVerbs = pawn.meleeVerbs.GetUpdatedAvailableVerbsList(terrainTools: false); + var cumulativeSelectionWeights = meleeVerbs.Sum(verbEntry => verbEntry.GetSelectionWeight(null)); + foreach (var verbEntry in meleeVerbs) + { + var adjustedDamage = verbEntry.verb.verbProps.AdjustedMeleeDamageAmount(verbEntry.verb, pawn); + var adjustedCooldownTime = verbEntry.verb.verbProps.AdjustedCooldown(verbEntry.verb, pawn); + var chanceFactor = verbEntry.GetSelectionWeight(null) / cumulativeSelectionWeights; + + if (chanceFactor > 0) + { + ShowExplanationForVerb( + stringBuilder, + verbEntry.verb.tool, + verbEntry.verb.maneuver, + skilledDamageVariationMin, + skilledDamageVariationMax, + adjustedDamage, + adjustedCooldownTime, + chanceFactor); + } + } + } + else { - var adjustedToolDamage = GetAdjustedDamage(tool, req.Thing); - var minDPS = adjustedToolDamage / tool.cooldownTime * skilledDamageVariationMin; - var maxDPS = adjustedToolDamage / tool.cooldownTime * skilledDamageVariationMax; + var verbPropsWithSource = AllMeleeVerbPropsWithSource(thingDef); - var maneuvers = DefDatabase.AllDefsListForReading.Where(d => tool.capacities.Contains(d.requiredCapacity)); - var maneuverString = "("; - foreach (var maneuver in maneuvers) + var cumulativeSelectionWeights = verbPropsWithSource.Sum(vps => AdjustedMeleeSelectionWeight(vps, wielder, req)); + + foreach (var vps in verbPropsWithSource) { - maneuverString += maneuver.ToString() + "/"; + var adjustedDamage = AdjustedMeleeDamageAmount(vps, wielder, req); + var adjustedCooldownTime = AdjustedCooldown(vps, wielder, req); + var chanceFactor = AdjustedMeleeSelectionWeight(vps, wielder, req) / cumulativeSelectionWeights; + + ShowExplanationForVerb( + stringBuilder, + vps.tool, + vps.maneuver, + skilledDamageVariationMin, + skilledDamageVariationMax, + adjustedDamage, + adjustedCooldownTime, + chanceFactor); } - maneuverString = maneuverString.TrimmedToLength(maneuverString.Length - 1) + ")"; - - stringBuilder.AppendLine(" " + "Tool".Translate() + ": " + tool.ToString() + " " + maneuverString); - stringBuilder.AppendLine(" " + "CE_DescBaseDamage".Translate() + ": " + tool.power.ToStringByStyle(ToStringStyle.FloatMaxTwo)); - stringBuilder.AppendLine(" " + "CE_AdjustedForWeapon".Translate() + ": " + adjustedToolDamage.ToStringByStyle(ToStringStyle.FloatMaxTwo)); - stringBuilder.AppendLine(" " + "CooldownTime".Translate() + ": " + tool.cooldownTime.ToStringByStyle(ToStringStyle.FloatMaxTwo) + " seconds"); - stringBuilder.AppendLine(" " + "CE_DPS".Translate() + ": " + (adjustedToolDamage / tool.cooldownTime).ToStringByStyle(ToStringStyle.FloatMaxTwo)); - stringBuilder.AppendLine(string.Format(" " + "CE_DamageVariation".Translate() + ": {0} - {1}", - minDPS.ToStringByStyle(ToStringStyle.FloatMaxTwo), - maxDPS.ToStringByStyle(ToStringStyle.FloatMaxTwo))); - stringBuilder.AppendLine(" " + "CE_FinalAverageDamage".Translate() + ": " + ((minDPS + maxDPS) / 2f).ToStringByStyle(ToStringStyle.FloatMaxTwo)); - stringBuilder.AppendLine(" " + "CE_ChanceFactor".Translate() + ": " + tool.chanceFactor.ToStringByStyle(ToStringStyle.FloatMaxTwo)); - stringBuilder.AppendLine(); } + + if (wielder != null) + { + var hitChanceReq = StatRequest.For(wielder); + stringBuilder.AppendLine(StatDefOf.MeleeHitChance.Worker.GetExplanationUnfinalized(hitChanceReq, StatDefOf.MeleeHitChance.toStringNumberSense).TrimEndNewlines().Indented()); + stringBuilder.Append(StatDefOf.MeleeHitChance.Worker.GetExplanationFinalizePart(hitChanceReq, StatDefOf.MeleeHitChance.toStringNumberSense, wielder.GetStatValue(StatDefOf.MeleeHitChance)).Indented()); + } + return stringBuilder.ToString(); } + private void ShowExplanationForVerb( + StringBuilder stringBuilder, + Tool tool, + ManeuverDef maneuver, + float skilledDamageVariationMin, + float skilledDamageVariationMax, + float adjustedDamage, + float adjustedCooldownTime, + float chanceFactor) + { + var minDPS = adjustedDamage / adjustedCooldownTime * skilledDamageVariationMin; + var maxDPS = adjustedDamage / adjustedCooldownTime * skilledDamageVariationMax; + + var maneuverString = "(" + maneuver + ")"; + + stringBuilder.AppendLine(" " + "Tool".Translate() + ": " + tool.ToString() + " " + maneuverString); + stringBuilder.AppendLine(" " + "CE_DescBaseDamage".Translate() + ": " + tool.power.ToStringByStyle(ToStringStyle.FloatMaxTwo)); + stringBuilder.AppendLine(" " + "CE_AdjustedForWeapon".Translate() + ": " + adjustedDamage.ToStringByStyle(ToStringStyle.FloatMaxTwo)); + stringBuilder.AppendLine(" " + "CooldownTime".Translate() + ": " + adjustedCooldownTime.ToStringByStyle(ToStringStyle.FloatMaxTwo) + " seconds"); + stringBuilder.AppendLine(" " + "CE_DPS".Translate() + ": " + (adjustedDamage / adjustedCooldownTime).ToStringByStyle(ToStringStyle.FloatMaxTwo)); + stringBuilder.AppendLine(string.Format(" " + "CE_DamageVariation".Translate() + ": {0} - {1}", + minDPS.ToStringByStyle(ToStringStyle.FloatMaxTwo), + maxDPS.ToStringByStyle(ToStringStyle.FloatMaxTwo))); + stringBuilder.AppendLine(" " + "CE_FinalAverageDamage".Translate() + ": " + ((minDPS + maxDPS) / 2f).ToStringByStyle(ToStringStyle.FloatMaxTwo)); + stringBuilder.AppendLine(" " + "CE_ChanceFactor".Translate() + ": " + chanceFactor.ToStringByStyle(ToStringStyle.FloatMaxTwo)); + stringBuilder.AppendLine(); + } + } diff --git a/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeDamageBase.cs b/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeDamageBase.cs index cdfe5d0758..f5ea340b1f 100755 --- a/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeDamageBase.cs +++ b/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeDamageBase.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text; using RimWorld; using Verse; @@ -54,9 +53,35 @@ public static bool ShouldUseSkillVariation(Pawn pawn, ref float unskilledReturnV return true; } - public static float GetAdjustedDamage(ToolCE tool, Thing thingOwner) + + /// + /// Get the melee damaged dealt by a single VerbProperties + Tool combination of a weapon, + /// adjusted according to the wielder's (if any) stats/capacities and multipliers from the weapon's stuff. + /// + /// Verb data (VerbProperties + Tool + Maneuver) + /// The wielder of the weapon, or null if the stat request is for an unwielded weapon. + /// Stat request for a weapon or weapon def + /// Melee damage adjusted for the wielder and weapon. + protected float AdjustedMeleeDamageAmount(VerbUtility.VerbPropertiesWithSource vps, Pawn pawn, StatRequest req) + { + return req.HasThing ? + vps.verbProps.AdjustedMeleeDamageAmount(vps.tool, pawn, req.Thing, hediffCompSource: null) : + vps.verbProps.AdjustedMeleeDamageAmount(vps.tool, pawn, req.Def as ThingDef, req.StuffDef, hediffCompSource: null); + } + + /// + /// Get the cooldown (in seconds) a single VerbProperties + Tool combination of a weapon, + /// adjusted according to the wielder's (if any) stats/capacities and multipliers from the weapon's stuff. + /// + /// Verb data (VerbProperties + Tool + Maneuver) + /// The wielder of the weapon, or null if the stat request is for an unwielded weapon. + /// Stat request for a weapon or weapon def + /// Cooldown time in seconds, adjusted for the wielder and weapon. + protected float AdjustedCooldown(VerbUtility.VerbPropertiesWithSource vps, Pawn pawn, StatRequest req) { - return tool.AdjustedBaseMeleeDamageAmount(thingOwner, tool.capacities?.First()?.VerbsProperties?.First()?.meleeDamageDef); + return req.HasThing ? + vps.verbProps.AdjustedCooldown(vps.tool, pawn, req.Thing) : + vps.verbProps.AdjustedCooldown(vps.tool, pawn, req.Def as ThingDef, req.StuffDef); } #endregion diff --git a/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeStats.cs b/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeStats.cs index 822cc47b3d..19a99ddd52 100755 --- a/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeStats.cs +++ b/Source/CombatExtended/CombatExtended/StatWorkers/StatWorker_MeleeStats.cs @@ -13,4 +13,68 @@ public override bool IsDisabledFor(Thing thing) { return thing?.def?.building?.IsTurret ?? base.IsDisabledFor(thing); } + + public override bool ShouldShowFor(StatRequest req) + { + if (base.ShouldShowFor(req)) + { + return true; + } + + // Show melee stats for artificial body parts that add melee tools. + return stat.category == StatCategoryDefOf.Weapon && + req.Def is ThingDef { isTechHediff: true } thingDef && AllMeleeVerbPropsWithSource(thingDef).Any(); + } + + /// + /// Get all melee verbs provided by a given weapon or tech hediff (e.g. artificial body part). + /// + /// The def to fetch verbs for. + /// Available melee VerbProperties + Tool + Maneuver combinations for the given def. + protected IEnumerable AllMeleeVerbPropsWithSource(ThingDef thingDef) + { + var verbs = thingDef.Verbs; + var tools = thingDef.tools; + + // For tech hediffs like artificial body parts, lookup whether they add any verbs when installed + // and return them if so. + if (thingDef.isTechHediff) + { + var props = DefDatabase.AllDefsListForReading + .Where(recipe => recipe.IsIngredient(thingDef)) + .Select(recipe => recipe.addsHediff.CompProps()) + .FirstOrDefault(props => props != null); + + if (props?.tools.All(tool => tool is ToolCE) ?? false) + { + verbs = props.verbs; + tools = props.tools; + } + } + + return VerbUtility + .GetAllVerbProperties(verbs, tools) + .Where(vps => vps.verbProps.IsMeleeAttack); + } + + /// + /// Get the selection weight fir a single VerbProperties + Tool combination of a weapon, + /// adjusted according to the wielder's (if any) stats/capacities and multipliers from the weapon's stuff. + /// + /// Note that this is only correct for calculating weights for a single weapon, since a pawn will likely + /// have "natural" melee verbs provided by body parts, which may influence final weighting. + /// + /// Verb data (VerbProperties + Tool + Maneuver) + /// The wielder of the weapon, or null if the stat request is for an unwielded weapon. + /// Stat request for a weapon or weapon def + /// Selection weight adjusted for the wielder and weapon. + + protected float AdjustedMeleeSelectionWeight(VerbUtility.VerbPropertiesWithSource vps, Pawn pawn, StatRequest req) + { + return req.HasThing ? + vps.verbProps.AdjustedMeleeSelectionWeight(vps.tool, pawn, req.Thing, hediffCompSource: null, comesFromPawnNativeVerbs: false) : + vps.verbProps.AdjustedMeleeSelectionWeight(vps.tool, pawn, req.Def as ThingDef, req.StuffDef, hediffCompSource: null, comesFromPawnNativeVerbs: false); + } + + protected Pawn GetCurrentWielder(StatRequest req) => req.Thing as Pawn ?? (req.Thing?.ParentHolder as Pawn_EquipmentTracker)?.pawn; }