UI work to improve look and feel of Viewers

This commit is contained in:
Stefan Zermatten
2021-10-17 23:28:39 +02:00
parent 247353f0ed
commit bc6c857b6b
13 changed files with 420 additions and 196 deletions

View File

@@ -1,6 +1,7 @@
import _variable from './computeByType/computeVariable.js';
import action from './computeByType/computeAction.js';
import attribute from './computeByType/computeAttribute.js';
import skill from './computeByType/computeSkill.js';
import slot from './computeByType/computeSlot.js';
import container from './computeByType/computeContainer.js';
@@ -9,6 +10,7 @@ export default Object.freeze({
action,
attribute,
container,
skill,
slot,
spell: action,
});

View File

@@ -0,0 +1,28 @@
// If we compute this skill without a variable name, it just
// uses its base value, proficiency, and damage since no effects can target it
// If this skill does have a variable name, it is recomputed later
// by computeVariableAsSkill
export default function computeSkill(computation, node){
const prop = node.data;
prop.proficiency = prop.baseProficiency;
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
// Multiply the proficiency bonus by the actual proficiency
if(prop.proficiency === 0.49){
// Round down proficiency bonus in the special case
profBonus = Math.floor(profBonus * 0.5);
} else {
profBonus = Math.ceil(profBonus * prop.proficiency);
}
const ability = computation.scope[prop.ability];
prop.abilityMod = ability?.modifier || 0;
const base = prop.baseValue?.value || 0;
let result = base + prop.abilityMod + profBonus;
if (Number.isFinite(result)){
result = Math.floor(result);
}
prop.value = result;
}

View File

