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/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/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/computeCreature.js b/app/imports/api/engine/computeCreature.js
index fc94edd2..611597c8 100644
--- a/app/imports/api/engine/computeCreature.js
+++ b/app/imports/api/engine/computeCreature.js
@@ -14,8 +14,9 @@ export default function computeCreature(creatureId){
} catch (e){
computation.errors.push({
type: 'crash',
- details: e.reason,
+ details: e.reason || e.message || e.toString(),
});
+ console.error(e);
} finally {
writeErrors(creatureId, computation.errors);
}
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/ui/components/global/SmartCombobox.vue b/app/imports/ui/components/global/SmartCombobox.vue
index 102931d0..4679fbde 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,32 @@
export default {
mixins: [SmartInput],
+ props: {
+ multiple: Boolean,
+ },
data(){ return {
searchInput: '',
}},
+ computed: {
+ // This component gets a longer default debounce time because it's all
+ // clicking no typing
+ debounceTime() {
+ if (Number.isFinite(this.debounce)){
+ return this.debounce;
+ } else if (Number.isFinite(this.context.debounceTime)){
+ return this.context.debounceTime;
+ } else {
+ return 1000;
+ }
+ },
+ },
methods: {
customChange(val){
- this.change(val);
+ if (this.multiple){
+ this.input(val);
+ } else {
+ this.change(val);
+ }
this.searchInput = '';
},
}
diff --git a/app/imports/ui/components/global/SmartInputMixin.js b/app/imports/ui/components/global/SmartInputMixin.js
index 5df4f5b2..66ebb058 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){
@@ -106,6 +131,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..59d3f5d4 100644
--- a/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue
+++ b/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue
@@ -22,6 +22,11 @@
+