Moved creature variables to their own collection

Another big change to the engine, expect bugs
This commit is contained in:
Stefan Zermatten
2022-06-29 14:54:25 +02:00
parent 9dd84a83d2
commit f07f05ca2c
14 changed files with 174 additions and 70 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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);
});

View File

@@ -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 => {

View File

@@ -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

View File

@@ -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 }});
}
}

View File

@@ -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);
}

View File

@@ -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)
}
}

View File

@@ -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,
}),

View File

@@ -27,33 +27,33 @@
>
<v-card class="class-details mb-2">
<v-card-title
v-if="creature.variables.level"
v-if="variables.level"
class="text-h6"
>
Level {{ creature.variables.level.value }}
Level {{ variables.level.value }}
</v-card-title>
<v-list two-line>
<v-list-item>
<v-list-item-content>
<v-list-item-title
v-if="
creature.variables.milestoneLevels &&
creature.variables.milestoneLevels.value
variables.milestoneLevels &&
variables.milestoneLevels.value
"
>
{{ creature.variables.milestoneLevels.value }} Milestone levels
{{ variables.milestoneLevels.value }} Milestone levels
</v-list-item-title>
<v-list-item-title
v-if="
!(creature.variables.milestoneLevels &&
creature.variables.milestoneLevels.value) ||
(creature.variables.xp &&
creature.variables.xp.value)
!(variables.milestoneLevels &&
variables.milestoneLevels.value) ||
(variables.xp &&
variables.xp.value)
"
>
{{
creature.variables.xp &&
creature.variables.xp.value ||
variables.xp &&
variables.xp.value ||
0
}} XP
</v-list-item-title>
@@ -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,
},
});
},

View File

@@ -31,12 +31,12 @@
<v-list-item-action>
<v-list-item-title>
<coin-value
:value="creature.variables && creature.variables.valueTotal && creature.variables.valueTotal.value|| 0"
:value="variables && variables.valueTotal && variables.valueTotal.value|| 0"
/>
</v-list-item-title>
</v-list-item-action>
</v-list-item>
<v-list-item v-if="creature.variables && creature.variables.itemsAttuned && creature.variables.itemsAttuned.value">
<v-list-item v-if="variables && variables.itemsAttuned && variables.itemsAttuned.value">
<v-list-item-avatar>
<v-icon>$vuetify.icons.spell</v-icon>
</v-list-item-avatar>
@@ -47,7 +47,7 @@
</v-list-item-content>
<v-list-item-action>
<v-list-item-title>
{{ creature.variables.itemsAttuned.value }}
{{ variables.itemsAttuned.value }}
</v-list-item-title>
</v-list-item-action>
</v-list-item>
@@ -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
);
},
},

View File

@@ -180,7 +180,7 @@
</template>
<script lang="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 LibraryNodes from '/imports/api/library/LibraryNodes.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
@@ -297,9 +297,9 @@ export default {
return model;
}
},
creature(){
if (!this.creatureId) return {variables: {}};
return Creatures.findOne(this.creatureId);
variables() {
if (!this.creatureId) return {};
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
},
currentLimit(){
return this._subs['slotFillers'].data('limit') || 50;
@@ -363,7 +363,7 @@ export default {
if (node.slotFillerCondition){
try {
let parseNode = parse(node.slotFillerCondition);
const {result: resultNode} = resolve('reduce', parseNode, this.creature.variables);
const {result: resultNode} = resolve('reduce', parseNode, this.variables);
if (resultNode?.parseType === 'constant'){
if (!resultNode.value){
node._disabledBySlotFillerCondition = true;

View File

@@ -35,6 +35,7 @@
<script lang="js">
import CreatureLogs, { logRoll } from '/imports/api/creature/log/CreatureLogs.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { parse, prettifyParseError } from '/imports/parser/parser.js';
import resolve, { toString } from '/imports/parser/resolve.js';
@@ -73,7 +74,7 @@ export default {
return;
}
try {
let {result: compiled} = resolve('compile', result, this.creature.variables);
let {result: compiled} = resolve('compile', result, this.variables);
this.inputHint = toString(compiled);
return;
} catch (e){
@@ -107,6 +108,9 @@ export default {
creature(){
return Creatures.findOne(this.creatureId) || {};
},
variables(){
return CreatureVariables.findOne({_creatureId: this.creatureId}) || {};
},
editPermission(){
try {
assertEditPermission(this.creature, Meteor.userId());

View File

@@ -126,7 +126,7 @@ import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import AttributeEffect from '/imports/ui/properties/components/attributes/AttributeEffect.vue';
import SkillProficiency from '/imports/ui/properties/components/skills/SkillProficiency.vue';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
import getProficiencyIcon from '/imports/ui/utility/getProficiencyIcon.js';
export default {
@@ -185,6 +185,9 @@ export default {
},
},
meteor: {
variables(){
return CreatureVariables.findOne({_creatureId: this.context.creatureId}) || {};
},
baseEffects(){
if (this.context.creatureId){
let creatureId = this.context.creatureId;
@@ -280,10 +283,8 @@ export default {
proficiencyBonus(){
let creatureId = this.context.creatureId;
if (!creatureId) return;
let creature = Creatures.findOne(creatureId)
return creature &&
creature.variables.proficiencyBonus &&
creature.variables.proficiencyBonus.value;
return this.variables.proficiencyBonus &&
this.variables.proficiencyBonus.value;
},
},
}