diff --git a/app/imports/api/creature/creatures/Creatures.js b/app/imports/api/creature/creatures/Creatures.js
index e75eb3c6..1c118fee 100644
--- a/app/imports/api/creature/creatures/Creatures.js
+++ b/app/imports/api/creature/creatures/Creatures.js
@@ -133,6 +133,7 @@ let CreatureSchema = new SimpleSchema({
'computeErrors.$.details' : {
type: Object,
blackbox: true,
+ optional: true,
},
// Tabletop
diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js
index a92255c1..0366bf49 100644
--- a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js
+++ b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js
@@ -11,11 +11,11 @@ export default function applyAction(node, {creature, targets, scope, log}){
const prop = node.node;
if (prop.target === 'self') targets = [creature];
- // Log the name and description
+ // Log the name and summary
let content = { name: prop.name };
- if (prop.description?.text){
- recalculateInlineCalculations(prop.description, scope, log);
- content.value = prop.description.value;
+ if (prop.summary?.text){
+ recalculateInlineCalculations(prop.summary, scope, log);
+ content.value = prop.summary.value;
}
if (content.name || content.value){
log.content.push(content);
@@ -33,7 +33,7 @@ export default function applyAction(node, {creature, targets, scope, log}){
targets.forEach(target => {
applyAttackToTarget({attack, target, scope, log});
// Apply the children, but only to the current target
- applyChildren(node, {targets: [target], scope, log});
+ applyChildren(node, {creature, targets: [target], scope, log});
});
} else {
applyAttackWithoutTarget({attack, scope, log});
@@ -65,6 +65,13 @@ function applyAttackWithoutTarget({attack, scope, log}){
} else if(scope['$attackAdvantage'] === -1){
name += ' (Disadvantage)';
}
+ if (!criticalMiss){
+ scope['$attackHit'] = {value: true}
+ }
+ if (!criticalHit){
+ scope['$attackMiss'] = {value: true};
+ }
+
log.content.push({
name,
value: `${resultPrefix}\n**${result}**`,
@@ -106,10 +113,10 @@ function applyAttackToTarget({attack, target, scope, log}){
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
- if ((result > armor) || (criticalHit)){
- scope['$attackHit'] = true;
+ if (criticalMiss || result < armor){
+ scope['$attackMiss'] = {value: true};
} else {
- scope['$attackMiss'] = true;
+ scope['$attackHit'] = {value: true};
}
} else {
log.content.push({
@@ -127,7 +134,7 @@ function applyAttackToTarget({attack, target, scope, log}){
function rollAttack(attack, scope){
const rollModifierText = numberToSignedString(attack.value, true);
let value, resultPrefix;
- if (attack.advantage === 1 || scope['$attackAdvantage']){
+ if (scope['$attackAdvantage'] === 1){
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
@@ -136,7 +143,7 @@ function rollAttack(attack, scope){
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
- } else if (attack.advantage === -1 || scope['$attackDisadvantage']){
+ } else if (scope['$attackAdvantage'] === -1){
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
@@ -161,15 +168,10 @@ function applyCrits(value, scope){
let criticalMiss;
if (criticalHit){
scope['$criticalHit'] = {value: true};
- scope['$attackHit'] = {value: true};
} else {
criticalMiss = value === 1;
if (criticalMiss){
- scope['$criticalMiss'] = 1;
- scope['$attackMiss'] = {value: true};
- } else {
- // Untargeted attacks hit by default
- scope['$attackHit'] = {value: true}
+ scope['$criticalMiss'] = {value: true};
}
}
return {criticalHit, criticalMiss};
diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js
index 63aa2e1d..6a46e07c 100644
--- a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js
+++ b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js
@@ -16,17 +16,47 @@ export default function applyBranch(node, {
recalculateCalculation(prop.condition, scope, log);
if (prop.condition?.value) applyChildren();
break;
+ case 'index':
+ if (node.children.length){
+ recalculateCalculation(prop.condition, scope, log);
+ if (!isFinite(prop.condition?.value)) {
+ log.content.push({
+ name: 'Branch Error',
+ value: 'Index did not resolve into a valid number'
+ });
+ break;
+ }
+ let index = Math.floor(prop.condition?.value);
+ if (index < 1) index = 1;
+ if (index > node.children.length) index = node.children.length;
+ applyProperty(node.children[index - 1], {
+ creature, targets, scope, log
+ });
+ }
+ break;
case 'hit':
- if (scope['$attackHit']?.value) applyChildren();
+ if (scope['$attackHit']?.value){
+ if (!targets.length) log.content.push({value: '**On hit**'});
+ applyChildren();
+ }
break;
case 'miss':
- if (scope['$attackMiss']?.value) applyChildren();
+ if (scope['$attackMiss']?.value){
+ if (!targets.length) log.content.push({value: '**On miss**'});
+ applyChildren();
+ }
break;
case 'failedSave':
- if (scope['$saveFailed']?.value) applyChildren();
+ if (scope['$saveFailed']?.value){
+ if (!targets.length) log.content.push({value: '**On failed save**'});
+ applyChildren();
+ }
break;
case 'successfulSave':
- if (scope['$saveSucceeded']?.value) applyChildren();
+ if (scope['$saveSucceeded']?.value){
+ if (!targets.length) log.content.push({value: '**On save**',});
+ applyChildren();
+ }
break;
case 'random':
if (node.children.length){
diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js
index 32d9f35a..66857f6c 100644
--- a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js
+++ b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js
@@ -10,6 +10,7 @@ import { get } from 'lodash';
import resolve, { map, toString } from '/imports/parser/resolve.js';
import symbol from '/imports/parser/parseTree/symbol.js';
import logErrors from './shared/logErrors.js';
+import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
export default function applyBuff(node, {creature, targets, scope, log}){
@@ -32,7 +33,30 @@ export default function applyBuff(node, {creature, targets, scope, log}){
collection: prop.parent.collection,
};
buffTargets.forEach(target => {
+ // Apply the buff
copyNodeListToTarget(propList, target, oldParent);
+
+ //Log the buff
+ if (prop.name || prop.description?.value){
+ if (target._id === creature._id){
+ // Targeting self
+ log.content.push({
+ name: prop.name,
+ value: prop.description?.value,
+ });
+ } else {
+ // Targeting other
+ insertCreatureLog.call({
+ log: {
+ creatureId: target._id,
+ content: [{
+ name: prop.name,
+ value: prop.description?.value,
+ }],
+ }
+ });
+ }
+ }
});
// Don't apply the children of the buff, they get copied to the target instead
diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js
index 5e2153fc..0978d07a 100644
--- a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js
+++ b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js
@@ -1,3 +1,4 @@
+import { some, intersection, difference } from 'lodash';
import applyProperty from '../applyProperty.js';
import { dealDamageWork } from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
@@ -35,15 +36,12 @@ export default function applyDamage(node, {
const logValue = [];
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
- // Compile the dice roll and store that string first
- // const {result: compiled} = resolve('compiled', prop.amount.parseNode, scope, context);
- // logValue.push(toString(compiled));
- // logErrors(context.errors, log);
-
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
- logValue.push(toString(rolled));
+ if (rolled.parseType !== 'constant'){
+ logValue.push(toString(rolled));
+ }
logErrors(context.errors, log);
// Reset the errors so we don't log the same errors twice
@@ -61,14 +59,16 @@ export default function applyDamage(node, {
} else {
prop.amount.value = toString(reduced);
}
-
- const damage = +reduced.value;
+ 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();
}
+ // Round the damage to a whole number
+ damage = Math.floor(damage);
+
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType +
@@ -78,6 +78,14 @@ export default function applyDamage(node, {
// Iterate through all the targets
damageTargets.forEach(target => {
+ // Apply weaknesses/resistances/immunities
+ damage = applyDamageMultipliers({
+ target,
+ damage,
+ damageProp: prop,
+ logValue
+ });
+
// Deal the damage to the target
let damageDealt = dealDamageWork({
creature: target,
@@ -114,3 +122,51 @@ export default function applyDamage(node, {
});
return applyChildren();
}
+
+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`;
+
+ if (
+ multiplier.immunity &&
+ some(multiplier.immunities, multiplierAppliesTo(damageProp))
+ ){
+ logValue.push(`Immune to ${damageTypeText}`);
+ return 0;
+ } else {
+ if (
+ multiplier.resistance &&
+ some(multiplier.resistances, multiplierAppliesTo(damageProp))
+ ){
+ logValue.push(`Resistant to ${damageTypeText}`);
+ damage = Math.floor(damage / 2);
+ }
+ if (
+ multiplier.vulnerability &&
+ some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp))
+ ){
+ logValue.push(`Vulnerable to ${damageTypeText}`);
+ damage = Math.floor(damage * 2);
+ }
+ }
+ return damage;
+}
+
+function multiplierAppliesTo(damageProp){
+ return multiplier => {
+ const hasRequiredTags = difference(
+ multiplier.includeTags, damageProp.tags
+ ).length === 0;
+
+ const hasNoExcludedTags = intersection(
+ multiplier.excludeTags, damageProp.tags
+ ).length === 0;
+
+ return hasRequiredTags && hasNoExcludedTags;
+ }
+}
diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js
index d37aabfe..a097f977 100644
--- a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js
+++ b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js
@@ -1,20 +1,58 @@
import applyProperty from '../applyProperty.js';
-import recalculateCalculation from './shared/recalculateCalculation.js';
+import logErrors from './shared/logErrors.js';
+import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
+import resolve, { toString } from '/imports/parser/resolve.js';
export default function applyRoll(node, {creature, targets, scope, log}){
const prop = node.node;
- if (prop.roll?.calculation){
- recalculateCalculation(prop.roll, scope, log);
+ const applyChildren = node.children.forEach(child => applyProperty(child, {
+ creature, targets, scope, log
+ }));
- if (isFinite(prop.roll.value)){
- scope[prop.variableName] = prop.roll.value;
+ if (prop.roll?.calculation){
+ const logValue = [];
+
+ // roll the dice only and store that string
+ applyEffectsToCalculationParseNode(prop.roll, log);
+ const {result: rolled, context} = resolve('roll', prop.roll.parseNode, scope);
+ if (rolled.parseType !== 'constant'){
+ logValue.push(toString(rolled));
+ }
+ logErrors(context.errors, log);
+
+ // Reset the errors so we don't log the same errors twice
+ context.errors = [];
+
+ // Resolve the roll to a final value
+ const {result: reduced} = resolve('reduce', rolled, scope, context);
+ logErrors(context.errors, log);
+
+ // Store the result
+ if (reduced.parseType === 'constant'){
+ prop.roll.value = reduced.value;
+ } 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)){
+ return applyChildren();
+ }
+ const value = reduced.value;
+
+ scope[prop.variableName] = value;
+ logValue.push(`**${value}**`);
+
+ if (!prop.silent){
+ log.content.push({
+ name: prop.name,
+ value: logValue.join('\n'),
+ inline: true,
+ });
}
- log.content.push({
- name: prop.name,
- value: prop.variableName + ' = ' + prop.roll.calculation + ' = ' + prop.roll.value,
- inline: true,
- });
}
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
diff --git a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js b/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js
index b94ccc13..503442ab 100644
--- a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js
+++ b/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js
@@ -22,10 +22,21 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
}
log.content.push({
name: prop.name,
- value: ' DC ' + dc,
+ value: `DC **${dc}**`,
inline: true,
});
+ // 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};
+ return node.children.forEach(child => applyProperty(child, {
+ creature, targets, scope, log
+ }));
+ }
+
+ // Each target makes the saving throw
saveTargets.forEach(target => {
delete scope['$saveFailed'];
delete scope['$saveSucceeded'];
@@ -55,24 +66,24 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
- resultPrefix = `Advantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
+ resultPrefix = `Advantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
- resultPrefix = `Advantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
+ resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (save.advantage === -1){
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
- resultPrefix = `Disadvantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
+ resultPrefix = `Disadvantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
- resultPrefix = `Disadvantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
+ resultPrefix = `Disadvantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else {
values = rollDice(1, 20);
value = values[0];
- resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
+ resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
}
scope['$saveDiceRoll'] = {value};
const result = value + save.value || 0;
@@ -84,8 +95,8 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
scope['$saveFailed'] = {value: true};
}
log.content.push({
- name: 'Save',
- value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed'),
+ name: saveSuccess ? 'Successful save' : 'Failed save',
+ value: resultPrefix + '\n**' + result + '**',
inline: true,
});
return applyChildren();
diff --git a/app/imports/api/engine/computation/CreatureComputation.js b/app/imports/api/engine/computation/CreatureComputation.js
index d6c45ebe..90e28f44 100644
--- a/app/imports/api/engine/computation/CreatureComputation.js
+++ b/app/imports/api/engine/computation/CreatureComputation.js
@@ -20,16 +20,28 @@ export default class CreatureComputation {
// Store by id
this.propsById[prop._id] = prop;
+ // Store tags
+ const storePropOnTag = (prop, tag) => {
+ if (!tag) return;
+ if (this.propsWithTag[tag]){
+ this.propsWithTag[tag].push(prop._id);
+ } else {
+ this.propsWithTag[tag] = [prop._id];
+ }
+ }
// Store sets of ids in each tag
if (prop.tags){
prop.tags.forEach(tag => {
- if (this.propsWithTag[tag]){
- this.propsWithTag[tag].push(prop._id);
- } else {
- this.propsWithTag[tag] = [prop._id];
- }
+ storePropOnTag(prop, tag);
});
}
+ // Store tags for the property type
+ storePropOnTag(prop, `#${prop.type}`);
+ // Store tags for some string properties
+ storePropOnTag(prop, prop.damageType);
+ storePropOnTag(prop, prop.skillType);
+ storePropOnTag(prop, prop.attributeType);
+ storePropOnTag(prop, prop.reset);
// Store the prop in the dependency graph
this.dependencyGraph.addNode(prop._id, prop);
diff --git a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js
index 9394edba..848cf1d3 100644
--- a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js
+++ b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js
@@ -218,7 +218,7 @@ function linkDamageMultiplier(dependencyGraph, prop){
prop.damageTypes.forEach(damageType => {
// Remove all non-letter characters from the damage name
const damageName = damageType.replace(/[^a-z]/gi, '')
- dependencyGraph.addLink(`${damageName}Multiplier`, prop._id, prop.type);
+ dependencyGraph.addLink(damageName, prop._id, prop.type);
});
}
@@ -242,7 +242,7 @@ function linkSkill(dependencyGraph, prop){
}
// Skills depend on the creature's proficiencyBonus
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
-
+
// Depends on base value
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
}
diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js
index 09cb0b00..279ad3d6 100644
--- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js
+++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js
@@ -54,6 +54,21 @@ function combineAggregations(computation, node){
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){
+ prop.immunity = node.data.immunity;
+ prop.immunities = node.data.immunities;
+ }
+ if (node.data.resistance){
+ prop.resistance = node.data.resistance;
+ prop.resistances = node.data.resistances;
+ }
+ if (node.data.vulnerability){
+ prop.vulnerability = node.data.vulnerability;
+ prop.vulnerabilities = node.data.vulnerabilities;
+ }
+
if (prop.type === 'attribute'){
computeVariableAsAttribute(computation, node, prop);
} else if (prop.type === 'skill'){
@@ -73,21 +88,16 @@ function combineMultiplierAggregator(node){
if (!aggregator) return;
// Combine
- let value;
- if (aggregator.immunityCount){
- value = 0;
- } else if (
- aggregator.resistanceCount &&
- !aggregator.vulnerabilityCount
- ){
- value = 0.5;
- } else if (
- !aggregator.resistanceCount &&
- aggregator.vulnerabilityCount
- ){
- value = 2;
- } else {
- value = 1;
+ if (aggregator.immunities?.length){
+ node.data.immunity = true;
+ node.data.immunities = aggregator.immunities;
+ }
+ if (aggregator.resistances?.length){
+ node.data.resistance = true;
+ node.data.resistances = aggregator.resistances;
+ }
+ if (aggregator.vulnerabilities?.length){
+ node.data.vulnerability = true;
+ node.data.vulnerabilities = aggregator.vulnerabilities;
}
- node.data.damageMultiplyValue = value;
}
diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js
index 15b5309c..d165b1bd 100644
--- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js
+++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js
@@ -1,22 +1,36 @@
+import { pick } from 'lodash';
+
export default function aggregateDamageMultipliers({node, linkedNode, link}){
if (link.data !== 'damageMultiplier') return;
const multiplierValue = linkedNode.data.value;
if (multiplierValue === undefined) return;
+
// Store an aggregator, its presence indicates damage multipliers target this
// variable
if (!node.data.multiplierAggregator) node.data.multiplierAggregator = {
- immunityCount: 0,
- resistanceCount: 0,
- vulnerabilityCount: 0,
+ immunities: [],
+ resistances: [],
+ vulnerabilities: [],
}
// Store a short reference to the aggregator
const aggregator = node.data.multiplierAggregator;
- // Sum the counts of each type of multiplier
+
+ // Make a stripped down copy of the multiplier to store in the aggregator
+ const keysToStore = ['_id', 'name'];
+ if (linkedNode.data.excludeTags?.length){
+ keysToStore.push('excludeTags');
+ }
+ if (linkedNode.data.includeTags?.length){
+ keysToStore.push('includeTags');
+ }
+ const storedMultiplier = pick(linkedNode.data, keysToStore);
+
+ // Store the multiplier in the appropriate field
if (multiplierValue === 0){
- aggregator.immunityCount += 1;
+ aggregator.immunities.push(storedMultiplier);
} else if (multiplierValue === 0.5){
- aggregator.resistanceCount += 1;
+ aggregator.resistances.push(storedMultiplier);
} else if (multiplierValue === 2){
- aggregator.vulnerabilityCount += 1;
+ aggregator.vulnerabilities.push(storedMultiplier);
}
}
diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js
index c10c164d..61e081f8 100644
--- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js
+++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js
@@ -6,6 +6,21 @@ import getAggregatorResult from './getAggregatorResult.js';
*/
export default function computeImplicitVariable(node){
const prop = {};
+
+ // Combine damage multipliers
+ if (node.data.immunity){
+ prop.immunity = node.data.immunity;
+ prop.immunities = node.data.immunities;
+ }
+ if (node.data.resistance){
+ prop.resistance = node.data.resistance;
+ prop.resistances = node.data.resistances;
+ }
+ if (node.data.vulnerability){
+ prop.vulnerability = node.data.vulnerability;
+ prop.vulnerabilities = node.data.vulnerabilities;
+ }
+
const result = getAggregatorResult(node);
if (result !== undefined){
prop.value = result;
diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js
index 21137749..6ec5df2d 100644
--- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js
+++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js
@@ -1,7 +1,7 @@
import getAggregatorResult from './getAggregatorResult.js';
export default function computeVariableAsAttribute(computation, node, prop){
- let result = getAggregatorResult(node, prop) || 0;
+ let result = getAggregatorResult(node) || 0;
prop.total = result;
prop.value = prop.total - (prop.damage || 0);
diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js
index da0ed54b..6b9c3222 100644
--- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js
+++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js
@@ -29,8 +29,9 @@ export default function computeVariableAsSkill(computation, node, prop){
}
// Combine everything to get the final result
- const statBase = node.data.baseValue;
+ const statBase = node.data.baseValue || 0;
const aggregator = node.data.effectAggregator;
+ const aggregatorBase = aggregator?.base || 0;
// If there is no aggregator, determine if the prop can hide, then exit
if (!aggregator){
@@ -41,7 +42,7 @@ export default function computeVariableAsSkill(computation, node, prop){
return;
}
// Combine aggregator
- const base = (statBase > aggregator.base ? statBase : aggregator.base) || 0;
+ const base = statBase > aggregatorBase ? statBase : aggregatorBase;
let result = (base + prop.abilityMod + profBonus + aggregator.add) * aggregator.mul;
if (result < aggregator.min) result = aggregator.min;
if (result > aggregator.max) result = aggregator.max;
diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js
index 95905904..3201009b 100644
--- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js
+++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js
@@ -1,15 +1,10 @@
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
export default function getAggregatorResult(node){
- // Work out the base value as the greater of the deining stat value or
- // the damage multiplier value
+ // Work out the base value as the greater of the deining stat value
// This baseValue comes from aggregating definitions
let statBase = node.data.baseValue;
- const damageMultiplyValue = node.data.damageMultiplyValue;
- if (statBase === undefined || damageMultiplyValue > statBase){
- statBase = damageMultiplyValue;
- }
// get a reference to the aggregator
const aggregator = node.data.effectAggregator;
diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js
index ef1ec555..ee777627 100644
--- a/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js
+++ b/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js
@@ -7,9 +7,11 @@ export default function(){
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
const scope = id => computation.scope[id];
- assert.equal(scope('blugeoningMultiplier').value, 1);
- assert.equal(scope('customDamageMultiplier').value, 0.5);
- assert.equal(scope('slashingMultiplier').value, 0);
+ assert.isTrue(scope('blugeoning').vulnerability);
+ assert.isTrue(scope('customDamage').resistance);
+ assert.isNotTrue(scope('customDamage').immunity);
+ assert.isNotTrue(scope('customDamage').vulnerability);
+ assert.isTrue(scope('slashing').immunity);
}
var testProperties = [
diff --git a/app/imports/api/engine/computeCreature.js b/app/imports/api/engine/computeCreature.js
index fc94edd2..a45188bf 100644
--- a/app/imports/api/engine/computeCreature.js
+++ b/app/imports/api/engine/computeCreature.js
@@ -12,10 +12,19 @@ export default function computeCreature(creatureId){
writeAlteredProperties(computation);
writeScope(creatureId, computation.scope);
} catch (e){
+ const errorText = e.reason || e.message || e.toString();
computation.errors.push({
type: 'crash',
- details: e.reason,
+ details: {error: errorText},
});
+ const logError = {
+ creatureId,
+ computeError: errorText,
+ };
+ if (e.stack){
+ logError.location = e.stack.split('\n')[1];
+ }
+ console.error(logError);
} finally {
writeErrors(creatureId, computation.errors);
}
diff --git a/app/imports/api/properties/Branches.js b/app/imports/api/properties/Branches.js
index 758d88fa..952c7715 100644
--- a/app/imports/api/properties/Branches.js
+++ b/app/imports/api/properties/Branches.js
@@ -18,6 +18,8 @@ let BranchSchema = createPropertySchema({
'eachTarget',
// Pick one child at random
'random',
+ // Pick one child based on a given index
+ 'index',
// if it has option children, asks to select one
// Otherwise presents its own text with yes/no
//'choice',
diff --git a/app/imports/api/properties/DamageMultipliers.js b/app/imports/api/properties/DamageMultipliers.js
index d40f08be..c88c4cbe 100644
--- a/app/imports/api/properties/DamageMultipliers.js
+++ b/app/imports/api/properties/DamageMultipliers.js
@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
+import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
/*
* DamageMultipliers are multipliers that affect how much damage is taken from
@@ -20,6 +21,7 @@ let DamageMultiplierSchema = new SimpleSchema({
'damageTypes.$': {
type: String,
max: STORAGE_LIMITS.calculation,
+ regEx: VARIABLE_NAME_REGEX,
},
// The value of the damage multiplier
value: {
diff --git a/app/imports/api/properties/Damages.js b/app/imports/api/properties/Damages.js
index 12e55623..85cafee0 100644
--- a/app/imports/api/properties/Damages.js
+++ b/app/imports/api/properties/Damages.js
@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
+import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
const DamageSchema = createPropertySchema({
// The roll that determines how much to damage the attribute
@@ -24,6 +25,7 @@ const DamageSchema = createPropertySchema({
type: String,
max: STORAGE_LIMITS.calculation,
defaultValue: 'slashing',
+ regEx: VARIABLE_NAME_REGEX,
},
});
diff --git a/app/imports/api/properties/Rolls.js b/app/imports/api/properties/Rolls.js
index 5acc0ce7..03e819a4 100644
--- a/app/imports/api/properties/Rolls.js
+++ b/app/imports/api/properties/Rolls.js
@@ -41,6 +41,11 @@ let RollSchema = createPropertySchema({
parseLevel: 'compile',
optional: true,
},
+ // Prevent the roll from showing up in the log
+ silent: {
+ type: Boolean,
+ optional: true,
+ },
});
let ComputedOnlyRollSchema = createPropertySchema({
diff --git a/app/imports/api/simpleSchemaConfig.js b/app/imports/api/simpleSchemaConfig.js
index aff36261..6b960a40 100644
--- a/app/imports/api/simpleSchemaConfig.js
+++ b/app/imports/api/simpleSchemaConfig.js
@@ -1,4 +1,9 @@
import SimpleSchema from 'simpl-schema';
+import { set } from 'lodash';
+
+set(Meteor.settings,
+ 'packages.collection2.disableCollectionNamesInValidation',
+ true);
SimpleSchema.extendOptions([
'parseLevel',
diff --git a/app/imports/migrations/methods/index.js b/app/imports/migrations/methods/index.js
index 6d5dc8c5..ed6bbb4e 100644
--- a/app/imports/migrations/methods/index.js
+++ b/app/imports/migrations/methods/index.js
@@ -1,2 +1,3 @@
import './migrateTo.js';
+import './validateDatabase.js';
import './getVersion.js';
diff --git a/app/imports/migrations/methods/migrateTo.js b/app/imports/migrations/methods/migrateTo.js
index c5248c9d..f61fb9b3 100644
--- a/app/imports/migrations/methods/migrateTo.js
+++ b/app/imports/migrations/methods/migrateTo.js
@@ -16,8 +16,8 @@ const migrateTo = new ValidatedMethod({
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
- numRequests: 5,
- timeInterval: 5000,
+ numRequests: 1,
+ timeInterval: 10000,
},
run({version}) {
if (Meteor.isClient) return;
diff --git a/app/imports/migrations/methods/validateDatabase.js b/app/imports/migrations/methods/validateDatabase.js
new file mode 100644
index 00000000..c555f3c5
--- /dev/null
+++ b/app/imports/migrations/methods/validateDatabase.js
@@ -0,0 +1,35 @@
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
+import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
+
+const validateDatabase = new ValidatedMethod({
+ name: 'validateDatabase',
+ validate: null,
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 1,
+ timeInterval: 10000,
+ },
+ run() {
+ assertAdmin(this.userId);
+ // Very computationally expensive data diagnostics
+ // Only run in an offline instance you control
+ return;
+ if (Meteor.isClient) return;
+
+ Meteor.Collection.getAll().forEach(collection => {
+ if (!collection.instance._c2?._simpleSchemas) return;
+ collection.instance.find({}).forEach(doc => {
+ const schema = collection.instance.simpleSchema(doc);
+ let cleanDoc = schema.clean(doc);
+ try {
+ schema.validate(cleanDoc, {modifier: false});
+ } catch (e){
+ console.log(collection.name, doc._id, e.message || e.reason || e.toString());
+ }
+ });
+ });
+ },
+});
+
+export default validateDatabase;
diff --git a/app/imports/migrations/server/dbv1/cleanAt1.js b/app/imports/migrations/server/dbv1/cleanAt1.js
new file mode 100644
index 00000000..d58e3c74
--- /dev/null
+++ b/app/imports/migrations/server/dbv1/cleanAt1.js
@@ -0,0 +1,39 @@
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
+import { get, set } from 'lodash';
+import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
+import { calculationUp } from '/imports/migrations/server/dbv1/dbv1.js';
+
+export default function cleanAt1(archive){
+ archive.properties = archive.properties.map(prop => {
+ let cleanProp = prop;
+ try {
+ if (prop.type === 'attack') prop.type = 'action';
+ // Get the schema
+ const schema = CreatureProperties.simpleSchema(prop);
+ // Clean all the text fields with inline calcs
+ schema.inlineCalculationFields().forEach(key => {
+ applyFnToKey(prop, key, (prop, key) => {
+ let field = get(prop, key);
+ if (typeof field === 'string' || typeof field === 'number'){
+ field = calculationUp(field);
+ set(prop, key, {text: `${field}`});
+ }
+ });
+ });
+ schema.computedFields().forEach(key => {
+ applyFnToKey(prop, key, (prop, key) => {
+ let field = get(prop, key) || get(prop, key + 'Calculation');
+ if (typeof field === 'string' || typeof field === 'number'){
+ field = calculationUp(field);
+ set(prop, key, {calculation: `${field}`});
+ }
+ });
+ });
+ cleanProp = schema.clean(prop);
+ schema.validate(cleanProp);
+ } catch (e){
+ console.warn({propId: prop._id, error: e.message || e.reason || e.toString()});
+ }
+ return cleanProp;
+ });
+}
diff --git a/app/imports/migrations/server/dbv1/dbv1.js b/app/imports/migrations/server/dbv1/dbv1.js
index d299fc58..b265cd9d 100644
--- a/app/imports/migrations/server/dbv1/dbv1.js
+++ b/app/imports/migrations/server/dbv1/dbv1.js
@@ -215,16 +215,17 @@ function getInlineComputationTransforms(key){
];
}
-function calculationUp(val){
+export function calculationUp(val){
if (typeof val !== 'string') return val;
+ if (!val.replace) console.log({val, replace: val.replace});
return val.replace(/#(\w+).(\w+)Result/g, '#$1.$2')
- .replace('.value', '.total')
- .replace('.currentValue', '.value');
+ .replace(/\.value/g, '.total')
+ .replace(/\.currentValue/g, '.value');
}
function calculationDown(val){
if (typeof val !== 'string') return val;
- return val.replace('.value', '.currentValue').replace('.total', '.value');
+ return val.replace(/\.value/g, '.currentValue').replace(/\.total/g, '.value');
}
function nanToNull(val){
diff --git a/app/imports/migrations/server/migrateArchive.js b/app/imports/migrations/server/migrateArchive.js
index 42f04c26..0fb557ed 100644
--- a/app/imports/migrations/server/migrateArchive.js
+++ b/app/imports/migrations/server/migrateArchive.js
@@ -1,4 +1,4 @@
-import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
+import cleanAt1 from '/imports/migrations/server/dbv1/cleanAt1.js';
/* eslint no-fallthrough: "off" -- Using switch fallthrough to run all
migration steps after the current version of the file. */
@@ -17,12 +17,3 @@ function migrateLegacyArchive(archive){
// TODO:
throw 'Not implemented';
}
-
-function cleanAt1(archive){
- archive.properties.map(prop => {
- const schema = CreatureProperties.simpleSchema(prop);
- const cleanProp = schema.clean(prop);
- schema.validate(cleanProp);
- return cleanProp;
- });
-}
diff --git a/app/imports/ui/components/global/SmartCombobox.vue b/app/imports/ui/components/global/SmartCombobox.vue
index 102931d0..07931531 100644
--- a/app/imports/ui/components/global/SmartCombobox.vue
+++ b/app/imports/ui/components/global/SmartCombobox.vue
@@ -7,6 +7,7 @@
:menu-props="{auto: true, lazy: true}"
:search-input.sync="searchInput"
:disabled="isDisabled"
+ :multiple="multiple"
outlined
@change="customChange"
@focus="focused = true"
@@ -24,12 +25,28 @@
export default {
mixins: [SmartInput],
+ props: {
+ multiple: Boolean,
+ },
data(){ return {
searchInput: '',
}},
+ computed: {
+ // Multiple combobox gets a long default debounce time while single
+ // value gets a shorter one
+ debounceTime() {
+ if (Number.isFinite(this.debounce)){
+ return this.debounce;
+ } else if (Number.isFinite(this.context.debounceTime)){
+ return this.context.debounceTime;
+ } else {
+ return this.multiple ? 1000 : 100;
+ }
+ },
+ },
methods: {
customChange(val){
- this.change(val);
+ this.input(val);
this.searchInput = '';
},
}
diff --git a/app/imports/ui/components/global/SmartInputMixin.js b/app/imports/ui/components/global/SmartInputMixin.js
index 5df4f5b2..5e44cc7b 100644
--- a/app/imports/ui/components/global/SmartInputMixin.js
+++ b/app/imports/ui/components/global/SmartInputMixin.js
@@ -16,6 +16,7 @@ export default {
data(){ return {
error: false,
ackErrors: null,
+ rulesErrors: null,
focused: false,
loading: false,
dirty: false,
@@ -30,6 +31,7 @@ export default {
type: Number,
default: undefined,
},
+ rules: Array,
},
watch: {
focused(newFocus){
@@ -42,7 +44,11 @@ export default {
// Start the loading bar on defocus if the input is dirty
// It might be a lie, we aren't doing the work yet, but it feels laggy
// to defocus an element and then it starts working after a delay
- if (!newFocus && this.dirty){
+ if (
+ !newFocus &&
+ this.dirty &&
+ !(this.rulesErrors && this.rulesErrors.length)
+ ){
if (this.hasChangeListener) this.loading = true;
}
},
@@ -54,7 +60,10 @@ export default {
}
},
value(newValue){
- if (!this.focused){
+ if (
+ !this.focused &&
+ !(this.rulesErrors && this.rulesErrors.length)
+ ){
this.safeValue = newValue;
}
},
@@ -69,6 +78,22 @@ export default {
this.$emit('input', val);
this.inputValue = val;
this.dirty = true;
+
+ // Apply the rules if there are any
+ this.rulesErrors = null;
+ if (this.rules && this.rules.length){
+ this.rules.forEach(rule => {
+ const result = rule(val);
+ if (typeof result === 'string'){
+ if (!this.rulesErrors) this.rulesErrors = [];
+ this.rulesErrors.push(result);
+ }
+ });
+ }
+ if (this.rulesErrors){
+ return;
+ }
+
this.debouncedChange(val);
},
acknowledgeChange(error){
@@ -81,6 +106,8 @@ export default {
this.ackErrors = error;
} else if (error.reason){
this.ackErrors = error.reason;
+ } else if (error.message){
+ this.ackErrors = error.message;
} else {
this.ackErrors = 'Something went wrong'
console.error(error);
@@ -106,6 +133,9 @@ export default {
computed: {
errors(){
let errors = this.ackErrors ? [this.ackErrors] : [];
+ if (Array.isArray(this.rulesErrors)){
+ errors.push(...this.rulesErrors)
+ }
if (Array.isArray(this.errorMessages)){
errors.push(...this.errorMessages);
} else if (typeof this.errorMessages === 'string' && this.errorMessages){
diff --git a/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue b/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue
index 9d8e2e3f..9862591f 100644
--- a/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue
+++ b/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue
@@ -22,6 +22,12 @@
+