From f878bae0c7165ba727bdaf9787c7e0a63817a0bb Mon Sep 17 00:00:00 2001 From: James Date: Mon, 10 Apr 2023 15:27:13 +0100 Subject: [PATCH] WIP CE compat, added outcome pct readout. --- Languages/English/Keyed/Keys.xml | 6 +- .../ThingGenerator/Outcome/IOutcomeWorker.cs | 12 ++ .../{ => Outcome}/OutcomeUtility.cs | 149 ++++++++---------- .../Outcome/PossibleMeleeAttack.cs | 15 ++ .../Outcome/VanillaOutcomeWorker.cs | 59 +++++++ ...atch_FloatMenuMakerMap_AddDraftedOrders.cs | 4 +- 6 files changed, 163 insertions(+), 82 deletions(-) create mode 100644 Source/ThingGenerator/Outcome/IOutcomeWorker.cs rename Source/ThingGenerator/{ => Outcome}/OutcomeUtility.cs (85%) create mode 100644 Source/ThingGenerator/Outcome/PossibleMeleeAttack.cs create mode 100644 Source/ThingGenerator/Outcome/VanillaOutcomeWorker.cs diff --git a/Languages/English/Keyed/Keys.xml b/Languages/English/Keyed/Keys.xml index 1cfa3b35..7ee90d74 100644 --- a/Languages/English/Keyed/Keys.xml +++ b/Languages/English/Keyed/Keys.xml @@ -12,7 +12,7 @@ Cannot execute: {0} Execute {0} - Perform an execution animation on {1}.\nPlease note, 'execution' animations are not always lethal - please read the workshop page and mod settings for more info. + Perform an execution animation on {1}.\nPossible outcomes based on weapon, melee skill, armor, mod settings etc:\n{2} (walk to target) (lasso) @@ -20,6 +20,10 @@ Int.Error: {Error} {Target} is already in melee range + Kill + Down + Injure + Int.Error: {Error} {Pawn} is dead diff --git a/Source/ThingGenerator/Outcome/IOutcomeWorker.cs b/Source/ThingGenerator/Outcome/IOutcomeWorker.cs new file mode 100644 index 00000000..15f88724 --- /dev/null +++ b/Source/ThingGenerator/Outcome/IOutcomeWorker.cs @@ -0,0 +1,12 @@ +using RimWorld; +using System.Collections.Generic; +using Verse; + +namespace AM.Outcome; + +public interface IOutcomeWorker +{ + IEnumerable GetMeleeAttacksFor(ThingWithComps weapon, Pawn pawn); + + float GetChanceToPenAprox(Pawn pawn, BodyPartRecord bodyPart, StatDef armorType, float armorPen); +} diff --git a/Source/ThingGenerator/OutcomeUtility.cs b/Source/ThingGenerator/Outcome/OutcomeUtility.cs similarity index 85% rename from Source/ThingGenerator/OutcomeUtility.cs rename to Source/ThingGenerator/Outcome/OutcomeUtility.cs index bdc69ddd..5753d732 100644 --- a/Source/ThingGenerator/OutcomeUtility.cs +++ b/Source/ThingGenerator/Outcome/OutcomeUtility.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using AM.Outcome; using UnityEngine; using Verse; @@ -14,6 +15,8 @@ namespace AM; /// public static class OutcomeUtility { + public static IOutcomeWorker OutcomeWorker = new VanillaOutcomeWorker(); + public static readonly Func GreaterThan = (x, y) => x > y; public static readonly Func LessThan = (x, y) => x < y; @@ -22,7 +25,7 @@ public static class OutcomeUtility public static readonly Func PenXDmg = a => a.Damage * a.ArmorPen; [UsedImplicitly] - [TweakValue("Melee Animations")] + [TweakValue("Melee Animation")] private static bool debugLogExecutionOutcome; public ref struct AdditionalArgs @@ -102,28 +105,26 @@ public static float ChanceToBeatInMelee(Pawn a, Pawn b) float bL = b.GetStatValue(AM_DefOf.AM_DuelAbility); float diff = aL - bL; - return Mathf.Clamp01((float)normal.LeftProbability(diff));; + return Mathf.Clamp01((float)normal.LeftProbability(diff)); } - public static ExecutionOutcome GenerateRandomOutcome(Pawn attacker, Pawn victim) => GenerateRandomOutcome(attacker, victim, out _); - - public static ExecutionOutcome GenerateRandomOutcome(Pawn attacker, Pawn victim, out float pct) + public static ExecutionOutcome GenerateRandomOutcome(Pawn attacker, Pawn victim, ProbabilityReport report = null) { // Get lethality, adjusted by settings. float aL = attacker.GetStatValue(AM_DefOf.AM_Lethality); int meleeSkill = attacker.skills?.GetSkill(SkillDefOf.Melee)?.Level ?? 0; var weapon = attacker.GetFirstMeleeWeapon(); - var mainAttack = SelectMainAttack(GetMeleeAttacksFor(weapon, attacker)); + var mainAttack = SelectMainAttack(OutcomeWorker.GetMeleeAttacksFor(weapon, attacker)); var damageDef = mainAttack.DamageDef; var corePart = GetCoreBodyPart(victim); - return GenerateRandomOutcome(damageDef, victim, corePart, mainAttack.ArmorPen, meleeSkill, aL, out pct); + return GenerateRandomOutcome(damageDef, victim, corePart, mainAttack.ArmorPen, meleeSkill, aL, report); } /// /// Generates a random execution outcome based on an lethality value. /// - public static ExecutionOutcome GenerateRandomOutcome(DamageDef dmgDef, Pawn victim, BodyPartRecord bodyPart, float weaponPen, float attackerMeleeSkill, float lethality, out float pct) + public static ExecutionOutcome GenerateRandomOutcome(DamageDef dmgDef, Pawn victim, BodyPartRecord bodyPart, float weaponPen, float attackerMeleeSkill, float lethality, ProbabilityReport report = null) { static void Log(string msg) { @@ -135,10 +136,14 @@ static void Log(string msg) var armorStat = dmgDef?.armorCategory?.armorRatingStat ?? StatDefOf.ArmorRating_Sharp; Log($"Armor stat: {armorStat}, weapon pen: {weaponPen:F1}, lethality: {lethality:F2}"); float armorMulti = Core.Settings.ExecutionArmorCoefficient; - float chanceToPen = GetChanceToPenAprox(victim, bodyPart, armorStat, weaponPen); + float chanceToPen = OutcomeWorker.GetChanceToPenAprox(victim, bodyPart, armorStat, weaponPen); Log($"Chance to pen (base): {chanceToPen:P1}"); - Log($"Chance to pen (post-process): {(armorMulti <= 0f ? 1f : chanceToPen / armorMulti):P1}"); - bool canPen = armorMulti <= 0 || Rand.Chance(chanceToPen / armorMulti); + if (armorMulti > 0f) + chanceToPen /= armorMulti; + else + chanceToPen = 1f; + Log($"Chance to pen (post-process): {chanceToPen:P1}"); + bool canPen = Rand.Chance(chanceToPen); Log($"Random will pen outcome: {canPen}"); // Calculate kill chance based on lethality and settings. @@ -146,27 +151,48 @@ static void Log(string msg) bool attemptKill = Rand.Chance(lethality); Log($"Prevent kill: {preventKill}"); Log($"Attempt kill: {attemptKill}"); + + if (report != null) + report.KillChance = preventKill ? 0f : chanceToPen * lethality; + + var outcome = ExecutionOutcome.Nothing; if (canPen && !preventKill && attemptKill) { Log("Killed!"); - pct = lethality; - return ExecutionOutcome.Kill; + outcome = ExecutionOutcome.Kill; + if (report == null) + return outcome; } Log("Moving on to down or injure..."); float downChance = RemapClamped(4, 20, 0.2f, 0.9f, attackerMeleeSkill); Log(canPen ? $"Chance to down, based on melee skill of {attackerMeleeSkill:N1}: {downChance:P1}" : "Cannot down, pen chance failed. Will damage."); - pct = 1f - lethality; - if (canPen && Rand.Chance(downChance)) + if (report != null) + { + report.DownChance = (1f - lethality) * downChance * chanceToPen; + report.InjureChance = (1f - lethality) * (1f - downChance); + } + + if (outcome == ExecutionOutcome.Nothing && canPen && Rand.Chance(downChance)) { Log("Downed"); - return ExecutionOutcome.Down; + outcome = ExecutionOutcome.Down; + if (report == null) + return outcome; } // Damage! - Log("Damaged"); - return ExecutionOutcome.Damage; + if (outcome == ExecutionOutcome.Nothing) + { + Log("Damaged"); + outcome = ExecutionOutcome.Damage; + if (report == null) + return outcome; + } + + report?.Normalize(); + return outcome; } private static float RemapClamped(float baseA, float baseB, float newA, float newB, float value) @@ -205,7 +231,7 @@ static string AllVerbs(ThingWithComps t) TableDataGetter[] table = new TableDataGetter[4]; table[0] = new TableDataGetter("Def Name", d => d.def.defName); table[1] = new TableDataGetter("Name", d => d.LabelCap); - table[2] = new TableDataGetter("Main Attack", d => VerbToString(SelectMainAttack(GetMeleeAttacksFor(d, null)).Verb)); + table[2] = new TableDataGetter("Main Attack", d => VerbToString(SelectMainAttack(OutcomeWorker.GetMeleeAttacksFor(d, null)).Verb)); table[3] = new TableDataGetter("All Verbs", AllVerbs); DebugTables.MakeTablesDialog(created, table); @@ -214,33 +240,6 @@ static string AllVerbs(ThingWithComps t) t.Destroy(); } - public static IEnumerable GetMeleeAttacksFor(ThingWithComps weapon, Pawn pawn) - { - var comp = weapon?.GetComp(); - if (comp == null) - yield break; - - - foreach (var verb in comp.AllVerbs) - { - if (!verb.IsMeleeAttack) - continue; - - float dmg = verb.verbProps.AdjustedMeleeDamageAmount(verb.tool, pawn, weapon, verb.HediffCompSource); - float ap = verb.verbProps.AdjustedArmorPenetration(verb.tool, pawn, weapon, verb.HediffCompSource); - - yield return new PossibleMeleeAttack - { - Damage = dmg, - ArmorPen = ap, - Pawn = pawn, - DamageDef = verb.GetDamageDef(), - Verb = verb, - Weapon = weapon - }; - } - } - public static PossibleMeleeAttack SelectMainAttack(IEnumerable attacks) => SelectMainAttack(attacks, Dmg, GreaterThan); public static PossibleMeleeAttack SelectMainAttack(IEnumerable attacks, Func valueSelector, Func compareFunc) @@ -273,31 +272,6 @@ record = value; return selected; } - public static float GetChanceToPenAprox(Pawn pawn, BodyPartRecord bodyPart, StatDef armorType, float armorPen) - { - // Get skin & hediff chance-to-pen. - float pawnArmor = pawn.GetStatValue(armorType) - armorPen; - float chance = Mathf.Clamp01(1f - pawnArmor); - - if (pawn?.apparel == null) - return chance; - - // Get apparel chance-to-pen. - foreach (var a in pawn.apparel.WornApparel) - { - if (!a.def.apparel.CoversBodyPart(bodyPart)) - continue; - - var armor = a.GetStatValue(armorType); - if (armor <= 0) - continue; - - chance *= Mathf.Clamp01(1f - (armor - armorPen)); - } - - return chance; - } - private static bool Damage(Pawn attacker, Pawn pawn, in AdditionalArgs args) { // Damage but do not kill or down the target. @@ -479,15 +453,30 @@ private static LogEntry_DamageResult CreateLog(RulePackDef def, Thing weapon, Pa return log; } - public readonly struct PossibleMeleeAttack + public class ProbabilityReport { - public Pawn Pawn { get; init; } - public float Damage { get; init; } - public float ArmorPen{ get; init; } - public DamageDef DamageDef { get; init; } - public Verb Verb { get; init; } - public ThingWithComps Weapon { get; init; } - - public override string ToString() => Verb.ToString(); + public float DownChance { get; set; } + public float InjureChance { get; set; } + public float KillChance { get; set; } + + public void Normalize() + { + float sum = DownChance + InjureChance + KillChance; + if (sum == 0f) + return; + + DownChance /= sum; + InjureChance /= sum; + KillChance /= sum; + } + + private static string InColor(float pct) + { + Color c = Color.Lerp(Color.red, Color.green, pct); + string hex = ColorUtility.ToHtmlStringRGB(c); + return $"{pct*100:F0}%"; + } + + public override string ToString() => $"{"AM.ProbReport.Kill".Trs()}: {InColor(KillChance)}\n{"AM.ProbReport.Down".Trs()}: {InColor(DownChance)}\n{"AM.ProbReport.Injure".Trs()}: {InColor(InjureChance)}"; } } diff --git a/Source/ThingGenerator/Outcome/PossibleMeleeAttack.cs b/Source/ThingGenerator/Outcome/PossibleMeleeAttack.cs new file mode 100644 index 00000000..53fbe125 --- /dev/null +++ b/Source/ThingGenerator/Outcome/PossibleMeleeAttack.cs @@ -0,0 +1,15 @@ +using Verse; + +namespace AM.Outcome; + +public readonly struct PossibleMeleeAttack +{ + public Pawn Pawn { get; init; } + public float Damage { get; init; } + public float ArmorPen { get; init; } + public DamageDef DamageDef { get; init; } + public Verb Verb { get; init; } + public ThingWithComps Weapon { get; init; } + + public override string ToString() => Verb.ToString(); +} diff --git a/Source/ThingGenerator/Outcome/VanillaOutcomeWorker.cs b/Source/ThingGenerator/Outcome/VanillaOutcomeWorker.cs new file mode 100644 index 00000000..001ab549 --- /dev/null +++ b/Source/ThingGenerator/Outcome/VanillaOutcomeWorker.cs @@ -0,0 +1,59 @@ +using RimWorld; +using System.Collections.Generic; +using UnityEngine; +using Verse; + +namespace AM.Outcome; + +public sealed class VanillaOutcomeWorker : IOutcomeWorker +{ + public IEnumerable GetMeleeAttacksFor(ThingWithComps weapon, Pawn pawn) + { + var comp = weapon?.GetComp(); + if (comp == null) + yield break; + + foreach (var verb in comp.AllVerbs) + { + if (!verb.IsMeleeAttack) + continue; + + float dmg = verb.verbProps.AdjustedMeleeDamageAmount(verb.tool, pawn, weapon, verb.HediffCompSource); + float ap = verb.verbProps.AdjustedArmorPenetration(verb.tool, pawn, weapon, verb.HediffCompSource); + yield return new PossibleMeleeAttack + { + Damage = dmg, + ArmorPen = ap, + Pawn = pawn, + DamageDef = verb.GetDamageDef(), + Verb = verb, + Weapon = weapon + }; + } + } + + public float GetChanceToPenAprox(Pawn pawn, BodyPartRecord bodyPart, StatDef armorType, float armorPen) + { + // Get skin & hediff chance-to-pen. + float pawnArmor = pawn.GetStatValue(armorType) - armorPen; + float chance = Mathf.Clamp01(1f - pawnArmor); + + if (pawn?.apparel == null) + return chance; + + // Get apparel chance-to-pen. + foreach (var a in pawn.apparel.WornApparel) + { + if (!a.def.apparel.CoversBodyPart(bodyPart)) + continue; + + var armor = a.GetStatValue(armorType); + if (armor <= 0) + continue; + + chance *= Mathf.Clamp01(1f - (armor - armorPen)); + } + + return chance; + } +} \ No newline at end of file diff --git a/Source/ThingGenerator/Patches/Patch_FloatMenuMakerMap_AddDraftedOrders.cs b/Source/ThingGenerator/Patches/Patch_FloatMenuMakerMap_AddDraftedOrders.cs index 64a94648..5adf36f8 100644 --- a/Source/ThingGenerator/Patches/Patch_FloatMenuMakerMap_AddDraftedOrders.cs +++ b/Source/ThingGenerator/Patches/Patch_FloatMenuMakerMap_AddDraftedOrders.cs @@ -298,7 +298,9 @@ void OnClick() string label = "AM.Exec.FloatMenu".Translate(targetName) + append; string tt = "AM.Exec.FloatMenu.Tip"; - tt = tt.Translate(grappler.Name.ToStringShort, targetName); + var probs = new OutcomeUtility.ProbabilityReport(); + OutcomeUtility.GenerateRandomOutcome(grappler, target, probs); + tt = tt.Translate(grappler.Name.ToStringShort, targetName, probs.ToString()); var priority = enemy ? MenuOptionPriority.AttackEnemy : MenuOptionPriority.VeryLow; return new FloatMenuOption(label, OnClick, priority, revalidateClickTarget: target)