diff --git a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js index 9558ff55..f3d82f75 100644 --- a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js +++ b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js @@ -18,23 +18,31 @@ export default function getSlotFillFilter({ slot, libraryIds }) { }] }); } else if (slot.type === 'class') { - filter.$and.push({ - $or: [{ - type: 'classLevel', - }, { - slotFillerType: 'classLevel', - }] - }); + const classLevelFilter = { + type: 'classLevel', + }; + const slotFillerFilter = { + slotFillerType: 'classLevel', + }; + + // Match variable name or tags if (slot.variableName) { - filter.variableName = slot.variableName; + classLevelFilter.variableName = slot.variableName; + slotFillerFilter.libraryTags = slot.variableName; } // Only search for levels the class needs if (slot.missingLevels && slot.missingLevels.length) { - filter.level = { $in: slot.missingLevels }; + classLevelFilter.level = { $in: slot.missingLevels }; + slotFillerFilter['cache.node.level'] = { $in: slot.missingLevels }; } else { - filter.level = { $gt: slot.level || 0 }; + classLevelFilter.level = { $gt: slot.level || 0 }; + slotFillerFilter['cache.node.level'] = { $gt: slot.level || 0 }; } + + filter.$and.push({ + $or: [classLevelFilter, slotFillerFilter] + }); } let tagsOr = []; let tagsNin = []; diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js index deb55bc7..469cd0fc 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js +++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js @@ -105,6 +105,8 @@ function insertPropertyFromNode(nodeId, ancestors, order) { // Convert all references into actual nodes nodes = reifyNodeReferences(nodes); + // Refetch the root node, it might have been reified + node = nodes[0] || node; // set libraryNodeIds storeLibraryNodeReferences(nodes); diff --git a/app/imports/api/creature/creatures/Creatures.js b/app/imports/api/creature/creatures/Creatures.js index 846b184b..8d8ef495 100644 --- a/app/imports/api/creature/creatures/Creatures.js +++ b/app/imports/api/creature/creatures/Creatures.js @@ -179,11 +179,6 @@ let CreatureSchema = new SimpleSchema({ blackbox: true, defaultValue: {} }, - variables: { - type: Object, - blackbox: true, - defaultValue: {} - }, computeErrors: { type: Array, optional: true, diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js b/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js index 0ada4ea1..6ee0a765 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js @@ -2,9 +2,8 @@ import operator from '/imports/parser/parseTree/operator.js'; import { parse } from '/imports/parser/parser.js'; import logErrors from './logErrors.js'; -export default function applyEffectsToCalculationParseNode(calcObj, actionContext){ - if (!calcObj.effects) return; - calcObj.effects.forEach(effect => { +export default function applyEffectsToCalculationParseNode(calcObj, actionContext) { + calcObj.effects?.forEach(effect => { if (effect.operation !== 'add') return; if (!effect.amount) return; if (effect.amount.value === null) return; @@ -17,8 +16,31 @@ export default function applyEffectsToCalculationParseNode(calcObj, actionContex operator: '+', fn: 'add' }); - } catch (e){ + } catch (e) { logErrors([e], actionContext) } }); + // Add the highest proficiency as well + let highestProficiency; + calcObj.proficiencies?.forEach(proficiency => { + if ( + proficiency.value > highestProficiency + || (highestProficiency === undefined && Number.isFinite(proficiency.value)) + ) { + highestProficiency = proficiency.value; + } + }); + if (highestProficiency) { + try { + let profParseNode = parse(highestProficiency.toString()); + calcObj.parseNode = operator.create({ + left: calcObj.parseNode, + right: profParseNode, + operator: '+', + fn: 'add' + }); + } catch (e) { + logErrors([e], actionContext) + } + } } diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js index 2f484567..16f2cfe3 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js @@ -2,7 +2,7 @@ import evaluateCalculation from '/imports/api/engine/computation/utility/evaluat import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; import logErrors from './logErrors.js'; -export default function recalculateCalculation(calc, actionContext, context){ +export default function recalculateCalculation(calc, actionContext, context) { if (!calc?.parseNode) return; calc._parseLevel = 'reduce'; applyEffectsToCalculationParseNode(calc, actionContext); diff --git a/app/imports/api/engine/computation/utility/evaluateCalculation.js b/app/imports/api/engine/computation/utility/evaluateCalculation.js index 580cafde..6d5f1f82 100644 --- a/app/imports/api/engine/computation/utility/evaluateCalculation.js +++ b/app/imports/api/engine/computation/utility/evaluateCalculation.js @@ -1,14 +1,14 @@ import resolve, { toString } from '/imports/parser/resolve.js'; -export default function evaluateCalculation(calculation, scope, givenContext){ +export default function evaluateCalculation(calculation, scope, givenContext) { const parseNode = calculation.parseNode; const fn = calculation._parseLevel; - const calculationScope = {...calculation._localScope, ...scope}; - const {result: resultNode, context} = resolve(fn, parseNode, calculationScope, givenContext); + const calculationScope = { ...calculation._localScope, ...scope }; + const { result: resultNode, context } = resolve(fn, parseNode, calculationScope, givenContext); calculation.errors = context.errors; - if (resultNode?.parseType === 'constant'){ + if (resultNode?.parseType === 'constant') { calculation.value = resultNode.value; - } else if (resultNode?.parseType === 'error'){ + } else if (resultNode?.parseType === 'error') { calculation.value = null; } else { calculation.value = toString(resultNode); diff --git a/app/imports/api/library/methods/duplicateLibraryNode.js b/app/imports/api/library/methods/duplicateLibraryNode.js index 83f20106..43aa0a8a 100644 --- a/app/imports/api/library/methods/duplicateLibraryNode.js +++ b/app/imports/api/library/methods/duplicateLibraryNode.js @@ -28,8 +28,8 @@ const duplicateLibraryNode = new ValidatedMethod({ }).validator(), mixins: [RateLimiterMixin], rateLimit: { - numRequests: 1, - timeInterval: 5000, + numRequests: 4, + timeInterval: 6000, }, run({ _id }) { let libraryNode = LibraryNodes.findOne(_id); diff --git a/app/imports/client/ui/components/ColumnLayout.vue b/app/imports/client/ui/components/ColumnLayout.vue index 2d5341a5..69306c67 100644 --- a/app/imports/client/ui/components/ColumnLayout.vue +++ b/app/imports/client/ui/components/ColumnLayout.vue @@ -16,42 +16,27 @@ export default { wideColumns: Boolean, }, }; - -/* -Removed to improve chrome layout performance, put it back if there are rendering errors -.column-layout>span>div { - display: table; - table-layout: fixed; -} -*/ diff --git a/app/imports/client/ui/components/snackbars/SnackbarQueue.vue b/app/imports/client/ui/components/snackbars/SnackbarQueue.vue index a17e085f..c746bbb4 100644 --- a/app/imports/client/ui/components/snackbars/SnackbarQueue.vue +++ b/app/imports/client/ui/components/snackbars/SnackbarQueue.vue @@ -52,7 +52,7 @@ export default { props: { timeout: { type: Number, - default: 6000000, + default: 15000, }, pause: { type: Number, diff --git a/app/imports/client/ui/creature/character/printedCharacterSheet/PrintedStats.vue b/app/imports/client/ui/creature/character/printedCharacterSheet/PrintedStats.vue index 039209e2..f6e2ecf3 100644 --- a/app/imports/client/ui/creature/character/printedCharacterSheet/PrintedStats.vue +++ b/app/imports/client/ui/creature/character/printedCharacterSheet/PrintedStats.vue @@ -596,7 +596,6 @@ export default { margin-top: 4px; margin-left: -30px; padding-left: 34px; - z-index: -1; } .number-label .number { diff --git a/app/imports/client/ui/creature/slots/LevelUpDialog.vue b/app/imports/client/ui/creature/slots/LevelUpDialog.vue index aefd4405..7b49f6f7 100644 --- a/app/imports/client/ui/creature/slots/LevelUpDialog.vue +++ b/app/imports/client/ui/creature/slots/LevelUpDialog.vue @@ -49,7 +49,7 @@ :key="libraryNode._id" :model="libraryNode" :data-id="libraryNode._id" - :class="{disabled: isDisabled(libraryNode)}" + :class="{disabled: isDisabled(libraryNode) || libraryNode._disabledBySlotFillerCondition}" > - + @@ -120,11 +121,12 @@ column align-center justify-center - class="ma-3" + class="ma-3 mt-8" > Load More @@ -192,7 +194,7 @@ import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/ import Libraries from '/imports/api/library/Libraries.js'; import LibraryNodeExpansionContent from '/imports/client/ui/library/LibraryNodeExpansionContent.vue'; import PropertyTags from '/imports/client/ui/properties/viewers/shared/PropertyTags.vue'; -import { clone, difference } from 'lodash'; +import { clone, difference, isEqual } from 'lodash'; export default { components: { @@ -247,29 +249,69 @@ export default { }); return { or, not }; }, + filledLevels() { + return LibraryNodes.find({ + _id: { $in: this.selectedNodeIds } + }).map( + node => node.level || node.cache?.node?.level || 0 + ).sort((a, b) => a - b); + } }, watch: { selectedNodeIds(selectedIds, oldSelectedIds) { - // Skip if we didn't increase the length by adding a new Id - if (oldSelectedIds.length >= selectedIds.length) return; - // Find out which library node was added - const addedId = difference(selectedIds, oldSelectedIds)[0]; - if (!addedId) return; - const addedNode = LibraryNodes.findOne(addedId); - if (!addedNode) return; - // Tick any unchecked nodes of a lower level, but only one per level - const backFilledLevels = new Set(); - this.libraryNodes.forEach(node => { - if ( - !selectedIds.includes(node._id) - && node.level < addedNode.level - && !backFilledLevels.has(node.level) - ) { - selectedIds.push(node._id); - } - }); - this.selectedNodeIds = selectedIds; - } + // Skip if we increased the length by adding a new Id, see if we need to backfill levels + if (oldSelectedIds.length < selectedIds.length) { + // Find out which library node was added + const addedId = difference(selectedIds, oldSelectedIds)[0]; + if (!addedId) return; + const addedNode = LibraryNodes.findOne(addedId); + if (!addedNode) return; + // Check which levels are already backfilled + const backFilledLevels = new Set(); + const sortedIds = LibraryNodes.find({ + _id: { $in: selectedIds } + }).map(node => backFilledLevels.add(node.level || node.cache?.node?.level || 0)); + // Tick any unchecked nodes of a lower level, but only one per level + this.libraryNodes.forEach(node => { + if ( + !selectedIds.includes(node._id) + && (node.level < addedNode.level) + && !backFilledLevels.has(node.level) + && !this.isDisabled(node) + && !node._disabledBySlotFillerCondition + ) { + selectedIds.push(node._id); + backFilledLevels.add(node.level) + } + }); + this.selectedNodeIds = sortedIds; + } + + // Refetch the library nodes to sort them correctly + const sortedIds = LibraryNodes.find({ + _id: { $in: selectedIds } + }, { + sort: { level: 1, name: 1, order: 1 } + }) + .fetch() + .sort((a, b) => (a.level || a.cache?.node?.level || 0) - (b.level || b.cache?.node?.level || 0)) + .map(node => node._id); + // Only update if the order changed + if (!isEqual(this.selectedNodeIds, sortedIds)) { + this.selectedNodeIds = sortedIds; + } + }, + activeCount(val) { + // Still loading fillers + if (!this._subs['classFillers'].ready()) return; + // Can load more, and not showing enough active choices, so load more + if ( + this.currentLimit < this.countAll + && val < 20 + ) { + this.loadMore(); + } + }, }, methods: { loadMore() { @@ -286,12 +328,10 @@ export default { }); }, isDisabled(node) { - return node._disabledBySlotFillerCondition || - node._disabledByAlreadyAdded || - ( - node._disabledByQuantityFilled && - !this.selectedNodeIds.includes(node._id) - ) + const selected = this.selectedNodeIds.includes(node._id); + return node._disabledByAlreadyAdded + || ( node._disabledByQuantityFilled && !selected ) + || ( this.filledLevels.includes(node.level || node.cache?.node?.level || 0) && !selected ) }, }, meteor: { @@ -324,6 +364,10 @@ export default { countAll() { return this._subs['classFillers'].data('countAll'); }, + activeCount() { + if (!this.libraryNodes) return; + return this.libraryNodes.length - (this.disabledNodeCount || 0); + }, alreadyAdded() { let added = new Set(); if (!this.model.unique) return added; @@ -377,12 +421,15 @@ export default { if (!this.libraryNodeFilter) return []; if (!this.$subReady.classFillers) return []; let nodes = LibraryNodes.find(this.libraryNodeFilter, { - sort: { name: 1, order: 1 } + sort: { level: 1, name: 1, order: 1 } }).fetch(); let disabledNodeCount = 0; // Mark classFillers whose condition isn't met or are too big to fit // the quantity to fill nodes.forEach(node => { + if (node.cache?.node) { + node.level = node.cache.node.level; + } if (node.slotFillerCondition) { try { let parseNode = parse(node.slotFillerCondition); @@ -390,18 +437,19 @@ export default { if (resultNode?.parseType === 'constant') { if (!resultNode.value) { node._disabledBySlotFillerCondition = true; + node._conditionError = node.slotFillerConditionNote || node.slotFillerCondition; disabledNodeCount += 1; } } else { node._disabledBySlotFillerCondition = true; - node._conditionError = toString(resultNode); + node._conditionError = node.slotFillerConditionNote || toString(resultNode); disabledNodeCount += 1; } } catch (e) { console.warn(e); let error = prettifyParseError(e); node._disabledBySlotFillerCondition = true; - node._conditionError = error; + node._conditionError = 'Condition error: ' + error; disabledNodeCount += 1; } } @@ -415,6 +463,7 @@ export default { node._disabledByAlreadyAdded = true; } }); + nodes.sort((a, b) => a.level - b.level); this.disabledNodeCount = disabledNodeCount; return nodes; }, diff --git a/app/imports/client/ui/creature/slots/SlotFillDialog.vue b/app/imports/client/ui/creature/slots/SlotFillDialog.vue index 75af7420..197aedd0 100644 --- a/app/imports/client/ui/creature/slots/SlotFillDialog.vue +++ b/app/imports/client/ui/creature/slots/SlotFillDialog.vue @@ -64,7 +64,7 @@ :key="libraryNode._id" :model="libraryNode" :data-id="libraryNode._id" - :class="{disabled: isDisabled(libraryNode)}" + :class="{disabled: isDisabled(libraryNode) || libraryNode._disabledBySlotFillerCondition}" > - + @@ -136,16 +137,38 @@ column align-center justify-center - class="ma-3" + class="ma-3 mt-8" > Load More + - +