Skip to content

Commit

Permalink
improve obscurement rules after third QA round
Browse files Browse the repository at this point in the history
  • Loading branch information
ThyWoof committed Jan 19, 2024
1 parent 7d51e2d commit 2c0a796
Show file tree
Hide file tree
Showing 24 changed files with 188 additions and 155 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$type": "ConditionDefinition, Assembly-CSharp",
"inDungeonEditor": false,
"parentCondition": null,
"parentCondition": "Definition:ConditionBlinded:0a89e8d8ad8f21649b744191035357b3",
"conditionType": "Detrimental",
"features": [
"Definition:CombatAffinityHeavilyObscured:37285cb4ed0361e45aadf367f6e8c421",
Expand Down
4 changes: 2 additions & 2 deletions Documentation/UnfinishedBusinessSubclasses.md
Original file line number Diff line number Diff line change
Expand Up @@ -1755,7 +1755,7 @@ Once per turn when you miss with a monk weapon or unarmed attack while you are i

* Shadowy Sanctuary

While you are in dim light or darkness and a creature is about to hit you with an attack, you can spend 3 ki and use your reaction to step into shadows and disappear, negating any damage or harmful effects you would have received from that attack. While in the shadows, you are considered banished until the start of your next turn.
When a creature is about to hit you with an attack, you can spend 3 ki and use your reaction to step into shadows and disappear, negating any damage or harmful effects you would have received from that attack. While in the shadows, you are considered banished until the start of your next turn. You do not lose concentration while banished this way.



Expand Down Expand Up @@ -3418,7 +3418,7 @@ You may cast Ice Storm as a fourth level spell a number of times equal to your p

* Icebound Soul

Gain immunity to cold damage. The first time you hit an enemy with an attack on your turn, they must make a Constitution saving throw against your warlock spell DC or become blinded until the end of your next turn.
Gain immunity to cold damage. The first time you hit an enemy with an attack on your turn, they must make a Constitution saving throw against your warlock spell DC or become blinded until the end of their next turn.



Expand Down
6 changes: 6 additions & 0 deletions SolastaUnfinishedBusiness/Api/DatabaseHelper-RELEASE.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1184,6 +1184,9 @@ internal static class FeatureDefinitionCombatAffinitys
internal static FeatureDefinitionCombatAffinity CombatAffinityBlessed { get; } =
GetDefinition<FeatureDefinitionCombatAffinity>("CombatAffinityBlessed");

internal static FeatureDefinitionCombatAffinity CombatAffinityBlinded { get; } =
GetDefinition<FeatureDefinitionCombatAffinity>("CombatAffinityBlinded");

internal static FeatureDefinitionCombatAffinity CombatAffinityBlurred { get; } =
GetDefinition<FeatureDefinitionCombatAffinity>("CombatAffinityBlurred");

Expand Down Expand Up @@ -2190,6 +2193,9 @@ internal static FeatureDefinitionSavingThrowAffinity

internal static class FeatureDefinitionSenses
{
internal static FeatureDefinitionSense SenseBlindSight16 { get; } =
GetDefinition<FeatureDefinitionSense>("SenseBlindSight16");

internal static FeatureDefinitionSense SenseDarkvision { get; } =
GetDefinition<FeatureDefinitionSense>("SenseDarkvision");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@ public static bool IsMyTurn(this GameLocationCharacter character)
return Gui.Battle != null && Gui.Battle.ActiveContenderIgnoringLegendary == character;
}

public static bool IsWithinRange(this GameLocationCharacter source, GameLocationCharacter target, int range)
public static float GetDistance(this GameLocationCharacter source, GameLocationCharacter target)
{
if (Main.Settings.UseOfficialDistanceCalculation)
{
return DistanceCalculation.CalculateDistanceFromTwoCharacters(source, target) <= range;
return DistanceCalculation.CalculateDistanceFromTwoCharacters(source, target);
}

return int3.Distance(source.LocationPosition, target.LocationPosition) <= range;
return int3.Distance(source.LocationPosition, target.LocationPosition);
}

public static bool IsWithinRange(this GameLocationCharacter source, GameLocationCharacter target, int range)
{
return GetDistance(source, target) <= range;
}

public static bool IsMagicEffectValidUnderBlindness(
Expand All @@ -45,7 +50,8 @@ public static bool IsMagicEffectValidUnderBlindness(
var rulesetSource = source.RulesetActor;
var rulesetTarget = target.RulesetActor;

if (!rulesetSource.HasBlindness() && !rulesetTarget.HasBlindness())
if (!rulesetSource.HasConditionOfTypeOrSubType(ConditionDefinitions.ConditionBlinded.Name) &&
!rulesetTarget.HasConditionOfTypeOrSubType(ConditionDefinitions.ConditionBlinded.Name))
{
return true;
}
Expand Down Expand Up @@ -244,13 +250,11 @@ static LocationDefinitions.LightingState ComputeIllumination(IIlluminable illumi
}
}

var illumination2 =
return
lightingState1 != LocationDefinitions.LightingState.Dim ||
lightingState2 != LocationDefinitions.LightingState.Unlit
? lightingState2
: LocationDefinitions.LightingState.Dim;

return illumination2;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Linq;
using SolastaUnfinishedBusiness.CustomBehaviors;
using SolastaUnfinishedBusiness.CustomBehaviors;
using SolastaUnfinishedBusiness.Models;
using TA;
using static SolastaUnfinishedBusiness.Api.DatabaseHelper.ConditionDefinitions;

namespace SolastaUnfinishedBusiness.Api.GameExtensions;

Expand All @@ -15,6 +15,7 @@ public static bool MyIsCellPerceivedByCharacter(
GameLocationCharacter target = null,
LocationDefinitions.LightingState additionalBlockedLightingState = LocationDefinitions.LightingState.Darkness)
{
// let vanilla do the heavy lift on perception
var result = instance.IsCellPerceivedByCharacter(cellPosition, sensor);

// if setting is off or vanilla cannot perceive
Expand All @@ -26,53 +27,83 @@ public static bool MyIsCellPerceivedByCharacter(
return false;
}

// might wanna dup some code and remove Silhouette Step handling below for performance reasons
var lightningState = sensor.ComputeLightingStateOnTargetPosition(cellPosition);

// Silhouette Step is the only one using additionalBlockedLightingState as it requires to block BRIGHT
return additionalBlockedLightingState == LocationDefinitions.LightingState.Darkness ||
lightningState != additionalBlockedLightingState;
sensor.ComputeLightingStateOnTargetPosition(cellPosition) != additionalBlockedLightingState;
}

var inRange = false;
var distance = DistanceCalculation.GetDistanceFromTwoPositions(sensor.LocationPosition, cellPosition);
var selectedSenseType = SenseMode.Type.None;
var selectedSenseRange = 0;
var lightingState = sensor.ComputeLightingStateOnTargetPosition(cellPosition);
var sourceIsBlindedButNotMagically =
sensor.RulesetActor.AllConditions.Exists(x =>
x.ConditionDefinition == ConditionBlinded ||
(x.ConditionDefinition != SrdAndHouseRulesContext.ConditionBlindedByDarkness &&
x.ConditionDefinition.parentCondition == ConditionBlinded) ||
sensor.LightingState is LocationDefinitions.LightingState.Darkness
or LocationDefinitions.LightingState.Unlit);
var sourceIsBlinded =
sensor.RulesetActor.HasConditionOfTypeOrSubType(ConditionBlinded.Name);

// force lighting state to unlit if target is blind and on a dim or bright cell
if (target != null &&
target.RulesetActor.HasConditionOfTypeOrSubType(ConditionBlinded.Name) &&
lightingState is LocationDefinitions.LightingState.Bright or LocationDefinitions.LightingState.Dim)
{
lightingState = LocationDefinitions.LightingState.Unlit;
}

// try to find any sense mode that is valid for the current lighting state and is within range
foreach (var senseMode in sensor.RulesetCharacter.SenseModes)
{
var inRange = false;
var distance = DistanceCalculation.GetDistanceFromTwoPositions(sensor.LocationPosition, cellPosition);
var lightingState = sensor.ComputeLightingStateOnTargetPosition(cellPosition);
var selectedSenseType = SenseMode.Type.None;
var selectedSenseRange = 0;
var nonMagicalDarkness =
target != null &&
target.RulesetActor.HasConditionOfType(SrdAndHouseRulesContext.ConditionBlindedByDarkness.Name);
var senseType = senseMode.SenseType;

// try to find any sense mode that is valid for the current lighting state and is within range
foreach (var senseMode in sensor.RulesetCharacter.SenseModes
.Where(senseMode => SenseMode.ValidForLighting(senseMode.SenseType, lightingState)))
if (!SenseMode.ValidForLighting(senseMode.SenseType, lightingState))
{
var senseType = senseMode.SenseType;
continue;
}

if (selectedSenseType != senseType && senseMode.SenseRange >= selectedSenseRange)
{
if (nonMagicalDarkness && senseType == SenseMode.Type.Truesight)
{
continue;
}
// these are the only senses a blinded creature can use
if (sourceIsBlinded &&
senseType is not (SenseMode.Type.Truesight or SenseMode.Type.Blindsight or SenseMode.Type.Tremorsense))
{
continue;
}

selectedSenseType = senseType;
selectedSenseRange = senseMode.SenseRange;
if (selectedSenseType != senseType && senseMode.SenseRange >= selectedSenseRange)
{
// can only use true sight on magical darkness
if (sourceIsBlindedButNotMagically &&
senseType == SenseMode.Type.Truesight)
{
continue;
}

inRange = distance <= senseMode.SenseRange;

if (inRange)
// can only use tremor sense on non flying creatures
if (target != null &&
!target.RulesetActor.IsTouchingGround() &&
senseType == SenseMode.Type.Tremorsense)
{
break;
continue;
}

selectedSenseType = senseType;
selectedSenseRange = senseMode.SenseRange;
}

return inRange &&
// Silhouette Step is the only one using additionalBlockedLightingState as it requires to block BRIGHT
(additionalBlockedLightingState == LocationDefinitions.LightingState.Darkness ||
lightingState != additionalBlockedLightingState);
inRange = distance <= senseMode.SenseRange;

if (inRange)
{
break;
}
}

return inRange &&
// Silhouette Step is the only one using additionalBlockedLightingState as it requires to block BRIGHT
(additionalBlockedLightingState == LocationDefinitions.LightingState.Darkness ||
lightingState != additionalBlockedLightingState);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEngine;
using static RuleDefinitions;

namespace SolastaUnfinishedBusiness.Api.GameExtensions;
Expand All @@ -29,7 +28,9 @@ internal static ICollection<T> EnumerateFeaturesToBrowse<T>(

actor.EnumerateFeaturesToBrowse<T>(features, featuresOrigin);

return features.OfType<T>().ToList();
return features
.OfType<T>()
.ToList();
}

[NotNull]
Expand Down Expand Up @@ -67,8 +68,7 @@ private static List<BaseDefinition> AllActiveDefinitions([CanBeNull] RulesetActo
case RulesetCharacterMonster { originalFormCharacter: RulesetCharacterHero rulesetCharacterHero }:
hero = rulesetCharacterHero;
list.AddRange(FeaturesByType<BaseDefinition>(hero)
.Where(f => !list.Contains(f))
.ToList());
.Where(f => !list.Contains(f)));
break;
}

Expand Down Expand Up @@ -160,27 +160,11 @@ internal static bool HasSubFeatureOfType<T>([CanBeNull] this RulesetActor actor,
.FirstOrDefault() != null;
}

internal static float DistanceTo(this RulesetActor actor, RulesetActor target)
{
var locA = GameLocationCharacter.GetFromActor(actor);
var locB = GameLocationCharacter.GetFromActor(target);

if (locA == null || locB == null)
{
return 0;
}

var service = ServiceRepository.GetService<IGameLocationPositioningService>();

return Vector3.Distance(service.ComputeGravityCenterPosition(locA), service.ComputeGravityCenterPosition(locB));
}

internal static bool IsTouchingGround(this RulesetActor actor)
{
return !actor.HasConditionOfType(ConditionFlying)
&& !actor.HasConditionOfType(ConditionLevitate)
&& !(actor is RulesetCharacter character &&
character.MoveModes.ContainsKey((int)MoveMode.Fly));
return !actor.HasConditionOfType(ConditionFlying) &&
!actor.HasConditionOfType(ConditionLevitate) &&
!(actor is RulesetCharacter character && character.MoveModes.ContainsKey((int)MoveMode.Fly));
}

internal static bool IsTemporarilyFlying(this RulesetActor actor)
Expand All @@ -199,11 +183,6 @@ internal static bool IsTemporarilyFlying(this RulesetActor actor)
);*/
}

