diff --git a/app/imports/api/creature/creatures/CreatureVariables.js b/app/imports/api/creature/creatures/CreatureVariables.js new file mode 100644 index 00000000..27fe0af7 --- /dev/null +++ b/app/imports/api/creature/creatures/CreatureVariables.js @@ -0,0 +1,21 @@ +//set up the collection for creature variables +let CreatureVariables = new Mongo.Collection('creatureVariables'); + +// Unique index on _creatureId +if (Meteor.isServer) { + CreatureVariables._ensureIndex({ _creatureId: 1 }, { unique: true }) +} + +/** No schema because the structure isn't known until compute time + * Expect documents to looke like: + * { + * _id: "nE8Ngd6K4L4jSxLY2", + * _creatureId: "nE8Ngd6K4L4jSxLY2", // indexed reference to the creature + * explicitlyDefinedVariableName: {...some creatureProperty} + * implicitVariableName: {value: 10}, + * undefinedVariableName: {}, + * } + * Where top level fields that don't start with `_` are variables on the sheet +**/ + +export default CreatureVariables; diff --git a/app/imports/api/creature/log/CreatureLogs.js b/app/imports/api/creature/log/CreatureLogs.js index 1cab572f..1ba50c1d 100644 --- a/app/imports/api/creature/log/CreatureLogs.js +++ b/app/imports/api/creature/log/CreatureLogs.js @@ -1,5 +1,6 @@ import SimpleSchema from 'simpl-schema'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; @@ -154,7 +155,6 @@ const logRoll = new ValidatedMethod({ }).validator(), run({roll, creatureId}){ const creature = Creatures.findOne(creatureId, {fields: { - variables: 1, readers: 1, writers: 1, owner: 1, @@ -163,6 +163,7 @@ const logRoll = new ValidatedMethod({ avatarPicture: 1, }}); assertEditPermission(creature, this.userId); + const variables = CreatureVariables.findOne({ _creatureId: creatureId }); let logContent = [] let parsedResult = undefined; try { @@ -175,7 +176,7 @@ const logRoll = new ValidatedMethod({ let { result: compiled, context - } = resolve('compile', parsedResult, creature.variables); + } = resolve('compile', parsedResult, variables); const compiledString = toString(compiled); if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({ value: roll @@ -183,12 +184,12 @@ const logRoll = new ValidatedMethod({ logContent.push({ value: compiledString }); - let {result: rolled} = resolve('roll', compiled, creature.variables, context); + let {result: rolled} = resolve('roll', compiled, variables, context); let rolledString = toString(rolled); if (rolledString !== compiledString) logContent.push({ value: rolledString }); - let {result} = resolve('reduce', rolled, creature.variables, context); + let {result} = resolve('reduce', rolled, variables, context); let resultString = toString(result); if (resultString !== rolledString) logContent.push({ value: resultString diff --git a/app/imports/api/engine/actions/doAction.js b/app/imports/api/engine/actions/doAction.js index 7d75fdc5..c22deff2 100644 --- a/app/imports/api/engine/actions/doAction.js +++ b/app/imports/api/engine/actions/doAction.js @@ -3,6 +3,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; @@ -38,6 +39,12 @@ const doAction = new ValidatedMethod({ let action = CreatureProperties.findOne(actionId); // Check permissions let creature = getRootCreatureAncestor(action); + const variables = CreatureVariables.findOne({ + _creatureId: creature._id + }, { + fields: {_id: 0, _creatureId: 0}, + }); + creature.variables = variables; assertEditPermission(creature, this.userId); @@ -46,6 +53,12 @@ const doAction = new ValidatedMethod({ targetIds.forEach(targetId => { let target = Creatures.findOne(targetId); assertEditPermission(target, this.userId); + const variables = CreatureVariables.findOne({ + _creatureId: targetId + }, { + fields: {_id: 0, _creatureId: 0}, + }); + target.variables = variables; targets.push(target); }); diff --git a/app/imports/api/engine/computation/CreatureComputation.js b/app/imports/api/engine/computation/CreatureComputation.js index 278b3276..fb850129 100644 --- a/app/imports/api/engine/computation/CreatureComputation.js +++ b/app/imports/api/engine/computation/CreatureComputation.js @@ -2,7 +2,7 @@ import { EJSON } from 'meteor/ejson'; import createGraph from 'ngraph.graph'; export default class CreatureComputation { - constructor(properties, creature){ + constructor(properties, creature, variables){ // Set up fields this.originalPropsById = {}; this.propsById = {}; @@ -12,6 +12,7 @@ export default class CreatureComputation { this.dependencyGraph = createGraph(); this.errors = []; this.creature = creature; + this.variables = variables; // Store properties for easy access later properties.forEach(prop => { diff --git a/app/imports/api/engine/computation/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.js index 2fe3c4b2..83cadace 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.js @@ -4,6 +4,7 @@ import CreatureProperties, from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { loadedCreatures } from '../loadCreatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; import linkInventory from './buildComputation/linkInventory.js'; @@ -33,8 +34,9 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js'; export default function buildCreatureComputation(creatureId){ const creature = getCreature(creatureId); + const variables = getVariables(creatureId); const properties = getProperties(creatureId); - const computation = buildComputationFromProps(properties, creature); + const computation = buildComputationFromProps(properties, creature, variables); return computation; } @@ -45,7 +47,7 @@ function getProperties(creatureId) { const cloneProps = EJSON.clone(props); return cloneProps } - console.time(`Cache miss fetching from db: ${creatureId}`) + // console.time(`Cache miss on creature properties: ${creatureId}`) const props = CreatureProperties.find({ 'ancestors.id': creatureId, 'removed': {$ne: true}, @@ -53,29 +55,41 @@ function getProperties(creatureId) { sort: { order: 1 }, fields: { icon: 0 }, }).fetch(); - console.timeEnd(`Cache miss fetching from db: ${creatureId}`); + // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); return props; } function getCreature(creatureId) { if (loadedCreatures.has(creatureId)) { const loadedCreature = loadedCreatures.get(creatureId); - const creature = loadedCreature.creatures.get(creatureId); + const creature = loadedCreature.creature; if (creature) return creature; } - console.time(`Cache miss on Creature: ${creatureId}`); + // console.time(`Cache miss on Creature: ${creatureId}`); const creature = Creatures.findOne(creatureId, { denormalizedStats: 1, variables: 1, dirty: 1, }); - console.timeEnd(`Cache miss on Creature: ${creatureId}`); + // console.timeEnd(`Cache miss on Creature: ${creatureId}`); return creature; } -export function buildComputationFromProps(properties, creature){ +function getVariables(creatureId) { + if (loadedCreatures.has(creatureId)) { + const loadedCreature = loadedCreatures.get(creatureId); + const variables = loadedCreature.variables; + if (variables) return variables; + } + // console.time(`Cache miss on variables: ${creatureId}`); + const variables = CreatureVariables.findOne({_creatureId: creatureId}); + // console.timeEnd(`Cache miss on variables: ${creatureId}`); + return variables; +} - const computation = new CreatureComputation(properties, creature); +export function buildComputationFromProps(properties, creature, variables){ + + const computation = new CreatureComputation(properties, creature, variables); // 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 diff --git a/app/imports/api/engine/computation/writeComputation/writeScope.js b/app/imports/api/engine/computation/writeComputation/writeScope.js index 6ab6a1e0..04858b8f 100644 --- a/app/imports/api/engine/computation/writeComputation/writeScope.js +++ b/app/imports/api/engine/computation/writeComputation/writeScope.js @@ -1,15 +1,14 @@ +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; import { EJSON } from 'meteor/ejson'; import { omitBy } from 'lodash'; export default function writeScope(creatureId, computation) { const scope = computation.scope; - const variables = computation.creature?.variables || {}; - let $set, $unset; + const variables = computation.variables || {}; + delete variables._id; - if (computation.creature.dirty) { - $unset = { dirty: 1 }; - } + let $set, $unset; for (const key in scope){ // Remove large properties that aren't likely to be accessed @@ -26,12 +25,14 @@ export default function writeScope(creatureId, computation) { // Only update changed fields if (!EJSON.equals(variables[key], scope[key])) { if (!$set) $set = {}; - // Set the changed key in the creature variables + /* Log detailed diffs const diff = omitBy(variables[key], (v, k) => EJSON.equals(scope[key][k], v)); for (let subkey in diff) { console.log(`${key}.${subkey}: ${variables[key][subkey]} => ${scope[key][subkey]}`) } - $set[`variables.${key}`] = scope[key]; + */ + // Set the changed key in the creature variables + $set[key] = scope[key]; } } @@ -39,12 +40,17 @@ export default function writeScope(creatureId, computation) { for (const key in variables) { if (!scope[key]) { if (!$unset) $unset = {}; - $unset[`variables.${key}`] = 1; + $unset[key] = 1; } } if ($set || $unset) { - const updates = Creatures.update(creatureId, { $set, $unset }); - console.log('wrote scope: ', updates); + const update = {}; + if ($set) update.$set = $set; + if ($unset) update.$unset = $unset; + CreatureVariables.upsert({_creatureId: creatureId}, update); + } + if (computation.creature.dirty) { + Creatures.update({_creatureId: creatureId}, {$unset: { dirty: 1 }}); } } diff --git a/app/imports/api/engine/computeCreature.js b/app/imports/api/engine/computeCreature.js index c1de8c44..d0a3d469 100644 --- a/app/imports/api/engine/computeCreature.js +++ b/app/imports/api/engine/computeCreature.js @@ -30,6 +30,7 @@ function computeComputation(computation, creatureId) { logError.location = e.stack.split('\n')[1]; } console.error(logError); + throw e; } finally { writeErrors(creatureId, computation.errors); } diff --git a/app/imports/api/engine/loadCreatures.js b/app/imports/api/engine/loadCreatures.js index e6336680..d7a07154 100644 --- a/app/imports/api/engine/loadCreatures.js +++ b/app/imports/api/engine/loadCreatures.js @@ -1,6 +1,7 @@ import { debounce } from 'lodash'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import computeCreature from './computeCreature'; const COMPUTE_DEBOUNCE_TIME = 100; // ms @@ -68,7 +69,6 @@ class LoadedCreature { }, }); - self.creatures = new Map(); // Observe the creature itself self.creatureObserver = Creatures.find({ _id: creatureId, @@ -87,26 +87,62 @@ class LoadedCreature { }, }); + // Observe the creature's variables + self.variablesObserver = CreatureVariables.find({ + _creatureId: creatureId, + }, { + fields: { _creatureId: 0}, + }).observeChanges({ + added(id, fields) { + fields._id = id; + self.addVariables(fields) + }, + changed(id, fields) { + self.changeVariables(id, fields); + }, + removed(id) { + self.removeVariables(id); + }, + }); }); } stop() { - this.creatureObserver.stop(); this.propertyObserver.stop(); + this.creatureObserver.stop(); + this.variablesObserver.stop(); } addProperty(prop) { this.properties.set(prop._id, prop); } - addCreature(creature) { - this.creatures.set(creature._id, creature); - } changeProperty(id, fields) { - this.changeMap(id, fields, this.properties); + LoadedCreature.changeMap(id, fields, this.properties); + } + removeProperty(id) { + this.properties.delete(id) + } + addCreature(creature) { + this.creature = creature; } changeCreature(id, fields) { - this.changeMap(id, fields, this.creatures); + LoadedCreature.changeDoc(this.creature, fields); } - changeMap(id, fields, map) { + removeCreature() { + delete this.creature; + } + addVariables(variables) { + this.variables = variables; + } + changeVariables(id, fields) { + LoadedCreature.changeDoc(this.variables, fields); + } + removeVariables() { + delete this.variables; + } + static changeMap(id, fields, map) { const doc = map.get(id); + LoadedCreature.changeDoc(doc, fields); + } + static changeDoc(doc, fields) { if (!doc) return; for (let key in fields) { if (key === undefined) { @@ -116,10 +152,4 @@ class LoadedCreature { } } } - removeProperty(id) { - this.properties.delete(id) - } - removeCreature(id) { - this.creatures.delete(id) - } } diff --git a/app/imports/server/publications/singleCharacter.js b/app/imports/server/publications/singleCharacter.js index c2ca8c0c..28d2da6f 100644 --- a/app/imports/server/publications/singleCharacter.js +++ b/app/imports/server/publications/singleCharacter.js @@ -1,5 +1,6 @@ import SimpleSchema from 'simpl-schema'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; @@ -41,6 +42,9 @@ Meteor.publish('singleCharacter', function (creatureId) { Creatures.find({ _id: creatureId, }), + CreatureVariables.find({ + _creatureId: creatureId, + }), CreatureProperties.find({ 'ancestors.id': creatureId, }), diff --git a/app/imports/ui/creature/character/characterSheetTabs/BuildTab.vue b/app/imports/ui/creature/character/characterSheetTabs/BuildTab.vue index 2aad0870..d9ae0e03 100644 --- a/app/imports/ui/creature/character/characterSheetTabs/BuildTab.vue +++ b/app/imports/ui/creature/character/characterSheetTabs/BuildTab.vue @@ -27,33 +27,33 @@ > - Level {{ creature.variables.level.value }} + Level {{ variables.level.value }} - {{ creature.variables.milestoneLevels.value }} Milestone levels + {{ variables.milestoneLevels.value }} Milestone levels {{ - creature.variables.xp && - creature.variables.xp.value || + variables.xp && + variables.xp.value || 0 }} XP @@ -103,6 +103,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; import BuildTreeNodeList from '/imports/ui/creature/buildTree/BuildTreeNodeList.vue'; import SlotCardsToFill from '/imports/ui/creature/slots/SlotCardsToFill.vue'; +import CreatureVariables from '../../../../api/creature/creatures/CreatureVariables'; function traverse(tree, callback, parents = []){ tree.forEach(node => { @@ -152,6 +153,9 @@ export default { creature(){ return Creatures.findOne(this.creatureId); }, + variables() { + return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {}; + }, classLevels(){ return CreatureProperties.find({ 'ancestors.id': this.creatureId, @@ -217,8 +221,8 @@ export default { elementId: 'experience-add-button', data: { creatureIds: [this.creatureId], - startAsMilestone: this.creature.variables.milestoneLevels && - !!this.creature.variables.milestoneLevels.value, + startAsMilestone: this.variables.milestoneLevels && + !!this.variables.milestoneLevels.value, }, }); }, @@ -228,8 +232,8 @@ export default { elementId: 'experience-info-button', data: { creatureId: this.creatureId, - startAsMilestone: this.creature.variables.milestoneLevels && - !!this.creature.variables.milestoneLevels.value, + startAsMilestone: this.variables.milestoneLevels && + !!this.variables.milestoneLevels.value, }, }); }, diff --git a/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue b/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue index 6e3faac6..ad8ef5d7 100644 --- a/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue +++ b/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue @@ -31,12 +31,12 @@ - + $vuetify.icons.spell @@ -47,7 +47,7 @@ - {{ creature.variables.itemsAttuned.value }} + {{ variables.itemsAttuned.value }} @@ -104,6 +104,7 @@ import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/ import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js'; import CoinValue from '/imports/ui/components/CoinValue.vue'; import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js'; +import CreatureVariables from '../../../../api/creature/creatures/CreatureVariables'; export default { components: { @@ -138,6 +139,9 @@ export default { color: 1, variables: 1, }}); + }, + variables() { + return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {}; }, containersWithoutAncestorContainers(){ return CreatureProperties.find({ @@ -206,9 +210,9 @@ export default { }, weightCarried(){ return stripFloatingPointOddities( - this.creature.variables && - this.creature.variables.weightCarried && - this.creature.variables.weightCarried.value || 0 + this.variables && + this.variables.weightCarried && + this.variables.weightCarried.value || 0 ); }, }, diff --git a/app/imports/ui/creature/slots/SlotFillDialog.vue b/app/imports/ui/creature/slots/SlotFillDialog.vue index 497b0401..e8c19896 100644 --- a/app/imports/ui/creature/slots/SlotFillDialog.vue +++ b/app/imports/ui/creature/slots/SlotFillDialog.vue @@ -180,7 +180,7 @@