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,
defaultValue: {},
},
// Stats that are computed and denormalised outside of recomputation
denormalizedStats: {
type: Object,
defaultValue: {},
},
// Sum of all XP gained by this character
xp: {
'denormalizedStats.xp': {
type: SimpleSchema.Integer,
defaultValue: 0,
},
// Sum of all levels granted by milestone XP
xpLevels: {
'denormalizedStats.milestoneLevels': {
type: SimpleSchema.Integer,
defaultValue: 0,
},
weightCarried: {
// Sum of all weights of items and containers that are carried
'denormalizedStats.weightCarried': {
type: Number,
defaultValue: 0,
},
level: {
type: SimpleSchema.Integer,
defaultValue: 0,
},
type: {
type: String,
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
// computation process
export default class ComputationMemo {
constructor(props){
constructor(props, creature){
this.statsByVariableName = {};
this.extraStatsByVariableName = {};
this.statsById = {};
@@ -51,6 +51,15 @@ export default class ComputationMemo {
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){
this.originalPropsById[prop._id] = cloneDeep(prop);
@@ -251,4 +260,10 @@ const propDetailsByType = {
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 writeAlteredProperties from '/imports/api/creature/computation/writeAlteredProperties.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({
@@ -17,8 +18,9 @@ export const recomputeCreature = new ValidatedMethod({
}).validator(),
run({charId}) {
let creature = Creatures.findOne(charId);
// Permission
assertEditPermission(charId, this.userId);
assertEditPermission(creature, this.userId);
// Work, call this direcly if you are already in a method that has checked
// for permission to edit a given character
recomputeCreatureById(charId);
@@ -35,6 +37,11 @@ const calculationPropertyTypes = [
'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,
* distilling down effects and proficiencies into the final stats that make up
@@ -71,14 +78,15 @@ const calculationPropertyTypes = [
* - Mark the stat as computed
* - Write the computed results back to the database
*/
export function recomputeCreatureById(creatureId){
function recomputeCreatureByDoc(creature){
const creatureId = creature._id;
let props = getActiveProperties({
ancestorId: creatureId,
filter: {type: {$in: calculationPropertyTypes}},
includeUntoggled: true,
// TODO filter out expensive fields, particularly icon field
});
let computationMemo = new ComputationMemo(props);
let computationMemo = new ComputationMemo(props, creature);
computeMemo(computationMemo);
writeAlteredProperties(computationMemo);
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 { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import Creatures from '/imports/api/creature/Creatures.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/recomputeCreature.js';
let Experiences = new Mongo.Collection('experiences');
@@ -47,13 +48,19 @@ Experiences.attachSchema(ExperienceSchema);
const insertExperienceForCreature = function({experience, creatureId, userId}){
assertEditPermission(creatureId, userId);
if (experience.xp){
Creatures.update(creatureId, {$inc: {xp: experience.xp}});
Creatures.update(creatureId, {$inc: {
'denormalizedStats.xp': experience.xp
}});
}
if (experience.levels) {
Creatures.update(creatureId, {$inc: {xpLevels: experience.levels}});
Creatures.update(creatureId, {$inc: {
'denormalizedStats.milestoneLevels': experience.levels
}});
}
experience.creatureId = creatureId;
return Experiences.insert(experience);
let id = Experiences.insert(experience);
recomputeCreatureById(creatureId);
return id;
};
const insertExperience = new ValidatedMethod({
@@ -115,13 +122,19 @@ const removeExperience = new ValidatedMethod({
let creatureId = experience.creatureId
assertEditPermission(creatureId, userId);
if (experience.xp){
Creatures.update(creatureId, {$inc: {xp: -experience.xp}});
Creatures.update(creatureId, {$inc: {
'denormalizedStats.xp': -experience.xp
}});
}
if (experience.levels) {
Creatures.update(creatureId, {$inc: {xpLevels: -experience.levels}});
Creatures.update(creatureId, {$inc: {
'denormalizedStats.milestoneLevels': -experience.levels
}});
}
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);
let xp = 0;
let xpLevels = 0;
let milestoneLevels = 0;
Experiences.find({
creatureId
}, {
fields: {xp: 1, levels: 1}
}).forEach(experience => {
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 }}
</v-card-title>
<v-list v-if="highestClassLevels.length">
<v-list>
<v-list-tile>
<v-list-tile-content>
<v-list-tile-title v-if="creature.xpLevels">
{{ creature.xpLevels }} Levels gained
<v-list-tile-title
v-if="
creature.variables.milestoneLevels &&
creature.variables.milestoneLevels.value
"
>
{{ creature.variables.milestoneLevels.value }} Milestone levels
</v-list-tile-title>
<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-content>
<v-list-tile-action>
@@ -161,7 +170,8 @@ export default {
elementId: 'experience-add-button',
data: {
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',
data: {
creatureId: this.creatureId,
startAsMilestone: this.creature.variables.milestoneLevels &&
!!this.creature.variables.milestoneLevels.value,
},
});
},

View File

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

View File

@@ -5,6 +5,14 @@
Experiences
</v-toolbar-title>
<v-spacer />
<v-btn
icon
flat
data-id="experience-add-button"
@click="addExperience"
>
<v-icon>add</v-icon>
</v-btn>
<v-btn
icon
flat
@@ -14,33 +22,56 @@
</v-btn>
</template>
<v-list>
<v-list-tile
v-for="experience in experiences"
:key="experience._id"
<v-slide-x-transition
group
mode="out"
>
<v-list-tile-content>
<v-list-tile-title>
{{ experience.name }}
</v-list-tile-title>
</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-list-tile
v-for="experience in experiences"
:key="experience._id"
:data-id="experience._id"
>
<v-list-tile-action class="mr-3">
<v-list-tile-action-text>
{{ formatDate(experience.date) }}
</v-list-tile-action-text>
</v-list-tile-action>
<v-list-tile-content>
<template v-if="experience.name">
<v-list-tile-title>
{{ experience.name }}
</v-list-tile-title>
<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>
</dialog-base>
</template>
<script>
import { format } from 'date-fns';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import Experiences, { removeExperience, recomputeExperiences } from '/imports/api/creature/experience/Experiences.js';
export default {
components: {
DialogBase,
@@ -50,6 +81,9 @@ export default {
type: String,
required: true,
},
startAsMilestone: {
type: Boolean,
},
},
data(){ return {
experiencesRemovalLoading: new Set(),
@@ -70,6 +104,21 @@ export default {
}
},
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){
this.experiencesRemovalLoading.add(experienceId);
removeExperience.call({experienceId}, (error) => {
@@ -84,6 +133,19 @@ export default {
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>