Began rebuilding computation engine to be dependency graph centric

This commit is contained in:
Stefan Zermatten
2021-09-08 17:23:00 +02:00
parent 43f056ae95
commit 06da15c44a
28 changed files with 451 additions and 296 deletions

View File

@@ -1,3 +1,4 @@
import '/imports/api/simpleSchemaConfig.js';
import '/imports/ui/vueSetup.js';
import '/imports/ui/styles/stylesIndex.js';
import '/imports/client/config.js';

View File

@@ -0,0 +1,46 @@
import { get } from 'lodash';
export function applyFnToKey(doc, key, fn){
if (key.includes('$.')){
applyToArrayKey(doc, key, fn);
} else {
applyToSingleKey(doc, key, fn);
}
}
function applyToSingleKey(doc, key, fn){
// call the function with the current value and document for context
fn(doc, key);
}
/**
* Applies the given function to all instances in a document key
* key.$.with.$.subdocs will apply to all key[i...n].with[j...m].subdocs
*/
function applyToArrayKey(doc, key, fn){
const keySplit = key.split('.$');
// Stack based depth first traversal of arrays
const stack = [{
array: get(doc, keySplit[0]),
paths: keySplit.slice(1),
currentPath: keySplit[0],
indices: [],
}];
while(stack.length){
const state = stack.pop();
for (let index in state.array.length){
const currentPath = `${state.currentPath}[${index}]${state.paths[0]}`
if (state.paths.length == 1){
applyToSingleKey(doc, currentPath, fn);
} else {
stack.push({
array: get(doc, currentPath),
paths: state.paths.slice(1),
currentPath,
indices: [...state.indices, index],
});
}
}
}
}

View File

