Iterated on XP system

This commit is contained in:
Stefan Zermatten
2020-06-05 21:48:28 +02:00
parent d1e7eb2fa0
commit db1ae5db3d
7 changed files with 163 additions and 60 deletions

View File

@@ -70,24 +70,26 @@ let CreatureSchema = new SimpleSchema({
type: deathSaveSchema, type: deathSaveSchema,
defaultValue: {}, defaultValue: {},
}, },
// Stats that are computed and denormalised outside of recomputation
denormalizedStats: {
type: Object,
defaultValue: {},
},
// Sum of all XP gained by this character // Sum of all XP gained by this character
xp: { 'denormalizedStats.xp': {
type: SimpleSchema.Integer, type: SimpleSchema.Integer,
defaultValue: 0, defaultValue: 0,
}, },
// Sum of all levels granted by milestone XP // Sum of all levels granted by milestone XP
xpLevels: { 'denormalizedStats.milestoneLevels': {
type: SimpleSchema.Integer, type: SimpleSchema.Integer,
defaultValue: 0, defaultValue: 0,
}, },
weightCarried: { // Sum of all weights of items and containers that are carried
'denormalizedStats.weightCarried': {
type: Number, type: Number,
defaultValue: 0, defaultValue: 0,
}, },
level: {
type: SimpleSchema.Integer,
defaultValue: 0,
},
type: { type: {
type: String, type: String,
defaultValue: 'pc', defaultValue: 'pc',

View File

@@ -3,7 +3,7 @@ import { includes, cloneDeep } from 'lodash';
// The computation memo is an in-memory data structure used only during the // The computation memo is an in-memory data structure used only during the
// computation process // computation process
export default class ComputationMemo { export default class ComputationMemo {
constructor(props){ constructor(props, creature){
this.statsByVariableName = {}; this.statsByVariableName = {};
this.extraStatsByVariableName = {}; this.extraStatsByVariableName = {};
this.statsById = {}; this.statsById = {};
@@ -51,6 +51,15 @@ export default class ComputationMemo {
this.addClassLevel(prop); this.addClassLevel(prop);
} }
}); });
for (let name in creature.denormalizedStats){
if (!this.statsByVariableName[name]){
this.statsByVariableName[name] = {
variableName: name,
value: creature.denormalizedStats[name],
computationDetails: propDetailsByType.denormalizedStat(),
}
}
}
} }
registerProperty(prop){ registerProperty(prop){
this.originalPropsById[prop._id] = cloneDeep(prop); this.originalPropsById[prop._id] = cloneDeep(prop);
@@ -251,4 +260,10 @@ const propDetailsByType = {
disabledByToggle: false, disabledByToggle: false,
}; };
}, },
denormalizedStat(){
return {
toggleAncestors: [],
disabledByToggle: false,
};
}
} }

View File

