Merge branch 'version-2-dev' into version-2-tabletop

This commit is contained in:
Stefan Zermatten
2022-11-23 16:00:31 +02:00
421 changed files with 8529 additions and 3323 deletions

View File

@@ -5,8 +5,9 @@ import applyProperty from '../applyProperty.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.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/ui/utility/numberToSignedString.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';
export default function applyAction(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
@@ -16,7 +17,7 @@ export default function applyAction(node, actionContext) {
// Log the name and summary
let content = { name: prop.name };
if (prop.summary?.text){
if (prop.summary?.text) {
recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value;
}
@@ -29,24 +30,27 @@ export default function applyAction(node, actionContext) {
const attack = prop.attackRoll || prop.attackRollBonus;
// Attack if there is an attack roll
if (attack && attack.calculation){
if (targets.length){
if (attack && attack.calculation) {
if (targets.length) {
targets.forEach(target => {
applyAttackToTarget({attack, target, actionContext});
applyAttackToTarget({ attack, target, actionContext });
// Apply the children, but only to the current target
actionContext.targets = [target];
applyChildren(node, actionContext);
});
} else {
applyAttackWithoutTarget({attack, actionContext});
applyAttackWithoutTarget({ attack, actionContext });
applyChildren(node, actionContext);
}
} else {
applyChildren(node, actionContext);
}
if (prop.actionType === 'event' && prop.variableName) {
resetProperties(actionContext.creature._id, prop.variableName, actionContext);
}
}
function applyAttackWithoutTarget({attack, actionContext}){
function applyAttackWithoutTarget({ attack, actionContext }) {
delete actionContext.scope['$attackHit'];
delete actionContext.scope['$attackMiss'];
delete actionContext.scope['$criticalHit'];
@@ -62,16 +66,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'] === 1) {
name += ' (Advantage)';
} else if(scope['$attackAdvantage'] === -1){
} else if (scope['$attackAdvantage'] === -1) {
name += ' (Disadvantage)';
}
if (!criticalMiss){
scope['$attackHit'] = {value: true}
if (!criticalMiss) {
scope['$attackHit'] = { value: true }
}
if (!criticalHit){
scope['$attackMiss'] = {value: true};
if (!criticalHit) {
scope['$attackMiss'] = { value: true };
}
actionContext.addLog({
@@ -81,7 +85,7 @@ function applyAttackWithoutTarget({attack, actionContext}){
});
}
function applyAttackToTarget({attack, target, actionContext}){
function applyAttackToTarget({ attack, target, actionContext }) {
const scope = actionContext.scope;
delete scope['$attackHit'];
delete scope['$attackMiss'];
@@ -99,15 +103,15 @@ function applyAttackToTarget({attack, target, actionContext}){
criticalMiss,
} = rollAttack(attack, scope);
if (target.variables.armor){
if (target.variables.armor) {
const armor = target.variables.armor.value;
let name = criticalHit ? 'Critical Hit!' :
criticalMiss ? 'Critical Miss!' :
result > armor ? 'Hit!' : 'Miss!';
if (scope['$attackAdvantage'] === 1){
result > armor ? 'Hit!' : 'Miss!';
if (scope['$attackAdvantage'] === 1) {
name += ' (Advantage)';
} else if(scope['$attackAdvantage'] === -1){
} else if (scope['$attackAdvantage'] === -1) {
name += ' (Disadvantage)';
}
@@ -116,15 +120,15 @@ function applyAttackToTarget({attack, target, actionContext}){
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
if (criticalMiss || result < armor){
scope['$attackMiss'] = {value: true};
if (criticalMiss || result < armor) {
scope['$attackMiss'] = { value: true };
} else {
scope['$attackHit'] = {value: true};
scope['$attackHit'] = { value: true };
}
} else {
actionContext.addLog({
name: 'Error',
value:'Target has no `armor`',
value: 'Target has no `armor`',
});
actionContext.addLog({
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
@@ -134,10 +138,10 @@ function applyAttackToTarget({attack, target, actionContext}){
}
}
function rollAttack(attack, scope){
function rollAttack(attack, scope) {
const rollModifierText = numberToSignedString(attack.value, true);
let value, resultPrefix;
if (scope['$attackAdvantage'] === 1){
if (scope['$attackAdvantage'] === 1) {
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
@@ -146,7 +150,7 @@ function rollAttack(attack, scope){
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (scope['$attackAdvantage'] === -1){
} else if (scope['$attackAdvantage'] === -1) {
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
@@ -159,25 +163,26 @@ function rollAttack(attack, scope){
value = rollDice(1, 20)[0];
resultPrefix = `1d20 [${value}] ${rollModifierText}`
}
scope['$attackRoll'] = {value};
scope['$attackDiceRoll'] = { value };
const result = value + attack.value;
const {criticalHit, criticalMiss} = applyCrits(value, scope);
return {resultPrefix, result, value, criticalHit, criticalMiss};
scope['$attackRoll'] = { result };
const { criticalHit, criticalMiss } = applyCrits(value, scope);
return { resultPrefix, result, value, criticalHit, criticalMiss };
}
function applyCrits(value, scope){
function applyCrits(value, scope) {
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
let criticalHit = value >= criticalHitTarget;
let criticalMiss;
if (criticalHit){
scope['$criticalHit'] = {value: true};
if (criticalHit) {
scope['$criticalHit'] = { value: true };
} else {
criticalMiss = value === 1;
if (criticalMiss){
scope['$criticalMiss'] = {value: true};
if (criticalMiss) {
scope['$criticalMiss'] = { value: true };
}
}
return {criticalHit, criticalMiss};
return { criticalHit, criticalMiss };
}
function applyChildren(node, actionContext) {
@@ -185,9 +190,9 @@ function applyChildren(node, actionContext) {
node.children.forEach(child => applyProperty(child, actionContext));
}
function spendResources(prop, actionContext){
function spendResources(prop, actionContext) {
// Check Uses
if (prop.usesLeft <= 0){
if (prop.usesLeft <= 0) {
if (!prop.silent) actionContext.addLog({
name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`,
@@ -195,7 +200,7 @@ function spendResources(prop, actionContext){
return true;
}
// Resources
if (prop.insufficientResources){
if (prop.insufficientResources) {
if (!prop.silent) actionContext.addLog({
name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action',
@@ -209,14 +214,14 @@ function spendResources(prop, actionContext){
try {
prop.resources.itemsConsumed.forEach(itemConsumed => {
recalculateCalculation(itemConsumed.quantity, actionContext);
if (!itemConsumed.itemId){
if (!itemConsumed.itemId) {
throw 'No ammo was selected for this prop';
}
let item = CreatureProperties.findOne(itemConsumed.itemId);
if (!item || item.ancestors[0].id !== prop.ancestors[0].id){
if (!item || item.ancestors[0].id !== prop.ancestors[0].id) {
throw 'The prop\'s ammo was not found on the creature';
}
if (!item.equipped){
if (!item.equipped) {
throw 'The selected ammo is not equipped';
}
if (
@@ -229,16 +234,16 @@ function spendResources(prop, actionContext){
value: itemConsumed.quantity.value,
});
let logName = item.name;
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1){
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1) {
logName = item.plural || logName;
}
if (itemConsumed.quantity.value > 0){
if (itemConsumed.quantity.value > 0) {
spendLog.push(logName + ': ' + itemConsumed.quantity.value);
} else if (itemConsumed.quantity.value < 0){
} else if (itemConsumed.quantity.value < 0) {
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
}
});
} catch (e){
} catch (e) {
actionContext.addLog({
name: 'Error',
value: e,
@@ -251,9 +256,9 @@ function spendResources(prop, actionContext){
itemQuantityAdjustments.forEach(adjustQuantityWork);
// Use uses
if (prop.usesLeft){
if (prop.usesLeft) {
CreatureProperties.update(prop._id, {
$inc: {usesUsed: 1}
$inc: { usesUsed: 1 }
}, {
selector: prop
});
@@ -270,7 +275,7 @@ function spendResources(prop, actionContext){
if (!attConsumed.quantity?.value) return;
let stat = actionContext.scope[attConsumed.variableName];
if (!stat){
if (!stat) {
spendLog.push(stat.name + ': ' + ' not found');
return;
}
@@ -280,9 +285,9 @@ function spendResources(prop, actionContext){
value: attConsumed.quantity.value,
actionContext,
});
if (attConsumed.quantity.value > 0){
if (attConsumed.quantity.value > 0) {
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
} else if (attConsumed.quantity.value < 0){
} else if (attConsumed.quantity.value < 0) {
gainLog.push(stat.name + ': ' + -attConsumed.quantity.value);
}
});

View File

@@ -14,6 +14,7 @@ import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
export default function applyBuff(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
@@ -46,12 +47,17 @@ export default function applyBuff(node, actionContext) {
copyNodeListToTarget(propList, target, oldParent);
//Log the buff
let logValue = prop.description?.value
if (prop.description?.text) {
recalculateInlineCalculations(prop.description, actionContext);
logValue = prop.description?.value;
}
if ((prop.name || prop.description?.value) && !prop.silent) {
if (target._id === actionContext.creature._id) {
// Targeting self
actionContext.addLog({
name: prop.name,
value: prop.description?.value,
value: logValue,
});
} else {
// Targeting other
@@ -60,7 +66,7 @@ export default function applyBuff(node, actionContext) {
creatureId: target._id,
content: [{
name: prop.name,
value: prop.description?.value,
value: logValue,
}],
}
});

View File

@@ -1,6 +1,6 @@
import { some, intersection, difference, remove, includes } from 'lodash';
import applyProperty from '../applyProperty.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import resolve, { Context, toString } from '/imports/parser/resolve.js';
import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
@@ -9,10 +9,11 @@ import {
getPropertiesOfType
} 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';
export default function applyDamage(node, actionContext){
export default function applyDamage(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const applyChildren = function(){
const applyChildren = function () {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
};
@@ -28,10 +29,10 @@ export default function applyDamage(node, actionContext){
// Determine if the hit is critical
let criticalHit = scope['$criticalHit']?.value &&
prop.damageType !== 'healing' // Can't critically heal
;
;
// Double the damage rolls if the hit is critical
let context = new Context({
options: {doubleRolls: criticalHit},
options: { doubleRolls: criticalHit },
});
// Gather all the lines we need to log into an array
@@ -40,8 +41,8 @@ export default function applyDamage(node, actionContext){
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
if (rolled.parseType !== 'constant'){
const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
}
logErrors(context.errors, actionContext);
@@ -50,13 +51,13 @@ export default function applyDamage(node, actionContext){
context.errors = [];
// Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context);
const { result: reduced } = resolve('reduce', rolled, scope, context);
logErrors(context.errors, actionContext);
// Store the result
if (reduced.parseType === 'constant'){
if (reduced.parseType === 'constant') {
prop.amount.value = reduced.value;
} else if (reduced.parseType === 'error'){
} else if (reduced.parseType === 'error') {
prop.amount.value = null;
} else {
prop.amount.value = toString(reduced);
@@ -64,7 +65,7 @@ export default function applyDamage(node, actionContext){
let damage = +reduced.value;
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)) {
return applyChildren();
}
@@ -83,7 +84,7 @@ export default function applyDamage(node, actionContext){
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage ': '');
(prop.damageType !== 'healing' ? ' damage ' : '');
if (damageTargets && damageTargets.length) {
// Iterate through all the targets
@@ -107,7 +108,7 @@ export default function applyDamage(node, actionContext){
});
// Log the damage done
if (target._id === actionContext.creature._id){
if (target._id === actionContext.creature._id) {
// Target is same as self, log damage as such
logValue.push(`**${damageDealt}** ${suffix} to self`);
} else {
@@ -135,33 +136,33 @@ export default function applyDamage(node, actionContext){
return applyChildren();
}
function applyDamageMultipliers({target, damage, damageProp, logValue}){
function applyDamageMultipliers({ target, damage, damageProp, logValue }) {
const damageType = damageProp?.damageType;
if (!damageType) return damage;
const multiplier = target?.variables?.[damageType];
if (!multiplier) return damage;
const damageTypeText = damageType == 'healing' ? 'healing': `${damageType} damage`;
const damageTypeText = damageType == 'healing' ? 'healing' : `${damageType} damage`;
if (
multiplier.immunity &&
some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity'))
){
) {
logValue.push(`Immune to ${damageTypeText}`);
return 0;
} else {
if (
multiplier.resistance &&
some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance'))
){
) {
logValue.push(`Resistant to ${damageTypeText}`);
damage = Math.floor(damage / 2);
}
if (
multiplier.vulnerability &&
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability'))
){
) {
logValue.push(`Vulnerable to ${damageTypeText}`);
damage = Math.floor(damage * 2);
}
@@ -169,24 +170,25 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
return damage;
}
function multiplierAppliesTo(damageProp, multiplierType){
function multiplierAppliesTo(damageProp, multiplierType) {
return multiplier => {
// Apply the default 'ignore x' tags
if (includes(damageProp.tags, `ignore ${multiplierType}`)) return false;
const effectiveTags = getEffectivePropTags(damageProp);
if (includes(effectiveTags, `ignore ${multiplierType}`)) return false;
const hasRequiredTags = difference(
multiplier.includeTags, damageProp.tags
multiplier.includeTags, effectiveTags
).length === 0;
const hasNoExcludedTags = intersection(
multiplier.excludeTags, damageProp.tags
multiplier.excludeTags, effectiveTags
).length === 0;
return hasRequiredTags && hasNoExcludedTags;
}
}
function dealDamage({target, damageType, amount, actionContext}){
function dealDamage({ target, damageType, amount, actionContext }) {
// Get all the health bars and do damage to them
let healthBars = getPropertiesOfType(target._id, 'attribute');
@@ -238,6 +240,14 @@ function dealDamage({target, damageType, amount, actionContext}){
actionContext
});
damageLeft -= damageAdded;
// Prevent overflow
if (
damageType === 'healing' ?
healthBar.healthBarNoHealingOverflow :
healthBar.healthBarNoDamageOverflow
) {
damageLeft = 0;
}
});
return totalDamage;
}

View File

@@ -1,10 +1,11 @@
import rollDice from '/imports/parser/rollDice.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import applyProperty from '../applyProperty.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck.js';
export default function applySavingThrow(node, actionContext){
export default function applySavingThrow(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
@@ -13,7 +14,7 @@ export default function applySavingThrow(node, actionContext){
recalculateCalculation(prop.dc, actionContext);
const dc = (prop.dc?.value);
if (!isFinite(dc)){
if (!isFinite(dc)) {
actionContext.addLog({
name: 'Error',
value: 'Saving throw requires a DC',
@@ -29,8 +30,8 @@ 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};
if (!saveTargets?.length) {
scope['$saveFailed'] = { value: true };
scope['$saveSucceeded'] = { value: true };
applyNodeTriggers(node, 'after', actionContext);
return node.children.forEach(child => applyProperty(child, actionContext));
@@ -51,7 +52,7 @@ export default function applySavingThrow(node, actionContext){
const save = target.variables[prop.stat];
if (!save){
if (!save) {
actionContext.addLog({
name: 'Saving throw error',
value: 'No saving throw found: ' + prop.stat,
@@ -59,10 +60,14 @@ export default function applySavingThrow(node, actionContext){
return applyChildren();
}
const rollModifierText = numberToSignedString(save.value, true);
let rollModifierText = numberToSignedString(save.value, true);
let rollModifier = save.value
const { effectBonus, effectString } = applyUnresolvedEffects(save, scope)
rollModifierText += effectString;
rollModifier += effectBonus;
let value, values, resultPrefix;
if (save.advantage === 1){
if (save.advantage === 1) {
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
@@ -71,7 +76,7 @@ export default function applySavingThrow(node, actionContext){
value = b;
resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (save.advantage === -1){
} else if (save.advantage === -1) {
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
@@ -85,14 +90,14 @@ export default function applySavingThrow(node, actionContext){
value = values[0];
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
}
scope['$saveDiceRoll'] = {value};
const result = value + save.value || 0;
scope['$saveRoll'] = {value: result};
scope['$saveDiceRoll'] = { value };
const result = value + rollModifier || 0;
scope['$saveRoll'] = { value: result };
const saveSuccess = result >= dc;
if (saveSuccess){
scope['$saveSucceeded'] = {value: true};
if (saveSuccess) {
scope['$saveSucceeded'] = { value: true };
} else {
scope['$saveFailed'] = {value: true};
scope['$saveFailed'] = { value: true };
}
if (!prop.silent) actionContext.addLog({
name: saveSuccess ? 'Successful save' : 'Failed save',

View File

@@ -4,7 +4,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import rollDice from '/imports/parser/rollDice.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js';
@@ -28,6 +28,7 @@ const doCheck = new ValidatedMethod({
const creatureId = prop.ancestors[0].id;
const actionContext = new ActionContext(creatureId, [creatureId], this);
Object.assign(actionContext.scope, scope);
actionContext.scope[`#${prop.type}`] = prop;
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
@@ -115,7 +116,7 @@ function rollCheck(prop, actionContext) {
});
}
function applyUnresolvedEffects(prop, scope) {
export function applyUnresolvedEffects(prop, scope) {
let effectBonus = 0;
let effectString = '';
if (!prop.effects) {

View File

@@ -1,7 +1,7 @@
import findAncestorByType from '/imports/api/engine/computation/utility/findAncestorByType.js';
import { traverse } from '/imports/parser/resolve.js';
export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){
export default function linkCalculationDependencies(dependencyGraph, prop, { propsById }) {
prop._computationDetails.calculations.forEach(calcObj => {
// Store resolved ancestors
const memo = {
@@ -16,12 +16,13 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop
// Skip nodes that aren't symbols or accessors
if (node.parseType !== 'symbol' && node.parseType !== 'accessor') return;
// Link ancestor references as direct property dependencies
if (node.name[0] === '#'){
if (node.name[0] === '#') {
let ancestorProp = getAncestorProp(
node.name.slice(1), memo, prop, propsById
);
if (!ancestorProp) return;
// Link the ancestor prop as a direct dependency
// TODO: we might be referencing a calculation sub-field, depend on that instead
dependencyGraph.addLink(
calcNodeId, ancestorProp._id, 'ancestorReference'
);
@@ -34,16 +35,16 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop
});
// Store the resolved ancestors in this calculation's local scope
if (memo.ancestors) {
calcObj._localScope = { ...calcObj._localScope, ...memo.ancestors};
calcObj._localScope = { ...calcObj._localScope, ...memo.ancestors };
}
});
}
function getAncestorProp(type, memo, prop, propsById){
if (memo.ancestors && memo.ancestors['#' + type]){
function getAncestorProp(type, memo, prop, propsById) {
if (memo.ancestors && memo.ancestors['#' + type]) {
return memo.ancestors['#' + type];
} else {
var ancestorProp = findAncestorByType( prop, type, propsById );
var ancestorProp = findAncestorByType(prop, type, propsById);
if (!memo.ancestors) memo.ancestors = {};
memo.ancestors['#' + type] = ancestorProp;
return ancestorProp;

View File

@@ -23,23 +23,26 @@ const linkDependenciesByType = {
toggle: linkToggle,
}
export default function linkTypeDependencies(dependencyGraph, prop, computation){
export default function linkTypeDependencies(dependencyGraph, prop, computation) {
linkDependenciesByType[prop.type]?.(dependencyGraph, prop, computation);
}
function dependOnCalc({dependencyGraph, prop, key}){
function dependOnCalc({ dependencyGraph, prop, key }) {
let calc = get(prop, key);
if (!calc) return;
if (calc.type !== '_calculation'){
if (calc.type !== '_calculation') {
throw `Expected calculation got ${calc.type}`
}
dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation');
}
function linkAction(dependencyGraph, prop, {propsById}){
function linkAction(dependencyGraph, prop, { propsById }) {
if (prop.variableName) {
dependencyGraph.addLink(prop.variableName, prop._id, 'eventDefinition');
}
// The action depends on its attack roll and uses calculations
dependOnCalc({dependencyGraph, prop, key: 'attackRoll'});
dependOnCalc({dependencyGraph, prop, key: 'uses'});
dependOnCalc({ dependencyGraph, prop, key: 'attackRoll' });
dependOnCalc({ dependencyGraph, prop, key: 'uses' });
// Link the resources the action uses
if (!prop.resources) return;
@@ -47,7 +50,7 @@ function linkAction(dependencyGraph, prop, {propsById}){
prop.resources.itemsConsumed.forEach((itemConsumed, index) => {
if (!itemConsumed.itemId) return;
const item = propsById[itemConsumed.itemId];
if (!item || item.inactive){
if (!item || item.inactive) {
// Unlink if the item doesn't exist or is inactive
itemConsumed.itemId = undefined;
return;
@@ -79,48 +82,48 @@ function linkAction(dependencyGraph, prop, {propsById}){
});
}
function linkAdjustment(dependencyGraph, prop){
function linkAdjustment(dependencyGraph, prop) {
// Adjustment depends on its amount
dependOnCalc({dependencyGraph, prop, key: 'amount'});
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
}
function linkAttribute(dependencyGraph, prop){
function linkAttribute(dependencyGraph, prop) {
linkVariableName(dependencyGraph, prop);
// Depends on spellSlotLevel
dependOnCalc({dependencyGraph, prop, key: 'spellSlotLevel'});
dependOnCalc({ dependencyGraph, prop, key: 'spellSlotLevel' });
// Depends on base value
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
// hit dice depend on constitution
if (prop.attributeType === 'hitDice'){
if (prop.attributeType === 'hitDice') {
dependencyGraph.addLink(prop._id, 'constitution', 'hitDiceConMod');
}
}
function linkBranch(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'condition'});
function linkBranch(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'condition' });
}
function linkBuff(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'duration'});
function linkBuff(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'duration' });
}
function linkClassLevel(dependencyGraph, prop) {
if (prop.inactive) return;
// The variableName of the prop depends on the prop
if (prop.variableName && prop.level){
if (prop.variableName && prop.level) {
dependencyGraph.addLink(prop.variableName, prop._id, 'classLevel');
// The level variable depends on the class variableName variable
let existingLevelLink = dependencyGraph.getLink('level', prop.variableName);
if (!existingLevelLink){
if (!existingLevelLink) {
dependencyGraph.addLink('level', prop.variableName, 'level');
}
}
}
function linkDamage(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'amount'});
function linkDamage(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
}
function linkEffects(dependencyGraph, prop, computation) {
@@ -132,7 +135,7 @@ function linkEffects(dependencyGraph, prop, computation) {
if (prop.inactive) {
// Inactive effects apply to no stats
return;
} else if (prop.targetByTags){
} else if (prop.targetByTags) {
getEffectTagTargets(prop, computation).forEach(targetId => {
const targetProp = computation.propsById[targetId];
if (
@@ -147,8 +150,8 @@ 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){
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect');
if (calcObj && calcObj.calculation) {
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'effect');
}
}
});
@@ -161,14 +164,14 @@ function linkEffects(dependencyGraph, prop, computation) {
}
// Returns an array of IDs of the properties the effect targets
function getEffectTagTargets(effect, computation){
function getEffectTagTargets(effect, computation) {
let targets = getTargetListFromTags(effect.targetTags, computation);
let notIds = [];
if (effect.extraTags){
if (effect.extraTags) {
effect.extraTags.forEach(ex => {
if (ex.operation === 'OR') {
targets = union(targets, getTargetListFromTags(ex.tags, computation));
} else if (ex.operation === 'NOT'){
} else if (ex.operation === 'NOT') {
ex.tags.forEach(tag => {
const idList = computation.propsWithTag[tag];
if (idList) {
@@ -181,7 +184,7 @@ function getEffectTagTargets(effect, computation){
return difference(targets, notIds);
}
function getTargetListFromTags(tags, computation){
function getTargetListFromTags(tags, computation) {
const targetTagIdLists = [];
if (!tags) return [];
tags.forEach(tag => {
@@ -192,8 +195,8 @@ function getTargetListFromTags(tags, computation){
return targets;
}
function getDefaultCalculationField(prop){
switch (prop.type){
function getDefaultCalculationField(prop) {
switch (prop.type) {
case 'action': return 'attackRoll';
case 'adjustment': return 'amount';
case 'attribute': return 'baseValue';
@@ -223,13 +226,13 @@ function getDefaultCalculationField(prop){
}
}
function linkRoll(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'roll'});
function linkRoll(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'roll' });
}
function linkVariableName(dependencyGraph, prop){
function linkVariableName(dependencyGraph, prop) {
// The variableName of the prop depends on the prop if the prop is active
if (prop.variableName && !prop.inactive){
if (prop.variableName && !prop.inactive) {
dependencyGraph.addLink(prop.variableName, prop._id, 'definition');
}
}
@@ -243,7 +246,7 @@ function linkDamageMultiplier(dependencyGraph, prop) {
});
}
function linkPointBuy(dependencyGraph, prop){
function linkPointBuy(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'min' });
dependOnCalc({ dependencyGraph, prop, key: 'max' });
dependOnCalc({ dependencyGraph, prop, key: 'cost' });
@@ -265,7 +268,7 @@ function linkPointBuy(dependencyGraph, prop){
if (prop.inactive) return;
}
function linkProficiencies(dependencyGraph, prop){
function linkProficiencies(dependencyGraph, prop) {
// The stats depend on the proficiency
if (prop.inactive) return;
prop.stats.forEach(statName => {
@@ -274,36 +277,36 @@ function linkProficiencies(dependencyGraph, prop){
});
}
function linkSavingThrow(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'dc'});
function linkSavingThrow(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'dc' });
}
function linkSkill(dependencyGraph, prop){
function linkSkill(dependencyGraph, prop) {
// Depends on base value
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
// Link dependents
if (prop.inactive) return;
linkVariableName(dependencyGraph, prop);
// The prop depends on the variable references as the ability
if (prop.ability){
if (prop.ability) {
dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore');
}
// Skills depend on the creature's proficiencyBonus
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
}
function linkSlot(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'quantityExpected'});
dependOnCalc({dependencyGraph, prop, key: 'slotCondition'});
function linkSlot(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'quantityExpected' });
dependOnCalc({ dependencyGraph, prop, key: 'slotCondition' });
}
function linkSpellList(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'maxPrepared'});
dependOnCalc({dependencyGraph, prop, key: 'attackRollBonus'});
dependOnCalc({dependencyGraph, prop, key: 'dc'});
function linkSpellList(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'maxPrepared' });
dependOnCalc({ dependencyGraph, prop, key: 'attackRollBonus' });
dependOnCalc({ dependencyGraph, prop, key: 'dc' });
}
function linkToggle(dependencyGraph, prop){
function linkToggle(dependencyGraph, prop) {
linkVariableName(dependencyGraph, prop);
dependOnCalc({dependencyGraph, prop, key: 'condition'});
dependOnCalc({ dependencyGraph, prop, key: 'condition' });
}

View File

@@ -5,6 +5,7 @@ import skill from './computeByType/computeSkill.js';
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 _calculation from './computeByType/computeCalculation.js';
export default Object.freeze({
@@ -17,4 +18,5 @@ export default Object.freeze({
pointBuy,
propertySlot,
spell: action,
spellList,
});

View File

@@ -1,8 +1,8 @@
export default function computeAction(computation, node){
export default function computeAction(computation, node) {
const prop = node.data;
if (prop.uses){
if (prop.uses) {
prop.usesLeft = prop.uses.value - (prop.usesUsed || 0);
if (!prop.usesLeft){
if (!prop.usesLeft) {
prop.insufficientResources = true;
}
}
@@ -10,19 +10,19 @@ export default function computeAction(computation, node){
if (!prop.resources) return;
prop.resources.itemsConsumed.forEach(itemConsumed => {
if (!itemConsumed.itemId) return;
if (itemConsumed.available < itemConsumed.quantity?.value){
if (itemConsumed.available < itemConsumed.quantity?.value) {
prop.insufficientResources = true;
}
});
prop.resources.attributesConsumed.forEach(attConsumed => {
if (!attConsumed.variableName) return;
if (attConsumed.available < attConsumed.quantity?.value){
if (attConsumed.available < attConsumed.quantity?.value) {
prop.insufficientResources = true;
}
});
}
function computeResources(computation, node){
function computeResources(computation, node) {
const resources = node.data?.resources;
if (!resources) return;
resources.attributesConsumed.forEach(attConsumed => {

View File

@@ -0,0 +1,10 @@
export default function computeSpelllist(computation, node) {
const prop = node.data;
const ability = computation.scope[prop.ability];
if (Number.isFinite(ability?.modifier)) {
prop.abilityMod = ability.modifier;
} else if (Number.isFinite(ability?.value)) {
prop.abilityMod = ability.value;
}
}

View File

@@ -7,7 +7,7 @@ import computeVariableAsToggle from './computeVariable/computeVariableAsToggle.j
import computeImplicitVariable from './computeVariable/computeImplicitVariable.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
export default function computeVariable(computation, node){
export default function computeVariable(computation, node) {
const scope = computation.scope;
if (!node.data) node.data = {};
aggregateLinks(computation, node);
@@ -15,7 +15,7 @@ export default function computeVariable(computation, node){
// Don't add to the scope if the node id is not a legitimate variable name
// Without this `some.thing` could break the entire sheet as a database key
if (!VARIABLE_NAME_REGEX.test(node.id)) return;
if (node.data.definingProp){
if (node.data.definingProp) {
// Add the defining variable to the scope
scope[node.id] = node.data.definingProp
} else {
@@ -24,7 +24,7 @@ export default function computeVariable(computation, node){
}
}
function aggregateLinks(computation, node){
function aggregateLinks(computation, node) {
computation.dependencyGraph.forEachLinkedNode(
node.id,
(linkedNode, link) => {
@@ -32,11 +32,12 @@ function aggregateLinks(computation, node){
// Ignore inactive props
if (linkedNode.data.inactive) return;
// Apply all the aggregations
let arg = {node, linkedNode, link, computation};
let arg = { node, linkedNode, link, computation };
aggregate.classLevel(arg);
aggregate.damageMultiplier(arg);
aggregate.definition(arg);
aggregate.effect(arg);
aggregate.eventDefinition(arg);
aggregate.inventory(arg);
aggregate.proficiency(arg);
},
@@ -44,7 +45,7 @@ function aggregateLinks(computation, node){
);
}
function combineAggregations(computation, node){
function combineAggregations(computation, node) {
combineMultiplierAggregator(node);
node.data.overridenProps?.forEach(prop => {
computeVariableProp(computation, node, prop);
@@ -52,51 +53,51 @@ function combineAggregations(computation, node){
computeVariableProp(computation, node, node.data.definingProp);
}
function computeVariableProp(computation, node, prop){
function computeVariableProp(computation, node, prop) {
if (!prop) return;
// Combine damage multipliers in all props so that they can't be overridden
if (node.data.immunity){
if (node.data.immunity) {
prop.immunity = node.data.immunity;
prop.immunities = node.data.immunities;
}
if (node.data.resistance){
if (node.data.resistance) {
prop.resistance = node.data.resistance;
prop.resistances = node.data.resistances;
}
if (node.data.vulnerability){
if (node.data.vulnerability) {
prop.vulnerability = node.data.vulnerability;
prop.vulnerabilities = node.data.vulnerabilities;
}
if (prop.type === 'attribute'){
if (prop.type === 'attribute') {
computeVariableAsAttribute(computation, node, prop);
} else if (prop.type === 'skill'){
} else if (prop.type === 'skill') {
computeVariableAsSkill(computation, node, prop);
} else if (prop.type === 'constant'){
} else if (prop.type === 'constant') {
computeVariableAsConstant(computation, node, prop);
} else if (prop.type === 'class'){
} else if (prop.type === 'class') {
computeVariableAsClass(computation, node, prop);
} else if (prop.type === 'toggle'){
} else if (prop.type === 'toggle') {
computeVariableAsToggle(computation, node, prop);
}
}
function combineMultiplierAggregator(node){
function combineMultiplierAggregator(node) {
// get a reference to the aggregator
const aggregator = node.data.multiplierAggregator;
if (!aggregator) return;
// Combine
if (aggregator.immunities?.length){
if (aggregator.immunities?.length) {
node.data.immunity = true;
node.data.immunities = aggregator.immunities;
}
if (aggregator.resistances?.length){
if (aggregator.resistances?.length) {
node.data.resistance = true;
node.data.resistances = aggregator.resistances;
}
if (aggregator.vulnerabilities?.length){
if (aggregator.vulnerabilities?.length) {
node.data.vulnerability = true;
node.data.vulnerabilities = aggregator.vulnerabilities;
}

View File

@@ -1,6 +1,6 @@
import { pick } from 'lodash';
export default function aggregateEffect({node, linkedNode, link}){
export default function aggregateEffect({ node, linkedNode, link }) {
if (link.data !== 'effect') return;
// store the effect aggregator, its presence indicates that the variable is
// targeted by effects
@@ -38,21 +38,22 @@ export default function aggregateEffect({node, linkedNode, link}){
operation: linkedNode.data.operation,
amount: effectAmount,
type: linkedNode.data.type,
text: linkedNode.data.text,
// ancestors: linkedNode.data.ancestors,
});
// get a shorter reference to the aggregator document
const aggregator = node.data.effectAggregator;
// Get the result of the effect
const result = linkedNode.data.amount?.value;
// Skip aggregating if the result is not resolved completely
if (typeof result === 'string' || result === undefined) return;
let result = linkedNode.data.amount?.value;
if (typeof result !== 'number') result = undefined;
// Aggregate the effect based on its operation
switch(linkedNode.data.operation){
switch (linkedNode.data.operation) {
case 'base':
// Take the largest base value
if (Number.isFinite(result)){
if(Number.isFinite(aggregator.base)){
if (Number.isFinite(result)) {
if (Number.isFinite(aggregator.base)) {
aggregator.base = Math.max(aggregator.base, result);
} else {
aggregator.base = result;

View File

@@ -0,0 +1,22 @@
export default function aggregateEventDefinition({ node, linkedNode, link }) {
// Look at all event definition links
if (link.data !== 'eventDefinition') return;
// Store which property is THE defining event and which are overridden
const prop = linkedNode.data;
// get current defining event
const definingEvent = node.data.definingEvent;
// Find the last defining event
if (
!definingEvent ||
prop.order > definingEvent.order
) {
// override the current defining prop
if (definingEvent) definingEvent.overridden = true;
// set this prop as the new defining prop
node.data.definingEvent = prop;
} else {
prop.overridden = true;
}
}

View File

@@ -1,6 +1,7 @@
import definition from './aggregateDefinition.js';
import damageMultiplier from './aggregateDamageMultiplier.js';
import effect from './aggregateEffect.js';
import eventDefinition from './aggregateEventDefinition.js';
import proficiency from './aggregateProficiency.js';
import classLevel from './aggregateClassLevel.js';
import inventory from './aggregateInventory.js';
@@ -10,6 +11,7 @@ export default Object.freeze({
damageMultiplier,
definition,
effect,
eventDefinition,
inventory,
proficiency,
});