diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js index fdf651d8..ba6acda9 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js @@ -3,8 +3,11 @@ import walkDown from '/imports/api/creature/computation/newEngine/utility/walkdo export default function computeInactiveStatus(node){ const prop = node.node; if (isActive(prop)) return; - prop.inactive = true; - prop.deactivatedBySelf = true; + // Unequipped items disable their children, but are not disabled themselves + if (prop.type !== 'item'){ + prop.inactive = true; + prop.deactivatedBySelf = true; + } // Mark children as inactive due to ancestor walkDown(node.children, child => { child.node.inactive = true; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js deleted file mode 100644 index c97f92d4..00000000 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Performs a depth first traversal of the character tree, summing the container - * and inventory contents on the way up the tree - */ -export default function computeInventory(forest, dependencyGraph){ - const data = { - weightTotal: 0, - weightEquipment: 0, - weightCarried: 0, - valueTotal: 0, - valueEquipment: 0, - valueCarried: 0, - itemsAttuned: 0, - }; - // The stack of properties to still navigate - const stack = [...forest]; - // The current containers we are inside of - const containerStack = []; - - while(stack.length){ - const top = stack[stack.length - 1]; - const prop = top.node; - if (prop._computationDetails.inventoryChildrenVisited){ - stack.pop(); - handleProp(prop, containerStack, data, dependencyGraph); - } else { - // Add all containers to the stack when we first visit them - if (prop.type === 'container'){ - containerStack.push(top.node); - setDefaultContainerData(prop); - } - // Push children onto the stack and mark this as children are visited - stack.push(...top.children); - prop._computationDetails.inventoryChildrenVisited = true; - } - } - // Store all the computed values on the dependency graph variables - for (let key in data){ - dependencyGraph.addNode(key, {engineValue: data[key]}); - } -} - -function setDefaultContainerData(container){ - container.contentsWeight = 0; - container.carriedWeight = 0; - container.contentsValue = 0; - container.carriedValue = 0; -} - -function handleProp(prop, containerStack, data, dependencyGraph){ - // Determine if this property is carried, items are carried by default - let carried = prop.type === 'container' ? prop.carried : true; - - // Weight and value for this property - const weight = (prop.weight || 0) + (prop.contentsWeight || 0); - const carriedWeight = (prop.weight || 0) + (prop.carriedWeight || 0); - const value = (prop.value || 0) + (prop.value || 0); - const carriedValue = (prop.value || 0) + (prop.carriedValue || 0); - - // Sum the item-specific data - if (prop.type === 'item'){ - dependencyGraph.addLink('itemsAttuned', prop._id, 'inventory'); - if (prop.attuned) data.itemsAttuned += 1; - if (prop.equipped){ - dependencyGraph.addLink('weightEquipment', prop._id, 'inventory'); - data.weightEquipment += weight; - dependencyGraph.addLink('valueEquipment', prop._id, 'inventory'); - data.valueEquipment += value; - } - } - - // Get the parent container - const container = containerStack[containerStack.length - 1]; - - if (container){ - // The container depends on this prop for its contents data - dependencyGraph.addLink(container._id, prop._id, 'inventory'); - // Add this property's weights and values to the container - if (!container.weightless){ - container.contentsWeight += weight; - if (carried) container.carriedWeight += carriedWeight; - } - container.contentsValue += value; - if (carried) container.carriedValue += carriedValue; - } else { - // There is no parent container, add weights/value to the character data - dependencyGraph.addLink('weightTotal', prop._id, 'inventory'); - data.weightTotal += weight; - dependencyGraph.addLink('valueTotal', prop._id, 'inventory'); - data.valueTotal += value; - if (carried){ - dependencyGraph.addLink('weightCarried', prop._id, 'inventory'); - data.weightCarried += carriedWeight; - dependencyGraph.addLink('valueCarried', prop._id, 'inventory'); - data.valueCarried += carriedValue; - } - } -} diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js index 7dd1694d..b44f2681 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js @@ -4,6 +4,7 @@ */ export default function computeSlotQuantityFilled(node, dependencyGraph){ let slot = node.node; + if (slot.type !== 'propertySlot' || slot.type !== 'characterClass') return; slot.totalFilled = 0; node.children.forEach(child => { let childProp = child.node; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js new file mode 100644 index 00000000..d0b0f738 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js @@ -0,0 +1,59 @@ +/** + * Performs a depth first traversal of the character tree, summing the container + * and inventory contents on the way up the tree + */ +export default function linkInventory(forest, dependencyGraph){ + // The stack of properties to still navigate + const stack = [...forest]; + // The current containers we are inside of + const containerStack = []; + + while(stack.length){ + const top = stack[stack.length - 1]; + const prop = top.node; + if (prop._computationDetails.inventoryChildrenVisited){ + stack.pop(); + handleProp(prop, containerStack, dependencyGraph); + } else { + // Add all containers to the stack when we first visit them + if (prop.type === 'container'){ + containerStack.push(top.node); + } + // Push children onto the stack and mark this as children are visited + stack.push(...top.children); + prop._computationDetails.inventoryChildrenVisited = true; + } + } +} + +function handleProp(prop, containerStack, dependencyGraph){ + // Determine if this property is carried, items are carried by default + let carried = prop.type === 'container' ? prop.carried : true; + + // Item-specific links + if (prop.type === 'item'){ + if (prop.attuned){ + dependencyGraph.addLink('itemsAttuned', prop._id, 'attunedItem'); + } + if (prop.equipped){ + dependencyGraph.addLink('weightEquipment', prop._id, 'equippedItem'); + dependencyGraph.addLink('valueEquipment', prop._id, 'equippedItem'); + } + } + + // Get the parent container + const container = containerStack[containerStack.length - 1]; + + if (container){ + // The container depends on this prop for its contents data + dependencyGraph.addLink(container._id, prop._id, 'containerContents'); + } else { + // There is no parent container, the character totals depend on this prop + dependencyGraph.addLink('weightTotal', prop._id, 'inventoryStats'); + dependencyGraph.addLink('valueTotal', prop._id, 'inventoryStats'); + if (carried){ + dependencyGraph.addLink('weightCarried', prop._id, 'inventoryStats'); + dependencyGraph.addLink('valueCarried', prop._id, 'inventoryStats'); + } + } +} diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js index ad3b231a..a16ad83c 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js @@ -2,7 +2,8 @@ const linkDependenciesByType = { action: linkResources, attack: linkResources, attribute: linkAttribute, - classLevel: linkVariableName, + characterClass: linkVariableName, + classLevel: linkClassLevel, constant: linkVariableName, damageMultiplier: linkDamageMultiplier, proficiency: linkStats, @@ -15,6 +16,18 @@ export default function linkTypeDependencies(dependencyGraph, prop){ linkDependenciesByType[prop.type]?.(prop); } +function linkClassLevel(dependencyGraph, prop){ + // The variableName of the prop depends on the prop + if (prop.variableName && prop.level){ + dependencyGraph.addLink(prop.variableName, prop._id, 'classLevel'); + // The level variable depends on the class variableName variable + let existingLevelLink = dependencyGraph.getLink('level', prop.variableName); + if (!existingLevelLink){ + dependencyGraph.addLink('level', prop.variableName, 'level'); + } + } +} + function linkVariableName(dependencyGraph, prop){ // The variableName of the prop depends on the prop if (prop.variableName){ diff --git a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js index c1a3886e..65a20ce0 100644 --- a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js @@ -7,7 +7,7 @@ import computedSchemas from '/imports/api/properties/computedPropertySchemasInde import applyFnToKey from '/imports/api/creature/computation/newEngine/utility/applyFnToKey.js'; import { cloneDeep, unset } from 'lodash'; import createGraph from 'ngraph.graph'; -import computeInventory from '/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js'; +import linkInventory from '/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js'; import walkDown from '/imports/api/creature/computation/newEngine/utility/walkdown.js'; import parseCalculationFields from '/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js'; import computeInactiveStatus from '/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js'; @@ -36,19 +36,26 @@ import computeSlotQuantityFilled from '/imports/api/creature/computation/newEngi */ export default function buildCreatureComputation(creatureId){ - let properties = CreatureProperties.find({ + const properties = getProperties(creatureId); + return buildComputationFromProps(properties); +} + +function getProperties(creatureId){ + return CreatureProperties.find({ 'ancestors.id': creatureId, 'removed': {$ne: true}, }, { sort: {order: 1} }); +} +export function buildComputationFromProps(properties){ // Dependency graph where edge(a, b) means a depends on b // The graph includes all dependencies even of inactive properties // such that any properties changing without changing their dependencies // can limit the recompute to connected parts of the graph // Each node's data represents a prop or a virtual prop like a variable - // Each link's data: {type: String, data: Object, requiresComputation: Boolean} + // Each link's data is a string representing the link type const dependencyGraph = createGraph(); const computation = { @@ -86,7 +93,7 @@ export default function buildCreatureComputation(creatureId){ }; // Parse all the calculations - parseCalculationFields(prop, computedSchemas) + parseCalculationFields(prop, computedSchemas); }); // Get all the properties as trees based on their ancestors @@ -98,8 +105,8 @@ export default function buildCreatureComputation(creatureId){ computeSlotQuantityFilled(node); }); - // Compute the inventory - computeInventory(forest, dependencyGraph); + // Link the inventory dependencies + linkInventory(forest, dependencyGraph); // Graph functions that rely on the props being stored first properties.forEach(prop => { diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js index 35352de6..c505e81b 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js @@ -2,6 +2,7 @@ import aggregate from '/imports/api/creature/computation/newEngine/computeComput import computeVariableAsAttribute from '/imports/api/creature/computation/newEngine/computeComputation/computeVariableAsType/computeVariableAsAttribute.js'; import computeVariableAsSkill from '/imports/api/creature/computation/newEngine/computeComputation/computeVariableAsType/computeVariableAsSkill.js'; import computeVariableAsConstant from '/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js'; +import computeVariableAsClass from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/computeVariableAsClass.js'; import computeImplicitVariable from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/computeImplicitVariable.js'; export default function computeVariable(graph, node, scope){ @@ -26,9 +27,11 @@ function aggregateLinks(graph, node){ if (linkedNode.data.inactive) return; // Apply all the aggregations let arg = {node, linkedNode, link}; - aggregate.definition(arg); + aggregate.classLevel(arg); aggregate.damageMultiplier(arg); + aggregate.definition(arg); aggregate.effect(arg); + aggregate.inventory(arg); aggregate.proficiency(arg); }, true // enumerate only outbound links @@ -50,6 +53,8 @@ function computeVariableProp(node, prop, scope){ computeVariableAsSkill(node, prop, scope) } else if (prop.type === 'constant'){ computeVariableAsConstant(node, prop, scope) + } else if (prop.type === 'characterClass'){ + computeVariableAsClass(node, prop, scope) } } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js new file mode 100644 index 00000000..84db29b2 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js @@ -0,0 +1,15 @@ +export default function aggregateClassLevel({node, linkedNode, link}){ + if (link.data === 'classLevel'){ + if (node.data.inactive) return; + if (!node.data.classLevelAggregator) node.data.classLevelAggregator = { + levelsFilled: [true], // Level 0 is always filled + level: 0, + }; + let linkedProp = linkedNode.data; + let aggregator = node.data.classLevelAggregator; + if (linkedProp.level > aggregator.level) aggregator.level = linkedProp.level; + aggregator.levelsFilled[linkedProp.level] = true; + } else if (link.data === 'level'){ + node.baseValue = (node.baseValue || 0) + node.data.classLevelAggregator.level; + } +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js new file mode 100644 index 00000000..e2df5384 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js @@ -0,0 +1,60 @@ +export default function aggregateInventory({node, linkedNode, link}){ + let linkedProp = linkedNode.data || {}; + const prop = node.data; + + switch (link.data){ + case 'attunedItem': + prop.baseValue = (prop.baseValue || 0) + 1; + return; + + case 'equippedItem': + if (node.id === 'weightEquipment'){ + prop.baseValue = (prop.baseValue || 0) + weight(linkedProp); + } else if (node.id === 'valueEquipment'){ + prop.baseValue = (prop.baseValue || 0) + value(linkedProp); + } + return; + + case 'containerContents': + // Add this property's weights and values to the container + if (!prop.weightless){ + prop.contentsWeight = (prop.contentsWeight || 0) + weight(linkedProp); + if (prop.carried){ + prop.carriedWeight = (prop.carriedWeight || 0) + carriedWeight(linkedProp); + } + } + prop.contentsValue = (prop.contentsValue || 0) + value(linkedProp); + if (prop.carried) { + prop.carriedValue = (prop.carriedValue || 0) + carriedValue(linkedProp); + } + return; + + case 'inventoryStats': + if (node.id === 'weightTotal'){ + prop.baseValue = (prop.baseValue || 0) + weight(linkedProp); + } else if (node.id === 'valueTotal'){ + prop.baseValue = (prop.baseValue || 0) + value(linkedProp); + } else if (node.did === 'weightCarried'){ + prop.baseValue = (prop.baseValue || 0) + carriedWeight(linkedProp); + } else if (node.did === 'valueCarried'){ + prop.carriedValue = (prop.carriedValue || 0) + carriedValue(linkedProp); + } + return; + } +} + +function weight(prop){ + return (prop.weight || 0) + (prop.contentsWeight || 0); +} + +function carriedWeight(prop){ + return (prop.weight || 0) + (prop.carriedWeight || 0); +} + +function value (prop){ + return (prop.value || 0) + (prop.value || 0); +} + +function carriedValue (prop){ + return (prop.value || 0) + (prop.carriedValue || 0); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js index 5cb17b1c..4b455afa 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js @@ -2,10 +2,14 @@ import definition from './aggregateDefinition.js'; import damageMultiplier from './aggregateDamageMultiplier.js'; import effect from './aggregateEffect.js'; import proficiency from './aggregateProficiency.js'; +import classLevel from './aggregateClassLevel.js'; +import inventory from './aggregateInventory.js'; export default Object.freeze({ - definition, + classLevel, damageMultiplier, + definition, effect, + inventory, proficiency, }); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js index 5d1d365c..1f15ff23 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js @@ -11,25 +11,33 @@ import getAggregatorResult from '/imports/api/creature/computation/newEngine/com prop.value = result; prop.proficiency = node.data.proficiency; - // denormalise the aggregator fields - const aggregator = node.data.effectAggregator; - if (aggregator.advantage && !aggregator.disadvantage){ - prop.advantage = 1; - } else if (aggregator.disadvantage && !aggregator.advantage){ - prop.advantage = -1; - } else { - prop.advantage = 0; + // denormalise class level aggregator + let classLevelAgg = node.data.classLevelAggregator; + if (classLevelAgg){ + prop.level = classLevelAgg.level; + } + + // denormalise the effect aggregator fields + const aggregator = node.data.effectAggregator; + if (aggregator){ + if (aggregator.advantage && !aggregator.disadvantage){ + prop.advantage = 1; + } else if (aggregator.disadvantage && !aggregator.advantage){ + prop.advantage = -1; + } else { + prop.advantage = 0; + } + // Passive bonus + prop.passiveBonus = aggregator.passiveAdd; + // conditional benefits + prop.conditionalBenefits = aggregator.conditional; + // Roll bonuses + prop.rollBonus = aggregator.rollBonus; + // Forced to fail + prop.fail = aggregator.fail; + // Rollbonus + prop.rollBonuses = aggregator.rollBonus; } - // Passive bonus - prop.passiveBonus = aggregator.passiveAdd; - // conditional benefits - prop.conditionalBenefits = aggregator.conditional; - // Roll bonuses - prop.rollBonus = aggregator.rollBonus; - // Forced to fail - prop.fail = aggregator.fail; - // Rollbonus - prop.rollBonuses = aggregator.rollBonus; return prop; } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js index 93b1aefe..81f4c73b 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -10,7 +10,7 @@ export default function computeVariableAsAttribute(node, prop, scope){ // Ability scores get modifiers if (prop.attributeType === 'ability'){ - prop.modifier = Math.floor((prop.currentValue - 10) / 2); + prop.modifier = Math.floor((prop.value - 10) / 2); } // Hit dice denormalise constitution modifier diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js new file mode 100644 index 00000000..d48839f2 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js @@ -0,0 +1,12 @@ +export default function computeVariableAsAttribute(node, prop){ + let classLevelAgg = node.data.classLevelAggregator; + if (!classLevelAgg) return; + prop.level = classLevelAgg.level; + classLevelAgg.levelsFilled.forEach((filled, index) => { + if (!filled){ + if (!prop.missingLevels) prop.missingLevels = []; + prop.missingLevels.push(index); + } + }); + prop.missingLevels?.sort((a, b) => a - b); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js index 9bd83f04..34a61124 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js @@ -16,8 +16,7 @@ function evaluateCalculation(calculation, scope){ const parseNode = calculation._parsedCalculation; const fn = calculation._parseLevel || 'reduce'; const calculationScope = {...calculation._localScope, ...scope}; - const result = parseNode[fn](calculationScope, context); - calculation.value = result; + calculation.value = parseNode[fn](calculationScope, context); calculation.errors = context.errors; } diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js index e9f670b1..b5261550 100644 --- a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js @@ -4,7 +4,7 @@ import computeByType from '/imports/api/creature/computation/newEngine/computeCo export default function computeCreatureComputation(computation){ const stack = []; - // dict of computed nodes by id + // Computation scope of {variableName: prop} const scope = {}; const graph = computation.dependencyGraph; // Add all nodes to the stack @@ -44,10 +44,7 @@ function compute(graph, node, scope){ function pushDependenciesToStack(nodeId, graph, stack){ graph.forEachLinkedNode( nodeId, - (linkedNode, link) => { - // Ignore inventory links, they are already fully computed when they are - // created - if (link.data === 'inventory' || link.data === 'classLevel') return; + (linkedNode) => { stack.push({ node: linkedNode, visited: false, diff --git a/app/imports/api/properties/ClassLevels.js b/app/imports/api/properties/ClassLevels.js index 37224f12..850f9851 100644 --- a/app/imports/api/properties/ClassLevels.js +++ b/app/imports/api/properties/ClassLevels.js @@ -9,7 +9,6 @@ let ClassLevelSchema = createPropertySchema({ optional: true, max: STORAGE_LIMITS.name, }, - // Only used by slot filling dialog, not computed description: { type: 'inlineCalculationFieldToCompute', optional: true, @@ -24,12 +23,7 @@ let ClassLevelSchema = createPropertySchema({ level: { type: SimpleSchema.Integer, defaultValue: 1, - }, - // Same as in SlotFillers.js - slotFillerCondition: { - type: String, - optional: true, - max: STORAGE_LIMITS.calculation, + max: STORAGE_LIMITS.levelMax, }, }); diff --git a/app/imports/api/properties/Classes.js b/app/imports/api/properties/Classes.js new file mode 100644 index 00000000..efa5ac17 --- /dev/null +++ b/app/imports/api/properties/Classes.js @@ -0,0 +1,91 @@ +import SimpleSchema from 'simpl-schema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; + +// Classes are like slots, except they only take class levels and enforce that +// lower levels are taken before higher levels +let ClassSchema = createPropertySchema({ + name: { + type: String, + optional: true, + max: STORAGE_LIMITS.name, + }, + description: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, + // Only `classLevel`s with the same variable name can fill the class + variableName: { + type: String, + optional: true, + max: STORAGE_LIMITS.variableName, + }, + classType: { + type: String, + allowedValues: ['startingClass', 'multiClass'], + defaultValue: 'startingClass', + }, + // Same tag format as Slots to match library classLevels against + slotTags: { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.tagCount, + }, + 'slotTags.$': { + type: String, + max: STORAGE_LIMITS.tagLength, + }, + extraTags: { + type: Array, + defaultValue: [], + 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, + }, + }); + +const ComputedOnlyClassSchema = createPropertySchema({ + description: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, + level: { + type: SimpleSchema.Integer, + optional: true, + }, + missingLevels: { + type: Array, + optional: true, + }, + 'missingLevels.$': { + type: SimpleSchema.Integer, + }, + }); + +const ComputedClassSchema = new SimpleSchema() + .extend(ClassSchema) + .extend(ComputedOnlyClassSchema); + +export { ClassSchema, ComputedOnlyClassSchema, ComputedClassSchema }; diff --git a/app/imports/api/properties/Slots.js b/app/imports/api/properties/Slots.js index 1d388969..8fb7c986 100644 --- a/app/imports/api/properties/Slots.js +++ b/app/imports/api/properties/Slots.js @@ -17,44 +17,44 @@ let SlotSchema = createPropertySchema({ optional: true, max: STORAGE_LIMITS.variableName, }, - slotTags: { + slotTags: { type: Array, - defaultValue: [], + defaultValue: [], maxCount: STORAGE_LIMITS.tagCount, }, - 'slotTags.$': { - type: String, + 'slotTags.$': { + type: String, max: STORAGE_LIMITS.tagLength, - }, - extraTags: { + }, + extraTags: { type: Array, - defaultValue: [], + defaultValue: [], maxCount: STORAGE_LIMITS.extraTagsCount, }, - 'extraTags.$': { - type: Object, - }, + 'extraTags.$': { + type: Object, + }, 'extraTags.$._id': { type: String, regEx: SimpleSchema.RegEx.Id, autoValue(){ if (!this.isSet) return Random.id(); } - }, + }, 'extraTags.$.operation': { - type: String, + type: String, allowedValues: ['OR', 'NOT'], defaultValue: 'OR', - }, + }, 'extraTags.$.tags': { - type: Array, + type: Array, defaultValue: [], maxCount: STORAGE_LIMITS.tagCount, - }, + }, 'extraTags.$.tags.$': { - type: String, + type: String, max: STORAGE_LIMITS.tagLength, - }, + }, quantityExpected: { type: 'fieldToCompute', optional: true, @@ -88,6 +88,10 @@ let SlotSchema = createPropertySchema({ const ComputedOnlySlotSchema = createPropertySchema({ // Computed fields + description: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, quantityExpected: { type: 'computedOnlyField', optional: true, @@ -109,7 +113,7 @@ const ComputedOnlySlotSchema = createPropertySchema({ }); const ComputedSlotSchema = new SimpleSchema() - .extend(ComputedOnlySlotSchema) - .extend(SlotSchema); + .extend(ComputedOnlySlotSchema) + .extend(SlotSchema); export { SlotSchema, ComputedSlotSchema, ComputedOnlySlotSchema }; diff --git a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js index 09ce6265..9d929cc2 100644 --- a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js +++ b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js @@ -4,6 +4,7 @@ import { ComputedOnlyAdjustmentSchema } from '/imports/api/properties/Adjustment import { ComputedOnlyAttackSchema } from '/imports/api/properties/Attacks.js'; import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js'; import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs.js'; +import { ComputedOnlyClassSchema } from '/imports/api/properties/Classes.js'; import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; import { ConstantSchema } from '/imports/api/properties/Constants.js'; import { ComputedOnlyContainerSchema } from '/imports/api/properties/Containers.js'; @@ -31,6 +32,7 @@ const propertySchemasIndex = { attack: ComputedOnlyAttackSchema, attribute: ComputedOnlyAttributeSchema, buff: ComputedOnlyBuffSchema, + characterClass: ComputedOnlyClassSchema, classLevel: ClassLevelSchema, constant: ConstantSchema, container: ComputedOnlyContainerSchema, diff --git a/app/imports/api/properties/computedPropertySchemasIndex.js b/app/imports/api/properties/computedPropertySchemasIndex.js index 02366c05..f30cd1c2 100644 --- a/app/imports/api/properties/computedPropertySchemasIndex.js +++ b/app/imports/api/properties/computedPropertySchemasIndex.js @@ -4,6 +4,7 @@ import { ComputedAdjustmentSchema } from '/imports/api/properties/Adjustments.js import { ComputedAttackSchema } from '/imports/api/properties/Attacks.js'; import { ComputedAttributeSchema } from '/imports/api/properties/Attributes.js'; import { ComputedBuffSchema } from '/imports/api/properties/Buffs.js'; +import { ComputedClassSchema } from '/imports/api/properties/Classes.js'; import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; import { ConstantSchema } from '/imports/api/properties/Constants.js'; import { ComputedContainerSchema } from '/imports/api/properties/Containers.js'; @@ -31,6 +32,7 @@ const propertySchemasIndex = { attack: ComputedAttackSchema, attribute: ComputedAttributeSchema, buff: ComputedBuffSchema, + characterClass: ComputedClassSchema, classLevel: ClassLevelSchema, constant: ConstantSchema, damage: ComputedDamageSchema, diff --git a/app/imports/api/properties/propertySchemasIndex.js b/app/imports/api/properties/propertySchemasIndex.js index 3ad0c4d9..00cc1644 100644 --- a/app/imports/api/properties/propertySchemasIndex.js +++ b/app/imports/api/properties/propertySchemasIndex.js @@ -4,6 +4,7 @@ import { AdjustmentSchema } from '/imports/api/properties/Adjustments.js'; import { AttackSchema } from '/imports/api/properties/Attacks.js'; import { AttributeSchema } from '/imports/api/properties/Attributes.js'; import { BuffSchema } from '/imports/api/properties/Buffs.js'; +import { ClassSchema } from '/imports/api/properties/Classes.js'; import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; import { ConstantSchema } from '/imports/api/properties/Constants.js'; import { DamageSchema } from '/imports/api/properties/Damages.js'; @@ -31,6 +32,7 @@ const propertySchemasIndex = { attack: AttackSchema, attribute: AttributeSchema, buff: BuffSchema, + characterClass: ClassSchema, classLevel: ClassLevelSchema, constant: ConstantSchema, damage: DamageSchema, diff --git a/app/imports/constants/PROPERTIES.js b/app/imports/constants/PROPERTIES.js index d1ee0bb7..38692a85 100644 --- a/app/imports/constants/PROPERTIES.js +++ b/app/imports/constants/PROPERTIES.js @@ -30,11 +30,17 @@ const PROPERTIES = Object.freeze({ helpText: 'When a buff is activated as a child of an action, it will copy the properties under itself onto a target character.', suggestedParents: ['action', 'attack', 'savingThrow', 'spell'], }, + characterClass: { + icon: 'mdi-card-account-details', + name: 'Class', + helpText: 'Your character should ideally have one starting class. Classes hold class levels', + suggestedParents: ['class'], + }, classLevel: { icon: '$vuetify.icons.class_level', name: 'Class level', helpText: 'Class levels represent a single level gained in a class', - suggestedParents: ['class'], + suggestedParents: [], }, constant: { icon: 'mdi-anchor', diff --git a/app/imports/constants/STORAGE_LIMITS.js b/app/imports/constants/STORAGE_LIMITS.js index 065089ca..d3e4251e 100644 --- a/app/imports/constants/STORAGE_LIMITS.js +++ b/app/imports/constants/STORAGE_LIMITS.js @@ -13,6 +13,9 @@ const STORAGE_LIMITS = Object.freeze({ url: 256, variableName: 64, + // Number limits + levelMax: 128, + //Array counts ancestorCount: 100, damageTypeCount: 32,