Merge branch 'develop' into feature-tabletop

This commit is contained in:
ThaumRystra
2023-09-24 19:10:04 +02:00
47 changed files with 1166 additions and 562 deletions

View File

@@ -1,13 +1,13 @@
import walkDown from '/imports/api/engine/computation/utility/walkdown.js';
export default function computeInactiveStatus(node){
export default function computeInactiveStatus(node) {
const prop = node.node;
if (!isActive(prop)){
if (!isActive(prop)) {
// Mark prop inactive due to self
prop.inactive = true;
prop.deactivatedBySelf = true;
}
if(!childrenActive(prop)){
if (!childrenActive(prop)) {
// Mark children as inactive due to ancestor
walkDown(node.children, child => {
child.node.inactive = true;
@@ -16,27 +16,25 @@ export default function computeInactiveStatus(node){
}
}
function isActive(prop){
function isActive(prop) {
if (prop.disabled) return false;
switch (prop.type){
switch (prop.type) {
// Unprepared spells are inactive
case 'spell': return !!prop.prepared || !!prop.alwaysPrepared;
default: return true;
}
}
function childrenActive(prop){
function childrenActive(prop) {
// Children of disabled properties are always inactive
if (prop.disabled) return false;
switch (prop.type){
switch (prop.type) {
// Only equipped items with non-zero quantity have active children
case 'item': return !!prop.equipped && prop.quantity !== 0;
// The children of actions, spells, and triggers are always inactive
case 'action': return false;
case 'spell': return false;
case 'trigger': return false;
// The children of notes are always inactive
case 'note': return false;
// Other children are active
default: return true;
}

View File

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

View File

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

View File

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

View File

@@ -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: [],
};

View File

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

View File

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

View File

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