@@ -0,0 +1,174 @@
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
import CreatureProperties,
{ DenormalisedOnlyCreaturePropertySchema as denormSchema }
from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
import applyFnToKey from '/imports/api/creature/computation/newEngine/applyFnToKey.js';
import { cloneDeep, unset } from 'lodash';
import { prettifyParseError, parse } from '/imports/parser/parser.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
import createGraph from 'ngraph.graph';
import findAncestorByType from 'imports/api/creature/computation/newEngine/findAncestorByType.js';
/**
* Store index of properties
* recompute static tree-based enabled/disabled status
* Build a dependency graph
* id -> id dependencies for docs that rely on other docs directly
* id -> variable deps for docs that rely on a variable's value
* TODO:
* variable -> id deps for variables that are impacted by docs
* Depth first traversal or dependency graph to:
* Find loops in the dependency graph
* resolve variables in dependency order
*/
export default function computeCreature(creatureId){
let properties = CreatureProperties.find({
'ancestors.id': creatureId,
'removed': {$ne: true},
}, {
sort: {order: 1}
});
const originalPropsById = {};
const propsById = {};
const propsByType = {};
// Process the properties one by one
properties.forEach(prop => {
// Store the prop by Id and Type
originalPropsById[prop._id] = cloneDeep(prop);
propsById[prop._id] = prop;
if (!propsByType[prop.type]) propsByType[prop.type] = [];
propsByType[prop.type].push(prop);
// Store the prop in the dependency graph
dependencyGraph.addNode(prop._id, prop);
// Remove all computed only fields
computedOnlySchemas[prop.type]._schemaKeys.forEach(key =>
applyFnToKey(prop, key, unset)
);
// Remove all denormalised fields
denormSchema._schemaKeys.forEach(key =>
applyFnToKey(prop, key, unset)
);
// Add a place to store all the computation details
prop._computationDetails = {
calculations: [],
toggleAncestors: [],
};
// parse every calculation field
computedSchemas[prop.type]._schemaKeys.forEach( key => {
if (key.slice(-11) !== 'calculation') return;
const calcKey = key.sclice(0, -11);
applyFnToKey(prop, calcKey, calcObj => {
// Store a reference to all the calculations
prop._computationDetails.calculations.push(calcObj);
// Parse the calculation
parseCalculation(calcObj);
return calcObj;
});
});
});
// Dependency graph where edge(a, b) means a depends on b
const dependencyGraph = createGraph();
// Build graph now that all props are stored
properties.forEach(prop => {
linkDependencies(dependencyGraph, prop, propsById);
});
// Process the properties in tree format
let creatureTree = nodeArrayToTree(properties);
walkDown(creatureTree, node => {
denormaliseInactiveStatus(node);
inheritToggleDependencies(node);
});
}
function walkDown(tree, callback){
let stack = [...tree];
while(stack.length){
let node = stack.pop();
callback(node);
stack.push(...node.children);
}
}
function denormaliseInactiveStatus(node){
const prop = node.node;
if (isActive(prop)) return;
prop.inactive = true;
prop.deactivatedBySelf = true;
// Mark children as inactive due to ancestor
walkDown(node.children, child => {
child.node.inactive = true;
child.node.deactivatedByAncestor = true;
});
}
function isActive(prop){
if (prop.disabled) return false;
switch (prop.type){
case 'buff': return !!prop.applied;
case 'item': return !!prop.equipped;
case 'spell': return !!prop.prepared || !!prop.alwaysPrepared;
default: return true;
}
}
function inheritToggleDependencies(node, dependencyGraph){
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;
walkDown(node.children, child => {
child.node._computationDetails.toggleAncestors.push(prop._id);
dependencyGraph.addLink(child.node._id, prop._id, prop.condition);
});
}
function parseCalculation(calcObj){
let calculation = calcObj.calculation || '';
try {
calcObj._parsedCalculation = parse(calculation);
} catch (e) {
let error = prettifyParseError(e);
calcObj.errors ?
calcObj.errors.push(error) :
calcObj.errors = [error];
calcObj._parsedCalculation = new ErrorNode({error});
}
}
function linkDependencies(dependencyGraph, prop, propsById){
let variableNames = [];
prop._computationDetails.calculations.forEach(calcObj => {
calcObj._parsedCalculation.travese(node => {
if (node instanceof SymbolNode || node instanceof AccessorNode){
if (node.name[0] !== '#'){
dependencyGraph.addLink(prop._id, node.name, calcObj);
} else {
let ancestorProp = findAncestorByType(
prop, node.name.slice(1), propsById
);
if (!ancestorProp) return;
dependencyGraph.addLink(prop._id, ancestorProp._id, calcObj);
}
}
});
});
return variableNames;
}

View File

@@ -0,0 +1,10 @@
export default function findAncestorByType(prop, type, propsById){
if (!prop || !prop.ancestors) return;
let ancestor;
for (let i = prop.ancestors.length - 1; i >= 0; i--){
ancestor = propsById[prop.ancestors[i].id];
if (ancestor && ancestor.type === type){
return ancestor;
}
}
}

View File