internal static bool HasBlindness(this RulesetActor character)
{
return character.HasConditionOfTypeOrSubType(DatabaseHelper.ConditionDefinitions.ConditionBlinded.Name);
}

internal static bool HasAnyConditionOfType(this RulesetActor actor, params string[] conditions)
{
return actor is RulesetCharacter && conditions.Any(actor.HasConditionOfType);
Expand Down
15 changes: 11 additions & 4 deletions SolastaUnfinishedBusiness/CustomBehaviors/MirrorImageLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,15 @@ internal static void AttackRollPrefix(
return;
}

var distance = (int)attacker.DistanceTo(target);
var locA = GameLocationCharacter.GetFromActor(attacker);
var locB = GameLocationCharacter.GetFromActor(target);

if (locA == null || locB == null)
{
return;
}

var distance = locA.GetDistance(locB);

foreach (var sense in attacker.SenseModes
.Where(sense => sense.senseType is SenseMode.Type.Blindsight or SenseMode.Type.Truesight
Expand All @@ -118,7 +126,6 @@ internal static void AttackRollPrefix(
}

//TODO: Bonus points if we can manage to change attack `GameConsole.AttackRolled` to show duplicate, instead of the target

//TODO: add custom context and modify Halfling's Lucky to include it
var result = target.RollDie(RuleDefinitions.DieType.D20, RuleDefinitions.RollContext.None, false,
RuleDefinitions.AdvantageType.None, out _, out _, skill: TargetMirrorImageTag);
Expand Down Expand Up @@ -156,8 +163,8 @@ internal static void AttackRollPostfix(
return;
}

if (!testMode
&& outcome is RuleDefinitions.RollOutcome.Success or RuleDefinitions.RollOutcome.CriticalSuccess)
if (!testMode &&
outcome is RuleDefinitions.RollOutcome.Success or RuleDefinitions.RollOutcome.CriticalSuccess)
{
//attacker hit our mirror image, need to remove one of them
var conditions = GetConditions(target as RulesetCharacter);
Expand Down
Loading

0 comments on commit 2c0a796

Please sign in to comment.