From 2442ae4fa023c7c568abb64815e0b2a5ad599a9c Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Sat, 5 Mar 2022 16:23:21 +0200 Subject: [PATCH] Overhauled damage multipliers UX Form and viewer revamp custom damage types Variables: `bludgeoning.resistance` --- .../api/creature/creatures/Creatures.js | 1 + .../buildComputation/linkTypeDependencies.js | 4 +- .../computeByType/computeVariable.js | 42 ++-- .../aggregate/aggregateDamageMultiplier.js | 28 ++- .../computeImplicitVariable.js | 15 ++ .../computeVariableAsAttribute.js | 2 +- .../computeVariable/getAggregatorResult.js | 7 +- app/imports/api/engine/computeCreature.js | 3 +- .../api/properties/DamageMultipliers.js | 2 + app/imports/api/properties/Damages.js | 2 + .../ui/components/global/SmartCombobox.vue | 23 ++- .../ui/components/global/SmartInputMixin.js | 32 ++- .../character/characterSheetTabs/StatsTab.vue | 31 +-- .../DamageMultiplierCard.vue | 140 ++++++++------ .../ui/properties/forms/DamageForm.vue | 13 +- .../properties/forms/DamageMultiplierForm.vue | 183 ++++++++++-------- .../viewers/DamageMultiplierViewer.vue | 50 ++++- 17 files changed, 388 insertions(+), 190 deletions(-) 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 @@ + +
-
- -
-
{ @@ -511,10 +520,6 @@ damageProperty.call({_id, operation: 'increment' ,value: -value}); } }, - numKeys(obj){ - if (!obj) return 0; - return Object.keys(obj).length; - }, softRemove(_id){ softRemoveProperty.call({_id}, error => { if (error) console.error(error); diff --git a/app/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue b/app/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue index c50048ca..c40b18de 100644 --- a/app/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue +++ b/app/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue @@ -1,72 +1,96 @@ diff --git a/app/imports/ui/properties/forms/DamageForm.vue b/app/imports/ui/properties/forms/DamageForm.vue index b56a913d..7acf3e36 100644 --- a/app/imports/ui/properties/forms/DamageForm.vue +++ b/app/imports/ui/properties/forms/DamageForm.vue @@ -23,6 +23,7 @@ label="Damage Type" style="flex-basis: 200px;" hint="Use the Healing type to restore hit points" + :rules="damageTypeRules" :items="DAMAGE_TYPES" :value="model.damageType" :error-messages="errors.damageType" @@ -46,7 +47,8 @@ multiple chips deletable-chips - hint="Used to let slots find this property in a library, should otherwise be left blank" + hint="" + :items="['magical', 'silvered']" :value="model.tags" :error-messages="errors.tags" @change="change('tags', ...arguments)" @@ -57,6 +59,7 @@ diff --git a/app/imports/ui/properties/viewers/DamageMultiplierViewer.vue b/app/imports/ui/properties/viewers/DamageMultiplierViewer.vue index 70b746bf..79a31b77 100644 --- a/app/imports/ui/properties/viewers/DamageMultiplierViewer.vue +++ b/app/imports/ui/properties/viewers/DamageMultiplierViewer.vue @@ -2,13 +2,57 @@
+ > + + {{ damageType }} + + + + + {{ damageType }} + + + + + {{ damageType }} + +