@@ -15,6 +15,7 @@ export default function(){
assert.equal(scope('strength').modifier, 1);
assert.equal(prop('referencesDexId').value, 4);
assert.equal(prop('hitDiceId').constitutionMod, 5);
assert.equal(prop('overriddenDexId').overridden, true, 'override properties with the same variable name');
assert.equal(
prop('parseErrorId').baseValue.value, null,
'Parse errors should null the value'
@@ -44,11 +45,22 @@ var testProperties = [
calculation: '12'
},
}),
clean({
_id: 'overriddenDexId',
variableName: 'dexterity',
type: 'attribute',
attributeType: 'ability',
order: 1,
baseValue: {
calculation: '15'
},
}),
clean({
_id: 'dexterityId',
variableName: 'dexterity',
type: 'attribute',
attributeType: 'ability',
order: 2,
baseValue: {
calculation: '15'
},

View File

@@ -20,6 +20,7 @@ let SkillSchema = createPropertySchema({
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
optional: true,
},
// The variable name of the ability this skill relies on
ability: {

View File

@@ -11,7 +11,7 @@ const unaryOperator = {
},
resolve(fn, node, scope, context){
const {result: rightNode} = resolve(fn, node.right, scope, context);
if (rightNode.parseType !== 'number'){
if (rightNode.valueType !== 'number'){
return {
result: unaryOperator.create({
operator: node.operator,

View File

@@ -39,13 +39,15 @@
},
meteor: {
children(){
return nodesToTree({
const children = nodesToTree({
collection: CreatureProperties,
ancestorId: this.root.id,
filter: this.filter,
includeFilteredDocAncestors: true,
includeFilteredDocDescendants: true,
});
this.$emit('length', children.length);
return children;
},
},
methods: {

View File

@@ -41,28 +41,34 @@
This property can't be viewed yet.
</p>
</v-fade-transition>
<template v-if="!editing && !embedded">
<v-divider class="my-2" />
<property-field
v-if="!editing && !embedded && childrenLength"
name="Child properties"
:cols="{cols: 12}"
>
<creature-properties-tree
v-if="!editing"
style="width: 100%;"
:root="{collection: 'creatureProperties', id: model._id}"
@length="childrenLength = $event"
@selected="selectSubProperty"
/>
<v-btn
text
data-id="insert-creature-property-btn"
@click="addProperty"
>
<v-icon>mdi-plus</v-icon>
Property
</v-btn>
</template>
</property-field>
</template>
<div
v-if="!embedded"
slot="actions"
class="layout justify-end"
class="layout"
>
<v-btn
v-if="!editing && !embedded"
text
data-id="insert-creature-property-btn"
@click="addProperty"
>
<v-icon>mdi-plus</v-icon>
Property
</v-btn>
<v-spacer />
<v-btn
text
@click="$store.dispatch('popDialogStack')"
@@ -99,6 +105,7 @@ import { getHighestOrder } from '/imports/api/parenting/order.js';
import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
import Breadcrumbs from '/imports/ui/creature/creatureProperties/Breadcrumbs.vue';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import PropertyField from '/imports/ui/properties/viewers/shared/PropertyField.vue';
let formIndex = {};
for (let key in propertyFormIndex){
@@ -119,6 +126,7 @@ export default {
PropertyToolbar,
CreaturePropertiesTree,
Breadcrumbs,
PropertyField,
},
props: {
_id: String,
@@ -130,6 +138,7 @@ export default {
// CurrentId lags behind Id by one tick so that events fired by destroying
// forms keyed to the old ID are applied before the new ID overwrites it
currentId: undefined,
childrenLength: 0,
}},
meteor: {
model(){

View File

@@ -70,8 +70,8 @@
/>
</template>
<template v-if="model.summary">
<property-description
:model="model.summary"
<markdown-text
:markdown="model.summary.value || model.summary.text"
/>
</template>
</div>
@@ -84,14 +84,14 @@ import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import doAction from '/imports/api/engine/actions/doAction.js';
import AttributeConsumedView from '/imports/ui/properties/components/actions/AttributeConsumedView.vue';
import ItemConsumedView from '/imports/ui/properties/components/actions/ItemConsumedView.vue';
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
export default {
components: {
AttributeConsumedView,
ItemConsumedView,
PropertyDescription,
MarkdownText,
PropertyIcon,
},
inject: {

View File

@@ -4,10 +4,10 @@
@click="click"
>
<div class="layout align-center">
<v-card-title class="value text-h4">
<v-card-title class="value text-h4 flex-shrink-0">
{{ computedValue }}
</v-card-title>
<v-card-title class="name text-subtitle-1 text-truncate pl-0">
<v-card-title class="name text-subtitle-1 text-truncate d-block pl-0">
{{ model.name }}
</v-card-title>
</div>
@@ -28,14 +28,10 @@
return this.$listeners && !!this.$listeners.click
},
computedValue(){
if (this.model.type === 'attribute'){
if (this.model.attributeType === 'modifier'){
return numberToSignedString(this.model.value);
} else {
return this.model.value
}
if (this.model.attributeType === 'modifier' || this.model.type === 'skill'){
return numberToSignedString(this.model.value);
} else {
return this.model.value;
return this.model.value
}
}
},

View File

@@ -1,10 +1,16 @@
<template lang="html">
<div class="attribute-viewer">
<v-layout
column
align-center
<v-row
dense
align="stretch"
justify="center"
justify-sm="start"
>
<v-layout v-if="model.value !== undefined">
<property-field
:name="model.damage !== undefined ? 'Value / Total': 'Value'"
center
>
<v-spacer />
<div
class="text-h4 mr-3"
>
@@ -17,93 +23,171 @@
{{ model.value }}
</div>
</div>
<v-spacer />
<increment-button
v-if="context.creatureId"
icon
large
outlined
icon
tile
color="primary"
:value="model.value"
@change="damageProperty"
>
<v-icon>$vuetify.icons.abacus</v-icon>
<v-icon>
$vuetify.icons.abacus
</v-icon>
</increment-button>
</v-layout>
<div
</property-field>
<property-field
v-if="model.modifier !== undefined"
class="text-h6"
name="Modifier"
center
:value="isFinite(model.modifier) ?
numberToSignedString(model.modifier) :
model.modifier"
>
{{ numberToSignedString(model.modifier) }}
</div>
</v-layout>
<div>
<property-name :value="model.name" />
<property-variable-name :value="model.variableName" />
</div>
<property-field
v-if="model.attributeType === 'hitDice' && model.hitDiceSize"
name="Hit dice size"
:value="model.hitDiceSize"
/>
<property-field
v-if="reset && model.attributeType !== 'hitDice'"
name="Reset"
:value="reset"
/>
<property-description
:model="model.description"
/>
<v-list>
<attribute-effect
v-for="effect in baseEffects"
:key="effect._id"
:model="effect"
:hide-breadcrumbs="effect._id === model._id"
:data-id="effect._id"
@click="effect._id !== model._id && clickEffect(effect._id)"
<div class="text-h6">
{{ numberToSignedString(model.modifier) }}
</div>
</property-field>
<property-field
name="Variable Name"
:value="model.variableName"
/>
<attribute-effect
v-for="effect in effects"
:key="effect._id"
:model="effect"
:data-id="effect._id"
@click="clickEffect(effect._id)"
<property-field
name="Type"
:value="attributeTypes[model.attributeType]"
/>
</v-list>
<property-field
v-if="model.attributeType === 'hitDice' && model.hitDiceSize"
name="Hit dice size"
:value="model.hitDiceSize"
/>
<property-field
v-if="model.attributeType === 'hitDice'"
name="Constitution modifier"
:value="isFinite(model.constitutionMod) ?
numberToSignedString(model.constitutionMod) :
model.constitutionMod"
/>
<property-field
v-if="model.attributeType === 'spellSlot' && model.spellSlotLevel"
name="Spell slot level"
:value="model.spellSlotLevel.value !== undefined ? model.spellSlotLevel.value : model.spellSlotLevel.calculation"
/>
<property-field
v-if="model.attributeType === 'ability' && model.proficiency !== undefined"
name="Proficiency"
>
<v-icon
style="height: 12px"
class="ml-1 mr-2"
>
{{ proficiencyIcon }}
</v-icon>
<div>
{{ proficiencyText[model.proficiency] }}
</div>
</property-field>
<property-field
v-if="reset && model.attributeType !== 'hitDice'"
name="Reset"
:value="reset"
/>
<property-field
v-if="model.overridden"
:cols="{cols: 6, md: 12}"
name="Overridden"
value="Overriden by another property with the same variable name"
/>
</v-row>
<v-row dense>
<property-description
label="Description"
:model="model.description"
/>
</v-row>
<v-row dense>
<property-field
v-if="baseEffects.length || effects.length"
:cols="{col: 12}"
name="Effects"
>
<v-list>
<attribute-effect
v-for="effect in baseEffects"
:key="effect._id"
:model="effect"
:hide-breadcrumbs="effect._id === model._id"
:data-id="effect._id"
@click="effect._id !== model._id && clickEffect(effect._id)"
/>
<attribute-effect
v-for="effect in effects"
:key="effect._id"
:model="effect"
:data-id="effect._id"
@click="clickEffect(effect._id)"
/>
</v-list>
</property-field>
</v-row>
</div>
</template>
<script lang="js">
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import AttributeEffect from '/imports/ui/properties/components/attributes/AttributeEffect.vue';
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import AttributeEffect from '/imports/ui/properties/components/attributes/AttributeEffect.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import IncrementButton from '/imports/ui/components/IncrementButton.vue';
import getProficiencyIcon from '/imports/ui/utility/getProficiencyIcon.js';
export default {
components: {
AttributeEffect,
export default {
components: {
AttributeEffect,
IncrementButton,
},
mixins: [propertyViewerMixin],
},
mixins: [propertyViewerMixin],
inject: {
context: { default: {} }
},
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';
}
data(){return {
attributeTypes: {
ability: 'Ability score',
stat: 'Stat',
modifier: 'Modifier',
hitDice: 'Hit dice',
healthBar: 'Health bar',
resource: 'Resource',
spellSlot: 'Spell slot',
utility: 'Utility',
},
proficiencyText: {
0: 'Not proficient',
1: 'Proficient',
0.49: 'Half proficiency bonus rounded down',
0.5: 'Half proficiency bonus rounded up',
2: 'Double proficiency bonus',
},
}},
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;
}
},
methods: {
numberToSignedString,
},
proficiencyIcon(){
return getProficiencyIcon(this.model.proficiency);
},
},
methods: {
numberToSignedString,
clickEffect(id){
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',
@@ -118,7 +202,7 @@
value: value
});
},
},
},
meteor: {
baseEffects(){
if (this.context.creatureId && this.model.variableName){
@@ -155,20 +239,20 @@
}
},
},
}
}
</script>
<style lang="css" scoped>
.ability-value {
font-weight: 600;
font-size: 24px !important;
color: rgba(0, 0, 0, 0.54);
}
.mod, .ability-value {
text-align: center;
width: 100%;
}
.attribute-value {
text-align: center;
}
.ability-value {
font-weight: 600;
font-size: 24px !important;
color: rgba(0, 0, 0, 0.54);
}
.mod, .ability-value {
text-align: center;
width: 100%;
}
.attribute-value {
text-align: center;
}
</style>

View File

@@ -1,84 +1,114 @@
<template lang="html">
<div class="skill-viewer">
<v-layout
column
align-center
<v-row
dense
justify="center"
justify-sm="start"
>
<div
<property-field
v-if="model.value !== undefined"
class="text-h4 layout align-center"
center
large
name="Roll bonus"
:value="isFinite(model.value) ?
numberToSignedString(model.value) :
model.value"
/>
<property-field
v-if="model.proficiency !== undefined"
name="Proficiency"
>
<v-icon class="mr-4">
<v-icon
style="height: 12px"
class="ml-1 mr-2"
>
{{ icon }}
</v-icon>
<div v-if="isFinite(model.value)">
{{ numberToSignedString(model.value) }}
<div>
{{ proficiencyText[model.proficiency] }}
</div>
</div>
</v-layout>
<property-name :value="model.name" />
<property-variable-name :value="model.variableName" />
<property-field
name="Ability"
:value="model.ability"
/>
<property-field
name="Type"
:value="model.skillType"
/>
<property-field
name="Base value"
:value="model.baseValue"
/>
<property-field
name="Base proficiency"
:value="model.baseProficiency"
/>
</property-field>
<property-field
name="Variable Name"
:value="model.variableName"
/>
<property-field
name="Ability"
:value="model.ability"
/>
<property-field
name="Skill type"
:value="model.skillType"
/>
</v-row>
<property-description
:string="model.description"
:calculations="model.descriptionCalculations"
:inactive="model.inactive"
/>
<attribute-effect
v-for="effect in baseEffects"
:key="effect._id"
:model="effect"
:hide-breadcrumbs="effect._id === model._id"
:data-id="effect._id"
@click="effect._id !== model._id && clickEffect(effect._id)"
/>
<attribute-effect
v-if="ability"
:key="ability._id"
:model="ability"
:data-id="ability._id"
@click="clickEffect(ability._id)"
/>
<attribute-effect
v-for="effect in effects"
:key="effect._id"
:model="effect"
:data-id="effect._id"
@click="clickEffect(effect._id)"
/>
<skill-proficiency
v-for="proficiency in baseProficiencies"
:key="proficiency._id"
:model="proficiency"
:proficiency-bonus="proficiencyBonus"
:hide-breadcrumbs="proficiency._id === model._id"
:data-id="proficiency._id"
@click="clickEffect(proficiency._id)"
/>
<skill-proficiency
v-for="proficiency in proficiencies"
:key="proficiency._id"
:model="proficiency"
:proficiency-bonus="proficiencyBonus"
:data-id="proficiency._id"
@click="clickEffect(proficiency._id)"
/>
<v-row
v-if="baseEffects.length || ability || effects.length"
dense
>
<property-field
:cols="{col: 12}"
name="Effects"
>
<v-list style="width: 100%">
<attribute-effect
v-for="effect in baseEffects"
:key="effect._id === model._id ? 'this_base' : effect._id"
:model="effect"
:hide-breadcrumbs="effect._id === model._id"
:data-id="effect._id"
@click="effect._id !== model._id && clickEffect(effect._id)"
/>
<attribute-effect
v-if="ability"
:key="ability._id"
:model="ability"
:data-id="ability._id"
@click="clickEffect(ability._id)"
/>
<attribute-effect
v-for="effect in effects"
:key="effect._id"
:model="effect"
:data-id="effect._id"
@click="clickEffect(effect._id)"
/>
</v-list>
</property-field>
</v-row>
<v-row
v-if="baseProficiencies.length || proficiencies.length"
dense
>
<property-field
:cols="{col: 12}"
name="Proficiencies"
>
<v-list style="width: 100%">
<skill-proficiency
v-for="proficiency in baseProficiencies"
:key="proficiency._id"
:model="proficiency"
:proficiency-bonus="proficiencyBonus"
:hide-breadcrumbs="proficiency._id === model._id"
:data-id="proficiency._id"
@click="clickEffect(proficiency._id)"
/>
<skill-proficiency
v-for="proficiency in proficiencies"
:key="proficiency._id"
:model="proficiency"
:proficiency-bonus="proficiencyBonus"
:data-id="proficiency._id"
@click="clickEffect(proficiency._id)"
/>
</v-list>
</property-field>
</v-row>
</div>
</template>
@@ -100,6 +130,15 @@ export default {
inject: {
context: { default: {} }
},
data(){return {
proficiencyText: {
0: 'Not proficient',
1: 'Proficient',
0.49: 'Half proficiency bonus rounded down',
0.5: 'Half proficiency bonus rounded up',
2: 'Double proficiency bonus',
},
}},
computed: {
displayedModifier(){
let mod = this.model.value;
@@ -139,23 +178,23 @@ export default {
name: 'Skill base value',
operation: 'base',
calculation: prop.baseValueCalculation,
result: prop.baseValue,
amount: {value: prop.baseValue?.value},
stats: [prop.variableName],
ancestors: prop.ancestors,
}) ).filter(effect => effect.result);
}) ).filter(effect => effect.amount?.value);
} else {
return [];
}
},
effects(){
if (this.context.creatureId){
if (this.context.creatureId && this.model.variableName){
let creatureId = this.context.creatureId;
return CreatureProperties.find({
'ancestors.id': creatureId,
stats: this.model.variableName,
type: 'effect',
removed: {$ne: true},
});
}).fetch();
} else {
return [];
}
@@ -189,7 +228,7 @@ export default {
type: 'proficiency',
removed: {$ne: true},
inactive: {$ne: true},
});
}).fetch();
} else {
return [];
}
@@ -211,7 +250,7 @@ export default {
_id: abilityProp._id,
name: abilityProp.name,
operation: 'base',
result: abilityProp.modifier,
amount: {value: abilityProp.modifier},
stats: [this.model.variableName],
ancestors: abilityProp.ancestors,
}

View File

@@ -1,21 +1,32 @@
<template lang="html">
<markdown-text
<property-field
v-if="model"
:markdown="model.value"
/>
:name="label"
:cols="{col: 12}"
>
<markdown-text
:markdown="model.value || model.text"
/>
</property-field>
</template>
<script lang="js">
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
import PropertyField from '/imports/ui/properties/viewers/shared/PropertyField.vue';
export default {
components: {
MarkdownText,
PropertyField,
},
props: {
model: {
type: Object,
default: undefined,
},
label: {
type: String,
default: undefined,
},
},
}

View File

@@ -1,14 +1,42 @@
<template lang="html">
<div v-if="value !== undefined || $slots.default">
<div class="text-caption">
{{ name }}
</div>
<p class="ml-2 subheading">
<slot>
{{ value }}
</slot>
</p>
</div>
<v-col
v-if="value !== undefined || ($slots.default && $slots.default.length)"
v-bind="cols"
class="mb-2"
>
<v-sheet
outlined
rounded
class="pa-2 layout column align-start fill-height"
>
<v-sheet
v-if="name"
class="text-caption px-1 name"
style="margin-top: -18px;"
>
{{ name }}
</v-sheet>
<div
class="flex-grow-1 layout align-center"
style="width: 100%;"
>
<div
class="layout align-center"
:class="{
'text-body-1': !large,
'text-h4': large,
'justify-center': center,
}"
style="overflow-x: auto;"
v-bind="$attrs"
>
<slot>
{{ value }}
</slot>
</div>
</div>
</v-sheet>
</v-col>
</template>
<script lang="js">
@@ -16,9 +44,21 @@ export default {
props: {
name: String,
value: [String, Number, Boolean],
}
center: Boolean,
large: Boolean,
cols: {
type: Object,
default: () => ({cols: 12, sm: 6, md: 4}),
},
},
}
</script>
<style lang="css" scoped>
.name {
color: rgba(0,0,0,.6);
}
.theme--dark .name {
color: rgba(255,255,255,.7);
}
</style>