642 lines
18 KiB
Vue
642 lines
18 KiB
Vue
<template lang="html">
|
|
<div
|
|
v-if="properties"
|
|
class="stats-tab ma-2"
|
|
>
|
|
<div
|
|
v-if="properties.attribute.healthBar && properties.attribute.healthBar.length"
|
|
class="px-2 pt-2"
|
|
>
|
|
<v-card class="pa-2">
|
|
<health-bar
|
|
v-for="healthBar in properties.attribute.healthBar"
|
|
:key="healthBar._id"
|
|
:model="healthBar"
|
|
@change="({ type, value }) => incrementChange(healthBar._id, { type, value: -value })"
|
|
@click="clickProperty({_id: healthBar._id})"
|
|
/>
|
|
</v-card>
|
|
</div>
|
|
|
|
<column-layout>
|
|
<folder-group-card
|
|
v-for="folder in properties.folder.start"
|
|
:key="folder._id"
|
|
:model="folder"
|
|
@click-property="clickProperty"
|
|
@sub-click="_id => clickTreeProperty({_id})"
|
|
@remove="softRemove"
|
|
/>
|
|
<div
|
|
v-if="!creature.settings.hideRestButtons || (properties.action && properties.action.event && properties.action.event.length)"
|
|
class="character-buttons"
|
|
>
|
|
<v-card>
|
|
<v-card-text class="layout column align-center">
|
|
<rest-button
|
|
v-if="!creature.settings.hideRestButtons"
|
|
:creature-id="creatureId"
|
|
type="shortRest"
|
|
class="ma-1"
|
|
/>
|
|
<rest-button
|
|
v-if="!creature.settings.hideRestButtons"
|
|
:creature-id="creatureId"
|
|
type="longRest"
|
|
class="ma-1"
|
|
/>
|
|
<event-button
|
|
v-for="event in properties.event"
|
|
:key="event._id"
|
|
:model="event"
|
|
class="ma-1"
|
|
/>
|
|
</v-card-text>
|
|
</v-card>
|
|
</div>
|
|
|
|
<folder-group-card
|
|
v-for="folder in properties.folder.events"
|
|
:key="folder._id"
|
|
:model="folder"
|
|
@click-property="clickProperty"
|
|
@sub-click="_id => clickTreeProperty({_id})"
|
|
@remove="softRemove"
|
|
/>
|
|
|
|
<damage-multiplier-card
|
|
v-if="properties.damageMultiplier && properties.damageMultiplier.length"
|
|
:multipliers="properties.damageMultiplier"
|
|
@click-multiplier="clickProperty"
|
|
/>
|
|
|
|
<div
|
|
v-if="properties.buff && properties.buff.length"
|
|
class="buffs"
|
|
>
|
|
<v-card>
|
|
<v-list>
|
|
<v-subheader>Buffs and conditions</v-subheader>
|
|
<buff-list-item
|
|
v-for="buff in properties.buff"
|
|
:key="buff._id"
|
|
:data-id="buff._id"
|
|
:model="buff"
|
|
@click="clickProperty({_id: buff._id})"
|
|
@remove="softRemove(buff._id)"
|
|
/>
|
|
</v-list>
|
|
</v-card>
|
|
</div>
|
|
|
|
<div
|
|
v-if="properties.attribute.ability && properties.attribute.ability.length"
|
|
class="ability-scores"
|
|
>
|
|
<v-card>
|
|
<v-list>
|
|
<template v-for="(ability, index) in properties.attribute.ability">
|
|
<v-divider
|
|
v-if="index !== 0"
|
|
:key="index"
|
|
/>
|
|
<ability-list-tile
|
|
:key="ability._id"
|
|
:model="ability"
|
|
:data-id="ability._id"
|
|
@click="clickProperty({_id: ability._id})"
|
|
/>
|
|
</template>
|
|
</v-list>
|
|
</v-card>
|
|
</div>
|
|
|
|
<div
|
|
v-for="toggle in properties.toggle"
|
|
:key="toggle._id"
|
|
class="toggle"
|
|
>
|
|
<toggle-card
|
|
:model="toggle"
|
|
:data-id="toggle._id"
|
|
@click="clickProperty({_id: toggle._id})"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-for="stat in properties.attribute.stat"
|
|
:key="stat._id"
|
|
class="stat"
|
|
>
|
|
<attribute-card
|
|
:model="stat"
|
|
:data-id="stat._id"
|
|
@click="clickProperty({_id: stat._id})"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-for="modifier in properties.attribute.modifier"
|
|
:key="modifier._id"
|
|
class="modifier"
|
|
>
|
|
<attribute-card
|
|
:model="modifier"
|
|
:data-id="modifier._id"
|
|
@click="clickProperty({_id: modifier._id})"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-for="check in properties.skill.check"
|
|
:key="check._id"
|
|
class="check"
|
|
>
|
|
<attribute-card
|
|
modifier
|
|
:model="check"
|
|
:data-id="check._id"
|
|
@click="clickProperty({_id: check._id})"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="properties.attribute.hitDice && properties.attribute.hitDice.length"
|
|
class="hit-dice"
|
|
>
|
|
<v-card>
|
|
<v-list>
|
|
<v-subheader>Hit Dice</v-subheader>
|
|
<template v-for="(hitDie, index) in properties.attribute.hitDice">
|
|
<v-divider
|
|
v-if="index !== 0"
|
|
:key="hitDie._id + 'divider'"
|
|
/>
|
|
<hit-dice-list-tile
|
|
:key="hitDie._id"
|
|
:model="hitDie"
|
|
:data-id="hitDie._id"
|
|
@click="clickProperty({_id: hitDie._id})"
|
|
@change="e => incrementChange(hitDie._id, e)"
|
|
/>
|
|
</template>
|
|
</v-list>
|
|
</v-card>
|
|
</div>
|
|
|
|
<div
|
|
v-for="resource in properties.attribute.resource"
|
|
:key="resource._id"
|
|
class="resource"
|
|
>
|
|
<resource-card
|
|
:model="resource"
|
|
:data-id="resource._id"
|
|
@click="clickProperty({_id: resource._id})"
|
|
@change="e => incrementChange(resource._id, e)"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="properties.attribute.spellSlot && properties.attribute.spellSlot.length"
|
|
class="spell-slots"
|
|
>
|
|
<spell-slot-card
|
|
:creature-id="creatureId"
|
|
:spell-slots="properties.attribute.spellSlot"
|
|
/>
|
|
</div>
|
|
|
|
<folder-group-card
|
|
v-for="folder in properties.folder.stats"
|
|
:key="folder._id"
|
|
:model="folder"
|
|
@click-property="clickProperty"
|
|
@sub-click="_id => clickTreeProperty({_id})"
|
|
@remove="softRemove"
|
|
/>
|
|
|
|
<div
|
|
v-if="properties.skill.save && properties.skill.save.length"
|
|
class="saving-throws"
|
|
>
|
|
<v-card>
|
|
<v-list>
|
|
<v-subheader>Saving Throws</v-subheader>
|
|
<skill-list-tile
|
|
v-for="save in properties.skill.save"
|
|
:key="save._id"
|
|
:model="save"
|
|
:data-id="save._id"
|
|
@click="clickProperty({_id: save._id})"
|
|
/>
|
|
<v-list-item
|
|
v-for="(effect, index) in saveConditionals"
|
|
:key="effect._id"
|
|
:data-id="effect._id"
|
|
:class="{'mt-2': !index}"
|
|
@click="clickProperty({_id: effect._id})"
|
|
>
|
|
<v-list-item-content>
|
|
<v-list-item-subtitle style="white-space: unset;">
|
|
{{ effect.text }}
|
|
</v-list-item-subtitle>
|
|
</v-list-item-content>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-card>
|
|
</div>
|
|
|
|
<div
|
|
v-if="properties.skill.skill && properties.skill.skill.length"
|
|
class="skills"
|
|
>
|
|
<v-card>
|
|
<v-list>
|
|
<v-subheader>Skills</v-subheader>
|
|
<skill-list-tile
|
|
v-for="skill in properties.skill.skill"
|
|
:key="skill._id"
|
|
:model="skill"
|
|
:data-id="skill._id"
|
|
@click="clickProperty({_id: skill._id})"
|
|
/>
|
|
<v-list-item
|
|
v-for="(effect, index) in skillConditionals"
|
|
:key="effect._id"
|
|
:data-id="effect._id"
|
|
:class="{'mt-2': !index}"
|
|
@click="clickProperty({_id: effect._id})"
|
|
>
|
|
<v-list-item-content>
|
|
<v-list-item-subtitle style="white-space: unset;">
|
|
{{ effect.text }}
|
|
</v-list-item-subtitle>
|
|
</v-list-item-content>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-card>
|
|
</div>
|
|
|
|
<folder-group-card
|
|
v-for="folder in properties.folder.skills"
|
|
:key="folder._id"
|
|
:model="folder"
|
|
@click-property="clickProperty"
|
|
@sub-click="_id => clickTreeProperty({_id})"
|
|
@remove="softRemove"
|
|
/>
|
|
|
|
<div
|
|
v-if="properties.skill.weapon && properties.skill.weapon.length"
|
|
class="weapon-proficiencies"
|
|
>
|
|
<v-card>
|
|
<v-list>
|
|
<v-subheader>
|
|
Weapons
|
|
</v-subheader>
|
|
<skill-list-tile
|
|
v-for="weapon in properties.skill.weapon"
|
|
:key="weapon._id"
|
|
hide-modifier
|
|
:model="weapon"
|
|
:data-id="weapon._id"
|
|
@click="clickProperty({_id: weapon._id})"
|
|
/>
|
|
</v-list>
|
|
</v-card>
|
|
</div>
|
|
<div
|
|
v-if="properties.skill.armor && properties.skill.armor.length"
|
|
class="armor-proficiencies"
|
|
>
|
|
<v-card>
|
|
<v-list>
|
|
<v-subheader>
|
|
Armor
|
|
</v-subheader>
|
|
<skill-list-tile
|
|
v-for="armor in properties.skill.armor"
|
|
:key="armor._id"
|
|
hide-modifier
|
|
:model="armor"
|
|
:data-id="armor._id"
|
|
@click="clickProperty({_id: armor._id})"
|
|
/>
|
|
</v-list>
|
|
</v-card>
|
|
</div>
|
|
<div
|
|
v-if="properties.skill.tool && properties.skill.tool.length"
|
|
class="tool-proficiencies"
|
|
>
|
|
<v-card>
|
|
<v-list>
|
|
<v-subheader>
|
|
Tools
|
|
</v-subheader>
|
|
<skill-list-tile
|
|
v-for="tool in properties.skill.tool"
|
|
:key="tool._id"
|
|
hide-modifier
|
|
:model="tool"
|
|
:data-id="tool._id"
|
|
@click="clickProperty({_id: tool._id})"
|
|
/>
|
|
</v-list>
|
|
</v-card>
|
|
</div>
|
|
<div
|
|
v-if="properties.skill.language && properties.skill.language.length"
|
|
class="language-proficiencies"
|
|
>
|
|
<v-card>
|
|
<v-list>
|
|
<v-subheader>
|
|
Languages
|
|
</v-subheader>
|
|
<skill-list-tile
|
|
v-for="language in properties.skill.language"
|
|
:key="language._id"
|
|
hide-modifier
|
|
:model="language"
|
|
:data-id="language._id"
|
|
@click="clickProperty({_id: language._id})"
|
|
/>
|
|
</v-list>
|
|
</v-card>
|
|
</div>
|
|
|
|
<folder-group-card
|
|
v-for="folder in properties.folder.proficiencies"
|
|
:key="folder._id"
|
|
:model="folder"
|
|
@click-property="clickProperty"
|
|
@sub-click="_id => clickTreeProperty({_id})"
|
|
@remove="softRemove"
|
|
/>
|
|
|
|
<folder-group-card
|
|
v-for="folder in properties.folder.end"
|
|
:key="folder._id"
|
|
:model="folder"
|
|
@click-property="clickProperty"
|
|
@sub-click="_id => clickTreeProperty({_id})"
|
|
@remove="softRemove"
|
|
/>
|
|
</column-layout>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="js">
|
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
|
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
|
|
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
|
import HealthBar from '/imports/client/ui/properties/components/attributes/HealthBar.vue';
|
|
import AttributeCard from '/imports/client/ui/properties/components/attributes/AttributeCard.vue';
|
|
import AbilityListTile from '/imports/client/ui/properties/components/attributes/AbilityListTile.vue';
|
|
import ColumnLayout from '/imports/client/ui/components/ColumnLayout.vue';
|
|
import DamageMultiplierCard from '/imports/client/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue';
|
|
import HitDiceListTile from '/imports/client/ui/properties/components/attributes/HitDiceListTile.vue';
|
|
import SkillListTile from '/imports/client/ui/properties/components/skills/SkillListTile.vue';
|
|
import ResourceCard from '/imports/client/ui/properties/components/attributes/ResourceCard.vue';
|
|
import RestButton from '/imports/client/ui/creature/RestButton.vue';
|
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
|
import ToggleCard from '/imports/client/ui/properties/components/toggles/ToggleCard.vue';
|
|
import BuffListItem from '/imports/client/ui/properties/components/buffs/BuffListItem.vue';
|
|
import SpellSlotCard from '/imports/client/ui/properties/components/attributes/SpellSlotCard.vue';
|
|
import EventButton from '/imports/client/ui/properties/components/actions/EventButton.vue';
|
|
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
|
|
import FolderGroupCard from '/imports/client/ui/properties/components/folders/FolderGroupCard.vue';
|
|
import { get, set, uniqBy } from 'lodash';
|
|
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'
|
|
|
|
function walkDown(forest, callback){
|
|
let stack = [...forest];
|
|
while(stack.length){
|
|
let node = stack.pop();
|
|
const { skipChildren } = callback(node) ?? { skipChildren: false };
|
|
if (!skipChildren) {
|
|
stack.push(...node.children);
|
|
}
|
|
}
|
|
}
|
|
|
|
const propertyHandlers = {
|
|
folder(prop) {
|
|
let propPath = null;
|
|
if (prop.groupStats && prop.tab === 'stats') {
|
|
propPath = ['folder', prop.location]
|
|
}
|
|
return { propPath }
|
|
},
|
|
attribute(prop) {
|
|
if (
|
|
prop.attributeType === 'utility' ||
|
|
prop.overridden ||
|
|
(prop.hideWhenTotalZero && prop.total === 0) ||
|
|
(prop.hideWhenValueZero && prop.value === 0)
|
|
) return { propPath: null };
|
|
return {
|
|
propPath: ['attribute', prop.attributeType],
|
|
}
|
|
},
|
|
skill(prop) {
|
|
if (
|
|
prop.skillType === 'utility'
|
|
) return { propPath: null };
|
|
return {
|
|
propPath: ['skill', prop.skillType],
|
|
}
|
|
},
|
|
toggle(prop) {
|
|
if (
|
|
prop.deactivatedByToggle || prop.deactivatedByAncestor || !prop.showUI
|
|
) return { propPath: null };
|
|
return { propPath: 'toggle' };
|
|
},
|
|
action(prop) {
|
|
if (prop.actionType === 'event' && !prop.overridden) {
|
|
return { propPath: 'event' };
|
|
}
|
|
return { propPath: null };
|
|
},
|
|
}
|
|
|
|
export default {
|
|
components: {
|
|
HealthBar,
|
|
RestButton,
|
|
BuffListItem,
|
|
AbilityListTile,
|
|
AttributeCard,
|
|
ColumnLayout,
|
|
DamageMultiplierCard,
|
|
HitDiceListTile,
|
|
SkillListTile,
|
|
ResourceCard,
|
|
SpellSlotCard,
|
|
ToggleCard,
|
|
EventButton,
|
|
FolderGroupCard,
|
|
},
|
|
props: {
|
|
creatureId: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
doCheckLoading: false,
|
|
}
|
|
},
|
|
computed: {
|
|
saveConditionals(){
|
|
const conditionals = [];
|
|
this.properties.skill?.save?.forEach(prop => {
|
|
prop?.effects?.forEach(effect => {
|
|
if (effect.operation === 'conditional') {
|
|
conditionals.push(effect);
|
|
}
|
|
});
|
|
});
|
|
return uniqBy(conditionals, '_id');
|
|
},
|
|
skillConditionals(){
|
|
const conditionals = [];
|
|
this.properties.skill?.skill?.forEach(prop => {
|
|
prop?.effects?.forEach(effect => {
|
|
if (effect.operation === 'conditional') {
|
|
conditionals.push(effect);
|
|
}
|
|
});
|
|
});
|
|
return uniqBy(conditionals, '_id');
|
|
},
|
|
},
|
|
meteor: {
|
|
properties() {
|
|
const creature = this.creature;
|
|
if (!creature) return;
|
|
const folderIds = CreatureProperties.find({
|
|
'ancestors.id': this.creatureId,
|
|
type: 'folder',
|
|
groupStats: true,
|
|
hideStatsGroup: true,
|
|
removed: { $ne: true },
|
|
inactive: { $ne: true },
|
|
}, { fields: { _id: 1 } }).map(folder => folder._id);
|
|
|
|
const filter = {
|
|
'ancestors.id': this.creatureId,
|
|
'parent.id': {
|
|
$nin: folderIds,
|
|
},
|
|
$or: [
|
|
{ inactive: { $ne: true } },
|
|
{ type: 'toggle' },
|
|
],
|
|
overridden: {$ne: true},
|
|
removed: { $ne: true },
|
|
type: {
|
|
$in: [
|
|
'action',
|
|
'attribute',
|
|
'buff',
|
|
'damageMultiplier',
|
|
'folder',
|
|
'skill',
|
|
'toggle',
|
|
]
|
|
}
|
|
};
|
|
if (creature.settings.hideUnusedStats) {
|
|
filter.hide = { $ne: true };
|
|
}
|
|
const allProps = CreatureProperties.find(filter, { sort: { order: -1 } });
|
|
const forest = nodeArrayToTree(allProps);
|
|
const properties = { folder: {}, attribute: {}, skill: {} };
|
|
walkDown(forest, node => {
|
|
const prop = node.node;
|
|
const { propPath, skipChildren } = propertyHandlers[prop.type]?.(prop) ||
|
|
{ propPath: prop.type };
|
|
if (propPath) {
|
|
let propArray = get(properties, propPath);
|
|
if (!propArray) {
|
|
propArray = [];
|
|
set(properties, propPath, propArray);
|
|
}
|
|
if (!propArray?.push) {
|
|
console.log({propArray});
|
|
}
|
|
propArray.push(prop);
|
|
}
|
|
return { skipChildren };
|
|
});
|
|
properties.damageMultiplier?.sort((a, b) => a.value - b.value);
|
|
return properties;
|
|
},
|
|
creature() {
|
|
return Creatures.findOne(this.creatureId, { fields: { settings: 1 } });
|
|
},
|
|
|
|
toggles() {
|
|
return CreatureProperties.find({
|
|
type: 'toggle',
|
|
'ancestors.id': this.creatureId,
|
|
removed: { $ne: true },
|
|
deactivatedByAncestor: { $ne: true },
|
|
deactivatedByToggle: { $ne: true },
|
|
showUI: true,
|
|
}, {
|
|
sort: { order: 1 }
|
|
});
|
|
},
|
|
},
|
|
methods: {
|
|
clickProperty({ _id }) {
|
|
this.$store.commit('pushDialogStack', {
|
|
component: 'creature-property-dialog',
|
|
elementId: `${_id}`,
|
|
data: { _id },
|
|
});
|
|
},
|
|
clickTreeProperty({ _id }) {
|
|
this.$store.commit('pushDialogStack', {
|
|
component: 'creature-property-dialog',
|
|
elementId: `tree-node-${_id}`,
|
|
data: { _id },
|
|
});
|
|
},
|
|
incrementChange(_id, { type, value, ack }) {
|
|
damageProperty.call({
|
|
_id,
|
|
operation: type,
|
|
value: -value
|
|
}, error => {
|
|
if (ack) {
|
|
ack(error);
|
|
} else if (error) {
|
|
snackbar({ text: error.reason || error.message || error.toString() });
|
|
console.error(error);
|
|
}
|
|
});
|
|
},
|
|
softRemove(_id) {
|
|
softRemoveProperty.call({ _id }, error => {
|
|
if (error) {
|
|
snackbar({ text: error.reason || error.message || error.toString() });
|
|
console.error(error);
|
|
}
|
|
});
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="css" scoped>
|
|
|
|
</style>
|