@@ -46,6 +46,9 @@ let CreaturePropertySchema = new SimpleSchema({
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
});
const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
// Denormalised flag if this property is inactive on the sheet for any reason
// Including being disabled, or a decendent of a disabled property
inactive: {
@@ -86,6 +89,8 @@ let CreaturePropertySchema = new SimpleSchema({
},
});
CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema);
for (let key in propertySchemasIndex){
let schema = new SimpleSchema({});
schema.extend(propertySchemasIndex[key]);
@@ -104,5 +109,6 @@ import '/imports/api/creature/actions/castSpellWithSlot.js';
export default CreatureProperties;
export {
DenormalisedOnlyCreaturePropertySchema,
CreaturePropertySchema,
};

View File

@@ -7,10 +7,8 @@ const AdjustmentSchema = createPropertySchema({
// This can be simplified, but should only compute when activated
amount: {
type: 'fieldToCompute',
parseLevel: 'compile',
optional: true,
},
'amount.calculation': {
type: String,
defaultValue: 1,
},
// Who this adjustment applies to
@@ -39,6 +37,7 @@ const AdjustmentSchema = createPropertySchema({
const ComputedOnlyAdjustmentSchema = createPropertySchema({
amount: {
type: 'computedOnlyField',
parseLevel: 'compile',
optional: true,
},
});

View File

@@ -10,10 +10,8 @@ let AttackSchema = new SimpleSchema()
// What gets added to the d20 roll
rollBonus: {
type: 'fieldToCompute',
parseLevel: 'compile',
optional: true,
},
'rollBonus.calculation': {
type: String,
defaultValue: 'strength.modifier + proficiencyBonus',
},
// Set better defaults for the action
@@ -38,6 +36,7 @@ const ComputedOnlyAttackSchema = new SimpleSchema()
.extend(createPropertySchema({
rollBonus: {
type: 'computedOnlyField',
parseLevel: 'compile',
optional: true,
},
}));

View File

@@ -57,7 +57,6 @@ let AttributeSchema = createPropertySchema({
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
max: STORAGE_LIMITS.description,
},
// The damage done to the attribute, always positive
damage: {
@@ -108,7 +107,7 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
type: SimpleSchema.Integer,
optional: true,
},
// The computed creature constitution modifier
// The computed creature constitution modifier for hit dice
constitutionMod: {
type: Number,
optional: true,

View File

@@ -1,22 +1,21 @@
import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let BuffSchema = new SimpleSchema({
name: {
let BuffSchema = createPropertySchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
description: {
type: String,
type: 'inlineCalculationFieldToCompute',
optional: true,
max: STORAGE_LIMITS.description,
},
// How many rounds this buff lasts
duration: {
type: String,
type: 'fieldToCompute',
optional: true,
max: STORAGE_LIMITS.name,
},
applied: {
type: Boolean,
@@ -34,13 +33,16 @@ let BuffSchema = new SimpleSchema({
},
});
let ComputedOnlyBuffSchema = new SimpleSchema({
descriptionCalculations: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.inlineCalculationCount,
},
'descriptionCalculations.$': InlineComputationSchema,
let ComputedOnlyBuffSchema = createPropertySchema({
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
max: STORAGE_LIMITS.description,
},
duration: {
type: 'computedOnlyField',
optional: true,
},
durationSpent: {
type: Number,
optional: true,

View File

@@ -1,19 +1,19 @@
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';
let ClassLevelSchema = new SimpleSchema({
let ClassLevelSchema = createPropertySchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// Only used by slot filling dialog, not computed
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.description,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
// The name of this class level's variable
variableName: {
type: String,
@@ -25,13 +25,6 @@ let ClassLevelSchema = new SimpleSchema({
type: SimpleSchema.Integer,
defaultValue: 1,
},
nextLevelTags: {
type: Array,
defaultValue: [],
},
'nextLevelTags.$': {
type: String,
},
// Same as in SlotFillers.js
slotFillerCondition: {
type: String,

View File

@@ -1,8 +1,8 @@
import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let ContainerSchema = new SimpleSchema({
let ContainerSchema = createPropertySchema({
name: {
type: String,
optional: true,
@@ -29,20 +29,16 @@ let ContainerSchema = new SimpleSchema({
optional: true,
},
description: {
type: String,
type: 'inlineCalculationFieldToCompute',
optional: true,
trim: false,
max: STORAGE_LIMITS.description,
},
});
const ComputedOnlyContainerSchema = new SimpleSchema({
descriptionCalculations: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.inlineCalculationCount,
},
'descriptionCalculations.$': InlineComputationSchema,
const ComputedOnlyContainerSchema = createPropertySchema({
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
// Weight of all the contents, zero if `contentsWeightless` is true
contentsWeight:{
type: Number,

View File

@@ -1,16 +1,14 @@
import SimpleSchema from 'simpl-schema';
import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
const DamageSchema = new SimpleSchema({
const DamageSchema = createPropertySchema({
// The roll that determines how much to damage the attribute
// This can be simplified, but only computed when applied
amount: {
type: String,
type: 'fieldToCompute',
optional: true,
defaultValue: '1d8 + strength.modifier',
max: STORAGE_LIMITS.calculation,
},
// Who this damage applies to
target: {
@@ -29,19 +27,11 @@ const DamageSchema = new SimpleSchema({
},
});
const ComputedOnlyDamageSchema = new SimpleSchema({
amountResult: {
type: SimpleSchema.oneOf(String, Number),
const ComputedOnlyDamageSchema = createPropertySchema({
amount: {
type: 'computedOnlyField',
optional: true,
},
amountErrors: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'amountErrors.$':{
type: ErrorSchema,
},
});
const ComputedDamageSchema = new SimpleSchema()

View File

@@ -1,12 +1,12 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
/*
* Effects are reason-value attached to skills and abilities
* that modify their final value or presentation in some way
*/
let EffectSchema = new SimpleSchema({
let EffectSchema = createPropertySchema({
name: {
type: String,
optional: true,
@@ -30,10 +30,9 @@ let EffectSchema = new SimpleSchema({
'rollBonus',
],
},
calculation: {
type: String,
amount: {
type: 'fieldToCompute',
optional: true,
max: STORAGE_LIMITS.calculation,
},
//which stats the effect is applied to
stats: {
@@ -47,20 +46,10 @@ let EffectSchema = new SimpleSchema({
},
});
const ComputedOnlyEffectSchema = new SimpleSchema({
// The computed result of the effect
result: {
type: SimpleSchema.oneOf(Number, String, Boolean),
optional: true,
},
// The errors encountered while computing the result
errors: {
type: Array,
const ComputedOnlyEffectSchema = createPropertySchema({
amount: {
type: 'computedOnlyField',
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'errors.$':{
type: ErrorSchema,
},
});

View File

@@ -1,40 +1,31 @@
import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let FeatureSchema = new SimpleSchema({
let FeatureSchema = createPropertySchema({
name: {
type: String,
max: STORAGE_LIMITS.name,
},
summary: {
type: String,
type: 'inlineCalculationFieldToCompute',
optional: true,
max: STORAGE_LIMITS.summary,
},
description: {
type: String,
type: 'inlineCalculationFieldToCompute',
optional: true,
max: STORAGE_LIMITS.description,
},
});
let ComputedOnlyFeatureSchema = new SimpleSchema({
summaryCalculations: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.inlineCalculationCount,
let ComputedOnlyFeatureSchema = createPropertySchema({
summary: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
'summaryCalculations.$': InlineComputationSchema,
descriptionCalculations: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.inlineCalculationCount,
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
'descriptionCalculations.$': InlineComputationSchema,
});
const ComputedFeatureSchema = new SimpleSchema()

View File

@@ -1,8 +1,8 @@
import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
const ItemSchema = new SimpleSchema({
const ItemSchema = createPropertySchema({
name: {
type: String,
optional: true,
@@ -14,10 +14,9 @@ const ItemSchema = new SimpleSchema({
optional: true,
max: STORAGE_LIMITS.name,
},
description: {
type: String,
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
max: STORAGE_LIMITS.description,
},
// Number currently held
quantity: {
@@ -58,13 +57,11 @@ const ItemSchema = new SimpleSchema({
},
});
let ComputedOnlyItemSchema = new SimpleSchema({
descriptionCalculations: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.inlineCalculationCount,
let ComputedOnlyItemSchema = createPropertySchema({
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
'descriptionCalculations.$': InlineComputationSchema,
});
const ComputedItemSchema = new SimpleSchema()

View File

@@ -1,41 +1,32 @@
import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let NoteSchema = new SimpleSchema({
let NoteSchema = createPropertySchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
summary: {
type: String,
summary: {
type: 'inlineCalculationFieldToCompute',
optional: true,
max: STORAGE_LIMITS.summary,
},
description: {
type: String,
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
max: STORAGE_LIMITS.description,
},
});
let ComputedOnlyNoteSchema = new SimpleSchema({
summaryCalculations: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.inlineCalculationCount,
let ComputedOnlyNoteSchema = createPropertySchema({
summary: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
'summaryCalculations.$': InlineComputationSchema,
descriptionCalculations: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.inlineCalculationCount,
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
'descriptionCalculations.$': InlineComputationSchema,
});
const ComputedNoteSchema = new SimpleSchema()

View File

@@ -1,7 +1,7 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
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';
/**
* Rolls are children to actions or other rolls, they are triggered with 0 or
@@ -21,7 +21,7 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
* If the roll fails to meet or exceed the target number, the adjustments and
* child rolls are applied
*/
let RollSchema = new SimpleSchema({
let RollSchema = createPropertySchema({
name: {
type: String,
defaultValue: 'New Roll',
@@ -37,25 +37,18 @@ let RollSchema = new SimpleSchema({
},
// The roll, can be simplified, but only computed in context
roll: {
type: String,
type: 'fieldToCompute',
parseLevel: 'compile',
optional: true,
max: STORAGE_LIMITS.calculation,
},
});
let ComputedOnlyRollSchema = new SimpleSchema({
rollResult: {
type: SimpleSchema.Integer,
let ComputedOnlyRollSchema = createPropertySchema({
roll: {
type: 'computedOnlyField',
parseLevel: 'compile',
optional: true,
},
rollErrors: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'rollErrors.$':{
type: ErrorSchema,
},
});
const ComputedRollSchema = new SimpleSchema()

View File

@@ -1,10 +1,10 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
// These are the rolls made when saves are called for
// For the saving throw bonus or proficiency, see ./Skills.js
let SavingThrowSchema = new SimpleSchema ({
let SavingThrowSchema = createPropertySchema({
name: {
type: String,
optional: true,
@@ -12,9 +12,8 @@ let SavingThrowSchema = new SimpleSchema ({
},
// The computed DC
dc: {
type: String,
type: 'fieldToCompute',
optional: true,
max: STORAGE_LIMITS.calculation,
},
// Who this saving throw applies to
target: {
@@ -34,19 +33,11 @@ let SavingThrowSchema = new SimpleSchema ({
},
});
const ComputedOnlySavingThrowSchema = new SimpleSchema({
dcResult: {
type: Number,
const ComputedOnlySavingThrowSchema = createPropertySchema({
dc: {
type: 'computedOnlyField',
optional: true,
},
dcErrors: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'dcErrors.$':{
type: ErrorSchema,
},
});
const ComputedSavingThrowSchema = new SimpleSchema()

View File

@@ -1,13 +1,13 @@
import SimpleSchema from 'simpl-schema';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
/*
* Skills are anything that results in a modifier to be added to a D20
* Skills have an ability score modifier that they use as their basis
*/
let SkillSchema = new SimpleSchema({
let SkillSchema = createPropertySchema({
name: {
type: String,
optional: true,
@@ -42,26 +42,24 @@ let SkillSchema = new SimpleSchema({
],
defaultValue: 'skill',
},
// The starting value, before effects
baseValueCalculation: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
// The base proficiency of this skill
baseProficiency: {
type: Number,
optional: true,
},
// The starting value, before effects
baseValue: {
type: 'fieldToCompute',
optional: true,
},
// Description of what the skill is used for
description: {
type: String,
type: 'inlineCalculationFieldToCompute',
optional: true,
max: STORAGE_LIMITS.description,
},
});
let ComputedOnlySkillSchema = new SimpleSchema({
let ComputedOnlySkillSchema = createPropertySchema({
// Computed value of skill to be added to skill rolls
value: {
type: Number,
@@ -69,17 +67,13 @@ let ComputedOnlySkillSchema = new SimpleSchema({
},
// The result of baseValueCalculation
baseValue: {
type: SimpleSchema.oneOf(Number, String, Boolean),
type: 'computedOnlyField',
optional: true,
},
baseValueErrors: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'baseValueErrors.$': {
type: ErrorSchema,
},
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
// Computed value added by the ability
abilityMod: {
type: SimpleSchema.Integer,

View File

@@ -1,17 +1,16 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let SlotSchema = new SimpleSchema({
let SlotSchema = createPropertySchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
description: {
type: String,
type: 'inlineCalculationFieldToCompute',
optional: true,
max: STORAGE_LIMITS.description,
},
slotType: {
type: String,
@@ -57,19 +56,17 @@ let SlotSchema = new SimpleSchema({
max: STORAGE_LIMITS.tagLength,
},
quantityExpected: {
type: String,
type: 'fieldToCompute',
optional: true,
defaultValue: '1',
max: STORAGE_LIMITS.calculation,
},
ignored: {
type: Boolean,
optional: true,
},
slotCondition: {
type: String,
type: 'fieldToCompute',
optional: true,
max: STORAGE_LIMITS.calculation,
},
hideWhenFull: {
type: Boolean,
@@ -89,33 +86,15 @@ let SlotSchema = new SimpleSchema({
},
});
const ComputedOnlySlotSchema = new SimpleSchema({
// Condition calculation results
slotConditionResult: {
type: SimpleSchema.oneOf(Number, String, Boolean),
optional: true,
},
slotConditionErrors: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'slotConditionErrors.$':{
type: ErrorSchema,
},
// Quantity Expected calculation results
quantityExpectedResult: {
type: SimpleSchema.Integer,
const ComputedOnlySlotSchema = createPropertySchema({
// Computed fields
quantityExpected: {
type: 'computedOnlyField',
optional: true,
},
quantityExpectedErrors: {
type: Array,
slotCondition: {
type: 'computedOnlyField',
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'quantityExpectedErrors.$':{
type: ErrorSchema,
},
// Denormalised fields

View File

@@ -1,87 +1,50 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let SpellListSchema = new SimpleSchema({
let SpellListSchema = createPropertySchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
description: {
type: String,
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
max: STORAGE_LIMITS.description,
},
// Calculation of how many spells in this list can be prepared
maxPrepared: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
type: 'fieldToCompute',
optional: true,
},
// Calculation of The attack roll bonus used by spell attacks in this list
attackRollBonus: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
type: 'fieldToCompute',
optional: true,
},
// Calculation of the save dc used by spells in this list
dc: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
type: 'fieldToCompute',
optional: true,
},
});
const ComputedOnlySpellListSchema = new SimpleSchema({
descriptionCalculations: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.inlineCalculationCount,
},
'descriptionCalculations.$': InlineComputationSchema,
// maxPrepared
maxPreparedResult: {
type: Number,
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
maxPreparedErrors: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'maxPreparedErrors.$':{
type: ErrorSchema,
},
// attackRollBonus
attackRollBonusResult: {
type: Number,
maxPrepared: {
type: 'fieldToCompute',
optional: true,
},
attackRollBonusErrors: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'attackRollBonusErrors.$':{
type: ErrorSchema,
},
// dc
dcResult: {
type: Number,
attackRollBonus: {
type: 'fieldToCompute',
optional: true,
},
dcErrors: {
type: Array,
dc: {
type: 'fieldToCompute',
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'dcErrors.$':{
type: ErrorSchema,
},
});

View File

@@ -1,8 +1,8 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
const ToggleSchema = new SimpleSchema({
const ToggleSchema = createPropertySchema({
name: {
type: String,
optional: true,
@@ -19,26 +19,15 @@ const ToggleSchema = new SimpleSchema({
// if neither disabled or enabled, the condition will be run to determine
// if the children of the toggle should be active
condition: {
type: String,
type: 'fieldToCompute',
optional: true,
max: STORAGE_LIMITS.calculation,
},
});
const ComputedOnlyToggleSchema = new SimpleSchema({
// The computed result of the effect
toggleResult: {
type: Boolean,
optional: true,
},
// The errors encountered while computing the result
errors: {
type: Array,
const ComputedOnlyToggleSchema = createPropertySchema({
condition: {
type: 'computedOnlyField',
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'errors.$': {
type: ErrorSchema,
},
});

View File

@@ -8,6 +8,9 @@ import {
} from '/imports/api/properties/subSchemas/computedField.js';
import SimpleSchema from 'simpl-schema';
// Search through the schema for keys whose type is 'fieldToCompute' etc.
// replace the type with Object and attach extend the schema with
// the required fields to make the computation work
export default function createPropertySchema(definition){
const computationFields = {
inlineCalculationFieldToCompute: [],
@@ -21,12 +24,20 @@ export default function createPropertySchema(definition){
const def = definition[key];
if (computedKeys.includes(def.type)){
computationFields[def.type].push(key);
applyDefaultCalculationValue(definition, key);
def.type = Object;
if (!def.optional){
console.warn(
`computed field: '${key}' of '${def.type}' is expected to be optional`
);
}
}
}
// Create a schema with the edited definition
const schema = new SimpleSchema(definition);
// Extend the schema with all the computation fields
computationFields.inlineCalculationFieldToCompute.forEach(key => {
schema.extend(inlineCalculationFieldToCompute(key))
});
@@ -41,3 +52,33 @@ export default function createPropertySchema(definition){
});
return schema
}
function applyDefaultCalculationValue(definition, key){
const def = definition[key];
if (
def.type === 'computedOnlyField' ||
def.type === 'computedOnlyInlineCalculationField'
){
// don't apply defaults to computed only fields
// because it would add the calculation field which should only appear
// on the fields to compute
return;
}
let defaultValue = def.defaultValue;
if (!defaultValue) return;
let calcField;
if (def.type === 'fieldToCompute'){
calcField = key + '.calculation'
} else {
calcField = key + '.text'
}
if (definition[calcField]){
definition[calcField].defaultValue = defaultValue;
} else {
definition[calcField] = {
type: String,
defaultValue,
};
}
delete def.defaultValue;
}

View File

@@ -0,0 +1,3 @@
import SimpleSchema from 'simpl-schema';
SimpleSchema.extendOptions(['parseLevel']);

View File

@@ -15,15 +15,19 @@ export default function transformFields(src, transformList, reversed = false){
} else {
transform = {...originalTransform};
}
if (transform.from?.includes('$.')){
transformArrayField(src, doc, transform, reversed);
} else {
transformSingleField(src, doc, transform);
}
transformField(src, doc, transform, reversed);
}
return doc;
}
export function transformField(src, doc, transform, reversed){
if (transform.from?.includes('$.')){
transformArrayField(src, doc, transform, reversed);
} else {
transformSingleField(src, doc, transform);
}
}
function transformSingleField(src, doc, {from, to, up}){
// Get the value in the `from` path and delete it
let value = undefined;

13
app/package-lock.json generated
View File

@@ -2408,6 +2408,19 @@
"randexp": "0.4.6"
}
},
"ngraph.events": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.1.tgz",
"integrity": "sha512-D4C+nXH/RFxioGXQdHu8ELDtC6EaCiNsZtih0IvyGN81OZSUby4jXoJ5+RNWasfsd0FnKxxpAROyUMzw64QNsw=="
},
"ngraph.graph": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-19.1.0.tgz",
"integrity": "sha512-9cws84qfPkrYa7BaBtT+KgZfLXrd6pNL9Gl5Do+MBO/0Hm6rOM7qK78MZaO1uEoIK6p2pgUs6lu29zn/6tP59w==",
"requires": {
"ngraph.events": "^1.2.1"
}
},
"node-addon-api": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz",

View File

@@ -35,6 +35,7 @@
"meteor-node-stubs": "^1.1.0",
"moo": "^0.5.1",
"nearley": "^2.19.1",
"ngraph.graph": "^19.1.0",
"qrcode": "^1.4.4",
"request": "^2.88.2",
"simpl-schema": "^1.12.0",

View File

@@ -1,3 +1,4 @@
import '/imports/api/simpleSchemaConfig.js';
import '/imports/server/config/accountsEmailConfig.js';
import '/imports/server/config/SimpleRestConfig.js';
import '/imports/server/config/simpleSchemaDebug.js';