Conditional Checks (#1052)

## Major features

- Added `conditionalChecks` to star system configs. These allow you to
automatically set or unset dialogue conditions, persistent conditions,
and ship log facts based on other conditions being met, to handle more
complex interactive situations that a single `requiredCondition` or
`requiredFact` can't cover. Implements #1048

## Bug fixes

- Fixed custom items breaking if `pickupAudio` and `dropAudio` were not
set. Custom items will now default to the warp core item sounds. Each
sound can be disabled individually by specifying "None" as the audio
type.
- Fixed custom items breaking if `removeComponents` was set on the
detail.
This commit is contained in:
xen-42 2025-02-17 13:44:18 -05:00 committed by GitHub
commit 101a6b1e9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 481 additions and 24 deletions

View File

@ -209,24 +209,6 @@ namespace NewHorizons.Builder.Props
} }
} }
if (detail.item != null)
{
ItemBuilder.MakeItem(prop, go, sector, detail.item, mod);
isItem = true;
if (detail.hasPhysics)
{
NHLogger.LogWarning($"An item with the path {detail.path} has both '{nameof(DetailInfo.hasPhysics)}' and '{nameof(DetailInfo.item)}' set. This will usually result in undesirable behavior.");
}
}
if (detail.itemSocket != null)
{
ItemBuilder.MakeSocket(prop, go, sector, detail.itemSocket);
}
// Items should always be kept loaded else they will vanish in your hand as you leave the sector
if (isItem) detail.keepLoaded = true;
prop.transform.localScale = detail.stretch ?? (detail.scale != 0 ? Vector3.one * detail.scale : prefab.transform.localScale); prop.transform.localScale = detail.stretch ?? (detail.scale != 0 ? Vector3.one * detail.scale : prefab.transform.localScale);
if (detail.removeChildren != null) if (detail.removeChildren != null)
@ -271,11 +253,29 @@ namespace NewHorizons.Builder.Props
prop = newDetailGO; prop = newDetailGO;
} }
if (detail.item != null)
{
ItemBuilder.MakeItem(prop, go, sector, detail.item, mod);
isItem = true;
if (detail.hasPhysics)
{
NHLogger.LogWarning($"An item with the path {detail.path} has both '{nameof(DetailInfo.hasPhysics)}' and '{nameof(DetailInfo.item)}' set. This will usually result in undesirable behavior.");
}
}
if (detail.itemSocket != null)
{
ItemBuilder.MakeSocket(prop, go, sector, detail.itemSocket);
}
if (isItem) if (isItem)
{ {
// Else when you put them down you can't pick them back up // Else when you put them down you can't pick them back up
var col = prop.GetComponent<OWCollider>(); var col = prop.GetComponent<OWCollider>();
if (col != null) col._physicsRemoved = false; if (col != null) col._physicsRemoved = false;
// Items should always be kept loaded else they will vanish in your hand as you leave the sector
detail.keepLoaded = true;
} }
if (!detail.keepLoaded) GroupsBuilder.Make(prop, sector); if (!detail.keepLoaded) GroupsBuilder.Make(prop, sector);

View File

