diff --git a/1.4/Assemblies/0PrepatcherAPI.dll b/1.4/Assemblies/0PrepatcherAPI.dll new file mode 100755 index 0000000..8213683 Binary files /dev/null and b/1.4/Assemblies/0PrepatcherAPI.dll differ diff --git a/1.4/Assemblies/CombatAI.dll b/1.4/Assemblies/CombatAI.dll index 49f8249..e36930c 100644 Binary files a/1.4/Assemblies/CombatAI.dll and b/1.4/Assemblies/CombatAI.dll differ diff --git a/1.4/Defs/DutyDefs/Duties_Misc.xml b/1.4/Defs/DutyDefs/Duties_Misc.xml new file mode 100755 index 0000000..cbc0627 --- /dev/null +++ b/1.4/Defs/DutyDefs/Duties_Misc.xml @@ -0,0 +1,101 @@ + + + + CombatAI_AssaultPoint + true + + + + +
  • + true +
  • +
  • + 65 + 72 +
  • +
  • + 16 + +
  • + SatisfyBasicNeedsAndWork +
  • +
    + +
  • + 8 + Sprint +
  • +
  • + true +
  • +
  • +
  • + + + + + + CombatAI_AssaultPawn + true + + + + +
  • + true +
  • +
  • + 65 + 72 +
  • +
  • + 16 + +
  • + SatisfyBasicNeedsAndWork +
  • + + +
  • + 8 + Sprint +
  • +
  • + true +
  • +
  • +
  • + + + + + + CombatAI_Escort + true + + +
  • + true +
  • +
  • + 65 + 72 +
  • +
  • +
  • + SatisfyVeryUrgentNeeds +
  • +
  • + 8 +
  • +
  • + true +
  • +
  • +
  • + + + + + \ No newline at end of file diff --git a/1.4/Defs/HyperText/HyperText_DebugTutorial.xml b/1.4/Defs/HyperText/HyperText_DebugTutorial.xml new file mode 100644 index 0000000..4e4d168 --- /dev/null +++ b/1.4/Defs/HyperText/HyperText_DebugTutorial.xml @@ -0,0 +1,76 @@ + + + + CombatAI_DevJobTutorial1 + + + + +

    Thank you for participating in [color=green]CAI-5000[/color] open test

    + +

    In this tutorial you will learn how create bug reports for [color=green]CAI-5000[/color]

    + +

    [color='yellow']NOTE:[/color] This is a test build thus it's significantly slower and will consume a lot more memory

    +
    +
    + + + CombatAI_DevJobTutorial2 + +

    Job Logs and Troubleshooting Pawns

    + +

    The [color='green']job log[/color] is used to troubleshoot pawns doing something they [color=red]shouldn't[/color] be doing.

    + +

    You can access a pawn's job log by:

    + +

    1. Select the problematic pawn

    +

    2. Click on the [color='yellow']"DEV: View job logs"[/color] gizmo button. The button is showen in screenshot (1). The window in screenshot (2) should appear

    + +

    [color=yellow]WARNING:[/color]If you select a different pawn while the window is open the window will automatically switch to viewing their job log.

    + +

    Screenshot (1): The job log gizmo button

    + + +

    Screenshot (2): The job log window

    + +

    [color=yellow]WARNING:[/color]If you select a different pawn while the window is open the window will automatically switch to viewing their job log.

    +
    +
    + + + CombatAI_DevJobTutorial3 + +

    Creating an initial report

    + +

    1. [color=yellow]Unpause[/color] the game after opening the job log window for a second or 2 incase the job log is empty.

    +

    2. Click on the [color=green]Copy short report to clipboard[/color] button in the top right corner. The green button shown in screenshot (3)

    +

    3. Join the RocketMan discord server https://discord.gg/ftCjYB7jDe and post the report in the [color=yellow]#combat-ai-feedback channel[/color]

    + +

    Screenshot (3): Copy short report to clipboard in the top right corner of the job log window.

    +
    +
    + + + CombatAI_DevJobTutorial4 + +

    Investigating AI choices

    +

    Sometimes you might want to investigate AI choices like a pawn choosing a really bad cover position, pawn standing still or pawn dancing around in loops.

    + +

    In the job log window, when you select a job, it'll show you information about the job. Example showen in screenshot (4)

    + +

    Screenshot (4): The section showing your current selection information.

    + +

    Investigating cover position

    +

    You go about investigating what job made the pawn go to a specific location by:

    + +

    1. Select a random job at the top of the job log.

    +

    2.1. In the information section clicking on the [color=green]"origin:"[/color] section will hightlight the cell at which the pawn made the decision.

    +

    2.2. In the information section clicking on the [color=green]"destination:"[/color] section will hightlight the target cell the pawn choose to go to. Note that if the value shown is (-1000,-1000,-1000) then that mean the selected job didn't include moving to another cell.

    +

    3. Repeat steps 1 and 2 until you land on a job that matches your obesrvations.

    + +

    Some tips:

    +

    1. Jobs that involve movement start typically with the prefix "Goto" so any job with Goto in the name is a good place to start searching.

    +

    2. If a pawn is not moving selecting jobs starting with "Wait" prefix will most likely lead to the problematic job.

    +
    +
    +
    \ No newline at end of file diff --git a/1.4/Defs/Jobs/Jobs_Misc.xml b/1.4/Defs/Jobs/Jobs_Misc.xml new file mode 100644 index 0000000..7575683 --- /dev/null +++ b/1.4/Defs/Jobs/Jobs_Misc.xml @@ -0,0 +1,37 @@ + + + + CombatAI_Goto_Retreat + JobDriver_Goto + retreating. + false + true + false + false + true + + + + CombatAI_Goto_Duck + JobDriver_Goto + moving. + false + true + false + Never + false + true + + + + CombatAI_Goto_Cover + JobDriver_Goto + moving. + true + true + false + Never + false + true + + \ No newline at end of file diff --git a/1.4/Languages/English/Keyed/Translations.xml b/1.4/Languages/English/Keyed/Translations.xml index 10d2665..5a8546f 100644 --- a/1.4/Languages/English/Keyed/Translations.xml +++ b/1.4/Languages/English/Keyed/Translations.xml @@ -14,6 +14,8 @@ This quick-setup page will help you quickly setup CAI-5000's different features! Quick Setup Options Please select a difficulty level + {0} difficulty level applied! + WARNING: {0} difficulty level might cause performance issues! Needs to be mounted to a wall or a solid structure. Preparing Combat AI Hold @@ -23,11 +25,12 @@ Easy/Perf Normal Hard + Make the AI aware of cell temperature Deathwish Applied preset: Performance presets will determine the complexity of AI calculations and their interval. More complex calculations means harder AI but lower performance. Default is normal. Enable sprinting - If this is disabled: Pathetic F F...\n if not: Chad :thumbsup + Allows the AI to use sprinting when dashing for cover. Turn this off if you don't like kitting simulators. Enable tactical groups Raiders will be divided into tactical groups (2-10) each with their own objective. Not all pawns will be assigned to groups, some will remain on the default assault duty. Cost multiplier for pathing through walls (default is 1.0) diff --git a/1.4/Patches/Duties_Misc.xml b/1.4/Patches/Duties_Misc.xml new file mode 100644 index 0000000..43fc4d1 --- /dev/null +++ b/1.4/Patches/Duties_Misc.xml @@ -0,0 +1,38 @@ + + + + + + \ No newline at end of file diff --git a/1.4/Patches/ThinkTree_Humanlike.xml b/1.4/Patches/ThinkTree_Humanlike.xml new file mode 100644 index 0000000..cf26a70 --- /dev/null +++ b/1.4/Patches/ThinkTree_Humanlike.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/1.4/Textures/Isma/Tutorials/JobLog/clipboard_screenshot.png b/1.4/Textures/Isma/Tutorials/JobLog/clipboard_screenshot.png new file mode 100644 index 0000000..64f6875 Binary files /dev/null and b/1.4/Textures/Isma/Tutorials/JobLog/clipboard_screenshot.png differ diff --git a/1.4/Textures/Isma/Tutorials/JobLog/gizmo_screenshot.png b/1.4/Textures/Isma/Tutorials/JobLog/gizmo_screenshot.png new file mode 100644 index 0000000..5fc5e1f Binary files /dev/null and b/1.4/Textures/Isma/Tutorials/JobLog/gizmo_screenshot.png differ diff --git a/1.4/Textures/Isma/Tutorials/JobLog/position_screenshot.png b/1.4/Textures/Isma/Tutorials/JobLog/position_screenshot.png new file mode 100644 index 0000000..b6ba166 Binary files /dev/null and b/1.4/Textures/Isma/Tutorials/JobLog/position_screenshot.png differ diff --git a/1.4/Textures/Isma/Tutorials/JobLog/selection_screenshot.png b/1.4/Textures/Isma/Tutorials/JobLog/selection_screenshot.png new file mode 100644 index 0000000..0000822 Binary files /dev/null and b/1.4/Textures/Isma/Tutorials/JobLog/selection_screenshot.png differ diff --git a/1.4/Textures/Isma/Tutorials/JobLog/window_screenshot.png b/1.4/Textures/Isma/Tutorials/JobLog/window_screenshot.png new file mode 100644 index 0000000..387b4c0 Binary files /dev/null and b/1.4/Textures/Isma/Tutorials/JobLog/window_screenshot.png differ diff --git a/1.4/Textures/Isma/logo.png b/1.4/Textures/Isma/logo.png new file mode 100644 index 0000000..3dba7fd Binary files /dev/null and b/1.4/Textures/Isma/logo.png differ diff --git a/About/About.xml b/About/About.xml index 3508450..d58ead0 100755 --- a/About/About.xml +++ b/About/About.xml @@ -14,14 +14,22 @@ steam://url/CommunityFilePage/2009463077 https://github.com/pardeike/HarmonyRimWorld/releases/latest
  • - +
  • + zetrith.prepatcher + Prepatcher + steam://url/CommunityFilePage/2934420800 + https://github.com/Zetrith/Prepatcher/releases/latest +
  • +
  • Ludeon.RimWorld
  • Ludeon.RimWorld.Royalty
  • brrainz.harmony
  • +
  • zetrith.prepatcher
  • +
  • Victor.WallsAreSolid
  • fed1sPlay.PawnTargetFix
  • brrainz.lineofsightfix
  • majorhoff.rimthreaded
  • diff --git a/About/Preview.png b/About/Preview.png index 3dbedcc..760ab11 100644 Binary files a/About/Preview.png and b/About/Preview.png differ diff --git a/Source/Rule56/ArmorReport.cs b/Source/Rule56/ArmorReport.cs index 237228a..66cc224 100644 --- a/Source/Rule56/ArmorReport.cs +++ b/Source/Rule56/ArmorReport.cs @@ -42,6 +42,10 @@ public struct ArmorReport /// public int createdAt; /// + /// Comp shield for the shield belt. + /// + public CompShield shield; + /// /// Weak attributes. /// public MetaCombatAttribute weaknessAttributes; @@ -83,5 +87,25 @@ public float GetArmor(DamageDef damage) { return damage != null ? damage.armorCategory == DamageArmorCategoryDefOf.Sharp ? Sharp : Blunt : 0f; } + + /// + /// Get the appropriate armor for a damage def. + /// + /// Damage def + /// Armor value + public float GetBodyArmor(DamageDef damage) + { + return damage != null ? damage.armorCategory == DamageArmorCategoryDefOf.Sharp ? bodySharp : bodyBlunt : 0f; + } + + /// + /// Get the appropriate armor for a damage def. + /// + /// Damage def + /// Armor value + public float GetArmor(DamageArmorCategoryDef category) + { + return category != null ? category == DamageArmorCategoryDefOf.Sharp ? Sharp : Blunt : 0f; + } } } diff --git a/Source/Rule56/ArmorUtility.cs b/Source/Rule56/ArmorUtility.cs index 5bc07f2..e17366d 100644 --- a/Source/Rule56/ArmorUtility.cs +++ b/Source/Rule56/ArmorUtility.cs @@ -76,6 +76,8 @@ private static void FillApparel(ref ArmorReport report, Listing_Collapsible coll { float armor_blunt = 0; float armor_sharp = 0; + float max_blunt = 0f; + float max_sharp = 0f; float coverage = 0; List apparels = pawn.apparel.WornApparel; for (int i = 0; i < apparels.Count; i++) @@ -88,16 +90,32 @@ private static void FillApparel(ref ArmorReport report, Listing_Collapsible coll report.hasShieldBelt |= isShield; if (apparel != null && apparel.def.apparel != null) { + if (isShield) + { + report.shield ??= apparel.GetComp(); + } float c = bodyApparels.Coverage(apparel.def.apparel); coverage += c; - armor_blunt += c * apparel.GetStatValue_Fast(StatDefOf.ArmorRating_Blunt, 2700); - armor_sharp += c * apparel.GetStatValue_Fast(StatDefOf.ArmorRating_Sharp, 2700); + float blunt = apparel.GetStatValue_Fast(StatDefOf.ArmorRating_Blunt, 2700); + float sharp = apparel.GetStatValue_Fast(StatDefOf.ArmorRating_Blunt, 2700); + if (max_sharp < sharp) + { + max_sharp = sharp; + } + if (max_blunt < blunt) + { + max_blunt = blunt; + } + armor_blunt += c * blunt; + armor_sharp += c * sharp; if (debug) { collapsible.Label($"{i}. {apparel.def.label},\tc={c}"); } } } + armor_blunt = Maths.Min(armor_blunt, max_blunt); + armor_sharp = Maths.Min(armor_sharp, max_sharp); if (coverage != 0) { report.apparelBlunt = armor_blunt; diff --git a/Source/Rule56/AvoidanceTracker.cs b/Source/Rule56/AvoidanceTracker.cs index cab22c4..0dc8b6f 100644 --- a/Source/Rule56/AvoidanceTracker.cs +++ b/Source/Rule56/AvoidanceTracker.cs @@ -65,7 +65,7 @@ public override void MapComponentTick() TryCastProximity(item.pawn, IntVec3.Invalid); if (item.pawn.pather?.MovingNow ?? false) { - TryCastPath(item.pawn); + TryCastPath(item); } } for (int i = 0; i < _removalList.Count; i++) @@ -211,7 +211,7 @@ public void Notify_Injury(Pawn pawn, DamageInfo dinfo) float f = Maths.Max(1 - node.dist / 5.65685424949f, 0.25f); affliction_dmg.Push(node.cell, dinfo.Amount * f); affliction_pen.Push(node.cell, dinfo.ArmorPenetrationInt * f); - }, maxDist: 5); + }, maxDist: 30, maxCellNum: 25, passThroughDoors: true); }); } @@ -240,11 +240,11 @@ private void TryCastProximity(Pawn pawn, IntVec3 dest) flooder.Flood(orig, node => { proximity.Set(node.cell, 1, flags); - }, maxDist: 2); + }, maxDist: 4, maxCellNum: 9, passThroughDoors: true); flooder.Flood(dest, node => { proximity.Set(node.cell, 1, flags); - }, maxDist: 2); + }, maxDist: 4, maxCellNum: 9, passThroughDoors: true); }); } else @@ -255,29 +255,34 @@ private void TryCastProximity(Pawn pawn, IntVec3 dest) flooder.Flood(orig, node => { proximity.Set(node.cell, 1, flags); - }, maxDist: 2); + }, maxDist: 4, maxCellNum: 9, passThroughDoors: true); }); } } - private void TryCastPath(Pawn pawn, PawnPath pawnPath = null) + private void TryCastPath(IBucketablePawn item, PawnPath pawnPath = null) { + Pawn pawn = item.pawn; pawnPath ??= pawn.pather?.curPath; if (pawnPath?.nodes == null || pawnPath.curNodeIndex <= 5) { return; } ulong flags = pawn.GetThingFlags(); - List cells = pawnPath.nodes.GetRange(Maths.Max(pawnPath.curNodeIndex - 80, 0), Maths.Min(pawnPath.curNodeIndex + 1, 80)); - //int count = Maths.Min(pawnPath.NodesLeftCount, 80); - //for (int i = 0; i < count; i++) - //{ - // cells.Add(pawnPath.Peek(i)); - //} + List cells = item.tempPath; + cells.Clear(); + int index = Maths.Max(pawnPath.curNodeIndex - 90, 0); + int limit = Maths.Min(index + 80, pawnPath.curNodeIndex + 1); + for (int i = index; i < limit; i++) + { + cells.Add(pawnPath.nodes[i]); + } +// cells.AddRange(pawnPath.nodes.GetRange(Maths.Max(pawnPath.curNodeIndex - 80, 0), Maths.Min(pawnPath.curNodeIndex + 1, 80))); if (cells.Count == 0) { return; } + WallGrid walls = map.GetComp_Fast(); asyncActions.EnqueueOffThreadAction(() => { path.Next(); @@ -285,25 +290,38 @@ private void TryCastPath(Pawn pawn, PawnPath pawnPath = null) for (int i = 1; i < cells.Count; i++) { IntVec3 cur = cells[i]; - path.Set(cur, 1, flags); - int dx = Math.Sign(prev.x - cur.x); - int dz = Math.Sign(prev.z - cur.z); - prev = cur; + int dx = Math.Sign(prev.x - cur.x); + int dz = Math.Sign(prev.z - cur.z); + int val = 1; + IntVec3 left; + IntVec3 right; if (dx == 0) { - path.Set(cur + new IntVec3(-1, 0, 0), 1, flags); - path.Set(cur + new IntVec3(1, 0, 0), 1, flags); + left = cur + new IntVec3(-1, 0, 0); + right = cur + new IntVec3(1, 0, 0); } else if (dz == 0) { - path.Set(cur + new IntVec3(0, 0, -1), 1, flags); - path.Set(cur + new IntVec3(0, 0, 1), 1, flags); + left = cur + new IntVec3(0, 0, -1); + right = cur + new IntVec3(0, 0, 1); } else { - path.Set(cur + new IntVec3(dx, 0, 0), 1, flags); - path.Set(cur + new IntVec3(0, 0, dz), 1, flags); + left = cur + new IntVec3(dx, 0, 0); + right = cur + new IntVec3(0, 0, dz); + } + if (!left.InBounds(map) || walls.GetFillCategory(left) == FillCategory.Full) + { + val++; + } + if (!right.InBounds(map) || walls.GetFillCategory(right) == FillCategory.Full) + { + val++; } + path.Set(left, (byte)val, flags); + path.Set(right, (byte)val, flags); + path.Set(cur, (byte)val, flags); + prev = cur; } cells.Clear(); }); @@ -321,8 +339,9 @@ private bool Valid(Pawn pawn) private struct IBucketablePawn : IBucketable { - public readonly Pawn pawn; - public readonly int bucketIndex; + public readonly Pawn pawn; + public readonly int bucketIndex; + public readonly List tempPath; public int BucketIndex { @@ -337,6 +356,7 @@ public IBucketablePawn(Pawn pawn, int bucketIndex) { this.pawn = pawn; this.bucketIndex = bucketIndex; + this.tempPath = new List(64); } } diff --git a/Source/Rule56/CellFlooder.cs b/Source/Rule56/CellFlooder.cs index 395ec1f..77e9f1e 100644 --- a/Source/Rule56/CellFlooder.cs +++ b/Source/Rule56/CellFlooder.cs @@ -28,15 +28,15 @@ public CellFlooder(Map map) sigArray = new int[map.cellIndices.NumGridCells]; } - public void Flood(IntVec3 center, Action action, Func costFunction = null, Func validator = null, int maxDist = 25) + public void Flood(IntVec3 center, Action action, Func costFunction = null, Func validator = null, int maxDist = 25, int maxCellNum = 9999, bool passThroughDoors = false) { - Flood(center, node => action(node.cell, node.parent, node.dist), costFunction, validator, maxDist); + Flood(center, node => action(node.cell, node.parent, node.dist), costFunction, validator, maxDist, maxCellNum, passThroughDoors); } - public void Flood(IntVec3 center, Action action, Func costFunction = null, Func validator = null, int maxDist = 25) + public void Flood(IntVec3 center, Action action, Func costFunction = null, Func validator = null, int maxDist = 25, int maxCellNum = 9999, bool passThroughDoors = false) { sig++; - Func blocked = GetBlockedTestFunc(validator); + Func blocked = GetBlockedTestFunc(validator, passThroughDoors); walls = map.GetComponent(); Node node = GetIntialFloodedCell(center); node.dist = costFunction != null ? costFunction(node.cell) : 0; @@ -50,13 +50,13 @@ public void Flood(IntVec3 center, Action action, Func cost // floodedCells.Add(node); floodQueue.Clear(); floodQueue.Enqueue(node); - while (floodQueue.Count > 0) + int num = 0; + while (floodQueue.Count > 0 && num++ < maxCellNum) { node = floodQueue.Dequeue(); // // TODO optimize this some more action(node); - // map.debugDrawer.FlashCell(node.cell, node.dist / 25f, $"{map.cellIndices.CellToIndex(node.cell)} {map.cellIndices.CellToIndex(node.parent)}", duration: 15); // // check for the distance @@ -104,13 +104,27 @@ public void Flood(IntVec3 center, Action action, Func cost } } - private Func GetBlockedTestFunc(Func validator) + private Func GetBlockedTestFunc(Func validator, bool passThroughDoors) { if (validator == null) { - return cell => walls.GetFillCategory(cell) == FillCategory.Full; + if (!passThroughDoors) + { + return cell => walls.GetFillCategory(cell) == FillCategory.Full; + } + else + { + return cell => walls.GetFillCategoryNoDoors(cell) == FillCategory.Full; + } + } + if (!passThroughDoors) + { + return cell => walls.GetFillCategory(cell) == FillCategory.Full || !validator(cell); + } + else + { + return cell => walls.GetFillCategoryNoDoors(cell) == FillCategory.Full || !validator(cell); } - return cell => walls.GetFillCategory(cell) == FillCategory.Full || !validator(cell); } private Node GetIntialFloodedCell(IntVec3 center) diff --git a/Source/Rule56/CellMetrics.cs b/Source/Rule56/CellMetrics.cs new file mode 100644 index 0000000..6be64bd --- /dev/null +++ b/Source/Rule56/CellMetrics.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using Verse; +namespace CombatAI +{ + public class CellMetrics + { + private SightTracker.SightReader sightReader; + private AvoidanceTracker.AvoidanceReader avoidanceReader; + private Map map; + + private readonly List> metrics = new List>(); + private readonly List metricsVal = new List(); + private readonly List metricsWeight = new List(); + private readonly List metricsComperative = new List(); +#pragma warning disable CS0649 + private readonly List metricKeys = new List(); +#pragma warning restore CS0649 + + public CellMetrics() + { + } + + public void Begin(Map map, SightTracker.SightReader sightReader, AvoidanceTracker.AvoidanceReader avoidanceReader, IntVec3 root) + { + this.map = map; + this.sightReader = sightReader; + this.avoidanceReader = avoidanceReader; + this.metricsVal.Clear(); + for (int i = 0; i < metrics.Count; i++) + { + if (this.metricsComperative[i]) + { + this.metricsVal.Add(metrics[i](root)); + } + else + { + this.metricsVal.Add(0); + } + } + } + + public void Add(string key, Func func, float weight = 1f, bool comperative = true) + { + this.metricKeys.Add(key); + this.metricsWeight.Add(weight); + this.metrics.Add((cell) => func(this.sightReader, cell)); + this.metricsComperative.Add(comperative); + } + + public void Add(string key, Func func, float weight = 1f, bool comperative = true) + { + this.metricKeys.Add(key); + this.metricsWeight.Add(weight); + this.metrics.Add((cell) => func(this.avoidanceReader, cell)); + this.metricsComperative.Add(comperative); + } + + public void Add(string key, Func func, float weight = 1f, bool comperative = true) + { + this.metricKeys.Add(key); + this.metricsWeight.Add(weight); + this.metrics.Add((cell) => func(this.sightReader, this.avoidanceReader, cell)); + this.metricsComperative.Add(comperative); + } + + public void Add(string key, Func func, float weight = 1f, bool comperative = true) + { + this.metricKeys.Add(key); + this.metricsWeight.Add(weight); + this.metrics.Add((cell) => func(map, cell)); + this.metricsComperative.Add(comperative); + } + + public void Add(string key, Func func, float weight = 1f, bool comperative = true) + { + this.metricKeys.Add(key); + this.metricsWeight.Add(weight); + this.metrics.Add(func); + this.metricsComperative.Add(comperative); + } + + public float Score(IntVec3 cell) + { + float f = 0; + for (int i = 0; i < metrics.Count; i++) + { + f += (metrics[i](cell) - metricsVal[i]) * metricsWeight[i]; + } + return f; + } + + public string MaxAbsKey(IntVec3 cell) + { + float max = float.MinValue; + string key = null; + int index = -1; + for (int i = 0; i < metrics.Count; i++) + { + float val = Mathf.Abs(metrics[i](cell) - metricsVal[i]); + if (max < val) + { + key = metricKeys[i]; + max = val; + index = i; + } + } + if (index != -1) + { + return $"{key}={metrics[index](cell)},{Mathf.Abs(metrics[index](cell) - metricsVal[index])}"; + } + return key; + } + + public void Print(IntVec3 cell) + { + string message = ""; + for (int i = 0; i < metrics.Count; i++) + { + message += $"{metricKeys[i]}=({Mathf.Abs(metrics[i](cell) - metricsVal[i])}, {metrics[i](cell)}, {metricsVal[i]})\n"; + } + Log.Message(message); + } + + public string MinAbsKey(IntVec3 cell) + { + float min = float.MaxValue; + string key = null; + int index = -1; + for (int i = 0; i < metrics.Count; i++) + { + float val = Mathf.Abs(metrics[i](cell) - metricsVal[i]); + if (min > val) + { + key = metricKeys[i]; + min = val; + index = i; + } + } + if (index != -1) + { + return $"{key}={metrics[index](cell)},{Mathf.Abs(metrics[index](cell) - metricsVal[index])}"; + } + return string.Empty; + } + + public void Reset() + { + this.sightReader = null; + this.avoidanceReader = null; + this.map = null; + this.metricsVal.Clear(); + } + } +} diff --git a/Source/Rule56/CombatAI.csproj b/Source/Rule56/CombatAI.csproj index 9845de4..88f0389 100644 --- a/Source/Rule56/CombatAI.csproj +++ b/Source/Rule56/CombatAI.csproj @@ -30,7 +30,7 @@ ..\..\1.4\Assemblies TRACE;DEBUG_REACTION;DEBUG;NETFRAMEWORK;NET472; true - true + false full @@ -105,6 +105,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/Source/Rule56/CombatAIMod.cs b/Source/Rule56/CombatAIMod.cs index c32824a..2805de1 100644 --- a/Source/Rule56/CombatAIMod.cs +++ b/Source/Rule56/CombatAIMod.cs @@ -28,16 +28,17 @@ public class CombatAIMod : Mod private bool collapsibleGroupInited; public CombatAIMod(ModContentPack contentPack) : base(contentPack) { - Finder.Mod = this; - Finder.Harmony = new Harmony("Krkr.Rule56"); - Finder.Harmony.PatchAll(); + Finder.Mod = this; Finder.Settings = GetSettings(); + Finder.Harmony = new Harmony("Krkr.Rule56"); + Finder.Harmony.PatchAll(); if (Finder.Settings == null) { Finder.Settings = new Settings(); } LongEventHandler.QueueLongEvent(ArmorUtility.Initialize, "CombatAI.Preparing", false, null); LongEventHandler.QueueLongEvent(CompatibilityManager.Initialize, "CombatAI.Preparing", false, null); + LongEventHandler.QueueLongEvent(ThinkNodeDatabase.Initialize, "CombatAI.Preparing", false, null); } public override string SettingsCategory() @@ -129,6 +130,7 @@ private void FillCollapsible_Basic(Listing_Collapsible collapsible) collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Basic_KillBoxKiller, ref Finder.Settings.Pather_KillboxKiller); collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Basic_Pather, ref Finder.Settings.Pather_Enabled); collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Basic_Caster, ref Finder.Settings.Caster_Enabled); + collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Basic_Temperature, ref Finder.Settings.Temperature_Enabled); collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Basic_Targeter, ref Finder.Settings.Targeter_Enabled); collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Basic_Reaction, ref Finder.Settings.React_Enabled); collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Basic_Flanking, ref Finder.Settings.Flank_Enabled); @@ -190,7 +192,7 @@ private void FillCollapsible_Performance(Listing_Collapsible collapsible) string color = Finder.Settings.Pathfinding_DestWeight < 0.75f ? "red" : "while"; string extra = Finder.Settings.Pathfinding_DestWeight < 0.75f ? " WILL IMPACT PERFORMANCE" : ""; Text.CurFontStyle.fontStyle = FontStyle.Bold; - Finder.Settings.Pathfinding_DestWeight = HorizontalSlider_NewTemp(rect, Finder.Settings.Pathfinding_DestWeight, 0.95f, Finder.Settings.AdvancedUser ? 0.3f : 0.65f, true, $"{Math.Round(Finder.Settings.Pathfinding_DestWeight * 100f, 1)}%{extra}", 0.05f); + Finder.Settings.Pathfinding_DestWeight = HorizontalSlider_NewTemp(rect, Finder.Settings.Pathfinding_DestWeight, 0.3f, 0.95f, true, $"{Math.Round(Finder.Settings.Pathfinding_DestWeight * 100f, 1)}%{extra}", 0.05f); }, useMargins: true); collapsible.Line(2); @@ -329,12 +331,15 @@ private void FillCollapsible_Debugging(Listing_Collapsible collapsible) { collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Debugging_Enable, ref Finder.Settings.Debug); collapsible.CheckboxLabeled("Disable quick setup menu", ref Finder.Settings.FinishedQuickSetup); + collapsible.CheckboxLabeled("Enable job logging", ref Finder.Settings.Debug_LogJobs); collapsible.CheckboxLabeled("Enable cinematic mode", ref Finder.Settings.Debug_DisablePawnGuiOverlay); collapsible.CheckboxLabeled("Draw sight grid", ref Finder.Settings.Debug_DrawShadowCasts); collapsible.CheckboxLabeled("Draw sight vector field", ref Finder.Settings.Debug_DrawShadowCastsVectors); collapsible.CheckboxLabeled("Draw threat (pawn armor vs enemy)", ref Finder.Settings.Debug_DrawThreatCasts); collapsible.CheckboxLabeled("Draw proximity grid", ref Finder.Settings.Debug_DrawAvoidanceGrid_Proximity); collapsible.CheckboxLabeled("Draw danger grid", ref Finder.Settings.Debug_DrawAvoidanceGrid_Danger); + collapsible.CheckboxLabeled("Draw path cost", ref Finder.Settings.Debug_DebugPathfinding); + collapsible.CheckboxLabeled("Draw availability", ref Finder.Settings.Debug_DebugAvailability); collapsible.CheckboxLabeled("Debug things tracker", ref Finder.Settings.Debug_DebugThingsTracker); collapsible.CheckboxLabeled("Debug validate sight EXTREMELY BAD FOR PERFORMANCE", ref Finder.Settings.Debug_ValidateSight); collapsible.Line(1); @@ -354,6 +359,9 @@ public override void DoSettingsWindowContents(Rect inRect) collapsible_performance.Group = collapsible_groupRight; collapsible_groupRight.Register(collapsible_performance); + collapsible_advance.Group = collapsible_groupRight; + collapsible_groupRight.Register(collapsible_advance); + collapsible_fog.Group = collapsible_groupLeft; collapsible_groupLeft.Register(collapsible_fog); @@ -386,7 +394,6 @@ public override void DoSettingsWindowContents(Rect inRect) // Right section Rect rectRight = inRect.RightHalf(); rectRight.xMin += 5; - collapsible_advance.Expanded = true; collapsible_advance.Begin(rectRight, Keyed.CombatAI_Settings_Advance); FillCollapsible_Advance(collapsible_advance); collapsible_advance.End(ref rectRight); @@ -395,15 +402,15 @@ public override void DoSettingsWindowContents(Rect inRect) // debug settings if (Finder.Settings.AdvancedUser) { - collapsible_performance.Begin(rectRight, Keyed.CombatAI_Settings_Advance_Sight_Performance); - FillCollapsible_Performance(collapsible_performance); - collapsible_performance.End(ref rectRight); - rectRight.yMin += 5; - collapsible_debug.Begin(rectRight, Keyed.CombatAI_Settings_Debugging); FillCollapsible_Debugging(collapsible_debug); collapsible_debug.End(ref rectRight); rectRight.yMin += 5; + + collapsible_performance.Begin(rectRight, Keyed.CombatAI_Settings_Advance_Sight_Performance); + FillCollapsible_Performance(collapsible_performance); + collapsible_performance.End(ref rectRight); + rectRight.yMin += 5; } WriteSettings(); } diff --git a/Source/Rule56/CombatAI_Utility.cs b/Source/Rule56/CombatAI_Utility.cs index 3de64ed..71d0657 100644 --- a/Source/Rule56/CombatAI_Utility.cs +++ b/Source/Rule56/CombatAI_Utility.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Runtime.CompilerServices; using RimWorld; using UnityEngine; using Verse; @@ -14,6 +15,26 @@ public static bool Is(this T def, T other) where T : Def { return def != null && other != null && def == other; } + + public static bool Is(this Job job, T other) where T : Def + { + return job != null && other != null && job.def == other; + } + + public static bool Is(this PawnDuty duty, T other) where T : Def + { + return duty != null && other != null && duty.def == other; + } + + public static bool Is(this Thing thing, T other) where T : Def + { + return thing != null && other != null && thing.def == other; + } + + public static bool Is(this Thing thing, Thing other) where T : Def + { + return thing != null && other != null && thing == other; + } public static bool IsDormant(this Thing thing) { @@ -62,11 +83,51 @@ public static IntVec3 TryGetNextDutyDest(this Pawn pawn, float maxDistFromPawn = return dutyDest; } + public static bool IsApproachingMeleeTarget(this Pawn pawn, float distLimit = 5, bool allowCached = true) + { + if (!allowCached || !TKVCache.TryGet(pawn.thingIDNumber, out bool result, expiry: 5)) + { + result = IsApproachingMeleeTarget(pawn, out _, distLimit); + if (allowCached) + { + TKVCache.Put(pawn.thingIDNumber, result); + } + } + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsApproachingMeleeTarget(this Pawn pawn, out Thing target, float distLimit = 5) + { + target = null; + Job attackJob; + return (attackJob = pawn.CurJob).Is(JobDefOf.AttackMelee) && attackJob.targetA.IsValid && attackJob.targetA.Cell.DistanceToSquared(pawn.Position) <= distLimit * distLimit && (target = attackJob.targetA.Thing) != null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Verb TryGetAttackVerb(this Thing thing) { if (thing is Pawn pawn) { - return pawn.CurrentEffectiveVerb; + Pawn_EquipmentTracker equipment = pawn.equipment; + if (equipment is { Primary: { } } && equipment.PrimaryEq.PrimaryVerb.Available() && (!equipment.PrimaryEq.PrimaryVerb.verbProps.onlyManualCast || (pawn.CurJob != null && pawn.CurJob.def != JobDefOf.Wait_Combat))) + { + return equipment.PrimaryEq.PrimaryVerb; + } + Pawn_MeleeVerbs meleeVerbs = pawn.meleeVerbs; + if (meleeVerbs != null) + { + if (meleeVerbs.curMeleeVerb != null) + { + return meleeVerbs.curMeleeVerb; + } + if (!TKVCache.TryGet(thing.thingIDNumber, out Verb verb, 480) || !verb.IsStillUsableBy(pawn)) + { + TKVCache.Put(thing.thingIDNumber, verb = meleeVerbs.TryGetMeleeVerb(null)); + return verb; + } + } + return null; } if (thing is Building_Turret turret) { @@ -118,5 +179,9 @@ public static int GetThingFlagsIndex(this Thing thing) { return thing.thingIDNumber % 64; } + + private class IsApproachingMeleeTargetCache + { + } } } diff --git a/Source/Rule56/Comps/ThingComp_CombatAI.cs b/Source/Rule56/Comps/ThingComp_CombatAI.cs index 757d73a..654e6a7 100644 --- a/Source/Rule56/Comps/ThingComp_CombatAI.cs +++ b/Source/Rule56/Comps/ThingComp_CombatAI.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using CombatAI.Abilities; using CombatAI.R; using CombatAI.Utilities; -using HarmonyLib; using RimWorld; using UnityEngine; using Verse; @@ -14,1195 +12,1626 @@ using GUIUtility = CombatAI.Gui.GUIUtility; namespace CombatAI.Comps { - public class ThingComp_CombatAI : ThingComp - { - public AIAgentData data; - /// - /// Number of enemies in range. - /// Updated by the sightgrid. - /// - public int enemiesInRangeNum; - /// - /// Set of visible enemies. A queue for visible enemies during scans. - /// - private readonly HashSet allEnemies; - private readonly HashSet allAllies; - - - private int _sap; + public class ThingComp_CombatAI : ThingComp + { + private readonly Dictionary allAllies; + private readonly Dictionary allEnemies; + /// + /// Escorting pawns. + /// + private readonly List escorts = new List(); + /// + /// Set of visible enemies. A queue for visible enemies during scans. + /// + private readonly List rangedEnemiesTargetingSelf = new List(4); + /// + /// Sapper path nodes. + /// + /// + private readonly List sapperNodes = new List(); + /// + /// Aggro countdown ticks. + /// + private int aggroTicks; + /// + /// Aggro target. + /// + private LocalTargetInfo aggroTarget; + + private Thing _bestEnemy; + private int _last; - /// - /// Parent armor report. - /// - private ArmorReport armor; - /// - /// Cell to stand on while sapping - /// - private IntVec3 cellBefore = IntVec3.Invalid; - /// - /// Custom pawn duty tracker. Allows the execution of new duties then going back to the old one once the new one is - /// finished. - /// - public Pawn_CustomDutyTracker duties; - /// - /// Pawn ability caster. - /// - public Pawn_AbilityCaster abilities; - /// - /// Escorting pawns. - /// - private readonly List escorts = new List(); - /// - /// Whether to find escorts. - /// - private bool findEscorts; - /// - /// Sapper path nodes. - /// - private readonly List sapperNodes = new List(); - /// - /// Sapper timestamp - /// - private int sapperStartTick; - //Whether a scan is occuring. - private bool scanning; - /// - /// Parent sight reader. - /// - public SightTracker.SightReader sightReader; - /// - /// Wait job started/queued by this comp. - /// - public Job waitJob; - /// - /// Parent pawn. - /// - public Pawn selPawn; - /// - /// Target forced by the player. - /// - public LocalTargetInfo forcedTarget = LocalTargetInfo.Invalid; + private int _sap; + /// + /// Pawn ability caster. + /// + public Pawn_AbilityCaster abilities; + /// + /// Saves job logs. for debugging only. + /// + public List jobLogs; + /// + /// Parent armor report. + /// + private ArmorReport armor; + /// + /// Cell to stand on while sapping + /// + private IntVec3 cellBefore = IntVec3.Invalid; + public AIAgentData data; + /// + /// Custom pawn duty tracker. Allows the execution of new duties then going back to the old one once the new one is + /// finished. + /// + public Pawn_CustomDutyTracker duties; + /// + /// Number of enemies in range. + /// Updated by the sightgrid. + /// + public int enemiesInRangeNum; + /// + /// Whether to find escorts. + /// + private bool findEscorts; + /// + /// Target forced by the player. + /// + public LocalTargetInfo forcedTarget = LocalTargetInfo.Invalid; + /// + /// Sapper timestamp + /// + private int sapperStartTick; + //Whether a scan is occuring. + private bool scanning; + /// + /// Parent pawn. + /// + public Pawn selPawn; + /// + /// Parent sight reader. + /// + public SightTracker.SightReader sightReader; - public ThingComp_CombatAI() - { - allEnemies = new HashSet(32); - allAllies = new HashSet(32); - data = new AIAgentData(); - } + public ThingComp_CombatAI() + { + allEnemies = new Dictionary(32); + allAllies = new Dictionary(32); + data = new AIAgentData(); + } - /// - /// Whether the pawn is downed or dead. - /// - public bool IsDeadOrDowned - { - get => selPawn.Dead || selPawn.Downed; - } - - /// - /// Whether the pawning is sapping. - /// - public bool IsSapping - { - get => cellBefore.IsValid && sapperNodes.Count > 0 && GenTicks.TicksGame - sapperStartTick < 4800 && parent.Position.DistanceToSquared(cellBefore) < 1600; - } - /// - /// Whether the pawn is available to escort other pawns or available for sapping. - /// - public bool CanSappOrEscort - { - get => GenTicks.TicksGame - releasedTick > 1200 && !IsSapping; - } + /// + /// Whether the pawn is downed or dead. + /// + public bool IsDeadOrDowned + { + get => selPawn.Dead || selPawn.Downed; + } - public override void Initialize(CompProperties props) - { - base.Initialize(props); - selPawn = parent as Pawn; - if (selPawn == null) - { - throw new Exception($"ThingComp_CombatAI initialized for a non pawn {parent}/def:{parent.def}"); - } - } + /// + /// Whether the pawning is sapping. + /// + public bool IsSapping + { + get => cellBefore.IsValid && sapperNodes.Count > 0; + } + /// + /// Whether the pawn is available to escort other pawns or available for sapping. + /// + public bool CanSappOrEscort + { + get => !IsSapping && GenTicks.TicksGame - releasedTick > 900; + } - public override void PostSpawnSetup(bool respawningAfterLoad) - { - base.PostSpawnSetup(respawningAfterLoad); - armor = selPawn.GetArmorReport(); - duties ??= new Pawn_CustomDutyTracker(selPawn); - duties.pawn = selPawn; - abilities ??= new Pawn_AbilityCaster(selPawn); - abilities.pawn = selPawn; - } + public override void Initialize(CompProperties props) + { + base.Initialize(props); + selPawn = parent as Pawn; + if (selPawn == null) + { + throw new Exception($"ThingComp_CombatAI initialized for a non pawn {parent}/def:{parent.def}"); + } + } - public override void CompTickRare() - { - base.CompTickRare(); - if (!selPawn.Spawned) - { - return; - } - if (duties != null) - { - duties.TickRare(); - } - if (abilities != null) - { -// abilities.TickRare(visibleEnemies); - } - if (IsSapping && !IsDeadOrDowned) - { - if (sapperNodes[0].GetEdifice(parent.Map) == null) - { - cellBefore = sapperNodes[0]; - sapperNodes.RemoveAt(0); - if (sapperNodes.Count > 0) - { - _sap++; - TryStartSapperJob(); - } - else - { - ReleaseEscorts(); - sapperNodes.Clear(); - cellBefore = IntVec3.Invalid; - sapperStartTick = -1; - releasedTick = GenTicks.TicksGame; - } - } - else - { - TryStartSapperJob(); - } - } - if (forcedTarget.IsValid && !IsDeadOrDowned) - { - if (Mod_CE.active && (selPawn.CurJobDef.Is(Mod_CE.ReloadWeapon) || selPawn.CurJobDef.Is(Mod_CE.HunkerDown))) - { - return; - } - // remove the forced target on when not drafted and near the target - if (!selPawn.Drafted || selPawn.Position.DistanceToSquared(forcedTarget.Cell) < 25) - { - forcedTarget = LocalTargetInfo.Invalid;; - } - else if (enemiesInRangeNum == 0 && (selPawn.jobs.curJob?.def.Is(JobDefOf.Goto) == false || selPawn.pather?.Destination != forcedTarget.Cell)) - { - Job gotoJob = JobMaker.MakeJob(JobDefOf.Goto, forcedTarget); - gotoJob.canUseRangedWeapon = true; - gotoJob.locomotionUrgency = LocomotionUrgency.Jog; - gotoJob.playerForced = true; - selPawn.jobs.ClearQueuedJobs(); - selPawn.jobs.StartJob(gotoJob); - } - } - } + public override void PostSpawnSetup(bool respawningAfterLoad) + { + base.PostSpawnSetup(respawningAfterLoad); + armor = selPawn.GetArmorReport(); + duties ??= new Pawn_CustomDutyTracker(selPawn); + duties.pawn = selPawn; + abilities ??= new Pawn_AbilityCaster(selPawn); + abilities.pawn = selPawn; + } - public override void CompTickLong() - { - base.CompTickLong(); - this.armor = this.selPawn.GetArmorReport(); - } + public override void CompTickRare() + { + base.CompTickRare(); + if (!selPawn.Spawned) + { + return; + } + if (IsDeadOrDowned) + { + if (escorts.Count > 0) + { + ReleaseEscorts(false); + sapperNodes.Clear(); + cellBefore = IntVec3.Invalid; + sapperStartTick = -1; + } + return; + } + if (aggroTicks > 0) + { + aggroTicks -= GenTicks.TickRareInterval; + if (aggroTicks <= 0) + { + if (aggroTarget.IsValid) + { + TryAggro(aggroTarget, 0.8f, Rand.Int); + } + aggroTarget = LocalTargetInfo.Invalid; + } + + } + if (duties != null) + { + duties.TickRare(); + } + if (abilities != null) + { + // abilities.TickRare(visibleEnemies); + } + if (selPawn.IsApproachingMeleeTarget(out Thing target)) + { + ThingComp_CombatAI comp = target.GetComp_Fast(); + if (comp != null) + { + comp.Notify_BeingTargeted(selPawn, selPawn.CurrentEffectiveVerb); + } + } + if (IsSapping && !IsDeadOrDowned) + { + // end if this pawn is in + if (sapperNodes[0].GetEdifice(parent.Map) == null) + { + cellBefore = sapperNodes[0]; + sapperNodes.RemoveAt(0); + if (sapperNodes.Count > 0) + { + _sap++; + TryStartSapperJob(); + } + else + { + ReleaseEscorts(success: true); + sapperNodes.Clear(); + cellBefore = IntVec3.Invalid; + sapperStartTick = -1; + } + } + else + { + TryStartSapperJob(); + } + } + else if (forcedTarget.IsValid && !IsDeadOrDowned) + { + if (Mod_CE.active && (selPawn.CurJobDef.Is(Mod_CE.ReloadWeapon) || selPawn.CurJobDef.Is(Mod_CE.HunkerDown))) + { + return; + } + // remove the forced target on when not drafted and near the target + if (!selPawn.Drafted || selPawn.Position.DistanceToSquared(forcedTarget.Cell) < 25) + { + forcedTarget = LocalTargetInfo.Invalid; + ; + } + else if (enemiesInRangeNum == 0 && (selPawn.jobs.curJob?.def.Is(JobDefOf.Goto) == false || selPawn.pather?.Destination != forcedTarget.Cell)) + { + Job gotoJob = JobMaker.MakeJob(JobDefOf.Goto, forcedTarget); + gotoJob.canUseRangedWeapon = true; + gotoJob.locomotionUrgency = LocomotionUrgency.Jog; + gotoJob.playerForced = true; + selPawn.jobs.ClearQueuedJobs(); + selPawn.jobs.StartJob(gotoJob); + } + } + } - /// - /// Returns whether the parent has retreated in the last number of ticks. - /// - /// The number of ticks - /// Whether the pawn retreated in the last number of ticks - public bool RetreatedRecently(int ticks) - { - return GenTicks.TicksGame - lastRetreated <= ticks; - } - /// - /// Returns whether the parent has took damage in the last number of ticks. - /// - /// The number of ticks - /// Whether the pawn took damage in the last number of ticks - public bool TookDamageRecently(int ticks) - { - return GenTicks.TicksGame - lastTookDamage <= ticks; - } - /// - /// Returns whether the parent has reacted in the last number of ticks. - /// - /// The number of ticks - /// Whether the reacted in the last number of ticks - public bool ReactedRecently(int ticks) - { - return GenTicks.TicksGame - lastInterupted <= ticks; - } + public override void CompTickLong() + { + base.CompTickLong(); + // update the current armor report. + armor = selPawn.GetArmorReport(); + } + + /// + /// Returns whether the parent has took damage in the last number of ticks. + /// + /// The number of ticks + /// Whether the pawn took damage in the last number of ticks + public bool TookDamageRecently(int ticks) + { + return data.TookDamageRecently(ticks); + } + /// + /// Returns whether the parent has reacted in the last number of ticks. + /// + /// The number of ticks + /// Whether the reacted in the last number of ticks + public bool ReactedRecently(int ticks) + { + return data.InterruptedRecently(ticks); + } - /// - /// Called when a scan for enemies starts. Will clear the visible enemy queue. If not called, calling OnScanFinished or - /// Notify_VisibleEnemy(s) will result in an error. - /// Should only be called from the main thread. - /// - public void OnScanStarted() - { - if (allEnemies.Count != 0) - { - if (scanning) - { - Log.Warning($"ISMA: OnScanStarted called while scanning. ({allEnemies.Count}, {Thread.CurrentThread.ManagedThreadId})"); - return; - } - allEnemies.Clear(); - } - if (allAllies.Count != 0) - { - allAllies.Clear(); - } - scanning = true; - lastScanned = GenTicks.TicksGame; - } + /// + /// Called when a scan for enemies starts. Will clear the visible enemy queue. If not called, calling OnScanFinished or + /// Notify_VisibleEnemy(s) will result in an error. + /// Should only be called from the main thread. + /// + public void OnScanStarted() + { + if (allEnemies.Count != 0) + { + if (scanning) + { + Log.Warning($"ISMA: OnScanStarted called while scanning. ({allEnemies.Count}, {Thread.CurrentThread.ManagedThreadId})"); + return; + } + allEnemies.Clear(); + } + if (allAllies.Count != 0) + { + allAllies.Clear(); + } + scanning = true; + data.LastScanned = lastScanned = GenTicks.TicksGame; + } - /// - /// Called a scan is finished. This will process enemies queued in visibleEnemies. Responsible for parent reacting. - /// If OnScanStarted is not called before then this will result in an error. - /// Should only be called from the main thread. - /// - public void OnScanFinished() - { - if (scanning == false) - { - Log.Warning($"ISMA: OnScanFinished called while not scanning. ({allEnemies.Count}, {Thread.CurrentThread.ManagedThreadId})"); - return; - } - scanning = false; - // set the agent data. - data.ReSetEnemies(allEnemies); - data.ReSetAllies(allAllies); - // skip for player pawns with no forced target. - if (selPawn.Faction.IsPlayerSafe() && !forcedTarget.IsValid) - { - return; - } + /// + /// Called a scan is finished. This will process enemies queued in visibleEnemies. Responsible for parent reacting. + /// If OnScanStarted is not called before then this will result in an error. + /// Should only be called from the main thread. + /// + public void OnScanFinished() + { + if (scanning == false) + { + Log.Warning($"ISMA: OnScanFinished called while not scanning. ({allEnemies.Count}, {Thread.CurrentThread.ManagedThreadId})"); + return; + } + scanning = false; + // set enemies. + data.ReSetEnemies(allEnemies); + // set allies. + data.ReSetAllies(allAllies); + // update when this pawn last saw enemies + data.LastSawEnemies = data.NumEnemies > 0 ? GenTicks.TicksGame : -1; + // skip for animals. + if (selPawn.mindState == null || selPawn.RaceProps.Animal || IsDeadOrDowned) + { + return; + } + // skip for player pawns with no forced target. + if (selPawn.Faction.IsPlayerSafe() && !forcedTarget.IsValid) + { + return; + } #if DEBUG_REACTION - if (Finder.Settings.Debug && Finder.Settings.Debug_ValidateSight) - { - _visibleEnemies.Clear(); - _visibleEnemies.AddRange(allEnemies.Where(t => t.thing is Pawn).Select(t => t.thing as Pawn)); - if (_path.Count == 0 || _path.Last() != parent.Position) - { - _path.Add(parent.Position); - if (GenTicks.TicksGame - lastInterupted < 150) - { - _colors.Add(Color.red); - } - else if (GenTicks.TicksGame - lastInterupted < 240) - { - _colors.Add(Color.yellow); - } - else - { - _colors.Add(Color.black); - } - if (_path.Count >= 30) - { - _path.RemoveAt(0); - _colors.RemoveAt(0); - } - } - } + if (Finder.Settings.Debug && Finder.Settings.Debug_ValidateSight) + { + _visibleEnemies.Clear(); + IEnumerator enumerator = data.Enemies(); + while (enumerator.MoveNext()) + { + AIEnvAgentInfo info = enumerator.Current; + if (info.thing == null) + { + Log.Warning("Found null thing (1)"); + continue; + } + if (info.thing.Spawned && info.thing is Pawn pawn) + { + _visibleEnemies.Add(pawn); + } + } + if (_path.Count == 0 || _path.Last() != parent.Position) + { + _path.Add(parent.Position); + if (GenTicks.TicksGame - lastInterupted < 150) + { + _colors.Add(Color.red); + } + else if (GenTicks.TicksGame - lastInterupted < 240) + { + _colors.Add(Color.yellow); + } + else + { + _colors.Add(Color.black); + } + if (_path.Count >= 30) + { + _path.RemoveAt(0); + _colors.RemoveAt(0); + } + } + } #endif - // if no enemies are visible skip. - if (allEnemies.Count == 0) - { - return; - } - // check if the TPS is good enough. - if (!Finder.Performance.TpsCriticallyLow) - { - // if the pawn haven't seen enemies in a while and recently reacted then reset lastInterupted. - // This is done to ensure fast reaction times when exiting then entering combat. - if (GenTicks.TicksGame - lastInterupted < 100 && GenTicks.TicksGame - lastSawEnemies > 90) - { - lastInterupted = -1; - if (Finder.Settings.Debug && Finder.Settings.Debug_ValidateSight) - { - parent.Map.debugDrawer.FlashCell(parent.Position, 1.0f, "X", 60); - } - } - lastSawEnemies = GenTicks.TicksGame; - } - if (!(selPawn.RaceProps?.Animal ?? true)) - { - float bodySize = selPawn.RaceProps.baseBodySize; - // pawn reaction cooldown changes with their bodysize. - if (GenTicks.TicksGame - lastInterupted < 60 * bodySize || GenTicks.TicksGame - lastRetreated < 65 * bodySize) - { - return; - } - // if the pawn is kidnaping a pawn skip. - if (selPawn.CurJobDef.Is(JobDefOf.Kidnap)) - { - return; - } - // Skip if some vanilla duties are active. - PawnDuty duty = selPawn.mindState.duty; - if (duty != null && (duty.def.Is(DutyDefOf.Build) || duty.def.Is(DutyDefOf.SleepForever) || duty.def.Is(DutyDefOf.TravelOrLeave))) - { - lastInterupted = GenTicks.TicksGame + Rand.Int % 240; - return; - } - // pawns above a certain bodysize who are worming up should be skiped. - // This is mainly for large mech pawns. - Stance_Warmup warmup = (selPawn.stances?.curStance ?? null) as Stance_Warmup; - if (warmup != null && (bodySize > 2.5f || warmup.ticksLeft < 60 && Rand.Chance(1.0f - Maths.Sqr(warmup.ticksLeft / 60f) ))) - { - return; - } - Verb verb = parent.TryGetAttackVerb(); - if (verb == null || verb.Bursting) - { - return; - } - if (selPawn.CurJobDef == JobDefOf.Mine) - { - selPawn.jobs.StopAll(); - } - if (verb.IsMeleeAttack) - { - // TODO create melee reactions. -// foreach (Thing enemy in visibleEnemies) -// { -// if (enemy is { Spawned: true, Destroyed: false }) -// { -// -// } -// } - } - else - { - // if CE is active skip reaction if the pawn is reloading or hunkering down. - if (Mod_CE.active && (selPawn.CurJobDef.Is(Mod_CE.ReloadWeapon) || selPawn.CurJobDef.Is(Mod_CE.HunkerDown))) - { - return; - } - // check if the verb is available. - if (!verb.Available() || Mod_CE.active && Mod_CE.IsAimingCE(verb)) - { - return; - } - // A not fast check will check for retreat and for reactions to enemies that are visible or soon to be visible. - // A fast check will check only for retreat. - bool fastCheck = warmup != null && (warmup.ticksLeft + GenTicks.TicksGame - warmup.startedTick > 120 || warmup.ticksLeft < 30); - Thing bestEnemy = selPawn.mindState.enemyTarget; - IntVec3 bestEnemyPositon = IntVec3.Invalid; - IntVec3 pawnPosition = selPawn.Position; - float bestEnemyScore = verb.currentTarget.IsValid && verb.currentTarget.Cell.IsValid ? verb.currentTarget.Cell.DistanceToSquared(pawnPosition) : 1e6f; - bool bestEnemyVisibleNow = false; - bool retreat = false; - bool canRetreat = Finder.Settings.Retreat_Enabled && selPawn.RaceProps.baseHealthScale <= 2.0f && selPawn.RaceProps.baseBodySize <= 2.2f; - float retreatDistSqr = Maths.Sqr(Maths.Min(verb.EffectiveRange / 4 * verb.verbProps.warmupTime, 9)); - foreach (AIEnvAgentInfo enemyInfo in allEnemies) - { - Thing enemy = enemyInfo.thing; - if (enemy is { Spawned: true, Destroyed: false }) - { - IntVec3 enemyPosition = enemy.Position; - Pawn enemyPawn = enemy as Pawn; - if (enemyPawn != null) - { - // skip for children - DevelopmentalStage stage = enemyPawn.DevelopmentalStage; - if (stage <= DevelopmentalStage.Child && stage != DevelopmentalStage.None) - { - continue; - } - } - float distSqr = pawnPosition.DistanceToSquared(enemyPosition); - if (enemyPawn != null) - { - float distSqrShifted = PawnPathUtility.GetMovingShiftedPosition(enemyPawn, 120).DistanceToSquared(pawnPosition); - if (canRetreat && distSqrShifted < retreatDistSqr) - { - bestEnemy = enemy; - bestEnemyScore = distSqr; - bestEnemyPositon = enemyPosition; - retreat = true; - } - } - if (!fastCheck && !retreat) - { - if (verb.CanHitTarget(enemyPosition)) - { - if (!bestEnemyVisibleNow) - { - bestEnemyVisibleNow = true; - bestEnemy = enemy; - bestEnemyScore = distSqr; - bestEnemyPositon = enemyPosition; - } - else - { - if (bestEnemyScore > distSqr) - { - bestEnemy = enemy; - bestEnemyScore = distSqr; - bestEnemyPositon = enemyPosition; - } - } - } - else if (!bestEnemyVisibleNow) - { - if (selPawn.CanReach(enemy, PathEndMode.InteractionCell, Danger.Unspecified, true, true)) - { - distSqr = pawnPosition.DistanceToSquared(enemyPosition); - if (bestEnemyScore > distSqr) - { - bestEnemy = enemy; - bestEnemyScore = distSqr; - bestEnemyPositon = enemyPosition; - } - } - } - } - } - } - if (bestEnemy == null) - { - return; - } - if (Prefs.DevMode && DebugSettings.godMode) - { - _bestEnemy = bestEnemy; - } - if (retreat) - { - float retreatDist = Maths.Sqrt_Fast(retreatDistSqr, 4); -// bool Validator_Retreat(IntVec3 cell) -// { -// float mul = 1f; -// float vis = sightReader.GetVisibilityToEnemies(pawnPosition); -// if (retreatDistSqr > bestEnemyPositon.DistanceToSquared(cell) && warmup != null ) -// { -// mul = 0.25f; -// } -// return Rand.Chance(mul * (vis - sightReader.GetVisibilityToEnemies(cell))) || -// Rand.Chance(mul * (sightReader.GetThreat(pawnPosition) - sightReader.GetThreat(cell))); -// } - if (TryRetreat(new LocalTargetInfo(bestEnemy), retreatDist, verb, null, warmup == null, true)) - { - lastRetreated = GenTicks.TicksGame - Rand.Int % Maths.Max(Mathf.FloorToInt(50 * (2 - verb.verbProps.warmupTime)), 1); - } - } - else if (!fastCheck) - { - if (TryAttack(new LocalTargetInfo(bestEnemy), verb, bestEnemyVisibleNow, out IntVec3 destCell, null, warmup == null, true)) - { - lastInterupted = lastMoved = GenTicks.TicksGame; - prevEnemyDir = sightReader.GetEnemyDirection(destCell).normalized; - } - else - { - lastInterupted = GenTicks.TicksGame - Rand.Int % 30; - } - } - } - } - } + List targetedBy = data.BeingTargetedBy; + // update when last saw enemies + data.LastSawEnemies = data.NumEnemies > 0 ? GenTicks.TicksGame : data.LastSawEnemies; + // if no enemies are visible nor anyone targeting self skip. + if (data.NumEnemies == 0 && targetedBy.Count == 0) + { + return; + } + // check if the TPS is good enough. + // reduce cooldown if the pawn hasn't seen enemies for a few ticks + if (!Finder.Performance.TpsCriticallyLow) + { + // if the pawn haven't seen enemies in a while and recently reacted then reset lastInterupted. + // This is done to ensure fast reaction times when exiting then entering combat. + if (GenTicks.TicksGame - lastSawEnemies > 90) + { + lastInterupted = -1; + if (Finder.Settings.Debug && Finder.Settings.Debug_ValidateSight) + { + parent.Map.debugDrawer.FlashCell(parent.Position, 1.0f, "X", 60); + } + } + lastSawEnemies = GenTicks.TicksGame; + } + // get body size and use it in cooldown math. + float bodySize = selPawn.RaceProps.baseBodySize; + // pawn reaction cooldown changes with their body size. + if (data.InterruptedRecently((int)(30 * bodySize)) || data.RetreatedRecently((int)(60 * bodySize))) + { + return; + } + // if the pawn is kidnapping a pawn skip. + if (selPawn.CurJobDef.Is(JobDefOf.Kidnap) || selPawn.CurJobDef.Is(JobDefOf.Flee)) + { + return; + } + // if the pawn is sapping, stop sapping. + if (selPawn.CurJobDef.Is(JobDefOf.Mine) && sightReader.GetVisibilityToEnemies(selPawn.Position) > 0) + { + selPawn.jobs.StopAll(); + } + // Skip if some vanilla duties are active. + PawnDuty duty = selPawn.mindState.duty; + if (duty.Is(DutyDefOf.Build) || duty.Is(DutyDefOf.SleepForever) || duty.Is(DutyDefOf.TravelOrLeave)) + { + data.LastInterrupted = GenTicks.TicksGame + Rand.Int % 240; + return; + } + IntVec3 selPos = selPawn.Position; + Pawn nearestMeleeEnemy = null; + float nearestMeleeEnemyDist = 1e5f; + Thing nearestEnemy = null; + float nearestEnemyDist = 1e5f; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryAttack(LocalTargetInfo enemy, Verb verb, bool enemyVisibleNow, Func validator = null, bool canAttackNow = false, bool updateDebugData = false) - { - return TryAttack(enemy, verb, enemyVisibleNow, out IntVec3 _, validator, canAttackNow, updateDebugData); - } + // used to update nearest enemy THing + void UpdateNearestEnemy(Thing enemy) + { + float dist = selPawn.DistanceTo_Fast(enemy); + if (dist < nearestEnemyDist) + { + nearestEnemyDist = dist; + nearestEnemy = enemy; + } + } - private bool TryAttack(LocalTargetInfo enemy, Verb verb, bool enemyVisibleNow, out IntVec3 destCell, Func validator = null, bool canAttackNow = false, bool updateDebugData = false) - { - bool changedPos = false; - destCell = selPawn.Position; - if (enemy.Thing != null && canAttackNow && enemyVisibleNow) - { - if (updateDebugData) - { - _last = 4; - } - waitJob = null; - selPawn.mindState.enemyTarget = enemy.Thing; - Job job_waitCombat = JobMaker.MakeJob(JobDefOf.Wait_Combat, Rand.Int % 100 + 100); - job_waitCombat.playerForced = forcedTarget.IsValid; - selPawn.jobs.ClearQueuedJobs(); - selPawn.jobs.StartJob(job_waitCombat, JobCondition.InterruptForced); - } - else - { - float moveSpeed = selPawn.GetStatValue_Fast(StatDefOf.MoveSpeed, 450); - if (selPawn.stances?.stagger?.Staggered ?? false) - { - moveSpeed = selPawn.stances.stagger.StaggerMoveSpeedFactor; - } - if (enemyVisibleNow && enemy.Thing != null) - { - if (updateDebugData) - { - _last = 2; - } - waitJob = null; - CastPositionRequest request = new CastPositionRequest(); - request.caster = selPawn; - request.target = enemy.Thing; - request.verb = verb; - request.maxRangeFromTarget = 9999; - request.maxRangeFromCaster = Mathf.Clamp(moveSpeed * 3 / (selPawn.BodySize + 0.01f), 4, 15); - request.wantCoverFromTarget = true; - if (CastPositionFinder.TryFindCastPosition(request, out IntVec3 cell)) - { - if ( cell != selPawn.Position && (validator == null || validator(cell))) - { - if (updateDebugData) - { - _last = 21; - } - Job job_goto = JobMaker.MakeJob(JobDefOf.Goto, cell); - job_goto.playerForced = forcedTarget.IsValid; - job_goto.locomotionUrgency = Finder.Settings.Enable_Sprinting ? LocomotionUrgency.Sprint : LocomotionUrgency.Jog; - Job job_waitCombat = JobMaker.MakeJob(JobDefOf.Wait_Combat, Rand.Int % 100 + 100); - job_waitCombat.playerForced = forcedTarget.IsValid; - job_waitCombat.checkOverrideOnExpire = true; - selPawn.jobs.ClearQueuedJobs(); - selPawn.jobs.StartJob(job_goto, JobCondition.InterruptForced); - selPawn.jobs.jobQueue.EnqueueFirst(waitJob = job_waitCombat); - selPawn.mindState.enemyTarget = enemy.Thing; - changedPos = true; - destCell = cell; - } - else if (canAttackNow) - { - if (updateDebugData) - { - _last = 22; - } - selPawn.mindState.enemyTarget = enemy.Thing; - Job job_waitCombat = JobMaker.MakeJob(JobDefOf.Wait_Combat, Rand.Int % 100 + 100); - job_waitCombat.playerForced = forcedTarget.IsValid; - job_waitCombat.checkOverrideOnExpire = true; - selPawn.jobs.ClearQueuedJobs(); - selPawn.jobs.StartJob(waitJob = job_waitCombat, JobCondition.InterruptForced); - changedPos = true; - } - } - } - else - { - if (updateDebugData) - { - _last = 3; - } - CoverPositionRequest request = new CoverPositionRequest(); - request.caster = selPawn; - request.target = new LocalTargetInfo(enemy.Cell); - request.verb = verb; - request.maxRangeFromCaster = Mathf.Clamp(moveSpeed * 4 / (selPawn.BodySize + 0.01f), 4, 15); - request.checkBlockChance = true; - if (CoverPositionFinder.TryFindCoverPosition(request, out IntVec3 cell) && cell != selPawn.Position) - { - if (validator == null || validator(cell)) - { - if (updateDebugData) - { - _last = 31; - } - if (enemy.Thing != null) - { - selPawn.mindState.enemyTarget = enemy.Thing; - } - Job job_goto = JobMaker.MakeJob(JobDefOf.Goto, cell); - job_goto.playerForced = forcedTarget.IsValid; - job_goto.locomotionUrgency = Finder.Settings.Enable_Sprinting ? LocomotionUrgency.Sprint : LocomotionUrgency.Jog; - Job job_waitCombat = JobMaker.MakeJob(JobDefOf.Wait_Combat, Rand.Int % 100 + 100); - job_waitCombat.playerForced = forcedTarget.IsValid; - job_waitCombat.checkOverrideOnExpire = true; - selPawn.jobs.ClearQueuedJobs(); - selPawn.jobs.StartJob(job_goto, JobCondition.InterruptForced); - selPawn.jobs.jobQueue.EnqueueFirst(waitJob = job_waitCombat); - changedPos = true; - destCell = cell; - } - } - } - } - return changedPos; - } + // used to update nearest melee pawn + void UpdateNearestEnemyMelee(Thing enemy) + { + if (enemy is Pawn enemyPawn) + { + float dist = selPos.DistanceTo_Fast(PawnPathUtility.GetMovingShiftedPosition(enemyPawn, 120f)); + if (dist < nearestMeleeEnemyDist) + { + nearestMeleeEnemyDist = dist; + nearestMeleeEnemy = enemyPawn; + } + } + } - private bool TryRetreat(LocalTargetInfo enemy, float retreatDist, Verb verb, Func validator = null, bool attackOnFallback = false, bool updateDebugData = false) - { - if (updateDebugData) - { - _last = 1; - } - waitJob = null; - if(enemy.Thing != null) - { - selPawn.mindState.enemyTarget = enemy.Thing; - } - CoverPositionRequest request = new CoverPositionRequest(); - request.caster = selPawn; - request.target = enemy; - request.verb = verb; - request.maxRangeFromCaster = Maths.Min(Mathf.Max(retreatDist * 2 / (selPawn.BodySize + 0.01f), 5), 15); - request.checkBlockChance = true; - if (CoverPositionFinder.TryFindRetreatPosition(request, out IntVec3 cell) && cell != selPawn.Position) - { - if (updateDebugData) - { - _last = 11; - } - if (validator == null || validator(cell)) - { - Job job_goto = JobMaker.MakeJob(JobDefOf.Goto, cell); - job_goto.playerForced = forcedTarget.IsValid; - job_goto.locomotionUrgency = Finder.Settings.Enable_Sprinting ? LocomotionUrgency.Sprint : LocomotionUrgency.Jog; - selPawn.jobs.ClearQueuedJobs(); - selPawn.jobs.StopAll(); - selPawn.jobs.StartJob(job_goto, JobCondition.InterruptForced); - return true; - } - } - if (attackOnFallback) - { - if (verb is { IsMeleeAttack: false }) - { - if (updateDebugData) - { - _last = 12; - } - Job job_waitCombat = JobMaker.MakeJob(JobDefOf.Wait_Combat, Rand.Int % 100 + 100); - job_waitCombat.playerForced = forcedTarget.IsValid; - selPawn.jobs.ClearQueuedJobs(); - selPawn.jobs.StartJob(job_waitCombat, JobCondition.InterruptForced); - return true; - } - } - return false; - } + // check if the chance of survivability is high enough + // defensive actions + Verb verb = selPawn.CurrentEffectiveVerb; + if (verb != null && verb.WarmupStance != null && verb.WarmupStance.ticksLeft < 40) + { + return; + } + float possibleDmgDistance = 0f; + float possibleDmgWarmup = 0f; + float possibleDmg = 0f; + AIEnvThings enemies = data.AllEnemies; - /// - /// Called When the parent takes damage. - /// - /// Damage info - public void Notify_TookDamage(DamageInfo dInfo) - { - // notify the custom duty manager that this pawn took damage. - if (duties != null) - { - duties.Notify_TookDamage(); - } - // if the pawn is tanky enough skip. - if (Finder.Settings.Retreat_Enabled && parent.Spawned && GenTicks.TicksGame - lastScanned < 90 && !IsDeadOrDowned && armor.TankInt < 0.4f) - { - if (!selPawn.mindState.MeleeThreatStillThreat) - { - if (!selPawn.RaceProps.IsMechanoid && dInfo.Def != null && dInfo.Instigator != null) - { - if (selPawn.CurJobDef.Is(JobDefOf.Mine)) - { - selPawn.jobs.StopAll(); - } - else - { - float armorVal = armor.GetArmor(dInfo.Def); - if (armorVal == 0 || Rand.Chance(dInfo.ArmorPenetrationInt / armorVal) || GenTicks.TicksGame - lastTookDamage < 30 && Rand.Chance(0.50f)) - { - bool Validator(IntVec3 cell) => (Rand.Chance(sightReader.GetVisibilityToEnemies(selPawn.Position) - sightReader.GetVisibilityToEnemies(cell)) || - Rand.Chance(sightReader.GetThreat(selPawn.Position) - sightReader.GetThreat(cell))); - float enemyRange = dInfo.Instigator.TryGetAttackVerb()?.EffectiveRange ?? 5f; - if (TryRetreat(new LocalTargetInfo(dInfo.Instigator), enemyRange, selPawn.CurrentEffectiveVerb, Validator, false, false)) - { - lastRetreated = GenTicks.TicksGame - Rand.Int % 50; - } - } - } - } - } - } - lastTookDamage = GenTicks.TicksGame; - } + rangedEnemiesTargetingSelf.Clear(); + if (bodySize < 2 || selPawn.RaceProps.Humanlike) + { + for (int i = 0; i < targetedBy.Count; i++) + { + Thing enemy = targetedBy[i]; +#if DEBUG_REACTION + if (enemy == null) + { + Log.Error("Found null thing (2)"); + continue; + } +#endif + if (GetEnemyAttackTargetId(enemy) == selPawn.thingIDNumber) + { + DamageReport damageReport = DamageUtility.GetDamageReport(enemy); + if (damageReport.IsValid && (!(enemy is Pawn enemyPawn) || enemyPawn.mindState?.MeleeThreatStillThreat == false)) + { + UpdateNearestEnemy(enemy); + if (!damageReport.primaryIsRanged) + { + UpdateNearestEnemyMelee(enemy); + } + float damage = damageReport.SimulatedDamage(armor); + if (!damageReport.primaryIsRanged) + { + // reduce the possible damage for far away melee pawns. + damage *= (5f - Mathf.Clamp(Maths.Sqrt_Fast(selPos.DistanceToSquared(enemy.Position), 4), 0f, 5f)) / 5f; + } + possibleDmg += damage; + possibleDmgDistance += enemy.DistanceTo_Fast(selPawn); + if (damageReport.primaryIsRanged) + { + possibleDmgWarmup += damageReport.primaryVerbProps.warmupTime; + rangedEnemiesTargetingSelf.Add(enemy); + } + } + } + } + if (rangedEnemiesTargetingSelf.Count > 0 && !selPawn.mindState.MeleeThreatStillThreat && !selPawn.IsApproachingMeleeTarget(distLimit: 8, false)) + { + int retreatRoll = Rand.Range(0, 50); + // major retreat attempt if the pawn is doomed + if (possibleDmg - retreatRoll > 0.001f && possibleDmg >= 50) + { + _last = 10; + _bestEnemy = nearestMeleeEnemy; + CoverPositionRequest request = new CoverPositionRequest(); + request.caster = selPawn; + request.target = nearestMeleeEnemy; + request.majorThreats = rangedEnemiesTargetingSelf; + request.maxRangeFromCaster = 12; + request.checkBlockChance = true; + if (CoverPositionFinder.TryFindRetreatPosition(request, out IntVec3 cell) && ShouldMoveTo(cell)) + { + if (cell != selPos) + { + _last = 11; + Job job_goto = JobMaker.MakeJob(CombatAI_JobDefOf.CombatAI_Goto_Retreat, cell); + job_goto.playerForced = forcedTarget.IsValid; + job_goto.locomotionUrgency = Finder.Settings.Enable_Sprinting ? LocomotionUrgency.Sprint : LocomotionUrgency.Jog; + selPawn.jobs.ClearQueuedJobs(); + selPawn.jobs.StartJob(job_goto, JobCondition.InterruptForced); + data.LastRetreated = GenTicks.TicksGame; + } + return; + } + } + // try minor retreat (duck for cover fast) + if (possibleDmg - retreatRoll * 0.5f > 0.001f && possibleDmg >= 20) + { + // selPawn.Map.debugDrawer.FlashCell(selPos, 1.0f, $"{possibleDmg}, {targetedBy.Count}, {rangedEnemiesTargetingSelf.Count}"); + CoverPositionRequest request = new CoverPositionRequest(); + request.caster = selPawn; + request.majorThreats = rangedEnemiesTargetingSelf; + request.checkBlockChance = true; + request.maxRangeFromCaster = Mathf.Clamp(possibleDmgWarmup * 5f - rangedEnemiesTargetingSelf.Count, 6f, 10f); + if (CoverPositionFinder.TryFindDuckPosition(request, out IntVec3 cell)) + { + bool diff = cell != selPos; + // run to cover + if (diff) + { + _last = 12; + Job job_goto = JobMaker.MakeJob(CombatAI_JobDefOf.CombatAI_Goto_Duck, cell); + job_goto.playerForced = forcedTarget.IsValid; + job_goto.locomotionUrgency = Finder.Settings.Enable_Sprinting ? LocomotionUrgency.Sprint : LocomotionUrgency.Jog; + selPawn.jobs.ClearQueuedJobs(); + selPawn.jobs.StartJob(job_goto, JobCondition.InterruptForced); + data.LastRetreated = lastRetreated = GenTicks.TicksGame; + } + if (data.TookDamageRecently(45) || !diff) + { + _last = 13; + Job job_waitCombat = JobMaker.MakeJob(JobDefOf.Wait_Combat, Rand.Int % 50 + 50); + job_waitCombat.playerForced = forcedTarget.IsValid; + job_waitCombat.checkOverrideOnExpire = true; + selPawn.jobs.jobQueue.EnqueueFirst(job_waitCombat); + data.LastRetreated = lastRetreated = GenTicks.TicksGame; + } + return; + } + } + } + } + if (duty.Is(DutyDefOf.ExitMapRandom)) + { + return; + } + // offensive actions + if (verb != null) + { + // if the pawn is retreating and the pawn is still in danger or recently took damage, skip any offensive reaction. + if (verb.IsMeleeAttack) + { + if ((selPawn.CurJob.Is(CombatAI_JobDefOf.CombatAI_Goto_Retreat) || selPawn.CurJob.Is(CombatAI_JobDefOf.CombatAI_Goto_Cover)) && (rangedEnemiesTargetingSelf.Count == 0 || possibleDmg < 2.5f)) + { + _last = 30; + selPawn.jobs.StopAll(); + } + bool bestEnemyIsRanged = false; + bool bestEnemyIsMeleeAttackingAlly = false; + // TODO create melee reactions. + IEnumerator enumeratorEnemies = data.EnemiesWhere(AIEnvAgentState.nearby); + while (enumeratorEnemies.MoveNext()) + { + AIEnvAgentInfo info = enumeratorEnemies.Current; +#if DEBUG_REACTION + if (info.thing == null) + { + Log.Error("Found null thing (2)"); + continue; + } +#endif + if (info.thing.Spawned && selPawn.CanReach(info.thing, PathEndMode.Touch, Danger.Unspecified)) + { + Verb enemyVerb = info.thing.TryGetAttackVerb(); + if (enemyVerb?.IsMeleeAttack == true && info.thing is Pawn enemyPawn && enemyPawn.CurJob.Is(JobDefOf.AttackMelee) && enemyPawn.CurJob.targetA.Thing?.TryGetAttackVerb()?.IsMeleeAttack == false) + { + if (!bestEnemyIsMeleeAttackingAlly) + { + bestEnemyIsMeleeAttackingAlly = true; + nearestEnemyDist = 1e5f; + nearestEnemy = null; + } + UpdateNearestEnemy(info.thing); + } + else if (!bestEnemyIsMeleeAttackingAlly) + { + if (enemyVerb?.IsMeleeAttack == false) + { + if (!bestEnemyIsRanged) + { + bestEnemyIsRanged = true; + nearestEnemyDist = 1e5f; + nearestEnemy = null; + } + UpdateNearestEnemy(info.thing); + } + else if (!bestEnemyIsRanged) + { + UpdateNearestEnemy(info.thing); + } + } + } + } + if (nearestEnemy == null) + { + nearestEnemy = selPawn.mindState.enemyTarget; + } + if (nearestEnemy == null || selPawn.CurJob.Is(JobDefOf.AttackMelee) && selPawn.CurJob.targetA.Thing == nearestEnemy) + { + return; + } + _bestEnemy = nearestEnemy; + if (!selPawn.mindState.MeleeThreatStillThreat || selPawn.stances?.stagger?.Staggered == false) + { + _last = 31; + Job job_melee = JobMaker.MakeJob(JobDefOf.AttackMelee, nearestEnemy); + job_melee.playerForced = forcedTarget.IsValid; + job_melee.locomotionUrgency = LocomotionUrgency.Jog; + selPawn.jobs.ClearQueuedJobs(); + selPawn.jobs.StartJob(job_melee, JobCondition.InterruptForced); + data.LastInterrupted = GenTicks.TicksGame; + // no enemy cannot be approached solo + // TODO + // no enemy can be approached solo + // TODO + } + } + // ranged + else + { + // if CE is active skip reaction if the pawn is reloading or hunkering down. + if (Mod_CE.active && (selPawn.CurJobDef.Is(Mod_CE.ReloadWeapon) || selPawn.CurJobDef.Is(Mod_CE.HunkerDown))) + { + return; + } + // check if the verb is available. + if (!verb.Available() || Mod_CE.active && Mod_CE.IsAimingCE(verb)) + { + return; + } + bool bestEnemyVisibleNow = false; + bool bestEnemyVisibleSoon = false; + // A not fast check will check for retreat and for reactions to enemies that are visible or soon to be visible. + // A fast check will check only for retreat. + IEnumerator enumerator = data.Enemies(); + while (enumerator.MoveNext()) + { + AIEnvAgentInfo info = enumerator.Current; +#if DEBUG_REACTION + if (info.thing == null) + { + Log.Error("Found null thing (3)"); + continue; + } +#endif + if (info.thing.Spawned) + { + Pawn enemyPawn = info.thing as Pawn; + if (verb.CanHitTarget(info.thing)) + { + if (!bestEnemyVisibleNow) + { + nearestEnemy = null; + nearestEnemyDist = 1e4f; + bestEnemyVisibleNow = true; + } + UpdateNearestEnemy(info.thing); + } + else if (enemyPawn != null && !bestEnemyVisibleNow) + { + if (verb.CanHitTarget(PawnPathUtility.GetMovingShiftedPosition(enemyPawn, 120))) + { + if (!bestEnemyVisibleSoon) + { + nearestEnemy = null; + nearestEnemyDist = 1e4f; + bestEnemyVisibleSoon = true; + } + UpdateNearestEnemy(info.thing); + } + else if (!bestEnemyVisibleSoon) + { + UpdateNearestEnemy(info.thing); + } + } + if (enemyPawn != null && enemyPawn.CurrentEffectiveVerb.IsMeleeAttack) + { + UpdateNearestEnemyMelee(enemyPawn); + } + } + } + + void StartOrQueueCoverJob(IntVec3 cell, int codeOffset) + { + if (selPawn.CurJob.Is(JobDefOf.Wait_Combat)) + { + _last = 50 + codeOffset; + Job job_goto = JobMaker.MakeJob(CombatAI_JobDefOf.CombatAI_Goto_Cover, cell); + job_goto.playerForced = forcedTarget.IsValid; + job_goto.locomotionUrgency = Finder.Settings.Enable_Sprinting ? LocomotionUrgency.Sprint : LocomotionUrgency.Jog; + Job job_waitCombat = JobMaker.MakeJob(JobDefOf.Wait_Combat, Rand.Int % 150 + 200); + job_waitCombat.targetA = nearestEnemy; + job_waitCombat.playerForced = forcedTarget.IsValid; + job_waitCombat.endIfCantShootTargetFromCurPos = true; + job_waitCombat.checkOverrideOnExpire = true; + selPawn.jobs.ClearQueuedJobs(); + selPawn.jobs.jobQueue.EnqueueFirst(job_waitCombat); + selPawn.jobs.jobQueue.EnqueueFirst(job_goto); + data.LastInterrupted = GenTicks.TicksGame; + } + else + { + _last = 51 + codeOffset; + selPawn.mindState.enemyTarget = nearestEnemy; + Job job_goto = JobMaker.MakeJob(CombatAI_JobDefOf.CombatAI_Goto_Cover, cell); + job_goto.playerForced = forcedTarget.IsValid; + job_goto.locomotionUrgency = Finder.Settings.Enable_Sprinting ? LocomotionUrgency.Sprint : LocomotionUrgency.Jog; + Job job_waitCombat = JobMaker.MakeJob(JobDefOf.Wait_Combat, Rand.Int % 150 + 200); + job_waitCombat.playerForced = forcedTarget.IsValid; + job_waitCombat.endIfCantShootTargetFromCurPos = true; + job_waitCombat.checkOverrideOnExpire = true; + selPawn.jobs.ClearQueuedJobs(); + selPawn.jobs.StartJob(job_goto, JobCondition.InterruptForced); + selPawn.jobs.jobQueue.EnqueueFirst(job_waitCombat); + data.LastInterrupted = GenTicks.TicksGame; + } + } + + if (nearestEnemy != null && rangedEnemiesTargetingSelf.Contains(nearestEnemy)) + { + rangedEnemiesTargetingSelf.Remove(nearestEnemy); + } + bool retreatMeleeThreat = nearestMeleeEnemy != null && nearestMeleeEnemyDist < Maths.Max(verb.EffectiveRange / 3f, 9); + bool retreatThreat = !retreatMeleeThreat && nearestEnemy != null && nearestEnemyDist < Maths.Max(verb.EffectiveRange / 4f, 5); + _bestEnemy = retreatMeleeThreat ? nearestMeleeEnemy : nearestEnemy; + // retreat because of a close melee threat + if (bodySize < 2.0f && (retreatThreat || retreatMeleeThreat)) + { + _bestEnemy = retreatThreat ? nearestEnemy : nearestMeleeEnemy; + _last = 40; + CoverPositionRequest request = new CoverPositionRequest(); + request.caster = selPawn; + request.target = nearestMeleeEnemy; + request.verb = verb; + request.majorThreats = rangedEnemiesTargetingSelf; + request.checkBlockChance = true; + request.maxRangeFromCaster = verb.EffectiveRange / 2 + 8; + if (CoverPositionFinder.TryFindRetreatPosition(request, out IntVec3 cell)) + { + if (cell != selPos) + { + _last = 41; + Job job_goto = JobMaker.MakeJob(CombatAI_JobDefOf.CombatAI_Goto_Retreat, cell); + job_goto.playerForced = forcedTarget.IsValid; + job_goto.locomotionUrgency = Finder.Settings.Enable_Sprinting ? LocomotionUrgency.Sprint : LocomotionUrgency.Jog; + selPawn.jobs.ClearQueuedJobs(); + selPawn.jobs.StartJob(job_goto, JobCondition.InterruptForced); + data.LastRetreated = GenTicks.TicksGame; + } + } + } + // best enemy is insight + else if (nearestEnemy != null) + { + _bestEnemy = nearestEnemy; + + if (!selPawn.RaceProps.Humanlike || bodySize > 2.0f) + { + if (bestEnemyVisibleNow && selPawn.mindState.enemyTarget == null) + { + selPawn.mindState.enemyTarget = nearestEnemy; + } + } + else if (bestEnemyVisibleNow) + { + if (nearestEnemyDist > 8) + { + CastPositionRequest request = new CastPositionRequest(); + request.caster = selPawn; + request.target = nearestEnemy; + request.maxRangeFromTarget = 9999; + request.verb = verb; + request.maxRangeFromCaster = Maths.Max(Maths.Min(verb.EffectiveRange, nearestEnemyDist) / 2f, 10f); + request.wantCoverFromTarget = true; + if (CastPositionFinder.TryFindCastPosition(request, out IntVec3 cell) && ShouldMoveTo(cell)) + { + StartOrQueueCoverJob(cell, 0); + } + else if (!selPawn.CurJob.Is(JobDefOf.Wait_Combat)) + { + _last = 52; + Job job_waitCombat = JobMaker.MakeJob(JobDefOf.Wait_Combat, Rand.Int % 100 + 100); + job_waitCombat.playerForced = forcedTarget.IsValid; + job_waitCombat.endIfCantShootTargetFromCurPos = true; + selPawn.jobs.ClearQueuedJobs(); + selPawn.jobs.StartJob(job_waitCombat, JobCondition.InterruptForced); + data.LastInterrupted = GenTicks.TicksGame; + } + } + else + { + _last = 53; + Job job_waitCombat = JobMaker.MakeJob(JobDefOf.Wait_Combat, Rand.Int % 100 + 100); + job_waitCombat.playerForced = forcedTarget.IsValid; + job_waitCombat.endIfCantShootTargetFromCurPos = true; + selPawn.jobs.ClearQueuedJobs(); + selPawn.jobs.StartJob(job_waitCombat, JobCondition.InterruptForced); + data.LastInterrupted = GenTicks.TicksGame; + } + } + // best enemy is approaching but not yet in view + else if (bestEnemyVisibleSoon || duty.Is(DutyDefOf.Escort) || duty.Is(CombatAI_DutyDefOf.CombatAI_Escort) || duty.Is(DutyDefOf.Defend) || duty.Is(CombatAI_DutyDefOf.CombatAI_AssaultPoint) || duty.Is(DutyDefOf.HuntEnemiesIndividual)) + { + _last = 60; + CoverPositionRequest request = new CoverPositionRequest(); + request.caster = selPawn; + request.verb = verb; + request.target = nearestEnemy; + if (!bestEnemyVisibleSoon && !Finder.Performance.TpsCriticallyLow) + { + while (rangedEnemiesTargetingSelf.Count > 3) + { + rangedEnemiesTargetingSelf.RemoveAt(Rand.Int % rangedEnemiesTargetingSelf.Count); + } + request.majorThreats = rangedEnemiesTargetingSelf; + request.maxRangeFromCaster = Maths.Min(verb.EffectiveRange, 10f); + } + else + { + request.maxRangeFromCaster = Maths.Max(verb.EffectiveRange / 2f, 10f); + } + request.checkBlockChance = true; + if (CoverPositionFinder.TryFindCoverPosition(request, out IntVec3 cell)) + { + if (ShouldMoveTo(cell)) + { + StartOrQueueCoverJob(cell, 10); + } + else if(nearestEnemy is Pawn enemyPawn) + { + _last = 71; + // fallback + request.target = PawnPathUtility.GetMovingShiftedPosition(enemyPawn, 90); + request.maxRangeFromCaster = Mathf.Min(request.maxRangeFromCaster, 5); + if (verb.CanHitFromCellIgnoringRange(selPos, request.target, out _) && CoverPositionFinder.TryFindCoverPosition(request, out cell) && ShouldMoveTo(cell)) + { + StartOrQueueCoverJob(cell, 20); + } + } + } + } + } + } + } + } - /// - /// Start a sapping task. - /// - /// Blocked cells - /// Cell before blocked cells - /// Whether to look for escorts - public void StartSapper(List blocked, IntVec3 cellBefore, bool findEscorts) - { - if (cellBefore.IsValid && sapperNodes.Count > 0 && GenTicks.TicksGame - sapperStartTick < 4800) - { - ReleaseEscorts(); - } - this.cellBefore = cellBefore; - this.findEscorts = findEscorts; - sapperStartTick = GenTicks.TicksGame; - sapperNodes.Clear(); - sapperNodes.AddRange(blocked); - _sap = 0; - TryStartSapperJob(); - } + /// + /// Returns whether parent pawn should move to a new position. + /// + /// New position + /// Whether to move or not + private bool ShouldMoveTo(IntVec3 newPos) + { + IntVec3 pos = selPawn.Position; + float curVisibility = sightReader.GetVisibilityToEnemies(pos); + float curThreat = sightReader.GetVisibilityToEnemies(pos); + Job job = selPawn.CurJob; + if (curThreat == 0 && curVisibility == 0 && !(job.Is(JobDefOf.Wait_Combat) || job.Is(CombatAI_JobDefOf.CombatAI_Goto_Cover) || job.Is(CombatAI_JobDefOf.CombatAI_Goto_Duck) || job.Is(CombatAI_JobDefOf.CombatAI_Goto_Retreat))) + { + return sightReader.GetVisibilityToEnemies(newPos) <= 2f && sightReader.GetThreat(newPos) < 1f; + } + float visDiff = curVisibility - sightReader.GetVisibilityToEnemies(newPos); + float magDiff = Maths.Sqrt_Fast(sightReader.GetEnemyDirection(pos).sqrMagnitude, 4) - Maths.Sqrt_Fast(sightReader.GetEnemyDirection(newPos).sqrMagnitude, 4); + float threatDiff = curThreat - sightReader.GetThreat(newPos); + return Rand.Chance(visDiff) && Rand.Chance(threatDiff) && Rand.Chance(magDiff); + } - /// - /// Returns debug gizmos. - /// - /// - public override IEnumerable CompGetGizmosExtra() - { - if (Prefs.DevMode && DebugSettings.godMode) - { - Verb verb = selPawn.TryGetAttackVerb(); - float retreatDistSqr = Maths.Max(verb.EffectiveRange * verb.EffectiveRange / 9, 36); - Map map = selPawn.Map; - Command_Action retreat = new Command_Action(); - retreat.defaultLabel = "DEV: Retreat position search"; - retreat.action = delegate - { - CoverPositionRequest request = new CoverPositionRequest(); - if (_bestEnemy != null) - { - request.target = new LocalTargetInfo(_bestEnemy.Position); - } - request.caster = selPawn; - request.verb = verb; - request.maxRangeFromCaster = Maths.Min(Mathf.Max(retreatDistSqr * 2 / (selPawn.BodySize + 0.01f), 5), 15); - request.checkBlockChance = true; - CoverPositionFinder.TryFindRetreatPosition(request, out IntVec3 cell, (cell, val) => map.debugDrawer.FlashCell(cell, Mathf.Clamp((val + 15f) / 30f, 0.01f, 0.99f), $"{Math.Round(val, 3)}")); - if (cell.IsValid) - { - map.debugDrawer.FlashCell(cell, 1, "XXXXXXX", duration: 150); - } - }; - Command_Action cover = new Command_Action(); - cover.defaultLabel = "DEV: Cover position search"; - cover.action = delegate - { - CoverPositionRequest request = new CoverPositionRequest(); - if (_bestEnemy != null) - { - request.target = new LocalTargetInfo(_bestEnemy.Position); - } - request.caster = selPawn; - request.verb = verb; - request.maxRangeFromCaster = Mathf.Clamp(selPawn.GetStatValue_Fast(StatDefOf.MoveSpeed, 60) * 3 / (selPawn.BodySize + 0.01f), 4, 15); - request.checkBlockChance = true; - CoverPositionFinder.TryFindCoverPosition(request, out IntVec3 cell, (cell, val) => map.debugDrawer.FlashCell(cell, Mathf.Clamp((val + 15f) / 30f, 0.01f, 0.99f), $"{Math.Round(val, 3)}")); - if (cell.IsValid) - { - map.debugDrawer.FlashCell(cell, 1, "XXXXXXX", duration: 150); - } - }; - Command_Action cast = new Command_Action(); - cast.defaultLabel = "DEV: Cast position search"; - cast.action = delegate - { - if (_bestEnemy == null) - { - return; - } - CastPositionRequest request = new CastPositionRequest(); - request.caster = selPawn; - request.target = _bestEnemy; - request.verb = verb; - request.maxRangeFromTarget = 9999; - request.maxRangeFromCaster = Mathf.Clamp(selPawn.GetStatValue_Fast(StatDefOf.MoveSpeed, 60) * 3 / (selPawn.BodySize + 0.01f), 4, 15); - request.wantCoverFromTarget = true; - try - { - DebugViewSettings.drawCastPositionSearch = true; - CastPositionFinder.TryFindCastPosition(request, out IntVec3 cell); - if (cell.IsValid) - { - map.debugDrawer.FlashCell(cell, 1, "XXXXXXX", duration: 150); - } - } - catch (Exception er) - { - Log.Error(er.ToString()); - } - finally - { - DebugViewSettings.drawCastPositionSearch = false; - } - }; - yield return retreat; - yield return cover; - yield return cast; - } - if (selPawn.IsColonist) - { - Command_Target attackMove = new Command_Target(); - attackMove.defaultLabel = R.Keyed.CombatAI_Gizmos_AttackMove; - attackMove.targetingParams = new TargetingParameters(); - attackMove.targetingParams.canTargetPawns = true; - attackMove.targetingParams.canTargetLocations = true; - attackMove.targetingParams.canTargetSelf = false; - attackMove.targetingParams.validator = (target) => - { - if (!target.IsValid || !target.Cell.InBounds(selPawn.Map)) - { - return false; - } - foreach (Pawn pawn in Find.Selector.SelectedPawns) - { - if (pawn == null) - { - continue; - } - if (pawn.CanReach(target.Cell, PathEndMode.OnCell, Danger.Unspecified, false, false)) - { - return true; - } - } - return false; - }; - attackMove.icon = R.Tex.Isma_Gizmos_move_attack; - attackMove.groupable = true; - attackMove.shrinkable = false; - attackMove.action = (LocalTargetInfo target) => - { - foreach (Pawn pawn in Find.Selector.SelectedPawns) - { - if (pawn.IsColonist && pawn.drafter != null) - { - if (!pawn.CanReach(target.Cell, PathEndMode.OnCell, Danger.Unspecified, false, false)) - { - continue; - } - if (!pawn.Drafted) - { - if (!pawn.drafter.ShowDraftGizmo) - { - continue; - } - DevelopmentalStage stage = pawn.DevelopmentalStage; - if (stage <= DevelopmentalStage.Child && stage != DevelopmentalStage.None) - { - continue; - } - pawn.drafter.Drafted = true; - } - if (pawn.CurrentEffectiveVerb?.IsMeleeAttack ?? true) - { - Messages.Message(R.Keyed.CombatAI_Gizmos_AttackMove_Warning, MessageTypeDefOf.RejectInput, false); - continue; - } - pawn.GetComp_Fast().forcedTarget = target; - Job gotoJob = JobMaker.MakeJob(JobDefOf.Goto, target); - gotoJob.canUseRangedWeapon = true; - gotoJob.locomotionUrgency = LocomotionUrgency.Jog; - gotoJob.playerForced = true; - pawn.jobs.ClearQueuedJobs(); - pawn.jobs.StartJob(gotoJob); - } - } - }; - yield return attackMove; - if (forcedTarget.IsValid) - { - Command_Action cancelAttackMove = new Command_Action(); - cancelAttackMove.defaultLabel = R.Keyed.CombatAI_Gizmos_AttackMove_Cancel; - cancelAttackMove.groupable = true; - // - // cancelAttackMove.disabled = forcedTarget.IsValid; - cancelAttackMove.action = () => - { - foreach (Pawn pawn in Find.Selector.SelectedPawns) - { - if (pawn.IsColonist) - { - pawn.GetComp_Fast().forcedTarget = LocalTargetInfo.Invalid; - pawn.jobs.ClearQueuedJobs(); - pawn.jobs.StopAll(); - } - } - }; - } - } - } + /// + /// Called When the parent takes damage. + /// + /// Damage info + public void Notify_TookDamage(DamageInfo dInfo) + { + // notify the custom duty manager that this pawn took damage. + if (duties != null) + { + duties.Notify_TookDamage(); + } + data.LastTookDamage = lastTookDamage = GenTicks.TicksGame; + if (dInfo.Instigator != null && data.NumAllies != 0 && dInfo.Instigator.HostileTo(selPawn)) + { + StartAggroCountdown(dInfo.Instigator); + } + } - /// - /// Release escorts pawns. - /// - public void ReleaseEscorts() - { - for (int i = 0; i < escorts.Count; i++) - { - Pawn escort = escorts[i]; - if (escort == null || escort.Destroyed || escort.Dead || escort.Downed || escort.mindState.duty == null) - { - continue; - } - if (escort.mindState.duty.focus == parent) - { - escort.GetComp_Fast().releasedTick = GenTicks.TicksGame; - escort.GetComp_Fast().duties.FinishAllDuties(DutyDefOf.Escort, parent); - } - } - escorts.Clear(); - } + /// + /// Called when a bullet impacts nearby. + /// + /// Attacker + /// Impact position + public void Notify_BulletImpact(Thing instigator, IntVec3 cell) + { + if (instigator == null) + { + StartAggroCountdown(new LocalTargetInfo(cell)); + } + else + { + StartAggroCountdown(new LocalTargetInfo(instigator)); + } + } + /// + /// Start aggro countdown. + /// + /// Enemy. + public void StartAggroCountdown(LocalTargetInfo enemy) + { + this.aggroTarget = enemy; + this.aggroTicks = Rand.Range(30, 90); + } + + /// + /// Switch the pawn to an aggro mode and their allies around them. + /// + /// Attacker + /// Chance to aggro nearbyAllies + /// Aggro sig + private void TryAggro(LocalTargetInfo enemy, float aggroAllyChance, int sig) + { + if (selPawn.mindState.duty.Is(DutyDefOf.Defend) && data.AgroSig != sig) + { + Pawn_CustomDutyTracker.CustomPawnDuty custom = CustomDutyUtility.HuntDownEnemies(enemy.Cell, ((Rand.Int % 1200) + 2400)); + if (selPawn.TryStartCustomDuty(custom)) + { + data.AgroSig = sig; + // aggro nearby Allies + IEnumerator allies = data.AlliesNearBy(); + while (allies.MoveNext()) + { + AIEnvAgentInfo ally = allies.Current; + // make allies not targeting anyone target the attacking enemy + if (Rand.Chance(aggroAllyChance) && ally.thing is Pawn { Destroyed: false, Spawned: true, Downed: false } other && other.mindState.duty.Is(DutyDefOf.Defend)) + { + ThingComp_CombatAI comp = other.GetComp_Fast(); + if (comp != null && comp.data.AgroSig != sig) + { + if (enemy.HasThing) + { + other.mindState.enemyTarget ??= enemy.Thing; + } + comp.TryAggro(enemy, aggroAllyChance / 2f, sig); + } + } + } + } + } + } - /// - /// Enqueue enemy for reaction processing. - /// - /// Spotted enemy - public void Notify_Enemy(AIEnvAgentInfo info) - { - if (!scanning) - { - Log.Warning($"ISMA: Notify_EnemiesVisible called while not scanning. ({allEnemies.Count}, {Thread.CurrentThread.ManagedThreadId})"); - return; - } - allEnemies.Add(info); - } - - /// - /// Enqueue ally for reaction processing. - /// - /// Spotted enemy - public void Notify_Ally(AIEnvAgentInfo info) - { - if (!scanning) - { - Log.Warning($"ISMA: Notify_EnemiesVisible called while not scanning. ({allEnemies.Count}, {Thread.CurrentThread.ManagedThreadId})"); - return; - } - allAllies.Add(info); - } + /// + /// Start a sapping task. + /// + /// Blocked cells + /// Cell before blocked cells + /// Whether to look for escorts + public void StartSapper(List blocked, IntVec3 cellBefore, bool findEscorts) + { + if (cellBefore.IsValid && sapperNodes.Count > 0 && GenTicks.TicksGame - sapperStartTick < 4800) + { + ReleaseEscorts(false); + } + this.cellBefore = cellBefore; + this.findEscorts = findEscorts; + sapperStartTick = GenTicks.TicksGame; + sapperNodes.Clear(); + sapperNodes.AddRange(blocked); + _sap = 0; + TryStartSapperJob(); + } - /// - /// Called to notify a wait job started by reaction has ended. Will reduce the reaction cooldown. - /// - public void Notify_WaitJobEnded() - { - lastInterupted -= 30; - } + /// + /// Returns debug gizmos. + /// + /// + public override IEnumerable CompGetGizmosExtra() + { + if (Finder.Settings.Debug && Finder.Settings.Debug_LogJobs) + { + Command_Action jobs = new Command_Action(); + jobs.defaultLabel = "DEV: View job logs"; + jobs.action = delegate + { + if (Find.WindowStack.windows.Any(w => w is Window_JobLogs logs && logs.comp == this)) + { + return; + } + jobLogs ??= new List(); + Window_JobLogs window = new Window_JobLogs(this); + Find.WindowStack.Add(window); + }; + yield return jobs; + } + if (Prefs.DevMode && DebugSettings.godMode) + { + Verb verb = selPawn.TryGetAttackVerb(); + float retreatDistSqr = Maths.Max(verb.EffectiveRange * verb.EffectiveRange / 9, 36); + Map map = selPawn.Map; + Command_Action retreat = new Command_Action(); + retreat.defaultLabel = "DEV: Retreat position search"; + retreat.action = delegate + { + CoverPositionRequest request = new CoverPositionRequest(); + if (_bestEnemy != null) + { + request.target = new LocalTargetInfo(_bestEnemy.Position); + } + request.caster = selPawn; + request.verb = verb; + request.maxRangeFromCaster = Maths.Min(Mathf.Max(retreatDistSqr * 2 / (selPawn.BodySize + 0.01f), 5), 15); + request.checkBlockChance = true; + CoverPositionFinder.TryFindRetreatPosition(request, out IntVec3 cell, (cell, val) => map.debugDrawer.FlashCell(cell, Mathf.Clamp((val + 15f) / 30f, 0.01f, 0.99f), $"{Math.Round(val, 3)}")); + if (cell.IsValid) + { + map.debugDrawer.FlashCell(cell, 1, "XXXXXXX", 150); + } + }; + Command_Action duck = new Command_Action(); + duck.defaultLabel = "DEV: Duck position search"; + duck.action = delegate + { + CoverPositionRequest request = new CoverPositionRequest(); + request.majorThreats = data.BeingTargetedBy; + request.caster = selPawn; + request.verb = verb; + request.maxRangeFromCaster = 5; + request.checkBlockChance = true; + CoverPositionFinder.TryFindDuckPosition(request, out IntVec3 cell, (cell, val) => map.debugDrawer.FlashCell(cell, Mathf.Clamp((val + 15f) / 30f, 0.01f, 0.99f), $"{Math.Round(val, 3)}")); + if (cell.IsValid) + { + map.debugDrawer.FlashCell(cell, 1, "XXXXXXX", 150); + } + }; + Command_Action cover = new Command_Action(); + cover.defaultLabel = "DEV: Cover position search"; + cover.action = delegate + { + CoverPositionRequest request = new CoverPositionRequest(); + if (_bestEnemy != null) + { + request.target = new LocalTargetInfo(_bestEnemy.Position); + } + request.caster = selPawn; + request.verb = verb; + request.maxRangeFromCaster = Mathf.Clamp(selPawn.GetStatValue_Fast(StatDefOf.MoveSpeed, 60) * 3 / (selPawn.BodySize + 0.01f), 4, 15); + request.checkBlockChance = true; + CoverPositionFinder.TryFindCoverPosition(request, out IntVec3 cell, (cell, val) => map.debugDrawer.FlashCell(cell, Mathf.Clamp((val + 15f) / 30f, 0.01f, 0.99f), $"{Math.Round(val, 3)}")); + if (cell.IsValid) + { + map.debugDrawer.FlashCell(cell, 1, "XXXXXXX", 150); + } + }; + Command_Action cast = new Command_Action(); + cast.defaultLabel = "DEV: Cast position search"; + cast.action = delegate + { + if (_bestEnemy == null) + { + return; + } + CastPositionRequest request = new CastPositionRequest(); + request.caster = selPawn; + request.target = _bestEnemy; + request.verb = verb; + request.maxRangeFromTarget = 9999; + request.maxRangeFromCaster = Mathf.Clamp(selPawn.GetStatValue_Fast(StatDefOf.MoveSpeed, 60) * 3 / (selPawn.BodySize + 0.01f), 4, 15); + request.wantCoverFromTarget = true; + try + { + DebugViewSettings.drawCastPositionSearch = true; + CastPositionFinder.TryFindCastPosition(request, out IntVec3 cell); + if (cell.IsValid) + { + map.debugDrawer.FlashCell(cell, 1, "XXXXXXX", 150); + } + } + catch (Exception er) + { + Log.Error(er.ToString()); + } + finally + { + DebugViewSettings.drawCastPositionSearch = false; + } + }; + yield return retreat; + yield return duck; + yield return cover; + yield return cast; + } + if (selPawn.IsColonist) + { + Command_Target attackMove = new Command_Target(); + attackMove.defaultLabel = Keyed.CombatAI_Gizmos_AttackMove; + attackMove.targetingParams = new TargetingParameters(); + attackMove.targetingParams.canTargetPawns = true; + attackMove.targetingParams.canTargetLocations = true; + attackMove.targetingParams.canTargetSelf = false; + attackMove.targetingParams.validator = target => + { + if (!target.IsValid || !target.Cell.InBounds(selPawn.Map)) + { + return false; + } + foreach (Pawn pawn in Find.Selector.SelectedPawns) + { + if (pawn == null) + { + continue; + } + if (pawn.CanReach(target.Cell, PathEndMode.OnCell, Danger.Unspecified)) + { + return true; + } + } + return false; + }; + attackMove.icon = Tex.Isma_Gizmos_move_attack; + attackMove.groupable = true; + attackMove.shrinkable = false; + attackMove.action = target => + { + foreach (Pawn pawn in Find.Selector.SelectedPawns) + { + if (pawn.IsColonist && pawn.drafter != null) + { + if (!pawn.CanReach(target.Cell, PathEndMode.OnCell, Danger.Unspecified)) + { + continue; + } + if (!pawn.Drafted) + { + if (!pawn.drafter.ShowDraftGizmo) + { + continue; + } + DevelopmentalStage stage = pawn.DevelopmentalStage; + if (stage <= DevelopmentalStage.Child && stage != DevelopmentalStage.None) + { + continue; + } + pawn.drafter.Drafted = true; + } + if (pawn.CurrentEffectiveVerb?.IsMeleeAttack ?? true) + { + Messages.Message(Keyed.CombatAI_Gizmos_AttackMove_Warning, MessageTypeDefOf.RejectInput, false); + continue; + } + pawn.GetComp_Fast().forcedTarget = target; + Job gotoJob = JobMaker.MakeJob(JobDefOf.Goto, target); + gotoJob.canUseRangedWeapon = true; + gotoJob.locomotionUrgency = LocomotionUrgency.Jog; + gotoJob.playerForced = true; + pawn.jobs.ClearQueuedJobs(); + pawn.jobs.StartJob(gotoJob); + } + } + }; + yield return attackMove; + if (forcedTarget.IsValid) + { + Command_Action cancelAttackMove = new Command_Action(); + cancelAttackMove.defaultLabel = Keyed.CombatAI_Gizmos_AttackMove_Cancel; + cancelAttackMove.groupable = true; + // + // cancelAttackMove.disabled = forcedTarget.IsValid; + cancelAttackMove.action = () => + { + foreach (Pawn pawn in Find.Selector.SelectedPawns) + { + if (pawn.IsColonist) + { + pawn.GetComp_Fast().forcedTarget = LocalTargetInfo.Invalid; + pawn.jobs.ClearQueuedJobs(); + pawn.jobs.StopAll(); + } + } + }; + } + } + } - /// - /// Called when the parent sightreader group has changed. - /// Should only be called from SighTracker/SightGrid. - /// - /// The new sightReader - public void Notify_SightReaderChanged(SightTracker.SightReader reader) - { - sightReader = reader; - } + /// + /// Release escorts pawns. + /// + public void ReleaseEscorts(bool success) + { + for (int i = 0; i < escorts.Count; i++) + { + Pawn escort = escorts[i]; + if (escort == null || escort.Destroyed || escort.Dead || escort.Downed || escort.mindState.duty == null) + { + continue; + } + if (escort.mindState.duty.focus == parent) + { + if (success) + { + escort.GetComp_Fast().releasedTick = GenTicks.TicksGame; + } + escort.GetComp_Fast().duties.FinishAllDuties(CombatAI_DutyDefOf.CombatAI_Escort, parent); + } + } + if (success) + { + Predicate validator = t => + { + if (!t.HostileTo(selPawn)) + { + ThingComp_CombatAI comp = t.GetComp_Fast(); + if (comp != null && comp.IsSapping && comp.sapperNodes.Count > 3) + { + ReleaseEscorts(false); + comp.cellBefore = IntVec3.Invalid; + comp.sapperStartTick = GenTicks.TicksGame + 800; + comp.sapperNodes.Clear(); + } + } + return false; + }; + Verse.GenClosest.RegionwiseBFSWorker(selPawn.Position, selPawn.Map, ThingRequest.ForGroup(ThingRequestGroup.Pawn), PathEndMode.InteractionCell, TraverseParms.For(selPawn), validator, null, 1, 4, 15, out int _); + } + escorts.Clear(); + } - public override void PostExposeData() - { - base.PostExposeData(); - Scribe_Deep.Look(ref data, "AIAgentData.0"); - data ??= new AIAgentData(); - Scribe_Deep.Look(ref duties, "duties2"); - Scribe_Deep.Look(ref abilities, "abilities2"); - Scribe_TargetInfo.Look(ref forcedTarget, "forcedTarget"); - if (duties == null) - { - duties = new Pawn_CustomDutyTracker(selPawn); - } - if (abilities == null) - { - abilities = new Pawn_AbilityCaster(selPawn); - } - duties.pawn = selPawn; - abilities.pawn = selPawn; - } - private void TryStartSapperJob() - { - if (sightReader.GetVisibilityToEnemies(cellBefore) > 0 || sapperNodes.Count == 0) - { - ReleaseEscorts(); - cellBefore = IntVec3.Invalid; - releasedTick = GenTicks.TicksGame; - sapperStartTick = -1; - sapperNodes.Clear(); - return; - } - if (selPawn.Destroyed || IsDeadOrDowned || selPawn.mindState?.duty == null || !(selPawn.mindState.duty.def.Is(DutyDefOf.AssaultColony) || selPawn.mindState.duty.def.Is(DutyDefOf.Defend) || selPawn.mindState.duty.def.Is(DutyDefOf.AssaultThing) || selPawn.mindState.duty.def.Is(DutyDefOf.Breaching))) - { - ReleaseEscorts(); - return; - } - Map map = selPawn.Map; - Thing blocker = sapperNodes[0].GetEdifice(map); - if (blocker != null) - { - Job job = DigUtility.PassBlockerJob(selPawn, blocker, cellBefore, true, true); - if (job != null) - { - job.playerForced = true; - job.expiryInterval = 3600; - job.maxNumMeleeAttacks = 300; - selPawn.jobs.StopAll(); - selPawn.jobs.StartJob(job, JobCondition.InterruptForced); - if (findEscorts && Rand.Chance(1 - Maths.Max(1f / (escorts.Count + 1f), 0.85f))) - { - int count = escorts.Count; - int countTarget = Rand.Int % 4 + 3 + Maths.Min(sapperNodes.Count, 10) - Maths.Min(Mathf.CeilToInt(selPawn.Position.DistanceTo(cellBefore) / 10f), 5); - Faction faction = selPawn.Faction; - Predicate validator = t => - { - if (count < countTarget && t.Faction == faction && t is Pawn ally && !ally.Destroyed - && !ally.CurJobDef.Is(JobDefOf.Mine) - && ally.mindState?.duty?.def != DutyDefOf.Escort - && (sightReader == null || sightReader.GetAbsVisibilityToEnemies(ally.Position) == 0) - && ally.skills?.GetSkill(SkillDefOf.Mining).Level < 10) - { - ThingComp_CombatAI comp = ally.GetComp_Fast(); - if (comp?.duties != null && comp.duties?.Any(DutyDefOf.Escort) == false && !comp.IsSapping && GenTicks.TicksGame - comp.releasedTick > 600) - { - Pawn_CustomDutyTracker.CustomPawnDuty custom = CustomDutyUtility.Escort(selPawn, 20, 100, (500 * sapperNodes.Count) / (escorts.Count + 1) + Rand.Int % 500); - if (ally.TryStartCustomDuty(custom)) - { - escorts.Add(ally); - } - if (comp.duties.curCustomDuty?.duty != duties.curCustomDuty?.duty) - { - count += 3; - } - else - { - count++; - } - } - return count == countTarget; - } - return false; - }; - Verse.GenClosest.RegionwiseBFSWorker(selPawn.Position, map, ThingRequest.ForGroup(ThingRequestGroup.Pawn), PathEndMode.InteractionCell, TraverseParms.For(selPawn), validator, null, 1, 10, 40, out int _); - } - } - } - } + /// + /// Add enemy targeting self to Env data. + /// + /// + /// + public void Notify_BeingTargeted(Thing enemy, Verb verb) + { + if (enemy != null) + { + data.BeingTargeted(enemy); + if (Rand.Chance(0.15f) && (selPawn.mindState.duty.Is(DutyDefOf.Defend) || selPawn.mindState.duty.Is(DutyDefOf.Escort))) + { + StartAggroCountdown(enemy); + } + } + else + { + Log.Error($"{selPawn} received a null thing in Notify_BeingTargeted"); + } + } - #region TimeStamps + /// + /// Enqueue enemy for reaction processing. + /// + /// Spotted enemy + public void Notify_Enemy(AIEnvAgentInfo info) + { + if (!scanning) + { + Log.Warning($"ISMA: Notify_EnemiesVisible called while not scanning. ({allEnemies.Count}, {Thread.CurrentThread.ManagedThreadId})"); + return; + } + if (info.thing is Pawn enemy) + { + // skip if the enemy is downed + if (enemy.Downed) + { + return; + } + // skip for children + DevelopmentalStage stage = enemy.DevelopmentalStage; + if (stage <= DevelopmentalStage.Child && stage != DevelopmentalStage.None) + { + return; + } + } + if (allEnemies.TryGetValue(info.thing, out AIEnvAgentInfo store)) + { + info = store.Combine(info); + } + allEnemies[info.thing] = info; + } - /// - /// When the pawn was last order to move by CAI. - /// - private int lastMoved; - /// - /// When the last injury occured/damage. - /// - private int lastTookDamage; - /// - /// When the last scan occured. SightGrid is responisble for these scan cycles. - /// - private int lastScanned; - /// - /// When did this comp last interupt the parent pawn. IE: reacted, retreated, etc. - /// - private int lastInterupted; - /// - /// When the pawn was last order to retreat by CAI. - /// - private int lastRetreated; - /// - /// Last tick any enemies where reported in a scan. - /// - private int lastSawEnemies; - /// - /// The general direction of enemies last time the pawn reacted. - /// - private Vector2 prevEnemyDir = Vector2.zero; - /// - /// Tick when this pawn was released as an escort. - /// - private int releasedTick; + /// + /// Enqueue ally for reaction processing. + /// + /// Spotted enemy + public void Notify_Ally(AIEnvAgentInfo info) + { + if (!scanning) + { + Log.Warning($"ISMA: Notify_EnemiesVisible called while not scanning. ({allEnemies.Count}, {Thread.CurrentThread.ManagedThreadId})"); + return; + } + if (info.thing is Pawn ally) + { + // skip if the ally is downed + if (ally.Downed) + { + return; + } + // skip for children + DevelopmentalStage stage = ally.DevelopmentalStage; + if (stage <= DevelopmentalStage.Child && stage != DevelopmentalStage.None) + { + return; + } + } + if (allAllies.TryGetValue(info.thing, out AIEnvAgentInfo store)) + { + info = store.Combine(info); + } + allAllies[info.thing] = info; + } - #endregion + /// + /// Called to notify a wait job started by reaction has ended. Will reduce the reaction cooldown. + /// + public void Notify_WaitJobEnded() + { + lastInterupted -= 30; + } + + /// + /// Called when the parent sightreader group has changed. + /// Should only be called from SighTracker/SightGrid. + /// + /// The new sightReader + public void Notify_SightReaderChanged(SightTracker.SightReader reader) + { + sightReader = reader; + } + + public override void PostExposeData() + { + base.PostExposeData(); + Scribe_Deep.Look(ref data, "AIAgentData.0"); + data ??= new AIAgentData(); + Scribe_Deep.Look(ref duties, "duties2"); + Scribe_Deep.Look(ref abilities, "abilities2"); + Scribe_TargetInfo.Look(ref forcedTarget, "forcedTarget"); + if (duties == null) + { + duties = new Pawn_CustomDutyTracker(selPawn); + } + if (abilities == null) + { + abilities = new Pawn_AbilityCaster(selPawn); + } + duties.pawn = selPawn; + abilities.pawn = selPawn; + } + + private void TryStartSapperJob() + { + if (sightReader.GetVisibilityToEnemies(cellBefore) > 0 || sapperNodes.Count == 0) + { + ReleaseEscorts(false); + cellBefore = IntVec3.Invalid; + sapperStartTick = -1; + sapperNodes.Clear(); + return; + } + if (selPawn.Destroyed || IsDeadOrDowned || selPawn.mindState.duty == null || !(selPawn.mindState.duty.Is(DutyDefOf.AssaultColony) || selPawn.mindState.duty.Is(CombatAI_DutyDefOf.CombatAI_AssaultPoint) || selPawn.mindState.duty.Is(DutyDefOf.AssaultThing))) + { + ReleaseEscorts(false); + return; + } + Map map = selPawn.Map; + Thing blocker = sapperNodes[0].GetEdifice(map); + if (blocker != null) + { + Job job = DigUtility.PassBlockerJob(selPawn, blocker, cellBefore, true, true); + if (job != null) + { + job.playerForced = true; + job.expiryInterval = 3600; + job.maxNumMeleeAttacks = 300; + selPawn.jobs.StopAll(); + selPawn.jobs.StartJob(job, JobCondition.InterruptForced); + if (findEscorts && Rand.Chance(1 - Maths.Min(escorts.Count / 12, 0.85f))) + { + int count = escorts.Count; + int countTarget = Rand.Int % 4 + 12 + Maths.Min(sapperNodes.Count, 10); + Faction faction = selPawn.Faction; + Predicate validator = t => + { + if (count < countTarget && t.Faction == faction && t is Pawn ally && !ally.Destroyed + && !ally.CurJobDef.Is(JobDefOf.Mine) + && ally.mindState?.duty?.def != CombatAI_DutyDefOf.CombatAI_Escort + && (sightReader == null || sightReader.GetAbsVisibilityToEnemies(ally.Position) == 0) + && ally.skills?.GetSkill(SkillDefOf.Mining).Level < 10) + { + ThingComp_CombatAI comp = ally.GetComp_Fast(); + if (comp?.duties != null && comp.duties?.Any(CombatAI_DutyDefOf.CombatAI_Escort) == false && !comp.IsSapping && GenTicks.TicksGame - comp.releasedTick > 600) + { + Pawn_CustomDutyTracker.CustomPawnDuty custom = CustomDutyUtility.Escort(selPawn, 20, 100, 600 + Mathf.CeilToInt(12 * selPawn.Position.DistanceTo(cellBefore)) + 540 * sapperNodes.Count + Rand.Int % 600); + if (ally.TryStartCustomDuty(custom)) + { + escorts.Add(ally); + } + if (comp.duties.curCustomDuty?.duty != duties.curCustomDuty?.duty) + { + count += 3; + } + else + { + count++; + } + } + return count == countTarget; + } + return false; + }; + Verse.GenClosest.RegionwiseBFSWorker(selPawn.Position, map, ThingRequest.ForGroup(ThingRequestGroup.Pawn), PathEndMode.InteractionCell, TraverseParms.For(selPawn), validator, null, 1, 10, 40, out int _); + } + } + } + } + + private static int GetEnemyAttackTargetId(Thing enemy) + { + if (!TKVCache.TryGet(enemy.thingIDNumber, out int attackTarget, 15) || attackTarget == -1) + { + Verb enemyVerb = enemy.TryGetAttackVerb(); + if (enemyVerb == null || enemyVerb is Verb_CastPsycast || enemyVerb is Verb_CastAbility) + { + attackTarget = -1; + } + else if (!enemyVerb.IsMeleeAttack && enemyVerb.currentTarget is { IsValid: true, HasThing: true } && ((enemyVerb.WarmingUp && enemyVerb.WarmupTicksLeft < 45) || enemyVerb.Bursting)) + { + attackTarget = enemyVerb.currentTarget.Thing.thingIDNumber; + } + else if (enemyVerb.IsMeleeAttack && enemy is Pawn enemyPawn && enemyPawn.CurJobDef.Is(JobDefOf.AttackMelee) && enemyPawn.CurJob.targetA.IsValid) + { + attackTarget = enemyPawn.CurJob.targetA.Thing.thingIDNumber; + } + else + { + attackTarget = -1; + } + TKVCache.Put(enemy.thingIDNumber, attackTarget); + } + return attackTarget; + } + + #region TimeStamps + + /// + /// When the last injury occured/damage. + /// + private int lastTookDamage; + /// + /// When the last scan occured. SightGrid is responisble for these scan cycles. + /// + private int lastScanned; + /// + /// When did this comp last interupt the parent pawn. IE: reacted, retreated, etc. + /// + private int lastInterupted; + /// + /// When the pawn was last order to retreat by CAI. + /// + private int lastRetreated; + /// + /// Last tick any enemies where reported in a scan. + /// + private int lastSawEnemies; + /// + /// The general direction of enemies last time the pawn reacted. + /// + private Vector2 prevEnemyDir = Vector2.zero; + /// + /// Tick when this pawn was released as an escort. + /// + private int releasedTick; + + #endregion #if DEBUG_REACTION - /* + /* * Debug only vars. */ - public override void DrawGUIOverlay() - { - if (Finder.Settings.Debug && Finder.Settings.Debug_ValidateSight && parent is Pawn pawn) - { - base.DrawGUIOverlay(); - Verb verb = pawn.CurrentEffectiveVerb; - float sightRange = Maths.Min(SightUtility.GetSightRadius(pawn).scan, !verb.IsMeleeAttack ? verb.EffectiveRange : 15); - float sightRangeSqr = sightRange * sightRange; - if (sightRange != 0 && verb != null) - { - Vector3 drawPos = pawn.DrawPos; - IntVec3 shiftedPos = PawnPathUtility.GetMovingShiftedPosition(pawn, 30); - List nearbyVisiblePawns = pawn.Position.ThingsInRange(pawn.Map, TrackedThingsRequestCategory.Pawns, sightRange) - .Select(t => t as Pawn) - .Where(p => !p.Dead && !p.Downed && PawnPathUtility.GetMovingShiftedPosition(p, 60).DistanceToSquared(shiftedPos) < sightRangeSqr && verb.CanHitTargetFrom(shiftedPos, PawnPathUtility.GetMovingShiftedPosition(p, 60)) && p.HostileTo(pawn)) - .ToList(); - GUIUtility.ExecuteSafeGUIAction(() => - { - Vector2 drawPosUI = drawPos.MapToUIPosition(); - Text.Font = GameFont.Tiny; - string state = GenTicks.TicksGame - lastInterupted > 120 ? "O" : "X"; - Widgets.Label(new Rect(drawPosUI.x - 25, drawPosUI.y - 15, 50, 30), $"{state}/{_visibleEnemies.Count}:{_last}:{data.AllEnemies.Count}:{data.NumAllies}"); - }); - bool bugged = nearbyVisiblePawns.Count != _visibleEnemies.Count; - if (bugged) - { - Rect rect; - Vector2 a = drawPos.MapToUIPosition(); - Vector2 b; - Vector2 mid; - foreach (Pawn other in nearbyVisiblePawns.Where(p => !_visibleEnemies.Contains(p))) - { - b = other.DrawPos.MapToUIPosition(); - Widgets.DrawLine(a, b, Color.red, 1); + public override void DrawGUIOverlay() + { + if (Finder.Settings.Debug && Finder.Settings.Debug_ValidateSight && parent is Pawn pawn) + { + base.DrawGUIOverlay(); + Verb verb = pawn.CurrentEffectiveVerb; + float sightRange = Maths.Min(SightUtility.GetSightRadius(pawn).scan, !verb.IsMeleeAttack ? verb.EffectiveRange : 15); + float sightRangeSqr = sightRange * sightRange; + if (sightRange != 0 && verb != null) + { + Vector3 drawPos = pawn.DrawPos; + IntVec3 shiftedPos = PawnPathUtility.GetMovingShiftedPosition(pawn, 30); + List nearbyVisiblePawns = pawn.Position.ThingsInRange(pawn.Map, TrackedThingsRequestCategory.Pawns, sightRange) + .Select(t => t as Pawn) + .Where(p => !p.Dead && !p.Downed && PawnPathUtility.GetMovingShiftedPosition(p, 60).DistanceToSquared(shiftedPos) < sightRangeSqr && verb.CanHitTargetFrom(shiftedPos, PawnPathUtility.GetMovingShiftedPosition(p, 60)) && p.HostileTo(pawn)) + .ToList(); + GUIUtility.ExecuteSafeGUIAction(() => + { + Vector2 drawPosUI = drawPos.MapToUIPosition(); + Text.Font = GameFont.Tiny; + string state = GenTicks.TicksGame - lastInterupted > 120 ? "O" : "X"; + Widgets.Label(new Rect(drawPosUI.x - 25, drawPosUI.y - 15, 50, 30), $"{state}/{_visibleEnemies.Count}:{_last}:{data.AllEnemies.Count}:{data.NumAllies}:{data.BeingTargetedBy.Count}"); + }); + bool bugged = nearbyVisiblePawns.Count != _visibleEnemies.Count; + Vector2 a = drawPos.MapToUIPosition(); + if (bugged) + { + Rect rect; + Vector2 b; + Vector2 mid; + foreach (Pawn other in nearbyVisiblePawns.Where(p => !_visibleEnemies.Contains(p))) + { + b = other.DrawPos.MapToUIPosition(); + Widgets.DrawLine(a, b, Color.red, 1); + + mid = (a + b) / 2; + rect = new Rect(mid.x - 25, mid.y - 15, 50, 30); + Widgets.DrawBoxSolid(rect, new Color(0.2f, 0.2f, 0.2f, 0.8f)); + Widgets.DrawBox(rect); + Widgets.Label(rect, $"Errored. {Math.Round(other.Position.DistanceTo(pawn.Position), 1)}"); + } + } + bool selected = Find.Selector.SelectedPawns.Contains(pawn); + if (bugged || selected) + { + GenDraw.DrawRadiusRing(pawn.Position, sightRange); + } + if (selected) + { + for (int i = 1; i < _path.Count; i++) + { + Widgets.DrawBoxSolid(new Rect((_path[i - 1].ToVector3().Yto0() + new Vector3(0.5f, 0, 0.5f)).MapToUIPosition() - new Vector2(5, 5), new Vector2(10, 10)), _colors[i]); + Widgets.DrawLine((_path[i - 1].ToVector3().Yto0() + new Vector3(0.5f, 0, 0.5f)).MapToUIPosition(), (_path[i].ToVector3().Yto0() + new Vector3(0.5f, 0, 0.5f)).MapToUIPosition(), Color.white, 1); + } + if (_path.Count > 0) + { + Vector2 v = pawn.DrawPos.Yto0().MapToUIPosition(); + Widgets.DrawLine((_path.Last().ToVector3().Yto0() + new Vector3(0.5f, 0, 0.5f)).MapToUIPosition(), v, _colors.Last(), 1); + Widgets.DrawBoxSolid(new Rect(v - new Vector2(5, 5), new Vector2(10, 10)), _colors.Last()); + } +// int index = 0; + foreach (AIEnvAgentInfo ally in data.AllAllies) + { + if (ally.thing != null) + { + Vector2 b = ally.thing.DrawPos.MapToUIPosition(); + Widgets.DrawLine(a, b, Color.green, 1); + + Vector2 mid = (a + b) / 2; + Rect rect = new Rect(mid.x - 25, mid.y - 15, 50, 30); + Widgets.DrawBoxSolid(rect, new Color(0.2f, 0.2f, 0.2f, 0.8f)); + Widgets.DrawBox(rect); + DamageReport report = DamageUtility.GetDamageReport(ally.thing); + if (report.IsValid) + { + Widgets.Label(rect, $"{Math.Round(report.SimulatedDamage(armor), 2)}"); + } + } + } + AIEnvThings enemies = data.AllEnemies; + foreach (AIEnvAgentInfo enemy in enemies) + { + if (enemy.thing != null) + { + Vector2 b = enemy.thing.DrawPos.MapToUIPosition(); + Widgets.DrawLine(a, b, Color.yellow, 1); - mid = (a + b) / 2; - rect = new Rect(mid.x - 25, mid.y - 15, 50, 30); - Widgets.DrawBoxSolid(rect, new Color(0.2f, 0.2f, 0.2f, 0.8f)); - Widgets.DrawBox(rect); - Widgets.Label(rect, $"Errored. {Math.Round(other.Position.DistanceTo(pawn.Position), 1)}"); - } - } - bool selected = Find.Selector.SelectedPawns.Contains(pawn); - if (bugged || selected) - { - GenDraw.DrawRadiusRing(pawn.Position, sightRange); - } - if (selected) - { - for (int i = 1; i < _path.Count; i++) - { - Widgets.DrawBoxSolid(new Rect((_path[i - 1].ToVector3().Yto0() + new Vector3(0.5f, 0, 0.5f)).MapToUIPosition() - new Vector2(5, 5), new Vector2(10, 10)), _colors[i]); - Widgets.DrawLine((_path[i - 1].ToVector3().Yto0() + new Vector3(0.5f, 0, 0.5f)).MapToUIPosition(), (_path[i].ToVector3().Yto0() + new Vector3(0.5f, 0, 0.5f)).MapToUIPosition(), Color.white, 1); - } - if (_path.Count > 0) - { - Vector2 v = pawn.DrawPos.Yto0().MapToUIPosition(); - Widgets.DrawLine((_path.Last().ToVector3().Yto0() + new Vector3(0.5f, 0, 0.5f)).MapToUIPosition(), v, _colors.Last(), 1); - Widgets.DrawBoxSolid(new Rect(v - new Vector2(5, 5), new Vector2(10, 10)), _colors.Last()); - } - if (!_visibleEnemies.EnumerableNullOrEmpty()) - { - Vector2 a = pawn.DrawPos.MapToUIPosition(); - Vector2 b; - Vector2 mid; - Rect rect; - int index = 0; - foreach (Pawn other in _visibleEnemies) - { - b = other.DrawPos.MapToUIPosition(); - Widgets.DrawLine(a, b, Color.blue, 1); + Vector2 mid = (a + b) / 2; + Rect rect = new Rect(mid.x - 25, mid.y - 15, 50, 30); + Widgets.DrawBoxSolid(rect, new Color(0.2f, 0.2f, 0.2f, 0.8f)); + Widgets.DrawBox(rect); + DamageReport report = DamageUtility.GetDamageReport(enemy.thing); + if (report.IsValid) + { + Widgets.Label(rect, $"{Math.Round(report.SimulatedDamage(armor), 2)}"); + } + } + } + foreach (Thing enemy in data.BeingTargetedBy) + { + if (enemy != null && enemy.TryGetAttackVerb() is Verb enemyVerb && GetEnemyAttackTargetId(enemy) == selPawn.thingIDNumber) + { + Vector2 b = enemy.DrawPos.MapToUIPosition(); + Ray2D ray = new Ray2D(a, b - a); + float dist = Vector2.Distance(a, b); + if (dist > 0) + { + for (int i = 1; i < dist; i++) + { + Widgets.DrawLine(ray.GetPoint(i - 1), ray.GetPoint(i), i % 2 == 1 ? Color.black : Color.magenta, 2); + } + Vector2 mid = (a + b) / 2; + Rect rect = new Rect(mid.x - 25, mid.y - 15, 50, 30); + Widgets.DrawBoxSolid(rect, new Color(0.2f, 0.2f, 0.2f, 0.8f)); + Widgets.DrawBox(rect); + DamageReport report = DamageUtility.GetDamageReport(enemy); + if (report.IsValid) + { + Widgets.Label(rect, $"{Math.Round(report.SimulatedDamage(armor), 2)}"); + } + } + } + } + } + } + } + } - mid = (a + b) / 2; - rect = new Rect(mid.x - 25, mid.y - 15, 50, 30); - Widgets.DrawBoxSolid(rect, new Color(0.2f, 0.2f, 0.2f, 0.8f)); - Widgets.DrawBox(rect); - Widgets.Label(rect, $"({index++}). {Math.Round(other.Position.DistanceTo(pawn.Position), 1)}"); - } - } - } - } - } - } - private readonly HashSet _visibleEnemies = new HashSet(); - private readonly List _path = new List(); - private readonly List _colors = new List(); + private readonly HashSet _visibleEnemies = new HashSet(); + private readonly List _path = new List(); + private readonly List _colors = new List(); #endif - private int _last; - private Thing _bestEnemy; - } + } } diff --git a/Source/Rule56/CoverPositionFinder.cs b/Source/Rule56/CoverPositionFinder.cs index 5bbc804..942b33d 100644 --- a/Source/Rule56/CoverPositionFinder.cs +++ b/Source/Rule56/CoverPositionFinder.cs @@ -7,8 +7,52 @@ namespace CombatAI { + [StaticConstructorOnStartup] public static class CoverPositionFinder { + private static readonly CellMetrics metric_cover = new CellMetrics(); + private static readonly CellMetrics metric_coverPath = new CellMetrics(); + private static readonly CellMetrics metric_retreat = new CellMetrics(); + private static readonly CellMetrics metric_retreatPath = new CellMetrics(); + private static readonly CellMetrics metric_duck = new CellMetrics(); + private static readonly CellMetrics metric_duckPath = new CellMetrics(); + + static CoverPositionFinder() + { + // covering? + + metric_cover.Add("visibilityEnemies", ((reader, cell) => reader.GetVisibilityToEnemies(cell)), 0.25f); + metric_cover.Add("threat", ((reader, cell) => reader.GetThreat(cell)), 0.25f); + + metric_coverPath.Add("visibilityEnemies", ((reader, cell) => reader.GetVisibilityToEnemies(cell))); + metric_coverPath.Add("dir", (reader, cell) => Maths.Sqrt_Fast(Mathf.CeilToInt(reader.GetEnemyDirection(cell).SqrMagnitude()), 3), -1); + metric_coverPath.Add("traverse", (map, cell) => (cell.GetEdifice(map)?.def.pathCost / 22f ?? 0) + (cell.GetTerrain(map)?.pathCost / 22f ?? 0), 1, false); + metric_coverPath.Add("visibilityFriendlies", ((reader, cell) => reader.GetVisibilityToFriendlies(cell)), -0.05f); + + // retreating + + metric_retreat.Add("visibilityEnemies", ((reader, cell) => reader.GetVisibilityToEnemies(cell)), 0.25f); + metric_retreat.Add("threat", ((reader, cell) => reader.GetThreat(cell)), 0.25f); + metric_retreat.Add("visibilityFriendlies", ((reader, cell) => reader.GetVisibilityToFriendlies(cell)), -0.10f); + + metric_retreatPath.Add("visibilityEnemies", ((reader, cell) => reader.GetVisibilityToEnemies(cell)), 1); + metric_retreatPath.Add("dir", (reader, cell) => Maths.Sqrt_Fast(Mathf.CeilToInt(reader.GetEnemyDirection(cell).SqrMagnitude()), 3), -1); + metric_retreatPath.Add("traverse", (map, cell) => (cell.GetEdifice(map)?.def.pathCost / 22f ?? 0) + (cell.GetTerrain(map)?.pathCost / 22f ?? 0), 1, false); + metric_retreatPath.Add("visibilityFriendlies", ((reader, cell) => reader.GetVisibilityToFriendlies(cell)), -0.05f); + metric_retreatPath.Add("danger", ((reader, cell) => reader.GetDanger(cell)), 0.05f); + + // ducking + + metric_duck.Add("visibilityEnemies", ((reader, cell) => reader.GetVisibilityToEnemies(cell)), 0.25f); + metric_duck.Add("threat", ((reader, cell) => reader.GetThreat(cell)), 0.25f); + + metric_duckPath.Add("visibilityEnemies", ((reader, cell) => reader.GetVisibilityToEnemies(cell)), 1); + metric_duckPath.Add("dir", (reader, cell) => Maths.Sqrt_Fast(Mathf.CeilToInt(reader.GetEnemyDirection(cell).SqrMagnitude()), 3), -1); + metric_duckPath.Add("traverse", (map, cell) => (cell.GetEdifice(map)?.def.pathCost / 22f ?? 0) + (cell.GetTerrain(map)?.pathCost / 22f ?? 0), 1, false); + metric_duckPath.Add("visibilityFriendlies", ((reader, cell) => reader.GetVisibilityToFriendlies(cell)), -0.05f); + } + + private static readonly List> enemyVerbs = new List>(); private static readonly Dictionary parentTree = new Dictionary(512); private static readonly Dictionary scores = new Dictionary(512); @@ -33,19 +77,35 @@ public static bool TryFindCoverPosition(CoverPositionRequest request, out IntVec { request.maxRangeFromLocus = float.MaxValue; } + enemyVerbs.Clear(); + int enemiesWarmingUp = 0; + if (request.majorThreats != null) + { + for (int i = 0; i < request.majorThreats.Count; i++) + { + Verb verb = request.majorThreats[i].TryGetAttackVerb(); + if (verb != null && !verb.IsMeleeAttack) + { + enemyVerbs.Add(GetCanHitTargetFunc(request.majorThreats[i], verb)); + if (verb.WarmingUp || verb.Bursting) + { + enemiesWarmingUp += 1; + } + } + } + } + metric_cover.Begin(map, sightReader, avoidanceReader, request.locus); + metric_coverPath.Begin(map, sightReader, avoidanceReader, request.locus); IntVec3 dutyDest = caster.TryGetNextDutyDest(request.maxRangeFromCaster); InterceptorTracker interceptors = map.GetComp_Fast().interceptors; float maxDistSqr = request.maxRangeFromLocus * request.maxRangeFromLocus; CellFlooder flooder = map.GetCellFlooder(); IntVec3 enemyLoc = request.target.Cell; IntVec3 bestCell = IntVec3.Invalid; - float rootVis = sightReader.GetVisibilityToEnemies(request.locus); - float rootThreat = sightReader.GetThreat(request.locus); float bestCellVisibility = 1e8f; float bestCellScore = 1e8f; float effectiveRange = request.verb != null && request.verb.EffectiveRange > 0 ? request.verb.EffectiveRange * 0.8f : -1; float rootDutyDestDist = dutyDest.IsValid ? dutyDest.DistanceTo(caster.Position) : -1; - bool tpsLow = Finder.Performance.TpsCriticallyLow; flooder.Flood(request.locus, node => { @@ -53,7 +113,11 @@ public static bool TryFindCoverPosition(CoverPositionRequest request, out IntVec { return; } - float c = (node.dist - node.distAbs) / (node.distAbs + 1f) * 2 - interceptors.grid.Get(node.cell) * 2 + (sightReader.GetThreat(node.cell) - rootThreat) * 0.25f; + float c = (node.dist - node.distAbs) / (node.distAbs + 1f) * 2 - interceptors.grid.Get(node.cell) * 2 + metric_cover.Score(node.cell); + if (node.cell == request.locus) + { + c += enemiesWarmingUp / 10f; + } if (rootDutyDestDist > 0) { c += Mathf.Clamp((Maths.Sqrt_Fast(dutyDest.DistanceToSquared(node.cell), 3) - rootDutyDestDist) * 0.25f, -1f, 1f); @@ -62,6 +126,16 @@ public static bool TryFindCoverPosition(CoverPositionRequest request, out IntVec { c += 2f * Mathf.Abs(effectiveRange - Maths.Sqrt_Fast(node.cell.DistanceToSquared(enemyLoc), 5)) / effectiveRange; } + if (enemyVerbs.Count > 0) + { + for (int i = 0; i < enemyVerbs.Count; i++) + { + if (enemyVerbs[i](node.cell)) + { + c += 1; + } + } + } if (bestCellScore - c >= 0.05f) { float v = sightReader.GetVisibilityToEnemies(node.cell); @@ -76,10 +150,21 @@ public static bool TryFindCoverPosition(CoverPositionRequest request, out IntVec { callback(node.cell, c); } + if (c > 5000) + { + Log.Message($"cell {node.cell} has exploding val {c} {Maths.Sqrt_Fast(dutyDest.DistanceToSquared(node.cell), 3)} {Maths.Sqrt_Fast(node.cell.DistanceToSquared(enemyLoc), 3)}, "); + metric_cover.Print(node.cell); + } }, cell => { - return (cell.GetEdifice(map)?.def.pathCost / 22f ?? 0) + (cell.GetTerrain(map)?.pathCost / 22f ?? 0) + (sightReader.GetVisibilityToEnemies(cell) - rootVis) * 2 - interceptors.grid.Get(cell); + float c = metric_coverPath.Score(cell); + if (c > 5000) + { + Log.Message($"cell path {cell} has exploding val {c}"); + metric_coverPath.Print(cell); + } + return c - interceptors.grid.Get(cell); }, cell => { @@ -112,22 +197,35 @@ public static bool TryFindRetreatPosition(CoverPositionRequest request, out IntV { request.maxRangeFromLocus = float.MaxValue; } + int enemiesWarmingUp = 0; + if (request.majorThreats != null) + { + for (int i = 0; i < request.majorThreats.Count; i++) + { + Verb verb = request.majorThreats[i].TryGetAttackVerb(); + if (verb != null && !verb.IsMeleeAttack) + { + enemyVerbs.Add(GetCanHitTargetFunc(request.majorThreats[i], verb)); + if (verb.WarmingUp || verb.Bursting) + { + enemiesWarmingUp += 1; + } + } + } + } parentTree.Clear(); scores.Clear(); - IntVec3 dutyDest = caster.TryGetNextDutyDest(request.maxRangeFromCaster); - float rootDutyDestDist = dutyDest.IsValid ? dutyDest.DistanceTo(caster.Position) : -1; - + metric_retreat.Begin(map, sightReader, avoidanceReader, request.locus); + metric_retreatPath.Begin(map, sightReader, avoidanceReader, request.locus); + IntVec3 dutyDest = caster.TryGetNextDutyDest(request.maxRangeFromCaster); + float rootDutyDestDist = dutyDest.IsValid ? dutyDest.DistanceTo(request.locus) : -1; + IntVec3 enemyLoc = request.target.Cell; InterceptorTracker interceptors = map.GetComp_Fast().interceptors; - CellIndices indices = map.cellIndices; float adjustedMaxDist = request.maxRangeFromLocus * 2; float adjustedMaxDistSqr = adjustedMaxDist * adjustedMaxDist; CellFlooder flooder = map.GetCellFlooder(); - IntVec3 enemyLoc = request.target.Cell; IntVec3 bestCell = IntVec3.Invalid; - float rootVis = sightReader.GetVisibilityToEnemies(request.locus); - float rootVisFriendlies = sightReader.GetVisibilityToFriendlies(request.locus); - float rootThreat = sightReader.GetThreat(request.locus); - float bestCellDist = request.locus.DistanceToSquared(enemyLoc); + float bestCellDist = 0; float bestCellScore = 1e8f; flooder.Flood(request.locus, node => @@ -139,14 +237,19 @@ public static bool TryFindRetreatPosition(CoverPositionRequest request, out IntV return; } // do math - float c = (node.dist - node.distAbs) / (node.distAbs + 1f) * 2 + avoidanceReader.GetProximity(node.cell) * 0.5f - interceptors.grid.Get(node.cell) + (sightReader.GetThreat(node.cell) - rootThreat) * 0.75f; + float c = (node.dist - node.distAbs) / (node.distAbs + 1f) - interceptors.grid.Get(node.cell) + metric_retreat.Score(node.cell); + // check for blocked line of sight with major threats. + if (node.cell == request.locus) + { + c += enemiesWarmingUp / 10f; + } if (rootDutyDestDist > 0) { - c += Mathf.Clamp((Maths.Sqrt_Fast(dutyDest.DistanceToSquared(node.cell), 5) - rootDutyDestDist) * 0.25f, -1f, 1f); + c += Mathf.Clamp((Maths.Sqrt_Fast(dutyDest.DistanceToSquared(node.cell), 5) - rootDutyDestDist) * 0.25f, -0.5f, 0.5f); } + float d = node.cell.DistanceToSquared(enemyLoc); if (bestCellScore - c >= 0.05f) { - float d = node.cell.DistanceToSquared(enemyLoc); if (d > bestCellDist) { bestCellScore = c; @@ -162,7 +265,18 @@ public static bool TryFindRetreatPosition(CoverPositionRequest request, out IntV }, cell => { - return (cell.GetEdifice(map)?.def.pathCost / 22f ?? 0) + (cell.GetTerrain(map)?.pathCost / 22f ?? 0) + (sightReader.GetVisibilityToEnemies(cell) - rootVis) * 2 - (rootVisFriendlies - sightReader.GetVisibilityToFriendlies(cell)) - interceptors.grid.Get(cell) + (sightReader.GetThreat(cell) - rootThreat) * 0.25f; + float cost = metric_retreatPath.Score(cell) - interceptors.grid.Get(cell); + if (enemyVerbs.Count > 0) + { + for (int i = 0; i < enemyVerbs.Count; i++) + { + if (enemyVerbs[i](cell)) + { + cost += 1.0f; + } + } + } + return cost; }, cell => { @@ -188,5 +302,120 @@ public static bool TryFindRetreatPosition(CoverPositionRequest request, out IntV coverCell = bestCell; return bestCell.IsValid; } + + public static bool TryFindDuckPosition(CoverPositionRequest request, out IntVec3 coverCell, Action callback = null) + { + request.caster.TryGetSightReader(out SightReader sightReader); + request.caster.TryGetAvoidanceReader(out AvoidanceReader avoidanceReader); + if (sightReader == null || avoidanceReader == null) + { + coverCell = IntVec3.Invalid; + return false; + } + Map map = request.caster.Map; + Pawn caster = request.caster; + sightReader.armor = caster.GetArmorReport(); + if (request.locus == IntVec3.Zero) + { + request.locus = request.caster.Position; + request.maxRangeFromLocus = request.maxRangeFromCaster; + } + if (request.maxRangeFromLocus == 0) + { + request.maxRangeFromLocus = float.MaxValue; + } + enemyVerbs.Clear(); + int enemiesWarmingUp = 0; + if (request.majorThreats != null) + { + for (int i = 0; i < request.majorThreats.Count; i++) + { + Verb verb = request.majorThreats[i].TryGetAttackVerb(); + if (verb != null && !verb.IsMeleeAttack) + { + enemyVerbs.Add(GetCanHitTargetFunc(request.majorThreats[i], verb)); + if (verb.WarmingUp || verb.Bursting) + { + enemiesWarmingUp += 1; + } + } + } + } + if (enemyVerbs.Count == 0) + { + Log.Warning("TryFindDuckPosition got no major threats nor a target thing. Locs num is 0."); + coverCell = IntVec3.Invalid; + return false; + } + metric_duck.Begin(map, sightReader, avoidanceReader, request.locus); + metric_duckPath.Begin(map, sightReader, avoidanceReader, request.locus); + IntVec3 dutyDest = caster.TryGetNextDutyDest(request.maxRangeFromCaster); + float rootDutyDestDist = dutyDest.IsValid ? dutyDest.DistanceTo(request.locus) : -1; + InterceptorTracker interceptors = map.GetComp_Fast().interceptors; + float maxDistSqr = request.maxRangeFromLocus * request.maxRangeFromLocus; + CellFlooder flooder = map.GetCellFlooder(); + IntVec3 bestCell = IntVec3.Invalid; + float bestCellScore = 1e8f; + float rootVis = sightReader.GetVisibilityToEnemies(request.locus); + float rootThreat = sightReader.GetThreat(request.locus); + int bestVisibleTo = 256; + flooder.Flood(request.locus, + node => + { + if (maxDistSqr < request.locus.DistanceToSquared(node.cell) || !map.reservationManager.CanReserve(caster, node.cell)) + { + return; + } + float c = (node.dist - node.distAbs) / (node.distAbs + 1f) - interceptors.grid.Get(node.cell) * 2 + metric_duck.Score(node.cell); + // check for blocked line of sight with major threats. + int visibleTo = 0; + for (int i = 0; i < enemyVerbs.Count; i++) + { + if (enemyVerbs[i](node.cell)) + { + c += 1; + visibleTo++; + } + } + if (node.cell == request.locus) + { + c += enemiesWarmingUp / 5f; + } + if (rootDutyDestDist > 0) + { + c += Mathf.Clamp((Maths.Sqrt_Fast(dutyDest.DistanceToSquared(node.cell), 5) - rootDutyDestDist) * 0.25f, -1f, 1f); + } + if (bestCellScore - c >= 0.05f) + { + if (visibleTo <= bestVisibleTo) + { + bestCellScore = c; + bestCell = node.cell; + bestVisibleTo = visibleTo; + } + } + if (callback != null) + { + callback(node.cell, c); + } + }, + cell => + { + return metric_duckPath.Score(cell) - interceptors.grid.Get(cell); + }, + cell => + { + return (request.validator == null || request.validator(cell)) && cell.WalkableBy(map, caster); + }, + (int)Maths.Min(request.maxRangeFromLocus, 30) + ); + coverCell = bestCell; + return bestCell.IsValid && bestVisibleTo == 0; + } + + private static Func GetCanHitTargetFunc(Thing thing, Verb enemyVerb) + { + return cell => enemyVerb.CanHitTarget(cell); + } } } diff --git a/Source/Rule56/CoverPositionRequest.cs b/Source/Rule56/CoverPositionRequest.cs index 69dd11b..94549e7 100644 --- a/Source/Rule56/CoverPositionRequest.cs +++ b/Source/Rule56/CoverPositionRequest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Verse; namespace CombatAI { @@ -19,5 +20,7 @@ public struct CoverPositionRequest public bool checkBlockChance; public Func validator; + + public List majorThreats; } } diff --git a/Source/Rule56/CustomDuties/CustomDutyUtility.cs b/Source/Rule56/CustomDuties/CustomDutyUtility.cs index 92e7278..e3186b4 100644 --- a/Source/Rule56/CustomDuties/CustomDutyUtility.cs +++ b/Source/Rule56/CustomDuties/CustomDutyUtility.cs @@ -54,7 +54,7 @@ public static Pawn_CustomDutyTracker.CustomPawnDuty Escort(Pawn escortee, int ra { Pawn_CustomDutyTracker.CustomPawnDuty custom = new Pawn_CustomDutyTracker.CustomPawnDuty { - duty = new PawnDuty(DutyDefOf.Escort, escortee, radius) + duty = new PawnDuty(CombatAI_DutyDefOf.CombatAI_Escort, escortee, radius) { locomotion = LocomotionUrgency.Sprint }, @@ -73,7 +73,7 @@ public static Pawn_CustomDutyTracker.CustomPawnDuty AssaultPoint(IntVec3 dest, i { Pawn_CustomDutyTracker.CustomPawnDuty custom = new Pawn_CustomDutyTracker.CustomPawnDuty { - duty = new PawnDuty(DutyDefOf.Defend, dest, switchAssaultRadius) + duty = new PawnDuty(CombatAI_DutyDefOf.CombatAI_AssaultPoint, dest, switchAssaultRadius) { locomotion = LocomotionUrgency.Sprint }, @@ -98,5 +98,40 @@ public static Pawn_CustomDutyTracker.CustomPawnDuty DefendPoint(IntVec3 dest, in }; return custom; } + + public static Pawn_CustomDutyTracker.CustomPawnDuty HuntDownEnemies(LocalTargetInfo enemy, IntVec3 fallbackPosition, int maxDist, int expireAfter = 0, int startAfter = 0) + { + PawnDuty duty = new PawnDuty(DutyDefOf.HuntEnemiesIndividual) + { + locomotion = LocomotionUrgency.Sprint, + focus = enemy, + focusSecond = fallbackPosition, + }; + Pawn_CustomDutyTracker.CustomPawnDuty custom = new Pawn_CustomDutyTracker.CustomPawnDuty + { + duty = duty, + expireAfter = expireAfter, + startAfter = startAfter, + endOnFocusDowned = true, + endOnDistToFocusLarger = maxDist, + }; + return custom; + } + + public static Pawn_CustomDutyTracker.CustomPawnDuty HuntDownEnemies(IntVec3 fallbackPosition, int expireAfter = 0, int startAfter = 0) + { + PawnDuty duty = new PawnDuty(DutyDefOf.HuntEnemiesIndividual) + { + locomotion = LocomotionUrgency.Sprint, + focusSecond = fallbackPosition, + }; + Pawn_CustomDutyTracker.CustomPawnDuty custom = new Pawn_CustomDutyTracker.CustomPawnDuty + { + duty = duty, + expireAfter = expireAfter, + startAfter = startAfter, + }; + return custom; + } } } diff --git a/Source/Rule56/DamageReport.cs b/Source/Rule56/DamageReport.cs index df8181a..f5d9817 100644 --- a/Source/Rule56/DamageReport.cs +++ b/Source/Rule56/DamageReport.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using RimWorld; +using UnityEngine; using Verse; namespace CombatAI { @@ -71,8 +72,10 @@ public struct DamageReport /// public VerbProperties primaryVerbProps; /// - /// Whether this is valid report or not. + /// Damage def for the primary attack verb. /// + public DamageDef primaryVerbDamageDef; + public bool IsValid { get => _finalized && GenTicks.TicksGame - _createdAt < 1800; @@ -138,18 +141,16 @@ public void AddVerb(Verb verb) attributes |= MetaCombatAttribute.AOELarge; } } - float warmupTime = Maths.Max(verb.verbProps.warmupTime, 0.5f); - float burstShotCount = verb.verbProps.burstShotCount; - float output = 1f / warmupTime * burstShotCount; + float burstShotCount = Mathf.Clamp(verb.verbProps.burstShotCount * 0.66f, 0.75f, 3f); if (projectile.damageDef.armorCategory == DamageArmorCategoryDefOf.Sharp) { - rangedSharp = (rangedSharp + output * projectile.damageAmountBase) / 2f; - rangedSharpAp = rangedSharpAp != 0 ? (rangedSharpAp + Maths.Max(GetArmorPenetration(projectile), 0f)) / 2f : Maths.Max(GetArmorPenetration(projectile), 0f); + rangedSharp = Maths.Max(projectile.damageAmountBase * burstShotCount, rangedSharp);; + rangedSharpAp = Maths.Max(GetArmorPenetration(projectile), rangedSharpAp); } else { - rangedBlunt = (rangedBlunt + output * projectile.damageAmountBase) / 2f; - rangedBluntAp = rangedBluntAp != 0 ? (rangedBluntAp + Maths.Max(GetArmorPenetration(projectile), 0f)) / 2f : Maths.Max(GetArmorPenetration(projectile), 0f); + rangedBlunt = Maths.Max(projectile.damageAmountBase * burstShotCount, rangedBlunt); + rangedBluntAp = Maths.Max(GetArmorPenetration(projectile), rangedBluntAp); } } } @@ -191,6 +192,64 @@ public void AddTool(Tool tool) } } } + + public float SimulatedDamage(ArmorReport armorReport, int iterations = 5) + { + float damage = 0f; +// bool hasWorkingShield = includeShields && armorReport.shield?.PawnOwner != null; + for (int i = 0; i < iterations; i++) + { + damage += SimulatedDamage_Internal(armorReport, (i + 1f) / (iterations + 2f)); +// if (!hasWorkingShield || armorReport.shield.Energy - damage * armorReport.shield.Props.energyLossPerDamage <= 0) +// { +// damage += temp; +// } + } + return damage / iterations; + } + + private float SimulatedDamage_Internal(ArmorReport armorReport, float roll) + { + DamageDef damageDef = primaryVerbDamageDef ?? (primaryIsRanged ? DamageDefOf.Bullet : DamageDefOf.Bullet); + DamageArmorCategoryDef category = damageDef?.armorCategory ?? DamageArmorCategoryDefOf.Sharp; + float damage = 0f; + float armorPen = 0f; + if (category == DamageArmorCategoryDefOf.Sharp) + { + Sharp(out damage, out armorPen); + } + else + { + Blunt(out damage, out armorPen); + } + ApplyDamage(ref damage, armorPen, armorReport.GetArmor(damageDef), ref damageDef, roll); + if (damage > 0.01f) + { + ApplyDamage(ref damage, armorPen, armorReport.GetBodyArmor(damageDef), ref damageDef, roll); + } + return damage; + } + + private void ApplyDamage(ref float damageAmount, float armorPenetration, float armorRating, ref DamageDef damageDef, float roll) + { + float pen = Mathf.Max(armorRating - armorPenetration, 0f); + float blocked = pen * 0.5f; + float reduced = pen; + if (roll < blocked) + { + // stopped. + damageAmount = 0f; + } + else if (roll < reduced) + { + // reduced enough to become blunt damage. + damageAmount = damageAmount / 2f; + if (damageDef.armorCategory == DamageArmorCategoryDefOf.Sharp) + { + damageDef = DamageDefOf.Blunt; + } + } + } private static float Adjust(float dmg, float ap) { @@ -205,6 +264,34 @@ private static float Adjust(float dmg, float ap) return dmg / 18f; } + private void Sharp(out float damage, out float ap) + { + if (primaryIsRanged) + { + damage = rangedSharp; + ap = rangedSharpAp; + } + else + { + damage = meleeSharp; + ap = meleeSharpAp; + } + } + + private void Blunt(out float damage, out float ap) + { + if (primaryIsRanged) + { + damage = rangedBlunt; + ap = rangedBluntAp; + } + else + { + damage = meleeBlunt; + ap = meleeBluntAp; + } + } + private float AdjustedSharp() { float damage; diff --git a/Source/Rule56/DamageUtility.cs b/Source/Rule56/DamageUtility.cs index b2d1657..e190074 100644 --- a/Source/Rule56/DamageUtility.cs +++ b/Source/Rule56/DamageUtility.cs @@ -75,7 +75,8 @@ public static DamageReport GetDamageReport(Thing thing, Listing_Collapsible coll { report.primaryIsRanged = true; } - report.primaryVerbProps = effectiveVerb.verbProps; + report.primaryVerbProps = effectiveVerb.verbProps; + report.primaryVerbDamageDef = effectiveVerb.GetDamageDef(); } float rangedMul = 1; float meleeMul = 1; @@ -114,7 +115,8 @@ record = pawn.skills.GetSkill(SkillDefOf.Melee); collapsible.Label($"r.melee mS:{Math.Round(report.meleeSharp, 2)}\tmB:{Math.Round(report.meleeBlunt, 2)}"); collapsible.Label($"r.melee mSAp:{Math.Round(report.meleeSharpAp, 2)}\tmBAp:{Math.Round(report.meleeBluntAp, 2)}"); } - report.primaryVerbProps = verb.verbProps; + report.primaryVerbProps = verb.verbProps; + report.primaryVerbDamageDef = verb.GetDamageDef(); } report.primaryIsRanged = true; report.Finalize(1, 1); @@ -152,6 +154,15 @@ public static float ThreatTo(this DamageReport damage, ArmorReport armor) } return Mathf.Clamp01(Maths.Max(damage.adjustedBlunt / (armor.Blunt + 1e-3f), damage.adjustedSharp / (armor.Sharp + 1e-3f), 0f)); } + + public static DamageDef GetDamageDef(this VerbProperties props) + { + if (props.LaunchesProjectile) + { + return props.defaultProjectile.projectile?.damageDef ?? null; + } + return props.meleeDamageDef; + } public static void ClearCache() { diff --git a/Source/Rule56/Data/AIAgentData.cs b/Source/Rule56/Data/AIAgentData.cs index 41744ff..bc8a9d4 100644 --- a/Source/Rule56/Data/AIAgentData.cs +++ b/Source/Rule56/Data/AIAgentData.cs @@ -1,18 +1,22 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; using Verse; namespace CombatAI { public class AIAgentData : IExposable { + private List _targetedBy = new List(4); /* Fields * --------------------------------------------------------- */ #region Fields - - private AIEnvThings enemies; - private AIEnvThings allies; + + private List> targetedBy; + private AIEnvThings enemies; + private AIEnvThings allies; #endregion @@ -22,8 +26,15 @@ public class AIAgentData : IExposable public AIAgentData() { - enemies = new AIEnvThings(); - allies = new AIEnvThings(); + enemies = new AIEnvThings(); + allies = new AIEnvThings(); + targetedBy = new List>(); + } + + public int AgroSig + { + get; + set; } /* Timestamps @@ -32,12 +43,17 @@ public AIAgentData() #region Timestamps + public int LastSawEnemies + { + get; + set; + } public int LastTookDamage { get; set; } - public int lastRetreated + public int LastRetreated { get; set; @@ -52,6 +68,41 @@ public int LastScanned get; set; } + public int LastFailedSapping + { + get; + set; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool RetreatedRecently(int ticks) + { + return GenTicks.TicksGame - LastRetreated <= ticks; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool FailedSappingRecently(int ticks) + { + return GenTicks.TicksGame - LastFailedSapping <= ticks; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TookDamageRecently(int ticks) + { + return GenTicks.TicksGame - LastTookDamage <= ticks; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool InterruptedRecently(int ticks) + { + return GenTicks.TicksGame - LastInterrupted <= ticks; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ScannedRecently(int ticks) + { + return GenTicks.TicksGame - LastScanned <= ticks; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool SawEnemiesRecently(int ticks) + { + return GenTicks.TicksGame - LastSawEnemies <= ticks; + } #endregion @@ -60,7 +111,30 @@ public int LastScanned */ #region Spotting - + + public List BeingTargetedBy + { + get + { + bool cleanUp = false; + _targetedBy.Clear(); + for (int i = 0; i < targetedBy.Count; i++) + { + Pair pair = targetedBy[i]; + if (GenTicks.TicksGame - pair.second > 240) + { + cleanUp = true; + continue; + } + _targetedBy.Add(pair.First); + } + if (cleanUp) + { + targetedBy.RemoveAll(t => GenTicks.TicksGame - t.Second > 240); + } + return _targetedBy; + } + } public AIEnvThings AllEnemies { get => enemies.AsReadonly; @@ -78,6 +152,10 @@ public IEnumerator EnemiesNearBy() { return enemies.GetEnumerator(AIEnvAgentState.visible); } + public IEnumerator Enemies() + { + return enemies.GetEnumerator(AIEnvAgentState.unknown); + } public IEnumerator MeleeEnemiesNearBy() { return enemies.GetEnumerator(AIEnvAgentState.melee & AIEnvAgentState.melee); @@ -91,6 +169,11 @@ public void ReSetEnemies(HashSet items) enemies.ClearAndAddRange(items); NumEnemies = enemies.Count; } + public void ReSetEnemies(Dictionary dict) + { + enemies.ClearAndAddRange(dict); + NumEnemies = enemies.Count; + } public void ReSetEnemies() { enemies.Clear(); @@ -110,6 +193,10 @@ public IEnumerator AlliesNearBy() { return allies.GetEnumerator(AIEnvAgentState.nearby); } + public IEnumerator Allies() + { + return allies.GetEnumerator(AIEnvAgentState.unknown); + } public IEnumerator AlliesWhere(AIEnvAgentState customState) { return allies.GetEnumerator(customState); @@ -119,12 +206,23 @@ public void ReSetAllies(HashSet items) allies.ClearAndAddRange(items); NumAllies = allies.Count; } + public void ReSetAllies(Dictionary dict) + { + allies.ClearAndAddRange(dict); + NumAllies = allies.Count; + } public void ReSetAllies() { allies.Clear(); NumAllies = 0; } - + + public void BeingTargeted(Thing targeter) + { + targetedBy.RemoveAll(t => GenTicks.TicksGame - t.Second > 90 || t.First == targeter); + targetedBy.Add(new Pair(targeter, GenTicks.TicksGame)); + } + #endregion /* @@ -133,13 +231,18 @@ public void ReSetAllies() public void ExposeData() { -// int v = 0; -// Scribe_Deep.Look(ref enemies, $"EnvEnemies.{v}"); -// enemies ??= new AIEnvThings(); -// NumEnemies = enemies.Count; -// Scribe_Deep.Look(ref allies, $"EnvAllies.{v}"); -// allies ??= new AIEnvThings(); -// NumAllies = allies.Count; + if (Scribe.mode != LoadSaveMode.Saving) + { + List things = BeingTargetedBy; + Scribe_Collections.Look(ref things, "targetedBy.1", LookMode.Reference); + } + int sig = AgroSig; + Scribe_Values.Look(ref sig, "aggro.sig"); + AgroSig = sig; + Scribe_Deep.Look(ref enemies, "enemies.1"); + enemies ??= new AIEnvThings(); + Scribe_Deep.Look(ref allies, "allies.1"); + allies ??= new AIEnvThings(); } } } diff --git a/Source/Rule56/Data/AIEnvAgentInfo.cs b/Source/Rule56/Data/AIEnvAgentInfo.cs index 82d61fb..4d67b30 100644 --- a/Source/Rule56/Data/AIEnvAgentInfo.cs +++ b/Source/Rule56/Data/AIEnvAgentInfo.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using RimWorld.Planet; using Verse; namespace CombatAI { - public struct AIEnvAgentInfo : IExposable, IEquatable, IEquatable + public struct AIEnvAgentInfo : IEquatable, IEquatable { - public AIEnvAgentState state; - public Thing thing; + public AIEnvAgentState state; + public readonly Thing thing; public AIEnvAgentInfo(Thing thing, AIEnvAgentState state) { @@ -14,12 +15,23 @@ public AIEnvAgentInfo(Thing thing, AIEnvAgentState state) this.state = state; } - public void ExposeData() + public bool IsValid { - Scribe_References.Look(ref thing, "obsThing"); - Scribe_Values.Look(ref state, "obsAIAgentState"); + get => this.thing != null; } - + + public AIEnvAgentInfo Combine(AIEnvAgentInfo other) + { + if (other.thing != this.thing) + { + throw new InvalidOperationException("Both items must have the same parent thing"); + } + return new AIEnvAgentInfo(this.thing, this.state | other.state) + { + // TODO remember to copy and process any new fields here. + }; + } + public bool Equals(Thing other) { return other == thing; diff --git a/Source/Rule56/Data/AIEnvAgentState.cs b/Source/Rule56/Data/AIEnvAgentState.cs index 745a454..4b2e545 100644 --- a/Source/Rule56/Data/AIEnvAgentState.cs +++ b/Source/Rule56/Data/AIEnvAgentState.cs @@ -1,6 +1,6 @@ namespace CombatAI { - public enum AIEnvAgentState : int + public enum AIEnvAgentState : uint { unknown = 0, visible = 1, diff --git a/Source/Rule56/Data/AIEnvThings.cs b/Source/Rule56/Data/AIEnvThings.cs index 902d507..b9559bc 100644 --- a/Source/Rule56/Data/AIEnvThings.cs +++ b/Source/Rule56/Data/AIEnvThings.cs @@ -8,24 +8,25 @@ namespace CombatAI { public class AIEnvThings : ICollection, IExposable { - private const AIEnvAgentState invalidState = (AIEnvAgentState)(-1); - - private List things; + private const AIEnvAgentState invalidState = AIEnvAgentState.unknown; - public AIEnvThings() : this(1) - { - } + private readonly Dictionary stateByThing = new Dictionary(); + private readonly List elements = new List(); - public AIEnvThings(int alloc) + public AIEnvThings() { - things = new List(alloc); IsReadOnly = false; SyncRoot = new object(); } - private AIEnvThings(List things) + private AIEnvThings(List elements) { - this.things = things; + this.elements = elements; + } + + public AIEnvAgentInfo Random + { + get => elements[Rand.Int % elements.Count]; } public AIEnvThings AsReadonly @@ -36,7 +37,7 @@ public AIEnvThings AsReadonly { return this; } - AIEnvThings copy = new AIEnvThings(things); + AIEnvThings copy = new AIEnvThings(elements); copy.IsReadOnly = true; copy.SyncRoot = SyncRoot; return copy; @@ -52,45 +53,45 @@ public bool IsReadOnly public AIEnvAgentInfo this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => things[index]; + get => elements[index]; } - public AIEnvAgentState this[Thing thing] + public AIEnvAgentInfo this[Thing thing] { - get - { - for (int i = 0; i < things.Count; i++) - { - AIEnvAgentInfo temp = things[i]; - if (temp.thing == thing) - { - return temp.state; - } - } - return AIEnvAgentState.unknown; - } - set + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => stateByThing.TryGetValue(thing.thingIDNumber, out AIEnvAgentInfo store) ? store : new AIEnvAgentInfo(null, AIEnvAgentState.unknown); + private set { if (IsReadOnly) { throw new Exception("Collection is readonly"); } - for (int i = 0; i < things.Count; i++) + if (thing == null) + { + return; + } + if (stateByThing.ContainsKey(thing.thingIDNumber)) { - AIEnvAgentInfo temp = things[i]; - if (temp.thing == thing) + for (int i = 0; i < elements.Count; i++) { - things[i] = new AIEnvAgentInfo(thing, value); - return; + AIEnvAgentInfo temp = elements[i]; + if (temp.thing == thing) + { + elements[i] = value; + stateByThing[thing.thingIDNumber] = value; + return; + } } + throw new Exception($"AIEnvThings stateByThing contains key but the key is missing from things."); } - things.Add(new AIEnvAgentInfo(thing, value)); + elements.Add(value); + stateByThing[thing.thingIDNumber] = value; } } public int Count { - get => things.Count; + get => elements.Count; } public object SyncRoot @@ -111,92 +112,71 @@ IEnumerator IEnumerable.GetEnumerator() public void CopyTo(Array array, int index) { - for (int i = 0; i < things.Count; i++) + for (int i = 0; i < elements.Count; i++) { - array.SetValue(things[i], index + i); + array.SetValue(elements[i], index + i); } } public void ExposeData() { - if (Scribe.mode == LoadSaveMode.Saving) - { - things.RemoveAll(t => t.thing == null || t.thing.Destroyed); - } -// Scribe_Collections.Look(ref things, "collectionThings", LookMode.Deep); - if (Scribe.mode != LoadSaveMode.Saving) - { - things ??= new List(); - things.RemoveAll(t => t.thing == null || t.thing.Destroyed); - } +// if (Scribe.mode != LoadSaveMode.Saving) +// { +// Scribe_Collections.Look(ref elements, $"collectionThings", LookMode.Deep); +// } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(AIEnvAgentInfo item) + public void Clear() { if (IsReadOnly) { throw new Exception("Collection is readonly"); } - this[item.thing] = item.state; + elements.Clear(); + stateByThing.Clear(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(Thing thing, AIEnvAgentState state) + public void ClearAndAddRange(HashSet items) { if (IsReadOnly) { throw new Exception("Collection is readonly"); } - this[thing] = state; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Remove(AIEnvAgentInfo item) - { - if (IsReadOnly) + this.elements.Clear(); + this.elements.AddRange(items); + // update ids. + this.stateByThing.Clear(); + for (int i = 0; i < items.Count; i++) { - throw new Exception("Collection is readonly"); + this.stateByThing[this.elements[i].thing.thingIDNumber] = this.elements[i]; } - return things.Remove(item); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Remove(Thing thing) + public void ClearAndAddRange(Dictionary dict) { if (IsReadOnly) { throw new Exception("Collection is readonly"); } - return things.RemoveAll(i => i.thing == thing) > 0; - } - - public void Clear() - { - if (IsReadOnly) + this.elements.Clear(); + this.stateByThing.Clear(); + foreach (KeyValuePair pair in dict) { - throw new Exception("Collection is readonly"); + if (pair.Key != pair.Value.thing) + { + throw new InvalidOperationException("Key must match the value of AIEnvAgentInfo.Thing"); + } + elements.Add(pair.Value); + stateByThing[pair.Key.thingIDNumber] = pair.Value; } - things.Clear(); } - public void ClearAndAddRange(HashSet things) + public void ClearAndAddRange(List items) { if (IsReadOnly) { throw new Exception("Collection is readonly"); } - this.things.Clear(); - this.things.AddRange(things); - } - - public void ClearAndAddRange(List things) - { - if (IsReadOnly) - { - throw new Exception("Collection is readonly"); - } - this.ClearAndAddRange(things.ToHashSet()); + this.ClearAndAddRange(items.ToHashSet()); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -208,29 +188,22 @@ public bool Contains(AIEnvAgentInfo item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Contains(Thing item) { - for (int i = 0; i < things.Count; i++) - { - if ( things[i].thing == item) - { - return true; - } - } - return false; + return stateByThing.ContainsKey(item.thingIDNumber); } public void CopyTo(AIEnvAgentInfo[] array, int arrayIndex) { - things.CopyTo(array, arrayIndex); + elements.CopyTo(array, arrayIndex); } public IEnumerator GetEnumerator() { - return new AIThingEnum(things, invalidState); + return new AIThingEnum(elements, invalidState); } public IEnumerator GetEnumerator(AIEnvAgentState state) { - return new AIThingEnum(things, state); + return new AIThingEnum(elements, state); } public class AIThingEnum : IEnumerator diff --git a/Source/Rule56/Debugging/CombatAI_DebugTooltipUtility.cs b/Source/Rule56/Debugging/CombatAI_DebugTooltipUtility.cs index 1652822..507bc1c 100644 --- a/Source/Rule56/Debugging/CombatAI_DebugTooltipUtility.cs +++ b/Source/Rule56/Debugging/CombatAI_DebugTooltipUtility.cs @@ -20,5 +20,11 @@ public static string TileIndexTip(World world, int tile) { return $"Tile index:\t\t{tile}"; } + + [CombatAI_DebugTooltip(CombatAI_DebugTooltipType.Map)] + public static string CellTemp(Map map, IntVec3 cell) + { + return $"Temp: {GenTemperature.TryGetTemperature(cell, map)}"; + } } } diff --git a/Source/Rule56/Debugging/JobLog.cs b/Source/Rule56/Debugging/JobLog.cs new file mode 100644 index 0000000..4ad17ca --- /dev/null +++ b/Source/Rule56/Debugging/JobLog.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Text; +using UnityEngine; +using Verse; +using Verse.AI; +namespace CombatAI +{ + public class JobLog + { + private readonly static StringBuilder builder = new StringBuilder(); + + public int timestamp; + public string job; + public int id; + public IntVec3 origin; + public IntVec3 destination; + public string duty; + public List thinknode; + public List stacktrace; + + private JobLog() + { + } + + public bool IsValid + { + get => true; + } + + public static JobLog For(Pawn pawn, Job job, ThinkNode jobGiver) + { + JobLog log = new JobLog(); + log.job = job.def.defName; + log.origin = pawn.Position; + log.id = job.loadID; + log.destination = job.targetA.IsValid ? job.targetA.Cell : IntVec3.Invalid; + log.duty = pawn.mindState.duty?.def.defName ?? "none"; + // fill thinknode trace + log.thinknode = new List(); + if (jobGiver != null) + { + ThinkNodeDatabase.GetTrace(jobGiver, pawn, log.thinknode); + } + // reset builder + StackTrace trace = new StackTrace(); + // fill stacktrace + log.stacktrace = new List(); + foreach (StackFrame frame in trace.GetFrames()) + { + MethodBase method = frame.GetMethod(); + Type type = method.DeclaringType; + if (typeof(Root).IsAssignableFrom(type)) + { + break; + } + log.stacktrace.Add("{0}.{1}:{2}".Formatted(type.Namespace, type.Name, method.Name)); + } + log.timestamp = GenTicks.TicksGame; + return log; + } + + public static JobLog For(Pawn pawn, Job job, string jobGiverTag) + { + JobLog log = new JobLog(); + log.job = job.def.defName; + log.id = job.loadID; + log.origin = pawn.Position; + log.destination = job.targetA.IsValid ? job.targetA.Cell : IntVec3.Invalid; + log.duty = pawn.mindState.duty?.def.defName ?? "none"; + // fill thinknode trace + log.thinknode = new List() { jobGiverTag }; + // reset builder + StackTrace trace = new StackTrace(); + // fill stacktrace + log.stacktrace = new List(); + foreach (StackFrame frame in trace.GetFrames()) + { + MethodBase method = frame.GetMethod(); + Type type = method.DeclaringType; + if (typeof(Root).IsAssignableFrom(type)) + { + break; + } + log.stacktrace.Add("{0}.{1}:{2}".Formatted(type.Namespace, type.Name, method.Name)); + } + log.timestamp = GenTicks.TicksGame; + return log; + } + + public override string ToString() + { + StringBuilder builder = new StringBuilder(); + builder.AppendFormat("job:\t{0} ({1})\n", job, id); + builder.AppendFormat("duty:\t{0}\n", duty); + builder.AppendLine(); + builder.Append("thinknode trace:\n"); + for (int i = 0; i < thinknode.Count; i++) + { + builder.AppendFormat(" {0}. {1}\n", i + 1, thinknode[i]); + } + builder.AppendLine(); + builder.Append("stacktrace:\n"); + for (int i = 0; i < stacktrace.Count; i++) + { + builder.AppendFormat(" {0}. {1}\n", i + 1, stacktrace[i]); + } + return builder.ToString(); + } + } +} diff --git a/Source/Rule56/Debugging/Window_JobLogs.cs b/Source/Rule56/Debugging/Window_JobLogs.cs new file mode 100644 index 0000000..45b2e72 --- /dev/null +++ b/Source/Rule56/Debugging/Window_JobLogs.cs @@ -0,0 +1,500 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using CombatAI.Comps; +using CombatAI.Gui; +using CombatAI.Patches; +using RimWorld; +using RimWorld.Planet; +using UnityEngine; +using Verse; +using Verse.AI; +using GUIUtility = CombatAI.Gui.GUIUtility; +namespace CombatAI +{ + public class Window_JobLogs : Window + { + private Map map; + private Listing_Collapsible collapsible; + private Listing_Collapsible collapsible_dutyTest; + private float viewRatio1; + private float viewRatio2; + private bool dragging1; + private bool dragging2; + private JobLog selectedLog; + private Vector2 scorllPos; + + public ThingComp_CombatAI comp; + + public Window_JobLogs(ThingComp_CombatAI comp) + { + this.collapsible = new Listing_Collapsible(); + this.collapsible_dutyTest = new Listing_Collapsible(); + this.viewRatio1 = 0.5f; + this.viewRatio2 = 0.8f; + this.comp = comp; + this.map = comp.parent.Map; + this.resizeable = true; + this.resizer = new WindowResizer(); + this.draggable = true; + this.doCloseX = true; + this.preventCameraMotion = false; + } + + public override Vector2 InitialSize + { + get => new Vector2(1000, 600); + } + + public Pawn Pawn + { + get => comp.selPawn; + } + + public List Logs + { + get => comp.jobLogs; + } + + public static void ShowTutorial() + { + HyperTextDef[] pages = new HyperTextDef[] + { + CombatAI_HyperTextDefOf.CombatAI_DevJobTutorial1, + CombatAI_HyperTextDefOf.CombatAI_DevJobTutorial2, + CombatAI_HyperTextDefOf.CombatAI_DevJobTutorial3, + CombatAI_HyperTextDefOf.CombatAI_DevJobTutorial4, + }; + Window_Slides slides = new Window_Slides(pages, forcePause:true, skippable: false); + Find.WindowStack.Add(slides); + } + + + public override void DoWindowContents(Rect inRect) + { + GUIUtility.ExecuteSafeGUIAction(() => + { + Rect right = inRect.RightPart(1 - viewRatio2); + Rect left = inRect.LeftPart(viewRatio2); + Rect barRect = right.LeftPartPixels(18); + right.xMin += 18; + Event current = Event.current; + bool mouseOverDragBar = Mouse.IsOver(barRect); + if (current.type == EventType.MouseDown && current.button == 0 && mouseOverDragBar) + { + dragging2 = true; + current.Use(); + } + if (dragging2) + { + viewRatio2 = Mathf.Clamp((current.mousePosition.x - inRect.xMin) / (inRect.xMax - inRect.xMin), 0.6f, 0.9f); + } + if (current.type == EventType.MouseUp && current.button == 0 && dragging2) + { + dragging2 = false; + current.Use(); + } + DrawDragBarVertical(barRect); + if (!(comp.parent?.Destroyed ?? true) && comp.parent.Spawned) + { + DoTestContents(right); + } + DoJobLogContents(left); + }); + } + + private void DoTestContents(Rect inRect) + { + Pawn pawn = comp.selPawn; + if (pawn == null) + { + return; + } + this.collapsible_dutyTest.Expanded = true; + this.collapsible_dutyTest.Begin(inRect, "Test tools", drawInfo:false, drawIcon: false); + this.collapsible_dutyTest.Label("Test suite"); + this.collapsible_dutyTest.Gap(2); + if (ButtonText(collapsible_dutyTest, "Assault colony duty")) + { + foreach (Pawn other in Find.Selector.SelectedPawns) + { + other.mindState.duty = new PawnDuty(DutyDefOf.AssaultColony); + } + Messages.Message($"Success: Assaulting colony", MessageTypeDefOf.CautionInput); + } + if (ButtonText(collapsible_dutyTest, "Defend position")) + { + + Find.Targeter.BeginTargeting(new TargetingParameters() + { + canTargetAnimals = false, + canTargetBuildings = false, + canTargetCorpses = false, + canTargetHumans = false, + canTargetSelf = false, + canTargetMechs = false, + canTargetLocations = true, + }, info => + { + if (info.Cell.IsValid) + { + foreach (Pawn other in Find.Selector.SelectedPawns) + { + other.mindState.duty = new PawnDuty(DutyDefOf.Defend, info); + } + Messages.Message($"Success: Defending current position", MessageTypeDefOf.CautionInput); + } + }); + } + if (ButtonText(collapsible_dutyTest, "Hunt down enemy")) + { + foreach (Pawn other in Find.Selector.SelectedPawns) + { + other.mindState.duty = new PawnDuty(DutyDefOf.HuntEnemiesIndividual); + } + Messages.Message($"Success: Hunting enemies individuals", MessageTypeDefOf.CautionInput); + } + if (ButtonText(collapsible_dutyTest, "Escort")) + { + Find.Targeter.BeginTargeting(new TargetingParameters() + { + canTargetAnimals = true, + canTargetBuildings = false, + canTargetCorpses = false, + canTargetHumans = true, + canTargetSelf = false, + canTargetLocations = false, + canTargetMechs = false, + }, info => + { + if (info.Thing is Pawn escortee) + { + foreach (Pawn other in Find.Selector.SelectedPawns) + { + other.mindState.duty = new PawnDuty(DutyDefOf.Defend, escortee); + } + Messages.Message($"Success: Escorting {escortee}", MessageTypeDefOf.CautionInput); + } + }); + } + this.collapsible_dutyTest.Line(1); + if (ButtonText(collapsible_dutyTest, "Flash pathfinding to")) + { + Find.Targeter.BeginTargeting(new TargetingParameters() + { + canTargetAnimals = false, + canTargetBuildings = false, + canTargetCorpses = false, + canTargetHumans = false, + canTargetSelf = false, + canTargetMechs = false, + canTargetLocations = true, + }, info => + { + if (info.Cell.IsValid) + { + PathFinder_Patch.FlashSearch = true; + try + { + PawnPath path = pawn.Map.pathFinder.FindPath(pawn.Position, info, pawn, PathEndMode.OnCell, null); + if (path is { Found: true }) + { + path.ReleaseToPool(); + } + } + catch (Exception er) + { + Log.Error(er.ToString()); + } + finally + { + PathFinder_Patch.FlashSearch = false; + } + } + }); + } + if (ButtonText(collapsible_dutyTest, "End all jobs")) + { + foreach (Pawn other in Find.Selector.SelectedPawns) + { + other.jobs.ClearQueuedJobs(); + other.jobs.StopAll(); + } + Messages.Message($"Success: All jobs stopped", MessageTypeDefOf.CautionInput); + } + + this.collapsible_dutyTest.End(ref inRect); + } + + private void DoJobLogContents(Rect inRect) + { + GUIUtility.ExecuteSafeGUIAction(() => + { + GUIFont.Font = GUIFontSize.Tiny; + GUIFont.CurFontStyle.fontStyle = FontStyle.Bold; + if (Find.Selector.SelectedPawns.Count == 0) + { + string message = $"WARNING: No pawn selected or the previously selected pawn died!"; + Widgets.DrawBoxSolid(inRect.TopPartPixels(20).LeftPartPixels(message.GetWidthCached() + 20), Color.red); + Widgets.Label(inRect.TopPartPixels(20), message); + } + else + { + Widgets.Label(inRect.TopPartPixels(20), $"Viewing job logs for {comp.parent}"); + } + GUIFont.Font = GUIFontSize.Tiny; + GUIFont.CurFontStyle.fontStyle = FontStyle.Normal; + if (Widgets.ButtonText(inRect.TopPartPixels(18).RightPartPixels(175).LeftPartPixels(175), "Open Job Log Tutorial")) + { + ShowTutorial(); + } + GUI.color = Color.green; + if (Widgets.ButtonText(inRect.TopPartPixels(18).RightPartPixels(350).LeftPartPixels(175), "Copy short report to clipboard") && comp.jobLogs.Count > 0) + { + StringBuilder builder = new StringBuilder(); + int limit = Maths.Min(comp.jobLogs.Count, 10); + builder.AppendFormat("{0} jobs copied", limit); + builder.AppendLine("------------------------------------------------------"); + for (int i = 0; i < limit; i++) + { + builder.Append(comp.jobLogs[i].ToString()); + if (i < limit - 1) + { + builder.AppendLine(); + builder.AppendLine("------------------------------------------------------"); + builder.AppendLine(); + } + } + UnityEngine.GUIUtility.systemCopyBuffer = builder.ToString(); + Messages.Message("Short report copied to clipboard", MessageTypeDefOf.CautionInput); + } + }); + if (Find.Selector.SelectedPawns.Count == 1) + { + var temp = Find.Selector.SelectedPawns[0].GetComp_Fast(); + if (temp != comp) + { + comp = temp; + map = comp.parent.Map; + selectedLog = null; + } + } + inRect.yMin += 20; + Rect header = inRect.TopPartPixels(22); + Widgets.DrawMenuSection(header); + header.xMin += 10; + CombatAI.Gui.GUIUtility.Row(header, new List>() + { + (rect) => + { + GUIFont.Anchor = TextAnchor.MiddleLeft; + Widgets.Label(rect, "Job".Fit(rect)); + }, + (rect) => + { + GUIFont.Anchor = TextAnchor.MiddleLeft; + Widgets.Label(rect, "ID".Fit(rect)); + }, + (rect) => + { + GUIFont.Anchor = TextAnchor.MiddleLeft; + Widgets.Label(rect, "Duty".Fit(rect)); + }, + (rect) => + { + GUIFont.Anchor = TextAnchor.MiddleLeft; + Widgets.Label(rect, "ThinkTrace.First".Fit(rect)); + }, + (rect) => + { + GUIFont.Anchor = TextAnchor.MiddleLeft; + Widgets.Label(rect, "ThinkTrace.Lasts".Fit(rect)); + }, + (rect) => + { + Widgets.Label(rect, "Timestamp".Fit(rect)); + } + }, false); + inRect.yMin += 25; + CombatAI.Gui.GUIUtility.ScrollView(selectedLog != null ? inRect.TopPart(viewRatio1) : inRect, ref scorllPos, Logs, GetHeight, DrawJobLog); + if (selectedLog != null) + { + Rect botRect = inRect.BottomPart(1 - viewRatio1); + Rect barRect = botRect.TopPartPixels(18); + botRect.yMin += 18; + Event current = Event.current; + bool mouseOverDragBar = Mouse.IsOver(barRect); + if (current.type == EventType.MouseDown && current.button == 0 && mouseOverDragBar) + { + dragging1 = true; + current.Use(); + } + if (dragging1) + { + viewRatio1 = Mathf.Clamp((current.mousePosition.y - inRect.yMin) / (inRect.yMax - inRect.yMin), 0.2f, 0.8f); + } + if (current.type == EventType.MouseUp && current.button == 0 && dragging1) + { + dragging1 = false; + current.Use(); + } + DrawDragBarHorizontal(barRect); + DrawSelection(botRect); + } + } + + private void DrawSelection(Rect inRect) + { + Widgets.DrawBoxSolidWithOutline(inRect, this.collapsible.CollapsibleBGColor, Widgets.MenuSectionBGBorderColor); + inRect = inRect.ContractedBy(1); + this.collapsible.CollapsibleBGBorderColor = this.collapsible.CollapsibleBGColor; + this.collapsible.Expanded = true; + this.collapsible.Begin(inRect, $"Details: {selectedLog.job}", false,false); + this.collapsible.Lambda(20, (rect) => + { + if (Widgets.ButtonText(rect.LeftPartPixels(150), "Copy job data to clipboard")) + { + UnityEngine.GUIUtility.systemCopyBuffer = selectedLog.ToString(); + Messages.Message("Job info copied to clipboard", MessageTypeDefOf.CautionInput); + } + }); + this.collapsible.Label($"JobDef.defName:\t{selectedLog.job}"); + this.collapsible.Line(1); + this.collapsible.Label($"DutyDef.defName:\t{selectedLog.duty}"); + this.collapsible.Line(1); + this.collapsible.Lambda(40, (rect) => + { + rect.xMin += 20; + Rect top = rect.TopHalf(); + Rect bot = rect.BottomHalf(); + if (Mouse.IsOver(top)) + { + GlobalTargetInfo target = new GlobalTargetInfo(selectedLog.origin, map); + TargetHighlighter.Highlight(target, true, false, true); + Widgets.DrawHighlight(top); + if (Widgets.ButtonInvisible(top)) + { + CameraJumper.TryJump(target); + map.debugDrawer.FlashCell(selectedLog.origin, 0.01f, "s", 120); + } + } + Widgets.Label(top, $"origin:\t\t{selectedLog.origin}"); + if (selectedLog.destination.IsValid && Mouse.IsOver(bot)) + { + GlobalTargetInfo target = new GlobalTargetInfo(selectedLog.destination, map); + TargetHighlighter.Highlight(target, true, false, true); + Widgets.DrawHighlight(bot); + if (Widgets.ButtonInvisible(bot)) + { + CameraJumper.TryJump(target); + map.debugDrawer.FlashCell(selectedLog.destination, 0.99f, "d", 120); + } + } + Widgets.Label(bot,$"destination:\t{selectedLog.destination}"); + }); + this.collapsible.Line(1); + foreach (string s in selectedLog.thinknode) + { + this.collapsible.Label(s); + } + this.collapsible.Line(1); + foreach (string s in selectedLog.stacktrace) + { + this.collapsible.Label(s); + } + this.collapsible.End(ref inRect); + } + + private void DrawDragBarHorizontal(Rect inRect) + { + if (Mouse.IsOver(inRect)) + { + Widgets.DrawHighlight(inRect); + } + inRect = inRect.ContractedBy(1); + GUIUtility.ExecuteSafeGUIAction(() => + { + inRect.yMin += inRect.height / 2; + Widgets.DrawLine(new Vector2(inRect.xMin, inRect.yMin), new Vector2(inRect.xMax, inRect.yMin), Widgets.MenuSectionBGBorderColor, 1); + }); + } + + private void DrawDragBarVertical(Rect inRect) + { + if (Mouse.IsOver(inRect)) + { + Widgets.DrawHighlight(inRect); + } + inRect = inRect.ContractedBy(1); + GUIUtility.ExecuteSafeGUIAction(() => + { + inRect.xMin += inRect.width / 2; + Widgets.DrawLine(new Vector2(inRect.xMin, inRect.yMin), new Vector2(inRect.xMin, inRect.yMax), Widgets.MenuSectionBGBorderColor, 1); + }); + } + + private void DrawJobLog(Rect inRect, JobLog jobLog) + { + if (Widgets.ButtonInvisible(inRect)) + { + selectedLog = jobLog; + } + if (selectedLog == jobLog) + { + Widgets.DrawHighlight(inRect); + } + Gui.GUIUtility.Row(inRect, new List>() + { + (rect) => + { + rect.xMin += 5; + Widgets.Label(rect, jobLog.job.Fit(rect)); + }, + (rect) => + { + Widgets.Label(rect, $"{jobLog.id}".Fit(rect)); + }, + (rect) => + { + Widgets.Label(rect, jobLog.duty.Fit(rect)); + }, + (rect) => + { + string val = jobLog.thinknode.NullOrEmpty() ? "unknown" : jobLog.thinknode.First(); + Widgets.Label(rect, val.Fit(rect)); + }, + (rect) => + { + string val = jobLog.thinknode.NullOrEmpty() ? "unknown" : jobLog.thinknode.Last(); + Widgets.Label(rect, val.Fit(rect)); + }, + (rect) => + { + Widgets.Label(rect, $"{Math.Round((GenTicks.TicksGame - jobLog.timestamp) / 60f, 0)} seconds ago".Fit(rect)); + } + }, false, false); + } + + private float GetHeight(JobLog jobLog) + { + return 20; + } + + private static bool ButtonText(Listing_Collapsible collapsible, string text) + { + bool result = false; + collapsible.Lambda(20, rect => + { + GUI.color = Color.yellow; + rect.xMin += 5; + if (Mouse.IsOver(rect)) + { + Widgets.DrawHighlight(rect); + } + result = Widgets.ButtonText(rect, text, false, overrideTextAnchor:TextAnchor.MiddleLeft); + }); + return result; + } + } +} diff --git a/Source/Rule56/DefOfs/CombatAI_DutyDefOf.cs b/Source/Rule56/DefOfs/CombatAI_DutyDefOf.cs new file mode 100644 index 0000000..e178a07 --- /dev/null +++ b/Source/Rule56/DefOfs/CombatAI_DutyDefOf.cs @@ -0,0 +1,11 @@ +using RimWorld; +using Verse.AI; +namespace CombatAI +{ + [DefOf] + public static class CombatAI_DutyDefOf + { + public static DutyDef CombatAI_AssaultPoint; + public static DutyDef CombatAI_Escort; + } +} diff --git a/Source/Rule56/DefOfs/CombatAI_HediffDefOf.cs b/Source/Rule56/DefOfs/CombatAI_HediffDefOf.cs new file mode 100644 index 0000000..81cdd81 --- /dev/null +++ b/Source/Rule56/DefOfs/CombatAI_HediffDefOf.cs @@ -0,0 +1,10 @@ +using RimWorld; +using Verse; +namespace CombatAI +{ + [DefOf] + public static class CombatAI_HediffDefOf + { + public static HediffDef MechlinkImplant; + } +} diff --git a/Source/Rule56/DefOfs/CombatAI_HyperTextDefOf.cs b/Source/Rule56/DefOfs/CombatAI_HyperTextDefOf.cs new file mode 100644 index 0000000..27a7017 --- /dev/null +++ b/Source/Rule56/DefOfs/CombatAI_HyperTextDefOf.cs @@ -0,0 +1,13 @@ +using CombatAI.Gui; +using RimWorld; +namespace CombatAI +{ + [DefOf] + public class CombatAI_HyperTextDefOf + { + public static HyperTextDef CombatAI_DevJobTutorial1; + public static HyperTextDef CombatAI_DevJobTutorial2; + public static HyperTextDef CombatAI_DevJobTutorial3; + public static HyperTextDef CombatAI_DevJobTutorial4; + } +} diff --git a/Source/Rule56/DefOfs/CombatAI_JobDefOf.cs b/Source/Rule56/DefOfs/CombatAI_JobDefOf.cs new file mode 100644 index 0000000..5812372 --- /dev/null +++ b/Source/Rule56/DefOfs/CombatAI_JobDefOf.cs @@ -0,0 +1,12 @@ +using RimWorld; +using Verse; +namespace CombatAI +{ + [DefOf] + public class CombatAI_JobDefOf + { + public static JobDef CombatAI_Goto_Retreat; + public static JobDef CombatAI_Goto_Cover; + public static JobDef CombatAI_Goto_Duck; + } +} diff --git a/Source/Rule56/DifficultyUtility.cs b/Source/Rule56/DifficultyUtility.cs index 1c1113e..9d4f1f4 100644 --- a/Source/Rule56/DifficultyUtility.cs +++ b/Source/Rule56/DifficultyUtility.cs @@ -8,8 +8,9 @@ public static void SetDifficulty(Difficulty difficulty) switch (difficulty) { case Difficulty.Easy: - Finder.Settings.Pathfinding_DestWeight = 0.9f; + Finder.Settings.Pathfinding_DestWeight = 0.875f; Finder.Settings.Caster_Enabled = false; + Finder.Settings.Temperature_Enabled = true; Finder.Settings.Targeter_Enabled = false; Finder.Settings.Pather_Enabled = true; Finder.Settings.Pather_KillboxKiller = false; @@ -39,8 +40,9 @@ public static void SetDifficulty(Difficulty difficulty) } break; case Difficulty.Normal: - Finder.Settings.Pathfinding_DestWeight = 0.8f; + Finder.Settings.Pathfinding_DestWeight = 0.725f; Finder.Settings.Caster_Enabled = true; + Finder.Settings.Temperature_Enabled = true; Finder.Settings.Targeter_Enabled = true; Finder.Settings.Pather_Enabled = true; Finder.Settings.Pather_KillboxKiller = true; @@ -68,8 +70,9 @@ public static void SetDifficulty(Difficulty difficulty) } break; case Difficulty.Hard: - Finder.Settings.Pathfinding_DestWeight = 0.75f; + Finder.Settings.Pathfinding_DestWeight = 0.625f; Finder.Settings.Caster_Enabled = true; + Finder.Settings.Temperature_Enabled = true; Finder.Settings.Targeter_Enabled = true; Finder.Settings.Pather_Enabled = true; Finder.Settings.Pather_KillboxKiller = true; @@ -99,8 +102,9 @@ public static void SetDifficulty(Difficulty difficulty) } break; case Difficulty.DeathWish: - Finder.Settings.Pathfinding_DestWeight = 0.6f; + Finder.Settings.Pathfinding_DestWeight = 0.45f; Finder.Settings.Caster_Enabled = true; + Finder.Settings.Temperature_Enabled = true; Finder.Settings.Targeter_Enabled = true; Finder.Settings.Pather_Enabled = true; Finder.Settings.Pather_KillboxKiller = true; diff --git a/Source/Rule56/GenTemperature.cs b/Source/Rule56/GenTemperature.cs new file mode 100644 index 0000000..dbe2685 --- /dev/null +++ b/Source/Rule56/GenTemperature.cs @@ -0,0 +1,27 @@ +using System.Runtime.CompilerServices; +using Verse; +namespace CombatAI +{ + public static class GenTemperature + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float TryGetTemperature(IntVec3 cell, Map map) + { + return TryGetTemperature(map.cellIndices.CellToIndex(cell), map); + } + + public static float TryGetTemperature(int index, Map map) + { + if (index >= 0 && index < map.cellIndices.NumGridCells) + { + Region region = map.regionGrid.regionGrid[index]; + Room room; + if (region is { valid: true } && (room = region.District?.Room) != null) + { + return room.Temperature; + } + } + return 21f; + } + } +} diff --git a/Source/Rule56/Gui/Core/GUIUtility.Text.cs b/Source/Rule56/Gui/Core/GUIUtility.Text.cs index c9d9b3f..b42399f 100644 --- a/Source/Rule56/Gui/Core/GUIUtility.Text.cs +++ b/Source/Rule56/Gui/Core/GUIUtility.Text.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Runtime.CompilerServices; using UnityEngine; using Verse; using GUITextState = System.Tuple, System.Tuple, System.Tuple>; @@ -10,7 +11,9 @@ public static partial class GUIUtility private const int MAX_CACHE_SIZE = 2000; private static readonly Dictionary textHeightCache = new Dictionary(512); + private static readonly Dictionary textWidthCache = new Dictionary(512); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void Cleanup() { if (textHeightCache.Count > MAX_CACHE_SIZE) @@ -21,13 +24,17 @@ private static void Cleanup() public static string Fit(this string text, Rect rect) { + if (string.IsNullOrEmpty(text)) + { + return text; + } Cleanup(); - float height = GetTextHeight(text, rect.width); - if (height <= rect.height) + float width = CalcTextWidth(text); + if (rect.width >= width) { return text; } - return text.Substring(0, (int)(text.Length * height / rect.height)) + "..."; + return text.Substring(0, Mathf.FloorToInt(Mathf.Clamp(text.Length * rect.width / width - 3, 1, text.Length))) + "..."; } public static float GetTextHeight(this string text, Rect rect) @@ -55,6 +62,17 @@ public static float CalcTextHeight(string text, float width) } return textHeightCache[key] = Text.CalcHeight(text, width); } + + public static float CalcTextWidth(string text) + { + Cleanup(); + GUITextState key = GetGUIState(text, -1); + if (textWidthCache.TryGetValue(key, out float width)) + { + return width; + } + return textWidthCache[key] = Text.CalcSize(text).x; + } private static GUITextState GetGUIState(string text, float width) { diff --git a/Source/Rule56/Gui/HyperText/HyperText.cs b/Source/Rule56/Gui/HyperText/HyperText.cs new file mode 100644 index 0000000..51e73c9 --- /dev/null +++ b/Source/Rule56/Gui/HyperText/HyperText.cs @@ -0,0 +1,25 @@ +using UnityEngine; +namespace CombatAI.Gui +{ + public class HyperText + { + public HyperTextDef def; + public Listing_Collapsible collapsible; + + public HyperText(HyperTextDef def, bool allowScrolling = true) + { + this.def = def; + this.collapsible = new Listing_Collapsible(scrollViewOnOverflow: allowScrolling); + this.collapsible.CollapsibleBGColor = new Color(0, 0, 0, 0); + this.collapsible.CollapsibleBGBorderColor = new Color(0, 0, 0, 0); + } + + public void Draw(Rect rect) + { + collapsible.Expanded = true; + collapsible.Begin(rect, "", false, false, hightlightIfMouseOver: false); + def.DrawParts(collapsible); + collapsible.End(ref rect); + } + } +} diff --git a/Source/Rule56/Gui/HyperText/HyperTextDef.cs b/Source/Rule56/Gui/HyperText/HyperTextDef.cs new file mode 100644 index 0000000..d2bb991 --- /dev/null +++ b/Source/Rule56/Gui/HyperText/HyperTextDef.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using UnityEngine; +using Verse; +namespace CombatAI.Gui +{ + [StaticConstructorOnStartup] + public class HyperTextDef : Def + { + [Unsaved(allowLoading: false)] + private readonly List> actions = new List>(); + + public void DrawParts(Listing_Collapsible collapsible) + { + foreach (Action part in actions) + { + part(collapsible); + } + } + + public void LoadDataFromXmlCustom(XmlNode xmlRoot) + { + try + { + foreach (XmlNode node in xmlRoot.ChildNodes) + { + if (node.Name == "content") + { + ParseXmlContent(node); + } + if (node.Name == "defName") + { + this.defName = node.InnerText; + } + } + } + catch (Exception er) + { + Log.Error(er.ToString()); + } + } + + private void ParseXmlContent(XmlNode xmlRoot) + { + foreach (XmlNode node in xmlRoot.ChildNodes) + { + if (node is XmlElement element) + { + if (element.Name == "p") + { + ParseTextXmlNode(element); + } + else if(element.Name == "img") + { + ParseMediaNode(element); + } + else if(element.Name == "gap") + { + ParseGapNode(element); + } + } + } + } + + private void ParseTextXmlNode(XmlElement element) + { + XmlAttribute fontSize = element.Attributes["fontSize"]; + if (fontSize == null || !GUIFontSize.TryParse(fontSize.Value, true, out GUIFontSize size)) + { + size = GUIFontSize.Small; + } + XmlAttribute textAnchor = element.Attributes["textAnchor"]; + if (textAnchor == null || !TextAnchor.TryParse(textAnchor.Value, true, out TextAnchor anchor)) + { + anchor = TextAnchor.UpperLeft; + } + string text = element.InnerText.Replace('[', '<').Replace(']', '>'); + void Action(Listing_Collapsible collapsible) + { + GUIUtility.ExecuteSafeGUIAction(() => + { + GUIFont.Anchor = anchor; + GUIFont.Font = size; + collapsible.Lambda(text.GetTextHeight(collapsible.Rect.width + 20) + 5, (rect) => + { + GUIFont.Anchor = anchor; + GUIFont.Font = size; + Widgets.Label(rect, text); + }); + }); + } + actions.Add(Action); + } + + private void ParseGapNode(XmlElement element) + { + XmlAttribute gapHeight = element.Attributes["height"]; + if (gapHeight == null || !int.TryParse(gapHeight.Value, out int height)) + { + height = 1; + } + void Action(Listing_Collapsible collapsible) + { + collapsible.Gap(height); + } + actions.Add(Action); + } + + private void ParseMediaNode(XmlElement element) + { + string path = element.Attributes["path"].Value; + string heightStr = null; + XmlAttribute imgHeight = element.Attributes["height"]; + if (imgHeight != null) + { + heightStr = imgHeight.Value; + } + int index = actions.Count; + LongEventHandler.ExecuteWhenFinished(delegate + { + Texture2D texture = ContentFinder.Get(path, reportFailure: true); + int width = texture.width; + if (heightStr == null || !int.TryParse(heightStr, out int height)) + { + height = texture.height; + } + void Action(Listing_Collapsible collapsible) + { + collapsible.Lambda(height, (rect) => + { + Widgets.DrawTextureFitted(rect, texture, 1.0f); + }); + } + actions[index] = Action; + }); + actions.Add(null); + } + } +} diff --git a/Source/Rule56/Gui/HyperText/HyperTextMaker.cs b/Source/Rule56/Gui/HyperText/HyperTextMaker.cs new file mode 100644 index 0000000..e8c69fc --- /dev/null +++ b/Source/Rule56/Gui/HyperText/HyperTextMaker.cs @@ -0,0 +1,11 @@ +namespace CombatAI.Gui +{ + public static class HyperTextMaker + { + public static HyperText Make(HyperTextDef def, bool allowScrolling = true) + { + HyperText text = new HyperText(def, allowScrolling); + return text; + } + } +} diff --git a/Source/Rule56/Window_QuickSetup.cs b/Source/Rule56/Gui/Window_QuickSetup.cs similarity index 60% rename from Source/Rule56/Window_QuickSetup.cs rename to Source/Rule56/Gui/Window_QuickSetup.cs index 575f32f..c254a67 100644 --- a/Source/Rule56/Window_QuickSetup.cs +++ b/Source/Rule56/Gui/Window_QuickSetup.cs @@ -1,26 +1,27 @@ using System; using System.Collections.Generic; -using CombatAI.Gui; using CombatAI.R; -using HarmonyLib; using RimWorld; using UnityEngine; -using UnityEngine.Experimental.Playables; using Verse; using GUIUtility = CombatAI.Gui.GUIUtility; -namespace CombatAI +namespace CombatAI.Gui { public class Window_QuickSetup : Window { + private Difficulty difficulty; + private bool presetSelected = false; private readonly Listing_Collapsible collapsible; + private readonly Listing_Collapsible collapsible_fogOfWar; public Window_QuickSetup() { - this.drawShadow = true; - this.forcePause = true; - this.layer = WindowLayer.Super; - this.draggable = false; - this.collapsible = new Listing_Collapsible(); + this.drawShadow = true; + this.forcePause = true; + this.layer = WindowLayer.Super; + this.draggable = false; + this.collapsible = new Listing_Collapsible(); + this.collapsible_fogOfWar = new Listing_Collapsible(); } public override Vector2 InitialSize @@ -38,8 +39,8 @@ public override void DoWindowContents(Rect inRect) { Rect titleRect = inRect.TopPartPixels(60); Rect optionsRect = inRect.BottomPartPixels(inRect.height - 60); - optionsRect.height -= 25; - inRect = inRect.BottomPartPixels(25); + optionsRect.height -= 60; + inRect = inRect.BottomPartPixels(60); Gui.GUIUtility.ExecuteSafeGUIAction(() => { Gui.GUIFont.Anchor = TextAnchor.MiddleCenter; @@ -57,23 +58,51 @@ public override void DoWindowContents(Rect inRect) DoDifficultySettings(rect); }); collapsible.Line(1); - FillCollapsible_FogOfWar(collapsible); + collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Basic_PerformanceOpt, ref Finder.Settings.PerformanceOpt_Enabled); + collapsible.Line(1); + collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Basic_KillBoxKiller, ref Finder.Settings.Pather_KillboxKiller); + collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Basic_Sprinting, ref Finder.Settings.Enable_Sprinting, Keyed.CombatAI_Settings_Basic_Sprinting_Description); collapsible.End(ref optionsRect); + optionsRect.yMin += 5; + collapsible_fogOfWar.Expanded = true; + collapsible_fogOfWar.Begin(optionsRect, R.Keyed.CombatAI_Settings_Basic_FogOfWar, drawIcon: false); + FillCollapsible_FogOfWar(collapsible_fogOfWar); + collapsible_fogOfWar.End(ref optionsRect); Gui.GUIUtility.ExecuteSafeGUIAction(() => { - Text.Font = GameFont.Small; - GUI.color = Color.green; - if (Widgets.ButtonText(inRect.LeftHalf(), R.Keyed.CombatAI_Apply)) + if (presetSelected) { - Finder.Settings.FinishedQuickSetup = true; - Finder.Settings.Write(); - Close(); + DoSelected(inRect.TopHalf()); } - GUI.color = Color.red; - if (Widgets.ButtonText(inRect.RightHalf(), R.Keyed.CombatAI_Close)) + Text.Font = GameFont.Small; + GUI.color = presetSelected ? Color.green : Color.red; + if (Widgets.ButtonText(inRect.BottomHalf(), R.Keyed.CombatAI_Apply)) { - Close(); - } + if (!presetSelected) + { + Messages.Message(R.Keyed.CombatAI_Quick_Difficulty, MessageTypeDefOf.RejectInput); + } + else + { + Finder.Settings.FinishedQuickSetup = true; + Finder.Settings.Write(); + Close(); + } + } + }); + } + + private void DoSelected(Rect rect) + { + GUIUtility.ExecuteSafeGUIAction(() => + { + rect.yMin += 2; + rect.yMax -= 2; + Widgets.DrawBox(rect.ContractedBy(1), 1); + rect.xMin += 10; + GUIFont.Anchor = TextAnchor.MiddleLeft; + GUIFont.Font = GUIFontSize.Small; + Widgets.Label(rect, difficulty < Difficulty.DeathWish ? R.Keyed.CombatAI_Quick_Difficulty_Selected.Formatted(difficulty.ToString()) : R.Keyed.CombatAI_Quick_Difficulty_Selected_Warning.Formatted(difficulty.ToString())); }); } @@ -88,7 +117,8 @@ private void DoDifficultySettings(Rect inRect) GUI.color = Color.green; if (Widgets.ButtonText(rect, Keyed.CombatAI_Settings_Basic_Presets_Easy)) { - DifficultyUtility.SetDifficulty(Difficulty.Easy); + presetSelected = true; + DifficultyUtility.SetDifficulty(difficulty = Difficulty.Easy); Messages.Message(Keyed.CombatAI_Settings_Basic_Presets_Applied + " " + Keyed.CombatAI_Settings_Basic_Presets_Easy, MessageTypeDefOf.TaskCompletion); } }, @@ -96,7 +126,8 @@ private void DoDifficultySettings(Rect inRect) { if (Widgets.ButtonText(rect, Keyed.CombatAI_Settings_Basic_Presets_Normal)) { - DifficultyUtility.SetDifficulty(Difficulty.Normal); + presetSelected = true; + DifficultyUtility.SetDifficulty(difficulty = Difficulty.Normal); Messages.Message(Keyed.CombatAI_Settings_Basic_Presets_Applied + " " + Keyed.CombatAI_Settings_Basic_Presets_Normal, MessageTypeDefOf.TaskCompletion); } }, @@ -104,7 +135,8 @@ private void DoDifficultySettings(Rect inRect) { if (Widgets.ButtonText(rect, Keyed.CombatAI_Settings_Basic_Presets_Hard)) { - DifficultyUtility.SetDifficulty(Difficulty.Hard); + presetSelected = true; + DifficultyUtility.SetDifficulty(difficulty = Difficulty.Hard); Messages.Message(Keyed.CombatAI_Settings_Basic_Presets_Applied + " " + Keyed.CombatAI_Settings_Basic_Presets_Hard, MessageTypeDefOf.TaskCompletion); } }, @@ -113,7 +145,8 @@ private void DoDifficultySettings(Rect inRect) GUI.color = Color.red; if (Widgets.ButtonText(rect, Keyed.CombatAI_Settings_Basic_Presets_Deathwish)) { - DifficultyUtility.SetDifficulty(Difficulty.DeathWish); + presetSelected = true; + DifficultyUtility.SetDifficulty(difficulty = Difficulty.DeathWish); Messages.Message(Keyed.CombatAI_Settings_Basic_PerformanceOpt_Warning, MessageTypeDefOf.CautionInput); Messages.Message(Keyed.CombatAI_Settings_Basic_Presets_Applied + " " + Keyed.CombatAI_Settings_Basic_Presets_Deathwish, MessageTypeDefOf.TaskCompletion); } @@ -131,26 +164,7 @@ private void FillCollapsible_FogOfWar(Listing_Collapsible collapsible) collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Basic_FogOfWar_Animals_SmartOnly, ref Finder.Settings.FogOfWar_AnimalsSmartOnly, disabled: !Finder.Settings.FogOfWar_Animals); collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Basic_FogOfWar_Allies, ref Finder.Settings.FogOfWar_Allies); collapsible.CheckboxLabeled(Keyed.CombatAI_Settings_Basic_FogOfWar_Turrets, ref Finder.Settings.FogOfWar_Turrets); - -// collapsible.Label(Keyed.CombatAI_Settings_Basic_FogOfWar_Density); -// collapsible.Lambda(25, rect => -// { -// Finder.Settings.FogOfWar_FogColor = HorizontalSlider_NewTemp(rect, Finder.Settings.FogOfWar_FogColor, 0.0f, 1.0f, false, Keyed.CombatAI_Settings_Basic_FogOfWar_Density_Readouts.Formatted(Finder.Settings.FogOfWar_FogColor.ToString()), 0.05f); -// }, useMargins: true); -// -// collapsible.Label(Keyed.CombatAI_Settings_Basic_FogOfWar_RangeMul); -// collapsible.Lambda(25, rect => -// { -// Finder.Settings.FogOfWar_RangeMultiplier = HorizontalSlider_NewTemp(rect, Finder.Settings.FogOfWar_RangeMultiplier, 0.75f, 2.0f, false, Keyed.CombatAI_Settings_Basic_FogOfWar_RangeMul_Readouts.Formatted(Finder.Settings.FogOfWar_RangeMultiplier.ToString()), 0.05f); -// }, useMargins: true); -// -// -// collapsible.Label(Keyed.CombatAI_Settings_Basic_FogOfWar_FadeMul); -// collapsible.Lambda(25, rect => -// { -// Finder.Settings.FogOfWar_RangeFadeMultiplier = HorizontalSlider_NewTemp(rect, Finder.Settings.FogOfWar_RangeFadeMultiplier, 0.0f, 1.0f, false, Keyed.CombatAI_Settings_Basic_FogOfWar_FadeMul_Readouts.Formatted(Finder.Settings.FogOfWar_RangeFadeMultiplier.ToString()), 0.05f); -// }, useMargins: true); - } + } } private float HorizontalSlider_NewTemp(Rect rect, float val, float min, float max, bool middleAlinment, string label, float roundTo = -1) diff --git a/Source/Rule56/Gui/Window_Slides.cs b/Source/Rule56/Gui/Window_Slides.cs new file mode 100644 index 0000000..a6a8bdf --- /dev/null +++ b/Source/Rule56/Gui/Window_Slides.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using UnityEngine; +using Verse; +namespace CombatAI.Gui +{ + public class Window_Slides : Window + { + private int curIndex; + private bool[] read; + private List pages = new List(); + + public Window_Slides(HyperTextDef[] defs, bool forcePause = true, bool skippable = true) + { + this.read = new bool[defs.Length]; + this.pages = new List(defs.Length); + foreach (HyperTextDef def in defs) + { + this.pages.Add(HyperTextMaker.Make(def)); + } + this.doCloseX = skippable; + this.forcePause = forcePause; + this.draggable = false; + } + + public override Vector2 InitialSize + { + get => new Vector2(800, 600); + } + + public override void DoWindowContents(Rect inRect) + { + HyperText page = pages[curIndex]; + page.Draw(inRect.TopPartPixels(inRect.height - 50)); + bool ButtonText(Rect rect, string text, Color? color) + { + bool result = false; + Gui.GUIUtility.ExecuteSafeGUIAction(() => + { + if (color != null) + { + GUI.color = color.Value; + } + result = Widgets.ButtonText(rect, text); + }); + return result; + } + Rect counterRect = inRect.BottomPartPixels(50).TopPartPixels(20); + Gui.GUIUtility.ExecuteSafeGUIAction(() => + { + GUIFont.Font = GUIFontSize.Smaller; + GUIFont.Anchor = TextAnchor.MiddleCenter; + Widgets.Label(counterRect, $"{curIndex + 1} / {pages.Count}"); + }); + if (curIndex != 0) + { + Rect buttonRect = inRect.BottomPartPixels(30); + buttonRect = buttonRect.ContractedBy(3); + buttonRect.width = 310; + buttonRect = buttonRect.CenteredOnXIn(inRect); + if (curIndex < pages.Count - 1) + { + if (ButtonText(buttonRect.RightPartPixels(150), $"Next page >", null)) + { + curIndex++; + } + } + else + { + if (ButtonText(buttonRect.RightPartPixels(150), R.Keyed.CombatAI_Close, Color.green)) + { + Close(); + } + } + if (curIndex > 0 && ButtonText(buttonRect.LeftPartPixels(150), $"< Previous page", null)) + { + curIndex--; + } + } + else + { + Rect buttonRect = inRect.BottomPartPixels(30); + buttonRect = buttonRect.ContractedBy(3); + buttonRect.width = 150; + buttonRect = buttonRect.CenteredOnXIn(inRect); + if (pages.Count != 0) + { + if (ButtonText(buttonRect, $"Next page >", null)) + { + curIndex++; + } + } + else + { + if (ButtonText(buttonRect, R.Keyed.CombatAI_Close, Color.green)) + { + Close(); + } + } + } + } + } +} diff --git a/Source/Rule56/ISGrid.cs b/Source/Rule56/ISGrid.cs index 8589338..14d76dd 100644 --- a/Source/Rule56/ISGrid.cs +++ b/Source/Rule56/ISGrid.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using Verse; namespace CombatAI { @@ -17,17 +18,21 @@ public ISGrid(Map map) public T this[IntVec3 cell] { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this[indices.CellToIndex(cell)]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] set => this[indices.CellToIndex(cell)] = value; } public T this[int index] { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { Cell cell = cells[index]; return cell.sig == sig ? cell.value : default(T); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] set => cells[index] = new Cell { sig = sig, @@ -40,10 +45,12 @@ public void Reset() sig++; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsSet(IntVec3 cell) { return IsSet(indices.CellToIndex(cell)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsSet(int index) { return cells[index].sig == sig; diff --git a/Source/Rule56/ITRegionGrid.cs b/Source/Rule56/ITRegionGrid.cs index 0dcfc6c..31e6665 100644 --- a/Source/Rule56/ITRegionGrid.cs +++ b/Source/Rule56/ITRegionGrid.cs @@ -12,9 +12,9 @@ public class ITRegionGrid private readonly int NumGridCells; private readonly IFieldInfo[] regions; private readonly IField[] regions_blunt; + private readonly IField[] regions_sharp; private readonly IField[] regions_flags; private readonly IField[] regions_meta; - private readonly IField[] regions_sharp; private float curBlunt; private ulong curFlag; diff --git a/Source/Rule56/ITSignalGrid.cs b/Source/Rule56/ITSignalGrid.cs index 321a4c0..c00af92 100644 --- a/Source/Rule56/ITSignalGrid.cs +++ b/Source/Rule56/ITSignalGrid.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using UnityEngine; using Verse; @@ -17,10 +18,10 @@ public class ITSignalGrid { private readonly IFieldInfo[] cells; private readonly IField[] cells_blunt; + private readonly IField[] cells_sharp; private readonly IField[] cells_dir; private readonly IField[] cells_flags; private readonly IField[] cells_meta; - private readonly IField[] cells_sharp; private readonly IField[] cells_strength; private readonly IField[] cells_aiming; @@ -296,6 +297,21 @@ public void Set(int index, float signalStrength, Vector2 dir, ulong flags) } } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsSet(IntVec3 cell) + { + return IsSet(indices.CellToIndex(cell)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsSet(int index) + { + if (index >= 0 && index < NumGridCells) + { + return cells[index].sig == r_sig; + } + return false; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetSignalNum(IntVec3 cell) @@ -321,26 +337,16 @@ public int GetSignalNum(int index) } [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete()] public float GetRawSignalStrengthAt(IntVec3 cell) { return GetRawSignalStrengthAt(indices.CellToIndex(cell)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete()] public float GetRawSignalStrengthAt(int index) { - if (index >= 0 && index < NumGridCells) - { - IFieldInfo cell = cells[index]; - switch (CycleNum - cell.cycle) - { - case 0: - IField strength = cells_strength[index]; - - return Maths.Max(strength.value, strength.valuePrev); - case 1: - return cells_strength[index].value; - } - } - return 0; + return GetSignalStrengthAt(index); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -357,10 +363,10 @@ public float GetSignalStrengthAt(int index) { case 0: IField strength = cells_strength[index]; - - return Maths.Max(strength.value, strength.valuePrev) * 0.9f + Maths.Max(cell.num, cell.numPrev) * 0.1f; + + return Maths.Max(strength.value, strength.valuePrev); case 1: - return cells_strength[index].value * 0.9f + cell.num * 0.1f; + return cells_strength[index].value; } } return 0; @@ -381,10 +387,10 @@ public float GetSignalStrengthAt(int index, out int signalNum) case 0: IField strength = cells_strength[index]; signalNum = Maths.Max(cell.num, cell.numPrev); - return Maths.Max(strength.value, strength.valuePrev) * 0.9f + signalNum * 0.1f; + return Maths.Max(strength.value, strength.valuePrev); case 1: signalNum = cell.num; - return cells_strength[index].value * 0.9f + signalNum * 0.1f; + return cells_strength[index].value; } } return signalNum = 0; diff --git a/Source/Rule56/InterceptorTracker.cs b/Source/Rule56/InterceptorTracker.cs index f85c7ec..5f65fb5 100644 --- a/Source/Rule56/InterceptorTracker.cs +++ b/Source/Rule56/InterceptorTracker.cs @@ -49,7 +49,7 @@ public void Tick() { _removalList.Add(item); } - if (!(item.parent is Pawn pawn && (pawn.IsCharging() || pawn.IsDormant())) && item.interceptor.Active) + if (!(item.parent is Pawn pawn && ((pawn.needs?.energy != null && pawn.IsCharging()) || pawn.IsDormant())) && item.interceptor.Active) { TryCastInterceptor(item); } diff --git a/Source/Rule56/Maths.cs b/Source/Rule56/Maths.cs index 5a5c333..a2d1d11 100644 --- a/Source/Rule56/Maths.cs +++ b/Source/Rule56/Maths.cs @@ -1,8 +1,39 @@ -using System.Runtime.CompilerServices; +using System; +using System.Runtime.CompilerServices; +using UnityEngine; +using Verse; namespace CombatAI { public static class Maths { + private const int DistTh1 = 35 * 35; + private const int DistTh2 = 70 * 70; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DistanceTo_Fast(this IntVec3 first, IntVec3 second) + { + float a = first.x - second.x; + float b = first.z - second.z; + float distSqr = a * a + b * b; + if (distSqr < DistTh1) + { + return Maths.Sqrt_Fast(distSqr, 3); + } + else if (distSqr < DistTh2) + { + return Maths.Sqrt_Fast(distSqr, 5); + } + else + { + return Maths.Sqrt_Fast(distSqr, 7); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DistanceTo_Fast(this Thing first, Thing second) + { + return first.Position.DistanceTo_Fast(second.Position); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float Max(float a, float b) { @@ -118,9 +149,17 @@ public static byte Sqr(byte a) public static float Sqrt_Fast(float x, int iterations) { + if (x < 0.001f) + { + return iterations >= 5 ? Mathf.Sqrt(x) : x; + } + if (x < 0) + { + throw new Exception("Input cannot be a negative value"); + } int n; int k; - int a = (int)(x * 1024); + int a = (int)(x * 1024f); if ((a & 0xFFFF0000) != 0) { if ((a & 0xFFF00000) != 0) @@ -176,6 +215,14 @@ public static float Sqrt_Fast(float x, int iterations) } public static int Sqrt_Fast(int a, int iterations) { + if (a == 0) + { + return 0; + } + if (a < 0) + { + throw new Exception("Input cannot be a negative value"); + } int n; int k; if ((a & 0xFFFFFF00) != 0) diff --git a/Source/Rule56/Patches/Game_Patch.cs b/Source/Rule56/Patches/Game_Patch.cs index 81b43a2..efbb939 100644 --- a/Source/Rule56/Patches/Game_Patch.cs +++ b/Source/Rule56/Patches/Game_Patch.cs @@ -1,4 +1,5 @@ -using HarmonyLib; +using CombatAI.Gui; +using HarmonyLib; using Verse; namespace CombatAI.Patches { @@ -14,7 +15,7 @@ public static void Prefix(Map map) CompCache.Notify_MapRemoved(map); } } - + [HarmonyPatch(typeof(Game), nameof(Game.ClearCaches))] private static class Game_ClearCaches_Patch { @@ -32,5 +33,23 @@ public static void Prefix() LordToil_AssaultColony_Patch.ClearCache(); } } + +#if DEBUG + [HarmonyPatch(typeof(Game), nameof(Game.FinalizeInit))] +#pragma warning restore CS0612 + private static class Game_FinalizeInit_Patch + { + private static bool debugWindowShowen; + + public static void Prefix() + { + if (!debugWindowShowen) + { + Window_JobLogs.ShowTutorial(); + debugWindowShowen = true; + } + } + } +#endif } } diff --git a/Source/Rule56/Patches/Hediff_Mechlink_Patch.cs b/Source/Rule56/Patches/Hediff_Mechlink_Patch.cs new file mode 100644 index 0000000..27b2f08 --- /dev/null +++ b/Source/Rule56/Patches/Hediff_Mechlink_Patch.cs @@ -0,0 +1,23 @@ +using HarmonyLib; +using Verse; +namespace CombatAI.Patches +{ + public static class Hediff_Mechlink_Patch + { +// [HarmonyPatch(typeof(Hediff_Mechlink), nameof(Hediff_Mechlink.PostAdd))] +// public static class Hediff_Mechlink_PostAdd_Patch +// { +// public static void Postfix(Hediff_Mechlink __instance) +// { +// } +// } +// +// [HarmonyPatch(typeof(Hediff_Mechlink), nameof(Hediff_Mechlink.PostRemoved))] +// public static class Hediff_Mechlink_PostRemoved_Patch +// { +// public static void Postfix(Hediff_Mechlink __instance) +// { +// } +// } + } +} diff --git a/Source/Rule56/Patches/JobDriver_Wait_Patch.cs b/Source/Rule56/Patches/JobDriver_Wait_Patch.cs index f9a8887..dd83e2c 100644 --- a/Source/Rule56/Patches/JobDriver_Wait_Patch.cs +++ b/Source/Rule56/Patches/JobDriver_Wait_Patch.cs @@ -12,44 +12,40 @@ private static class JobDriver_Wait_MakeNewToils_Patch { public static void Postfix(JobDriver_Wait __instance) { - if (__instance.job.def.alwaysShowWeapon && __instance.pawn.mindState?.enemyTarget != null && __instance.job.def == JobDefOf.Wait_Combat && __instance.GetType() == typeof(JobDriver_Wait)) + if (__instance.job.Is(JobDefOf.Wait_Combat) && !__instance.pawn.Faction.IsPlayerSafe()) { if (__instance.job.targetC.IsValid) { __instance.rotateToFace = TargetIndex.C; } - __instance.AddEndCondition(() => + __instance.AddFailCondition(() => { if (!__instance.pawn.IsHashIntervalTick(30) || GenTicks.TicksGame - __instance.startTick < 30) { - return JobCondition.Ongoing; + return false; } - if (__instance.pawn.mindState?.enemyTarget is Pawn enemy) + Verb verb = __instance.job.verbToUse ?? __instance.pawn.CurrentEffectiveVerb; + if (verb == null || verb.WarmingUp || verb.Bursting || __instance.pawn.Faction.IsPlayerSafe()) { - ThingComp_CombatAI comp = __instance.pawn.GetComp_Fast(); - if (comp?.waitJob == __instance.job) + // just skip the fail check if something is not right. + return false; + } + LocalTargetInfo target = verb.currentTarget.IsValid ? verb.currentTarget : (__instance.pawn.mindState?.enemyTarget ?? null); + if (target.IsValid) + { + if (target.Thing is Pawn { Dead: false, Downed: false } pawn) { - if (__instance.job.verbToUse != null) + if (verb.CanHitTarget(PawnPathUtility.GetMovingShiftedPosition(pawn, 60))) { - if (__instance.pawn.stances?.curStance is Stance_Warmup || __instance.job.verbToUse.Bursting || Mod_CE.IsAimingCE(__instance.job.verbToUse)) - { - return JobCondition.Ongoing; - } - if (__instance.job.verbToUse.CanHitTarget(PawnPathUtility.GetMovingShiftedPosition(enemy, 40))) - { - return JobCondition.Ongoing; - } - if (__instance.job.verbToUse.CanHitTarget(PawnPathUtility.GetMovingShiftedPosition(enemy, 80))) - { - return JobCondition.Ongoing; - } + return false; } - comp.Notify_WaitJobEnded(); - comp.waitJob = null; - return JobCondition.Succeeded; + }else if (verb.CanHitTarget(target)) + { + return false; } + return true; } - return JobCondition.Ongoing; + return __instance.job.endIfCantShootTargetFromCurPos; }); } } diff --git a/Source/Rule56/Patches/JobGiver_AIDefendPoint_Patch.cs b/Source/Rule56/Patches/JobGiver_AIDefendPoint_Patch.cs new file mode 100644 index 0000000..148477c --- /dev/null +++ b/Source/Rule56/Patches/JobGiver_AIDefendPoint_Patch.cs @@ -0,0 +1,54 @@ +using System; +using HarmonyLib; +using RimWorld; +using Verse; +using Verse.AI; +namespace CombatAI.Patches +{ + public static class JobGiver_AIDefendPoint_Patch + { +// [HarmonyPatch(typeof(JobGiver_AIDefendPoint), nameof(JobGiver_AIDefendPoint.TryFindShootingPosition))] +// private static class JobGiver_AIDefendPoint_TryFindShootingPosition_Patch +// { +// public static bool Prefix(Pawn pawn, out IntVec3 dest, ref bool __result, Verb verbToUse) +// { +// dest = IntVec3.Invalid; +// if (verbToUse != null && pawn.CanReach(pawn.mindState.duty.focus.Cell, PathEndMode.OnCell, Danger.Unspecified) && pawn.TryGetSightReader(out SightTracker.SightReader reader) && reader.GetAbsVisibilityToEnemies(pawn.mindState.duty.focus.Cell) > 0) +// { +// Map map = pawn.Map; +// WallGrid grid = map.GetComp_Fast(); +// PawnPath path = map.pathFinder.FindPath(pawn.Position, pawn.mindState.duty.focus.Cell, pawn, PathEndMode.OnCell, null); +// if (path is { Found: true } && path.nodes.Count > 0) +// { +// try +// { +// int index = 0; +// int limit = Maths.Min((int)verbToUse.EffectiveRange + 1, path.nodes.Count); +// IntVec3 cell; +// while (index < limit && reader.GetAbsVisibilityToEnemies(cell = path.nodes[index]) > 0 && grid.GetFillCategory(cell) != FillCategory.Full) +// { +// index++; +// map.debugDrawer.FlashCell(cell, 1, "x"); +// } +// if (index >= path.nodes.Count || index == 0) +// { +// path.ReleaseToPool(); +// return true; +// } +// dest = path.nodes[index - 1]; +// path.ReleaseToPool(); +// __result = true; +// return false; +// } +// catch (Exception er) +// { +// path.ReleaseToPool(); +// throw er; +// } +// } +// } +// return true; +// } +// } + } +} diff --git a/Source/Rule56/Patches/JobGiver_AIGotoNearestHostile_Patch.cs b/Source/Rule56/Patches/JobGiver_AIGotoNearestHostile_Patch.cs index 817f185..bb4ac75 100644 --- a/Source/Rule56/Patches/JobGiver_AIGotoNearestHostile_Patch.cs +++ b/Source/Rule56/Patches/JobGiver_AIGotoNearestHostile_Patch.cs @@ -15,51 +15,6 @@ public static bool Prefix(Pawn pawn, ref Job __result) { if (pawn.TryGetSightReader(out SightTracker.SightReader reader)) { - //if (pawn.Faction.HostileTo(pawn.Map.ParentFaction)) - //{ - // Thing nearestBuildingEnemy = null; - // RegionFlooder.Flood(pawn.Position, pawn.mindState.enemyTarget == null ? pawn.Position : pawn.mindState.enemyTarget.Position, pawn.Map, - // (region, depth) => - // { - // if (reader.GetRegionAbsVisibilityToEnemies(region) > 0) - // { - // List things = region.ListerThings.ThingsInGroup(ThingRequestGroup.BuildingArtificial); - // if (things != null) - // { - // for (int i = 0; i < things.Count; i++) - // { - // Thing thing = things[i]; - // if (TrashUtility.CanTrash(pawn, thing)) - // { - - // } - // //if(thing.Faction == F) - // //Pawn other; - // //if (thing is IAttackTarget target && !target.ThreatDisabled(pawn) && AttackTargetFinder.IsAutoTargetable(target) && thing.HostileTo(pawn) && ((other = thing as Pawn) == null || other.IsCombatant())) - // //{ - // // nearestEnemy = thing; - // // return true; - // //} - // } - // } - // things = region.ListerThings.ThingsInGroup(ThingRequestGroup.BuildingArtificial); - // } - // return false; - // }, - // cost: region => - // { - // return Maths.Min(reader.GetRegionAbsVisibilityToEnemies(region), 8 * Finder.P50) * 10 * Mathf.Clamp(reader.GetRegionThreat(region) + 0.5f, 1.0f, 2.0f); - // }, - // validator: region => - // { - // return reader.GetRegionAbsVisibilityToEnemies(region) == 0; - // }, maxRegions: 512); - // if (nearestBuildingEnemy != null) - // { - // __result = TrashUtility.TrashJob(pawn, nearestBuildingEnemy, true); - // return false; - // } - //} Thing nearestEnemy = null; RegionFlooder.Flood(pawn.Position, pawn.mindState.enemyTarget == null ? pawn.Position : pawn.mindState.enemyTarget.Position, pawn.Map, (region, depth) => diff --git a/Source/Rule56/Patches/JobGiver_Wander_Patch.cs b/Source/Rule56/Patches/JobGiver_Wander_Patch.cs new file mode 100644 index 0000000..52757ea --- /dev/null +++ b/Source/Rule56/Patches/JobGiver_Wander_Patch.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using CombatAI.Comps; +using HarmonyLib; +using RimWorld; +using Verse; +using Verse.AI; +namespace CombatAI.Patches +{ + public static class JobGiver_Wander_Patch + { +// [HarmonyPatch(typeof(JobGiver_Wander), nameof(JobGiver_Wander.TryGiveJob))] +// private static class JobGiver_Wander_TryGiveJob_Patch +// { +// public static bool Prefix(JobGiver_Wander __instance, Pawn pawn) +// { +// if (pawn.Faction.IsPlayerSafe()) +// { +// return true; +// } +// // skip if the pawn is firing or warming up +// if (pawn.stances?.curStance is Stance_Warmup) +// { +// return false; +// } +// // don't skip unless it's JobGiver_WanderNearDutyLocation +// if (!(__instance is JobGiver_WanderNearDutyLocation) && !(__instance is JobGiver_WanderAnywhere)) +// { +// return true; +// } +// ThingComp_CombatAI comp = pawn.GetComp_Fast(); +// if (comp != null) +// { +// if(comp.data.InterruptedRecently(600) || comp.data.RetreatedRecently(600)) +// { +// return false; +// } +// if (comp.sightReader != null && comp.sightReader.GetVisibilityToEnemies(pawn.Position) > 0 && pawn.mindState.enemyTarget == null) +// { +// if (comp.data.NumAllies != 0) +// { +// IEnumerator allies = comp.data.AlliesNearBy(); +// while (allies.MoveNext()) +// { +// AIEnvAgentInfo ally = allies.Current; +// if (ally.thing is Pawn { Destroyed: false, Spawned: true } other && other.mindState.enemyTarget != null) +// { +// pawn.mindState.enemyTarget = other.mindState.enemyTarget; +// return false; +// } +// } +// } +// if (comp.data.NumEnemies != 0) +// { +// float minDist = float.MaxValue; +// Thing minEnemy = null; +// IEnumerator enemies = comp.data.Enemies(); +// while (enemies.MoveNext()) +// { +// AIEnvAgentInfo enemy = enemies.Current; +// if (enemy.thing is { Destroyed: false, Spawned: true }) +// { +// float dist = enemy.thing.DistanceTo_Fast(pawn); +// if (dist < minDist) +// { +// minEnemy = enemy.thing; +// minDist = dist; +// } +// } +// } +// if (minEnemy != null) +// { +// pawn.mindState.enemyTarget = minEnemy; +// return false; +// } +// } +// } +// } +// return true; +// } +// } + } +} diff --git a/Source/Rule56/Patches/LordToil_AssaultColony_Patch.cs b/Source/Rule56/Patches/LordToil_AssaultColony_Patch.cs index c68ba80..797b192 100644 --- a/Source/Rule56/Patches/LordToil_AssaultColony_Patch.cs +++ b/Source/Rule56/Patches/LordToil_AssaultColony_Patch.cs @@ -105,7 +105,7 @@ public static void Postfix(LordToil_AssaultColony __instance) for (int j = 0; j < force.Count; j++) { ThingComp_CombatAI comp = force[j].GetComp_Fast(); - if (comp != null && !comp.duties.Any(DutyDefOf.Defend)) + if (comp != null && !comp.duties.Any(CombatAI_DutyDefOf.CombatAI_AssaultPoint)) { Pawn_CustomDutyTracker.CustomPawnDuty customDuty = CustomDutyUtility.AssaultPoint(zone.Position, Rand.Range(7, 15), 3600 * Rand.Range(3, 8)); if(force[j].TryStartCustomDuty(customDuty)) @@ -135,7 +135,7 @@ public static void Postfix(LordToil_AssaultColony __instance) for (int j = 0; j < force.Count; j++) { ThingComp_CombatAI comp = force[j].GetComp_Fast(); - if (comp != null && !comp.duties.Any(DutyDefOf.Defend)) + if (comp != null && !comp.duties.Any(CombatAI_DutyDefOf.CombatAI_AssaultPoint)) { Pawn_CustomDutyTracker.CustomPawnDuty customDuty = CustomDutyUtility.AssaultPoint(thing.Position, Rand.Range(7, 15), 3600 * Rand.Range(3, 8)); if (force[j].TryStartCustomDuty(customDuty)) diff --git a/Source/Rule56/Patches/MainMenuDrawer_Patch.cs b/Source/Rule56/Patches/MainMenuDrawer_Patch.cs index f4582f0..c8cde1e 100644 --- a/Source/Rule56/Patches/MainMenuDrawer_Patch.cs +++ b/Source/Rule56/Patches/MainMenuDrawer_Patch.cs @@ -1,3 +1,4 @@ +using CombatAI.Gui; using HarmonyLib; using RimWorld; using Verse; diff --git a/Source/Rule56/Patches/PathFinder_Patch.cs b/Source/Rule56/Patches/PathFinder_Patch.cs index aac4a0d..b24129f 100644 --- a/Source/Rule56/Patches/PathFinder_Patch.cs +++ b/Source/Rule56/Patches/PathFinder_Patch.cs @@ -13,29 +13,38 @@ namespace CombatAI.Patches { public static class PathFinder_Patch { - + public static bool FlashSearch; + [HarmonyPatch(typeof(PathFinder), nameof(PathFinder.FindPath), typeof(IntVec3), typeof(LocalTargetInfo), typeof(TraverseParms), typeof(PathEndMode), typeof(PathFinderCostTuning))] private static class PathFinder_FindPath_Patch { - //private static IGridBufferedWriter gridWriter; + // debugging only + #if DEBUG + private static PathFinder flashInstance; + #endif private static ThingComp_CombatAI comp; - private static DataWriter_Path pathWriter; - private static bool dump; + private static FloatRange temperatureRange; + private static bool checkAvoidance; + private static bool checkVisibility; private static bool dig; private static Pawn pawn; private static Map map; + private static IntVec3 destPos; private static PathFinder instance; private static SightTracker.SightReader sightReader; + private static WallGrid walls; private static AvoidanceTracker.AvoidanceReader avoidanceReader; - private static bool raiders; - private static int counter; - private static ArmorReport armor; - private static bool isPlayer; +// private static int counter; private static float threatAtDest; + private static float availabilityAtDest; private static float visibilityAtDest; private static float multiplier = 1.0f; - private static readonly List blocked = new List(128); + private static bool flashCost; + private static bool isRaider; + private static bool isPlayer; + private static readonly List blocked = new List(128); private static bool fallbackCall; + private static ISGrid f_grid; private static TraverseParms original_traverseParms; private static PathEndMode origina_peMode; @@ -47,103 +56,84 @@ internal static bool Prefix(PathFinder __instance, ref PawnPath __result, IntVec { return __state = true; } - dump = false; + #if DEBUG + flashInstance = flashCost ? __instance : null; + #endif + // only allow factioned pawns. if (Finder.Settings.Pather_Enabled && (pawn = traverseParms.pawn) != null && pawn.Faction != null && (pawn.RaceProps.Humanlike || pawn.RaceProps.IsMechanoid || pawn.RaceProps.Insect)) { + destPos = dest.Cell; original_traverseParms = traverseParms; origina_peMode = peMode; // prepare the modifications instance = __instance; map = __instance.map; - pawn = traverseParms.pawn; - isPlayer = pawn.Faction.IsPlayerSafe(); - if (!isPlayer) - { - multiplier = 1; - } - else if (!pawn.Drafted) - { - multiplier = 0.75f; - } - else + walls = __instance.map.GetComp_Fast(); + f_grid = __instance.map.GetComp_Fast().f_grid; + f_grid.Reset(); + // get temperature data. + temperatureRange = new FloatRange(pawn.GetStatValue_Fast(StatDefOf.ComfyTemperatureMin, 1600), pawn.GetStatValue_Fast(StatDefOf.ComfyTemperatureMax, 1600)); + temperatureRange = temperatureRange.ExpandedBy(12); + if (temperatureRange.min >= temperatureRange.max) { - multiplier = 0.25f; + temperatureRange.min = -32; + temperatureRange.max = 128; } - // make tankier pawns unless affect. - armor = pawn.GetArmorReport(); - if (armor.createdAt != 0) - { - multiplier = Maths.Max(multiplier, 1 - armor.TankInt, 0.25f); - } - // retrive CE elements + // set faction params. + isPlayer = pawn.Faction.IsPlayerSafe(); + isRaider = !isPlayer && (pawn.HostileTo(Faction.OfPlayerSilentFail) || (map.ParentFaction != null && pawn.Faction.HostileTo(map.ParentFaction))); + // grab different readers. + pawn.TryGetAvoidanceReader(out avoidanceReader); pawn.TryGetSightReader(out sightReader); + // prepare sapping data if (sightReader != null) { - sightReader.armor = armor; - threatAtDest = sightReader.GetThreat(dest.Cell) * Finder.Settings.Pathfinding_DestWeight; - - } - pawn.Map.GetComp_Fast().TryGetReader(pawn, out avoidanceReader); - comp = pawn.GetComp_Fast(); - /* - * dump pathfinding data - */ - if (dump = Finder.Settings.Debug_DebugDumpData && !isPlayer && sightReader != null && avoidanceReader != null && Finder.Settings.Debug && Prefs.DevMode) - { - //if (gridWriter == null) - //{ - // gridWriter = new IGridBufferedWriter(__instance.map, "pathing", "path_1", new string[] - // { - // "pref", "enRel", "enAbs", "frRel", "frAbs", "path", "dang" - // }, new Type[] - // { - // typeof(float), typeof(float), typeof(float), typeof(float), typeof(float), typeof(float), typeof(float) - // }); - //} - //gridWriter.Clear(); - if (pathWriter == null) - { - pathWriter = new DataWriter_Path("pathing_csv", "pather_1"); - } - pathWriter.Clear(); - } - - float miningSkill = pawn.skills?.GetSkill(SkillDefOf.Mining)?.Level ?? 0f; - if (dig = Finder.Settings.Pather_KillboxKiller && !dump && comp != null && comp.CanSappOrEscort && pawn.mindState?.duty?.def != DutyDefOf.Sapper && pawn.CurJob?.def != JobDefOf.Mine && !comp.IsSapping && !comp.TookDamageRecently(360) && sightReader != null && sightReader.GetAbsVisibilityToEnemies(pawn.Position) == 0 && pawn.RaceProps.Humanlike && pawn.HostileTo(map.ParentFaction) - && (pawn.mindState?.duty?.def == DutyDefOf.AssaultColony || pawn.mindState?.duty?.def == DutyDefOf.AssaultThing || pawn.mindState?.duty?.def == DutyDefOf.HuntEnemiesIndividual || pawn.mindState?.duty?.def == DutyDefOf.Defend)) - { - raiders = true; - TraverseParms parms = traverseParms; - parms.canBashDoors = true; - parms.canBashFences = true; - parms.mode = TraverseMode.PassAllDestroyableThings; - parms.maxDanger = Danger.Unspecified; - traverseParms = parms; - if (tuning == null) - { - tuning = new PathFinderCostTuning(); - tuning.costBlockedDoor = 34; - tuning.costBlockedDoorPerHitPoint = 0; - tuning.costBlockedWallBase = (int)(Maths.Max(10 * Maths.Max(13 - miningSkill, 0), 24) * Finder.Settings.Pathfinding_SappingMul); - tuning.costBlockedWallExtraForNaturalWalls = (int)(Maths.Max(45 * Maths.Max(10 - miningSkill, 0), 45) * Finder.Settings.Pathfinding_SappingMul); - tuning.costBlockedWallExtraPerHitPoint = Maths.Max(3 - miningSkill, 0) * Finder.Settings.Pathfinding_SappingMul; - tuning.costOffLordWalkGrid = 0; - } - } - // get the visibility at the destination - if (sightReader != null) - { - if (!Finder.Performance.TpsCriticallyLow) + threatAtDest = sightReader.GetThreat(dest.Cell) * Finder.Settings.Pathfinding_DestWeight; + availabilityAtDest = sightReader.GetEnemyAvailability(dest.Cell) * Finder.Settings.Pathfinding_DestWeight; + visibilityAtDest = sightReader.GetVisibilityToEnemies(dest.Cell) * Finder.Settings.Pathfinding_DestWeight; + comp = pawn.GetComp_Fast(); + if (dig = Finder.Settings.Pather_KillboxKiller + && isRaider + && comp != null && comp.CanSappOrEscort && !comp.IsSapping + && !pawn.mindState.duty.Is(DutyDefOf.Sapper) && !pawn.CurJob.Is(JobDefOf.Mine) && !pawn.mindState.duty.Is(DutyDefOf.ExitMapRandom) && !pawn.mindState.duty.Is(DutyDefOf.Escort)) { - visibilityAtDest = Maths.Min(sightReader.GetVisibilityToEnemies(dest.Cell) * Finder.Settings.Pathfinding_DestWeight, 5); - } - else - { - visibilityAtDest = Maths.Min(sightReader.GetVisibilityToEnemies(dest.Cell) * 0.875f, 5); + float costMultiplier = 1; + costMultiplier *= comp.TookDamageRecently(360) ? 4 : 1; + float miningSkill = pawn.skills?.GetSkill(SkillDefOf.Mining)?.Level ?? 0f; + isRaider = true; + TraverseParms parms = traverseParms; + parms.canBashDoors = true; + parms.canBashFences = true; + bool humanlike = pawn.RaceProps.Humanlike; + if (humanlike) + { + parms.mode = TraverseMode.PassAllDestroyableThingsNotWater; + parms.maxDanger = Danger.Unspecified; + } + else + { + parms.mode = TraverseMode.PassDoors; + } + traverseParms = parms; + if (tuning == null) + { + tuning = new PathFinderCostTuning(); + tuning.costBlockedDoor = (int)(15f * costMultiplier); + tuning.costBlockedDoorPerHitPoint = costMultiplier - 1; + if (humanlike) + { + tuning.costBlockedWallBase = (int)(32f * costMultiplier); + tuning.costBlockedWallExtraForNaturalWalls = (int)(32f * costMultiplier); + tuning.costBlockedWallExtraPerHitPoint = (20f - Mathf.Clamp(miningSkill, 5, 15)) / 100 * Finder.Settings.Pathfinding_SappingMul * costMultiplier; + tuning.costOffLordWalkGrid = 0; + } + } } } - raiders |= pawn.HostileTo(Faction.OfPlayerSilentFail); - counter = 0; + checkAvoidance = Finder.Settings.Flank_Enabled && avoidanceReader != null && !isPlayer; + checkVisibility = sightReader != null; +// counter = 0; + flashCost = Finder.Settings.Debug_DebugPathfinding && Find.Selector.SelectedPawns.Contains(pawn); return __state = true; } __state = false; @@ -158,16 +148,9 @@ public static void Postfix(PathFinder __instance, ref PawnPath __result, bool __ { return; } - if (dump) - { - if (pathWriter != null) - { - pathWriter.Write(); - } - } if (__state) { - if (Finder.Settings.Pather_KillboxKiller && dig && __result != null && !__result.nodes.NullOrEmpty() && (pawn?.RaceProps.Humanlike ?? false)) + if (dig && !(__result?.nodes.NullOrEmpty() ?? true)) { blocked.Clear(); Thing blocker; @@ -206,23 +189,30 @@ public static void Postfix(PathFinder __instance, ref PawnPath __result, bool __ tracker.Notify_PathFound(pawn, __result); } } + if (FlashSearch) + { + FlashSearch = false; + } Reset(); } public static void Reset() { avoidanceReader = null; - raiders = false; + isRaider = false; + isPlayer = false; multiplier = 1f; sightReader = null; - counter = 0; +// counter = 0; dig = false; threatAtDest = 0; - armor = default(ArmorReport); instance = null; visibilityAtDest = 0f; map = null; + walls = null; + flashCost = false; pawn = null; + f_grid = null; } /* @@ -254,96 +244,81 @@ public static IEnumerable Transpiler(IEnumerable visibilityAtDest) + // find the cell cost offset + if (Finder.Settings.Temperature_Enabled) { - value += (int)(visibility * 65); - threat = sightReader.GetThreat(index); - if (threat >= threatAtDest) + float temperature = GenTemperature.TryGetTemperature(index, map); + if (!temperatureRange.Includes(temperature)) { - value += (int)(threat * 64f); + if (temperatureRange.min > temperature) + { + value += Maths.Min(temperatureRange.min - temperature, 32) * 3; + } + else + { + value += Maths.Min(temperature - temperatureRange.max, 32) * 3; + } } } - if (!isPlayer) + if (checkVisibility) { - MetaCombatAttribute attributes = sightReader.GetMetaAttributes(index); - if ((attributes & MetaCombatAttribute.AOELarge) != MetaCombatAttribute.None) + float visibility = Maths.Max((sightReader.GetVisibilityToEnemies(index) - visibilityAtDest) * 0.5f + (sightReader.GetEnemyAvailability(index) - availabilityAtDest) * 0.5f, 0); + if (visibility > 0) { - path = 2f; + // this allows us to reduce the cost as we approach the target. + int dist = (destPos - map.cellIndices.IndexToCell(index)).LengthManhattan; + float mul = dist < 15 ? dist / 15 : 1f; + value += (int)(visibility * 11f * mul); + float threat = Maths.Max(sightReader.GetThreat(index) - threatAtDest, 0); + if (threat > 0) + { + value += (threat * 22f * mul); + } + } + if (dig && walls.GetFillCategory(index) == FillCategory.Full) + { + float visibilityParent = sightReader.GetAbsVisibilityToEnemies(parentIndex); + if (visibilityParent > 0) + { + // we do this to prevent sapping where there is enemies. + value = (1000 * visibilityParent); + } } } - } - if (avoidanceReader != null && !isPlayer && Finder.Settings.Flank_Enabled) - { - if (value > 3) - { - value += (int)(avoidanceReader.GetPath(index) * Maths.Min(visibility, 5) * 21f * path); - } - else - { - value += (int)(Maths.Min(avoidanceReader.GetPath(index), 4) * 42 * path); - } - float danger = avoidanceReader.GetDanger(index); - if (danger > 0) + if (checkAvoidance) { - value += 23; + value += (avoidanceReader.GetDanger(index) * 8) + avoidanceReader.GetProximity(index) * 6; } - value += (int)danger; + f_grid[index] = value; } - if (dump) + if (checkAvoidance) { - /* - * fields - * "pref", "enRel", "enAbs", "frRel", "frAbs", "path", "dang" - */ - //if (gridWriter != null) - //{ - // gridWriter["pref"].SetValue(value, index); - // gridWriter["enRel"].SetValue(sightReader.GetVisibilityToEnemies(index), index); - // gridWriter["enAbs"].SetValue(sightReader.GetAbsVisibilityToEnemies(index), index); - // gridWriter["frRel"].SetValue(sightReader.GetVisibilityToFriendlies(index), index); - // gridWriter["frAbs"].SetValue(sightReader.GetAbsVisibilityToFriendlies(index), index); - // gridWriter["path"].SetValue(avoidanceReader.GetPath(index), index); - // gridWriter["dang"].SetValue(avoidanceReader.GetDanger(index), index); - //} - if (pathWriter != null) - { - pathWriter.Push(new DataWriter_Path.PathCell - { - pref = value, - enRel = sightReader.GetVisibilityToEnemies(index), - enAbs = sightReader.GetAbsVisibilityToEnemies(index), - frRel = sightReader.GetVisibilityToFriendlies(index), - frAbs = sightReader.GetAbsVisibilityToFriendlies(index), - dang = avoidanceReader.GetDanger(index), - prox = avoidanceReader.GetProximity(index), - path = avoidanceReader.GetPath(index) - }); - } + // we only care if the paths are parallel to each others. + value += Maths.Min(avoidanceReader.GetPath(index), avoidanceReader.GetPath(parentIndex)) * 11; } - //Log.Message($"{value} {sightReader != null} {sightReader.hostile != null} {sightReader.GetVisibility(index)} {sightReader.hostile.GetSignalStrengthAt(index)}"); - //map.debugDrawer.FlashCell(map.cellIndices.IndexToCell(index), sightReader.hostile.GetSignalStrengthAt(index), $"{value}_ "); - if (value > 10f) + if (value > 0) { - counter++; // // TODO make this into a maxcost -= something system - float l1 = 350 * (1f - Mathf.Lerp(0f, 0.75f, counter / (openNum + 1f))) * (1f - Maths.Min(openNum, 5000) / 7500); - float l2 = 250 * (1f - Mathf.Clamp01(PathFinder.calcGrid[parentIndex].knownCost / 2500)); - //we use this so the game doesn't die - float v = Maths.Min(value, l1 + l2) * multiplier * 1; - //map.debugDrawer.FlashCell(map.cellIndices.IndexToCell(index), v, $" {l1 + l2}"); - return (int)(Maths.Min(value, l1 + l2) * multiplier * Finder.P75); - //return (int)(Maths.Min(value, 1000f) * factionMultiplier * 1); + value = (value * multiplier * Finder.P75) * Mathf.Clamp(1 - PathFinder.calcGrid[parentIndex].knownCost / 2500, 0.1f, 1); } + return (int) value; } return 0; } diff --git a/Source/Rule56/Patches/Pawn_JobTracker_Patch.cs b/Source/Rule56/Patches/Pawn_JobTracker_Patch.cs index 010baad..149832e 100644 --- a/Source/Rule56/Patches/Pawn_JobTracker_Patch.cs +++ b/Source/Rule56/Patches/Pawn_JobTracker_Patch.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Diagnostics; using CombatAI.Comps; using HarmonyLib; using RimWorld; @@ -12,11 +14,27 @@ public static class Pawn_JobTracker_TryTakeOrderedJob_Patch { public static void Postfix(Pawn_JobTracker __instance) { - if (__instance.pawn.Faction.IsPlayerSafe() && __instance.pawn.GetComp() is var comp) + if (__instance.pawn.Faction.IsPlayerSafe() && __instance.pawn.GetComp() is ThingComp_CombatAI comp) { comp.forcedTarget = LocalTargetInfo.Invalid; } } - } + } + + [HarmonyPatch(typeof(Pawn_JobTracker), nameof(Pawn_JobTracker.StartJob))] + public static class Pawn_StartJob_Patch + { + public static void Postfix(Pawn_JobTracker __instance, Job newJob, JobCondition lastJobEndCondition, ThinkNode jobGiver, bool cancelBusyStances) + { + if (Finder.Settings.Debug_LogJobs && Finder.Settings.Debug && __instance.pawn.GetComp() is ThingComp_CombatAI comp) + { + comp.jobLogs ??= new List(); + if (!comp.jobLogs.Any(j => j.id == newJob.loadID)) + { + comp.jobLogs.Insert(0,JobLog.For(__instance.pawn, newJob, "unknown")); + } + } + } + } } } diff --git a/Source/Rule56/Patches/TargetHighlighter_Patch.cs b/Source/Rule56/Patches/TargetHighlighter_Patch.cs new file mode 100644 index 0000000..000a50d --- /dev/null +++ b/Source/Rule56/Patches/TargetHighlighter_Patch.cs @@ -0,0 +1,21 @@ +using HarmonyLib; +using RimWorld; +using RimWorld.Planet; +namespace CombatAI.Patches +{ + public static class TargetHighlighter_Patch + { + [HarmonyPatch(typeof(TargetHighlighter), nameof(TargetHighlighter.Highlight))] + private static class TargetHighlighter_Highlight + { + public static bool Prefix(GlobalTargetInfo target) + { + if (Finder.Settings.FogOfWar_Enabled && target is { IsValid: true, IsMapTarget: true }) + { + return !target.Map.GetComp_Fast().IsFogged(target.Cell); + } + return true; + } + } + } +} diff --git a/Source/Rule56/Patches/ThinkNode_Patch.cs b/Source/Rule56/Patches/ThinkNode_Patch.cs new file mode 100644 index 0000000..aee8c7c --- /dev/null +++ b/Source/Rule56/Patches/ThinkNode_Patch.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using CombatAI.Comps; +using HarmonyLib; +using Verse; +using Verse.AI; +namespace CombatAI.Patches +{ + public class ThinkNode_Patch + { + private static Pawn curPawn; + private static readonly HashSet processedJobs = new HashSet(); + + [HarmonyPatch] + private static class ThinkNode_TryIssueJobPackage_Patch + { + public static IEnumerable TargetMethods() + { + HashSet methods = new HashSet(); + foreach (Type t in typeof(ThinkNode).AllSubclasses()) + { + foreach (var method in t.GetMethods(AccessTools.all)) + { + if (method != null && !methods.Contains(method) && method.HasMethodBody() && !method.IsAbstract && method.ReturnType == typeof(ThinkResult) && method.Name == "TryIssueJobPackage" && method.DeclaringType == t) + { + if (Finder.Settings.Debug) + { + Log.Message($"ISMA: Patched thinknode type {t.FullName}:{method.Name}"); + } + methods.Add(method); + } + } + } + return methods; + } + + public static void Postfix(ThinkNode __instance, ThinkResult __result, Pawn pawn) + { + if (Enabled && __result is { IsValid: true, Job: { } } && !processedJobs.Contains(__result.Job)) + { + if (curPawn != pawn) + { + curPawn = pawn; + processedJobs.Clear(); + } + processedJobs.Add(__result.Job); + JobLog log = JobLog.For(pawn, __result.Job, __instance); + if (log.IsValid) + { + ThingComp_CombatAI comp = pawn.GetComp_Fast(); + if (comp != null) + { + comp.jobLogs ??= new List(); + comp.jobLogs.Insert(0, log); + while (comp.jobLogs.Count > 64) + { + // limit size to 32 + comp.jobLogs.RemoveAt(comp.jobLogs.Count - 1); + } + } + } + } + } + } + + private static bool Enabled + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Finder.Settings.Debug && Finder.Settings.Debug_LogJobs; + } + } +} diff --git a/Source/Rule56/Patches/Verb_Patch.cs b/Source/Rule56/Patches/Verb_Patch.cs new file mode 100644 index 0000000..09d2192 --- /dev/null +++ b/Source/Rule56/Patches/Verb_Patch.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Reflection; +using HarmonyLib; +using System; +using CombatAI.Comps; +using RimWorld; +using UnityEngine; +using Verse; +namespace CombatAI.Patches +{ + public class Verb_Patch + { + private static Verb callerVerb; + + [HarmonyPatch] + private static class Verb_TryStartCast_Patch + { + public static IEnumerable TargetMethods() + { + HashSet methods = new HashSet(); + foreach (Type t in typeof(Verb).AllSubclasses()) + { + foreach (var method in t.GetMethods(AccessTools.all)) + { + if (method != null && !methods.Contains(method) && !method.IsStatic && method.ReturnType == typeof(bool) && (method.Name.Contains("TryStartCastOn") || method.Name.Contains("TryCastShot")) && !method.IsAbstract && method.HasMethodBody() && method.DeclaringType == t) + { + Log.Message($"ISMA: Patched verb type {t.FullName}:{method.Name}"); + methods.Add(method); + } + } + } + foreach (var method in typeof(Verb).GetMethods(AccessTools.all)) + { + if (method != null && !methods.Contains(method) && !method.IsStatic && method.ReturnType == typeof(bool) && (method.Name.Contains("TryStartCastOn") || method.Name.Contains("TryCastShot")) && !method.IsAbstract && method.HasMethodBody() && method.DeclaringType == typeof(Verb)) + { + Log.Message($"ISMA: Patched verb type {typeof(Verb).FullName}:{method.Name}"); + methods.Add(method); + } + } + return methods; + } + + public static void Prefix(Verb __instance, out bool __state) + { + if (__state = (__instance != callerVerb)) + { + callerVerb = __instance; + } + } + + public static void Postfix(Verb __instance, bool __result, bool __state) + { + if (__state && __result && __instance.caster != null) + { + if (__instance.CurrentTarget is { IsValid: true, Thing: Pawn targetPawn } && (__instance.caster?.HostileTo(targetPawn) ?? false)) + { + ThingComp_CombatAI comp = targetPawn.GetComp_Fast(); + if (comp != null) + { + // + // Log.Message($"{targetPawn} is being targeted by {__instance.caster}"); + comp.Notify_BeingTargeted(__instance.caster, __instance); + } + } + } + } + } + } +} diff --git a/Source/Rule56/PerformanceTracker.cs b/Source/Rule56/PerformanceTracker.cs index a39c360..d2412b7 100644 --- a/Source/Rule56/PerformanceTracker.cs +++ b/Source/Rule56/PerformanceTracker.cs @@ -88,13 +88,13 @@ public override void GameComponentTick() public override void GameComponentOnGUI() { base.GameComponentOnGUI(); - if (Finder.Settings.Debug) - { - string lowMsg = TpsCriticallyLow ? "LOW" : "NROMAL"; - Widgets.DrawBoxSolid(new Rect(Vector2.zero, new Vector2(100, 5)), Color.gray); - Widgets.DrawBoxSolid(new Rect(Vector2.zero, new Vector2(100 * Performance, 5)), TpsCriticallyLow ? Color.yellow : Color.blue); - Widgets.Label(new Rect(Vector2.zero, new Vector2(100, 25)), $"{Tps}\t{lowMsg}\t{Math.Round(Performance, 2)}"); - } +// if (Finder.Settings.Debug) +// { +// string lowMsg = TpsCriticallyLow ? "LOW" : "NROMAL"; +// Widgets.DrawBoxSolid(new Rect(Vector2.zero, new Vector2(100, 5)), Color.gray); +// Widgets.DrawBoxSolid(new Rect(Vector2.zero, new Vector2(100 * Performance, 5)), TpsCriticallyLow ? Color.yellow : Color.blue); +// Widgets.Label(new Rect(Vector2.zero, new Vector2(100, 25)), $"{Tps}\t{lowMsg}\t{Math.Round(Performance, 2)}"); +// } } } } diff --git a/Source/Rule56/Settings.cs b/Source/Rule56/Settings.cs index fbcad43..e65aad3 100644 --- a/Source/Rule56/Settings.cs +++ b/Source/Rule56/Settings.cs @@ -4,7 +4,7 @@ namespace CombatAI public class Settings : ModSettings { - private const int version = 2; + private const int version = 3; public int Advanced_SightThreadIdleSleepTimeMS = 1; /* @@ -21,8 +21,14 @@ public class Settings : ModSettings * -- Debug -- * */ - - public bool Debug; + + #if DEBUG + public bool Debug = true; + public bool Debug_LogJobs = true; + #else + public bool Debug = false; + public bool Debug_LogJobs = false; + #endif public bool Debug_DebugDumpData = false; public bool Debug_DebugThingsTracker = false; public bool Debug_DrawAvoidanceGrid_Danger = false; @@ -31,6 +37,8 @@ public class Settings : ModSettings public bool Debug_DrawShadowCastsVectors = false; public bool Debug_DrawThreatCasts = false; public bool Debug_DisablePawnGuiOverlay = false; + public bool Debug_DebugPathfinding = false; + public bool Debug_DebugAvailability = false; public bool Debug_ValidateSight; public bool Enable_Groups = true; @@ -62,6 +70,7 @@ public class Settings : ModSettings public bool Pather_KillboxKiller = true; public float Pathfinding_DestWeight = 0.85f; public float Pathfinding_SappingMul = 1.0f; + public bool Temperature_Enabled = true; public bool PerformanceOpt_Enabled = true; public bool React_Enabled = true; public bool Retreat_Enabled = true; @@ -102,18 +111,20 @@ public override void ExposeData() SightSettings_SettlementTurrets = new SightPerformanceSettings(8, 15, 12); } Scribe_Values.Look(ref LeanCE_Enabled, $"LeanCE_Enabled.{version}"); - //if (!LeanCE_Enabled && Mod_CE.active) - //{ - // LeanCE_Enabled = true; - //} - //else if (LeanCE_Enabled && !Mod_CE.active) - //{ - // LeanCE_Enabled = false; - //} + + #if DEBUG + Scribe_Values.Look(ref Debug, $"Debug.Debug.{version}", true); + Scribe_Values.Look(ref Debug_LogJobs, $"Debug.Debug_LogJobs.1.{version}", true); + #else + Scribe_Values.Look(ref Debug, $"Release.Debug.{version}", false); + Scribe_Values.Look(ref Debug_LogJobs, $"Release.Debug_LogJobs.{version}", false); + #endif + Scribe_Values.Look(ref FinishedQuickSetup, $"FinishedQuickSetup2.{version}", false); Scribe_Values.Look(ref Pather_Enabled, $"Pather_Enabled.{version}", true); Scribe_Values.Look(ref Caster_Enabled, $"Caster_Enabled.{version}", true); Scribe_Values.Look(ref Targeter_Enabled, $"Targeter_Enabled.{version}", true); + Scribe_Values.Look(ref Temperature_Enabled, $"Temperature_Enabled.{version}", true); Scribe_Values.Look(ref React_Enabled, $"React_Enabled.{version}", true); Scribe_Values.Look(ref Retreat_Enabled, $"Retreat_Enabled.{version}", true); Scribe_Values.Look(ref Flank_Enabled, $"Flank_Enabled.{version}", true); @@ -125,7 +136,6 @@ public override void ExposeData() Scribe_Values.Look(ref Pather_KillboxKiller, $"Pather_KillboxKiller.{version}", true); Scribe_Values.Look(ref PerformanceOpt_Enabled, $"PerformanceOpt_Enabled.{version}", true); Scribe_Values.Look(ref FogOfWar_Enabled, $"FogOfWar_Enabled.{version}"); - Scribe_Values.Look(ref Debug, $"Debug.{version}"); Scribe_Values.Look(ref Debug_ValidateSight, $"Debug_ValidateSight.{version}"); Scribe_Values.Look(ref Debug_DrawShadowCasts, $"Debug_DrawShadowCasts.{version}"); Scribe_Values.Look(ref Enable_Sprinting, $"Enable_Sprinting.{version}", true); diff --git a/Source/Rule56/ShadowCastingUtility.cs b/Source/Rule56/ShadowCastingUtility.cs index a55f41b..2335962 100644 --- a/Source/Rule56/ShadowCastingUtility.cs +++ b/Source/Rule56/ShadowCastingUtility.cs @@ -400,6 +400,7 @@ private static void TryCast(Action castAction, float startSlope, fl arcLeft.visibilityCarry = 1; arcLeft.maxDepth = maxDepth - 1; arcLeft.quartor = quartor; + arcLeft.visible = true; requestLeft.firstRow = arcLeft; requestLeft.grid = grid; requestLeft.carryLimit = carryLimit; @@ -418,6 +419,7 @@ private static void TryCast(Action castAction, float startSlope, fl arcRight.visibilityCarry = 1; arcRight.maxDepth = maxDepth - 1; arcRight.quartor = quartor; + arcRight.visible = true; requestRight.firstRow = arcRight; requestRight.grid = grid; requestRight.carryLimit = carryLimit; @@ -437,6 +439,7 @@ private static void TryCast(Action castAction, float startSlope, fl arc.endSlope = endSlope; arc.visibilityCarry = 1; arc.maxDepth = maxDepth; + arc.visible = true; arc.quartor = quartor; request.firstRow = arc; request.grid = grid; @@ -465,6 +468,7 @@ private static void TryCastSimple(Action castAction, float startSlo arc.visibilityCarry = 1; arc.maxDepth = maxDepth; arc.quartor = quartor; + arc.visible = true; request.firstRow = arc; request.grid = grid; request.carryLimit = carryLimit; @@ -496,6 +500,7 @@ private struct VisibleRow public int quartor; public int maxDepth; public float blockChance; + public bool visible; public void Tiles(List buffer) { @@ -546,6 +551,7 @@ public VisibleRow Next() row.quartor = quartor; row.visibilityCarry = visibilityCarry; row.blockChance = blockChance; + row.visible = visible; return row; } @@ -558,6 +564,7 @@ public static VisibleRow First row.endSlope = 1; row.depth = 1; row.visibilityCarry = 1; + row.visible = true; return row; } } diff --git a/Source/Rule56/SightGrid.cs b/Source/Rule56/SightGrid.cs index e0fe0dd..e050f60 100644 --- a/Source/Rule56/SightGrid.cs +++ b/Source/Rule56/SightGrid.cs @@ -13,8 +13,8 @@ public class SightGrid private const int COVERCARRYLIMIT = 6; private readonly AsyncActions asyncActions; private readonly IBuckets buckets; - private readonly CellFlooder flooder; private readonly List buffer = new List(1024); + private readonly CellFlooder flooder; /// /// Sight grid contains all sight data. /// @@ -207,15 +207,15 @@ public virtual void TryDeRegister(Thing thing) if (trackFactions) { IBucketableThing bucketable = buckets.GetById(thing.thingIDNumber); - if (bucketable != null && numsByFaction.TryGetValue(bucketable.faction, out int num)) + if (bucketable != null && numsByFaction.TryGetValue(bucketable.registeredFaction, out int num)) { if (num > 1) { - numsByFaction[bucketable.faction] = num - 1; + numsByFaction[bucketable.registeredFaction] = num - 1; } else { - numsByFaction.Remove(bucketable.faction); + numsByFaction.Remove(bucketable.registeredFaction); } } } @@ -260,7 +260,7 @@ private void Continue() private bool Consistent(IBucketableThing item) { - if (item.faction != item.thing.Faction) + if (item.registeredFaction != item.thing.Faction) { return false; } @@ -282,7 +282,7 @@ private bool Valid(Thing thing) private bool Valid(IBucketableThing item) { - return !item.thing.Destroyed && item.thing.Spawned && (item.pawn == null || !item.pawn.Dead); + return !item.thing.Destroyed && item.thing.Spawned && (item.Pawn == null || !item.Pawn.Dead); } private bool Skip(IBucketableThing item) @@ -291,17 +291,17 @@ private bool Skip(IBucketableThing item) { return !item.dormant.Awake || item.dormant.WaitingToWakeUp; } - if (item.pawn != null) + if (item.Pawn != null) { - return !playerAlliance && (GenTicks.TicksGame - item.pawn.needs?.rest?.lastRestTick <= 30 || item.pawn.Downed); + return !playerAlliance && (GenTicks.TicksGame - item.Pawn.needs?.rest?.lastRestTick <= 30 || item.Pawn.Downed); } if (item.sighter != null) { return playerAlliance && !item.sighter.Active; } - if (item.turretGun != null) + if (item.TurretGun != null) { - return playerAlliance && (!item.turretGun.Active || item.turretGun.IsMannable && !(item.turretGun.mannableComp?.MannedNow ?? false)); + return playerAlliance && (!item.TurretGun.Active || item.TurretGun.IsMannable && !(item.TurretGun.mannableComp?.MannedNow ?? false)); } if (Mod_CE.active && item.thing is Building_Turret turret) { @@ -317,7 +317,6 @@ private ulong GetFlags(IBucketableThing item) private bool TryCastSight(IBucketableThing item) { - if (grid.CycleNum == item.lastCycle || Skip(item)) { return false; @@ -343,29 +342,33 @@ private bool TryCastSight(IBucketableThing item) return false; } IntVec3 flagPos = pos; - if (item.pawn != null) + if (item.Pawn != null) { - flagPos = GetShiftedPosition(item.pawn, 60, null); + flagPos = GetShiftedPosition(item.Pawn, 60, null); } SightTracker.SightReader reader = item.ai?.sightReader ?? null; bool scanForEnemies; - bool moveAttackScan = false; if (scanForEnemies = Finder.Settings.React_Enabled && item.sighter == null && reader != null && item.ai != null && !item.ai.ReactedRecently(45) && ticks - item.lastScannedForEnemies >= (!Finder.Performance.TpsCriticallyLow ? 10 : 15)) { - if (!item.isPlayer || (moveAttackScan = (item.ai?.forcedTarget.IsValid ?? false))) + if (!item.registeredFaction.IsPlayerSafe() || (item.ai?.forcedTarget.IsValid ?? false)) { if (item.dormant != null && !item.dormant.Awake) { scanForEnemies = false; } - else if (item.pawn != null && item.pawn.mindState?.duty?.def == DutyDefOf.SleepForever) + else if (item.Pawn != null && item.Pawn.mindState?.duty?.def == DutyDefOf.SleepForever) { scanForEnemies = false; } } } + bool defenseMode = false; if (scanForEnemies) { + if (item.Pawn != null && (item.Pawn.mindState.duty.Is(DutyDefOf.Defend) || item.Pawn.mindState.duty.Is(CombatAI_DutyDefOf.CombatAI_AssaultPoint)) && item.Pawn.CurJob.Is(JobDefOf.Wait_Wander)) + { + defenseMode = true; + } item.lastScannedForEnemies = ticks; try { @@ -390,18 +393,34 @@ private bool TryCastSight(IBucketableThing item) if (item.thing != null) { Verb verb = item.thing.TryGetAttackVerb(); - if (verb != null && !verb.IsMeleeAttack) + if (verb != null) { - if (verb.state != VerbState.Idle || verb.WarmingUp) + if (!verb.IsMeleeAttack) { - availability = MetaCombatAttribute.Occupied; + if (verb.WarmingUp || verb.Bursting) + { + availability = MetaCombatAttribute.Occupied; + } + else + { + availability = MetaCombatAttribute.Free; + } } - else + else if (item.Pawn != null) { - availability = MetaCombatAttribute.Free; + if (item.Pawn.mindState.MeleeThreatStillThreat) + { + availability = MetaCombatAttribute.Occupied; + } + else + { + availability = MetaCombatAttribute.Free; + } } } } + bool engagedInMelee = item.Pawn?.mindState.MeleeThreatStillThreat == true; + scanForEnemies &= !engagedInMelee; ISightRadius sightRadius = item.cachedSightRadius; Action action = () => { @@ -414,8 +433,9 @@ private bool TryCastSight(IBucketableThing item) gridFog.Set(item.path[i], 1.0f); } } - grid.Next(item.cachedDamage.adjustedSharp, item.cachedDamage.adjustedBlunt, item.cachedDamage.attributes | availability); - grid_regions.Next(GetFlags(item), item.cachedDamage.adjustedSharp, item.cachedDamage.adjustedBlunt, item.cachedDamage.attributes | availability); + MetaCombatAttribute attr = item.cachedDamage.attributes | availability; + grid.Next(item.cachedDamage.adjustedSharp, item.cachedDamage.adjustedBlunt, attr); + grid_regions.Next(GetFlags(item), item.cachedDamage.adjustedSharp, item.cachedDamage.adjustedBlunt, attr); float r_fade = sightRadius.fog * Finder.Settings.FogOfWar_RangeFadeMultiplier; float d_fade = sightRadius.fog - r_fade; float rSqr_sight = Maths.Sqr(sightRadius.sight); @@ -426,9 +446,9 @@ private bool TryCastSight(IBucketableThing item) { float d2 = pos.DistanceToSquared(cell); float visibility = 0f; - if (d2 < rSqr_sight) + if (!engagedInMelee && d2 < rSqr_sight) { - visibility = (float)(sightRadius.sight - dist) / sightRadius.sight * (1 - coverRating); + visibility = Maths.Max(1f - coverRating, 0.20f); if (visibility > 0f) { grid.Set(cell, visibility, new Vector2(cell.x - pos.x, cell.z - pos.z)); @@ -472,8 +492,11 @@ private bool TryCastSight(IBucketableThing item) } flooder.Flood(origin, node => { - grid.Set(node.cell, item.cachedDamage.attributes | availability); - grid_regions.Set(node.cell); + if (!grid.IsSet(node.cell)) + { + grid.Set(node.cell, 0.198f, new Vector2(node.cell.x - node.parent.x, node.cell.z - node.parent.z)); + grid_regions.Set(node.cell); + } if (scanForEnemies) { ulong flag = reader.GetEnemyFlags(node.cell) | reader.GetFriendlyFlags(node.cell); @@ -487,29 +510,26 @@ private bool TryCastSight(IBucketableThing item) item.spottings.Add(spotting); } } - }, maxDist: 10); + }, maxDist: defenseMode ? 32 : 12, maxCellNum: defenseMode ? 325 : 225, passThroughDoors: true); + grid.Set(origin, 1.0f, new Vector2(origin.x - pos.x, origin.z - pos.z)); grid.Set(pos, 1.0f, new Vector2(origin.x - pos.x, origin.z - pos.z)); grid.Next(0, 0, item.cachedDamage.attributes); - grid.Set(flagPos, item.pawn == null || !item.pawn.Downed ? GetFlags(item) : 0); + grid.Set(flagPos, item.Pawn == null || !item.Pawn.Downed ? GetFlags(item) : 0); if (scanForEnemies) { - if (item.ai.data.NumEnemies > 0 || item.ai.data.NumAllies > 0 || item.spottings.Count > 0 || (Finder.Settings.Debug && Finder.Settings.Debug_ValidateSight)) + if (item.ai.data.NumEnemies > 0 || item.ai.data.NumAllies > 0 || item.spottings.Count > 0 || Finder.Settings.Debug && Finder.Settings.Debug_ValidateSight) { // on the main thread check for enemies on or near this cell. asyncActions.EnqueueMainThreadAction(delegate - { + { if (!item.thing.Destroyed && item.thing.Spawned) { for (int i = 0; i < item.spottings.Count; i++) - { + { ISpotRecord record = item.spottings[i]; thingBuffer1.Clear(); sightTracker.factionedUInt64Map.GetThings(record.flag, thingBuffer1); -// if (Find.Selector.selected.Contains(item.thing)) -// { -// map.debugDrawer.FlashCell(record.cell); -// } for (int j = 0; j < thingBuffer1.Count; j++) { Thing agent = thingBuffer1[j]; @@ -520,12 +540,12 @@ private bool TryCastSight(IBucketableThing item) { if (agent.HostileTo(item.thing)) { - item.ai.Notify_Enemy(new AIEnvAgentInfo(agent, (AIEnvAgentState) record.state)); + item.ai.Notify_Enemy(new AIEnvAgentInfo(agent, (AIEnvAgentState)record.state)); } else { - item.ai.Notify_Ally(new AIEnvAgentInfo(agent, (AIEnvAgentState) record.state)); - } + item.ai.Notify_Ally(new AIEnvAgentInfo(agent, (AIEnvAgentState)record.state)); + } } } } @@ -621,7 +641,7 @@ private struct ISpotRecord /// public IntVec3 cell; /// - /// state + /// state /// public int state; /// @@ -645,25 +665,17 @@ private class IBucketableThing : IBucketable /// public readonly CompCanBeDormant dormant; /// - /// Thing's faction on IBucketableThing instance creation. - /// - public readonly Faction faction; - /// /// Current sight grid. /// public readonly SightGrid grid; /// - /// Thing. - /// - public readonly bool isPlayer; - /// /// Pawn pawn /// public readonly List path = new List(16); /// - /// Thing. + /// Thing's faction on IBucketableThing instance creation. /// - public readonly Pawn pawn; + public readonly Faction registeredFaction; /// /// Sighting component. /// @@ -677,10 +689,6 @@ private class IBucketableThing : IBucketable /// public readonly Thing thing; /// - /// Thing. - /// - public readonly Building_TurretGun turretGun; - /// /// Thing potential damage report. /// public DamageReport cachedDamage; @@ -701,19 +709,30 @@ public IBucketableThing(SightGrid grid, Thing thing, int bucketIndex) { this.grid = grid; this.thing = thing; - pawn = thing as Pawn; - turretGun = thing as Building_TurretGun; - isPlayer = thing.Faction.IsPlayerSafe(); dormant = thing.GetComp_Fast(); ai = thing.GetComp_Fast(); sighter = thing.GetComp_Fast(); - faction = thing.Faction; + registeredFaction = thing.Faction; BucketIndex = bucketIndex; cachedDamage = DamageUtility.GetDamageReport(thing); cachedSightRadius = SightUtility.GetSightRadius(thing); CctvTop = thing.GetComp_Fast(); } /// + /// Thing. + /// + public Pawn Pawn + { + get => thing as Pawn; + } + /// + /// Thing. + /// + public Building_TurretGun TurretGun + { + get => thing as Building_TurretGun; + } + /// /// Bucket index. /// public int BucketIndex diff --git a/Source/Rule56/SightTracker.cs b/Source/Rule56/SightTracker.cs index f5c4300..c7a8998 100644 --- a/Source/Rule56/SightTracker.cs +++ b/Source/Rule56/SightTracker.cs @@ -78,7 +78,7 @@ public override void MapComponentTick() } // // debugging stuff. - if ((Finder.Settings.Debug_DrawShadowCasts || Finder.Settings.Debug_DrawThreatCasts) && GenTicks.TicksGame % 15 == 0) + if ((Finder.Settings.Debug_DrawShadowCasts || Finder.Settings.Debug_DrawThreatCasts || Finder.Settings.Debug_DebugAvailability) && GenTicks.TicksGame % 15 == 0) { _drawnCells.Clear(); if (!Find.Selector.SelectedPawns.NullOrEmpty()) @@ -128,7 +128,7 @@ public override void MapComponentTick() } } } - else if (Finder.Settings.Debug_DrawShadowCasts) + else if (Finder.Settings.Debug_DrawShadowCasts || Finder.Settings.Debug_DebugAvailability) { IntVec3 center = UI.MouseMapPosition().ToIntVec3(); if (center.InBounds(map)) @@ -141,14 +141,28 @@ public override void MapComponentTick() if (cell.InBounds(map) && !_drawnCells.Contains(cell)) { _drawnCells.Add(cell); -// float value = raidersAndHostiles.grid.GetSignalStrengthAt(cell, out int enemies1) + colonistsAndFriendlies.grid.GetSignalStrengthAt(cell, out int enemies2) + insectsAndMechs.grid.GetSignalStrengthAt(cell, out int enemies4); - Region region = cell.GetRegion(map); - if (region != null) + if (Finder.Settings.Debug_DrawShadowCasts) { - float value = raidersAndHostiles.grid_regions.GetSignalNumByRegion(region) + colonistsAndFriendlies.grid_regions.GetSignalNumByRegion(region); + float value = raidersAndHostiles.grid.GetSignalStrengthAt(cell, out int enemies1) + colonistsAndFriendlies.grid.GetSignalStrengthAt(cell, out int enemies2) + insectsAndMechs.grid.GetSignalStrengthAt(cell, out int enemies4); if (value > 0) { - map.debugDrawer.FlashCell(cell, Mathf.Clamp(value / 7f, 0.1f, 0.99f), $"{Math.Round(value, 3)}", 15); + map.debugDrawer.FlashCell(cell, Mathf.Clamp01(value / 20f), $"{Math.Round(value, 2)}", 15); + } + else + { + MetaCombatAttribute attr = raidersAndHostiles.grid.GetCombatAttributesAt(cell) | colonistsAndFriendlies.grid.GetCombatAttributesAt(cell) | insectsAndMechs.grid.GetCombatAttributesAt(cell); + if ((attr & MetaCombatAttribute.Free) == MetaCombatAttribute.Free) + { + map.debugDrawer.FlashCell(cell, 0.001f, "F", 15); + } + } + } + else if(Finder.Settings.Debug_DebugAvailability) + { + float value = raidersAndHostiles.grid.GetAvailability(cell) + colonistsAndFriendlies.grid.GetAvailability(cell) + insectsAndMechs.grid.GetAvailability(cell); + if (value > 0) + { + map.debugDrawer.FlashCell(cell, Mathf.Clamp01(value / 20f), $"{Math.Round(value, 2)}", 15); } } } @@ -581,7 +595,8 @@ public float GetThreat(int index) val = armor.createdAt != 0 ? Mathf.Clamp01(Maths.Max(GetBlunt(index) / (armor.Blunt + 0.001f), GetSharp(index) / (armor.Sharp + 0.001f), 0f)) : 0f; } } - val = Maths.Max((float)GetEnemyAvailability(index) * val, val); + // + // val = Maths.Max((float)GetEnemyAvailability(index) * val / 2f, val); return val; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Source/Rule56/SightUtility.cs b/Source/Rule56/SightUtility.cs index f2e5eea..72677ce 100644 --- a/Source/Rule56/SightUtility.cs +++ b/Source/Rule56/SightUtility.cs @@ -12,6 +12,7 @@ public static class SightUtility private static readonly Dictionary> rangeCache = new Dictionary>(256); public static SightGrid.ISightRadius GetSightRadius(Thing thing) { + bool isSmartPawn = false; SightGrid.ISightRadius result; ThingComp_Sighter sighter = thing.GetComp_Fast(); if (sighter != null) @@ -28,7 +29,7 @@ public static SightGrid.ISightRadius GetSightRadius(Thing thing) { result = GetSightRadius_Pawn(pawn); Faction f = thing.Faction; - + isSmartPawn = !pawn.RaceProps.Animal && !(pawn.Dead || pawn.Downed); if (f != null && (f.IsPlayerSafe() || !f.HostileTo(Faction.OfPlayer) && Finder.Settings.FogOfWar_Allies)) { if (pawn.RaceProps.Animal) @@ -63,6 +64,11 @@ public static SightGrid.ISightRadius GetSightRadius(Thing thing) throw new Exception($"ISMA: GetSightRadius got an object that is niether a pawn, turret nor does it have sighter. {thing}"); } finalize: + if (isSmartPawn) + { + result.scan = result.scan + 16; + result.sight = result.sight + 8; + } result.createdAt = GenTicks.TicksGame; return result; } @@ -76,7 +82,7 @@ private static SightGrid.ISightRadius GetSightRadius_Pawn(Pawn pawn) result.scan = result.sight; return result; } - Verb verb = pawn.CurrentEffectiveVerb; + Verb verb = pawn.TryGetAttackVerb(); if (verb == null || !verb.Available()) { result.sight = 4; diff --git a/Source/Rule56/T4/Outputs/Keyed.generated.cs b/Source/Rule56/T4/Outputs/Keyed.generated.cs index aae7f7b..66fd53f 100644 --- a/Source/Rule56/T4/Outputs/Keyed.generated.cs +++ b/Source/Rule56/T4/Outputs/Keyed.generated.cs @@ -162,6 +162,26 @@ public static TaggedString CombatAI_Quick_Difficulty { _CombatAI_Quick_Difficulty : _CombatAI_Quick_Difficulty = "CombatAI.Quick.Difficulty".Translate(); } + private static TaggedString _CombatAI_Quick_Difficulty_Selected = null; + /// Keyed string. key=CombatAI.Quick.Difficulty.Selected. inner text: + /// + /// {0} difficulty level applied! + /// + public static TaggedString CombatAI_Quick_Difficulty_Selected { + get => _CombatAI_Quick_Difficulty_Selected != null ? + _CombatAI_Quick_Difficulty_Selected : _CombatAI_Quick_Difficulty_Selected = "CombatAI.Quick.Difficulty.Selected".Translate(); + } + + private static TaggedString _CombatAI_Quick_Difficulty_Selected_Warning = null; + /// Keyed string. key=CombatAI.Quick.Difficulty.Selected.Warning. inner text: + /// + /// WARNING: {0} difficulty level might cause performance issues! + /// + public static TaggedString CombatAI_Quick_Difficulty_Selected_Warning { + get => _CombatAI_Quick_Difficulty_Selected_Warning != null ? + _CombatAI_Quick_Difficulty_Selected_Warning : _CombatAI_Quick_Difficulty_Selected_Warning = "CombatAI.Quick.Difficulty.Selected.Warning".Translate(); + } + private static TaggedString _CombatAI_PlaceWorker_WallMounted = null; /// Keyed string. key=CombatAI.PlaceWorker.WallMounted. inner text: /// @@ -252,6 +272,16 @@ public static TaggedString CombatAI_Settings_Basic_Presets_Hard { _CombatAI_Settings_Basic_Presets_Hard : _CombatAI_Settings_Basic_Presets_Hard = "CombatAI.Settings.Basic.Presets.Hard".Translate(); } + private static TaggedString _CombatAI_Settings_Basic_Temperature = null; + /// Keyed string. key=CombatAI.Settings.Basic.Temperature. inner text: + /// + /// Make the AI aware of cell temperature + /// + public static TaggedString CombatAI_Settings_Basic_Temperature { + get => _CombatAI_Settings_Basic_Temperature != null ? + _CombatAI_Settings_Basic_Temperature : _CombatAI_Settings_Basic_Temperature = "CombatAI.Settings.Basic.Temperature".Translate(); + } + private static TaggedString _CombatAI_Settings_Basic_Presets_Deathwish = null; /// Keyed string. key=CombatAI.Settings.Basic.Presets.Deathwish. inner text: /// @@ -295,7 +325,7 @@ public static TaggedString CombatAI_Settings_Basic_Sprinting { private static TaggedString _CombatAI_Settings_Basic_Sprinting_Description = null; /// Keyed string. key=CombatAI.Settings.Basic.Sprinting.Description. inner text: /// - /// If this is disabled: Pathetic F F...\n if not: Chad :thumbsup + /// Allows the AI to use sprinting when dashing for cover. Turn this off if you don't like kitting simulators. /// public static TaggedString CombatAI_Settings_Basic_Sprinting_Description { get => _CombatAI_Settings_Basic_Sprinting_Description != null ? diff --git a/Source/Rule56/T4/Outputs/Tex.generated.cs b/Source/Rule56/T4/Outputs/Tex.generated.cs index e33fc75..bf36d89 100644 --- a/Source/Rule56/T4/Outputs/Tex.generated.cs +++ b/Source/Rule56/T4/Outputs/Tex.generated.cs @@ -28,6 +28,11 @@ namespace CombatAI.R public static class Tex { + /// Texture at $(SolutionDir)/../../../1.4/Textures/Isma/logo.png: + /// Isma/logo.png + /// + public static readonly Texture2D Isma_logo = ContentFinder.Get( "Isma/logo", true); + /// Texture at $(SolutionDir)/../../../1.4/Textures/Isma/Gizmos/move_attack.png: /// Isma/Gizmos/move_attack.png /// @@ -82,5 +87,30 @@ public static class Tex /// Isma/Buildings/CCTV/cctv_wall_base_north.png /// public static readonly Texture2D Isma_Buildings_CCTV_cctv_wall_base_north = ContentFinder.Get( "Isma/Buildings/CCTV/cctv_wall_base_north", true); + + /// Texture at $(SolutionDir)/../../../1.4/Textures/Isma/Tutorials/JobLog/selection_screenshot.png: + /// Isma/Tutorials/JobLog/selection_screenshot.png + /// + public static readonly Texture2D Isma_Tutorials_JobLog_selection_screenshot = ContentFinder.Get( "Isma/Tutorials/JobLog/selection_screenshot", true); + + /// Texture at $(SolutionDir)/../../../1.4/Textures/Isma/Tutorials/JobLog/gizmo_screenshot.png: + /// Isma/Tutorials/JobLog/gizmo_screenshot.png + /// + public static readonly Texture2D Isma_Tutorials_JobLog_gizmo_screenshot = ContentFinder.Get( "Isma/Tutorials/JobLog/gizmo_screenshot", true); + + /// Texture at $(SolutionDir)/../../../1.4/Textures/Isma/Tutorials/JobLog/position_screenshot.png: + /// Isma/Tutorials/JobLog/position_screenshot.png + /// + public static readonly Texture2D Isma_Tutorials_JobLog_position_screenshot = ContentFinder.Get( "Isma/Tutorials/JobLog/position_screenshot", true); + + /// Texture at $(SolutionDir)/../../../1.4/Textures/Isma/Tutorials/JobLog/clipboard_screenshot.png: + /// Isma/Tutorials/JobLog/clipboard_screenshot.png + /// + public static readonly Texture2D Isma_Tutorials_JobLog_clipboard_screenshot = ContentFinder.Get( "Isma/Tutorials/JobLog/clipboard_screenshot", true); + + /// Texture at $(SolutionDir)/../../../1.4/Textures/Isma/Tutorials/JobLog/window_screenshot.png: + /// Isma/Tutorials/JobLog/window_screenshot.png + /// + public static readonly Texture2D Isma_Tutorials_JobLog_window_screenshot = ContentFinder.Get( "Isma/Tutorials/JobLog/window_screenshot", true); } } \ No newline at end of file diff --git a/Source/Rule56/TargetDatabase.cs b/Source/Rule56/TargetDatabase.cs new file mode 100644 index 0000000..c26c77e --- /dev/null +++ b/Source/Rule56/TargetDatabase.cs @@ -0,0 +1,15 @@ +using Verse; +namespace CombatAI +{ + public class TargetDatabase + { + public Map map; + public bool isPlayerAllience; + + public TargetDatabase(Map map, bool isPlayerAllience) + { + this.map = map; + this.isPlayerAllience = isPlayerAllience; + } + } +} diff --git a/Source/Rule56/ThinkNodeDatabase.cs b/Source/Rule56/ThinkNodeDatabase.cs new file mode 100644 index 0000000..ade8ce3 --- /dev/null +++ b/Source/Rule56/ThinkNodeDatabase.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using RimWorld; +using Verse; +using Verse.AI; +namespace CombatAI +{ + public class ThinkNodeDatabase + { + private static StringBuilder builder = new StringBuilder(); + private static readonly HashSet roots = new HashSet(); + private static readonly List queue = new List(); + + public static readonly Dictionary dutyNodes = new Dictionary(); + public static readonly Dictionary treeNodes = new Dictionary(); + public static readonly Dictionary nodesNodes = new Dictionary(); + + public static void Initialize() + { + Log.Message("Initialize thinknodes"); + foreach (DutyDef duty in DefDatabase.AllDefs) + { + if (duty.thinkNode != null) + { + dutyNodes[duty.thinkNode] = duty; + roots.Add(duty.thinkNode); + } + } + foreach (ThinkTreeDef tree in DefDatabase.AllDefs) + { + if (tree.thinkRoot != null) + { + treeNodes[tree.thinkRoot] = tree; + roots.Add(tree.thinkRoot); + } + } + foreach (DutyDef duty in DefDatabase.AllDefs) + { + if (duty.thinkNode != null) + { + BuildNodeTree(duty.thinkNode); + } + } + foreach (ThinkTreeDef tree in DefDatabase.AllDefs) + { + if (tree.thinkRoot != null) + { + BuildNodeTree(tree.thinkRoot); + } + } + } + + public static void GetTrace(ThinkNode head, Pawn pawn, List trace) + { + trace.Add(GetTraceMessage(head, pawn)); + ThinkNode node = head; + while (node != null) + { + trace.Add(GetTraceMessage(node, pawn)); + if (dutyNodes.TryGetValue(node, out DutyDef duty)) + { + trace.Add(GetTraceMessage(duty, pawn)); + } + if (treeNodes.TryGetValue(node, out ThinkTreeDef tree)) + { + trace.Add(GetTraceMessage(tree, pawn)); + } + nodesNodes.TryGetValue(node, out node); + } + } + + private static void BuildNodeTree(ThinkNode root) + { + if (root.subNodes != null && !nodesNodes.ContainsKey(root)) + { + queue.Clear(); + queue.Add(root); + while (queue.Count > 0) + { + ThinkNode node = queue[0]; + queue.RemoveAt(0); + if (node.subNodes != null) + { + foreach (ThinkNode child in node.subNodes) + { + if (!nodesNodes.ContainsKey(child) && !roots.Contains(child)) + { + nodesNodes[child] = node; + queue.Add(child); + } + } + } + } + } + } + + private static string GetTraceMessage(ThinkNode node, Pawn pawn) + { + builder.Clear(); + int index = -1; + if(nodesNodes.TryGetValue(node, out ThinkNode parent)) + { + index = parent.subNodes.IndexOf(node); + } + else if (treeNodes.TryGetValue(node, out ThinkTreeDef treeDef)) + { + index = treeDef.thinkRoot.subNodes.IndexOf(node); + } + else if (dutyNodes.TryGetValue(node, out DutyDef dutyDef)) + { + index = dutyDef.thinkNode.subNodes.IndexOf(node); + } + if (node is ThinkNode_Subtree subtree) + { + builder.AppendFormat("(p{0}, {1})(treeDef={2})", index, node.GetType(), subtree.treeDef); + } + else if (node is ThinkNode_Duty dutyNode) + { + builder.AppendFormat("(p{0}, {1})(dutyDef={2})", index, node.GetType(), pawn.mindState.duty?.def.defName); + } + else + { + builder.AppendFormat("(p{0}, {1})", index, node.GetType()); + } + return builder.ToString(); + } + + private static string GetTraceMessage(DutyDef def, Pawn pawn) + { + return $"{def.defName}"; + } + + private static string GetTraceMessage(ThinkTreeDef def, Pawn pawn) + { + return $"{def.defName}"; + } + } +} diff --git a/Source/Rule56/ThinkNodes/JobGiver_DuckOrRetreat.cs b/Source/Rule56/ThinkNodes/JobGiver_DuckOrRetreat.cs new file mode 100644 index 0000000..c5b805e --- /dev/null +++ b/Source/Rule56/ThinkNodes/JobGiver_DuckOrRetreat.cs @@ -0,0 +1,43 @@ +using CombatAI.Comps; +using RimWorld; +using Verse; +using Verse.AI; +namespace CombatAI +{ + public class JobGiver_DuckOrRetreat : ThinkNode_JobGiver + { + private int radius = 8; + + public override Job TryGiveJob(Pawn pawn) + { + Verb verb = pawn.TryGetAttackVerb(); + if (verb == null || !verb.IsMeleeAttack) + { + ThingComp_CombatAI comp = pawn.GetComp_Fast(); + var list = comp.data.BeingTargetedBy; + if (comp != null && !list.NullOrEmpty()) + { + CoverPositionRequest request = new CoverPositionRequest(); + request.caster = pawn; + request.maxRangeFromCaster = radius; + request.majorThreats = list; + request.checkBlockChance = true; + if (!CoverPositionFinder.TryFindDuckPosition(request, out IntVec3 cell)) + { + return null; + } + Job goto_job = JobMaker.MakeJob(CombatAI_JobDefOf.CombatAI_Goto_Retreat, cell); + return goto_job; + } + } + return null; + } + + public override ThinkNode DeepCopy(bool resolve = true) + { + JobGiver_DuckOrRetreat node = (JobGiver_DuckOrRetreat) base.DeepCopy(resolve); + node.radius = radius; + return node; + } + } +} diff --git a/Source/Rule56/ThinkNodes/ThinkNode_ConditionalBeingTargeted.cs b/Source/Rule56/ThinkNodes/ThinkNode_ConditionalBeingTargeted.cs new file mode 100644 index 0000000..7fbc2dd --- /dev/null +++ b/Source/Rule56/ThinkNodes/ThinkNode_ConditionalBeingTargeted.cs @@ -0,0 +1,27 @@ +using CombatAI.Comps; +using Verse; +using Verse.AI; +namespace CombatAI +{ + public class ThinkNode_ConditionalBeingTargeted : ThinkNode_Conditional + { + private bool fallback = true; + + public override bool Satisfied(Pawn pawn) + { + ThingComp_CombatAI comp = pawn.GetComp_Fast(); + if (comp != null) + { + return comp.data.BeingTargetedBy.Count > 0; + } + return fallback; + } + + public override ThinkNode DeepCopy(bool resolve = true) + { + ThinkNode_ConditionalBeingTargeted obj = (ThinkNode_ConditionalBeingTargeted)base.DeepCopy(resolve); + obj.fallback = fallback; + return obj; + } + } +} diff --git a/Source/Rule56/ThinkNodes/ThinkNode_ConditionalHumanlike.cs b/Source/Rule56/ThinkNodes/ThinkNode_ConditionalHumanlike.cs new file mode 100644 index 0000000..5a4c580 --- /dev/null +++ b/Source/Rule56/ThinkNodes/ThinkNode_ConditionalHumanlike.cs @@ -0,0 +1,12 @@ +using Verse; +using Verse.AI; +namespace CombatAI +{ + public class ThinkNode_ConditionalHumanlike : ThinkNode_Conditional + { + public override bool Satisfied(Pawn pawn) + { + return pawn.RaceProps.Humanlike; + } + } +} diff --git a/Source/Rule56/ThinkNodes/ThinkNode_ConditionalRaider.cs b/Source/Rule56/ThinkNodes/ThinkNode_ConditionalRaider.cs new file mode 100644 index 0000000..4302aae --- /dev/null +++ b/Source/Rule56/ThinkNodes/ThinkNode_ConditionalRaider.cs @@ -0,0 +1,13 @@ +using RimWorld; +using Verse; +using Verse.AI; +namespace CombatAI +{ + public class ThinkNode_ConditionalRaider : ThinkNode_Conditional + { + public override bool Satisfied(Pawn pawn) + { + return pawn.Map.ParentFaction != null && pawn.HostileTo(pawn.Map.ParentFaction); + } + } +} diff --git a/Source/Rule56/ThinkNodes/ThinkNode_ConditionalReacted.cs b/Source/Rule56/ThinkNodes/ThinkNode_ConditionalReacted.cs new file mode 100644 index 0000000..fb54ad0 --- /dev/null +++ b/Source/Rule56/ThinkNodes/ThinkNode_ConditionalReacted.cs @@ -0,0 +1,31 @@ +using CombatAI.Comps; +using Verse; +using Verse.AI; +namespace CombatAI +{ + public class ThinkNode_ConditionalReacted : ThinkNode_Conditional + { +#pragma warning disable CS0649 + private int ticks; +#pragma warning restore CS0649 + private bool fallback = true; + + public override bool Satisfied(Pawn pawn) + { + ThingComp_CombatAI comp = pawn.GetComp_Fast(); + if (comp != null) + { + return comp.data.InterruptedRecently(ticks) || comp.data.RetreatedRecently(ticks); + } + return fallback; + } + + public override ThinkNode DeepCopy(bool resolve = true) + { + ThinkNode_ConditionalReacted obj = (ThinkNode_ConditionalReacted)base.DeepCopy(resolve); + obj.ticks = ticks; + obj.fallback = fallback; + return obj; + } + } +} diff --git a/Source/Rule56/ThinkNodes/ThinkNode_ConditionalSafe.cs b/Source/Rule56/ThinkNodes/ThinkNode_ConditionalSafe.cs new file mode 100644 index 0000000..2272d5f --- /dev/null +++ b/Source/Rule56/ThinkNodes/ThinkNode_ConditionalSafe.cs @@ -0,0 +1,27 @@ +using CombatAI.Comps; +using Verse; +using Verse.AI; +namespace CombatAI +{ + public class ThinkNode_ConditionalSafe : ThinkNode_Conditional + { + private bool fallback = true; + + public override bool Satisfied(Pawn pawn) + { + ThingComp_CombatAI comp = pawn.GetComp_Fast(); + if (comp != null && pawn.TryGetSightReader(out SightTracker.SightReader reader)) + { + return comp.data.NumEnemies == 0 && reader.GetAbsVisibilityToEnemies(pawn.Position) == 0; + } + return fallback; + } + + public override ThinkNode DeepCopy(bool resolve = true) + { + ThinkNode_ConditionalSafe obj = (ThinkNode_ConditionalSafe)base.DeepCopy(resolve); + obj.fallback = fallback; + return obj; + } + } +} diff --git a/Source/Rule56/ThinkNodes/ThinkNode_ConditionalSight.cs b/Source/Rule56/ThinkNodes/ThinkNode_ConditionalSight.cs new file mode 100644 index 0000000..9f28823 --- /dev/null +++ b/Source/Rule56/ThinkNodes/ThinkNode_ConditionalSight.cs @@ -0,0 +1,29 @@ +using Verse; +using Verse.AI; +namespace CombatAI +{ + public class ThinkNode_ConditionalSight : ThinkNode_Conditional + { +#pragma warning disable CS0649 + private int visibilityToEnemies; +#pragma warning restore CS0649 + private bool fallback = true; + + public override bool Satisfied(Pawn pawn) + { + if (pawn.TryGetSightReader(out SightTracker.SightReader reader)) + { + return reader.GetVisibilityToEnemies(pawn.Position) <= visibilityToEnemies; + } + return fallback; + } + + public override ThinkNode DeepCopy(bool resolve = true) + { + ThinkNode_ConditionalSight obj = (ThinkNode_ConditionalSight)base.DeepCopy(resolve); + obj.fallback = fallback; + obj.visibilityToEnemies = visibilityToEnemies; + return obj; + } + } +} \ No newline at end of file diff --git a/Source/Rule56/ThinkNodes/ThinkNode_Timed.cs b/Source/Rule56/ThinkNodes/ThinkNode_Timed.cs new file mode 100644 index 0000000..2419987 --- /dev/null +++ b/Source/Rule56/ThinkNodes/ThinkNode_Timed.cs @@ -0,0 +1,26 @@ +using Verse; +using Verse.AI; +namespace CombatAI +{ + public class ThinkNode_Timed : ThinkNode_Conditional + { + private int interval; + + public override bool Satisfied(Pawn pawn) + { + if (!pawn.mindState.thinkData.TryGetValue(base.UniqueSaveKey, out int val) || GenTicks.TicksGame - val > interval) + { + pawn.mindState.thinkData[base.UniqueSaveKey] = GenTicks.TicksGame; + return true; + } + return false; + } + + public override ThinkNode DeepCopy(bool resolve = true) + { + ThinkNode_Timed obj = (ThinkNode_Timed)base.DeepCopy(resolve); + obj.interval = interval; + return obj; + } + } +} diff --git a/Source/Rule56/WallGrid.cs b/Source/Rule56/WallGrid.cs index eda0cf6..5cdc2f7 100644 --- a/Source/Rule56/WallGrid.cs +++ b/Source/Rule56/WallGrid.cs @@ -7,23 +7,23 @@ public class WallGrid : MapComponent { private readonly CellIndices cellIndices; private readonly float[] grid; + private readonly float[] gridNoDoors; public WallGrid(Map map) : base(map) { cellIndices = map.cellIndices; grid = new float[cellIndices.NumGridCells]; + gridNoDoors = new float[cellIndices.NumGridCells]; } public float this[IntVec3 cell] { get => this[cellIndices.CellToIndex(cell)]; - set => this[cellIndices.CellToIndex(cell)] = value; } public float this[int index] { get => grid[index]; - set => grid[index] = value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -45,6 +45,26 @@ public FillCategory GetFillCategory(int index) } return FillCategory.Full; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FillCategory GetFillCategoryNoDoors(IntVec3 cell) + { + return GetFillCategoryNoDoors(cellIndices.CellToIndex(cell)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FillCategory GetFillCategoryNoDoors(int index) + { + float f = gridNoDoors[index]; + if (f == 0) + { + return FillCategory.None; + } + if (f < 1f) + { + return FillCategory.Partial; + } + return FillCategory.Full; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CanBeSeenOver(IntVec3 cell) @@ -56,6 +76,17 @@ public bool CanBeSeenOver(int index) { return grid[index] < 0.998f; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool CanBeSeenOverNoDoors(IntVec3 cell) + { + return CanBeSeenOverNoDoors(cellIndices.CellToIndex(cell)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool CanBeSeenOverNoDoors(int index) + { + return gridNoDoors[index] < 0.998f; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RecalculateCell(IntVec3 cell, Thing t) @@ -72,30 +103,31 @@ public void RecalculateCell(int index, Thing t) { if (t is Plant plant) { - grid[index] = plant.Growth * t.def.fillPercent / 4f; + gridNoDoors[index] = grid[index] = plant.Growth * t.def.fillPercent / 4f; } else { - grid[index] = t.def.fillPercent / 4f; + gridNoDoors[index] = grid[index] = t.def.fillPercent / 4f; } } } else if (t is Building_Door door) { - grid[index] = 1 - door.OpenPct; + grid[index] = 1 - door.OpenPct; + gridNoDoors[index] = 0; } else if (t is Building ed && ed.def.Fillage == FillCategory.Full) { - grid[index] = 1.0f; + gridNoDoors[index] = grid[index] = 1.0f; } else { - grid[index] = t.def.fillPercent; + gridNoDoors[index] = grid[index] = t.def.fillPercent; } } else { - grid[index] = 0; + gridNoDoors[index] = grid[index] = 0; } } }