diff --git a/app/.github/pull_request_template.md b/app/.github/pull_request_template.md new file mode 100644 index 00000000..78e990e6 --- /dev/null +++ b/app/.github/pull_request_template.md @@ -0,0 +1,16 @@ +# Checklists + +## Adding features + +- [ ] My new pull request has zero code changes +- [ ] I have described the feature I intend to work on +- [ ] I have described how I intend to implement the feature +- [ ] I will wait for comment from the project's maintainers before submitting code changes + +## Fixing bugs +- [ ] I have performed a self-review of my code +- [ ] I have included a link to the relevant github issue or discord post in the description + +# Description + +`Detailed description of your changes` diff --git a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js index 25044a7d..88e2f900 100644 --- a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js @@ -77,7 +77,8 @@ const duplicateProperty = new ValidatedMethod({ }); // Give the docs new IDs without breaking internal references - renewDocIds({ docArray: nodes }); + const allNodes = [property, ...nodes]; + renewDocIds({ docArray: allNodes }); // Order the root node property.order += 0.5; @@ -86,7 +87,7 @@ const duplicateProperty = new ValidatedMethod({ property.dirty = true; // Insert the properties - CreatureProperties.batchInsert([property, ...nodes]); + CreatureProperties.batchInsert(allNodes); // Tree structure changed by inserts, reorder the tree reorderDocs({ diff --git a/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js index 4b606114..5664d8bd 100644 --- a/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js @@ -21,7 +21,7 @@ const updateCreatureProperty = new ValidatedMethod({ }, mixins: [RateLimiterMixin], rateLimit: { - numRequests: 5, + numRequests: 12, timeInterval: 5000, }, run({ _id, path, value }) { diff --git a/app/imports/api/engine/actions/applyProperty.js b/app/imports/api/engine/actions/applyProperty.js index 18f35d32..b8e809d8 100644 --- a/app/imports/api/engine/actions/applyProperty.js +++ b/app/imports/api/engine/actions/applyProperty.js @@ -1,4 +1,5 @@ import action from './applyPropertyByType/applyAction.js'; +import ammo from './applyPropertyByType/applyItemAsAmmo.js' import adjustment from './applyPropertyByType/applyAdjustment.js'; import branch from './applyPropertyByType/applyBranch.js'; import buff from './applyPropertyByType/applyBuff.js'; @@ -12,6 +13,7 @@ import toggle from './applyPropertyByType/applyToggle.js'; const applyPropertyByType = { action, + ammo, adjustment, branch, buff, diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js index 115d1998..b44c1b44 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js @@ -4,13 +4,10 @@ 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'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js'; -import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js'; -import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; export default function applyAction(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); @@ -174,7 +171,11 @@ function rollAttack(attack, scope) { } function applyCrits(value, scope) { - const criticalHitTarget = scope['~criticalHitTarget']?.value || 20; + let scopeCrit = scope['~criticalHitTarget']?.value; + if (scopeCrit?.parseType === 'constant') { + scopeCrit = scopeCrit.value; + } + const criticalHitTarget = scopeCrit || 20; let criticalHit = value >= criticalHitTarget; let criticalMiss; if (criticalHit) { @@ -206,10 +207,9 @@ function spendResources(prop, actionContext) { return true; } // Items - let itemQuantityAdjustments = []; let spendLog = []; let gainLog = []; - let ammoChildren = []; + const ammoToApply = []; try { prop.resources.itemsConsumed.forEach(itemConsumed => { recalculateCalculation(itemConsumed.quantity, actionContext); @@ -224,11 +224,6 @@ function spendResources(prop, actionContext) { !itemConsumed?.quantity?.value || !isFinite(itemConsumed.quantity.value) ) return; - itemQuantityAdjustments.push({ - property: item, - operation: 'increment', - value: itemConsumed.quantity.value, - }); let logName = item.name; if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1) { logName = item.plural || logName; @@ -238,7 +233,20 @@ function spendResources(prop, actionContext) { } else if (itemConsumed.quantity.value < 0) { gainLog.push(logName + ': ' + -itemConsumed.quantity.value); } - ammoChildren.push(...getItemChildren(item, actionContext, prop)); + // So long as the item isn't an ancestor of the current prop apply it + // If it was an ancestor this would be an infinite loop + if (!hasAncestorRelationship(item, prop)) { + ammoToApply.push({ + node: { + ...item, + // Use ammo pseudo-type + type: 'ammo', + // Store the adjustment to be applied + adjustment: itemConsumed.quantity.value, + }, + children: [] + }); + } }); } catch (e) { actionContext.addLog({ @@ -249,9 +257,6 @@ function spendResources(prop, actionContext) { return true; } // No more errors should be thrown after this line - // Now that we have confirmed that there are no errors, do actual work - //Items - itemQuantityAdjustments.forEach(adjustQuantityWork); // Use uses if (prop.usesLeft) { @@ -291,6 +296,11 @@ function spendResources(prop, actionContext) { } }); + // Apply the ammo children + ammoToApply.forEach(node => { + applyProperty(node, actionContext); + }); + // Log all the spending if (gainLog.length && !prop.silent) actionContext.addLog({ name: 'Gained', @@ -302,21 +312,6 @@ function spendResources(prop, actionContext) { value: spendLog.join('\n'), inline: true, }); - - // Apply the ammo children - ammoChildren.forEach(prop => { - applyProperty(prop, actionContext); - }); -} - -function getItemChildren(item, actionContext, prop) { - // Skip if the prop or the item are ancestors of one another, otherwise infinite loop - if (hasAncestorRelationship(item, prop)) return []; - // Get the item children - const itemProperties = getPropertyDecendants(actionContext.creature._id, item._id); - // Tree them up - const propertyForest = nodeArrayToTree(itemProperties); - return propertyForest } function hasAncestorRelationship(a, b) { diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js index b1923a4d..3cbe225c 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js @@ -10,6 +10,7 @@ import { } from '/imports/api/engine/loadCreatures.js'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js'; +import applySavingThrow from '/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js'; export default function applyDamage(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); @@ -36,7 +37,7 @@ export default function applyDamage(node, actionContext) { const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage'; // roll the dice only and store that string - applyEffectsToCalculationParseNode(prop.amount, actionContext.log); + applyEffectsToCalculationParseNode(prop.amount, actionContext); const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context); if (rolled.parseType !== 'constant') { logValue.push(toString(rolled)); @@ -67,6 +68,7 @@ export default function applyDamage(node, actionContext) { // Round the damage to a whole number damage = Math.floor(damage); + scope['~damage'] = damage; // Convert extra damage into the stored type if (prop.damageType === 'extra' && scope['~lastDamageType']?.value) { @@ -82,24 +84,74 @@ export default function applyDamage(node, actionContext) { prop.damageType + (prop.damageType !== 'healing' ? ' damage ' : ''); + // If there is a save, calculate the save damage + let damageOnSave, saveNode, saveRoll; + if (prop.save) { + if (prop.save.damageFunction?.calculation) { + applyEffectsToCalculationParseNode(prop.save.damageFunction, actionContext); + let { result: saveDamageRolled } = resolve('roll', prop.save.damageFunction.parseNode, scope, context); + saveRoll = toString(saveDamageRolled); + let { result: saveDamageResult } = resolve('reduce', saveDamageRolled, scope, context); + // If we didn't end up with a constant of finite amount, give up + if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)) { + return applyChildren(node, actionContext); + } + damageOnSave = +saveDamageResult.value; + // Round the damage to a whole number + damageOnSave = Math.floor(damageOnSave); + } else { + damageOnSave = Math.floor(damage / 2); + } + saveNode = { + node: { + ...prop.save, + name: prop.save.stat, + silent: prop.silent, + }, + children: [], + } + } + if (damageTargets && damageTargets.length) { // Iterate through all the targets damageTargets.forEach(target => { + actionContext.target = [target]; + let damageToApply = damage; + + // If there is a saving throw, apply that first + if (prop.save) { + applySavingThrow(saveNode, actionContext); + if (scope['~saveSucceeded']?.value) { + // Log the total damage + logValue.push(toString(reduced)); + // Log the save damage + const damageText = damageFunctionText(prop.save); + if (damageText) { + logValue.push(damageText); + } else { + logValue.push( + '**Damage on successful save**', + prop.save.damageFunction.calculation, + saveRoll + ); + } + damageToApply = damageOnSave; + } + } // Apply weaknesses/resistances/immunities - damage = applyDamageMultipliers({ + damageToApply = applyDamageMultipliers({ target, - damage, + damage: damageToApply, damageProp: prop, logValue }); - actionContext.target = [target]; // Deal the damage to the target let damageDealt = dealDamage({ target, damageType: prop.damageType, - amount: damage, + amount: damageToApply, actionContext }); @@ -124,6 +176,10 @@ export default function applyDamage(node, actionContext) { } else { // There are no targets, just log the result logValue.push(`**${damage}** ${suffix}`); + if (prop.save) { + applySavingThrow(saveNode, actionContext); + logValue.push(`**${damageOnSave}** ${suffix} on a successful save`); + } } if (!prop.silent) actionContext.addLog({ name: logName, @@ -133,6 +189,16 @@ export default function applyDamage(node, actionContext) { return applyChildren(node, actionContext); } +function damageFunctionText(save, scope, context, actionContext) { + if (!save) return []; + if (!save.damageFunction) { + return '**Half damage on successful save**'; + } + if (save.damageFunction.calculation == '0' || save.damageFunction.value === 0) { + return '**No damage on successful save**' + } +} + function applyDamageMultipliers({ target, damage, damageProp, logValue }) { const damageType = damageProp?.damageType; if (!damageType) return damage; diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyItemAsAmmo.js b/app/imports/api/engine/actions/applyPropertyByType/applyItemAsAmmo.js new file mode 100644 index 00000000..3bc3b24e --- /dev/null +++ b/app/imports/api/engine/actions/applyPropertyByType/applyItemAsAmmo.js @@ -0,0 +1,42 @@ +import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js'; +import applyProperty from '../applyProperty.js'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; +import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; +import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js'; + +export default function applyItemAsAmmo(node, actionContext) { + // The item node should come without children, since it is not part of the original action tree + const prop = node.node; + // Get all the item's descendant properties + const properties = getPropertyDecendants(actionContext.creature._id, prop._id); + properties.sort((a, b) => a.order - b.order); + const propertyForest = nodeArrayToTree(properties); + + // Apply the item + applyNodeTriggers(node, 'before', actionContext); + + // Do the quantity adjustment + const itemProp = { ...prop, type: 'item' }; + delete itemProp.adjustment; + adjustQuantityWork({ + property: itemProp, + operation: 'increment', + value: prop.adjustment, + }); + + // Simulate the change to quantity + prop.quantity -= prop.adjustment; + + // Log the item name as a heading if it's not silent and has child properties to apply + if (!prop.silent && propertyForest.length) { + actionContext.addLog({ + name: prop.name || 'Ammo', + inline: false, + }); + } + applyNodeTriggers(node, 'after', actionContext); + + // Apply the item's children + propertyForest.forEach(node => applyProperty(node, actionContext)); + applyNodeTriggers(node, 'afterChildren', actionContext); +} diff --git a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js index 68a19951..35ba9b60 100644 --- a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js @@ -9,8 +9,15 @@ export default function linkCalculationDependencies(dependencyGraph, prop, { pro }; // Add this calculation to the dependency graph const calcNodeId = `${prop._id}.${calcObj._key}`; - dependencyGraph.addNode(calcNodeId, calcObj); + // Skip empty calculations that aren't targeted by anything + if ( + !calcObj.calculation + && !calcObj.effects + && !calcObj.proficiencies + ) return; + + dependencyGraph.addNode(calcNodeId, calcObj); // Traverse the parsed calculation looking for variable names traverse(calcObj.parseNode, node => { // Skip nodes that aren't symbols or accessors diff --git a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js index 85a7b551..814449c3 100644 --- a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js @@ -98,8 +98,10 @@ function linkAdjustment(dependencyGraph, prop) { function linkAttribute(dependencyGraph, prop) { linkVariableName(dependencyGraph, prop); - // Depends on spellSlotLevel - dependOnCalc({ dependencyGraph, prop, key: 'spellSlotLevel' }); + // Spell slots depend on spellSlotLevel + if (prop.type === 'spellSlot') { + dependOnCalc({ dependencyGraph, prop, key: 'spellSlotLevel' }); + } // Depends on base value dependOnCalc({ dependencyGraph, prop, key: 'baseValue' }); @@ -159,7 +161,7 @@ function linkEffects(dependencyGraph, prop, computation) { // Otherwise target a field on that property const key = prop.targetField || getDefaultCalculationField(targetProp); const calcObj = get(targetProp, key); - if (calcObj && calcObj.calculation) { + if (calcObj) { dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'effect'); } } @@ -175,7 +177,7 @@ function linkEffects(dependencyGraph, prop, computation) { // Returns an array of IDs of the properties the effect targets export function getEffectTagTargets(effect, computation) { let targets = getTargetListFromTags(effect.targetTags, computation); - let notIds = []; + let notIds = [effect._id]; // Can't target itself if (effect.extraTags) { effect.extraTags.forEach(ex => { if (ex.operation === 'OR') { @@ -257,21 +259,23 @@ function linkDamageMultiplier(dependencyGraph, prop) { function linkPointBuy(dependencyGraph, prop) { dependOnCalc({ dependencyGraph, prop, key: 'min' }); dependOnCalc({ dependencyGraph, prop, key: 'max' }); - dependOnCalc({ dependencyGraph, prop, key: 'cost' }); dependOnCalc({ dependencyGraph, prop, key: 'total' }); - prop.values?.forEach(row => { + + prop.values?.forEach((row, index) => { + // Get a unique id for the row because it might be shared among duplicated point buy tables + // prop._id is forced unique by the database, so it can be used instead + const uniqueRowId = prop._id + '_row_' + index; // Wrap the document in a new object so we don't bash it unintentionally const pointBuyRow = { ...row, + _id: uniqueRowId, type: 'pointBuyRow', tableName: prop.name, tableId: prop._id, } - dependencyGraph.addNode(row._id, pointBuyRow); + dependencyGraph.addNode(pointBuyRow._id, pointBuyRow); linkVariableName(dependencyGraph, pointBuyRow); - dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.min' }); - dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.max' }); - dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.cost' }); + dependencyGraph.addLink(pointBuyRow._id, prop._id, 'pointBuyRow'); }); if (prop.inactive) return; } @@ -297,7 +301,7 @@ function linkProficiencies(dependencyGraph, prop, computation) { // Otherwise target a field on that property const key = prop.targetField || getDefaultCalculationField(targetProp); const calcObj = get(targetProp, key); - if (calcObj && calcObj.calculation) { + if (calcObj) { dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency'); } } @@ -335,7 +339,7 @@ function linkSkill(dependencyGraph, prop, computation) { // other skill isn't supported const key = prop.targetField || getDefaultCalculationField(targetProp); const calcObj = get(targetProp, key); - if (calcObj && calcObj.calculation) { + if (calcObj) { dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency'); } }); diff --git a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index 73217f80..6ef2b165 100644 --- a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -1,7 +1,7 @@ import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; import { prettifyParseError, parse } from '/imports/parser/parser.js'; import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; -import { get, unset } from 'lodash'; +import { get, set, unset } from 'lodash'; import errorNode from '/imports/parser/parseTree/error.js'; import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js'; @@ -63,12 +63,21 @@ function parseAllCalculationFields(prop, schemas) { // For all fields matching they keys // supports `keys.$.with.$.arrays` applyFnToKey(prop, calcKey, (prop, key) => { - const calcObj = get(prop, key); + let calcObj = get(prop, key); + // Create a calculation object if one doesn't exist, it will get deleted again later if + // it's not used, but if an effect targets a calculated field, we should have one to target + if ( + !calcObj + && subDocsExist(prop, key) + ) { + calcObj = {}; + set(prop, key, calcObj); + } + // Sub document didn't exist, skip this field if (!calcObj) return; - // Delete the whole calculation object if the calculation string isn't set + // Keep a list of empty calculations for potential deletion if they aren't used if (!calcObj.calculation) { - unset(prop, calcKey); - return; + prop._computationDetails.emptyCalculations.push(calcObj); } // Store a reference to all the calculations prop._computationDetails.calculations.push(calcObj); @@ -84,15 +93,31 @@ function parseAllCalculationFields(prop, schemas) { }); } +function subDocsExist(prop, key) { + const path = key.split('.'); + if (path.length < 2) return !!prop; + path.pop(); + const subPath = path.join('.'); + return !!get(prop, subPath); +} + +export function removeEmptyCalculations(prop) { + prop._computationDetails.emptyCalculations.forEach(calcObj => { + if (!calcObj.effects?.length) { + unset(prop, calcObj._key); + } + }); +} + function parseCalculation(calcObj) { - const calcHash = cyrb53(calcObj.calculation); + const calcHash = cyrb53(calcObj.calculation || '0'); // If the cached parse calculation is equal to the calculation, skip if (calcHash === calcObj.hash) { return; } calcObj.hash = calcHash; try { - calcObj.parseNode = parse(calcObj.calculation); + calcObj.parseNode = parse(calcObj.calculation || '0'); calcObj.parseError = null; } catch (e) { let error = { diff --git a/app/imports/api/engine/computation/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.js index a97f5b0a..e9dbb625 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.js @@ -75,6 +75,7 @@ export function buildComputationFromProps(properties, creature, variables) { // Add a place to store all the computation details prop._computationDetails = { calculations: [], + emptyCalculations: [], inlineCalculations: [], toggleAncestors: [], }; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js index a8243f40..48a71fb5 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js @@ -1,6 +1,6 @@ export default function computeAction(computation, node) { const prop = node.data; - if (prop.uses) { + if (Number.isFinite(prop.uses?.value)) { prop.usesLeft = prop.uses.value - (prop.usesUsed || 0); if (!prop.usesLeft) { prop.insufficientResources = true; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js b/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js index c2cc7606..a7727247 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js @@ -3,8 +3,8 @@ import evaluateCalculation from '../../utility/evaluateCalculation.js'; export default function computePointBuy(computation, node) { const prop = node.data; - const tableMin = prop.min?.value || null; - const tableMax = prop.max?.value || null; + const min = has(prop, 'min.value') ? prop.min.value : null; + const max = has(prop, 'max.value') ? prop.max.value : null; prop.spent = 0; prop.values?.forEach(row => { // Clean up added properties @@ -14,9 +14,7 @@ export default function computePointBuy(computation, node) { row.spent = 0; if (row.value === undefined) return; - const min = has(row, 'min.value') ? row.min.value : tableMin; - const max = has(row, 'max.value') ? row.max.value : tableMax; - const costFunction = EJSON.clone(row.cost || prop.cost); + const costFunction = EJSON.clone(prop.cost); if (costFunction) costFunction.parseLevel = 'reduce'; // Check min and max diff --git a/app/imports/api/engine/computation/computeCreatureComputation.js b/app/imports/api/engine/computation/computeCreatureComputation.js index 7cd4b41c..1ffe58be 100644 --- a/app/imports/api/engine/computation/computeCreatureComputation.js +++ b/app/imports/api/engine/computation/computeCreatureComputation.js @@ -1,9 +1,10 @@ import computeToggles from '/imports/api/engine/computation/computeComputation/computeToggles.js'; import computeByType from '/imports/api/engine/computation/computeComputation/computeByType.js'; import embedInlineCalculations from './utility/embedInlineCalculations.js'; +import { removeEmptyCalculations } from './buildComputation/parseCalculationFields.js'; import path from 'ngraph.path'; -export default function computeCreatureComputation(computation){ +export default function computeCreatureComputation(computation) { const stack = []; // Computation scope of {variableName: prop} const graph = computation.dependencyGraph; @@ -20,16 +21,16 @@ export default function computeCreatureComputation(computation){ stack.reverse(); // Depth first traversal of nodes - while (stack.length){ + while (stack.length) { let top = stack[stack.length - 1]; - if (top._visited){ + if (top._visited) { // The object has already been computed, skip stack.pop(); - } else if (top._visitedChildren){ + } else if (top._visitedChildren) { // Mark the object as visited and remove from stack top._visited = true; stack.pop(); - // Compute the top object of the stack + // Compute the top object of the stack compute(computation, top); } else { top._visitedChildren = true; @@ -42,14 +43,14 @@ export default function computeCreatureComputation(computation){ computation.props.forEach(finalizeProp); } -function compute(computation, node){ +function compute(computation, node) { // Determine the prop's active status by its toggles computeToggles(computation, node); // Compute the property by type computeByType[node.data?.type || '_variable']?.(computation, node); } -function pushDependenciesToStack(nodeId, graph, stack, computation){ +function pushDependenciesToStack(nodeId, graph, stack, computation) { graph.forEachLinkedNode(nodeId, linkedNode => { if (linkedNode._visitedChildren && !linkedNode._visited) { // This is a dependency loop, find a path from the node to itself @@ -66,7 +67,7 @@ function pushDependenciesToStack(nodeId, graph, stack, computation){ loop = [linkedNode, ...newLoop]; } }, true); - + if (loop.length) { computation.errors.push({ type: 'dependencyLoop', @@ -80,11 +81,13 @@ function pushDependenciesToStack(nodeId, graph, stack, computation){ }, true); } -function finalizeProp(prop){ +function finalizeProp(prop) { // Embed the inline calculations prop._computationDetails?.inlineCalculations?.forEach(inlineCalcObj => { embedInlineCalculations(inlineCalcObj); }); + // Clean up the calculations that were never used + removeEmptyCalculations(prop); // Clean up the computation details delete prop._computationDetails; } diff --git a/app/imports/api/library/methods/duplicateLibraryNode.js b/app/imports/api/library/methods/duplicateLibraryNode.js index 43aa0a8a..eef9f10c 100644 --- a/app/imports/api/library/methods/duplicateLibraryNode.js +++ b/app/imports/api/library/methods/duplicateLibraryNode.js @@ -67,12 +67,13 @@ const duplicateLibraryNode = new ValidatedMethod({ }); // Give the docs new IDs without breaking internal references - renewDocIds({ docArray: nodes }); + const allNodes = [libraryNode, ...nodes]; + renewDocIds({ docArray: allNodes }); // Order the root node libraryNode.order += 0.5; - LibraryNodes.batchInsert([libraryNode, ...nodes]); + LibraryNodes.batchInsert(allNodes); // Tree structure changed by inserts, reorder the tree reorderDocs({ diff --git a/app/imports/api/properties/Damages.js b/app/imports/api/properties/Damages.js index d01a2e0d..b5510444 100644 --- a/app/imports/api/properties/Damages.js +++ b/app/imports/api/properties/Damages.js @@ -33,10 +33,23 @@ const DamageSchema = createPropertySchema({ type: Boolean, optional: true, }, + // remove the entire object if there is no saving throw save: { - type: SavingThrowSchema, + type: Object, optional: true, }, + // The computed DC + 'save.dc': { + type: 'fieldToCompute', + optional: true, + }, + // The variable name of save to roll + 'save.stat': { + type: String, + optional: true, + max: STORAGE_LIMITS.variableName, + }, + // The damage to deal on a successful save 'save.damageFunction': { type: 'fieldToCompute', optional: true, @@ -51,13 +64,18 @@ const ComputedOnlyDamageSchema = createPropertySchema({ parseLevel: 'compile', }, save: { - type: ComputedOnlySavingThrowSchema, + type: Object, + optional: true, + }, + 'save.dc': { + type: 'computedOnlyField', + parseLevel: 'compile', optional: true, }, 'save.damageFunction': { type: 'computedOnlyField', - optional: true, parseLevel: 'compile', + optional: true, }, }); diff --git a/app/imports/api/properties/Items.js b/app/imports/api/properties/Items.js index a328cfcf..b4bb7319 100644 --- a/app/imports/api/properties/Items.js +++ b/app/imports/api/properties/Items.js @@ -55,6 +55,11 @@ const ItemSchema = createPropertySchema({ type: Boolean, defaultValue: false, }, + // Prevent the property from showing up in the log + silent: { + type: Boolean, + optional: true, + }, }); let ComputedOnlyItemSchema = createPropertySchema({ diff --git a/app/imports/api/properties/PointBuys.js b/app/imports/api/properties/PointBuys.js index db20911a..fcbb8f6e 100644 --- a/app/imports/api/properties/PointBuys.js +++ b/app/imports/api/properties/PointBuys.js @@ -29,7 +29,7 @@ let PointBuySchema = createPropertySchema({ 'values.$._id': { type: String, regEx: SimpleSchema.RegEx.Id, - autoValue(){ + autoValue() { if (!this.isSet) return Random.id(); } }, @@ -49,18 +49,6 @@ let PointBuySchema = createPropertySchema({ type: Number, optional: true, }, - 'values.$.min': { - type: 'fieldToCompute', - optional: true, - }, - 'values.$.max': { - type: 'fieldToCompute', - optional: true, - }, - 'values.$.cost': { - type: 'fieldToCompute', - optional: true, - }, min: { type: 'fieldToCompute', optional: true, @@ -102,19 +90,6 @@ const ComputedOnlyPointBuySchema = createPropertySchema({ 'values.$': { type: Object, }, - 'values.$.min': { - type: 'computedOnlyField', - optional: true, - }, - 'values.$.max': { - type: 'computedOnlyField', - optional: true, - }, - 'values.$.cost': { - type: 'computedOnlyField', - optional: true, - parseLevel: 'compile', - }, 'values.$.spent': { type: Number, optional: true, diff --git a/app/imports/api/properties/Triggers.js b/app/imports/api/properties/Triggers.js index 250d6bda..09d777e2 100644 --- a/app/imports/api/properties/Triggers.js +++ b/app/imports/api/properties/Triggers.js @@ -23,6 +23,7 @@ const timingOptions = { const actionPropertyTypeOptions = { action: 'Action', + ammo: 'Ammo used', adjustment: 'Attribute damage', branch: 'Branch', buff: 'Buff', diff --git a/app/imports/client/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue b/app/imports/client/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue index 1d47cd09..c4e5e2ab 100644 --- a/app/imports/client/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue +++ b/app/imports/client/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue @@ -32,7 +32,10 @@ light >