public static bool IsFlankedByUnits( this UnitEntityData target, UnitEntityData unit1, UnitEntityData unit2, Func <UnitEntityData, UnitEntityData, UnitEntityData, bool> flankingPreconditions = null) { // Block nonsensical parameters. It takes three to tango. if (target == null || unit1 == null || unit2 == null || target == unit1 || target == unit2 || unit1 == unit2) { return(false); } // Flanking requires not being flat-footed (see above). if (unit1.IsFlatFootedTo(target)) { return(false); } if (unit2.IsFlatFootedTo(target)) { return(false); } // Flanking requires threatening if (!unit1.IsEngage(target) || !unit2.IsEngage(target)) { return(false); } // Check for any extra preconditions on the flanking participants if (flankingPreconditions != null && !flankingPreconditions(target, unit1, unit2)) { return(false); } // Ideally, the flanking partners should be more than 120 degrees from each other with respect to the target. // However, I found that with the isometric perspective, the angle is often hard to judge, so we're lenient here // and make it 115 degrees. return(VectorMath.AngleBetweenPoints(target.Position.To2D(), unit1.Position.To2D(), unit2.Position.To2D()) > 115.0f); }
public override void OnTrigger(RulebookEventContext context) { if (!Main.Settings.UseSoftCover) { Main.Logger?.Write("Soft Cover Rules disabled, result of soft cover check is Cover.None"); Result = Cover.None; return; } Main.Logger?.Append("RuleCheckSoftCover Triggered"); Main.Logger?.Append($"Attacker = {Initiator.CharacterName} ({Initiator.UniqueId})"); Main.Logger?.Append($"Target = {Target.CharacterName} ({Target.UniqueId})"); Main.Logger?.Append($"#IgnoreUnits = {IgnoreUnits.Count}"); Main.Logger?.Append($"AttackerIgnoresCover = {AttackerIgnoresCover}"); Main.Logger?.Append($"AttackType = {AttackType}"); if (AttackerIgnoresCover) { Main.Logger?.Flush(); return; } Vector2 toPosition = Target.Position.To2D(); Vector2 fromPosition = Initiator.Position.To2D(); int i = 0; List <UnitEntityData> unitsToCheck = new List <UnitEntityData>(); Main.Logger?.Append("Looping over units in combat..."); foreach (UnitEntityData unit in Game.Instance.State.AwakeUnits) { i++; if (unit == Initiator || unit == Target) { Main.Logger?.Append($"{i}: unit {unit.CharacterName} ({unit.UniqueId}) is either the Initiator or the Target and does not count for cover"); continue; } if (IgnoreUnits.Contains(unit)) { Main.Logger?.Append($"{i}: unit {unit.CharacterName} ({unit.UniqueId}) is in the IgnoreUnits list and does not count for cover"); continue; } if (unit.Descriptor.State.IsDead) { Main.Logger?.Append($"{i}: unit {unit.CharacterName} ({unit.UniqueId}) is dead and does not count for cover"); continue; } if (unit.Descriptor.State.Prone.Active) { Main.Logger?.Append($"{i}: unit {unit.CharacterName} ({unit.UniqueId}) is prone and does not count for cover"); continue; } int sizeDifference = Target.Descriptor.State.Size - unit.Descriptor.State.Size; // e.g. a Small character cannot provide soft cover for a Large character if (sizeDifference >= 2) { Main.Logger?.Append($"{i}: the target is of size {Target.Descriptor.State.Size}, while unit {unit.CharacterName} ({unit.UniqueId}) is of size {unit.Descriptor.State.Size}, so the unit does not count for cover."); continue; } Vector2 unitPosition = unit.Position.To2D(); // A unit that is closer to the attacker than to the target and is smaller than the attacker does not provide cover to the target if (unit.Descriptor.State.Size < Initiator.Descriptor.State.Size && Vector2.Distance(fromPosition, unitPosition) < Vector2.Distance(unitPosition, toPosition)) { Main.Logger?.Append($"{i}: the attacker is of size {Initiator.Descriptor.State.Size}, the unit is of size {unit.Descriptor.State.Size}, and the unit is {Vector2.Distance(fromPosition, unitPosition)} away from the attacker and {Vector2.Distance(unitPosition, toPosition)} away from the target, so the unit does not count for cover."); continue; } // Filter nonsensical cases if (Vector2.Distance(fromPosition, toPosition) < Vector2.Distance(fromPosition, unitPosition) || VectorMath.AngleBetweenPoints(unitPosition, fromPosition, toPosition) < 90.0f) { Main.Logger?.Append($"{i}: Possibility of {unit.CharacterName} ({unit.UniqueId}) providing cover considered nonsensical: "); Main.Logger?.Append($" - Distance from attacker to target: {Vector2.Distance(fromPosition, toPosition)}"); Main.Logger?.Append($" - Distance from attacker to unit: {Vector2.Distance(fromPosition, unitPosition)}"); Main.Logger?.Append($" - Angle between attacker - unit - target: {VectorMath.AngleBetweenPoints(unitPosition, fromPosition, toPosition)}"); continue; } unitsToCheck.Add(unit); Main.Logger?.Append($"{i}: unit {unit.CharacterName} ({unit.UniqueId}) added to list of potential cover-providing units"); } if (unitsToCheck.Count < 1) { Main.Logger?.Append("No units could possibly provide cover."); Main.Logger?.Flush(); return; } Vector2 direction = (toPosition - fromPosition).normalized; Vector2 up = new Vector2(direction.y, -direction.x); Vector2 down = new Vector2(-direction.y, direction.x); Vector2 fromPointsStart = fromPosition + up * Initiator.Corpulence; Vector2 fromPointsEnd = fromPosition + down * Initiator.Corpulence; Vector2 tangent1 = Vector2.zero; Vector2 tangent2 = Vector2.zero; VectorMath.TangentPointsOnCircleFromPoint(fromPointsStart, toPosition, Target.Corpulence, out tangent1, out tangent2); Vector2 toPointsStart = Vector2.zero; float normDist = 0.0f; if (Pathfinding.VectorMath.SegmentsIntersect2D(fromPosition, toPosition, fromPointsStart, tangent1, out normDist)) { toPointsStart = tangent2; } else { toPointsStart = tangent1; } Vector2 tangent3 = Vector2.zero; Vector2 tangent4 = Vector2.zero; VectorMath.TangentPointsOnCircleFromPoint(fromPointsEnd, toPosition, Target.Corpulence, out tangent3, out tangent4); Vector2 toPointsEnd = Vector2.zero; if (Pathfinding.VectorMath.SegmentsIntersect2D(fromPosition, toPosition, fromPointsEnd, tangent3, out normDist)) { toPointsEnd = tangent4; } else { toPointsEnd = tangent3; } Vector2[] fromPoints = VectorMath.FindEquidistantPointsOnArc(fromPointsStart, fromPointsEnd, fromPosition, Initiator.Corpulence, 10); Vector2[] toPoints = VectorMath.FindEquidistantPointsOnArc(toPointsStart, toPointsEnd, toPosition, Target.Corpulence, 10); Main.Logger?.Append("Looping over lines..."); int raysBlocked = 0; for (i = 0; i < fromPoints.Length; i++) { Main.Logger?.Append($" - Checking line {i} ({fromPoints[i]} to {toPoints[i]})..."); #if DEBUG if (i > 0) { Main.Logger?.Append($" * Validate: lines {i} and {i - 1} intersect: {Pathfinding.VectorMath.SegmentsIntersect2D(fromPoints[i - 1], toPoints[i - 1], fromPoints[i], toPoints[i], out normDist)}"); } #endif for (int j = 0; j < unitsToCheck.Count; j++) { var p = VectorMath.NearestPointOnSegmentToPoint(fromPoints[i], toPoints[i], unitsToCheck[j].Position.To2D()); var d = Vector2.Distance(p, unitsToCheck[j].Position.To2D()); Main.Logger?.Append($" * closest distance of line {i} to unit {unitsToCheck[j].CharacterName} ({unitsToCheck[j].UniqueId}): {d}. Unit's corpulence: {unitsToCheck[j].Corpulence}"); if (Vector2.Distance(p, unitsToCheck[j].Position.To2D()) < unitsToCheck[j].Corpulence) { raysBlocked++; Main.Logger?.Append($" * line {i} is obstructed by unit {unitsToCheck[j].CharacterName} ({unitsToCheck[j].UniqueId}). Number of obstructed lines so far = {raysBlocked}. Continuing to next line..."); break; } else { Main.Logger?.Append($" * line {i} is not obstructed by unit {unitsToCheck[j].CharacterName} ({unitsToCheck[j].UniqueId})"); } } if (raysBlocked > 6) { break; } } Main.Logger?.Append($"Finished looping over target lines. Total lines blocked: {raysBlocked}"); if (raysBlocked > 6) { Result = Cover.Full; } else if (raysBlocked > 2) { Result = Cover.Partial; } else { Result = Cover.None; } Main.Logger?.Append($"Final cover result: {Result}"); Main.Logger?.Flush(); }