Merge branch 'develop'

This commit is contained in:
Stefan Zermatten
2023-08-24 13:03:37 +02:00
38 changed files with 362 additions and 207 deletions

View File

@@ -3,7 +3,6 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
zegenie:redis-oplog
accounts-password@2.3.4
random@1.2.1
underscore@1.0.13

View File

@@ -124,5 +124,4 @@ underscore@1.0.13
url@1.3.2
webapp@1.13.5
webapp-hashing@1.1.1
zegenie:redis-oplog@2.0.16
zer0th:meteor-vuetify-loader@0.1.41

View File

@@ -132,6 +132,9 @@ function insertNodeFromProperty(propId, ancestors, order, method) {
prop.order = order;
}
// Clean the props
props = cleanProps(props);
// Insert the props as library nodes
LibraryNodes.batchInsert(props);
return prop;
@@ -186,4 +189,11 @@ function assertSourceLibraryCopyPermission(props, method) {
});
}
function cleanProps(props) {
return props.map(prop => {
let schema = LibraryNodes.simpleSchema(prop);
return schema.clean(prop);
});
}
export default copyPropertyToLibrary;

View File

@@ -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 = [];

View File

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

View File

@@ -19,6 +19,7 @@ const applyPropertyByType = {
damage,
folder,
note,
propertySlot: folder,
roll,
savingThrow,
spell: action,
@@ -26,6 +27,7 @@ const applyPropertyByType = {
};
export default function applyProperty(node, actionContext, ...rest) {
if (node.node.deactivatedByToggle) return;
actionContext.scope[`#${node.node.type}`] = node.node;
applyPropertyByType[node.node.type]?.(node, actionContext, ...rest);
}

View File

@@ -3,6 +3,7 @@ import recalculateCalculation from './shared/recalculateCalculation.js';
import rollDice from '/imports/parser/rollDice.js';
import applyProperty from '../applyProperty.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js';
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
@@ -187,11 +188,6 @@ function applyCrits(value, scope) {
return { criticalHit, criticalMiss };
}
function applyChildren(node, actionContext) {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
}
function spendResources(prop, actionContext) {
// Check Uses
if (prop.usesLeft <= 0) {
@@ -276,9 +272,10 @@ function spendResources(prop, actionContext) {
recalculateCalculation(attConsumed.quantity, actionContext);
if (!attConsumed.quantity?.value) return;
if (!attConsumed.variableName) return;
let stat = actionContext.scope[attConsumed.variableName];
if (!stat) {
spendLog.push(stat.name + ': ' + ' not found');
spendLog.push(attConsumed.variableName + ': ' + ' not found');
return;
}
damagePropertyWork({

View File

@@ -1,9 +1,9 @@
import applyProperty from '../applyProperty.js';
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyAdjustment(node, actionContext){
export default function applyAdjustment(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
const damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
@@ -39,7 +39,7 @@ export default function applyAdjustment(node, actionContext){
if (!prop.silent) actionContext.addLog({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`,
` ${value}`,
inline: true,
});
});
@@ -47,15 +47,10 @@ export default function applyAdjustment(node, actionContext){
if (!prop.silent) actionContext.addLog({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`,
` ${value}`,
inline: true,
});
}
return applyChildren(node, actionContext);
}
function applyChildren(node, actionContext){
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
}

View File

@@ -1,21 +1,18 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import rollDice from '/imports/parser/rollDice.js';
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyBranch(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const applyChildren = function () {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
};
const scope = actionContext.scope;
const targets = actionContext.targets;
const prop = node.node;
switch (prop.branchType) {
case 'if':
recalculateCalculation(prop.condition, actionContext);
if (prop.condition?.value) applyChildren();
if (prop.condition?.value) applyChildren(node, actionContext);
break;
case 'index':
if (node.children.length) {
@@ -32,30 +29,31 @@ export default function applyBranch(node, actionContext) {
if (index > node.children.length) index = node.children.length;
applyNodeTriggers(node, 'after', actionContext);
applyProperty(node.children[index - 1], actionContext);
applyNodeTriggers(node, 'afterChildren', actionContext);
}
break;
case 'hit':
if (scope['~attackHit']?.value) {
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On hit**' });
applyChildren();
applyChildren(node, actionContext);
}
break;
case 'miss':
if (scope['~attackMiss']?.value) {
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On miss**' });
applyChildren();
applyChildren(node, actionContext);
}
break;
case 'failedSave':
if (scope['~saveFailed']?.value) {
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On failed save**' });
applyChildren();
applyChildren(node, actionContext);
}
break;
case 'successfulSave':
if (scope['~saveSucceeded']?.value) {
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On save**', });
applyChildren();
applyChildren(node, actionContext);
}
break;
case 'random':
@@ -63,6 +61,7 @@ export default function applyBranch(node, actionContext) {
let index = rollDice(1, node.children.length)[0] - 1;
applyNodeTriggers(node, 'after', actionContext);
applyProperty(node.children[index], actionContext);
applyNodeTriggers(node, 'afterChildren', actionContext);
}
break;
case 'eachTarget':
@@ -71,9 +70,10 @@ export default function applyBranch(node, actionContext) {
applyNodeTriggers(node, 'after', actionContext);
actionContext.targets = [target]
node.children.forEach(child => applyProperty(child, actionContext));
applyNodeTriggers(node, 'afterChildren', actionContext);
});
} else {
applyChildren();
applyChildren(node, actionContext);
}
break;
}

View File

@@ -77,6 +77,7 @@ export default function applyBuff(node, actionContext) {
}
});
applyNodeTriggers(node, 'after', actionContext);
applyNodeTriggers(node, 'afterChildren', actionContext);
// Don't apply the children of the buff, they get copied to the target instead
}

View File

@@ -5,6 +5,7 @@ import { getProperyAncestors, getPropertiesOfType } from '/imports/api/engine/lo
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { softRemove } from '/imports/api/parenting/softRemove.js';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js';
export default function applyBuffRemover(node, actionContext) {
// Apply triggers
@@ -13,7 +14,7 @@ export default function applyBuffRemover(node, actionContext) {
const prop = node.node;
// Log Name
if (prop.name && !prop.silent){
if (prop.name && !prop.silent) {
actionContext.addLog({ name: prop.name });
}
@@ -53,11 +54,7 @@ export default function applyBuffRemover(node, actionContext) {
}
}
}
// Apply triggers
applyNodeTriggers(node, 'after', actionContext);
// Apply children
node.children.forEach(child => applyProperty(child, actionContext));
applyChildren(node, actionContext);
}
function removeBuff(buff, actionContext, prop) {

View File

@@ -1,5 +1,5 @@
import { some, intersection, difference, remove, includes } from 'lodash';
import applyProperty from '../applyProperty.js';
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import resolve, { Context, toString } from '/imports/parser/resolve.js';
import logErrors from './shared/logErrors.js';
@@ -13,10 +13,6 @@ import getEffectivePropTags from '/imports/api/engine/computation/utility/getEff
export default function applyDamage(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const applyChildren = function () {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
};
const prop = node.node;
const scope = actionContext.scope;
@@ -66,7 +62,7 @@ export default function applyDamage(node, actionContext) {
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)) {
return applyChildren();
return applyChildren(node, actionContext);
}
// Round the damage to a whole number
@@ -134,7 +130,7 @@ export default function applyDamage(node, actionContext) {
value: logValue.join('\n'),
inline: true,
});
return applyChildren();
return applyChildren(node, actionContext);
}
function applyDamageMultipliers({ target, damage, damageProp, logValue }) {

View File

@@ -1,11 +1,9 @@
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
import applyProperty from '../applyProperty.js';
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyFolder(node, actionContext) {
// Apply triggers
applyNodeTriggers(node, 'before', actionContext);
applyNodeTriggers(node, 'after', actionContext);
// Apply children
node.children.forEach(child => applyProperty(child, actionContext));
applyChildren(node, actionContext);
}

View File

@@ -1,27 +1,25 @@
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
import applyProperty from '../applyProperty.js';
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyNote(node, actionContext){
export default function applyNote(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
// Log Name, summary
let content = { name: prop.name };
if (prop.summary?.text){
if (prop.summary?.text) {
recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value;
}
if (content.name || content.value){
if (content.name || content.value) {
actionContext.addLog(content);
}
// Log description
if (prop.description?.text){
if (prop.description?.text) {
recalculateInlineCalculations(prop.description, actionContext);
actionContext.addLog({value: prop.description.value});
actionContext.addLog({ value: prop.description.value });
}
// Apply triggers
applyNodeTriggers(node, 'after', actionContext);
// Apply children
node.children.forEach(child => applyProperty(child, actionContext));
applyChildren(node, actionContext);
}

View File

@@ -1,4 +1,4 @@
import applyProperty from '../applyProperty.js';
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js';
import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import resolve, { toString } from '/imports/parser/resolve.js';
@@ -8,11 +8,6 @@ export default function applyRoll(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
const applyChildren = function () {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
};
if (prop.roll?.calculation) {
const logValue = [];
@@ -42,7 +37,7 @@ export default function applyRoll(node, actionContext) {
// If we didn't end up with a constant or a number of finite value, give up
if (reduced?.parseType !== 'constant' || (reduced.valueType === 'number' && !isFinite(reduced.value))) {
return applyChildren();
return applyChildren(node, actionContext);
}
const value = reduced.value;
@@ -57,5 +52,5 @@ export default function applyRoll(node, actionContext) {
});
}
}
return applyChildren();
return applyChildren(node, actionContext);
}

View File

@@ -2,6 +2,7 @@ import rollDice from '/imports/parser/rollDice.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import applyProperty from '../applyProperty.js';
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck.js';
@@ -34,8 +35,7 @@ export default function applySavingThrow(node, actionContext) {
if (!saveTargets?.length) {
scope['~saveFailed'] = { value: true };
scope['~saveSucceeded'] = { value: true };
applyNodeTriggers(node, 'after', actionContext);
return node.children.forEach(child => applyProperty(child, actionContext));
return applyChildren(node, actionContext);
}
// Each target makes the saving throw
@@ -45,10 +45,9 @@ export default function applySavingThrow(node, actionContext) {
delete scope['~saveDiceRoll'];
delete scope['~saveRoll'];
const applyChildren = function () {
actionContext.targets = [target]
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
const applyChildrenToTarget = function () {
actionContext.targets = [target];
return applyChildren(node, actionContext);
};
const save = target.variables[prop.stat];
@@ -58,7 +57,7 @@ export default function applySavingThrow(node, actionContext) {
name: 'Saving throw error',
value: 'No saving throw found: ' + prop.stat,
});
return applyChildren();
return applyChildrenToTarget();
}
let rollModifierText = numberToSignedString(save.value, true);
@@ -105,7 +104,7 @@ export default function applySavingThrow(node, actionContext) {
value: resultPrefix + '\n**' + result + '**',
inline: true,
});
return applyChildren();
return applyChildrenToTarget();
});
// reset the targets after the save to each child
actionContext.targets = originalTargets;

View File

@@ -1,13 +1,12 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js';
export default function applyToggle(node, actionContext){
export default function applyToggle(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
recalculateCalculation(prop.condition, actionContext);
if (prop.condition?.value) {
applyNodeTriggers(node, 'after', actionContext);
return node.children.forEach(child => applyProperty(child, actionContext));
return applyChildren(node, actionContext);
}
}

View File

@@ -0,0 +1,8 @@
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import applyProperty from '/imports/api/engine/actions/applyProperty.js';
export default function applyChildren(node, actionContext) {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
applyNodeTriggers(node, 'afterChildren', actionContext);
}

View File

@@ -2,22 +2,22 @@
* 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){
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){
while (stack.length) {
const top = stack[stack.length - 1];
const prop = top.node;
if (prop._computationDetails.inventoryChildrenVisited){
if (prop._computationDetails.inventoryChildrenVisited) {
if (prop.type === 'container') containerStack.pop();
stack.pop();
handleProp(prop, containerStack, dependencyGraph);
} else {
// Add all containers to the stack when we first visit them
if (prop.type === 'container'){
if (prop.type === 'container') {
containerStack.push(top.node);
}
// Push children onto the stack and mark this as children are visited
@@ -27,18 +27,18 @@ export default function linkInventory(forest, dependencyGraph){
}
}
function handleProp(prop, containerStack, dependencyGraph){
function handleProp(prop, containerStack, dependencyGraph) {
// Skip props that aren't part of the inventory
if (prop.type !== 'item' && prop.type !== 'container') return;
// 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){
if (prop.type === 'item') {
if (prop.attuned) {
dependencyGraph.addLink('itemsAttuned', prop._id, 'attunedItem');
}
if (prop.equipped){
if (prop.equipped) {
dependencyGraph.addLink('weightEquipment', prop._id, 'equippedItem');
dependencyGraph.addLink('valueEquipment', prop._id, 'equippedItem');
}
@@ -47,14 +47,14 @@ function handleProp(prop, containerStack, dependencyGraph){
// Get the parent container
const container = containerStack[containerStack.length - 1];
if (container){
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){
if (carried) {
dependencyGraph.addLink('weightCarried', prop._id, 'inventoryStats');
dependencyGraph.addLink('valueCarried', prop._id, 'inventoryStats');
}

View File

@@ -16,7 +16,7 @@ export default function computeAction(computation, node) {
});
prop.resources.attributesConsumed.forEach(attConsumed => {
if (!attConsumed.variableName) return;
if (attConsumed.available < attConsumed.quantity?.value) {
if (!(attConsumed.available >= attConsumed.quantity?.value)) {
prop.insufficientResources = true;
}
});

View File

@@ -1,11 +1,17 @@
import aggregate from './computeVariable/aggregate/index.js';
import { safeStrip } from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
export default function computeContainer(computation, node){
export default function computeContainer(computation, node) {
if (!node.data) node.data = {};
aggregateLinks(computation, node);
// Clean up floating points
const prop = node.data;
prop.contentsWeight = safeStrip(prop.contentsWeight);
prop.carriedWeight = safeStrip(prop.carriedWeight);
}
function aggregateLinks(computation, node){
function aggregateLinks(computation, node) {
computation.dependencyGraph.forEachLinkedNode(
node.id,
(linkedNode, link) => {
@@ -13,7 +19,7 @@ function aggregateLinks(computation, node){
// Ignore inactive props
if (linkedNode.data.inactive) return;
// Aggregate inventory links
aggregate.inventory({node, linkedNode, link});
aggregate.inventory({ node, linkedNode, link });
},
true // enumerate only outbound links
);

View File

@@ -1,25 +1,25 @@
export default function aggregateInventory({node, linkedNode, link}){
export default function aggregateInventory({ node, linkedNode, link }) {
let linkedProp = linkedNode.data || {};
const prop = node.data;
switch (link.data){
switch (link.data) {
case 'attunedItem':
prop.baseValue = (prop.baseValue || 0) + 1;
return;
case 'equippedItem':
if (node.id === 'weightEquipment'){
if (node.id === 'weightEquipment') {
prop.baseValue = (prop.baseValue || 0) + weight(linkedProp);
} else if (node.id === 'valueEquipment'){
} 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){
if (!prop.weightless) {
prop.contentsWeight = (prop.contentsWeight || 0) + weight(linkedProp);
if (prop.carried){
if (prop.carried && !prop.contentsWeightless) {
prop.carriedWeight = (prop.carriedWeight || 0) + carriedWeight(linkedProp);
}
}
@@ -30,39 +30,39 @@ export default function aggregateInventory({node, linkedNode, link}){
return;
case 'inventoryStats':
if (node.id === 'weightTotal'){
if (node.id === 'weightTotal') {
prop.baseValue = (prop.baseValue || 0) + weight(linkedProp);
} else if (node.id === 'valueTotal'){
} else if (node.id === 'valueTotal') {
prop.baseValue = (prop.baseValue || 0) + value(linkedProp);
} else if (node.id === 'weightCarried'){
} else if (node.id === 'weightCarried') {
prop.baseValue = (prop.baseValue || 0) + carriedWeight(linkedProp);
} else if (node.id === 'valueCarried'){
} else if (node.id === 'valueCarried') {
prop.baseValue = (prop.baseValue || 0) + carriedValue(linkedProp);
}
return;
}
}
function quantity(prop){
if (typeof prop.quantity === 'number'){
function quantity(prop) {
if (typeof prop.quantity === 'number') {
return prop.quantity;
} else {
return 1;
}
}
function weight(prop){
function weight(prop) {
return (prop.weight || 0) * quantity(prop) + (prop.contentsWeight || 0);
}
function carriedWeight(prop){
function carriedWeight(prop) {
return (prop.weight || 0) * quantity(prop) + (prop.carriedWeight || 0);
}
function value (prop){
function value(prop) {
return (prop.value || 0) * quantity(prop) + (prop.contentsValue || 0);
}
function carriedValue (prop){
function carriedValue(prop) {
return (prop.value || 0) * quantity(prop) + (prop.carriedValue || 0);
}

View File

@@ -5,8 +5,26 @@ export default function evaluateToggles(computation, node) {
if (!toggles) return;
toggles.forEach(toggle => {
if (
(!toggle.enabled && !toggle.disabled && toggle.condition && !toggle.condition.value)
|| (toggle.disabled)
(
// Toggle isn't set to constantly enabled or disabled
!toggle.enabled &&
!toggle.disabled &&
// Toggle is not disabled by another toggle targeting it
// Ancestor toggles would've handled this child anyway,
// and tag targeted toggles break the link
!toggle.deactivatedByToggle &&
!toggle.deactivatedByAncestor &&
// Toggle has a condition with a falsy value
toggle.condition &&
!toggle.condition.value
)
|| (
// Toggle is disabled manually
toggle.disabled &&
// Toggle isn't deactivated by something else
!toggle.deactivatedByToggle &&
!toggle.deactivatedByAncestor
)
) {
prop.inactive = true;
prop.deactivatedByToggle = true;

View File

@@ -1,3 +1,8 @@
export default function stripFloatingPointOddities(num, precision = 12){
export default function stripFloatingPointOddities(num, precision = 12) {
return +parseFloat(num.toPrecision(precision))
}
export function safeStrip(num, precision = 12) {
if (!Number.isFinite(num)) return num;
return stripFloatingPointOddities(num, precision);
}

View File

@@ -18,6 +18,7 @@ const eventOptions = {
const timingOptions = {
before: 'Before',
after: 'After',
afterChildren: 'After Children',
}
const actionPropertyTypeOptions = {
@@ -91,7 +92,7 @@ let TriggerSchema = createPropertySchema({
'extraTags.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
autoValue() {
if (!this.isSet) return Random.id();
}
},

View File

@@ -18,7 +18,7 @@
{{ label }}
<v-icon
:right="!!label"
:color="value"
:color="noColorChange ? undefined : value"
>
mdi-format-paint
</v-icon>
@@ -148,6 +148,7 @@
type: Number,
default: undefined,
},
noColorChange: Boolean,
},
data(){ return {
colors: [

View File

@@ -10,6 +10,7 @@
<v-spacer />
<color-picker
:value="model.color"
no-color-change
@input="value => change({path: ['color'], value})"
/>
</template>

View File

@@ -49,7 +49,7 @@
:key="libraryNode._id"
:model="libraryNode"
:data-id="libraryNode._id"
:class="{disabled: isDisabled(libraryNode)}"
:class="{disabled: isDisabled(libraryNode) || libraryNode._disabledBySlotFillerCondition}"
>
<v-expansion-panel-header>
<template #default="{ open }">
@@ -69,6 +69,7 @@
v-model="selectedNodeIds"
class="my-0 py-0"
hide-details
:color="libraryNode._disabledBySlotFillerCondition ? 'error' : ''"
:disabled="isDisabled(libraryNode)"
:value="libraryNode._id"
@click.stop
@@ -110,7 +111,7 @@
</template>
</v-expansion-panel-header>
<v-expansion-panel-content>
<library-node-expansion-content :model="libraryNode" />
<library-node-expansion-content :id="libraryNode._id" />
</v-expansion-panel-content>
</v-expansion-panel>
</template>
@@ -120,11 +121,12 @@
column
align-center
justify-center
class="ma-3"
class="ma-3 mt-8"
>
<v-btn
:loading="!$subReady.classFillers"
color="accent"
outlined
@click="loadMore"
>
Load More
@@ -247,6 +249,13 @@ 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) {
@@ -257,14 +266,19 @@ export default {
if (!addedId) return;
const addedNode = LibraryNodes.findOne(addedId);
if (!addedNode) return;
// Tick any unchecked nodes of a lower level, but only one per level
// 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
&& (node.level < addedNode.level)
&& !backFilledLevels.has(node.level)
&& !this.isDisabled(node)
&& !node._disabledBySlotFillerCondition
) {
selectedIds.push(node._id);
backFilledLevels.add(node.level)
@@ -278,12 +292,26 @@ export default {
_id: { $in: selectedIds }
}, {
sort: { level: 1, name: 1, order: 1 }
}).map(node => node._id);
})
.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() {
@@ -300,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: {
@@ -338,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;
@@ -397,6 +427,9 @@ export default {
// 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);
@@ -430,6 +463,7 @@ export default {
node._disabledByAlreadyAdded = true;
}
});
nodes.sort((a, b) => a.level - b.level);
this.disabledNodeCount = disabledNodeCount;
return nodes;
},

View File

@@ -1,39 +1,35 @@
<template>
<column-layout
wide-columns
class="slots-to-fill"
<v-fade-transition
group
leave-absolute
hide-on-leave
class="column-layout wide-columns"
>
<v-fade-transition
group
leave-absolute
hide-on-leave
<div
v-for="pointBuy in pointBuys"
:key="pointBuy._id"
style="transition: all 0.3s !important"
>
<div
v-for="pointBuy in pointBuys"
:key="pointBuy._id"
style="transition: all 0.3s !important"
>
<point-buy-card
:model="pointBuy"
hover
@ignore="ignoreProp(pointBuy._id)"
@click="editPointBuy(pointBuy._id)"
/>
</div>
<div
v-for="slot in slots"
:key="slot._id"
style="transition: all 0.3s !important"
>
<slot-card
:model="slot"
hover
@ignore="ignoreProp(slot._id)"
@click="fillSlot(slot._id)"
/>
</div>
</v-fade-transition>
</column-layout>
<point-buy-card
:model="pointBuy"
hover
@ignore="ignoreProp(pointBuy._id)"
@click="editPointBuy(pointBuy._id)"
/>
</div>
<div
v-for="slot in slots"
:key="slot._id"
style="transition: all 0.3s !important"
>
<slot-card
:model="slot"
hover
@ignore="ignoreProp(slot._id)"
@click="fillSlot(slot._id)"
/>
</div>
</v-fade-transition>
</template>
<script lang="js">

View File

@@ -58,13 +58,13 @@
multiple
hover
>
<template v-for="libraryNode in libraryNodes">
<template v-for="libraryNode in [...selectedExcludedNodes, ...libraryNodes]">
<v-expansion-panel
v-if="showDisabled || !libraryNode._disabledBySlotFillerCondition"
:key="libraryNode._id"
:model="libraryNode"
:data-id="libraryNode._id"
:class="{disabled: isDisabled(libraryNode)}"
:class="{disabled: isDisabled(libraryNode) || libraryNode._disabledBySlotFillerCondition}"
>
<v-expansion-panel-header>
<template #default="{ open }">
@@ -84,6 +84,7 @@
v-model="selectedNodeIds"
class="my-0 py-0"
hide-details
:color="libraryNode._disabledBySlotFillerCondition ? 'error' : ''"
:disabled="isDisabled(libraryNode)"
:value="libraryNode._id"
@click.stop
@@ -125,7 +126,7 @@
</template>
</v-expansion-panel-header>
<v-expansion-panel-content>
<library-node-expansion-content :model="libraryNode" />
<library-node-expansion-content :id="libraryNode._id" />
</v-expansion-panel-content>
</v-expansion-panel>
</template>
@@ -136,16 +137,38 @@
column
align-center
justify-center
class="ma-3"
class="ma-3 mt-8"
>
<v-btn
:loading="!$subReady.slotFillers"
color="accent"
outlined
@click="loadMore"
>
Load More
</v-btn>
</v-layout>
<template v-if="!showDisabled && disabledNodeCount">
<v-layout
column
align-center
justify-center
class="ma-3 mt-8"
>
<div>
Requirements of {{ disabledNodeCount }} properties were not met
</div>
<v-btn
class="mt-2"
elevation="0"
color="accent"
outlined
@click="showDisabled = true"
>
Show All
</v-btn>
</v-layout>
</template>
<v-layout
align-center
justify-center
@@ -162,6 +185,7 @@
<v-btn
v-if="!dummySlot"
text
small
data-id="library-browser-button"
:disabled="!model"
@click="openLibraryBrowser"
@@ -171,7 +195,7 @@
<v-btn
v-if="!dummySlot"
text
color="accent"
small
:disabled="!model"
data-id="custom-button"
@click="insertCustomFiller"
@@ -179,26 +203,7 @@
Create custom filler
</v-btn>
</v-layout>
<template v-if="!showDisabled && disabledNodeCount">
<v-layout
column
align-center
justify-center
class="ma-3"
>
<div>
Requirements of {{ disabledNodeCount }} properties were not met
</div>
<v-btn
class="mt-2"
elevation="0"
color="accent"
@click="showDisabled = true"
>
Show All
</v-btn>
</v-layout>
</template>
<template slot="actions">
<v-btn
text
@@ -240,7 +245,7 @@ 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 { getPropertyName } from '/imports/constants/PROPERTIES.js';
import { clone } from 'lodash';
import { clone, difference } from 'lodash';
import getDefaultSlotFiller from '/imports/api/library/methods/getDefaultSlotFiller.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
@@ -306,6 +311,19 @@ export default {
return propName;
},
},
watch: {
activeCount(val) {
// Still loading fillers
if (!this._subs['slotFillers'].ready()) return;
// Can load more, and not showing enough active choices, so load more
if (
this.currentLimit < this.countAll
&& val < 25
) {
this.loadMore();
}
},
},
methods: {
loadMore() {
if (this.currentLimit >= this.countAll) return;
@@ -327,8 +345,7 @@ export default {
});
},
isDisabled(node) {
return node._disabledBySlotFillerCondition ||
node._disabledByAlreadyAdded ||
return node._disabledByAlreadyAdded ||
(
node._disabledByQuantityFilled &&
!this.selectedNodeIds.includes(node._id)
@@ -383,6 +400,9 @@ export default {
'slotFillers'() {
return [this.slotId || this.dummySlot?._id, this.searchValue || undefined, !!this.dummySlot]
},
'selectedFillers'() {
return [this.slotId || this.dummySlot?._id, this.selectedNodeIds, !!this.dummySlot]
},
},
searchLoading() {
return !!this.searchValue && !this.$subReady.slotFillers;
@@ -408,6 +428,10 @@ export default {
countAll() {
return this._subs['slotFillers'].data('countAll');
},
activeCount() {
if (!this.libraryNodes) return;
return this.libraryNodes.length - (this.disabledNodeCount || 0);
},
libraryNodeFilter() {
const filterString = this._subs['slotFillers'].data('libraryNodeFilter');
if (!filterString) return;
@@ -520,6 +544,11 @@ export default {
this.disabledNodeCount = disabledNodeCount;
return nodes;
},
selectedExcludedNodes() {
const displayedIds = this.libraryNodes.map(node => node._id);
const excludedNodeIds = difference(this.selectedNodeIds, displayedIds);
return LibraryNodes.find({ _id: { $in: excludedNodeIds } });
}
}
}
</script>

View File

@@ -1,5 +1,5 @@
<template lang="html">
<div>
<div :key="id">
<v-progress-linear
v-if="!subsReady"
indeterminate
@@ -37,8 +37,8 @@ export default {
...propertyViewerIndex,
},
props: {
model: {
type: Object,
id: {
type: String,
required: true,
},
},
@@ -61,16 +61,19 @@ export default {
meteor: {
$subscribe: {
libraryNode(){
return [this.model._id];
return [this.id];
},
descendantLibraryNodes(){
return [this.model._id];
return [this.id];
},
},
model() {
return LibraryNodes.findOne(this.id);
},
propertyChildren(){
return nodesToTree({
collection: LibraryNodes,
ancestorId: this.model._id
ancestorId: this.id
});
},
}

View File

@@ -4,12 +4,13 @@
:class="insufficient && 'error--text'"
>
<div
class="mr-2"
style="width: 24px; text-align: center;"
class="mr-2 text-no-wrap text-truncate"
style="min-width: 24px; text-align: center;"
>
{{ model.quantity && model.quantity.value }}
</div>
<div
v-if="(typeof model.quantity.value !== 'string')"
class="text-no-wrap text-truncate"
>
{{ model.statName || model.variableName }}

View File

@@ -8,7 +8,7 @@
<v-list-item-content>
<v-list-item-title v-if="Number.isFinite(model.total)">
<div
v-if="model.total > 4"
v-if="model.total <= 0 || model.total > 5 || model.value > model.total || model.value < 0"
class="layout value"
style="align-items: baseline;"
>
@@ -18,7 +18,10 @@
>
{{ model.value }}
</div>
<div class="ml-2 max-value">
<div
v-if="model.total"
class="ml-2 max-value"
>
/{{ model.total }}
</div>
</div>

View File

@@ -199,5 +199,7 @@ export default {
</script>
<style lang="css">
.property-viewer ol, .property-viewer ul {
padding-left: 36px;
}
</style>

View File

@@ -1,4 +1,4 @@
// Generated automatically by nearley, version 2.20.1
// Generated automatically by nearley, version 2.16.0
// http://github.com/Hardmath123/nearley
function id(x) { return x[0]; }
@@ -93,7 +93,7 @@ let ParserRules = [
d => [d[2], ...d[3]]
},
{"name": "arguments", "symbols": [{"literal":"("}, "_", {"literal":")"}], "postprocess": d => []},
{"name": "indexExpression", "symbols": ["arrayExpression", {"literal":"["}, "_", "expression", "_", {"literal":"]"}], "postprocess": d => node.index.create({array: d[0], index: d[3]})},
{"name": "indexExpression", "symbols": ["indexExpression", {"literal":"["}, "_", "expression", "_", {"literal":"]"}], "postprocess": d => node.index.create({array: d[0], index: d[3]})},
{"name": "indexExpression", "symbols": ["arrayExpression"], "postprocess": id},
{"name": "arrayExpression$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]},
{"name": "arrayExpression$ebnf$1", "symbols": []},

View File

@@ -125,7 +125,7 @@ arguments ->
| "(" _ ")" {% d => [] %}
indexExpression ->
arrayExpression "[" _ expression _ "]" {% d => node.index.create({array: d[0], index: d[3]}) %}
indexExpression "[" _ expression _ "]" {% d => node.index.create({array: d[0], index: d[3]}) %}
| arrayExpression {% id %}
arrayExpression ->

View File

@@ -7,6 +7,58 @@ import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js
import { LIBRARY_NODE_TREE_FIELDS } from '/imports/server/publications/library.js';
import escapeRegex from '/imports/api/utility/escapeRegex.js';
// Publish docs the user has already selected so they don't disappear when searching
Meteor.publish('selectedFillers', function (slotId, nodeIds, isDummySlot) {
let autorun = this.autorun;
autorun(function () {
let userId = this.userId;
if (!userId) {
return [];
}
// Get the slot from the right collection
let slot;
if (isDummySlot) {
slot = LibraryNodes.findOne(slotId);
} else {
slot = CreatureProperties.findOne(slotId);
}
if (!slot) return [];
// Get all the ids of libraries the user can access
const creatureId = slot.ancestors[0].id;
const libraryIds = getCreatureLibraryIds(creatureId, userId);
const libraries = Libraries.find({
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ _id: { $in: libraryIds }, public: true },
]
}, {
sort: { name: 1 }
});
let filter = { _id: { $in: nodeIds } };
// Get the limit of the documents the user can fetch
let options = {
sort: {
name: 1,
order: 1,
},
limit: 100,
fields: LIBRARY_NODE_TREE_FIELDS,
};
autorun(function () {
return [
LibraryNodes.find(filter, options),
libraries
];
});
});
});
Meteor.publish('slotFillers', function (slotId, searchTerm, isDummySlot) {
if (searchTerm) check(searchTerm, String);
@@ -50,6 +102,7 @@ Meteor.publish('slotFillers', function (slotId, searchTerm, isDummySlot) {
let options = undefined;
if (searchTerm) {
if (!filter.$and) filter.$and = [];
filter.$and.push({
$or: [
{ name: { $regex: escapeRegex(searchTerm), '$options': 'i' } },