Denormalized some calculations into recomputation step

This commit is contained in:
Stefan Zermatten
2020-06-07 21:08:53 +02:00
parent 5198c655e9
commit 1535e00093
22 changed files with 320 additions and 132 deletions

View File

@@ -15,6 +15,8 @@ export default class ComputationMemo {
this.classes = {};
this.togglesById = {};
this.toggleIds = new Set();
// Properties that have calculations, but don't impact other properties
this.endStepPropsById = {};
// First note all the ids of all the toggles
props.forEach((prop) => {
if (
@@ -49,6 +51,8 @@ export default class ComputationMemo {
this.addProficiency(prop);
} else if (prop.type === 'classLevel'){
this.addClassLevel(prop);
} else {
this.addEndStepProp(prop);
}
});
for (let name in creature.denormalizedStats){
@@ -181,6 +185,10 @@ export default class ComputationMemo {
});
return targets;
}
addEndStepProp(prop){
prop = this.registerProperty(prop);
this.endStepPropsById[prop._id] = prop;
}
}
function isAbility(prop){
@@ -206,7 +214,7 @@ function isSkillOperation(prop){
}
function propDetails(prop){
return propDetailsByType[prop.type]() || {};
return propDetailsByType[prop.type] && propDetailsByType[prop.type]() || {};
}
const propDetailsByType = {

View File

@@ -0,0 +1,61 @@
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
export default function computeEndStepProperty(prop, memo){
switch (prop.type){
case 'action':
case 'spell':
computeAction(prop, memo);
break;
case 'attack':
computeAction(prop, memo);
computeAttack(prop, memo);
break;
case 'savingThrow':
computeSavingThrow(prop, memo);
break;
case 'spellList':
computeSpellList(prop, memo);
break;
}
}
function computeAction(prop, memo){
let {value, errors} = evaluateCalculation(prop.uses, memo);
prop.usesResult = value;
if (errors.length){
prop.usesErrors = errors;
} else {
delete prop.usesErrors;
}
// TODO compute resources.$.$.available and insufficientResources
}
function computeAttack(prop, memo){
let {value, errors} = evaluateCalculation(prop.rollBonus, memo);
prop.rollBonusResult = value;
if (errors.length){
prop.rollBonusErrors = errors;
} else {
delete prop.rollBonusErrors;
}
}
function computeSavingThrow(prop, memo){
let {value, errors} = evaluateCalculation(prop.dc, memo);
prop.dcResult = value;
if (errors.length){
prop.dcErrors = errors;
} else {
delete prop.dcErrors;
}
}
function computeSpellList(prop, memo){
let {value, errors} = evaluateCalculation(prop.maxPrepared, memo);
prop.maxPreparedResult = value;
if (errors.length){
prop.maxPreparedErrors = errors;
} else {
delete prop.maxPreparedErrors;
}
}

View File

@@ -3,6 +3,7 @@ import computeLevels from '/imports/api/creature/computation/computeLevels.js';
import computeStat from '/imports/api/creature/computation/computeStat.js';
import computeEffect from '/imports/api/creature/computation/computeEffect.js';
import computeToggle from '/imports/api/creature/computation/computeToggle.js';
import computeEndStepProperty from '/imports/api/creature/computation/computeEndStepProperty.js';
export default function computeMemo(memo){
// Compute level
@@ -15,8 +16,12 @@ export default function computeMemo(memo){
each(memo.unassignedEffects, effect => {
computeEffect(effect, memo);
});
// Compute toggles which didn't already get computed by dependencies
forOwn(memo.togglesById, toggle => {
computeToggle(toggle, memo);
});
// Compute class levels
// Compute end step properties
forOwn(memo.endStepPropsById, prop => {
computeEndStepProperty(prop, memo);
});
}

View File

@@ -3,7 +3,7 @@ import math from '/imports/math.js';
/* Convert a calculation into a constant output and errors*/
export default function evaluateCalculation(string, memo){
if (!string) return string;
if (!string) return {errors: [], value: string};
let errors = [];
// Parse the string using mathjs
let calc;

View File

@@ -42,6 +42,12 @@ const calculationPropertyTypes = [
'proficiency',
'classLevel',
'toggle',
// End step types
'action',
'attack',
'savingThrow',
'spellList',
'spell',
];
export function recomputeCreatureById(creatureId){

View File

@@ -1,16 +1,29 @@
import { Meteor } from 'meteor/meteor'
import { isEqual, forOwn } from 'lodash';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
// Schemas
// Calculated props
import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js';
import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js';
import { ComputedOnlyEffectSchema } from '/imports/api/properties/Effects.js';
import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
// End step props
import { ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js';
import { ComputedOnlyAttackSchema } from '/imports/api/properties/Attacks.js';
import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js';
import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js';
const schemasByType = {
'skill': ComputedOnlySkillSchema,
'attribute': ComputedOnlyAttributeSchema,
'effect': ComputedOnlyEffectSchema,
'toggle': ComputedOnlyToggleSchema,
'action': ComputedOnlyActionSchema,
'attack': ComputedOnlyAttackSchema,
'savingThrow': ComputedOnlySavingThrowSchema,
'spellList': ComputedOnlySpellListSchema,
'spell': ComputedOnlySpellSchema,
};
export default function writeAlteredProperties(memo){

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema';
import ResourcesSchema from '/imports/api/properties/subSchemas/ResourcesSchema.js'
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
/*
* Actions are things a character can do
@@ -64,4 +65,39 @@ let ActionSchema = new SimpleSchema({
},
});
export { ActionSchema };
const ComputedOnlyActionSchema = new SimpleSchema({
usesResult: {
type: SimpleSchema.Integer,
optional: true,
},
usesErrors: {
type: Array,
optional: true,
},
'usesErrors.$':{
type: ErrorSchema,
},
resources: Object,
'resources.itemsConsumed': Array,
'resources.itemsConsumed.$': Object,
'resources.itemsConsumed.$.available': {
type: Number,
optional: true,
},
'resources.attributesConsumed': Array,
'resources.attributesConsumed.$': Object,
'resources.attributesConsumed.$.available': {
type: Number,
optional: true,
},
insufficientResources: {
type: Boolean,
optional: true,
},
});
const ComputedActionSchema = new SimpleSchema()
.extend(ActionSchema)
.extend(ComputedOnlyActionSchema);
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema};

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
const AdjustmentSchema = new SimpleSchema({
// The roll that determines how much to change the attribute
// This can be simplified, but should only compute when activated
amount: {
type: String,
optional: true,

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema';
import { ActionSchema } from '/imports/api/properties/Actions.js';
import { ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
// Attacks are special instances of actions
let AttackSchema = new SimpleSchema()
@@ -25,4 +26,24 @@ let AttackSchema = new SimpleSchema()
},
});
export { AttackSchema };
const ComputedOnlyAttackSchema = new SimpleSchema()
.extend(ComputedOnlyActionSchema)
.extend({
rollBonusResult: {
type: Number,
optional: true,
},
rollBonusErrors: {
type: Array,
optional: true,
},
'rollBonusErrors.$':{
type: ErrorSchema,
},
});
const ComputedAttackSchema = new SimpleSchema()
.extend(AttackSchema)
.extend(ComputedOnlyAttackSchema);
export { AttackSchema, ComputedOnlyAttackSchema, ComputedAttackSchema };

View File

@@ -3,6 +3,7 @@ import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
const DamageSchema = new SimpleSchema({
// The roll that determines how much to damage the attribute
// This can be simplified, but only computed when applied
amount: {
type: String,
optional: true,

View File

@@ -19,7 +19,7 @@ import SimpleSchema from 'simpl-schema';
* child rolls are applied
*/
let RollSchema = new SimpleSchema({
// The roll
// The roll, can be simplified, but only computed in context
roll: {
type: String,
optional: true,

View File

@@ -1,4 +1,5 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
// These are the rolls made when saves are called for
// For the saving throw bonus or proficiency, see ./Skills.js
@@ -18,4 +19,22 @@ let SavingThrowSchema = new SimpleSchema ({
},
});
export { SavingThrowSchema };
const ComputedOnlySavingThrowSchema = new SimpleSchema({
dcResult: {
type: Number,
optional: true,
},
dcErrors: {
type: Array,
optional: true,
},
'dcErrors.$':{
type: ErrorSchema,
},
});
const ComputedSavingThrowSchema = new SimpleSchema()
.extend(SavingThrowSchema)
.extend(ComputedOnlySavingThrowSchema);
export { SavingThrowSchema, ComputedOnlySavingThrowSchema, ComputedSavingThrowSchema };

View File

@@ -1,4 +1,5 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
let SpellListSchema = new SimpleSchema({
name: {
@@ -16,4 +17,22 @@ let SpellListSchema = new SimpleSchema({
},
});
export { SpellListSchema }
const ComputedOnlySpellListSchema = new SimpleSchema({
maxPreparedResult: {
type: Number,
optional: true,
},
maxPreparedErrors: {
type: Array,
optional: true,
},
'maxPreparedErrors.$':{
type: ErrorSchema,
},
});
const ComputedSpellListSchema = new SimpleSchema()
.extend(SpellListSchema)
.extend(ComputedOnlySpellListSchema);
export { SpellListSchema, ComputedOnlySpellListSchema, ComputedSpellListSchema };

View File

@@ -1,4 +1,4 @@
import { ActionSchema } from '/imports/api/properties/Actions.js';
import { ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js';
import SimpleSchema from 'simpl-schema';
const magicSchools = [
@@ -93,4 +93,11 @@ let SpellSchema = new SimpleSchema({})
},
});
export { SpellSchema };
const ComputedOnlySpellSchema = new SimpleSchema()
.extend(ComputedOnlyActionSchema);
const ComputedSpellSchema = new SimpleSchema()
.extend(SpellSchema)
.extend(ComputedOnlySpellSchema);
export { SpellSchema, ComputedOnlySpellSchema, ComputedSpellSchema };

View File

@@ -1,7 +1,7 @@
import SimpleSchema from 'simpl-schema';
import { ActionSchema } from '/imports/api/properties/Actions.js';
import { ComputedActionSchema } from '/imports/api/properties/Actions.js';
import { AdjustmentSchema } from '/imports/api/properties/Adjustments.js';
import { AttackSchema } from '/imports/api/properties/Attacks.js';
import { ComputedAttackSchema } from '/imports/api/properties/Attacks.js';
import { ComputedAttributeSchema } from '/imports/api/properties/Attributes.js';
import { BuffSchema } from '/imports/api/properties/Buffs.js';
import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
@@ -15,17 +15,17 @@ import { ItemSchema } from '/imports/api/properties/Items.js';
import { NoteSchema } from '/imports/api/properties/Notes.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { RollSchema } from '/imports/api/properties/Rolls.js';
import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedSkillSchema } from '/imports/api/properties/Skills.js';
import { SlotSchema } from '/imports/api/properties/Slots.js';
import { SpellSchema } from '/imports/api/properties/Spells.js';
import { SpellListSchema } from '/imports/api/properties/SpellLists.js';
import { ComputedSpellSchema } from '/imports/api/properties/Spells.js';
import { ComputedSpellListSchema } from '/imports/api/properties/SpellLists.js';
import { ToggleSchema } from '/imports/api/properties/Toggles.js';
const propertySchemasIndex = {
action: ActionSchema,
action: ComputedActionSchema,
adjustment: AdjustmentSchema,
attack: AttackSchema,
attack: ComputedAttackSchema,
attribute: ComputedAttributeSchema,
buff: BuffSchema,
classLevel: ClassLevelSchema,
@@ -37,11 +37,11 @@ const propertySchemasIndex = {
note: NoteSchema,
proficiency: ProficiencySchema,
roll: RollSchema,
savingThrow: SavingThrowSchema,
savingThrow: ComputedSavingThrowSchema,
skill: ComputedSkillSchema,
slot: SlotSchema,
spellList: SpellListSchema,
spell: SpellSchema,
spellList: ComputedSpellSchema,
spell: ComputedSpellListSchema,
toggle: ToggleSchema,
container: ContainerSchema,
item: ItemSchema,

View File

@@ -1,11 +1,9 @@
import SimpleSchema from 'simpl-schema';
const ErrorSchema = new SimpleSchema({
// The roll that determines how much to change the attribute
message: {
type: String,
},
// Who this adjustment applies to
type: {
type: String,
},

View File

@@ -276,9 +276,10 @@
subheader
>
<v-subheader>Attacks</v-subheader>
<attack-list-tile
<action-list-tile
v-for="attack in attacks"
:key="attack._id"
attack
:model="attack"
:data-id="attack._id"
@click="clickProperty({_id: attack._id})"
@@ -303,7 +304,6 @@
import ResourceCard from '/imports/ui/properties/components/attributes/ResourceCard.vue';
import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue';
import ActionListTile from '/imports/ui/properties/components/actions/ActionListTile.vue';
import AttackListTile from '/imports/ui/properties/components/actions/AttackListTile.vue';
import RestButton from '/imports/ui/creature/RestButton.vue';
import getActiveProperties from '/imports/api/creature/getActiveProperties.js';
@@ -346,7 +346,6 @@
ResourceCard,
SpellSlotListTile,
ActionListTile,
AttackListTile,
},
props: {
creatureId: {
@@ -398,10 +397,24 @@
return getSkillOfType(this.creature, 'language');
},
actions(){
return getProperties(this.creature, {type: 'action'});
let props = getProperties(this.creature, {type: 'action'}).map(action => {
action.children = getActiveProperties({
ancestorId: action._id,
options: {sort: {order: 1}},
});
return action;
});
return props;
},
attacks(){
return getProperties(this.creature, {type: 'attack'});
let props = getProperties(this.creature, {type: 'attack'}).map(attack => {
attack.children = getActiveProperties({
ancestorId: attack._id,
options: {sort: {order: 1}},
});
return attack;
});
return props;
},
},
methods: {

View File

@@ -3,26 +3,69 @@
class="ability-list-tile"
v-on="hasClickListener ? {click} : {}"
>
<v-list-tile-avatar
v-if="attack"
class="headline"
>
{{ rollBonus }}
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>
{{ model.name }}
</v-list-tile-title>
<v-list-tile-sub-title
v-if="childText"
v-html="childText"
/>
</v-list-tile-content>
<v-list-tile-action v-if="model.usesResult">
<v-list-tile-action-text>
{{ model.usesResult - (model.usesUsed) }}/{{ model.usesResult }}
</v-list-tile-action-text>
</v-list-tile-action>
</v-list-tile>
</template>
<script>
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default {
inject: {
context: {
default: {},
},
},
props: {
model: {
type: Object,
required: true,
}
},
attack: {
type: Boolean,
},
},
computed: {
hasClickListener(){
return this.$listeners && this.$listeners.click
},
rollBonus(){
return numberToSignedString(this.model.rollBonusResult);
},
childText(){
let scope = this.context.creature && this.context.creature.variables;
if (!this.model.children || !this.model.children.length) return;
let textArray = [];
this.model.children.forEach(child => {
if (child.type === 'damage'){
let { result } = evaluateString(child.amount, scope);
textArray.push(`${result} ${child.damageType}`);
} else if (child.type === 'savingThrow'){
textArray.push(`DC ${child.dcResult} ${child.name}`);
}
});
return textArray.join(' ');
},
},
methods: {
click(e){

View File

@@ -1,49 +0,0 @@
<template lang="html">
<v-list-tile
class="ability-list-tile"
avatar
v-on="hasClickListener ? {click} : {}"
>
<v-list-tile-avatar color="grey darken-1">
<computed
signed
:value="model.rollBonus"
class="white--text headline"
/>
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>
{{ model.name }}
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</template>
<script>
import ComputedForCreature from '/imports/ui/components/computation/ComputedForCreature.vue';
export default {
components: {
Computed: ComputedForCreature,
},
props: {
model: {
type: Object,
required: true,
}
},
computed: {
hasClickListener(){
return this.$listeners && this.$listeners.click
},
},
methods: {
click(e){
this.$emit('click', e);
},
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,31 +1,33 @@
<template lang="html">
<div class="action-viewer">
<property-field
name="Action type"
:value="model.actionType"
/>
<property-field
name="Target"
:value="model.target"
/>
<property-field
v-if="model.tags.length"
name="tags"
:value="model.tags.join(', ')"
/>
<property-field
name="Uses"
<div class="layout row wrap align-center">
<div>
{{ model.actionType }}
</div>
<div class="flex">
<property-tags :tags="model.tags" />
</div>
<div v-if="model.usesResult">
{{ model.usesResult - (model.usesUsed) }}/{{ model.usesResult }}
</div>
</div>
<div
v-if="attack"
class="layout row justify-center align-center ma-3"
>
<computed :value="model.uses"/>
</property-field>
<property-field
name="Uses used"
:value="model.usesUsed"
/>
<property-field
name="Reset"
:value="reset"
/>
<div class="headline mr-2">
{{ rollBonus }}
</div>
<em>
to hit
</em>
</div>
<div
v-if="reset"
class="my-2"
>
{{ reset }}
</div>
<property-description
v-if="model.description"
:value="model.description"
@@ -35,12 +37,12 @@
<script>
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js';
import ComputedForCreature from '/imports/ui/components/computation/ComputedForCreature.vue';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
export default {
mixins: [propertyViewerMixin],
components: {
Computed: ComputedForCreature,
props: {
attack: Boolean,
},
computed: {
reset(){
@@ -51,7 +53,10 @@ export default {
return 'Reset on a long rest';
}
return undefined;
}
},
rollBonus(){
return numberToSignedString(this.model.rollBonusResult);
},
},
}
</script>

View File

@@ -1,37 +1,18 @@
<template lang="html">
<div class="attack-viewer">
<property-field name="Attack roll bonus">
<computed
signed
:value="model.rollBonus"
/>
</property-field>
<action-viewer :model="model" />
</div>
<action-viewer
:model="model"
attack
/>
</template>
<script>
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js';
import ComputedForCreature from '/imports/ui/components/computation/ComputedForCreature.vue';
import ActionViewer from '/imports/ui/properties/viewers/ActionViewer.vue';
export default {
components: {
ActionViewer,
Computed: ComputedForCreature,
},
mixins: [propertyViewerMixin],
computed: {
reset(){
let reset = this.model.reset
if (reset === 'shortRest'){
return 'Reset on a short rest';
} else if (reset === 'longRest'){
return 'Reset on a long rest';
}
return undefined;
}
},
}
</script>

View File

@@ -7,7 +7,7 @@
/>
{{ model.damageType }}
<span v-if="model.damageType !== 'healing'">
damage
&nbsp;damage
</span>
</div>
</template>