@@ -6,7 +6,8 @@ import computeMemo from '/imports/api/creature/computation/computeMemo.js';
import getActiveProperties from '/imports/api/creature/getActiveProperties.js'; import getActiveProperties from '/imports/api/creature/getActiveProperties.js';
import writeAlteredProperties from '/imports/api/creature/computation/writeAlteredProperties.js'; import writeAlteredProperties from '/imports/api/creature/computation/writeAlteredProperties.js';
import writeCreatureVariables from '/imports/api/creature/computation/writeCreatureVariables.js'; import writeCreatureVariables from '/imports/api/creature/computation/writeCreatureVariables.js';
import { recomputeDamageMultipliersById } from '/imports/api/creature/damageMultiplierDenormalise/recomputeDamageMultipliers.js' import { recomputeDamageMultipliersById } from '/imports/api/creature/damageMultiplierDenormalise/recomputeDamageMultipliers.js';
import Creatures from '/imports/api/creature/Creatures.js';
export const recomputeCreature = new ValidatedMethod({ export const recomputeCreature = new ValidatedMethod({
@@ -17,8 +18,9 @@ export const recomputeCreature = new ValidatedMethod({
}).validator(), }).validator(),
run({charId}) { run({charId}) {
let creature = Creatures.findOne(charId);
// Permission // Permission
assertEditPermission(charId, this.userId); assertEditPermission(creature, this.userId);
// Work, call this direcly if you are already in a method that has checked // Work, call this direcly if you are already in a method that has checked
// for permission to edit a given character // for permission to edit a given character
recomputeCreatureById(charId); recomputeCreatureById(charId);
@@ -35,6 +37,11 @@ const calculationPropertyTypes = [
'toggle', 'toggle',
]; ];
export function recomputeCreatureById(creatureId){
let creature = Creatures.findOne(creatureId);
recomputeCreatureByDoc(creature);
}
/** /**
* This function is the heart of DiceCloud. It recomputes a creature's stats, * This function is the heart of DiceCloud. It recomputes a creature's stats,
* distilling down effects and proficiencies into the final stats that make up * distilling down effects and proficiencies into the final stats that make up
@@ -71,14 +78,15 @@ const calculationPropertyTypes = [
* - Mark the stat as computed * - Mark the stat as computed
* - Write the computed results back to the database * - Write the computed results back to the database
*/ */
export function recomputeCreatureById(creatureId){ function recomputeCreatureByDoc(creature){
const creatureId = creature._id;
let props = getActiveProperties({ let props = getActiveProperties({
ancestorId: creatureId, ancestorId: creatureId,
filter: {type: {$in: calculationPropertyTypes}}, filter: {type: {$in: calculationPropertyTypes}},
includeUntoggled: true, includeUntoggled: true,
// TODO filter out expensive fields, particularly icon field // TODO filter out expensive fields, particularly icon field
}); });
let computationMemo = new ComputationMemo(props); let computationMemo = new ComputationMemo(props, creature);
computeMemo(computationMemo); computeMemo(computationMemo);
writeAlteredProperties(computationMemo); writeAlteredProperties(computationMemo);
writeCreatureVariables(computationMemo, creatureId); writeCreatureVariables(computationMemo, creatureId);

View File

@@ -3,6 +3,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { getUserTier } from '/imports/api/users/patreon/tiers.js'; import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import Creatures from '/imports/api/creature/Creatures.js'; import Creatures from '/imports/api/creature/Creatures.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/recomputeCreature.js';
let Experiences = new Mongo.Collection('experiences'); let Experiences = new Mongo.Collection('experiences');
@@ -47,13 +48,19 @@ Experiences.attachSchema(ExperienceSchema);
const insertExperienceForCreature = function({experience, creatureId, userId}){ const insertExperienceForCreature = function({experience, creatureId, userId}){
assertEditPermission(creatureId, userId); assertEditPermission(creatureId, userId);
if (experience.xp){ if (experience.xp){
Creatures.update(creatureId, {$inc: {xp: experience.xp}}); Creatures.update(creatureId, {$inc: {
'denormalizedStats.xp': experience.xp
}});
} }
if (experience.levels) { if (experience.levels) {
Creatures.update(creatureId, {$inc: {xpLevels: experience.levels}}); Creatures.update(creatureId, {$inc: {
'denormalizedStats.milestoneLevels': experience.levels
}});
} }
experience.creatureId = creatureId; experience.creatureId = creatureId;
return Experiences.insert(experience); let id = Experiences.insert(experience);
recomputeCreatureById(creatureId);
return id;
}; };
const insertExperience = new ValidatedMethod({ const insertExperience = new ValidatedMethod({
@@ -115,13 +122,19 @@ const removeExperience = new ValidatedMethod({
let creatureId = experience.creatureId let creatureId = experience.creatureId
assertEditPermission(creatureId, userId); assertEditPermission(creatureId, userId);
if (experience.xp){ if (experience.xp){
Creatures.update(creatureId, {$inc: {xp: -experience.xp}}); Creatures.update(creatureId, {$inc: {
'denormalizedStats.xp': -experience.xp
}});
} }
if (experience.levels) { if (experience.levels) {
Creatures.update(creatureId, {$inc: {xpLevels: -experience.levels}}); Creatures.update(creatureId, {$inc: {
'denormalizedStats.milestoneLevels': -experience.levels
}});
} }
experience.creatureId = creatureId; experience.creatureId = creatureId;
return Experiences.remove(experienceId); let numRemoved = Experiences.remove(experienceId);
recomputeCreatureById(creatureId);
return numRemoved;
}, },
}); });
@@ -147,16 +160,20 @@ const recomputeExperiences = new ValidatedMethod({
assertEditPermission(creatureId, userId); assertEditPermission(creatureId, userId);
let xp = 0; let xp = 0;
let xpLevels = 0; let milestoneLevels = 0;
Experiences.find({ Experiences.find({
creatureId creatureId
}, { }, {
fields: {xp: 1, levels: 1} fields: {xp: 1, levels: 1}
}).forEach(experience => { }).forEach(experience => {
xp += experience.xp || 0; xp += experience.xp || 0;
xpLevels += experience.levels || 0; milestoneLevels += experience.levels || 0;
}); });
Creatures.update(creatureId, {$set: {xp, xpLevels}}); Creatures.update(creatureId, {$set: {
'denormalizedStats.xp': xp,
'denormalizedStats.milestoneLevels': milestoneLevels
}});
recomputeCreatureById(creatureId);
}, },
}); });

View File

@@ -28,14 +28,23 @@
> >
Level {{ creature.variables.level.value }} Level {{ creature.variables.level.value }}
</v-card-title> </v-card-title>
<v-list v-if="highestClassLevels.length"> <v-list>
<v-list-tile> <v-list-tile>
<v-list-tile-content> <v-list-tile-content>
<v-list-tile-title v-if="creature.xpLevels"> <v-list-tile-title
{{ creature.xpLevels }} Levels gained v-if="
creature.variables.milestoneLevels &&
creature.variables.milestoneLevels.value
"
>
{{ creature.variables.milestoneLevels.value }} Milestone levels
</v-list-tile-title> </v-list-tile-title>
<v-list-tile-title v-else> <v-list-tile-title v-else>
{{ creature.xp }} XP {{
creature.variables.xp &&
creature.variables.xp.value ||
0
}} XP
</v-list-tile-title> </v-list-tile-title>
</v-list-tile-content> </v-list-tile-content>
<v-list-tile-action> <v-list-tile-action>
@@ -161,7 +170,8 @@ export default {
elementId: 'experience-add-button', elementId: 'experience-add-button',
data: { data: {
creatureIds: [this.creatureId], creatureIds: [this.creatureId],
startAsMilestone: !!this.creature.xpLevels startAsMilestone: this.creature.variables.milestoneLevels &&
!!this.creature.variables.milestoneLevels.value,
}, },
}); });
}, },
@@ -171,6 +181,8 @@ export default {
elementId: 'experience-info-button', elementId: 'experience-info-button',
data: { data: {
creatureId: this.creatureId, creatureId: this.creatureId,
startAsMilestone: this.creature.variables.milestoneLevels &&
!!this.creature.variables.milestoneLevels.value,
}, },
}); });
}, },

View File

@@ -8,12 +8,6 @@
@push="push" @push="push"
@pull="pull" @pull="pull"
/> />
<p
v-if="error"
class="error"
>
{{ error }}
</p>
<div <div
slot="actions" slot="actions"
class="layout row justify-end" class="layout row justify-end"
@@ -21,7 +15,6 @@
<v-btn <v-btn
flat flat
:disabled="!valid" :disabled="!valid"
:loading="loading"
@click="insertExperience" @click="insertExperience"
> >
Insert Insert
@@ -67,26 +60,20 @@ export default {
schema: schema, schema: schema,
validationContext: schema.newContext(), validationContext: schema.newContext(),
debounceTime: 0, debounceTime: 0,
loading: false,
error: undefined,
}; };
}, },
methods:{ methods:{
insertExperience(){ insertExperience(){
this.loading = true;
let experience = this.schema.clean(this.model); let experience = this.schema.clean(this.model);
insertExperience.call({ let id = insertExperience.call({
experience, experience,
creatureIds: this.creatureIds, creatureIds: this.creatureIds,
}, (error) => { }, (error) => {
this.loading = false;
if (error){ if (error){
this.error = error.message || error;
console.error(error); console.error(error);
} else {
this.$store.dispatch('popDialogStack');
} }
}); });
this.$store.dispatch('popDialogStack', id);
} }
} }
} }

View File

@@ -5,6 +5,14 @@
Experiences Experiences
</v-toolbar-title> </v-toolbar-title>
<v-spacer /> <v-spacer />
<v-btn
icon
flat
data-id="experience-add-button"
@click="addExperience"
>
<v-icon>add</v-icon>
</v-btn>
<v-btn <v-btn
icon icon
flat flat
@@ -14,33 +22,56 @@
</v-btn> </v-btn>
</template> </template>
<v-list> <v-list>
<v-list-tile <v-slide-x-transition
v-for="experience in experiences" group
:key="experience._id" mode="out"
> >
<v-list-tile-content> <v-list-tile
<v-list-tile-title> v-for="experience in experiences"
{{ experience.name }} :key="experience._id"
</v-list-tile-title> :data-id="experience._id"
</v-list-tile-content> >
<v-list-tile-action> <v-list-tile-action class="mr-3">
<v-btn <v-list-tile-action-text>
icon {{ formatDate(experience.date) }}
flat </v-list-tile-action-text>
:loading="experiencesRemovalLoading.has(experience._id)" </v-list-tile-action>
@click="removeExperience(experience._id)" <v-list-tile-content>
> <template v-if="experience.name">
<v-icon>delete</v-icon> <v-list-tile-title>
</v-btn> {{ experience.name }}
</v-list-tile-action> </v-list-tile-title>
</v-list-tile> <v-list-tile-sub-title>
{{ xpText(experience) }}
</v-list-tile-sub-title>
</template>
<template v-else>
<v-list-tile-title>
{{ xpText(experience) }}
</v-list-tile-title>
</template>
</v-list-tile-content>
<v-list-tile-action>
<v-btn
icon
flat
:loading="experiencesRemovalLoading.has(experience._id)"
@click="removeExperience(experience._id)"
>
<v-icon>delete</v-icon>
</v-btn>
</v-list-tile-action>
</v-list-tile>
</v-slide-x-transition>
</v-list> </v-list>
</dialog-base> </dialog-base>
</template> </template>
<script> <script>
import { format } from 'date-fns';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue'; import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import Experiences, { removeExperience, recomputeExperiences } from '/imports/api/creature/experience/Experiences.js'; import Experiences, { removeExperience, recomputeExperiences } from '/imports/api/creature/experience/Experiences.js';
export default { export default {
components: { components: {
DialogBase, DialogBase,
@@ -50,6 +81,9 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
startAsMilestone: {
type: Boolean,
},
}, },
data(){ return { data(){ return {
experiencesRemovalLoading: new Set(), experiencesRemovalLoading: new Set(),
@@ -70,6 +104,21 @@ export default {
} }
}, },
methods: { methods: {
xpText(experience){
let xpText = [];
if (experience.levels === 1){
xpText.push('1 Milestone level');
} else if (experience.levels){
xpText.push(`${experience.levels} Milestone levels`);
}
if (experience.xp || !experience.levels){
xpText.push(`${experience.xp || 0} XP`);
}
return xpText.join(', ');
},
formatDate(date){
return format(date, 'YYYY-MM-DD');
},
removeExperience(experienceId){ removeExperience(experienceId){
this.experiencesRemovalLoading.add(experienceId); this.experiencesRemovalLoading.add(experienceId);
removeExperience.call({experienceId}, (error) => { removeExperience.call({experienceId}, (error) => {
@@ -84,6 +133,19 @@ export default {
if (error) console.error(error); if (error) console.error(error);
}); });
}, },
addExperience(){
this.$store.commit('pushDialogStack', {
component: 'experience-insert-dialog',
elementId: 'experience-add-button',
data: {
creatureIds: [this.creatureId],
startAsMilestone: this.startAsMilestone,
},
callback(id){
return id;
}
});
},
}, },
} }
</script> </script>