diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js index c3957dfd..b451dd1d 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.js +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js @@ -88,6 +88,12 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ index: 1, removeBeforeCompute: true, }, + deactivatingToggleId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + removeBeforeCompute: true, + }, // When this is true on any property, the creature needs to be recomputed dirty: { type: Boolean, diff --git a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js index b7a98595..3d9bf564 100644 --- a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js @@ -1,16 +1,31 @@ import walkDown from '/imports/api/engine/computation/utility/walkdown.js'; +import { getEffectTagTargets } from '/imports/api/engine/computation/buildComputation/linkTypeDependencies.js'; -export default function computeToggleDependencies(node, dependencyGraph){ +export default function computeToggleDependencies(node, dependencyGraph, computation, forest) { const prop = node.node; - // Only for toggles that aren't inactive and aren't set to enabled or disabled - if ( - prop.inactive || - prop.type !== 'toggle' || - prop.disabled || - prop.enabled - ) return; + // Only for toggles + if (prop.type !== 'toggle') return; + + if (prop.targetByTags) { + // Find all the props targeted by tags, and disable them and their children + getEffectTagTargets(prop, computation).forEach(targetId => { + const target = forest.nodeIndex[targetId]; + if (!target) return; + target.node._computationDetails.toggleAncestors.push(prop); + dependencyGraph.addLink(target.node._id, prop._id, 'toggle'); + walkDown(target.children, child => { + // The child nodes depend on the toggle + child.node._computationDetails.toggleAncestors.push(prop); + dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); + }); + }); + } + + // We don't need to link direct children of static toggles, it's already done + if (prop.disabled || prop.enabled) return; + walkDown(node.children, child => { - // The child nodes depend on the toggle condition compuation + // The child nodes depend on the toggle child.node._computationDetails.toggleAncestors.push(prop); dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); }); diff --git a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js index 4913dc7d..826eaaee 100644 --- a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js @@ -164,7 +164,7 @@ function linkEffects(dependencyGraph, prop, computation) { } // Returns an array of IDs of the properties the effect targets -function getEffectTagTargets(effect, computation) { +export function getEffectTagTargets(effect, computation) { let targets = getTargetListFromTags(effect.targetTags, computation); let notIds = []; if (effect.extraTags) { diff --git a/app/imports/api/engine/computation/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.js index a736354c..a97f5b0a 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.js @@ -29,7 +29,7 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js'; * computed toggles */ -export default function buildCreatureComputation(creatureId){ +export default function buildCreatureComputation(creatureId) { const creature = getCreature(creatureId); const variables = getVariables(creatureId); const properties = getProperties(creatureId); @@ -37,7 +37,7 @@ export default function buildCreatureComputation(creatureId){ return computation; } -export function buildComputationFromProps(properties, creature, variables){ +export function buildComputationFromProps(properties, creature, variables) { const computation = new CreatureComputation(properties, creature, variables); // Dependency graph where edge(a, b) means a depends on b @@ -49,14 +49,14 @@ export function buildComputationFromProps(properties, creature, variables){ const dependencyGraph = computation.dependencyGraph; // Link the denormalizedStats from the creature - if (creature && creature.denormalizedStats){ - if (creature.denormalizedStats.xp){ + if (creature && creature.denormalizedStats) { + if (creature.denormalizedStats.xp) { dependencyGraph.addNode('xp', { baseValue: creature.denormalizedStats.xp, type: '_variable' }); } - if (creature.denormalizedStats.milestoneLevels){ + if (creature.denormalizedStats.milestoneLevels) { dependencyGraph.addNode('milestoneLevels', { baseValue: creature.denormalizedStats.milestoneLevels, type: '_variable' @@ -93,7 +93,7 @@ export function buildComputationFromProps(properties, creature, variables){ // Inactive status must be complete for the whole tree before toggle deps // are calculated walkDown(forest, node => { - computeToggleDependencies(node, dependencyGraph); + computeToggleDependencies(node, dependencyGraph, computation, forest); computeSlotQuantityFilled(node, dependencyGraph); }); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType.js b/app/imports/api/engine/computation/computeComputation/computeByType.js index 0b5633e3..0f3d5436 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType.js @@ -6,6 +6,7 @@ import pointBuy from './computeByType/computePointBuy.js'; import propertySlot from './computeByType/computeSlot.js'; import container from './computeByType/computeContainer.js'; import spellList from './computeByType/computeSpellList.js'; +import toggle from './computeByType/computeToggle.js'; import _calculation from './computeByType/computeCalculation.js'; export default Object.freeze({ @@ -19,4 +20,5 @@ export default Object.freeze({ propertySlot, spell: action, spellList, + toggle, }); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeToggle.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeToggle.js new file mode 100644 index 00000000..bba5c4ff --- /dev/null +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeToggle.js @@ -0,0 +1,7 @@ +export default function computeToggle(computation, node) { + const prop = node.data; + if (!prop.enabled && !prop.disabled && prop.condition && !prop.condition.value) { + prop.inactive = true; + prop.deactivatedBySelf = true; + } +} diff --git a/app/imports/api/engine/computation/computeComputation/computeToggles.js b/app/imports/api/engine/computation/computeComputation/computeToggles.js index caa310d2..657305d6 100644 --- a/app/imports/api/engine/computation/computeComputation/computeToggles.js +++ b/app/imports/api/engine/computation/computeComputation/computeToggles.js @@ -1,13 +1,16 @@ -export default function evaluateToggles(computation, node){ +export default function evaluateToggles(computation, node) { let prop = node.data; if (!prop) return; let toggles = prop._computationDetails?.toggleAncestors; if (!toggles) return; toggles.forEach(toggle => { - if (!toggle.condition) return; - if (!toggle.condition.value){ + if ( + (!toggle.enabled && !toggle.disabled && toggle.condition && !toggle.condition.value) + || (toggle.disabled) + ) { prop.inactive = true; prop.deactivatedByToggle = true; + prop.deactivatingToggleId = toggle._id; } }); } diff --git a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js index fcad024c..0548fedd 100644 --- a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js +++ b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js @@ -3,12 +3,12 @@ import { EJSON } from 'meteor/ejson'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; -export default function writeAlteredProperties(computation){ +export default function writeAlteredProperties(computation) { let bulkWriteOperations = []; // Loop through all properties on the memo computation.props.forEach(changed => { let schema = propertySchemasIndex[changed.type]; - if (!schema){ + if (!schema) { console.warn('No schema for ' + changed.type); return; } @@ -20,12 +20,13 @@ export default function writeAlteredProperties(computation){ 'deactivatedBySelf', 'deactivatedByAncestor', 'deactivatedByToggle', + 'deactivatingToggleId', 'damage', 'dirty', ...schema.objectKeys(), ]; op = addChangedKeysToOp(op, keys, original, changed); - if (op){ + if (op) { bulkWriteOperations.push(op); } }); @@ -37,10 +38,10 @@ function addChangedKeysToOp(op, keys, original, changed) { // Loop through all keys that can be changed by computation // and compile an operation that sets all those keys for (let key of keys) { - if (!EJSON.equals(original[key], changed[key])){ + if (!EJSON.equals(original[key], changed[key])) { if (!op) op = newOperation(original._id, changed.type); let value = changed[key]; - if (value === undefined){ + if (value === undefined) { // Unset values that become undefined addUnsetOp(op, key); } else { @@ -52,32 +53,32 @@ function addChangedKeysToOp(op, keys, original, changed) { return op; } -function newOperation(_id, type){ +function newOperation(_id, type) { let newOp = { updateOne: { - filter: {_id}, + filter: { _id }, update: {}, } }; - if (Meteor.isClient){ + if (Meteor.isClient) { newOp.type = type; } return newOp; } -function addSetOp(op, key, value){ - if (op.updateOne.update.$set){ +function addSetOp(op, key, value) { + if (op.updateOne.update.$set) { op.updateOne.update.$set[key] = value; } else { - op.updateOne.update.$set = {[key]: value}; + op.updateOne.update.$set = { [key]: value }; } } -function addUnsetOp(op, key){ - if (op.updateOne.update.$unset){ +function addUnsetOp(op, key) { + if (op.updateOne.update.$unset) { op.updateOne.update.$unset[key] = 1; } else { - op.updateOne.update.$unset = {[key]: 1}; + op.updateOne.update.$unset = { [key]: 1 }; } } @@ -100,14 +101,14 @@ function writePropertiesSequentially(bulkWriteOps) { // in the UI because of incompatibility with latency compensation. If the // duplicate redraws can be fixed, this is a strictly better way of processing // writes -function bulkWriteProperties(bulkWriteOps){ +function bulkWriteProperties(bulkWriteOps) { if (!bulkWriteOps.length) return; // bulkWrite is only available on the server if (Meteor.isServer) { CreatureProperties.rawCollection().bulkWrite( bulkWriteOps, - {ordered : false}, - function(e){ + { ordered: false }, + function (e) { if (e) { console.error('Bulk write failed: '); console.error(e); diff --git a/app/imports/api/parenting/nodesToTree.js b/app/imports/api/parenting/nodesToTree.js index 3e4b5733..fae36df1 100644 --- a/app/imports/api/parenting/nodesToTree.js +++ b/app/imports/api/parenting/nodesToTree.js @@ -26,6 +26,7 @@ export function nodeArrayToTree(nodes) { forest.push(treeNode); } }); + forest.nodeIndex = nodeIndex; return forest; } diff --git a/app/imports/api/properties/Effects.js b/app/imports/api/properties/Effects.js index e1ba0c37..77eadbeb 100644 --- a/app/imports/api/properties/Effects.js +++ b/app/imports/api/properties/Effects.js @@ -1,6 +1,7 @@ import SimpleSchema from 'simpl-schema'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js'; /* * Effects are reason-value attached to skills and abilities @@ -50,57 +51,7 @@ let EffectSchema = createPropertySchema({ type: String, max: STORAGE_LIMITS.variableName, }, - // True when targeting by tags instead of stats - targetByTags: { - type: Boolean, - optional: true, - }, - // If targeting by tags, the field which will be targeted - targetField: { - type: String, - optional: true, - max: STORAGE_LIMITS.variableName, - }, - // Which tags the effect is applied to - targetTags: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.tagCount, - }, - 'targetTags.$': { - type: String, - max: STORAGE_LIMITS.tagLength, - }, - extraTags: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.extraTagsCount, - }, - 'extraTags.$': { - type: Object, - }, - 'extraTags.$._id': { - type: String, - regEx: SimpleSchema.RegEx.Id, - autoValue(){ - if (!this.isSet) return Random.id(); - } - }, - 'extraTags.$.operation': { - type: String, - allowedValues: ['OR', 'NOT'], - defaultValue: 'OR', - }, - 'extraTags.$.tags': { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.tagCount, - }, - 'extraTags.$.tags.$': { - type: String, - max: STORAGE_LIMITS.tagLength, - }, -}); +}).extend(TagTargetingSchema); const ComputedOnlyEffectSchema = createPropertySchema({ amount: { diff --git a/app/imports/api/properties/Proficiencies.js b/app/imports/api/properties/Proficiencies.js index 151f5509..cec32217 100644 --- a/app/imports/api/properties/Proficiencies.js +++ b/app/imports/api/properties/Proficiencies.js @@ -1,5 +1,6 @@ import SimpleSchema from 'simpl-schema'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js'; let ProficiencySchema = new SimpleSchema({ name: { @@ -24,57 +25,7 @@ let ProficiencySchema = new SimpleSchema({ allowedValues: [0.49, 0.5, 1, 2], defaultValue: 1, }, - // True when targeting by tags instead of stats - targetByTags: { - type: Boolean, - optional: true, - }, - // If targeting by tags, the field which will be targeted - targetField: { - type: String, - optional: true, - max: STORAGE_LIMITS.variableName, - }, - // Which tags the proficiency is applied to - targetTags: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.tagCount, - }, - 'targetTags.$': { - type: String, - max: STORAGE_LIMITS.tagLength, - }, - extraTags: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.extraTagsCount, - }, - 'extraTags.$': { - type: Object, - }, - 'extraTags.$._id': { - type: String, - regEx: SimpleSchema.RegEx.Id, - autoValue() { - if (!this.isSet) return Random.id(); - } - }, - 'extraTags.$.operation': { - type: String, - allowedValues: ['OR', 'NOT'], - defaultValue: 'OR', - }, - 'extraTags.$.tags': { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.tagCount, - }, - 'extraTags.$.tags.$': { - type: String, - max: STORAGE_LIMITS.tagLength, - }, -}); +}).extend(TagTargetingSchema); const ComputedOnlyProficiencySchema = new SimpleSchema({}); diff --git a/app/imports/api/properties/Skills.js b/app/imports/api/properties/Skills.js index ecd9dd20..7f15df3a 100644 --- a/app/imports/api/properties/Skills.js +++ b/app/imports/api/properties/Skills.js @@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema'; import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js'; /* * Skills are anything that results in a modifier to be added to a D20 @@ -59,52 +60,8 @@ let SkillSchema = createPropertySchema({ type: 'inlineCalculationFieldToCompute', optional: true, }, - // Skills can apply their value to other calculations as a proficiency - // True when applying skill to tagged props - targetByTags: { - type: Boolean, - optional: true, - }, - // Which tags the proficiency is applied to - targetTags: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.tagCount, - }, - 'targetTags.$': { - type: String, - max: STORAGE_LIMITS.tagLength, - }, - extraTags: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.extraTagsCount, - }, - 'extraTags.$': { - type: Object, - }, - 'extraTags.$._id': { - type: String, - regEx: SimpleSchema.RegEx.Id, - autoValue() { - if (!this.isSet) return Random.id(); - } - }, - 'extraTags.$.operation': { - type: String, - allowedValues: ['OR', 'NOT'], - defaultValue: 'OR', - }, - 'extraTags.$.tags': { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.tagCount, - }, - 'extraTags.$.tags.$': { - type: String, - max: STORAGE_LIMITS.tagLength, - }, -}); + // Skills can apply their value to other calculations as a proficiency using tag targeting +}).extend(TagTargetingSchema); let ComputedOnlySkillSchema = createPropertySchema({ // Computed value of skill to be added to skill rolls diff --git a/app/imports/api/properties/Toggles.js b/app/imports/api/properties/Toggles.js index 755ed89b..c5babdc6 100644 --- a/app/imports/api/properties/Toggles.js +++ b/app/imports/api/properties/Toggles.js @@ -1,6 +1,7 @@ import SimpleSchema from 'simpl-schema'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js'; const ToggleSchema = createPropertySchema({ name: { @@ -31,7 +32,7 @@ const ToggleSchema = createPropertySchema({ type: 'fieldToCompute', optional: true, }, -}); +}).extend(TagTargetingSchema); const ComputedOnlyToggleSchema = createPropertySchema({ condition: { diff --git a/app/imports/api/properties/subSchemas/TagTargetingSchema.js b/app/imports/api/properties/subSchemas/TagTargetingSchema.js new file mode 100644 index 00000000..8bbe1ddb --- /dev/null +++ b/app/imports/api/properties/subSchemas/TagTargetingSchema.js @@ -0,0 +1,57 @@ +import SimpleSchema from 'simpl-schema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; + +const TagTargetingSchema = new SimpleSchema({ + // True when targeting by tags instead of stats + targetByTags: { + type: Boolean, + optional: true, + }, + // If targeting by tags, the field which will be targeted + targetField: { + type: String, + optional: true, + max: STORAGE_LIMITS.variableName, + }, + // Which tags the effect is applied to + targetTags: { + type: Array, + optional: true, + maxCount: STORAGE_LIMITS.tagCount, + }, + 'targetTags.$': { + type: String, + max: STORAGE_LIMITS.tagLength, + }, + extraTags: { + type: Array, + optional: true, + maxCount: STORAGE_LIMITS.extraTagsCount, + }, + 'extraTags.$': { + type: Object, + }, + 'extraTags.$._id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + autoValue() { + if (!this.isSet) return Random.id(); + } + }, + 'extraTags.$.operation': { + type: String, + allowedValues: ['OR', 'NOT'], + defaultValue: 'OR', + }, + 'extraTags.$.tags': { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.tagCount, + }, + 'extraTags.$.tags.$': { + type: String, + max: STORAGE_LIMITS.tagLength, + }, +}); + +export default TagTargetingSchema; diff --git a/app/imports/client/ui/properties/forms/ToggleForm.vue b/app/imports/client/ui/properties/forms/ToggleForm.vue index d288666a..24cbd6d5 100644 --- a/app/imports/client/ui/properties/forms/ToggleForm.vue +++ b/app/imports/client/ui/properties/forms/ToggleForm.vue @@ -45,6 +45,29 @@ /> + + + + + + + + @@ -69,8 +92,12 @@ diff --git a/app/imports/client/ui/properties/viewers/shared/PropertyTargetTags.vue b/app/imports/client/ui/properties/viewers/shared/PropertyTargetTags.vue new file mode 100644 index 00000000..d7b14d3b --- /dev/null +++ b/app/imports/client/ui/properties/viewers/shared/PropertyTargetTags.vue @@ -0,0 +1,57 @@ + + + \ No newline at end of file