Added simple feature UI components and insertion dialog

This commit is contained in:
Stefan Zermatten
2019-02-25 15:38:57 +02:00
parent e5ff116208
commit b7a4a3d3fa
23 changed files with 579 additions and 93 deletions

View File

@@ -36,6 +36,9 @@ let creatureSchema = schema({
level: {type: SimpleSchema.Integer, defaultValue: 0},
type: {type: String, defaultValue: "pc", allowedValues: ["pc", "npc", "monster"]},
//computed
variables: {type: Object, blackbox: true},
//permissions
owner: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
readers: {type: Array, defaultValue: [], index: 1},

View File

@@ -94,9 +94,48 @@ function writeCreature(char) {
writeSkills(char);
writeDamageMultipliers(char);
writeEffects(char);
Creatures.update(char.id, {$set: {level: char.level}});
writeCreatureDoc(char);
};
function writeCreatureDoc(char) {
// Store all the variables, using the same priority as computation evaluation
// Attributes
let variables = {};
for (let key in char.atts){
variables[key] = char.atts[key].result;
if (
char.atts[key].attributeType === 'ability' &&
!variables.hasOwnProperty(key + 'Mod')
){
variables[key + 'Mod'] = char.atts[key].mod;
}
}
for (let key in char.skills){
if (!variables.hasOwnProperty(key)){
variables[key] = char.skills[key].result;
}
}
// Damage Multipliers
for (let key in char.dms){
if (!variables.hasOwnProperty(key)){
variables[key] = char.dms[key].result;
}
}
// Class levels
for (let key in char.classes){
if (!variables.hasOwnProperty(key + 'Level')){
variables[key + 'Level'] = char.classes[key].level;
}
}
// Creature level
if (!variables.hasOwnProperty('level')){
variables['level'] = char.level;
}
// Write the creature
Creatures.update(char.id, {$set: {level: char.level, variables}});
}
/*
* Write all the attributes from the in-memory char object to the Attirbute docs
*/

View File

@@ -2,6 +2,7 @@ import {makeChild} from "/imports/api/parenting.js";
import SimpleSchema from 'simpl-schema';
import schema from '/imports/api/schema.js';
import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js";
import OrderSchema from "/imports/api/creature/subSchemas/OrderSchema.js";
import { canEditCreature } from '/imports/api/creature/creaturePermission.js';
import { recomputeCreatureById } from '/imports/api/creature/creatureComputation.js'
import { getHighestOrder } from '/imports/api/order.js';
@@ -29,12 +30,6 @@ let attributeSchema = schema({
regEx: /^\w*[a-z]\w*$/i,
index: 1,
},
// Attributes need to store their order to keep the sheet consistent
order: {
type: SimpleSchema.Integer,
// Indexed because we update order in bulk using the current order as a query
index: 1,
},
type: {
type: String,
allowedValues: [
@@ -58,6 +53,10 @@ let attributeSchema = schema({
type: Number,
defaultValue: 0,
},
enabled: {
type: Boolean,
defaultValue: true,
},
// The computed modifier, provided the attribute is an ability
mod: {
type: SimpleSchema.Integer,
@@ -82,6 +81,8 @@ let attributeSchema = schema({
type: Number,
optional: true,
},
// Attributes need to store their order to keep the sheet consistent
order: OrderSchema(),
color: ColorSchema(),
});
@@ -108,10 +109,9 @@ const insertAttribute = new ValidatedMethod({
validate: schema({
attribute: {
type: Object,
blackbox: true,
type: attributeSchema.omit('order', 'parent'),
},
}).validator(),
}).validator({ clean: true }),
run({attribute}) {
const charId = attribute.charId;

View File

@@ -1,6 +1,10 @@
import SimpleSchema from 'simpl-schema';
import schema from '/imports/api/schema.js';
import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js";
import OrderSchema from "/imports/api/creature/subSchemas/OrderSchema.js";
import { canEditCreature } from '/imports/api/creature/creaturePermission.js';
import { recomputeCreatureById } from '/imports/api/creature/creatureComputation.js'
import { getHighestOrder } from '/imports/api/order.js';
import {makeParent} from "/imports/api/parenting.js";
let Features = new Mongo.Collection("features");
@@ -13,17 +17,58 @@ let featureSchema = schema({
used: {type: SimpleSchema.Integer, defaultValue: 0},
reset: {
type: String,
allowedValues: ["manual", "longRest", "shortRest"],
defaultValue: "manual",
allowedValues: ["longRest", "shortRest"],
optional: true,
},
enabled: {type: Boolean, defaultValue: true},
alwaysEnabled:{type: Boolean, defaultValue: true},
order: {
type: SimpleSchema.Integer,
// Indexed because we update order in bulk using the current order as a query
index: 1,
defaultValue: 0,
},
order: OrderSchema(),
color: ColorSchema(),
});
Features.attachSchema(featureSchema);
Features.attachSchema(ColorSchema);
//Features.attachBehaviour("softRemovable");
makeParent(Features, ["name", "enabled"]); //parents of effects and attacks
const insertFeature = new ValidatedMethod({
name: "Features.methods.insert",
validate: schema({
feature: {
type: featureSchema.omit('order', 'parent'),
},
}).validator({clean: true}),
run({feature}) {
const charId = feature.charId;
if (canEditCreature(charId, this.userId)){
// Set order
feature.order = getHighestOrder({
collection: Features,
charId,
}) + 1;
// Set parent
feature.parent = {
id: charId,
collection: 'Creatures',
};
// Insert
let featureId = Features.insert(feature);
recomputeCreatureById(charId);
return featureId;
}
},
});
export default Features;
export { insertFeature }

View File

@@ -1,6 +1,3 @@
import SimpleSchema from 'simpl-schema';
import schema from '/imports/api/schema.js';
const ColorSchema = ({optional = false} = {}) => ({
type: String,
defaultValue: "#9E9E9E",

View File

@@ -0,0 +1,6 @@
const OrderSchema = () => ({
type: Number,
index: true,
});
export default OrderSchema;

View File

@@ -47,6 +47,7 @@
import DialogStack from '/imports/ui/dialogStack/DialogStack.Story.vue';
import EffectEdit from '/imports/ui/components/EffectEdit.Story.vue';
import EffectEditExpansionList from '/imports/ui/components/EffectEditExpansionList.Story.vue';
import FeatureCard from '/imports/ui/components/FeatureCard.Story.vue';
import HealthBar from '/imports/ui/components/HealthBar.Story.vue';
import HitDiceListTile from '/imports/ui/components/HitDiceListTile.Story.vue';
import IconSearch from '/imports/ui/components/IconSearch.Story.vue';
@@ -64,6 +65,7 @@
DialogStack,
EffectEdit,
EffectEditExpansionList,
FeatureCard,
HealthBar,
HitDiceListTile,
IconSearch,

View File

@@ -15,6 +15,9 @@
>
<v-tab>
Stats
</v-tab>
<v-tab>
Features
</v-tab>
<v-tab>
Tree
@@ -25,6 +28,9 @@
<v-tabs-items v-model="tab">
<v-tab-item>
<stats-tab :char-id="character._id"/>
</v-tab-item>
<v-tab-item>
<features-tab :char-id="character._id"/>
</v-tab-item>
<v-tab-item>
<character-tree-view :char-id="character._id"/>
@@ -43,6 +49,7 @@
import { mapMutations } from "vuex";
import { theme } from '/imports/ui/theme.js';
import StatsTab from '/imports/ui/character/StatsTab.vue';
import FeaturesTab from '/imports/ui/character/FeaturesTab.vue';
import CharacterTreeView from '/imports/ui/character/CharacterTreeView.vue';
import { recomputeCreature } from '/imports/api/creature/creatureComputation.js'
@@ -53,6 +60,7 @@
},
components: {
StatsTab,
FeaturesTab,
CharacterTreeView,
},
data(){return {

View File

@@ -0,0 +1,73 @@
<template lang="html">
<div class="features">
<column-layout>
<div v-for="feature in features" :key="feature._id">
<feature-card
v-bind="feature"
:data-id="feature._id"
/>
</div>
</column-layout>
<v-btn fixed fab bottom right
color="primary"
@click="insertFeature"
data-id="insert-feature-fab"
>
<v-icon>add</v-icon>
</v-btn>
</div>
</template>
<script>
import Creatures from '/imports/api/creature/Creatures.js';
import Features from '/imports/api/creature/properties/Features.js';
import { insertFeature } from '/imports/api/creature/properties/Features.js';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import FeatureCard from '/imports/ui/components/FeatureCard.vue';
import { evaluateComputation, evaluateString } from '/imports/ui/utility/evaluate.js';
export default {
props: {
charId: String,
},
components: {
ColumnLayout,
FeatureCard,
},
meteor: {
features(){
let char = Creatures.findOne(this.charId, {fields: {variables: 1}});
if (!char) return [];
let vars = char.variables;
return Features.find({
charId: this.charId,
}, {
sort: {order: 1},
}).map(f => {
f.uses = evaluateComputation(f.uses, vars);
f.description = evaluateString(f.description, vars);
return f;
});
},
},
methods: {
insertFeature(){
const charId = this.charId;
this.$store.commit('pushDialogStack', {
component: 'feature-creation-dialog',
elementId: 'insert-feature-fab',
callback(feature){
if (!feature) return;
feature.charId = charId;
let featureId = insertFeature.call({feature});
return featureId
}
});
}
}
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -9,16 +9,15 @@
@change="change"
:debounce-time="0"
/>
<div slot="actions">
<v-spacer/>
<v-btn
flat
:disabled="!valid"
@click="$store.dispatch('popDialogStack', attribute)"
>
Insert Attribute
</v-btn>
</div>
<v-spacer slot="actions"/>
<v-btn
flat
slot="actions"
:disabled="!valid"
@click="$store.dispatch('popDialogStack', attribute)"
>
Insert Attribute
</v-btn>
</dialog-base>
</template>
@@ -26,7 +25,6 @@
import AttributeEdit from '/imports/ui/components/AttributeEdit.vue';
import Attributes from '/imports/api/creature/properties/Attributes.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { Tracker } from 'meteor/tracker';
export default {
components: {

View File

@@ -49,7 +49,6 @@
/>
<smart-select
label="Reset"
append-icon="arrow_drop_down"
clearable
:items="resetOptions"
:value="attribute.reset"

View File

@@ -0,0 +1,51 @@
<template lang="html">
<column-layout>
<div
v-for="(feature, index) in features"
:key="index"
>
<feature-card
v-bind="feature"
/>
</div>
</column-layout>
</template>
<script>
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import FeatureCard from '/imports/ui/components/FeatureCard.vue';
export default {
dontWrap: true,
components: {
ColumnLayout,
FeatureCard,
},
data(){return {
features: [
{
name: 'Feature 1',
enabled: true,
alwaysEnabled: true,
description: `
blah blah, with
spacing
`,
color: '#f44336',
}, {
name: 'Feature 2',
enabled: false,
alwaysEnabled: false,
description: `Short Description`,
uses: 5,
used: 2,
},
],
}},
}
</script>
<style lang="css' scoped>
</style>

View File

@@ -0,0 +1,60 @@
<template lang="html">
<toolbar-card :color="color" @click="$emit('click')">
<span slot="toolbar">
{{name}}
</span>
<v-spacer slot="toolbar"/>
<v-checkbox
hide-details
class="shrink"
v-if="!alwaysEnabled"
:value="enabled"
@change="enabled => $emit('change', {enabled})"
slot="toolbar"
/>
<v-card-text>
{{description}}
</v-card-text>
<v-card-actions v-if="uses">
<v-spacer/>
<v-btn
flat
:disabled="uses - used <= 0"
@click="$emit('used')"
>
Use
</v-btn>
<v-btn
flat
:disabled="!used"
@click="$emit('reset')"
>
Reset
</v-btn>
</v-card-actions>
</toolbar-card>
</template>
<script>
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
export default {
props: {
charId: String,
name: String,
description: String,
uses: Number,
used: Number,
reset: String,
color: String,
enabled: Boolean,
alwaysEnabled: Boolean,
},
components: {
ToolbarCard,
},
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,78 @@
<template lang="html">
<dialog-base>
<div slot="toolbar">
New Feature
</div>
<feature-edit
:feature="feature"
:errors="errors"
@change="change"
:debounce-time="0"
/>
<v-spacer slot="actions"/>
<v-btn
flat
slot="actions"
:disabled="!valid"
@click="$store.dispatch('popDialogStack', feature)"
>
Insert Feature
</v-btn>
</dialog-base>
</template>
<script>
import FeatureEdit from '/imports/ui/components/FeatureEdit.vue';
import Features from '/imports/api/creature/properties/Features.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
export default {
components: {
FeatureEdit,
DialogBase,
},
data(){ return {
feature: {
name: 'New Feature',
description: null,
uses: null,
used: 0,
reset: null,
enabled: true,
alwaysEnabled: true,
color: '#9E9E9E',
},
valid: true,
}},
methods: {
change(update, ack){
for (key in update){
this.feature[key] = update[key];
}
if (ack) ack();
},
},
created(){
this.validationContext = Features.simpleSchema().newContext();
},
computed: {
errors(){
this.valid = true;
let cleanAtt = this.validationContext.clean(this.feature)
this.validationContext.validate(cleanAtt, {keys: [
'name', 'description', 'uses', 'used', 'reset', 'enabled',
'alwaysEnabled', 'color',
]});
let errors = {};
this.validationContext.validationErrors().forEach(error => {
if (this.valid) this.valid = false;
errors[error.name] = Features.simpleSchema().messageForError(error);
});
return errors;
},
},
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,113 @@
<template lang="html">
<div>
<text-field
label="Name"
:value="feature.name"
@change="(name, ack) => $emit('change', {name}, ack)"
:error-messages="errors.name"
:debounce-time="debounceTime"
/>
<text-field
label="Used"
type="number"
:value="feature.used"
@change="(used, ack) => $emit('change', {used}, ack)"
:error-messages="errors.used"
:debounce-time="debounceTime"
/>
<text-field
label="Uses"
:value="feature.uses"
@change="(uses, ack) => $emit('change', {uses}, ack)"
:error-messages="errors.uses"
:debounce-time="debounceTime"
/>
<smart-select
label="Reset"
clearable
:items="resetOptions"
:value="feature.reset"
:error-messages="errors.reset"
:menu-props="{auto: true, lazy: true}"
@change="(reset, ack) => $emit('change', {reset}, ack)"
:debounce-time="debounceTime"
/>
<smart-select
label="Enabled"
:items="enabledOptions"
:value="enabledStatus"
:error-messages="errors.enabled || errors.alwaysEnabled"
:menu-props="{auto: true, lazy: true}"
@change="changeEnabled"
:debounce-time="debounceTime"
/>
<text-area
label="Description"
:value="feature.description"
:error-messages="errors.description"
@change="(description, ack) => $emit('change', {description}, ack)"
:debounce-time="debounceTime"
/>
</div>
</template>
<script>
export default {
props: {
feature: {
type: Object,
default: () => ({}),
},
errors: {
type: Object,
default: () => ({}),
},
debounceTime: Number,
},
data(){ return{
resetOptions: [
{
text: 'Short rest',
value: 'shortRest',
}, {
text: 'Long rest',
value: 'longRest',
}
],
enabledOptions: [
{
text: 'Always enabled',
value: 'always',
}, {
text: 'Enabled',
value: 'enabled',
}, {
text: 'Disabled',
value: 'disabled',
}
],
}},
computed: {
enabledStatus(){
if (!this.feature) return;
if (this.feature.alwaysEnabled) return 'always';
if (this.feature.enabled) return 'enabled';
return 'disabled';
},
},
methods: {
changeEnabled(value, ack){
if (value === 'always'){
this.$emit('change', {enabled: true, alwaysEnabled: true}, ack);
} else if (value === 'enabled'){
this.$emit('change', {enabled: true, alwaysEnabled: false}, ack);
} else if (value === 'disabled'){
this.$emit('change', {enabled: false, alwaysEnabled: false}, ack);
}
}
}
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,38 @@
<template lang="html">
<v-card>
<v-toolbar
flat
style="transform: none;"
@click="$emit('click')"
:color="color"
:dark="isDark"
>
<slot name="toolbar"/>
</v-toolbar>
<div>
<slot/>
</div>
</v-card>
</template>
<script>
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
export default {
props: {
color: {
type: String,
default(){
return this.$vuetify.theme.secondary;
},
},
},
computed: {
isDark(){
return isDarkColor(this.color);
}
}
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -4,6 +4,7 @@
:loading="loading"
:error-messages="errors"
:value="safeValue"
:auto-grow="autoGrow"
@input="input"
@focus="focused = true"
@blur="focused = false"
@@ -15,5 +16,11 @@
export default {
mixins: [SmartInput],
props: {
autoGrow: {
type: Boolean,
default: true,
},
},
};
</script>

View File

@@ -16,7 +16,7 @@
</template>
</v-toolbar>
<v-card-text id="base-dialog-body" v-scroll:#base-dialog-body="onScroll">
<v-tabs-items :value="isEditing ? 1 : 0">
<v-tabs-items :value="isEditing ? 1 : 0" touchless>
<v-tab-item>
<slot/>
</v-tab-item>

View File

@@ -1,11 +1,13 @@
import AttributeDialog from '/imports/ui/components/AttributeDialog.vue';
import AttributeDialogContainer from '/imports/ui/components/AttributeDialogContainer.vue';
import AttributeCreationDialog from '/imports/ui/components/AttributeCreationDialog.vue';
import FeatureCreationDialog from '/imports/ui/components/FeatureCreationDialog.vue';
import SkillDialogContainer from '/imports/ui/components/SkillDialogContainer.vue';
export default {
AttributeDialog,
AttributeDialogContainer,
AttributeCreationDialog,
FeatureCreationDialog,
SkillDialogContainer,
};

View File

@@ -217,7 +217,7 @@
right: 0;
bottom: 0;
pointer-events: none;
z-index: 3;
z-index: 4;
}
.dialog-sizer {
position: relative;

View File

@@ -0,0 +1,29 @@
// Computations resolve to numbers
// vars is a dict of variables to substitute
function evaluateComputation(string, vars){
if (!string) return string;
// Replace all the string variables with numbers if possible
let substitutedString = string.replace(
/\w*[a-z]\w*/gi,
sub => vars.hasOwnProperty(sub) ? vars[sub] : sub
);
// Evaluate the expression to a number or return it as is.
try {
return math.eval(substitutedString);
} catch (e){
return substitutedString;
}
};
// Strings can have computations in bracers like so: {computation}
// vars is a dict of variables to substitute
function evaluateString(string, vars){
if (!string) return string;
// Compute everything inside bracers
return string.replace(/\{([^\{\}]*)\}/g, function(match, p1){
return evaluateComputation(p1, vars);
});
}
export { evaluateComputation, evaluateString };

62
app/package-lock.json generated
View File

@@ -1406,37 +1406,6 @@
"requires": {
"inherits": "~2.0.1",
"readable-stream": "^2.0.2"
},
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
},
"dependencies": {
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
}
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"stream-http": {
@@ -1449,37 +1418,6 @@
"readable-stream": "^2.3.3",
"to-arraybuffer": "^1.0.0",
"xtend": "^4.0.0"
},
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
},
"dependencies": {
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
}
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"string_decoder": {

View File

@@ -2,5 +2,5 @@ import SimpleSchema from 'simpl-schema';
import schema from '/imports/api/schema.js';
if (Meteor.isDevelopment){
SimpleSchema.debug = true
//SimpleSchema.debug = true
}