@ -59,10 +59,18 @@ namespace NewHorizons.Builder.Props
{ {
item.PickupAudio = AudioTypeHandler.GetAudioType(info.pickupAudio, mod); item.PickupAudio = AudioTypeHandler.GetAudioType(info.pickupAudio, mod);
} }
else
{
item.PickupAudio = AudioType.ToolItemWarpCorePickUp;
}
if (!string.IsNullOrEmpty(info.dropAudio)) if (!string.IsNullOrEmpty(info.dropAudio))
{ {
item.DropAudio = AudioTypeHandler.GetAudioType(info.dropAudio, mod); item.DropAudio = AudioTypeHandler.GetAudioType(info.dropAudio, mod);
} }
else
{
item.DropAudio = AudioType.ToolItemWarpCoreDrop;
}
if (!string.IsNullOrEmpty(info.socketAudio)) if (!string.IsNullOrEmpty(info.socketAudio))
{ {
item.SocketAudio = AudioTypeHandler.GetAudioType(info.socketAudio, mod); item.SocketAudio = AudioTypeHandler.GetAudioType(info.socketAudio, mod);

View File

@ -0,0 +1,99 @@
using NewHorizons.External.Modules.Conditionals;
using NewHorizons.Handlers;
using NewHorizons.Utility.OWML;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace NewHorizons.Components.Conditionals
{
public class ConditionalsManager : MonoBehaviour
{
public const int MAX_RECURSION = 120;
List<ConditionalCheckInfo> _checks = new();
bool _checksScheduled;
int _recursionCount = 0;
bool _avoidRecursionLogSpam = false;
public void AddCheck(ConditionalCheckInfo check)
{
_checks.Add(check);
}
public void RemoveCheck(ConditionalCheckInfo check)
{
_checks.Remove(check);
}
protected void Awake()
{
GlobalMessenger.AddListener("DialogueConditionsReset", ScheduleChecks);
GlobalMessenger<string, bool>.AddListener("DialogueConditionChanged", ScheduleChecks);
GlobalMessenger<string, bool>.AddListener("NHPersistentConditionChanged", ScheduleChecks);
GlobalMessenger.AddListener("ShipLogUpdated", ScheduleChecks);
}
protected void OnDestroy()
{
GlobalMessenger.RemoveListener("DialogueConditionsReset", ScheduleChecks);
GlobalMessenger<string, bool>.RemoveListener("DialogueConditionChanged", ScheduleChecks);
GlobalMessenger<string, bool>.RemoveListener("NHPersistentConditionChanged", ScheduleChecks);
GlobalMessenger.RemoveListener("ShipLogUpdated", ScheduleChecks);
}
protected void LateUpdate()
{
if (_checksScheduled)
{
_checksScheduled = false;
DoChecks();
}
else
{
// We had a frame without any checks being scheduled, so reset the recursion count and disable Update() again
_recursionCount = 0;
enabled = false;
}
}
void DoChecks()
{
if (_recursionCount >= MAX_RECURSION)
{
if (!_avoidRecursionLogSpam)
{
NHLogger.LogError($"Possible infinite loop detected while processing conditional checks; conditions were changed every single frame for {MAX_RECURSION} frames. This is likely caused by a mod using conflicting conditional checks that both set and unset the same condition.");
_avoidRecursionLogSpam = true;
}
return;
}
foreach (var check in _checks)
{
bool checkPassed = ConditionalsHandler.Check(check.check);
ConditionalsHandler.ApplyEffects(check.then, checkPassed);
}
_recursionCount++;
// Allow Update() to run
enabled = true;
}
// We schedule checks for the end of the frame instead of doing them immediately because GlobalMessenger doesn't support recursion and will throw an error if we update a condition that fires off another GlobalMessenger event
void ScheduleChecks()
{
_checksScheduled = true;
// Allow Update() to run
enabled = true;
}
// We could theoretically filter checks by conditionName here and only do the checks that matter, but the performance gain is likely not significant enough to be worth the extra complexity
void ScheduleChecks(string conditionName, bool conditionState)
{
ScheduleChecks();
}
}
}

View File

@ -20,6 +20,8 @@ namespace NewHorizons.Components.Props
public bool ClearPickupConditionOnDrop; public bool ClearPickupConditionOnDrop;
public string PickupFact; public string PickupFact;
string _translatedName;
public ItemType ItemType public ItemType ItemType
{ {
get => _type; get => _type;
@ -28,7 +30,11 @@ namespace NewHorizons.Components.Props
public override string GetDisplayName() public override string GetDisplayName()
{ {
return TranslationHandler.GetTranslation(DisplayName, TranslationHandler.TextType.UI); if (_translatedName == null)
{
_translatedName = TranslationHandler.GetTranslation(DisplayName, TranslationHandler.TextType.UI);
}
return _translatedName;
} }
public override bool CheckIsDroppable() public override bool CheckIsDroppable()
@ -90,6 +96,7 @@ namespace NewHorizons.Components.Props
void PlayCustomSound(AudioType audioType) void PlayCustomSound(AudioType audioType)
{ {
if (audioType == AudioType.None) return;
if (ItemBuilder.IsCustomItemType(ItemType)) if (ItemBuilder.IsCustomItemType(ItemType))
{ {
Locator.GetPlayerAudioController()._oneShotExternalSource.PlayOneShot(audioType); Locator.GetPlayerAudioController()._oneShotExternalSource.PlayOneShot(audioType);

View File

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Xml; using System.Xml;
using NewHorizons.External.Modules; using NewHorizons.External.Modules;
using NewHorizons.External.Modules.Conditionals;
using NewHorizons.External.SerializableData; using NewHorizons.External.SerializableData;
using Newtonsoft.Json; using Newtonsoft.Json;
using static NewHorizons.External.Modules.ShipLogModule; using static NewHorizons.External.Modules.ShipLogModule;
@ -155,6 +156,11 @@ namespace NewHorizons.External.Configs
/// </summary> /// </summary>
public CuriosityColorInfo[] curiosities; public CuriosityColorInfo[] curiosities;
/// <summary>
/// A list of conditional checks to be performed while in this star system.
/// </summary>
public ConditionalCheckInfo[] conditionalChecks;
/// <summary> /// <summary>
/// Extra data that may be used by extension mods /// Extra data that may be used by extension mods
/// </summary> /// </summary>
@ -383,6 +389,15 @@ namespace NewHorizons.External.Configs
GlobalMusic ??= otherConfig.GlobalMusic; GlobalMusic ??= otherConfig.GlobalMusic;
} }
if (conditionalChecks != null && otherConfig.conditionalChecks != null)
{
conditionalChecks = Concatenate(conditionalChecks, otherConfig.conditionalChecks);
}
else
{
conditionalChecks ??= otherConfig.conditionalChecks;
}
entryPositions = Concatenate(entryPositions, otherConfig.entryPositions); entryPositions = Concatenate(entryPositions, otherConfig.entryPositions);
curiosities = Concatenate(curiosities, otherConfig.curiosities); curiosities = Concatenate(curiosities, otherConfig.curiosities);
initialReveal = Concatenate(initialReveal, otherConfig.initialReveal); initialReveal = Concatenate(initialReveal, otherConfig.initialReveal);

View File

@ -0,0 +1,86 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NewHorizons.External.Modules.Conditionals
{
[JsonObject]
public class ConditionalCheckInfo
{
/// <summary>
/// The conditions that must be met for the check to pass
/// </summary>
public ConditionalCheckConditionsInfo check;
/// <summary>
/// The effects of the check if it passes
/// </summary>
public ConditionalCheckEffectsInfo then;
}
[JsonObject]
public class ConditionalCheckConditionsInfo
{
/// <summary>
/// The check will only pass if all of these dialogue conditions are set
/// </summary>
public string[] allConditionsSet;
/// <summary>
/// The check will only pass if any of these dialogue conditions are set
/// </summary>
public string[] anyConditionsSet;
/// <summary>
/// The check will only pass if all of these persistent conditions are set
/// </summary>
public string[] allPersistentConditionsSet;
/// <summary>
/// The check will only pass if any of these persistent conditions are set
/// </summary>
public string[] anyPersistentConditionsSet;
/// <summary>
/// The check will only pass if all of these ship log facts are revealed
/// </summary>
public string[] allFactsRevealed;
/// <summary>
/// The check will only pass if any of these ship log facts are revealed
/// </summary>
public string[] anyFactsRevealed;
/// <summary>
/// If the check should pass only if the conditions are not met
/// </summary>
public bool invert;
}
[JsonObject]
public class ConditionalCheckEffectsInfo
{
/// <summary>
/// The check will set these dialogue conditions if it passes
/// </summary>
public string[] setConditions;
/// <summary>
/// The check will unset these dialogue conditions if it passes
/// </summary>
public string[] unsetConditions;
/// <summary>
/// The check will set these persistent conditions if it passes
/// </summary>
public string[] setPersistentConditions;
/// <summary>
/// The check will unset these persistent conditions if it passes
/// </summary>
public string[] unsetPersistentConditions;
/// <summary>
/// The check will reveal these ship log facts if it passes
/// </summary>
public string[] revealFacts;
/// <summary>
/// If the check should undo its effects if the conditions are not met anymore (unset the set conditions, etc.). Note: ship log facts cannot currently be unrevealed.
/// </summary>
public bool reversible;
}
}

View File

@ -62,21 +62,25 @@ namespace NewHorizons.External.Modules.Props.Item
/// <summary> /// <summary>
/// The audio to play when this item is picked up. Only applies to custom/non-vanilla item types. /// The audio to play when this item is picked up. Only applies to custom/non-vanilla item types.
/// Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. /// Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.
/// Defaults to "ToolItemWarpCorePickUp". Set to "None" to disable the sound entirely.
/// </summary> /// </summary>
public string pickupAudio; public string pickupAudio;
/// <summary> /// <summary>
/// The audio to play when this item is dropped. Only applies to custom/non-vanilla item types. /// The audio to play when this item is dropped. Only applies to custom/non-vanilla item types.
/// Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. /// Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.
/// Defaults to "ToolItemWarpCoreDrop". Set to "None" to disable the sound entirely.
/// </summary> /// </summary>
public string dropAudio; public string dropAudio;
/// <summary> /// <summary>
/// The audio to play when this item is inserted into a socket. Only applies to custom/non-vanilla item types. /// The audio to play when this item is inserted into a socket. Only applies to custom/non-vanilla item types.
/// Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. /// Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.
/// Defaults to the pickup audio. Set to "None" to disable the sound entirely.
/// </summary> /// </summary>
public string socketAudio; public string socketAudio;
/// <summary> /// <summary>
/// The audio to play when this item is removed from a socket. Only applies to custom/non-vanilla item types. /// The audio to play when this item is removed from a socket. Only applies to custom/non-vanilla item types.
/// Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. /// Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.
/// Defaults to the drop audio. Set to "None" to disable the sound entirely.
/// </summary> /// </summary>
public string unsocketAudio; public string unsocketAudio;
/// <summary> /// <summary>

View File

@ -0,0 +1,103 @@
using NewHorizons.External.Modules.Conditionals;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NewHorizons.Handlers
{
public static class ConditionalsHandler
{
public static bool Check(ConditionalCheckConditionsInfo check)
{
var dcm = DialogueConditionManager.SharedInstance;
var passed = true;
if (check.allConditionsSet != null && check.allConditionsSet.Length > 0)
{
passed = passed && check.allConditionsSet.All(dcm.GetConditionState);
}
if (check.anyConditionsSet != null && check.anyConditionsSet.Length > 0)
{
passed = passed && check.anyConditionsSet.Any(dcm.GetConditionState);
}
if (check.allPersistentConditionsSet != null && check.allPersistentConditionsSet.Length > 0)
{
passed = passed && check.allPersistentConditionsSet.All(PlayerData.GetPersistentCondition);
}
if (check.anyPersistentConditionsSet != null && check.anyPersistentConditionsSet.Length > 0)
{
passed = passed && check.anyPersistentConditionsSet.Any(PlayerData.GetPersistentCondition);
}
if (check.allFactsRevealed != null && check.allFactsRevealed.Length > 0)
{
passed = passed && check.allFactsRevealed.All(ShipLogHandler.KnowsFact);
}
if (check.anyFactsRevealed != null && check.anyFactsRevealed.Length > 0)
{
passed = passed && check.anyFactsRevealed.Any(ShipLogHandler.KnowsFact);
}
if (check.invert)
{
passed = !passed;
}
return passed;
}
public static void ApplyEffects(ConditionalCheckEffectsInfo effects, bool checkPassed)
{
if ((checkPassed || effects.reversible) && effects.setConditions != null)
{
foreach (var condition in effects.setConditions)
{
if (DialogueConditionManager.SharedInstance.GetConditionState(condition) != checkPassed)
{
DialogueConditionManager.SharedInstance.SetConditionState(condition, checkPassed);
}
}
}
else if ((!checkPassed || effects.reversible) && effects.unsetConditions != null)
{
foreach (var condition in effects.unsetConditions)
{
if (DialogueConditionManager.SharedInstance.GetConditionState(condition) != !checkPassed)
{
DialogueConditionManager.SharedInstance.SetConditionState(condition, !checkPassed);
}
}
}
if ((checkPassed || effects.reversible) && effects.setPersistentConditions != null)
{
foreach (var condition in effects.setPersistentConditions)
{
if (!PlayerData.GetPersistentCondition(condition) != checkPassed)
{
PlayerData.SetPersistentCondition(condition, checkPassed);
}
}
}
else if ((!checkPassed || effects.reversible) && effects.unsetPersistentConditions != null)
{
foreach (var condition in effects.unsetPersistentConditions)
{
if (PlayerData.GetPersistentCondition(condition) != !checkPassed)
{
PlayerData.SetPersistentCondition(condition, !checkPassed);
}
}
}
if (checkPassed && effects.revealFacts != null)
{
foreach (var fact in effects.revealFacts)
{
if (!ShipLogHandler.KnowsFact(fact))
{
Locator.GetShipLogManager().RevealFact(fact);
}
}
}
}
}
}

View File

@ -9,7 +9,7 @@ using UnityEngine;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
using NewHorizons.OtherMods; using NewHorizons.OtherMods;
using NewHorizons.Components.EOTE; using NewHorizons.Components.EOTE;
using Epic.OnlineServices.Presence; using NewHorizons.Components.Conditionals;
namespace NewHorizons.Handlers namespace NewHorizons.Handlers
{ {
@ -32,6 +32,15 @@ namespace NewHorizons.Handlers
SkyboxBuilder.Make(system.Config.Skybox, system.Mod); SkyboxBuilder.Make(system.Config.Skybox, system.Mod);
} }
if (system.Config.conditionalChecks != null)
{
var conditionalsManager = new GameObject("ConditionalsManager").AddComponent<ConditionalsManager>();
foreach (var check in system.Config.conditionalChecks)
{
conditionalsManager.AddCheck(check);
}
}
// No time loop or travel audio at the eye // No time loop or travel audio at the eye
if (Main.Instance.CurrentStarSystem == "EyeOfTheUniverse") return; if (Main.Instance.CurrentStarSystem == "EyeOfTheUniverse") return;

View File

@ -143,5 +143,13 @@ namespace NewHorizons.Patches.PlayerPatches
{ {
NewHorizonsData.Save(); NewHorizonsData.Save();
} }
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerData.SetPersistentCondition))]
public static void PlayerData_SetPersistentCondition(string condition, bool state)
{
// Firing off a custom event for the Conditionals system to use. This could've been done with direct calls or a Unity event but it felt cleaner to mirror the vanilla "DialogueConditionChanged" event.
GlobalMessenger<string, bool>.FireEvent("NHPersistentConditionChanged", condition, state);
}
} }
} }

