From 2c0a7963cf0f6e968e2b010b4d0ebbd7661e2f3a Mon Sep 17 00:00:00 2001 From: ThyWolf Date: Fri, 19 Jan 2024 05:53:20 -0800 Subject: [PATCH] improve obscurement rules after third QA round --- .../ConditionBlindedByDarkness.json | 2 +- Documentation/UnfinishedBusinessSubclasses.md | 4 +- .../Api/DatabaseHelper-RELEASE.cs | 6 ++ .../GameLocationCharacterExtensions.cs | 18 ++-- ...GameLocationVisibilityManagerExtensions.cs | 99 ++++++++++++------- .../GameExtensions/RulesetActorExtensions.cs | 35 ++----- .../CustomBehaviors/MirrorImageLogic.cs | 15 ++- .../Models/SrdAndHouseRulesContext.cs | 61 +++++------- .../ConsiderationCanCastMagicPatcher.cs | 1 + .../ConsiderationCanPerceiveCellPatcher.cs | 1 + .../CursorLocationSelectTargetPatcher.cs | 4 +- .../GameLocationBattleManagerPatcher.cs | 78 +++++++++------ .../Patches/GameLocationCharacterPatcher.cs | 3 +- .../GameLocationVisibilityManagerPatcher.cs | 6 +- .../Translations/de/Others-de.txt | 1 - .../Translations/en/Others-en.txt | 1 - .../Translations/es/Others-es.txt | 1 - .../Translations/fr/Others-fr.txt | 1 - .../Translations/it/Others-it.txt | 1 - .../Translations/ja/Others-ja.txt | 1 - .../Translations/ko/Others-ko.txt | 1 - .../Translations/pt-BR/Others-pt-BR.txt | 1 - .../Translations/ru/Others-ru.txt | 1 - .../Translations/zh-CN/Others-zh-CN.txt | 1 - 24 files changed, 188 insertions(+), 155 deletions(-) diff --git a/Diagnostics/UnfinishedBusinessBlueprints/ConditionDefinition/ConditionBlindedByDarkness.json b/Diagnostics/UnfinishedBusinessBlueprints/ConditionDefinition/ConditionBlindedByDarkness.json index 028cf25b44..5a2c76ac79 100644 --- a/Diagnostics/UnfinishedBusinessBlueprints/ConditionDefinition/ConditionBlindedByDarkness.json +++ b/Diagnostics/UnfinishedBusinessBlueprints/ConditionDefinition/ConditionBlindedByDarkness.json @@ -1,7 +1,7 @@ { "$type": "ConditionDefinition, Assembly-CSharp", "inDungeonEditor": false, - "parentCondition": null, + "parentCondition": "Definition:ConditionBlinded:0a89e8d8ad8f21649b744191035357b3", "conditionType": "Detrimental", "features": [ "Definition:CombatAffinityHeavilyObscured:37285cb4ed0361e45aadf367f6e8c421", diff --git a/Documentation/UnfinishedBusinessSubclasses.md b/Documentation/UnfinishedBusinessSubclasses.md index 8bc2c6ca48..da87073c4d 100644 --- a/Documentation/UnfinishedBusinessSubclasses.md +++ b/Documentation/UnfinishedBusinessSubclasses.md @@ -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. @@ -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. diff --git a/SolastaUnfinishedBusiness/Api/DatabaseHelper-RELEASE.cs b/SolastaUnfinishedBusiness/Api/DatabaseHelper-RELEASE.cs index a0d718d58e..c40288e367 100644 --- a/SolastaUnfinishedBusiness/Api/DatabaseHelper-RELEASE.cs +++ b/SolastaUnfinishedBusiness/Api/DatabaseHelper-RELEASE.cs @@ -1184,6 +1184,9 @@ internal static class FeatureDefinitionCombatAffinitys internal static FeatureDefinitionCombatAffinity CombatAffinityBlessed { get; } = GetDefinition("CombatAffinityBlessed"); + internal static FeatureDefinitionCombatAffinity CombatAffinityBlinded { get; } = + GetDefinition("CombatAffinityBlinded"); + internal static FeatureDefinitionCombatAffinity CombatAffinityBlurred { get; } = GetDefinition("CombatAffinityBlurred"); @@ -2190,6 +2193,9 @@ internal static FeatureDefinitionSavingThrowAffinity internal static class FeatureDefinitionSenses { + internal static FeatureDefinitionSense SenseBlindSight16 { get; } = + GetDefinition("SenseBlindSight16"); + internal static FeatureDefinitionSense SenseDarkvision { get; } = GetDefinition("SenseDarkvision"); diff --git a/SolastaUnfinishedBusiness/Api/GameExtensions/GameLocationCharacterExtensions.cs b/SolastaUnfinishedBusiness/Api/GameExtensions/GameLocationCharacterExtensions.cs index ec3c712425..4bb8683d87 100644 --- a/SolastaUnfinishedBusiness/Api/GameExtensions/GameLocationCharacterExtensions.cs +++ b/SolastaUnfinishedBusiness/Api/GameExtensions/GameLocationCharacterExtensions.cs @@ -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( @@ -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; } @@ -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; } } diff --git a/SolastaUnfinishedBusiness/Api/GameExtensions/GameLocationVisibilityManagerExtensions.cs b/SolastaUnfinishedBusiness/Api/GameExtensions/GameLocationVisibilityManagerExtensions.cs index d6f18f2451..752f49b67f 100644 --- a/SolastaUnfinishedBusiness/Api/GameExtensions/GameLocationVisibilityManagerExtensions.cs +++ b/SolastaUnfinishedBusiness/Api/GameExtensions/GameLocationVisibilityManagerExtensions.cs @@ -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; @@ -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 @@ -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); } } diff --git a/SolastaUnfinishedBusiness/Api/GameExtensions/RulesetActorExtensions.cs b/SolastaUnfinishedBusiness/Api/GameExtensions/RulesetActorExtensions.cs index e0dbdfc80b..64c72746b8 100644 --- a/SolastaUnfinishedBusiness/Api/GameExtensions/RulesetActorExtensions.cs +++ b/SolastaUnfinishedBusiness/Api/GameExtensions/RulesetActorExtensions.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; -using UnityEngine; using static RuleDefinitions; namespace SolastaUnfinishedBusiness.Api.GameExtensions; @@ -29,7 +28,9 @@ internal static ICollection EnumerateFeaturesToBrowse( actor.EnumerateFeaturesToBrowse(features, featuresOrigin); - return features.OfType().ToList(); + return features + .OfType() + .ToList(); } [NotNull] @@ -67,8 +68,7 @@ private static List AllActiveDefinitions([CanBeNull] RulesetActo case RulesetCharacterMonster { originalFormCharacter: RulesetCharacterHero rulesetCharacterHero }: hero = rulesetCharacterHero; list.AddRange(FeaturesByType(hero) - .Where(f => !list.Contains(f)) - .ToList()); + .Where(f => !list.Contains(f))); break; } @@ -160,27 +160,11 @@ internal static bool HasSubFeatureOfType([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(); - - 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) @@ -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); diff --git a/SolastaUnfinishedBusiness/CustomBehaviors/MirrorImageLogic.cs b/SolastaUnfinishedBusiness/CustomBehaviors/MirrorImageLogic.cs index cec2930e20..5c89f48726 100644 --- a/SolastaUnfinishedBusiness/CustomBehaviors/MirrorImageLogic.cs +++ b/SolastaUnfinishedBusiness/CustomBehaviors/MirrorImageLogic.cs @@ -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 @@ -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); @@ -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); diff --git a/SolastaUnfinishedBusiness/Models/SrdAndHouseRulesContext.cs b/SolastaUnfinishedBusiness/Models/SrdAndHouseRulesContext.cs index e8f6d6aec2..8598e987d0 100644 --- a/SolastaUnfinishedBusiness/Models/SrdAndHouseRulesContext.cs +++ b/SolastaUnfinishedBusiness/Models/SrdAndHouseRulesContext.cs @@ -82,6 +82,7 @@ internal static class SrdAndHouseRulesContext internal static readonly ConditionDefinition ConditionBlindedByDarkness = ConditionDefinitionBuilder .Create(ConditionDefinitions.ConditionBlinded, "ConditionBlindedByDarkness") .SetOrUpdateGuiPresentation(Category.Condition) + .SetParentCondition(ConditionDefinitions.ConditionBlinded) .SetFeatures( CombatAffinityHeavilyObscured, CombatAffinityHeavilyObscuredSelf, @@ -423,7 +424,6 @@ internal static void SwitchOfficialObscurementRules() { if (Main.Settings.UseOfficialLightingObscurementAndVisionRules) { - // reuse blinded condition as many depend on it as parent ConditionDefinitions.ConditionBlinded.Features.SetRange( CombatAffinityHeavilyObscured, CombatAffinityHeavilyObscuredSelf, @@ -441,14 +441,13 @@ internal static void SwitchOfficialObscurementRules() } ConditionDefinitions.ConditionBlinded.GuiPresentation.description = - "Rules/&ConditionBlindedExtendedDescription"; + ConditionBlindedByDarkness.GuiPresentation.description; // >> ConditionVeil // ConditionAffinityVeilImmunity // PowerDefilerDarkness - ConditionAffinityVeilImmunity.conditionType = - ConditionBlindedByDarkness.Name; + ConditionAffinityVeilImmunity.conditionType = ConditionBlindedByDarkness.Name; PowerDefilerDarkness.EffectDescription.EffectForms[1].ConditionForm.ConditionDefinition = ConditionBlindedByDarkness; @@ -457,8 +456,7 @@ internal static void SwitchOfficialObscurementRules() // ConditionAffinityInvocationDevilsSight // Darkness - ConditionAffinityInvocationDevilsSight.conditionType = - ConditionBlindedByDarkness.Name; + FeatureDefinitionFeatureSets.FeatureSetInvocationDevilsSight.FeatureSet.SetRange(SenseBlindSight16); Darkness.EffectDescription.EffectForms[1].ConditionForm.ConditionDefinition = ConditionBlindedByDarkness; @@ -485,39 +483,41 @@ internal static void SwitchOfficialObscurementRules() SleetStorm.EffectDescription.EffectForms[0].ConditionForm.ConditionDefinition = ConditionDefinitions.ConditionBlinded; - // Cloud Kill / Incendiary Cloud - + // Cloud Kill / Incendiary Cloud need same debuff as other heavily obscured CloudKill.EffectDescription.EffectForms.TryAdd(FormBlinded); IncendiaryCloud.EffectDescription.EffectForms.TryAdd(FormBlinded); // Make Insect Plague lightly obscured InsectPlague.EffectDescription.EffectForms.Add(FormLightlyObscured); + InsectPlague.EffectDescription.EffectForms[1].TopologyForm.changeType = TopologyForm.Type.None; // vanilla has this set as disadvantage so we flip it with nullified requirements CombatAffinityHeavilyObscured.attackOnMeAdvantage = AdvantageType.Advantage; (CombatAffinityHeavilyObscured.nullifiedBySenses, CombatAffinityHeavilyObscured.nullifiedBySelfSenses) = (CombatAffinityHeavilyObscured.nullifiedBySelfSenses, CombatAffinityHeavilyObscured.nullifiedBySenses); - - // replace sight impaired from all non magical light and heavy obscurement effects - CloudKill.EffectDescription.EffectForms[2].TopologyForm.changeType = TopologyForm.Type.None; - FogCloud.EffectDescription.EffectForms[1].TopologyForm.changeType = TopologyForm.Type.None; - IncendiaryCloud.EffectDescription.EffectForms[2].TopologyForm.changeType = TopologyForm.Type.None; - InsectPlague.effectDescription.EffectForms[1].TopologyForm.changeType = TopologyForm.Type.None; - SleetStorm.EffectDescription.EffectForms[5].TopologyForm.changeType = TopologyForm.Type.None; - StinkingCloud.EffectDescription.EffectForms[1].TopologyForm.changeType = TopologyForm.Type.None; - SpellsContext.PetalStorm.EffectDescription.EffectForms[2].TopologyForm.changeType = TopologyForm.Type.None; } else { - ConditionDefinitions.ConditionBlinded.GuiPresentation.description = - "Rules/&ConditionBlindedExtendedDescription"; + ConditionDefinitions.ConditionBlinded.Features.SetRange( + CombatAffinityBlinded, + FeatureDefinitionPerceptionAffinitys.PerceptionAffinityConditionBlinded); + + if (Main.Settings.BlindedConditionDontAllowAttackOfOpportunity) + { + ConditionDefinitions.ConditionBlinded.Features.Add(ActionAffinityConditionBlind); + } + else + { + ConditionDefinitions.ConditionBlinded.Features.Remove(ActionAffinityConditionBlind); + } + + ConditionDefinitions.ConditionBlinded.GuiPresentation.description = "Rules/&ConditionBlindedDescription"; // >> ConditionVeil // ConditionAffinityVeilImmunity // PowerDefilerDarkness - ConditionAffinityVeilImmunity.conditionType = - ConditionVeil.Name; + ConditionAffinityVeilImmunity.conditionType = ConditionVeil.Name; PowerDefilerDarkness.EffectDescription.EffectForms[1].ConditionForm.ConditionDefinition = ConditionVeil; @@ -526,8 +526,9 @@ internal static void SwitchOfficialObscurementRules() // ConditionAffinityInvocationDevilsSight // Darkness - ConditionAffinityInvocationDevilsSight.conditionType = - ConditionDefinitions.ConditionDarkness.Name; + FeatureDefinitionFeatureSets.FeatureSetInvocationDevilsSight.FeatureSet.SetRange(SenseBlindSight16, + SenseSeeInvisible16, + ConditionAffinityInvocationDevilsSight); Darkness.EffectDescription.EffectForms[1].ConditionForm.ConditionDefinition = ConditionDefinitions.ConditionDarkness; @@ -554,28 +555,18 @@ internal static void SwitchOfficialObscurementRules() SleetStorm.EffectDescription.EffectForms[0].ConditionForm.ConditionDefinition = ConditionSleetStorm; - // Cloud Kill / Incendiary Cloud - + // Cloud Kill / Incendiary Cloud need same debuff as other heavily obscured CloudKill.EffectDescription.EffectForms.Remove(FormBlinded); IncendiaryCloud.EffectDescription.EffectForms.Remove(FormBlinded); // Remove lightly obscured from Insect Plague InsectPlague.EffectDescription.EffectForms.Remove(FormLightlyObscured); + InsectPlague.effectDescription.EffectForms[1].TopologyForm.changeType = TopologyForm.Type.SightImpaired; // vanilla has this set as disadvantage so we flip it with nullified requirements CombatAffinityHeavilyObscured.attackOnMeAdvantage = AdvantageType.Disadvantage; (CombatAffinityHeavilyObscured.nullifiedBySelfSenses, CombatAffinityHeavilyObscured.nullifiedBySenses) = (CombatAffinityHeavilyObscured.nullifiedBySenses, CombatAffinityHeavilyObscured.nullifiedBySelfSenses); - - // add sight impaired from all non magical light and heavy obscurement effects - CloudKill.EffectDescription.EffectForms[2].TopologyForm.changeType = TopologyForm.Type.SightImpaired; - FogCloud.EffectDescription.EffectForms[1].TopologyForm.changeType = TopologyForm.Type.SightImpaired; - IncendiaryCloud.EffectDescription.EffectForms[2].TopologyForm.changeType = TopologyForm.Type.SightImpaired; - InsectPlague.effectDescription.EffectForms[1].TopologyForm.changeType = TopologyForm.Type.SightImpaired; - SleetStorm.EffectDescription.EffectForms[5].TopologyForm.changeType = TopologyForm.Type.SightImpaired; - StinkingCloud.EffectDescription.EffectForms[1].TopologyForm.changeType = TopologyForm.Type.SightImpaired; - SpellsContext.PetalStorm.EffectDescription.EffectForms[2].TopologyForm.changeType = - TopologyForm.Type.SightImpaired; } } diff --git a/SolastaUnfinishedBusiness/Patches/ConsiderationCanCastMagicPatcher.cs b/SolastaUnfinishedBusiness/Patches/ConsiderationCanCastMagicPatcher.cs index 42c99a60e5..fec324a8aa 100644 --- a/SolastaUnfinishedBusiness/Patches/ConsiderationCanCastMagicPatcher.cs +++ b/SolastaUnfinishedBusiness/Patches/ConsiderationCanCastMagicPatcher.cs @@ -12,6 +12,7 @@ namespace SolastaUnfinishedBusiness.Patches; [UsedImplicitly] public static class ConsiderationCanCastMagicPatcher { + //PATCH: supports `UseOfficialLightingObscurementAndVisionRules` [HarmonyPatch(typeof(CanCastMagic), nameof(CanCastMagic.Score))] [SuppressMessage("Minor Code Smell", "S101:Types should be named in PascalCase", Justification = "Patch")] [UsedImplicitly] diff --git a/SolastaUnfinishedBusiness/Patches/ConsiderationCanPerceiveCellPatcher.cs b/SolastaUnfinishedBusiness/Patches/ConsiderationCanPerceiveCellPatcher.cs index afcb54c661..1dfb5398be 100644 --- a/SolastaUnfinishedBusiness/Patches/ConsiderationCanPerceiveCellPatcher.cs +++ b/SolastaUnfinishedBusiness/Patches/ConsiderationCanPerceiveCellPatcher.cs @@ -10,6 +10,7 @@ namespace SolastaUnfinishedBusiness.Patches; [UsedImplicitly] public static class ConsiderationCanPerceiveCellPatcher { + //PATCH: supports `UseOfficialLightingObscurementAndVisionRules` [HarmonyPatch(typeof(CanPerceiveCell), nameof(CanPerceiveCell.Score))] [SuppressMessage("Minor Code Smell", "S101:Types should be named in PascalCase", Justification = "Patch")] [UsedImplicitly] diff --git a/SolastaUnfinishedBusiness/Patches/CursorLocationSelectTargetPatcher.cs b/SolastaUnfinishedBusiness/Patches/CursorLocationSelectTargetPatcher.cs index 83b8ea6a32..078b726b3e 100644 --- a/SolastaUnfinishedBusiness/Patches/CursorLocationSelectTargetPatcher.cs +++ b/SolastaUnfinishedBusiness/Patches/CursorLocationSelectTargetPatcher.cs @@ -33,7 +33,7 @@ public static void Postfix( // required for familiar attack actingCharacter.UsedSpecialFeatures.Remove("FamiliarAttack"); - //PATCH: supports UseOfficialObscurementRules + //PATCH: supports `UseOfficialLightingObscurementAndVisionRules` if (__result && Main.Settings.UseOfficialLightingObscurementAndVisionRules && definition is IMagicEffect magicEffect && @@ -45,7 +45,7 @@ definition is IMagicEffect magicEffect && return; } - //PATCH: supports IFilterTargetingCharacter + //PATCH: supports `IFilterTargetingCharacter` foreach (var filterTargetingMagicEffect in definition.GetAllSubFeaturesOfType()) { diff --git a/SolastaUnfinishedBusiness/Patches/GameLocationBattleManagerPatcher.cs b/SolastaUnfinishedBusiness/Patches/GameLocationBattleManagerPatcher.cs index 1ebd35ac57..213bc7ebe7 100644 --- a/SolastaUnfinishedBusiness/Patches/GameLocationBattleManagerPatcher.cs +++ b/SolastaUnfinishedBusiness/Patches/GameLocationBattleManagerPatcher.cs @@ -649,59 +649,81 @@ public static void Postfix( //PATCH: support for features removing ranged attack disadvantage RangedAttackInMeleeDisadvantageRemover.CheckToRemoveRangedDisadvantage(attackParams); - //PATCH: handle lighting and obscurement logic disabled in `GLC.ComputeLightingModifierForIlluminable` - ApplyObscurementRules(attackParams, __result); + if (!__result) + { + return; + } + + //PATCH: supports `UseOfficialLightingObscurementAndVisionRules` + //handle lighting and obscurement logic disabled in `GLC.ComputeLightingModifierForIlluminable` + ApplyObscurementRules(attackParams); //PATCH: add modifier or advantage/disadvantage for physical and spell attack - ApplyCustomModifiers(attackParams, __result); + ApplyCustomModifiers(attackParams); } - private static void ApplyObscurementRules( - BattleDefinitions.AttackEvaluationParams attackParams, - bool __result) + private static void ApplyObscurementRules(BattleDefinitions.AttackEvaluationParams attackParams) { - if (!__result || - !Main.Settings.UseOfficialLightingObscurementAndVisionRules) + const string TAG = "Obscurement"; + + if (!Main.Settings.UseOfficialLightingObscurementAndVisionRules) { return; } var attackModifier = attackParams.attackModifier; + var attacker = attackParams.attacker; + var defender = attackParams.defender; + // blinded as in "heavily obscured" or in "darkness" from condition not as in "unlit" + var attackerIsBlinded = attacker.RulesetActor.HasAnyConditionOfTypeOrSubType(ConditionBlinded); + var defenderIsBlinded = defender.RulesetActor.HasAnyConditionOfTypeOrSubType(ConditionBlinded); - var alreadyHasHeavilyObscuredOrMagicalDarknessOrBlindnessModifiers = - attackModifier.attackAdvantageTrends.Any( - x => x.sourceName - is "ConditionBlinded" // already enforces DIS - or "ConditionDarkness" - or "ConditionVeil" - or "ConditionHeavilyObscured" - or "ConditionInStinkingCloud" - or "ConditionSleetStorm"); - - if (alreadyHasHeavilyObscuredOrMagicalDarknessOrBlindnessModifiers) + // nothing to do here if both contenders are already blinded as this use case is handled by vanilla + if (attackerIsBlinded && defenderIsBlinded) { return; } - var attacker = attackParams.attacker; - var defender = attackParams.defender; var attackerCanSeeDefender = attacker.CanPerceiveTarget(defender); var defenderCanSeeAttacker = defender.CanPerceiveTarget(attacker); + // add ADV/DIS based on perception if (attackerCanSeeDefender ^ defenderCanSeeAttacker) { + // no reason to add add/dis if blinded already in play + if ((attackerCanSeeDefender && defenderIsBlinded) || (defenderCanSeeAttacker && attackerIsBlinded)) + { + return; + } + attackModifier.attackAdvantageTrends.Add( - new TrendInfo(attackerCanSeeDefender ? 1 : -1, FeatureSourceType.Lighting, "Obscurement", null)); + new TrendInfo(attackerCanSeeDefender ? 1 : -1, FeatureSourceType.Lighting, TAG, null)); } - } - - private static void ApplyCustomModifiers(BattleDefinitions.AttackEvaluationParams attackParams, bool __result) - { - if (!__result) + // add ADV/DIS based on lighting + else { - return; + var adv = !defenderCanSeeAttacker && attacker.LightingState is + LocationDefinitions.LightingState.Unlit or LocationDefinitions.LightingState.Darkness; + + var dis = !attackerCanSeeDefender && defender.LightingState is + LocationDefinitions.LightingState.Unlit or LocationDefinitions.LightingState.Darkness; + + // no reason to add add/dis if blinded already in play + if ((adv && defenderIsBlinded) || (dis && attackerIsBlinded)) + { + return; + } + + if (adv ^ dis) + { + attackModifier.attackAdvantageTrends.Add( + new TrendInfo(adv ? 1 : -1, FeatureSourceType.Lighting, TAG, null)); + } } + } + private static void ApplyCustomModifiers(BattleDefinitions.AttackEvaluationParams attackParams) + { var attacker = attackParams.attacker.RulesetCharacter; var defender = attackParams.defender.RulesetCharacter; diff --git a/SolastaUnfinishedBusiness/Patches/GameLocationCharacterPatcher.cs b/SolastaUnfinishedBusiness/Patches/GameLocationCharacterPatcher.cs index ea07f62c79..f262154b23 100644 --- a/SolastaUnfinishedBusiness/Patches/GameLocationCharacterPatcher.cs +++ b/SolastaUnfinishedBusiness/Patches/GameLocationCharacterPatcher.cs @@ -20,7 +20,8 @@ namespace SolastaUnfinishedBusiness.Patches; [UsedImplicitly] public static class GameLocationCharacterPatcher { - //PATCH: let ADV/DIS be handled elsewhere in `GLBM.CanAttack` if alternate lighting and obscurement rules in place + //PATCH: supports `UseOfficialLightingObscurementAndVisionRules` + //let ADV/DIS be handled elsewhere in `GLBM.CanAttack` if alternate lighting and obscurement rules in place [HarmonyPatch(typeof(GameLocationCharacter), nameof(GameLocationCharacter.ComputeLightingModifierForIlluminable))] [SuppressMessage("Minor Code Smell", "S101:Types should be named in PascalCase", Justification = "Patch")] [UsedImplicitly] diff --git a/SolastaUnfinishedBusiness/Patches/GameLocationVisibilityManagerPatcher.cs b/SolastaUnfinishedBusiness/Patches/GameLocationVisibilityManagerPatcher.cs index 824b74d140..7f426b0517 100644 --- a/SolastaUnfinishedBusiness/Patches/GameLocationVisibilityManagerPatcher.cs +++ b/SolastaUnfinishedBusiness/Patches/GameLocationVisibilityManagerPatcher.cs @@ -68,9 +68,9 @@ public static bool Prefix( { var gridAccessor = GridAccessor.Default; - hasImpairedSight = rulesetCharacter.ImpairedSight || - (gridAccessor.RuntimeFlags(fromWorldPosition1) & - CellFlags.Runtime.DynamicSightImpaired) != 0; + hasImpairedSight = + rulesetCharacter.ImpairedSight || + (gridAccessor.RuntimeFlags(fromWorldPosition1) & CellFlags.Runtime.DynamicSightImpaired) != 0; } // END PATCH diff --git a/SolastaUnfinishedBusiness/Translations/de/Others-de.txt b/SolastaUnfinishedBusiness/Translations/de/Others-de.txt index acc040e199..1baf4197b2 100644 --- a/SolastaUnfinishedBusiness/Translations/de/Others-de.txt +++ b/SolastaUnfinishedBusiness/Translations/de/Others-de.txt @@ -180,7 +180,6 @@ Rules/&ActivationTypeOnPowerActivatedAutoTitle=Automatische Stromversorgung Rules/&ActivationTypeOnRageStartAutomaticTitle=Automatischer Rage-Start Rules/&ActivationTypeOnReduceCreatureToZeroHPAutoTitle=Automatische Kreatur auf null HP reduziert Rules/&ActivationTypeOnSneakAttackHitAutoTitle=Automatischer Schleichangriff -Rules/&ConditionBlindedExtendedDescription=Das Sehen ist geblendet. Angriffswürfe gegen die Kreatur haben einen Vorteil, und die Angriffswürfe der Kreatur haben einen Nachteil. Rules/&CounterFormDismissCreatureFormat=Entlässt eine beschworene Zielkreatur Rules/&SituationalContext1000Format=Hat Klingenmeister-Waffentypen in der Hand: Rules/&SituationalContext1001Format=Hat Großschwert in Händen: diff --git a/SolastaUnfinishedBusiness/Translations/en/Others-en.txt b/SolastaUnfinishedBusiness/Translations/en/Others-en.txt index 9f784f84ce..4fabec765b 100644 --- a/SolastaUnfinishedBusiness/Translations/en/Others-en.txt +++ b/SolastaUnfinishedBusiness/Translations/en/Others-en.txt @@ -180,7 +180,6 @@ Rules/&ActivationTypeOnPowerActivatedAutoTitle=Auto Power Rules/&ActivationTypeOnRageStartAutomaticTitle=Auto Rage Start Rules/&ActivationTypeOnReduceCreatureToZeroHPAutoTitle=Auto Creature Reduced to Zero HP Rules/&ActivationTypeOnSneakAttackHitAutoTitle=Auto Sneak Attack -Rules/&ConditionBlindedExtendedDescription=Vision is blinded. Attack rolls against the creature have advantage, and the creature's attack rolls have disadvantage. Rules/&CounterFormDismissCreatureFormat=Dismisses a target conjured creature Rules/&SituationalContext1000Format=Has Blade Mastery weapon types in hands: Rules/&SituationalContext1001Format=Has Greatsword in hands: diff --git a/SolastaUnfinishedBusiness/Translations/es/Others-es.txt b/SolastaUnfinishedBusiness/Translations/es/Others-es.txt index ef6907d54c..7da8fb8cb4 100644 --- a/SolastaUnfinishedBusiness/Translations/es/Others-es.txt +++ b/SolastaUnfinishedBusiness/Translations/es/Others-es.txt @@ -180,7 +180,6 @@ Rules/&ActivationTypeOnPowerActivatedAutoTitle=Apagado automático Rules/&ActivationTypeOnRageStartAutomaticTitle=Inicio automático de furia Rules/&ActivationTypeOnReduceCreatureToZeroHPAutoTitle=Criatura automática reducida a cero HP Rules/&ActivationTypeOnSneakAttackHitAutoTitle=Ataque furtivo automático -Rules/&ConditionBlindedExtendedDescription=La visión está ciega. Las tiradas de ataque contra la criatura tienen ventaja y las tiradas de ataque de la criatura tienen desventaja. Rules/&CounterFormDismissCreatureFormat=Desestima a una criatura conjurada objetivo. Rules/&SituationalContext1000Format=Tiene tipos de armas Blade Mastery en las manos: Rules/&SituationalContext1001Format=Tiene Greatsword en las manos: diff --git a/SolastaUnfinishedBusiness/Translations/fr/Others-fr.txt b/SolastaUnfinishedBusiness/Translations/fr/Others-fr.txt index 86e3e2e7ab..920f4e15a7 100644 --- a/SolastaUnfinishedBusiness/Translations/fr/Others-fr.txt +++ b/SolastaUnfinishedBusiness/Translations/fr/Others-fr.txt @@ -180,7 +180,6 @@ Rules/&ActivationTypeOnPowerActivatedAutoTitle=Pouvoir automatique Rules/&ActivationTypeOnRageStartAutomaticTitle=Démarrage en rage automatique Rules/&ActivationTypeOnReduceCreatureToZeroHPAutoTitle=Créature à zéro HP automatique Rules/&ActivationTypeOnSneakAttackHitAutoTitle=Attaque sournoise automatique -Rules/&ConditionBlindedExtendedDescription=La vision est aveuglée. Les jets d'attaque contre la créature ont un avantage, et les jets d'attaque de la créature ont un désavantage. Rules/&CounterFormDismissCreatureFormat=Renvoi une créature invoquée ciblée Rules/&SituationalContext1000Format=A en main des armes compatibles avec Maîtrise des Lames : Rules/&SituationalContext1001Format=A en main une épée à deux mains : diff --git a/SolastaUnfinishedBusiness/Translations/it/Others-it.txt b/SolastaUnfinishedBusiness/Translations/it/Others-it.txt index 5ab42e889b..7e62edb29b 100644 --- a/SolastaUnfinishedBusiness/Translations/it/Others-it.txt +++ b/SolastaUnfinishedBusiness/Translations/it/Others-it.txt @@ -181,7 +181,6 @@ Rules/&ActivationTypeOnPowerActivatedAutoTitle=Alimentazione automatica Rules/&ActivationTypeOnRageStartAutomaticTitle=Avvio automatico della rabbia Rules/&ActivationTypeOnReduceCreatureToZeroHPAutoTitle=Creatura automatica ridotta a zero HP Rules/&ActivationTypeOnSneakAttackHitAutoTitle=Attacco furtivo automatico -Rules/&ConditionBlindedExtendedDescription=La vista è accecata. I tiri per colpire contro la creatura hanno vantaggio, mentre i tiri per colpire della creatura hanno svantaggio. Rules/&CounterFormDismissCreatureFormat=Congeda una creatura evocata bersaglio Rules/&SituationalContext1000Format=Ha i tipi di armi Blade Mastery nelle mani: Rules/&SituationalContext1001Format=Ha lo spadone in mano: diff --git a/SolastaUnfinishedBusiness/Translations/ja/Others-ja.txt b/SolastaUnfinishedBusiness/Translations/ja/Others-ja.txt index 6326d87862..d34b706c39 100644 --- a/SolastaUnfinishedBusiness/Translations/ja/Others-ja.txt +++ b/SolastaUnfinishedBusiness/Translations/ja/Others-ja.txt @@ -180,7 +180,6 @@ Rules/&ActivationTypeOnPowerActivatedAutoTitle=オートパワー Rules/&ActivationTypeOnRageStartAutomaticTitle=オートレイジスタート Rules/&ActivationTypeOnReduceCreatureToZeroHPAutoTitle=自動クリーチャーのHPがゼロに減少 Rules/&ActivationTypeOnSneakAttackHitAutoTitle=オートスニークアタック -Rules/&ConditionBlindedExtendedDescription=視界が遮られます。クリーチャーに対する攻撃ロールは有利であり、クリーチャーの攻撃ロールは不利です。 Rules/&CounterFormDismissCreatureFormat=対象の召喚されたクリーチャーを退ける Rules/&SituationalContext1000Format=ブレードマスタリーの武器タイプを手に持っています: Rules/&SituationalContext1001Format=グレートソードを手に持っています。 diff --git a/SolastaUnfinishedBusiness/Translations/ko/Others-ko.txt b/SolastaUnfinishedBusiness/Translations/ko/Others-ko.txt index 82fd4716b5..16827cc5c5 100644 --- a/SolastaUnfinishedBusiness/Translations/ko/Others-ko.txt +++ b/SolastaUnfinishedBusiness/Translations/ko/Others-ko.txt @@ -180,7 +180,6 @@ Rules/&ActivationTypeOnPowerActivatedAutoTitle=자동 능력 트리거 Rules/&ActivationTypeOnRageStartAutomaticTitle=자동 격노 개시 트리거 Rules/&ActivationTypeOnReduceCreatureToZeroHPAutoTitle=자동 HP 0 감소 트리거 Rules/&ActivationTypeOnSneakAttackHitAutoTitle=자동 암습 트리거 -Rules/&ConditionBlindedExtendedDescription=시력이 흐려졌습니다. 생물에 대한 공격 굴림에는 이점이 있고 생물의 공격 굴림에는 불리한 점이 있습니다. Rules/&CounterFormDismissCreatureFormat=소환된 크리쳐 대상을 소환해제 Rules/&SituationalContext1000Format=칼날 단련 무기 유형을 손에 들고 있음: Rules/&SituationalContext1001Format=그레이트 소드를 손에 들고 있음: diff --git a/SolastaUnfinishedBusiness/Translations/pt-BR/Others-pt-BR.txt b/SolastaUnfinishedBusiness/Translations/pt-BR/Others-pt-BR.txt index 481150bbfb..99a978c78f 100644 --- a/SolastaUnfinishedBusiness/Translations/pt-BR/Others-pt-BR.txt +++ b/SolastaUnfinishedBusiness/Translations/pt-BR/Others-pt-BR.txt @@ -180,7 +180,6 @@ Rules/&ActivationTypeOnPowerActivatedAutoTitle=Alimentação automática Rules/&ActivationTypeOnRageStartAutomaticTitle=Início da raiva automática Rules/&ActivationTypeOnReduceCreatureToZeroHPAutoTitle=Criatura automática reduzida a zero HP Rules/&ActivationTypeOnSneakAttackHitAutoTitle=Ataque furtivo automático -Rules/&ConditionBlindedExtendedDescription=A visão está cega. As jogadas de ataque contra a criatura têm vantagem e as jogadas de ataque da criatura têm desvantagem. Rules/&CounterFormDismissCreatureFormat=Dispensa uma criatura alvo conjurada Rules/&SituationalContext1000Format=Tem os tipos de armas Blade Mastery em mãos: Rules/&SituationalContext1001Format=Tem Greatsword em mãos: diff --git a/SolastaUnfinishedBusiness/Translations/ru/Others-ru.txt b/SolastaUnfinishedBusiness/Translations/ru/Others-ru.txt index 888f49e346..73d263228c 100644 --- a/SolastaUnfinishedBusiness/Translations/ru/Others-ru.txt +++ b/SolastaUnfinishedBusiness/Translations/ru/Others-ru.txt @@ -180,7 +180,6 @@ Rules/&ActivationTypeOnPowerActivatedAutoTitle=Автоматическое ум Rules/&ActivationTypeOnRageStartAutomaticTitle=Автоматическое начало ярости Rules/&ActivationTypeOnReduceCreatureToZeroHPAutoTitle=Автоматическое уменьшение хитов существа до нуля Rules/&ActivationTypeOnSneakAttackHitAutoTitle=Автоматическая скрытая атака -Rules/&ConditionBlindedExtendedDescription=Существо ослеплено. Броски атаки по существу совершаются с преимуществом, а существо совершает броски атаки с помехой. Rules/&CounterFormDismissCreatureFormat=Отпускает призванное существо Rules/&SituationalContext1000Format=Держит в руках тип оружия Мастерства клинка: Rules/&SituationalContext1001Format=Держит в руках двуручный меч: diff --git a/SolastaUnfinishedBusiness/Translations/zh-CN/Others-zh-CN.txt b/SolastaUnfinishedBusiness/Translations/zh-CN/Others-zh-CN.txt index 0ee1955236..943b92d6cd 100644 --- a/SolastaUnfinishedBusiness/Translations/zh-CN/Others-zh-CN.txt +++ b/SolastaUnfinishedBusiness/Translations/zh-CN/Others-zh-CN.txt @@ -180,7 +180,6 @@ Rules/&ActivationTypeOnPowerActivatedAutoTitle=自动电源 Rules/&ActivationTypeOnRageStartAutomaticTitle=自动愤怒开始 Rules/&ActivationTypeOnReduceCreatureToZeroHPAutoTitle=自动生物生命值降至零 Rules/&ActivationTypeOnSneakAttackHitAutoTitle=自动偷袭 -Rules/&ConditionBlindedExtendedDescription=视力被蒙蔽了。针对该生物的攻击检定具有优势,而该生物的攻击检定则具有劣势。 Rules/&CounterFormDismissCreatureFormat=解散一个目标召唤生物 Rules/&SituationalContext1000Format=手持剑刃精通武器: Rules/&SituationalContext1001Format=手持巨剑: