Merge branch 'master' into version-2-tabletop
This commit is contained in:
@@ -8,6 +8,8 @@ import { damagePropertyWork } from '/imports/api/creature/creatureProperties/met
|
||||
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);
|
||||
@@ -51,11 +53,11 @@ export default function applyAction(node, actionContext) {
|
||||
}
|
||||
|
||||
function applyAttackWithoutTarget({ attack, actionContext }) {
|
||||
delete actionContext.scope['$attackHit'];
|
||||
delete actionContext.scope['$attackMiss'];
|
||||
delete actionContext.scope['$criticalHit'];
|
||||
delete actionContext.scope['$criticalMiss'];
|
||||
delete actionContext.scope['$attackRoll'];
|
||||
delete actionContext.scope['~attackHit'];
|
||||
delete actionContext.scope['~attackMiss'];
|
||||
delete actionContext.scope['~criticalHit'];
|
||||
delete actionContext.scope['~criticalMiss'];
|
||||
delete actionContext.scope['~attackRoll'];
|
||||
|
||||
recalculateCalculation(attack, actionContext);
|
||||
const scope = actionContext.scope;
|
||||
@@ -66,16 +68,16 @@ function applyAttackWithoutTarget({ attack, actionContext }) {
|
||||
criticalMiss,
|
||||
} = rollAttack(attack, scope);
|
||||
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
|
||||
if (scope['$attackAdvantage'] === 1) {
|
||||
if (scope['~attackAdvantage']?.value === 1) {
|
||||
name += ' (Advantage)';
|
||||
} else if (scope['$attackAdvantage'] === -1) {
|
||||
} else if (scope['~attackAdvantage']?.value === -1) {
|
||||
name += ' (Disadvantage)';
|
||||
}
|
||||
if (!criticalMiss) {
|
||||
scope['$attackHit'] = { value: true }
|
||||
scope['~attackHit'] = { value: true }
|
||||
}
|
||||
if (!criticalHit) {
|
||||
scope['$attackMiss'] = { value: true };
|
||||
scope['~attackMiss'] = { value: true };
|
||||
}
|
||||
|
||||
actionContext.addLog({
|
||||
@@ -87,12 +89,12 @@ function applyAttackWithoutTarget({ attack, actionContext }) {
|
||||
|
||||
function applyAttackToTarget({ attack, target, actionContext }) {
|
||||
const scope = actionContext.scope;
|
||||
delete scope['$attackHit'];
|
||||
delete scope['$attackMiss'];
|
||||
delete scope['$criticalHit'];
|
||||
delete scope['$criticalMiss'];
|
||||
delete scope['$attackDiceRoll'];
|
||||
delete scope['$attackRoll'];
|
||||
delete scope['~attackHit'];
|
||||
delete scope['~attackMiss'];
|
||||
delete scope['~criticalHit'];
|
||||
delete scope['~criticalMiss'];
|
||||
delete scope['~attackDiceRoll'];
|
||||
delete scope['~attackRoll'];
|
||||
|
||||
recalculateCalculation(attack, actionContext);
|
||||
|
||||
@@ -109,9 +111,9 @@ function applyAttackToTarget({ attack, target, actionContext }) {
|
||||
let name = criticalHit ? 'Critical Hit!' :
|
||||
criticalMiss ? 'Critical Miss!' :
|
||||
result > armor ? 'Hit!' : 'Miss!';
|
||||
if (scope['$attackAdvantage'] === 1) {
|
||||
if (scope['~attackAdvantage']?.value === 1) {
|
||||
name += ' (Advantage)';
|
||||
} else if (scope['$attackAdvantage'] === -1) {
|
||||
} else if (scope['~attackAdvantage']?.value === -1) {
|
||||
name += ' (Disadvantage)';
|
||||
}
|
||||
|
||||
@@ -121,9 +123,9 @@ function applyAttackToTarget({ attack, target, actionContext }) {
|
||||
inline: true,
|
||||
});
|
||||
if (criticalMiss || result < armor) {
|
||||
scope['$attackMiss'] = { value: true };
|
||||
scope['~attackMiss'] = { value: true };
|
||||
} else {
|
||||
scope['$attackHit'] = { value: true };
|
||||
scope['~attackHit'] = { value: true };
|
||||
}
|
||||
} else {
|
||||
actionContext.addLog({
|
||||
@@ -141,7 +143,7 @@ function applyAttackToTarget({ attack, target, actionContext }) {
|
||||
function rollAttack(attack, scope) {
|
||||
const rollModifierText = numberToSignedString(attack.value, true);
|
||||
let value, resultPrefix;
|
||||
if (scope['$attackAdvantage'] === 1) {
|
||||
if (scope['~attackAdvantage']?.value === 1) {
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
value = a;
|
||||
@@ -150,7 +152,7 @@ function rollAttack(attack, scope) {
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||
}
|
||||
} else if (scope['$attackAdvantage'] === -1) {
|
||||
} else if (scope['~attackAdvantage']?.value === -1) {
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
value = a;
|
||||
@@ -163,23 +165,23 @@ function rollAttack(attack, scope) {
|
||||
value = rollDice(1, 20)[0];
|
||||
resultPrefix = `1d20 [${value}] ${rollModifierText}`
|
||||
}
|
||||
scope['$attackDiceRoll'] = { value };
|
||||
scope['~attackDiceRoll'] = { value };
|
||||
const result = value + attack.value;
|
||||
scope['$attackRoll'] = { result };
|
||||
scope['~attackRoll'] = { value: result };
|
||||
const { criticalHit, criticalMiss } = applyCrits(value, scope);
|
||||
return { resultPrefix, result, value, criticalHit, criticalMiss };
|
||||
}
|
||||
|
||||
function applyCrits(value, scope) {
|
||||
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
|
||||
const criticalHitTarget = scope['~criticalHitTarget']?.value || 20;
|
||||
let criticalHit = value >= criticalHitTarget;
|
||||
let criticalMiss;
|
||||
if (criticalHit) {
|
||||
scope['$criticalHit'] = { value: true };
|
||||
scope['~criticalHit'] = { value: true };
|
||||
} else {
|
||||
criticalMiss = value === 1;
|
||||
if (criticalMiss) {
|
||||
scope['$criticalMiss'] = { value: true };
|
||||
scope['~criticalMiss'] = { value: true };
|
||||
}
|
||||
}
|
||||
return { criticalHit, criticalMiss };
|
||||
@@ -211,6 +213,7 @@ function spendResources(prop, actionContext) {
|
||||
let itemQuantityAdjustments = [];
|
||||
let spendLog = [];
|
||||
let gainLog = [];
|
||||
let ammoChildren = [];
|
||||
try {
|
||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
||||
recalculateCalculation(itemConsumed.quantity, actionContext);
|
||||
@@ -221,11 +224,8 @@ function spendResources(prop, actionContext) {
|
||||
if (!item || item.ancestors[0].id !== prop.ancestors[0].id) {
|
||||
throw 'The prop\'s ammo was not found on the creature';
|
||||
}
|
||||
if (!item.equipped) {
|
||||
throw 'The selected ammo is not equipped';
|
||||
}
|
||||
if (
|
||||
!itemConsumed.quantity.value ||
|
||||
!itemConsumed?.quantity?.value ||
|
||||
!isFinite(itemConsumed.quantity.value)
|
||||
) return;
|
||||
itemQuantityAdjustments.push({
|
||||
@@ -242,12 +242,14 @@ function spendResources(prop, actionContext) {
|
||||
} else if (itemConsumed.quantity.value < 0) {
|
||||
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
|
||||
}
|
||||
ammoChildren.push(...getItemChildren(item, actionContext, prop));
|
||||
});
|
||||
} catch (e) {
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: e,
|
||||
value: e.toString(),
|
||||
});
|
||||
console.error(e);
|
||||
return true;
|
||||
}
|
||||
// No more errors should be thrown after this line
|
||||
@@ -303,4 +305,36 @@ 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) {
|
||||
let top, bottom;
|
||||
if (a.ancestors.length === b.ancestors.length) {
|
||||
// Can't be ancestors of one another if they have the same number of ancestors
|
||||
return false;
|
||||
} else if (a.ancestors.length > b.ancestors.length) {
|
||||
// longer ancestor list goes on the bottom
|
||||
top = b;
|
||||
bottom = a;
|
||||
} else {
|
||||
top = a;
|
||||
bottom = b;
|
||||
}
|
||||
const expectedAncestorPosition = top.ancestors.length;
|
||||
return bottom.ancestors[expectedAncestorPosition]?.id === top._id;
|
||||
}
|
||||
|
||||
@@ -3,22 +3,22 @@ import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import rollDice from '/imports/parser/rollDice.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyBranch(node, actionContext){
|
||||
export default function applyBranch(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const applyChildren = function(){
|
||||
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){
|
||||
switch (prop.branchType) {
|
||||
case 'if':
|
||||
recalculateCalculation(prop.condition, actionContext);
|
||||
if (prop.condition?.value) applyChildren();
|
||||
break;
|
||||
case 'index':
|
||||
if (node.children.length){
|
||||
if (node.children.length) {
|
||||
recalculateCalculation(prop.condition, actionContext);
|
||||
if (!isFinite(prop.condition?.value)) {
|
||||
actionContext.addLog({
|
||||
@@ -35,31 +35,31 @@ export default function applyBranch(node, actionContext){
|
||||
}
|
||||
break;
|
||||
case 'hit':
|
||||
if (scope['$attackHit']?.value){
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On hit**'});
|
||||
if (scope['~attackHit']?.value) {
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On hit**' });
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'miss':
|
||||
if (scope['$attackMiss']?.value){
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On miss**'});
|
||||
if (scope['~attackMiss']?.value) {
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On miss**' });
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'failedSave':
|
||||
if (scope['$saveFailed']?.value){
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On failed save**'});
|
||||
if (scope['~saveFailed']?.value) {
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On failed save**' });
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'successfulSave':
|
||||
if (scope['$saveSucceeded']?.value){
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On save**',});
|
||||
if (scope['~saveSucceeded']?.value) {
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On save**', });
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'random':
|
||||
if (node.children.length){
|
||||
if (node.children.length) {
|
||||
let index = rollDice(1, node.children.length)[0] - 1;
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
applyProperty(node.children[index], actionContext);
|
||||
|
||||
@@ -21,7 +21,10 @@ export default function applyBuff(node, actionContext) {
|
||||
const prop = node.node;
|
||||
let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||
|
||||
// Then copy the decendants of the buff to the targets
|
||||
// Mark the buff as dirty for recalculation
|
||||
prop.dirty = true;
|
||||
|
||||
// Then copy the descendants of the buff to the targets
|
||||
let propList = [prop];
|
||||
function addChildrenToPropList(children, { skipCrystalize } = {}) {
|
||||
children.forEach(child => {
|
||||
@@ -97,7 +100,7 @@ function copyNodeListToTarget(propList, target, oldParent) {
|
||||
|
||||
/**
|
||||
* Replaces all variables with their resolved values
|
||||
* except variables of the form `$target.thing.total` become `thing.total`
|
||||
* except variables of the form `~target.thing.total` become `thing.total`
|
||||
*/
|
||||
function crystalizeVariables({ propList, actionContext }) {
|
||||
propList.forEach(prop => {
|
||||
@@ -116,8 +119,8 @@ function crystalizeVariables({ propList, actionContext }) {
|
||||
node.parseType !== 'accessor' && node.parseType !== 'symbol'
|
||||
) return node;
|
||||
// Handle variables
|
||||
if (node.name === '$target') {
|
||||
// strip $target
|
||||
if (node.name === '~target') {
|
||||
// strip ~target
|
||||
if (node.parseType === 'accessor') {
|
||||
node.name = node.path.shift();
|
||||
if (!node.path.length) {
|
||||
@@ -127,7 +130,7 @@ function crystalizeVariables({ propList, actionContext }) {
|
||||
// Can't strip symbols
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'Variable `$target` should not be used without a property: $target.property',
|
||||
value: 'Variable `~target` should not be used without a property: ~target.property',
|
||||
});
|
||||
}
|
||||
return node;
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function applyDamage(node, actionContext) {
|
||||
// Choose target
|
||||
let damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||
// Determine if the hit is critical
|
||||
let criticalHit = scope['$criticalHit']?.value &&
|
||||
let criticalHit = scope['~criticalHit']?.value &&
|
||||
prop.damageType !== 'healing' // Can't critically heal
|
||||
;
|
||||
// Double the damage rolls if the hit is critical
|
||||
@@ -73,12 +73,12 @@ export default function applyDamage(node, actionContext) {
|
||||
damage = Math.floor(damage);
|
||||
|
||||
// Convert extra damage into the stored type
|
||||
if (prop.damageType === 'extra' && scope['$lastDamageType']) {
|
||||
prop.damageType = scope['$lastDamageType'];
|
||||
if (prop.damageType === 'extra' && scope['~lastDamageType']?.value) {
|
||||
prop.damageType = scope['~lastDamageType']?.value;
|
||||
}
|
||||
// Store current damage type
|
||||
if (prop.damageType !== 'healing') {
|
||||
scope['$lastDamageType'] = prop.damageType;
|
||||
scope['~lastDamageType'] = { value: prop.damageType };
|
||||
}
|
||||
|
||||
// Memoise the damage suffix for the log
|
||||
@@ -193,14 +193,18 @@ function dealDamage({ target, damageType, amount, actionContext }) {
|
||||
let healthBars = getPropertiesOfType(target._id, 'attribute');
|
||||
|
||||
// Keep only the healthbars that can take damage/healing
|
||||
remove(healthBars, (bar) =>
|
||||
bar.attributeType !== 'healthBar' ||
|
||||
bar.inactive ||
|
||||
bar.removed ||
|
||||
bar.overridden ||
|
||||
(amount >= 0 && bar.healthBarNoDamage) ||
|
||||
(amount < 0 && bar.healthBarNoHealing)
|
||||
);
|
||||
healthBars = healthBars.filter((bar) => {
|
||||
if (bar.attributeType !== 'healthBar' || bar.inactive || bar.removed || bar.overridden) {
|
||||
return false;
|
||||
}
|
||||
if (damageType === 'healing' && bar.healthBarNoHealing) {
|
||||
return false;
|
||||
}
|
||||
if (damageType !== 'healing' && amount >= 0 && bar.healthBarNoDamage) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Sort healthbars by damage/healing order or tree order as a fallback
|
||||
healthBars.sort((a, b) => {
|
||||
|
||||
@@ -4,22 +4,22 @@ import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/appl
|
||||
import resolve, { toString } from '/imports/parser/resolve.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyRoll(node, actionContext){
|
||||
export default function applyRoll(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
|
||||
const applyChildren = function(){
|
||||
const applyChildren = function () {
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
|
||||
if (prop.roll?.calculation){
|
||||
if (prop.roll?.calculation) {
|
||||
const logValue = [];
|
||||
|
||||
// roll the dice only and store that string
|
||||
applyEffectsToCalculationParseNode(prop.roll, actionContext);
|
||||
const {result: rolled, context} = resolve('roll', prop.roll.parseNode, actionContext.scope);
|
||||
if (rolled.parseType !== 'constant'){
|
||||
const { result: rolled, context } = resolve('roll', prop.roll.parseNode, actionContext.scope);
|
||||
if (rolled.parseType !== 'constant') {
|
||||
logValue.push(toString(rolled));
|
||||
}
|
||||
logErrors(context.errors, actionContext);
|
||||
@@ -28,28 +28,28 @@ export default function applyRoll(node, actionContext){
|
||||
context.errors = [];
|
||||
|
||||
// Resolve the roll to a final value
|
||||
const {result: reduced} = resolve('reduce', rolled, actionContext.scope, context);
|
||||
const { result: reduced } = resolve('reduce', rolled, actionContext.scope, context);
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// Store the result
|
||||
if (reduced.parseType === 'constant'){
|
||||
if (reduced.parseType === 'constant') {
|
||||
prop.roll.value = reduced.value;
|
||||
} else if (reduced.parseType === 'error'){
|
||||
} else if (reduced.parseType === 'error') {
|
||||
prop.roll.value = null;
|
||||
} else {
|
||||
prop.roll.value = toString(reduced);
|
||||
}
|
||||
|
||||
// If we didn't end up with a constant of finite amount, give up
|
||||
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
|
||||
// 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();
|
||||
}
|
||||
const value = reduced.value;
|
||||
|
||||
actionContext.scope[prop.variableName] = value;
|
||||
actionContext.scope[prop.variableName] = { value };
|
||||
logValue.push(`**${value}**`);
|
||||
|
||||
if (!prop.silent){
|
||||
if (!prop.silent) {
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: logValue.join('\n'),
|
||||
|
||||
@@ -8,6 +8,7 @@ import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck.js';
|
||||
export default function applySavingThrow(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
const originalTargets = actionContext.targets;
|
||||
|
||||
let saveTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||
|
||||
@@ -31,22 +32,22 @@ export default function applySavingThrow(node, actionContext) {
|
||||
// If there are no save targets, apply all children as if the save both
|
||||
// succeeeded and failed
|
||||
if (!saveTargets?.length) {
|
||||
scope['$saveFailed'] = { value: true };
|
||||
scope['$saveSucceeded'] = { value: true };
|
||||
scope['~saveFailed'] = { value: true };
|
||||
scope['~saveSucceeded'] = { value: true };
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
return node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
// Each target makes the saving throw
|
||||
saveTargets.forEach(target => {
|
||||
delete scope['$saveFailed'];
|
||||
delete scope['$saveSucceeded'];
|
||||
delete scope['$saveDiceRoll'];
|
||||
delete scope['$saveRoll'];
|
||||
delete scope['~saveFailed'];
|
||||
delete scope['~saveSucceeded'];
|
||||
delete scope['~saveDiceRoll'];
|
||||
delete scope['~saveRoll'];
|
||||
|
||||
const applyChildren = function () {
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
actionContext.targets = [target]
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
|
||||
@@ -90,14 +91,14 @@ export default function applySavingThrow(node, actionContext) {
|
||||
value = values[0];
|
||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
|
||||
}
|
||||
scope['$saveDiceRoll'] = { value };
|
||||
scope['~saveDiceRoll'] = { value };
|
||||
const result = value + rollModifier || 0;
|
||||
scope['$saveRoll'] = { value: result };
|
||||
scope['~saveRoll'] = { value: result };
|
||||
const saveSuccess = result >= dc;
|
||||
if (saveSuccess) {
|
||||
scope['$saveSucceeded'] = { value: true };
|
||||
scope['~saveSucceeded'] = { value: true };
|
||||
} else {
|
||||
scope['$saveFailed'] = { value: true };
|
||||
scope['~saveFailed'] = { value: true };
|
||||
}
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: saveSuccess ? 'Successful save' : 'Failed save',
|
||||
@@ -106,4 +107,6 @@ export default function applySavingThrow(node, actionContext) {
|
||||
});
|
||||
return applyChildren();
|
||||
});
|
||||
// reset the targets after the save to each child
|
||||
actionContext.targets = originalTargets;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function applyTrigger(trigger, prop, actionContext) {
|
||||
if (trigger.inactive) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Prevent triggers from firing if their condition is false
|
||||
if (trigger.condition?.parseNode) {
|
||||
recalculateCalculation(trigger.condition, actionContext);
|
||||
@@ -61,11 +61,11 @@ export function applyTrigger(trigger, prop, actionContext) {
|
||||
value: trigger.description,
|
||||
inline: false,
|
||||
}
|
||||
if (trigger.description?.text){
|
||||
if (trigger.description?.text) {
|
||||
recalculateInlineCalculations(trigger.description, actionContext);
|
||||
content.value = trigger.description.value;
|
||||
}
|
||||
if(!trigger.silent) actionContext.addLog(content);
|
||||
if (!trigger.silent) actionContext.addLog(content);
|
||||
|
||||
// Get all the trigger's properties and apply them
|
||||
const properties = getPropertyDecendants(actionContext.creature._id, trigger._id);
|
||||
@@ -78,7 +78,7 @@ export function applyTrigger(trigger, prop, actionContext) {
|
||||
trigger.firing = false;
|
||||
}
|
||||
|
||||
function triggerMatchTags(trigger, prop) {
|
||||
export function triggerMatchTags(trigger, prop) {
|
||||
let matched = false;
|
||||
const propTags = getEffectivePropTags(prop);
|
||||
// Check the target tags
|
||||
@@ -89,23 +89,26 @@ function triggerMatchTags(trigger, prop) {
|
||||
matched = true;
|
||||
}
|
||||
// Check the extra tags
|
||||
trigger.extraTags?.forEach(extra => {
|
||||
if (extra.operation === 'OR') {
|
||||
if (matched) return;
|
||||
if (
|
||||
!extra.tags.length ||
|
||||
difference(extra.tags, propTags).length === 0
|
||||
) {
|
||||
matched = true;
|
||||
}
|
||||
} else if (extra.operation === 'NOT') {
|
||||
if (
|
||||
extra.tags.length &&
|
||||
intersection(extra.tags, propTags)
|
||||
) {
|
||||
return false;
|
||||
if (trigger.extraTags) {
|
||||
for (const extra of trigger.extraTags) {
|
||||
if (extra.operation === 'OR') {
|
||||
if (matched) break;
|
||||
if (
|
||||
!extra.tags.length ||
|
||||
difference(extra.tags, propTags).length === 0
|
||||
) {
|
||||
matched = true;
|
||||
}
|
||||
} else if (extra.operation === 'NOT') {
|
||||
if (
|
||||
extra.tags.length &&
|
||||
intersection(extra.tags, propTags).length > 0
|
||||
) {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
67
app/imports/api/engine/actions/applyTriggers.testFn.js
Normal file
67
app/imports/api/engine/actions/applyTriggers.testFn.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { triggerMatchTags } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import clean from '/imports/api/engine/computation/utility/cleanProp.testFn.js';
|
||||
import { assert } from 'chai';
|
||||
|
||||
export default function () {
|
||||
const prop = clean({
|
||||
id: 'propWithTags',
|
||||
type: 'action',
|
||||
tags: ['yes1', 'notUsed', 'no1', 'yes2', 'no2', 'or1', 'or2'],
|
||||
});
|
||||
const positiveProp = clean({
|
||||
id: 'propWithTags',
|
||||
type: 'action',
|
||||
tags: ['yes1', 'notUsed', 'yes2', 'or1', 'or2'],
|
||||
});
|
||||
assert.isTrue(
|
||||
triggerMatchTags(clean({
|
||||
type: 'trigger',
|
||||
targetTags: ['yes1'],
|
||||
}), prop),
|
||||
'Trigger matches on a single target tag'
|
||||
);
|
||||
assert.isTrue(
|
||||
triggerMatchTags(clean({
|
||||
type: 'trigger',
|
||||
targetTags: ['yes1', 'yes2'],
|
||||
}), prop),
|
||||
'Trigger matches on a multiple target tags'
|
||||
);
|
||||
assert.isFalse(
|
||||
triggerMatchTags(clean({
|
||||
type: 'trigger',
|
||||
targetTags: ['yes1'],
|
||||
extraTags: [{ operation: 'NOT', tags: ['no1'] }]
|
||||
}), prop),
|
||||
'Trigger correctly fails to match when not tags are present'
|
||||
);
|
||||
assert.isFalse(
|
||||
triggerMatchTags(clean({
|
||||
type: 'trigger',
|
||||
extraTags: [{ operation: 'NOT', tags: ['no1'] }]
|
||||
}), prop),
|
||||
'Trigger correctly fails to match when only not tags are present'
|
||||
);
|
||||
assert.isTrue(
|
||||
triggerMatchTags(clean({
|
||||
type: 'trigger',
|
||||
extraTags: [{ operation: 'NOT', tags: ['no1'] }]
|
||||
}), positiveProp),
|
||||
'Trigger matches when only not tags are present'
|
||||
);
|
||||
assert.isTrue(
|
||||
triggerMatchTags(clean({
|
||||
type: 'trigger',
|
||||
extraTags: [{ operation: 'OR', tags: ['or1'] }]
|
||||
}), positiveProp),
|
||||
'Trigger matches when OR tags are present'
|
||||
);
|
||||
assert.isTrue(
|
||||
triggerMatchTags(clean({
|
||||
type: 'trigger',
|
||||
targetTags: ['missing1'],
|
||||
extraTags: [{ operation: 'OR', tags: ['or1'] }]
|
||||
}), positiveProp),
|
||||
'Trigger matches when only OR tags are present'
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import '/imports/api/simpleSchemaConfig.js';
|
||||
//import testTypes from './testTypes/index.js';
|
||||
import applyTriggers from '/imports/api/engine/actions/applyTriggers.testFn.js';
|
||||
import { doActionWork } from './doAction.js';
|
||||
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
|
||||
function cleanProp(prop){
|
||||
function cleanProp(prop) {
|
||||
let schema = CreatureProperties.simpleSchema(prop);
|
||||
return schema.clean(prop);
|
||||
}
|
||||
|
||||
function cleanCreature(creature){
|
||||
function cleanCreature(creature) {
|
||||
let schema = Creatures.simpleSchema(creature);
|
||||
return schema.clean(creature);
|
||||
}
|
||||
@@ -28,7 +29,7 @@ const testActionContext = {
|
||||
}),
|
||||
scope: {},
|
||||
addLog(content) {
|
||||
if (content.name || content.value){
|
||||
if (content.name || content.value) {
|
||||
this.log.content.push(content);
|
||||
}
|
||||
},
|
||||
@@ -40,8 +41,8 @@ const action = cleanProp({
|
||||
});
|
||||
const actionAncestors = [];
|
||||
|
||||
describe('Do Action', function(){
|
||||
it('Does an empty action', function(){
|
||||
describe('Do Action', function () {
|
||||
it('Does an empty action', function () {
|
||||
doActionWork({
|
||||
properties: [action],
|
||||
ancestors: actionAncestors,
|
||||
@@ -51,3 +52,7 @@ describe('Do Action', function(){
|
||||
});
|
||||
//testTypes.forEach(test => it(test.text, test.fn));
|
||||
});
|
||||
|
||||
describe('Action utility functions', function () {
|
||||
it('Triggers match tags', applyTriggers);
|
||||
})
|
||||
|
||||
@@ -117,7 +117,8 @@ const doAction = new ValidatedMethod({
|
||||
}
|
||||
}
|
||||
|
||||
actionContext.scope['slotLevel'] = slotLevel;
|
||||
actionContext.scope['slotLevel'] = { value: slotLevel };
|
||||
actionContext.scope['~slotLevel'] = { value: slotLevel };
|
||||
|
||||
// Do the action
|
||||
doActionWork({
|
||||
|
||||
@@ -81,7 +81,7 @@ function rollCheck(prop, actionContext) {
|
||||
rollModifier += effectBonus;
|
||||
|
||||
let value, values, resultPrefix;
|
||||
if (scope['$checkAdvantage'] === 1) {
|
||||
if (scope['~checkAdvantage']?.value === 1) {
|
||||
logName += ' (Advantage)';
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
@@ -91,7 +91,7 @@ function rollCheck(prop, actionContext) {
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||
}
|
||||
} else if (scope['$checkAdvantage'] === -1) {
|
||||
} else if (scope['~checkAdvantage']?.value === -1) {
|
||||
logName += ' (Disadvantage)';
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
@@ -107,9 +107,9 @@ function rollCheck(prop, actionContext) {
|
||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
|
||||
}
|
||||
const result = (value + rollModifier) || 0;
|
||||
scope['$checkDiceRoll'] = value;
|
||||
scope['$checkRoll'] = result;
|
||||
scope['$checkModifier'] = rollModifier;
|
||||
scope['~checkDiceRoll'] = { value };
|
||||
scope['~checkRoll'] = { value: result };
|
||||
scope['~checkModifier'] = { value: rollModifier };
|
||||
actionContext.addLog({
|
||||
name: logName,
|
||||
value: `${resultPrefix} **${result}**`,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Only computes `totalFilled`, need to compute `quantityExpected.value`
|
||||
* before `spacesLeft` can be computed
|
||||
*/
|
||||
export default function computeSlotQuantityFilled(node, dependencyGraph){
|
||||
export default function computeSlotQuantityFilled(node, dependencyGraph) {
|
||||
let slot = node.node;
|
||||
if (slot.type !== 'propertySlot') return;
|
||||
slot.totalFilled = 0;
|
||||
@@ -10,9 +10,8 @@ export default function computeSlotQuantityFilled(node, dependencyGraph){
|
||||
let childProp = child.node;
|
||||
dependencyGraph.addLink(slot._id, childProp._id, 'slotFill');
|
||||
if (
|
||||
childProp.type === 'slotFiller' &&
|
||||
Number.isFinite(childProp.slotQuantityFilled)
|
||||
){
|
||||
) {
|
||||
slot.totalFilled += childProp.slotQuantityFilled;
|
||||
} else {
|
||||
slot.totalFilled++;
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
import walkDown from '/imports/api/engine/computation/utility/walkdown.js';
|
||||
import { getEffectTagTargets } from '/imports/api/engine/computation/buildComputation/linkTypeDependencies.js';
|
||||
|
||||
export default function computeToggleDependencies(node, dependencyGraph){
|
||||
export default function computeToggleDependencies(node, dependencyGraph, computation, forest) {
|
||||
const prop = node.node;
|
||||
// Only for toggles that aren't inactive and aren't set to enabled or disabled
|
||||
if (
|
||||
prop.inactive ||
|
||||
prop.type !== 'toggle' ||
|
||||
prop.disabled ||
|
||||
prop.enabled
|
||||
) return;
|
||||
// Only for toggles
|
||||
if (prop.type !== 'toggle') return;
|
||||
|
||||
if (prop.targetByTags) {
|
||||
// Find all the props targeted by tags, and disable them and their children
|
||||
getEffectTagTargets(prop, computation).forEach(targetId => {
|
||||
const target = forest.nodeIndex[targetId];
|
||||
if (!target) return;
|
||||
target.node._computationDetails.toggleAncestors.push(prop);
|
||||
dependencyGraph.addLink(target.node._id, prop._id, 'toggle');
|
||||
walkDown(target.children, child => {
|
||||
// The child nodes depend on the toggle
|
||||
child.node._computationDetails.toggleAncestors.push(prop);
|
||||
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// We don't need to link direct children of static toggles, it's already done
|
||||
if (prop.disabled || prop.enabled) return;
|
||||
|
||||
walkDown(node.children, child => {
|
||||
// The child nodes depend on the toggle condition compuation
|
||||
// The child nodes depend on the toggle
|
||||
child.node._computationDetails.toggleAncestors.push(prop);
|
||||
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
|
||||
});
|
||||
|
||||
@@ -29,9 +29,9 @@ export default function linkTypeDependencies(dependencyGraph, prop, computation)
|
||||
|
||||
function dependOnCalc({ dependencyGraph, prop, key }) {
|
||||
let calc = get(prop, key);
|
||||
if (!calc) return;
|
||||
if (!calc?.type) return;
|
||||
if (calc.type !== '_calculation') {
|
||||
throw `Expected calculation got ${calc.type}`
|
||||
throw `Failed to dependOnCal for prop: ${prop._id}, key: ${key}. Expected calculation got ${calc.type}`
|
||||
}
|
||||
dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation');
|
||||
}
|
||||
@@ -164,7 +164,7 @@ function linkEffects(dependencyGraph, prop, computation) {
|
||||
}
|
||||
|
||||
// Returns an array of IDs of the properties the effect targets
|
||||
function getEffectTagTargets(effect, computation) {
|
||||
export function getEffectTagTargets(effect, computation) {
|
||||
let targets = getTargetListFromTags(effect.targetTags, computation);
|
||||
let notIds = [];
|
||||
if (effect.extraTags) {
|
||||
@@ -218,7 +218,6 @@ function getDefaultCalculationField(prop) {
|
||||
case 'roll': return 'roll';
|
||||
case 'savingThrow': return 'dc';
|
||||
case 'skill': return 'baseValue';
|
||||
case 'slotFiller': return null;
|
||||
case 'slot': return 'quantityExpected';
|
||||
case 'spellList': return 'attackRollBonus';
|
||||
case 'spell': return null;
|
||||
@@ -268,20 +267,45 @@ function linkPointBuy(dependencyGraph, prop) {
|
||||
if (prop.inactive) return;
|
||||
}
|
||||
|
||||
function linkProficiencies(dependencyGraph, prop) {
|
||||
function linkProficiencies(dependencyGraph, prop, computation) {
|
||||
// The stats depend on the proficiency
|
||||
if (prop.inactive) return;
|
||||
prop.stats.forEach(statName => {
|
||||
if (!statName) return;
|
||||
dependencyGraph.addLink(statName, prop._id, prop.type);
|
||||
});
|
||||
if (prop.targetByTags) {
|
||||
// Tag targeted proficiencies depend on the creature's proficiencyBonus,
|
||||
// since they add it directly to the targeted field
|
||||
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
|
||||
getEffectTagTargets(prop, computation).forEach(targetId => {
|
||||
const targetProp = computation.propsById[targetId];
|
||||
if (
|
||||
(targetProp.type === 'attribute' || targetProp.type === 'skill')
|
||||
&& targetProp.variableName
|
||||
&& !prop.targetField
|
||||
) {
|
||||
// If the field wasn't specified and we're targeting an attribute or
|
||||
// skill, just treat it like a normal proficiency on its variable name
|
||||
dependencyGraph.addLink(targetProp.variableName, prop._id, 'proficiency');
|
||||
} else {
|
||||
// Otherwise target a field on that property
|
||||
const key = prop.targetField || getDefaultCalculationField(targetProp);
|
||||
const calcObj = get(targetProp, key);
|
||||
if (calcObj && calcObj.calculation) {
|
||||
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency');
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
prop.stats.forEach(statName => {
|
||||
if (!statName) return;
|
||||
dependencyGraph.addLink(statName, prop._id, 'proficiency');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function linkSavingThrow(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'dc' });
|
||||
}
|
||||
|
||||
function linkSkill(dependencyGraph, prop) {
|
||||
function linkSkill(dependencyGraph, prop, computation) {
|
||||
// Depends on base value
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
|
||||
// Link dependents
|
||||
@@ -293,6 +317,20 @@ function linkSkill(dependencyGraph, prop) {
|
||||
}
|
||||
// Skills depend on the creature's proficiencyBonus
|
||||
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
|
||||
|
||||
// Skills can apply their value as a proficiency bonus to calculations based on tag
|
||||
if (prop.targetByTags) {
|
||||
getEffectTagTargets(prop, computation).forEach(targetId => {
|
||||
const targetProp = computation.propsById[targetId];
|
||||
// Always target a field on the target property, applying a skill to an attribute or
|
||||
// other skill isn't supported
|
||||
const key = prop.targetField || getDefaultCalculationField(targetProp);
|
||||
const calcObj = get(targetProp, key);
|
||||
if (calcObj && calcObj.calculation) {
|
||||
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function linkSlot(dependencyGraph, prop) {
|
||||
|
||||
@@ -5,14 +5,14 @@ import { get, unset } from 'lodash';
|
||||
import errorNode from '/imports/parser/parseTree/error.js';
|
||||
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
|
||||
|
||||
export default function parseCalculationFields(prop, schemas){
|
||||
export default function parseCalculationFields(prop, schemas) {
|
||||
discoverInlineCalculationFields(prop, schemas);
|
||||
parseAllCalculationFields(prop, schemas);
|
||||
}
|
||||
|
||||
function discoverInlineCalculationFields(prop, schemas){
|
||||
function discoverInlineCalculationFields(prop, schemas) {
|
||||
// For each key in the schema
|
||||
schemas[prop.type].inlineCalculationFields().forEach( calcKey => {
|
||||
schemas[prop.type]?.inlineCalculationFields?.()?.forEach(calcKey => {
|
||||
// That ends in .inlineCalculations
|
||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||
const inlineCalcObj = get(prop, key);
|
||||
@@ -22,7 +22,7 @@ function discoverInlineCalculationFields(prop, schemas){
|
||||
// Extract the calculations and store them on the property
|
||||
let string = inlineCalcObj.text;
|
||||
// If there is no text, delete the whole field
|
||||
if (!string){
|
||||
if (!string) {
|
||||
unset(prop, calcKey);
|
||||
return;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ function discoverInlineCalculationFields(prop, schemas){
|
||||
|
||||
// Has the text, if it matches the existing hash, stop
|
||||
const inlineCalcHash = cyrb53(inlineCalcObj.text);
|
||||
if (inlineCalcHash === inlineCalcObj.hash){
|
||||
if (inlineCalcHash === inlineCalcObj.hash) {
|
||||
return;
|
||||
}
|
||||
inlineCalcObj.hash = inlineCalcHash;
|
||||
@@ -41,7 +41,7 @@ function discoverInlineCalculationFields(prop, schemas){
|
||||
// It will be re set including the embedded calculation at the end of
|
||||
// the computation
|
||||
let matches = string.matchAll(INLINE_CALCULATION_REGEX);
|
||||
for (let match of matches){
|
||||
for (let match of matches) {
|
||||
let calculation = match[1];
|
||||
inlineCalcObj.inlineCalculations.push({
|
||||
calculation,
|
||||
@@ -51,9 +51,9 @@ function discoverInlineCalculationFields(prop, schemas){
|
||||
});
|
||||
}
|
||||
|
||||
function parseAllCalculationFields(prop, schemas){
|
||||
function parseAllCalculationFields(prop, schemas) {
|
||||
// For each computed key in the schema
|
||||
schemas[prop.type].computedFields().forEach( calcKey => {
|
||||
schemas[prop.type]?.computedFields?.()?.forEach(calcKey => {
|
||||
// Determine the level the calculation should compute down to
|
||||
let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel || 'reduce';
|
||||
|
||||
@@ -66,7 +66,7 @@ function parseAllCalculationFields(prop, schemas){
|
||||
const calcObj = get(prop, key);
|
||||
if (!calcObj) return;
|
||||
// Delete the whole calculation object if the calculation string isn't set
|
||||
if (!calcObj.calculation){
|
||||
if (!calcObj.calculation) {
|
||||
unset(prop, calcKey);
|
||||
return;
|
||||
}
|
||||
@@ -84,10 +84,10 @@ function parseAllCalculationFields(prop, schemas){
|
||||
});
|
||||
}
|
||||
|
||||
function parseCalculation(calcObj){
|
||||
function parseCalculation(calcObj) {
|
||||
const calcHash = cyrb53(calcObj.calculation);
|
||||
// If the cached parse calculation is equal to the calculation, skip
|
||||
if (calcHash === calcObj.hash){
|
||||
if (calcHash === calcObj.hash) {
|
||||
return;
|
||||
}
|
||||
calcObj.hash = calcHash;
|
||||
@@ -100,6 +100,6 @@ function parseCalculation(calcObj){
|
||||
message: prettifyParseError(e),
|
||||
};
|
||||
calcObj.parseError = error;
|
||||
calcObj.parseNode = errorNode.create({error});
|
||||
calcObj.parseNode = errorNode.create({ error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import applyFnToKey from '../utility/applyFnToKey.js';
|
||||
import { unset } from 'lodash';
|
||||
|
||||
export default function removeSchemaFields(schemas, prop){
|
||||
export default function removeSchemaFields(schemas, prop) {
|
||||
schemas.forEach(schema => {
|
||||
schema.removeBeforeComputeFields().forEach(
|
||||
schema?.removeBeforeComputeFields?.().forEach(
|
||||
key => applyFnToKey(prop, key, unset)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { buildComputationFromProps } from '/imports/api/engine/computation/build
|
||||
import { assert } from 'chai';
|
||||
import clean from '../../utility/cleanProp.testFn.js';
|
||||
|
||||
export default function(){
|
||||
export default function () {
|
||||
const computation = buildComputationFromProps(testProperties);
|
||||
const totalFilled = computation.propsById['slotId'].totalFilled;
|
||||
assert.equal(totalFilled, 4);
|
||||
@@ -13,24 +13,24 @@ var testProperties = [
|
||||
clean({
|
||||
_id: 'slotId',
|
||||
type: 'propertySlot',
|
||||
ancestors: [{id: 'charId'}],
|
||||
ancestors: [{ id: 'charId' }],
|
||||
}),
|
||||
// Children
|
||||
clean({
|
||||
_id: 'slotFillerId',
|
||||
type: 'slotFiller',
|
||||
type: 'folder',
|
||||
slotQuantityFilled: 3,
|
||||
slotFillerType: 'item',
|
||||
ancestors: [{id: 'charId'}, {id: 'slotId'}],
|
||||
ancestors: [{ id: 'charId' }, { id: 'slotId' }],
|
||||
}),
|
||||
clean({
|
||||
_id: 'slotChildId',
|
||||
type: 'item',
|
||||
ancestors: [{id: 'charId'}, {id: 'slotId'}],
|
||||
ancestors: [{ id: 'charId' }, { id: 'slotId' }],
|
||||
}),
|
||||
clean({
|
||||
_id: 'slotGrandchildId',
|
||||
type: 'effect',
|
||||
ancestors: [{id: 'charId'}, {id: 'slotId'}, {id: 'slotChildId'}],
|
||||
ancestors: [{ id: 'charId' }, { id: 'slotId' }, { id: 'slotChildId' }],
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -29,7 +29,7 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js';
|
||||
* computed toggles
|
||||
*/
|
||||
|
||||
export default function buildCreatureComputation(creatureId){
|
||||
export default function buildCreatureComputation(creatureId) {
|
||||
const creature = getCreature(creatureId);
|
||||
const variables = getVariables(creatureId);
|
||||
const properties = getProperties(creatureId);
|
||||
@@ -37,7 +37,7 @@ export default function buildCreatureComputation(creatureId){
|
||||
return computation;
|
||||
}
|
||||
|
||||
export function buildComputationFromProps(properties, creature, variables){
|
||||
export function buildComputationFromProps(properties, creature, variables) {
|
||||
|
||||
const computation = new CreatureComputation(properties, creature, variables);
|
||||
// Dependency graph where edge(a, b) means a depends on b
|
||||
@@ -49,14 +49,14 @@ export function buildComputationFromProps(properties, creature, variables){
|
||||
const dependencyGraph = computation.dependencyGraph;
|
||||
|
||||
// Link the denormalizedStats from the creature
|
||||
if (creature && creature.denormalizedStats){
|
||||
if (creature.denormalizedStats.xp){
|
||||
if (creature && creature.denormalizedStats) {
|
||||
if (creature.denormalizedStats.xp) {
|
||||
dependencyGraph.addNode('xp', {
|
||||
baseValue: creature.denormalizedStats.xp,
|
||||
type: '_variable'
|
||||
});
|
||||
}
|
||||
if (creature.denormalizedStats.milestoneLevels){
|
||||
if (creature.denormalizedStats.milestoneLevels) {
|
||||
dependencyGraph.addNode('milestoneLevels', {
|
||||
baseValue: creature.denormalizedStats.milestoneLevels,
|
||||
type: '_variable'
|
||||
@@ -93,7 +93,7 @@ export function buildComputationFromProps(properties, creature, variables){
|
||||
// Inactive status must be complete for the whole tree before toggle deps
|
||||
// are calculated
|
||||
walkDown(forest, node => {
|
||||
computeToggleDependencies(node, dependencyGraph);
|
||||
computeToggleDependencies(node, dependencyGraph, computation, forest);
|
||||
computeSlotQuantityFilled(node, dependencyGraph);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import pointBuy from './computeByType/computePointBuy.js';
|
||||
import propertySlot from './computeByType/computeSlot.js';
|
||||
import container from './computeByType/computeContainer.js';
|
||||
import spellList from './computeByType/computeSpellList.js';
|
||||
import toggle from './computeByType/computeToggle.js';
|
||||
import _calculation from './computeByType/computeCalculation.js';
|
||||
|
||||
export default Object.freeze({
|
||||
@@ -19,4 +20,5 @@ export default Object.freeze({
|
||||
propertySlot,
|
||||
spell: action,
|
||||
spellList,
|
||||
toggle,
|
||||
});
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import evaluateCalculation from '../../utility/evaluateCalculation.js';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
|
||||
export default function computeCalculation(computation, node){
|
||||
export default function computeCalculation(computation, node) {
|
||||
const calcObj = node.data;
|
||||
evaluateCalculation(calcObj, computation.scope);
|
||||
if (calcObj.effects || calcObj.proficiencies) {
|
||||
calcObj.baseValue = calcObj.value;
|
||||
}
|
||||
aggregateCalculationEffects(node, computation);
|
||||
aggregateCalculationProficiencies(node, computation);
|
||||
}
|
||||
|
||||
export function aggregateCalculationEffects(node, computation){
|
||||
function aggregateCalculationEffects(node, computation) {
|
||||
const calcObj = node.data;
|
||||
delete calcObj.effects;
|
||||
computation.dependencyGraph.forEachLinkedNode(
|
||||
@@ -34,15 +39,77 @@ export function aggregateCalculationEffects(node, computation){
|
||||
},
|
||||
true // enumerate only outbound links
|
||||
);
|
||||
if (calcObj.effects && typeof calcObj.value === 'number'){
|
||||
calcObj.baseValue = calcObj.value;
|
||||
if (calcObj.effects && typeof calcObj.value === 'number') {
|
||||
calcObj.effects.forEach(effect => {
|
||||
if (
|
||||
effect.operation === 'add' &&
|
||||
effect.amount && typeof effect.amount.value === 'number'
|
||||
){
|
||||
) {
|
||||
calcObj.value += effect.amount.value
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function aggregateCalculationProficiencies(node, computation) {
|
||||
const calcObj = node.data;
|
||||
delete calcObj.proficiencies;
|
||||
delete calcObj.proficiency;
|
||||
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
|
||||
|
||||
// Go through all the links and collect them on the calculation
|
||||
computation.dependencyGraph.forEachLinkedNode(
|
||||
node.id,
|
||||
(linkedNode, link) => {
|
||||
// Only proficiency links
|
||||
if (link.data !== 'proficiency') return;
|
||||
// That have data
|
||||
if (!linkedNode.data) return;
|
||||
// Ignoring inactive props
|
||||
if (linkedNode.data.inactive) return;
|
||||
// Compute the proficiency and value
|
||||
let proficiency, value;
|
||||
if (linkedNode.data.type === 'proficiency') {
|
||||
proficiency = linkedNode.data.value || 0;
|
||||
// Multiply the proficiency bonus by the actual proficiency
|
||||
if (proficiency === 0.49) {
|
||||
// Round down proficiency bonus in the special case
|
||||
value = Math.floor(profBonus * 0.5);
|
||||
} else {
|
||||
value = Math.ceil(profBonus * proficiency);
|
||||
}
|
||||
} else if (linkedNode.data.type === 'skill') {
|
||||
value = linkedNode.data.value || 0;
|
||||
proficiency = linkedNode.data.proficiency || 0;
|
||||
}
|
||||
// Collate proficiencies
|
||||
calcObj.proficiencies = calcObj.proficiencies || [];
|
||||
calcObj.proficiencies.push({
|
||||
_id: linkedNode.data._id,
|
||||
name: linkedNode.data.name,
|
||||
type: linkedNode.data.type,
|
||||
proficiency,
|
||||
value,
|
||||
});
|
||||
},
|
||||
true // enumerate only outbound links
|
||||
);
|
||||
|
||||
// Apply the highest proficiency, marking all others as overridden
|
||||
if (calcObj.proficiencies && typeof calcObj.value === 'number') {
|
||||
calcObj.proficiency = 0;
|
||||
calcObj.proficiencyBonus = 0;
|
||||
let currentProf;
|
||||
calcObj.proficiencies.forEach(prof => {
|
||||
if (prof.value > calcObj.proficiencyBonus) {
|
||||
if (currentProf) currentProf.overridden = true;
|
||||
calcObj.proficiencyBonus = prof.value;
|
||||
calcObj.proficiency = prof.proficiency;
|
||||
currentProf = prof;
|
||||
} else {
|
||||
prof.overridden = true;
|
||||
}
|
||||
});
|
||||
calcObj.value += calcObj.proficiencyBonus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default function computSlot(computation, node){
|
||||
export default function computeSlot(computation, node) {
|
||||
const prop = node.data;
|
||||
if (prop.quantityExpected && prop.quantityExpected.value){
|
||||
if (prop.quantityExpected && prop.quantityExpected.value) {
|
||||
prop.spaceLeft = prop.quantityExpected.value - prop.totalFilled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function computeToggle(computation, node) {
|
||||
const prop = node.data;
|
||||
if (!prop.enabled && !prop.disabled && prop.condition && !prop.condition.value) {
|
||||
prop.inactive = true;
|
||||
prop.deactivatedBySelf = true;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,40 @@
|
||||
import getAggregatorResult from './getAggregatorResult.js';
|
||||
|
||||
export default function computeVariableAsAttribute(computation, node, prop){
|
||||
export default function computeVariableAsAttribute(computation, node, prop) {
|
||||
let result = getAggregatorResult(node) || 0;
|
||||
|
||||
prop.total = result;
|
||||
|
||||
// Apply damage in a way that respects the damage rules, modifying damage if need be
|
||||
// Bound the damage
|
||||
if (!prop.ignoreLowerLimit && prop.damage > prop.total) {
|
||||
console.log(`reducing damage from ${prop.damage} to ${prop.total}`);
|
||||
prop.damage = prop.total;
|
||||
}
|
||||
if (!prop.ignoreUpperLimit && prop.damage < 0) {
|
||||
console.log(`increasing damage from ${prop.damage} to 0`);
|
||||
prop.damage = 0;
|
||||
}
|
||||
// Apply damage
|
||||
prop.value = prop.total - (prop.damage || 0);
|
||||
|
||||
// Proficiency
|
||||
prop.proficiency = node.data.proficiency;
|
||||
|
||||
// Advantage/disadvantage
|
||||
const aggregator = node.data.effectAggregator;
|
||||
if (aggregator) {
|
||||
if (aggregator.advantage && !aggregator.disadvantage) {
|
||||
prop.advantage = 1;
|
||||
} else if (aggregator.disadvantage && !aggregator.advantage) {
|
||||
prop.advantage = -1;
|
||||
} else {
|
||||
prop.advantage = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Ability scores get modifiers
|
||||
if (prop.attributeType === 'ability'){
|
||||
if (prop.attributeType === 'ability') {
|
||||
prop.modifier = Math.floor((prop.value - 10) / 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import aggregate from './aggregate/index.js';
|
||||
|
||||
export default function computeVariableAsSkill(computation, node, prop){
|
||||
export default function computeVariableAsSkill(computation, node, prop) {
|
||||
// Skills are based on some ability Modifier
|
||||
let ability = computation.scope[prop.ability];
|
||||
prop.abilityMod = ability?.modifier || 0;
|
||||
|
||||
// Inherit the ability's skill effects and proficiencies if skill is not a save
|
||||
if (prop.skillType !== 'save' && ability){
|
||||
if (prop.skillType !== 'save' && ability) {
|
||||
aggregateAbilityEffects({
|
||||
computation,
|
||||
skillNode: node,
|
||||
@@ -21,7 +21,7 @@ export default function computeVariableAsSkill(computation, node, prop){
|
||||
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
|
||||
|
||||
// Multiply the proficiency bonus by the actual proficiency
|
||||
if(prop.proficiency === 0.49){
|
||||
if (prop.proficiency === 0.49) {
|
||||
// Round down proficiency bonus in the special case
|
||||
profBonus = Math.floor(profBonus * 0.5);
|
||||
} else {
|
||||
@@ -37,7 +37,7 @@ export default function computeVariableAsSkill(computation, node, prop){
|
||||
prop.effects = node.data.effects;
|
||||
|
||||
// If there is no aggregator, determine if the prop can hide, then exit
|
||||
if (!aggregator){
|
||||
if (!aggregator) {
|
||||
prop.hide = statBase === undefined &&
|
||||
prop.proficiency == 0 ||
|
||||
undefined;
|
||||
@@ -52,20 +52,32 @@ export default function computeVariableAsSkill(computation, node, prop){
|
||||
if (aggregator.set !== undefined) {
|
||||
result = aggregator.set;
|
||||
}
|
||||
if (Number.isFinite(result)){
|
||||
if (Number.isFinite(result)) {
|
||||
result = Math.floor(result);
|
||||
}
|
||||
prop.value = result;
|
||||
// Advantage/disadvantage
|
||||
if (aggregator.advantage && !aggregator.disadvantage){
|
||||
if (aggregator.advantage && !aggregator.disadvantage) {
|
||||
prop.advantage = 1;
|
||||
} else if (aggregator.disadvantage && !aggregator.advantage){
|
||||
} else if (aggregator.disadvantage && !aggregator.advantage) {
|
||||
prop.advantage = -1;
|
||||
} else {
|
||||
prop.advantage = 0;
|
||||
}
|
||||
// Passive bonus
|
||||
prop.passiveBonus = aggregator.passiveAdd;
|
||||
// +/- 5 to passive bonus if the skill has advantage/disadvantage
|
||||
if (
|
||||
prop.advantage === 1
|
||||
&& Number.isFinite(prop.passiveBonus)
|
||||
) {
|
||||
prop.passiveBonus += 5;
|
||||
} else if (
|
||||
prop.advantage === -1
|
||||
&& Number.isFinite(prop.passiveBonus)
|
||||
) {
|
||||
prop.bassiveBonus -= 5;
|
||||
}
|
||||
// conditional benefits
|
||||
prop.conditionalBenefits = aggregator.conditional;
|
||||
// Roll bonuses
|
||||
@@ -76,7 +88,8 @@ export default function computeVariableAsSkill(computation, node, prop){
|
||||
prop.rollBonuses = aggregator.rollBonus;
|
||||
}
|
||||
|
||||
function aggregateAbilityEffects({computation, skillNode, abilityNode}){
|
||||
function aggregateAbilityEffects({ computation, skillNode, abilityNode }) {
|
||||
if (!abilityNode?.id) return;
|
||||
computation.dependencyGraph.forEachLinkedNode(
|
||||
abilityNode.id,
|
||||
(linkedNode, link) => {
|
||||
@@ -85,15 +98,15 @@ function aggregateAbilityEffects({computation, skillNode, abilityNode}){
|
||||
if (linkedNode.data.inactive) return;
|
||||
// Check that the link is a valid effect/proficiency to pass on
|
||||
// to a skill from its ability
|
||||
if (link.data === 'effect'){
|
||||
if (link.data === 'effect') {
|
||||
if (![
|
||||
'advantage', 'disadvantage', 'passiveAdd', 'fail', 'conditional'
|
||||
].includes(linkedNode.data.operation)){
|
||||
].includes(linkedNode.data.operation)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Apply the aggregations
|
||||
let arg = {node: skillNode, linkedNode, link};
|
||||
let arg = { node: skillNode, linkedNode, link };
|
||||
aggregate.effect(arg);
|
||||
aggregate.proficiency(arg);
|
||||
},
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
export default function evaluateToggles(computation, node){
|
||||
export default function evaluateToggles(computation, node) {
|
||||
let prop = node.data;
|
||||
if (!prop) return;
|
||||
let toggles = prop._computationDetails?.toggleAncestors;
|
||||
if (!toggles) return;
|
||||
toggles.forEach(toggle => {
|
||||
if (!toggle.condition) return;
|
||||
if (!toggle.condition.value){
|
||||
if (
|
||||
(!toggle.enabled && !toggle.disabled && toggle.condition && !toggle.condition.value)
|
||||
|| (toggle.disabled)
|
||||
) {
|
||||
prop.inactive = true;
|
||||
prop.deactivatedByToggle = true;
|
||||
prop.deactivatingToggleId = toggle._id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js';
|
||||
import { assert } from 'chai';
|
||||
import computeCreatureComputation from '../../computeCreatureComputation.js';
|
||||
import clean from '../../utility/cleanProp.testFn.js';
|
||||
|
||||
export default function () {
|
||||
const computation = buildComputationFromProps(testProperties);
|
||||
computeCreatureComputation(computation);
|
||||
const prop = id => computation.propsById[id];
|
||||
assert.equal(
|
||||
prop('strengthId').value, 8,
|
||||
'The proficiency bonus should not change the strength score'
|
||||
);
|
||||
assert.equal(
|
||||
prop('strengthId').modifier, -1,
|
||||
'The proficiency bonus should not change the strength modifier'
|
||||
);
|
||||
assert.exists(prop('actionId').attackRoll.proficiencies, 'The proficiency aggregator should be here')
|
||||
assert.exists(prop('actionId').attackRoll.proficiencies[0], 'The proficiency should be here')
|
||||
// attack roll = strength.mod + proficiencyBonus/2 rounded down
|
||||
// = -1 + 13/2 = -1 + 6 = 5
|
||||
assert.equal(
|
||||
prop('actionId').attackRoll.value, 5,
|
||||
'The proficiency should apply correctly to modify the attack roll'
|
||||
);
|
||||
}
|
||||
|
||||
var testProperties = [
|
||||
clean({
|
||||
_id: 'strengthId',
|
||||
variableName: 'strength',
|
||||
type: 'attribute',
|
||||
attributeType: 'ability',
|
||||
baseValue: {
|
||||
calculation: '8'
|
||||
},
|
||||
}),
|
||||
clean({
|
||||
_id: 'actionId',
|
||||
type: 'action',
|
||||
ancestors: [{ id: 'charId' }],
|
||||
attackRoll: {
|
||||
calculation: 'strength.modifier',
|
||||
},
|
||||
tags: ['rapier', 'martial weapon', 'weapon', 'attack']
|
||||
}),
|
||||
clean({
|
||||
_id: 'profBonusId',
|
||||
type: 'attribute',
|
||||
variableName: 'proficiencyBonus',
|
||||
ancestors: [{ id: 'charId' }],
|
||||
baseValue: {
|
||||
calculation: '13'
|
||||
},
|
||||
}),
|
||||
clean({
|
||||
_id: 'tagTargetedProficiency',
|
||||
type: 'proficiency',
|
||||
stats: ['strength'], // Should be ignored, we are targeting by tags
|
||||
value: 0.49,
|
||||
targetByTags: true,
|
||||
targetTags: ['martial weapon']
|
||||
}),
|
||||
];
|
||||
@@ -6,29 +6,33 @@ import computeInventory from './computeInventory.testFn.js';
|
||||
import computeDamageMultipliers from './computeDamageMultipliers.testFn.js';
|
||||
import computeEffects from './computeEffects.testFn.js';
|
||||
import computeSkills from './computeSkills.testFn.js';
|
||||
import computeProficiencies from './computeProficiencies.testFn.js';
|
||||
|
||||
export default [{
|
||||
text: 'Computes actions',
|
||||
fn: computeAction,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes attributes',
|
||||
fn: computeAttribute,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes classes',
|
||||
fn: computeClasses,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes constants',
|
||||
fn: computeConstants,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes inventory',
|
||||
fn: computeInventory,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes damage multipliers',
|
||||
fn: computeDamageMultipliers,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes effects',
|
||||
fn: computeEffects,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes skills',
|
||||
fn: computeSkills,
|
||||
}, {
|
||||
text: 'Computes proficiencies',
|
||||
fn: computeProficiencies,
|
||||
}];
|
||||
|
||||
@@ -3,12 +3,12 @@ import { EJSON } from 'meteor/ejson';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
|
||||
|
||||
export default function writeAlteredProperties(computation){
|
||||
export default function writeAlteredProperties(computation) {
|
||||
let bulkWriteOperations = [];
|
||||
// Loop through all properties on the memo
|
||||
computation.props.forEach(changed => {
|
||||
let schema = propertySchemasIndex[changed.type];
|
||||
if (!schema){
|
||||
if (!schema) {
|
||||
console.warn('No schema for ' + changed.type);
|
||||
return;
|
||||
}
|
||||
@@ -20,12 +20,13 @@ export default function writeAlteredProperties(computation){
|
||||
'deactivatedBySelf',
|
||||
'deactivatedByAncestor',
|
||||
'deactivatedByToggle',
|
||||
'deactivatingToggleId',
|
||||
'damage',
|
||||
'dirty',
|
||||
...schema.objectKeys(),
|
||||
];
|
||||
op = addChangedKeysToOp(op, keys, original, changed);
|
||||
if (op){
|
||||
if (op) {
|
||||
bulkWriteOperations.push(op);
|
||||
}
|
||||
});
|
||||
@@ -37,10 +38,10 @@ function addChangedKeysToOp(op, keys, original, changed) {
|
||||
// Loop through all keys that can be changed by computation
|
||||
// and compile an operation that sets all those keys
|
||||
for (let key of keys) {
|
||||
if (!EJSON.equals(original[key], changed[key])){
|
||||
if (!EJSON.equals(original[key], changed[key])) {
|
||||
if (!op) op = newOperation(original._id, changed.type);
|
||||
let value = changed[key];
|
||||
if (value === undefined){
|
||||
if (value === undefined) {
|
||||
// Unset values that become undefined
|
||||
addUnsetOp(op, key);
|
||||
} else {
|
||||
@@ -52,32 +53,32 @@ function addChangedKeysToOp(op, keys, original, changed) {
|
||||
return op;
|
||||
}
|
||||
|
||||
function newOperation(_id, type){
|
||||
function newOperation(_id, type) {
|
||||
let newOp = {
|
||||
updateOne: {
|
||||
filter: {_id},
|
||||
filter: { _id },
|
||||
update: {},
|
||||
}
|
||||
};
|
||||
if (Meteor.isClient){
|
||||
if (Meteor.isClient) {
|
||||
newOp.type = type;
|
||||
}
|
||||
return newOp;
|
||||
}
|
||||
|
||||
function addSetOp(op, key, value){
|
||||
if (op.updateOne.update.$set){
|
||||
function addSetOp(op, key, value) {
|
||||
if (op.updateOne.update.$set) {
|
||||
op.updateOne.update.$set[key] = value;
|
||||
} else {
|
||||
op.updateOne.update.$set = {[key]: value};
|
||||
op.updateOne.update.$set = { [key]: value };
|
||||
}
|
||||
}
|
||||
|
||||
function addUnsetOp(op, key){
|
||||
if (op.updateOne.update.$unset){
|
||||
function addUnsetOp(op, key) {
|
||||
if (op.updateOne.update.$unset) {
|
||||
op.updateOne.update.$unset[key] = 1;
|
||||
} else {
|
||||
op.updateOne.update.$unset = {[key]: 1};
|
||||
op.updateOne.update.$unset = { [key]: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,14 +101,14 @@ function writePropertiesSequentially(bulkWriteOps) {
|
||||
// in the UI because of incompatibility with latency compensation. If the
|
||||
// duplicate redraws can be fixed, this is a strictly better way of processing
|
||||
// writes
|
||||
function bulkWriteProperties(bulkWriteOps){
|
||||
function bulkWriteProperties(bulkWriteOps) {
|
||||
if (!bulkWriteOps.length) return;
|
||||
// bulkWrite is only available on the server
|
||||
if (Meteor.isServer) {
|
||||
CreatureProperties.rawCollection().bulkWrite(
|
||||
bulkWriteOps,
|
||||
{ordered : false},
|
||||
function(e){
|
||||
{ ordered: false },
|
||||
function (e) {
|
||||
if (e) {
|
||||
console.error('Bulk write failed: ');
|
||||
console.error(e);
|
||||
|
||||
@@ -15,18 +15,21 @@ export default function writeScope(creatureId, computation) {
|
||||
|
||||
let $set, $unset;
|
||||
|
||||
for (const key in scope){
|
||||
for (const key in scope) {
|
||||
// Mongo can't handle keys that start with a dollar sign
|
||||
if (key[0] === '$' || key[0] === '_') continue;
|
||||
|
||||
// Remove large properties that aren't likely to be accessed
|
||||
delete scope[key].parent;
|
||||
delete scope[key].ancestors;
|
||||
|
||||
|
||||
// Remove empty keys
|
||||
for (const subKey in scope[key]) {
|
||||
if (scope[key][subKey] === undefined) {
|
||||
delete scope[key][subKey]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Only update changed fields
|
||||
if (!EJSON.equals(variables[key], scope[key])) {
|
||||
if (!$set) $set = {};
|
||||
@@ -53,9 +56,19 @@ export default function writeScope(creatureId, computation) {
|
||||
const update = {};
|
||||
if ($set) update.$set = $set;
|
||||
if ($unset) update.$unset = $unset;
|
||||
CreatureVariables.update({_creatureId: creatureId}, update);
|
||||
CreatureVariables.update({ _creatureId: creatureId }, update);
|
||||
}
|
||||
if (computation.creature?.dirty) {
|
||||
Creatures.update({_id: creatureId}, {$unset: { dirty: 1 }});
|
||||
Creatures.update({ _id: creatureId }, { $unset: { dirty: 1 } });
|
||||
}
|
||||
}
|
||||
/*
|
||||
function calculateSize(computation) {
|
||||
const sizeEstimator = {
|
||||
creature: computation.creature,
|
||||
variables: computation.variables,
|
||||
props: computation.originalPropsById,
|
||||
};
|
||||
return MongoInternals.NpmModule.BSON.calculateObjectSize(sizeEstimator, { checkKeys: false })
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@ import writeAlteredProperties from './computation/writeComputation/writeAlteredP
|
||||
import writeScope from './computation/writeComputation/writeScope.js';
|
||||
import writeErrors from './computation/writeComputation/writeErrors.js';
|
||||
|
||||
export default function computeCreature(creatureId){
|
||||
export default function computeCreature(creatureId) {
|
||||
if (Meteor.isClient) return;
|
||||
// console.log('compute ' + creatureId);
|
||||
const computation = buildCreatureComputation(creatureId);
|
||||
@@ -16,7 +16,7 @@ function computeComputation(computation, creatureId) {
|
||||
computeCreatureComputation(computation);
|
||||
writeAlteredProperties(computation);
|
||||
writeScope(creatureId, computation);
|
||||
} catch (e){
|
||||
} catch (e) {
|
||||
const errorText = e.reason || e.message || e.toString();
|
||||
computation.errors.push({
|
||||
type: 'crash',
|
||||
@@ -32,6 +32,19 @@ function computeComputation(computation, creatureId) {
|
||||
console.error(logError);
|
||||
throw e;
|
||||
} finally {
|
||||
checkPropertyCount(computation)
|
||||
writeErrors(creatureId, computation.errors);
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_PROPS = 1000;
|
||||
function checkPropertyCount(computation) {
|
||||
const count = computation.props.length;
|
||||
if (count <= MAX_PROPS) return;
|
||||
computation.errors.push({
|
||||
type: 'warning',
|
||||
details: {
|
||||
error: `This character sheet has too many properties and may perform poorly ( ${count} / ${MAX_PROPS} )`
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user