View File

@ -1152,19 +1152,19 @@
}, },
"pickupAudio": { "pickupAudio": {
"type": "string", "type": "string",
"description": "The audio to play when this item is picked up. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." "description": "The audio to play when this item is picked up. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.\nDefaults to \"ToolItemWarpCorePickUp\". Set to \"None\" to disable the sound entirely."
}, },
"dropAudio": { "dropAudio": {
"type": "string", "type": "string",
"description": "The audio to play when this item is dropped. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." "description": "The audio to play when this item is dropped. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.\nDefaults to \"ToolItemWarpCoreDrop\". Set to \"None\" to disable the sound entirely."
}, },
"socketAudio": { "socketAudio": {
"type": "string", "type": "string",
"description": "The audio to play when this item is inserted into a socket. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." "description": "The audio to play when this item is inserted into a socket. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.\nDefaults to the pickup audio. Set to \"None\" to disable the sound entirely."
}, },
"unsocketAudio": { "unsocketAudio": {
"type": "string", "type": "string",
"description": "The audio to play when this item is removed from a socket. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." "description": "The audio to play when this item is removed from a socket. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.\nDefaults to the drop audio. Set to \"None\" to disable the sound entirely."
}, },
"pickupCondition": { "pickupCondition": {
"type": "string", "type": "string",

View File

@ -110,6 +110,13 @@
"$ref": "#/definitions/CuriosityColorInfo" "$ref": "#/definitions/CuriosityColorInfo"
} }
}, },
"conditionalChecks": {
"type": "array",
"description": "A list of conditional checks to be performed while in this star system.",
"items": {
"$ref": "#/definitions/ConditionalCheckInfo"
}
},
"extras": { "extras": {
"type": "object", "type": "object",
"description": "Extra data that may be used by extension mods", "description": "Extra data that may be used by extension mods",
@ -451,6 +458,117 @@
"minimum": 0.0 "minimum": 0.0
} }
} }
},
"ConditionalCheckInfo": {
"type": "object",
"additionalProperties": false,
"properties": {
"check": {
"description": "The conditions that must be met for the check to pass",
"$ref": "#/definitions/ConditionalCheckConditionsInfo"
},
"then": {
"description": "The effects of the check if it passes",
"$ref": "#/definitions/ConditionalCheckEffectsInfo"
}
}
},
"ConditionalCheckConditionsInfo": {
"type": "object",
"additionalProperties": false,
"properties": {
"allConditionsSet": {
"type": "array",
"description": "The check will only pass if all of these dialogue conditions are set",
"items": {
"type": "string"
}
},
"anyConditionsSet": {
"type": "array",
"description": "The check will only pass if any of these dialogue conditions are set",
"items": {
"type": "string"
}
},
"allPersistentConditionsSet": {
"type": "array",
"description": "The check will only pass if all of these persistent conditions are set",
"items": {
"type": "string"
}
},
"anyPersistentConditionsSet": {
"type": "array",
"description": "The check will only pass if any of these persistent conditions are set",
"items": {
"type": "string"
}
},
"allFactsRevealed": {
"type": "array",
"description": "The check will only pass if all of these ship log facts are revealed",
"items": {
"type": "string"
}
},
"anyFactsRevealed": {
"type": "array",
"description": "The check will only pass if any of these ship log facts are revealed",
"items": {
"type": "string"
}
},
"invert": {
"type": "boolean",
"description": "If the check should pass only if the conditions are not met"
}
}
},
"ConditionalCheckEffectsInfo": {
"type": "object",
"additionalProperties": false,
"properties": {
"setConditions": {
"type": "array",
"description": "The check will set these dialogue conditions if it passes",
"items": {
"type": "string"
}
},
"unsetConditions": {
"type": "array",
"description": "The check will unset these dialogue conditions if it passes",
"items": {
"type": "string"
}
},
"setPersistentConditions": {
"type": "array",
"description": "The check will set these persistent conditions if it passes",
"items": {
"type": "string"
}
},
"unsetPersistentConditions": {
"type": "array",
"description": "The check will unset these persistent conditions if it passes",
"items": {
"type": "string"
}
},
"revealFacts": {
"type": "array",
"description": "The check will reveal these ship log facts if it passes",
"items": {
"type": "string"
}
},
"reversible": {
"type": "boolean",
"description": "If the check should undo its effects if the conditions are not met anymore (unset the set conditions, etc.). Note: ship log facts cannot currently be unrevealed."
}
}
} }
}, },
"$docs": { "$docs": {