Skip to content

Commit

Permalink
WIP CE compat, added outcome pct readout.
Browse files Browse the repository at this point in the history
  • Loading branch information
Epicguru committed Apr 10, 2023
1 parent aa95aed commit f878bae
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 82 deletions.
6 changes: 5 additions & 1 deletion Languages/English/Keyed/Keys.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@

<AM.Error.Exec.FloatMenu>Cannot execute: {0}</AM.Error.Exec.FloatMenu>
<AM.Exec.FloatMenu>Execute {0}</AM.Exec.FloatMenu>
<AM.Exec.FloatMenu.Tip>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.</AM.Exec.FloatMenu.Tip>
<AM.Exec.FloatMenu.Tip>Perform an execution animation on {1}.\nPossible outcomes based on weapon, melee skill, armor, mod settings etc:\n{2}</AM.Exec.FloatMenu.Tip>
<AM.Exec.FloatMenu.Walk> (walk to target)</AM.Exec.FloatMenu.Walk>
<AM.Exec.FloatMenu.Lasso> (lasso)</AM.Exec.FloatMenu.Lasso>

<AM.Error.Grapple.Internal>Int.Error: {Error}</AM.Error.Grapple.Internal>
<AM.Error.Exec.Internal>Int.Error: {Error}</AM.Error.Exec.Internal>
<AM.Error.Grapple.AlreadyAtTarget>{Target} is already in melee range</AM.Error.Grapple.AlreadyAtTarget>

<AM.ProbReport.Kill>Kill</AM.ProbReport.Kill>
<AM.ProbReport.Down>Down</AM.ProbReport.Down>
<AM.ProbReport.Injure>Injure</AM.ProbReport.Injure>

<!-- Duel command messages -->
<AM.Error.Duel.Internal>Int.Error: {Error}</AM.Error.Duel.Internal>
<AM.Error.Duel.Dead>{Pawn} is dead</AM.Error.Duel.Dead>
Expand Down
12 changes: 12 additions & 0 deletions Source/ThingGenerator/Outcome/IOutcomeWorker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using RimWorld;
using System.Collections.Generic;
using Verse;

namespace AM.Outcome;

public interface IOutcomeWorker
{
IEnumerable<PossibleMeleeAttack> GetMeleeAttacksFor(ThingWithComps weapon, Pawn pawn);

float GetChanceToPenAprox(Pawn pawn, BodyPartRecord bodyPart, StatDef armorType, float armorPen);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AM.Outcome;
using UnityEngine;
using Verse;

Expand All @@ -14,6 +15,8 @@ namespace AM;
/// </summary>
public static class OutcomeUtility
{
public static IOutcomeWorker OutcomeWorker = new VanillaOutcomeWorker();

public static readonly Func<float, float, bool> GreaterThan = (x, y) => x > y;
public static readonly Func<float, float, bool> LessThan = (x, y) => x < y;

Expand All @@ -22,7 +25,7 @@ public static class OutcomeUtility
public static readonly Func<PossibleMeleeAttack, float> PenXDmg = a => a.Damage * a.ArmorPen;

[UsedImplicitly]
[TweakValue("Melee Animations")]
[TweakValue("Melee Animation")]
private static bool debugLogExecutionOutcome;

public ref struct AdditionalArgs
Expand Down Expand Up @@ -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);
}

/// <summary>
/// Generates a random execution outcome based on an lethality value.
/// </summary>
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)
{
Expand All @@ -135,38 +136,63 @@ 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.
bool preventKill = Core.Settings.ExecutionsOnFriendliesAreNotLethal && (victim.IsColonist || victim.IsSlaveOfColony || victim.IsPrisonerOfColony || victim.Faction == Faction.OfPlayerSilentFail);
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)
Expand Down Expand Up @@ -205,7 +231,7 @@ static string AllVerbs(ThingWithComps t)
TableDataGetter<ThingWithComps>[] table = new TableDataGetter<ThingWithComps>[4];
table[0] = new TableDataGetter<ThingWithComps>("Def Name", d => d.def.defName);
table[1] = new TableDataGetter<ThingWithComps>("Name", d => d.LabelCap);
table[2] = new TableDataGetter<ThingWithComps>("Main Attack", d => VerbToString(SelectMainAttack(GetMeleeAttacksFor(d, null)).Verb));
table[2] = new TableDataGetter<ThingWithComps>("Main Attack", d => VerbToString(SelectMainAttack(OutcomeWorker.GetMeleeAttacksFor(d, null)).Verb));
table[3] = new TableDataGetter<ThingWithComps>("All Verbs", AllVerbs);

DebugTables.MakeTablesDialog(created, table);
Expand All @@ -214,33 +240,6 @@ static string AllVerbs(ThingWithComps t)
t.Destroy();
}

public static IEnumerable<PossibleMeleeAttack> GetMeleeAttacksFor(ThingWithComps weapon, Pawn pawn)
{
var comp = weapon?.GetComp<CompEquippable>();
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<PossibleMeleeAttack> attacks) => SelectMainAttack(attacks, Dmg, GreaterThan);

public static PossibleMeleeAttack SelectMainAttack(IEnumerable<PossibleMeleeAttack> attacks, Func<PossibleMeleeAttack, float> valueSelector, Func<float, float, bool> compareFunc)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 $"<color={hex}>{pct*100:F0}%</color>";
}

public override string ToString() => $"{"AM.ProbReport.Kill".Trs()}: {InColor(KillChance)}\n{"AM.ProbReport.Down".Trs()}: {InColor(DownChance)}\n{"AM.ProbReport.Injure".Trs()}: {InColor(InjureChance)}";
}
}
15 changes: 15 additions & 0 deletions Source/ThingGenerator/Outcome/PossibleMeleeAttack.cs
Original file line number Diff line number Diff line change
@@ -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();
}
59 changes: 59 additions & 0 deletions Source/ThingGenerator/Outcome/VanillaOutcomeWorker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using RimWorld;
using System.Collections.Generic;
using UnityEngine;
using Verse;

namespace AM.Outcome;

public sealed class VanillaOutcomeWorker : IOutcomeWorker
{
public IEnumerable<PossibleMeleeAttack> GetMeleeAttacksFor(ThingWithComps weapon, Pawn pawn)
{
var comp = weapon?.GetComp<CompEquippable>();
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit f878bae

Please sign in to comment.