Merge branch 'feature-tabletop' into develop
This commit is contained in:
97
app/imports/client/ui/tabletop/CharacterSheetDialog.vue
Normal file
97
app/imports/client/ui/tabletop/CharacterSheetDialog.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<dialog-base>
|
||||
<template #toolbar>
|
||||
<v-toolbar-title>
|
||||
{{ creature && creature.name }}
|
||||
</v-toolbar-title>
|
||||
</template>
|
||||
<template #unwrapped-content>
|
||||
<character-sheet
|
||||
show-menu-button
|
||||
embedded
|
||||
:creature-id="creatureId"
|
||||
/>
|
||||
</template>
|
||||
<v-bottom-navigation
|
||||
slot="actions"
|
||||
shift
|
||||
mandatory
|
||||
class="bottom-nav-btns"
|
||||
style="position: relative;"
|
||||
:value="$store.getters.tabById(creatureId)"
|
||||
@change="e => $store.commit(
|
||||
'setTabForCharacterSheet',
|
||||
{id: creatureId, tab: e}
|
||||
)"
|
||||
>
|
||||
<v-btn>
|
||||
<span>Stats</span>
|
||||
<v-icon>mdi-chart-box</v-icon>
|
||||
</v-btn>
|
||||
<v-btn>
|
||||
<span>Actions</span>
|
||||
<v-icon>mdi-lightning-bolt</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="!creature.settings.hideSpellsTab">
|
||||
<span>Spells</span>
|
||||
<v-icon>mdi-fire</v-icon>
|
||||
</v-btn>
|
||||
<v-btn>
|
||||
<span>Inventory</span>
|
||||
<v-icon>mdi-cube</v-icon>
|
||||
</v-btn>
|
||||
<v-btn>
|
||||
<span>Features</span>
|
||||
<v-icon>mdi-text</v-icon>
|
||||
</v-btn>
|
||||
<v-btn>
|
||||
<span>Journal</span>
|
||||
<v-icon>mdi-book-open-variant</v-icon>
|
||||
</v-btn>
|
||||
<v-btn>
|
||||
<span>Build</span>
|
||||
<v-icon>mdi-wrench</v-icon>
|
||||
</v-btn>
|
||||
</v-bottom-navigation>
|
||||
</dialog-base>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
|
||||
import CharacterSheet from '/imports/client/ui/creature/character/CharacterSheet.vue';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
CharacterSheet,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
creature() {
|
||||
if (!this.creatureId) return;
|
||||
return Creatures.findOne(this.creatureId);
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.bottom-nav-btns {
|
||||
margin: -8px;
|
||||
box-shadow: none !important;
|
||||
flex-grow: 1;
|
||||
max-width: unset !important;
|
||||
}
|
||||
.bottom-nav-btns > .v-btn{
|
||||
min-width: 0 !important;
|
||||
padding: 0 !important;
|
||||
flex: 1 1 auto !important;
|
||||
font-size: 0.6rem !important;
|
||||
}
|
||||
</style>
|
||||
394
app/imports/client/ui/tabletop/TabletopActionCard.vue
Normal file
394
app/imports/client/ui/tabletop/TabletopActionCard.vue
Normal file
@@ -0,0 +1,394 @@
|
||||
<template lang="html">
|
||||
<v-sheet
|
||||
class="action-card overflow-y-auto"
|
||||
rounded
|
||||
:class="cardClasses"
|
||||
:data-id="model._id"
|
||||
>
|
||||
<div class="layout align-center px-3">
|
||||
<div
|
||||
class="avatar"
|
||||
:style="{ opacity: active ? '' : '0.5'}"
|
||||
>
|
||||
<roll-popup
|
||||
v-if="rollBonus"
|
||||
:icon="!active"
|
||||
:outlined="!active"
|
||||
:fab="active"
|
||||
style="letter-spacing: normal;"
|
||||
class="mr-2"
|
||||
:no-click="!active"
|
||||
:style="{
|
||||
fontSize: active ? '24px' : '16px'
|
||||
}"
|
||||
:large="active"
|
||||
:color="model.color || 'primary'"
|
||||
:loading="doActionLoading"
|
||||
:disabled="model.insufficientResources || !context.editPermission || !!targetingError"
|
||||
:roll-text="rollBonus"
|
||||
:name="model.name"
|
||||
:advantage="model.attackRoll && model.attackRoll.advantage"
|
||||
@roll="doAction"
|
||||
>
|
||||
<template v-if="rollBonus && !rollBonusTooLong">
|
||||
{{ rollBonus }}
|
||||
</template>
|
||||
<property-icon
|
||||
v-else
|
||||
:model="model"
|
||||
/>
|
||||
</roll-popup>
|
||||
<v-btn
|
||||
v-else
|
||||
:icon="!active"
|
||||
:outlined="!active"
|
||||
:fab="active"
|
||||
style="letter-spacing: normal;"
|
||||
class="mr-2"
|
||||
:style="{
|
||||
fontSize: active ? '24px' : '16px'
|
||||
}"
|
||||
:large="active"
|
||||
:color="model.color || 'primary'"
|
||||
:loading="doActionLoading"
|
||||
:disabled="model.insufficientResources || !context.editPermission || !!targetingError"
|
||||
v-on="active ? {
|
||||
click: e => {
|
||||
if (!active) return;
|
||||
e.stopPropagation();
|
||||
doAction({});
|
||||
}
|
||||
} : {}"
|
||||
>
|
||||
<template v-if="rollBonus && !rollBonusTooLong">
|
||||
{{ rollBonus }}
|
||||
</template>
|
||||
<property-icon
|
||||
v-else
|
||||
:model="model"
|
||||
/>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div
|
||||
class="action-header flex layout column justify-center pl-1"
|
||||
style="height: 72px; cursor: pointer;"
|
||||
@mouseover="() => { if (active) hovering = true }"
|
||||
@mouseleave="() => { if (active) hovering = false }"
|
||||
@click="(e) => { if (active) { $emit('deactivate'); e.stopPropagation() } }"
|
||||
>
|
||||
<div class="action-title my-1">
|
||||
{{ model.name || propertyName }}
|
||||
</div>
|
||||
<div class="action-sub-title layout align-center">
|
||||
<div
|
||||
v-if="targetingError"
|
||||
class="flex error--text"
|
||||
>
|
||||
{{ targetingError }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="flex">
|
||||
{{ model.actionType }}
|
||||
</div>
|
||||
<div v-if="Number.isFinite(model.usesLeft)">
|
||||
{{ model.usesLeft }} uses
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<v-btn
|
||||
v-if="active"
|
||||
icon
|
||||
class="flex-grow-0"
|
||||
@click.stop="openPropertyDetails(model._id)"
|
||||
>
|
||||
<v-icon>mdi-window-restore</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="px-3 pb-3">
|
||||
<template
|
||||
v-if="model.resources && model.resources.attributesConsumed.length ||
|
||||
model.resources.itemsConsumed.length"
|
||||
>
|
||||
<attribute-consumed-view
|
||||
v-for="attributeConsumed in model.resources.attributesConsumed"
|
||||
:key="attributeConsumed._id"
|
||||
class="action-child"
|
||||
:model="attributeConsumed"
|
||||
/>
|
||||
<item-consumed-view
|
||||
v-for="itemConsumed in model.resources.itemsConsumed"
|
||||
:key="itemConsumed._id"
|
||||
class="action-child"
|
||||
:model="itemConsumed"
|
||||
:action="model"
|
||||
/>
|
||||
<v-divider
|
||||
v-if="active && model.summary"
|
||||
class="my-2"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="active && model.summary">
|
||||
<markdown-text :markdown="model.summary.value || model.summary.text" />
|
||||
</template>
|
||||
<v-divider v-if="active && children && children.length" />
|
||||
<tree-node-list
|
||||
v-if="active && children && children.length"
|
||||
start-expanded
|
||||
:children="children"
|
||||
@selected="e => $emit('sub-click', e)"
|
||||
/>
|
||||
</div>
|
||||
<card-highlight :active="hovering" />
|
||||
<div
|
||||
style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; cursor: pointer"
|
||||
:style="{pointerEvents: active ? 'none' : ''}"
|
||||
@mouseover="() => { if (!active) hovering = true }"
|
||||
@mouseleave="hovering = false"
|
||||
@click="() => { if (!active) $emit('activate') }"
|
||||
/>
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
||||
import doAction from '/imports/api/engine/actions/doAction.js';
|
||||
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
||||
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||
import RollPopup from '/imports/client/ui/components/RollPopup.vue';
|
||||
import MarkdownText from '/imports/client/ui/components/MarkdownText.vue';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
|
||||
import CardHighlight from '/imports/client/ui/components/CardHighlight.vue';
|
||||
import TreeNodeList from '/imports/client/ui/components/tree/TreeNodeList.vue';
|
||||
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { some } from 'lodash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AttributeConsumedView,
|
||||
ItemConsumedView,
|
||||
MarkdownText,
|
||||
PropertyIcon,
|
||||
RollPopup,
|
||||
CardHighlight,
|
||||
TreeNodeList,
|
||||
},
|
||||
inject: {
|
||||
context: {
|
||||
default: {},
|
||||
},
|
||||
theme: {
|
||||
default: {
|
||||
isDark: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
targets: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
active: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activated: undefined,
|
||||
doActionLoading: false,
|
||||
hovering: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rollBonus() {
|
||||
if (!this.model.attackRoll) return;
|
||||
return numberToSignedString(this.model.attackRoll.value);
|
||||
},
|
||||
rollBonusTooLong() {
|
||||
return this.rollBonus && this.rollBonus.length > 3;
|
||||
},
|
||||
propertyName() {
|
||||
return getPropertyName(this.model.type);
|
||||
},
|
||||
cardClasses() {
|
||||
return {
|
||||
'theme--dark': this.theme.isDark,
|
||||
'theme--light': !this.theme.isDark,
|
||||
'muted-text': this.model.insufficientResources,
|
||||
'active': this.activated,
|
||||
'tabletop-active': this.active,
|
||||
'elevation-8': this.hovering,
|
||||
}
|
||||
},
|
||||
actionTypeIcon() {
|
||||
return `$vuetify.icons.${this.model.actionType}`;
|
||||
},
|
||||
targetingError() {
|
||||
if (!this.active) return;
|
||||
const targets = this.targets || [];
|
||||
if (this.model.target === 'singleTarget' && targets.length === 0) {
|
||||
return 'Select target';
|
||||
} else if (targets.length > 1 && this.model.target !== 'multipleTargets'){
|
||||
return 'Single target only';
|
||||
} else if (this.model.target === 'self' && targets.length > 0){
|
||||
return 'Can only target self';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
children() {
|
||||
const indicesOfTerminatingProps = [];
|
||||
const decendants = CreatureProperties.find({
|
||||
'ancestors.id': this.model._id,
|
||||
'removed': { $ne: true },
|
||||
}, {
|
||||
sort: {order: 1}
|
||||
}).map(prop => {
|
||||
// Get all the props we don't want to show the decendants of and
|
||||
// where they might appear in the ancestor list
|
||||
if (prop.type === 'buff' || prop.type === 'folder') {
|
||||
indicesOfTerminatingProps.push({
|
||||
id: prop._id,
|
||||
ancestorIndex: prop.ancestors.length,
|
||||
});
|
||||
}
|
||||
return prop;
|
||||
}).filter(prop => {
|
||||
// Filter out folders entirely
|
||||
if (prop.type === 'folder') return false;
|
||||
// Filter out decendants of terminating props
|
||||
return !some(indicesOfTerminatingProps, buffIndex => {
|
||||
return prop.ancestors[buffIndex.ancestorIndex]?.id === buffIndex.id;
|
||||
});
|
||||
});
|
||||
return nodeArrayToTree(decendants);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
click(e) {
|
||||
this.$emit('click', e);
|
||||
},
|
||||
doAction({ advantage }) {
|
||||
this.doActionLoading = true;
|
||||
this.shwing();
|
||||
doAction.call({
|
||||
actionId: this.model._id,
|
||||
targetIds: this.targets,
|
||||
scope: {
|
||||
$attackAdvantage: advantage,
|
||||
}
|
||||
}, error => {
|
||||
this.doActionLoading = false;
|
||||
this.$emit('deactivate');
|
||||
if (error) {
|
||||
console.error(error);
|
||||
snackbar({ text: error.reason });
|
||||
}
|
||||
});
|
||||
},
|
||||
shwing() {
|
||||
this.activated = true;
|
||||
setTimeout(() => {
|
||||
this.activated = undefined;
|
||||
}, 150);
|
||||
},
|
||||
openPropertyDetails() {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `${this.model._id}`,
|
||||
data: {_id: this.model._id},
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.action-card {
|
||||
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1),
|
||||
transform 0.075s ease,
|
||||
width .3s ease,
|
||||
margin-top .3s ease,
|
||||
height .3s ease;
|
||||
max-width: 100vw;
|
||||
position: relative;
|
||||
}
|
||||
.action-card.tabletop-active {
|
||||
margin-top: -100px;
|
||||
width: 320px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.action-card.active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
.action-card-container {
|
||||
transition: width .3s ease;
|
||||
}
|
||||
|
||||
.action-title {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: .3s cubic-bezier(.25, .8, .5, 1);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-sub-title {
|
||||
color: #9e9e9e;
|
||||
flex-grow: 0;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
height: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-child {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.theme--light.muted-text {
|
||||
color: rgba(0, 0, 0, .3) !important;
|
||||
}
|
||||
|
||||
.theme--dark.muted-text {
|
||||
color: hsla(0, 0%, 100%, .3) !important;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
transition: transform 0.15s cubic;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="css">
|
||||
.action-card.theme--light.muted-text .v-icon {
|
||||
color: rgba(0, 0, 0, .3) !important;
|
||||
}
|
||||
|
||||
.action-card.theme--dark.muted-text .v-icon {
|
||||
color: hsla(0, 0%, 100%, .3) !important;
|
||||
}
|
||||
|
||||
.action-card .property-description>p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.action-card .v-btn--icon {
|
||||
transition: all .3s ease, height .3s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -1,55 +0,0 @@
|
||||
<template lang="html">
|
||||
<div class="action-cards">
|
||||
<action-card
|
||||
v-for="action in actions"
|
||||
:key="action._id"
|
||||
:model="action"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import ActionCard from '/imports/client/ui/properties/components/actions/ActionCard.vue';
|
||||
|
||||
function getProperties(ancestorId, type){
|
||||
if (!ancestorId) return [];
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': ancestorId,
|
||||
type,
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
sort: {order: 1}
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ActionCard,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data(){ return {
|
||||
actionType: 'action',
|
||||
}},
|
||||
meteor: {
|
||||
actions(){
|
||||
return getProperties(this.creatureId, 'action');
|
||||
},
|
||||
attacks(){
|
||||
return getProperties(this.creatureId, 'attack');
|
||||
},
|
||||
spells(){
|
||||
return getProperties(this.creatureId, 'spell');
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -1,56 +1,134 @@
|
||||
<template lang="html">
|
||||
<v-container
|
||||
class="tabletop"
|
||||
fluid
|
||||
<div
|
||||
class="tabletop layout column"
|
||||
style="height: 100%;"
|
||||
>
|
||||
<v-row
|
||||
dense
|
||||
class="initiative-row"
|
||||
style="flex-wrap: nowrap; overflow-x: auto;"
|
||||
<tabletop-map
|
||||
class="play-area"
|
||||
style="
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
"
|
||||
/>
|
||||
<v-container
|
||||
fluid
|
||||
>
|
||||
<tabletop-creature-card
|
||||
v-for="creature in creatures"
|
||||
:key="creature._id"
|
||||
:model="creature"
|
||||
/>
|
||||
<v-card
|
||||
class="layout column justify-center align-center"
|
||||
style="height: 150px; min-width: 120px;"
|
||||
data-id="select-creatures"
|
||||
hover
|
||||
@click="addCreature"
|
||||
<v-row
|
||||
dense
|
||||
class="initiative-row flex-grow-0"
|
||||
style="flex-wrap: nowrap; overflow-x: auto; padding-bottom: 50px;"
|
||||
@wheel="transformScroll($event)"
|
||||
>
|
||||
<div class="flex layout justify-center align-center">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
<tabletop-creature-card
|
||||
v-for="creature in creatures"
|
||||
:key="creature._id"
|
||||
:model="creature"
|
||||
:active="activeCreatureId === creature._id"
|
||||
:targeted="targets.includes(creature._id)"
|
||||
:show-target-btn="targets.includes(creature._id) || moreTargets"
|
||||
v-on="(!activeActionId || (targets.includes(creature._id) || moreTargets)) ? {
|
||||
click: () => {
|
||||
if (activeActionId) {
|
||||
if (targets.includes(creature._id)) {
|
||||
untarget(creature._id)
|
||||
} else {
|
||||
if (moreTargets) targets.push(creature._id);
|
||||
}
|
||||
} else {
|
||||
activeCreatureId = creature._id;
|
||||
targets = [];
|
||||
activeActionId = undefined;
|
||||
}
|
||||
}
|
||||
} : {}"
|
||||
@target="targets.push(creature._id)"
|
||||
@untarget="untarget(creature._id)"
|
||||
/>
|
||||
<div
|
||||
class="layout column ma-1 flex-grow-0"
|
||||
>
|
||||
<v-btn
|
||||
data-id="select-creatures"
|
||||
class="mb-2"
|
||||
@click="addCreature"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
Add Character
|
||||
</v-btn>
|
||||
<v-btn disabled>
|
||||
<v-icon left>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
Add Creature
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-card-title>
|
||||
Add<br>creature
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-row>
|
||||
<tabletop-map class="play-area" />
|
||||
<section class="action-row">
|
||||
<mini-character-sheet />
|
||||
<tabletop-action-cards />
|
||||
</section>
|
||||
</v-container>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<v-footer
|
||||
inset
|
||||
class="pa-0"
|
||||
style="
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom:0;
|
||||
right: 0;
|
||||
overflow-x: auto;
|
||||
"
|
||||
>
|
||||
<v-slide-y-reverse-transition mode="out-in">
|
||||
<selected-creature-bar
|
||||
:key="activeCreatureId"
|
||||
:creature-id="activeCreatureId"
|
||||
/>
|
||||
</v-slide-y-reverse-transition>
|
||||
</v-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import addCreaturesToTabletop from '/imports/api/tabletop/methods/addCreaturesToTabletop';
|
||||
import TabletopCreatureCard from '/imports/client/ui/tabletop/TabletopCreatureCard.vue';
|
||||
import TabletopMap from '/imports/client/ui/tabletop/TabletopMap.vue';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
import TabletopActionCards from '/imports/client/ui/tabletop/TabletopActionCards.vue';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import MiniCharacterSheet from '/imports/client/ui/creature/character/MiniCharacterSheet.vue';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import ActionCard from '/imports/client/ui/tabletop/TabletopActionCard.vue';
|
||||
import SelectedCreatureBar from '/imports/client/ui/tabletop/selectedCreatureBar/SelectedCreatureBar.vue';
|
||||
|
||||
const getProperties = function (creatureId, selector = {}) {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': {
|
||||
$eq: creatureId,
|
||||
},
|
||||
inactive: { $ne: true },
|
||||
removed: { $ne: true },
|
||||
overridden: { $ne: true },
|
||||
$nor: [
|
||||
{ hideWhenTotalZero: true, total: 0 },
|
||||
{ hideWhenValueZero: true, value: 0 },
|
||||
],
|
||||
...selector,
|
||||
}, {
|
||||
sort: { order: 1 }
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TabletopCreatureCard,
|
||||
TabletopMap,
|
||||
TabletopActionCards,
|
||||
ActionCard,
|
||||
MiniCharacterSheet,
|
||||
SelectedCreatureBar,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
@@ -58,9 +136,20 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['editPermission'],
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeCreature: undefined,
|
||||
activeCreatureId: undefined,
|
||||
activeActionId: undefined,
|
||||
targets: [],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeCreatureId(id) {
|
||||
this.$root.$emit('active-tabletop-character-change', id);
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
@@ -69,9 +158,29 @@ export default {
|
||||
return [this.model._id];
|
||||
},
|
||||
},
|
||||
creatures() {
|
||||
creatures(){
|
||||
return Creatures.find({ tabletop: this.model._id });
|
||||
},
|
||||
actions(){
|
||||
return getProperties(this.activeCreatureId, { type: 'action', actionType: { $ne: 'event'} });
|
||||
},
|
||||
moreTargets(){
|
||||
const activeAction = CreatureProperties.findOne(this.activeActionId);
|
||||
if (!activeAction) return;
|
||||
if (activeAction.target === 'singleTarget') {
|
||||
return this.targets.length === 0;
|
||||
} else if (activeAction.target === 'multipleTargets') {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
editPermission(){
|
||||
try {
|
||||
assertEditPermission(this.activeCreatureId, Meteor.userId());
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addCreature() {
|
||||
@@ -91,16 +200,53 @@ export default {
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
openCharacterSheetDialog(){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'character-sheet-dialog',
|
||||
elementId: 'mini-character-sheet',
|
||||
data: {
|
||||
creatureId: this.activeCreatureId,
|
||||
},
|
||||
});
|
||||
},
|
||||
clickProperty({_id}){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `${_id}`,
|
||||
data: {_id},
|
||||
});
|
||||
},
|
||||
transformScroll(event) {
|
||||
if (!event.deltaY) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.scrollLeft += event.deltaY;
|
||||
event.preventDefault();
|
||||
},
|
||||
untarget(id){
|
||||
const index = this.targets.indexOf(id);
|
||||
if (index > -1) {
|
||||
this.targets.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.initiative-row>.v-card {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
height: 162px;
|
||||
width: 100px;
|
||||
margin: 4px;
|
||||
}
|
||||
.action-row > div {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
height: 120px;
|
||||
width: 200px;
|
||||
margin: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,37 +1,146 @@
|
||||
<template lang="html">
|
||||
<v-card
|
||||
style="height: 150px; min-width: 120px;"
|
||||
:style="`height: ${height}px; width: ${width}px; overflow: hidden;`"
|
||||
class="tabletop-creature-card"
|
||||
:class="{ active }"
|
||||
:hover="hasClickListener"
|
||||
:elevation="active ? 8 : 2"
|
||||
@mouseover="() => { if (hasClickListener) hover = true; }"
|
||||
@mouseleave="hover = false"
|
||||
v-on="hasClickListener ? {click: () => $emit('click')} : {}"
|
||||
>
|
||||
<v-img
|
||||
:src="model.picture"
|
||||
aspect-ratio="1"
|
||||
:src="model.picture || '/images/ui/missing-portrait.png'"
|
||||
:lazy-src="loadingImg"
|
||||
:height="height"
|
||||
:width="width"
|
||||
class="align-end"
|
||||
:class="{placeholder: !model.picture}"
|
||||
gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)"
|
||||
position="top center"
|
||||
/>
|
||||
<div
|
||||
class="small-title"
|
||||
>
|
||||
{{ model.name }}
|
||||
<v-card-title
|
||||
class="small-title"
|
||||
v-text="model.name"
|
||||
/>
|
||||
<health-bar-progress
|
||||
v-for="bar in healthBars"
|
||||
:key="bar._id"
|
||||
:model="bar"
|
||||
:height="4"
|
||||
style="opacity: 0.7; margin-top: 2px"
|
||||
/>
|
||||
</v-img>
|
||||
<card-highlight :active="hover" />
|
||||
<div class="d-flex justify-center">
|
||||
<v-scale-transition>
|
||||
<v-btn
|
||||
v-if="showTargetBtn"
|
||||
:color="targeted ? 'accent' : ''"
|
||||
:elevation="targeted ? 8 : 2"
|
||||
fab
|
||||
small
|
||||
@click.stop="targeted ? $emit('untarget') : $emit('target')"
|
||||
>
|
||||
<v-icon>{{ targeted ? 'mdi-target' : 'mdi-target' }}</v-icon>
|
||||
</v-btn>
|
||||
</v-scale-transition>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import CardHighlight from '/imports/client/ui/components/CardHighlight.vue';
|
||||
import HealthBarProgress from '/imports/client/ui/properties/components/attributes/HealthBarProgress.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CardHighlight,
|
||||
HealthBarProgress,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 75,
|
||||
},
|
||||
active: Boolean,
|
||||
targeted: Boolean,
|
||||
showTargetBtn: Boolean,
|
||||
},
|
||||
data(){return {
|
||||
hover: false,
|
||||
loadingImg: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAXdJREFUWEftlq1vAkEQxedSB7pYDklBNsgmyCa4Cv5GBI6k8tJKUglULrWcBlvySA4B+/HeAcV07O3t/ObtvNnNHsyc3TEyAGRm+T0Yfs3WFwE0mk3bbbe12WsDtDsda+e5PbZaR4C1c/a9XEowtQC6vZ499ftniaCECiEDoPLnwSBa5WdRWLnZUErIAK+jkeHcY4HkgGBCAmCqr5KyKkgAobP3VXoTAJw9VGBitVhQjpAUeBkOD7Zj4sc5+5rPk0slAOUIkBwQqZAAUD1UYOImALAfbMjE+2xGjWhJASRmGpFtQOwnAzAqTCcTRqTDGhkAP8UGklK9BIDKq9svZUVcShjHZVkmnUApoNjvVHvAfBRFsCGTAJckr2AAAVf4Igqg+D7VdaHJGAVQRm8KIKRCFOBtPE7tK3333ZBBAOXuZyl8Fv1TAF8fBAGYkctWXq3zPdWCANdswJgdgwDM41NVwOeEf4BoE6be/3WO4PSdeARQN7vm+j0BlQ9wWvLB6AAAAABJRU5ErkJggg==',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasClickListener() {
|
||||
return this.$listeners && !!this.$listeners.click;
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
healthBars() {
|
||||
const folderIds = CreatureProperties.find({
|
||||
'ancestors.id': this.model._id,
|
||||
type: 'folder',
|
||||
groupStats: true,
|
||||
hideStatsGroup: true,
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}, { fields: { _id: 1 } }).map(folder => folder._id);
|
||||
|
||||
// Get the properties that need to be shown as a health bar
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.model._id,
|
||||
'parent.id': {
|
||||
$nin: folderIds,
|
||||
},
|
||||
type: 'attribute',
|
||||
attributeType: 'healthBar',
|
||||
healthBarNoDamage: { $ne: true },
|
||||
inactive: { $ne: true } ,
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
sort: {
|
||||
order: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.small-title {
|
||||
font-size: 14px;
|
||||
padding: 4px;
|
||||
font-size: 12px;
|
||||
padding: 4px 4px 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
line-height: normal;
|
||||
}
|
||||
.active {
|
||||
transform: scale(1.2);
|
||||
margin-left: 12px !important;
|
||||
margin-right: 12px !important;
|
||||
transform-origin: top center;
|
||||
}
|
||||
.tabletop-creature-card {
|
||||
transition: all .15s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="css">
|
||||
.tabletop-creature-card .v-btn {
|
||||
transition: all .3s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,52 +1,38 @@
|
||||
<template lang="html">
|
||||
<div class="tabletop-log">
|
||||
<div class="messages layout column justify-end align-end">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message._id"
|
||||
class="message"
|
||||
>
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
<v-textarea
|
||||
v-model="messageContent"
|
||||
@keyup.enter.prevent="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
<character-log
|
||||
:tabletop-id="tabletopId"
|
||||
:creature-id="activeCreatureId"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Messages, { sendMessage } from '/imports/api/tabletop/Messages';
|
||||
import { insertTabletopLog } from '/imports/api/creature/log/CreatureLogs';
|
||||
import CharacterLog from '/imports/client/ui/log/CharacterLog.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CharacterLog,
|
||||
},
|
||||
inject: {
|
||||
context: {
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
tabletopId: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data(){ return {
|
||||
messageContent: '',
|
||||
}},
|
||||
meteor: {
|
||||
messages() {
|
||||
return Messages.find({
|
||||
tabletopId: this.tabletopId,
|
||||
}, {
|
||||
sort: {
|
||||
timeStamp: 1,
|
||||
},
|
||||
});
|
||||
data() {
|
||||
return {
|
||||
activeCreatureId: undefined,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendMessage(){
|
||||
sendMessage.call({
|
||||
content: this.messageContent,
|
||||
tabletopId: this.tabletopId,
|
||||
});
|
||||
this.messageContent = '';
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on('active-tabletop-character-change', (id) => {
|
||||
this.activeCreatureId = id;
|
||||
});
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,134 @@
|
||||
<template lang="html">
|
||||
<div class="tabletop-map" />
|
||||
<template>
|
||||
<div>
|
||||
<canvas
|
||||
ref="map"
|
||||
class="tabletop-map"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import * as THREE from 'three';
|
||||
import { Tracker } from 'meteor/tracker'
|
||||
import TabletopObjects from '/imports/api/tabletop/TabletopObjects.js';
|
||||
import TabletopMaps from '/imports/api/tabletop/TabletopMaps.js';
|
||||
import { OrbitControls } from '/imports/api/tabletop/three/OrbitControls.js';
|
||||
import { DragControls } from 'three/examples/jsm/controls/DragControls.js';
|
||||
import { MapControls } from 'three/examples/jsm/controls/MapControls.js';
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader';
|
||||
|
||||
const maps = [
|
||||
{
|
||||
name: 'first map',
|
||||
position: { x: 0, y: 0 },
|
||||
width: 25,
|
||||
height: 25,
|
||||
texture: '/images/battlemap.webp',
|
||||
}
|
||||
];
|
||||
|
||||
export default {
|
||||
mounted(){
|
||||
const scene = new THREE.Scene();
|
||||
const perspectiveCam = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
|
||||
perspectiveCam.position.z = 5;
|
||||
const orthoCam = new THREE.OrthographicCamera( -2, 2, 2, -2, 0, 1000 );
|
||||
orthoCam.position.z = 500
|
||||
const activeCamera = orthoCam;
|
||||
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ canvas: this.$refs.map });
|
||||
renderer.shadowMap.enabled = true;
|
||||
|
||||
activeCamera.up.set( 0, 0, 1 ); // Use z as upwards
|
||||
const controls = new MapControls( activeCamera, renderer.domElement );
|
||||
|
||||
maps.forEach(map => {
|
||||
const texture = new THREE.TextureLoader().load( map.texture );
|
||||
const material = new THREE.MeshStandardMaterial({ map: texture });
|
||||
material.map.needsUpdate = true;
|
||||
const plane = new THREE.Mesh(new THREE.PlaneGeometry(map.width, map.height), material);
|
||||
plane.overdraw = true;
|
||||
plane.receiveShadow = true;
|
||||
scene.add(plane);
|
||||
});
|
||||
|
||||
// Example model
|
||||
const loader = new STLLoader()
|
||||
loader.load(
|
||||
'/models/example-mini.stl',
|
||||
function (geometry) {
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: 0xb2ffc8,
|
||||
});
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.castShadow = true;
|
||||
scene.add(mesh)
|
||||
const light = new THREE.PointLight()
|
||||
light.position.set(0, 0, 50)
|
||||
light.castShadow = true;
|
||||
scene.add(light)
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
|
||||
directionalLight.position.set(20, 50, 100)
|
||||
directionalLight.castShadow = true;
|
||||
scene.add( directionalLight );
|
||||
const dragControls = new DragControls([mesh], activeCamera, renderer.domElement);
|
||||
dragControls.addEventListener( 'dragstart', function ( event ) {
|
||||
controls.enabled = false;
|
||||
});
|
||||
dragControls.addEventListener( 'dragend', function ( event ) {
|
||||
controls.enabled = true;
|
||||
});
|
||||
},
|
||||
(xhr) => {
|
||||
console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
|
||||
},
|
||||
(error) => {
|
||||
console.log(error)
|
||||
}
|
||||
)
|
||||
//controls.enabled = false;
|
||||
|
||||
//});
|
||||
|
||||
/*
|
||||
const axesHelper = new THREE.AxesHelper( 5 );
|
||||
scene.add( axesHelper );
|
||||
*/
|
||||
|
||||
function resizeCanvasToDisplaySize() {
|
||||
const canvas = renderer.domElement;
|
||||
// look up the size the canvas is being displayed
|
||||
const width = canvas.clientWidth;
|
||||
const height = canvas.clientHeight - 50;
|
||||
|
||||
// adjust displayBuffer size to match
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
// you must pass false here or three.js sadly fights the browser
|
||||
perspectiveCam.aspect = width / height;
|
||||
orthoCam.left= width / -200;
|
||||
orthoCam.right = width / 200;
|
||||
orthoCam.top = height / 200;
|
||||
orthoCam.bottom = height / -200;
|
||||
perspectiveCam.updateProjectionMatrix();
|
||||
orthoCam.updateProjectionMatrix();
|
||||
controls.update();
|
||||
renderer.setSize(width, height, false);
|
||||
}
|
||||
}
|
||||
function animate() {
|
||||
resizeCanvasToDisplaySize();
|
||||
renderer.render( scene, activeCamera );
|
||||
requestAnimationFrame( animate );
|
||||
}
|
||||
animate();
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
<style>
|
||||
.tabletop-map {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template lang="html">
|
||||
<v-btn
|
||||
icon
|
||||
:plain="!selected"
|
||||
large
|
||||
tile
|
||||
:outlined="selected"
|
||||
:color="prop && prop.color"
|
||||
@click.prevent="$emit('click', $event)"
|
||||
@mouseenter="$emit('mouseenter', $event)"
|
||||
@mouseleave="$emit('mouseleave', $event)"
|
||||
>
|
||||
<property-icon
|
||||
v-if="prop"
|
||||
:model="prop"
|
||||
:color="prop.color"
|
||||
/>
|
||||
<v-icon v-else-if="icon">
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<v-icon v-else>
|
||||
mdi-help
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PropertyIcon,
|
||||
},
|
||||
props: {
|
||||
propId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
selected: Boolean,
|
||||
},
|
||||
meteor: {
|
||||
prop() {
|
||||
if (!this.propId) return;
|
||||
return CreatureProperties.findOne(this.propId);
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,432 @@
|
||||
<template lang="html">
|
||||
<div
|
||||
v-if="creatureId"
|
||||
class="selected-creature-bar d-flex pa-3 align-end"
|
||||
style="gap: 8px;"
|
||||
>
|
||||
<!--
|
||||
<tabletop-buff-icons
|
||||
creature-id="creatureId"
|
||||
@select-icon="selectIcon"
|
||||
/>
|
||||
<tabletop-portrait
|
||||
creature-id="creatureId"
|
||||
@select-icon="selectIcon"
|
||||
/>
|
||||
-->
|
||||
<v-menu
|
||||
v-model="menuOpen"
|
||||
v-click-outside="{
|
||||
handler: clickOutsideMenu,
|
||||
include: menuClickOutsideInclude,
|
||||
}"
|
||||
:position-x="menuX"
|
||||
:position-y="menuY"
|
||||
absolute
|
||||
top
|
||||
:nudge-left="150"
|
||||
origin="center bottom"
|
||||
:close-on-click="false"
|
||||
:content-class="`tabletop-prop-menu rows-${rows}`"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<tabletop-action-card
|
||||
v-if="selectedProp && selectedProp.type === 'action'"
|
||||
style="width: 300px;"
|
||||
:style="{
|
||||
width: '300px',
|
||||
opacity: selectedIcon ? 1 : 0.7,
|
||||
transition: 'opacity 0.2s ease',
|
||||
}"
|
||||
:model="selectedProp"
|
||||
/>
|
||||
<v-card
|
||||
v-else-if="activeIcon && activeIcon.tab"
|
||||
style="width: 300px"
|
||||
>
|
||||
<v-card-title>
|
||||
<v-icon left>
|
||||
{{ activeIcon.icon }}
|
||||
</v-icon>
|
||||
{{ activeIcon.tabName }}
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<v-card
|
||||
v-if="iconGroups.buffs"
|
||||
class="buffs-card"
|
||||
>
|
||||
<div
|
||||
v-for="(row, rowIndex) in iconGroups.buffs.rows"
|
||||
:key="rowIndex"
|
||||
class="d-flex"
|
||||
>
|
||||
<template
|
||||
v-for="(icon, iconIndex) in row"
|
||||
>
|
||||
<creature-bar-icon
|
||||
:key="icon.propId || iconIndex"
|
||||
:prop-id="icon.propId"
|
||||
:icon="icon.icon"
|
||||
:selected="selectedIcon === icon"
|
||||
:data-id="icon.propId || icon.standardId"
|
||||
@click="e => selectIcon(e, icon)"
|
||||
@mouseenter="e => hoverIcon(e, icon)"
|
||||
@mouseleave="unHoverIcon(icon)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</v-card>
|
||||
<v-card
|
||||
class="creature-portrait"
|
||||
:width="90"
|
||||
:height="120"
|
||||
>
|
||||
<v-img
|
||||
v-if="creature.picture"
|
||||
:height="120"
|
||||
:src="creature.picture"
|
||||
position="top center"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="fill-height d-flex align-center justify-center"
|
||||
style="opacity: 0.2;"
|
||||
>
|
||||
<v-icon
|
||||
size="90"
|
||||
>
|
||||
mdi-account
|
||||
</v-icon>
|
||||
</div>
|
||||
</v-card>
|
||||
<v-card
|
||||
v-for="group in iconGroups"
|
||||
:key="group.name"
|
||||
>
|
||||
<div
|
||||
v-for="(row, rowIndex) in group.rows"
|
||||
:key="rowIndex"
|
||||
class="d-flex"
|
||||
>
|
||||
<template
|
||||
v-for="(icon, iconIndex) in row"
|
||||
>
|
||||
<creature-bar-icon
|
||||
:key="icon.propId || iconIndex"
|
||||
:prop-id="icon.propId"
|
||||
:icon="icon.icon"
|
||||
:selected="selectedIcon === icon"
|
||||
:data-id="icon.propId || icon.standardId"
|
||||
@click="e => selectIcon(e, icon)"
|
||||
@mouseenter="e => hoverIcon(e, icon)"
|
||||
@mouseleave="unHoverIcon(icon)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</v-card>
|
||||
<!--<tabletop-actions
|
||||
creature-id="creatureId"
|
||||
@select-icon="selectIcon"
|
||||
/>
|
||||
<tabletop-detail-popover />
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import TabletopActionCard from '/imports/client/ui/tabletop/TabletopActionCard.vue';
|
||||
import CreatureBarIcon from '/imports/client/ui/tabletop/selectedCreatureBar/CreatureBarIcon.vue';
|
||||
|
||||
//import TabletopPortrait from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopPortrait.vue';
|
||||
//import TabletopBuffIcons from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopBuffIcons.vue';
|
||||
//import TabletopActions from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopActions.vue';
|
||||
//import TabletopGroupedFolders from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopGroupedFolders.vue';
|
||||
//import TabletopResources from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopResources.vue';
|
||||
//import TabletopCreatureSheetTabs from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopCreatureSheetTabs.vue';
|
||||
//import TabletopDetailPopover from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopDetailPopover.vue';
|
||||
|
||||
function splitToNChunks(inputArray, n) {
|
||||
let result = [];
|
||||
const array = [...inputArray] // Create shallow copy, because splice mutates array
|
||||
for (let i = n; i > 0; i--) {
|
||||
result.push(array.splice(0, Math.ceil(array.length / i)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
//TabletopPortrait,
|
||||
//TabletopBuffIcons,
|
||||
//TabletopActions,
|
||||
//TabletopGroupedFolders,
|
||||
//TabletopResources,
|
||||
//TabletopCreatureSheetTabs,
|
||||
CreatureBarIcon,
|
||||
TabletopActionCard,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rows: 2,
|
||||
hoveredIcon: undefined,
|
||||
selectedIcon: undefined,
|
||||
menuOpen: false,
|
||||
menuX: 200,
|
||||
menuY: window.innerHeight - 216,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
activeIcon() {
|
||||
return this.selectedIcon || this.hoveredIcon;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
menuOpen(val) {
|
||||
if (!val && this.selectIcon) {
|
||||
this.selectedIcon = undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
log(e) {
|
||||
console.log(e);
|
||||
},
|
||||
hoverIcon(e, icon) {
|
||||
if (this.selectedIcon) return;
|
||||
// this.menuX = e.clientX - (e.clientX % 44);
|
||||
const { left, right } = e.target.getBoundingClientRect();
|
||||
const x = ( left + right ) / 2
|
||||
this.menuX = x;
|
||||
this.hoveredIcon = icon;
|
||||
this.menuOpen = true;
|
||||
},
|
||||
unHoverIcon(icon) {
|
||||
if (this.hoveredIcon === icon) {
|
||||
this.hoveredIcon = undefined;
|
||||
if (!this.selectedIcon) {
|
||||
this.menuOpen = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
selectIcon(e, icon) {
|
||||
if (icon.tab) {
|
||||
this.openCharacterSheet(icon.tab, icon.standardId);
|
||||
return;
|
||||
}
|
||||
if (this.selectedIcon === icon) {
|
||||
this.selectedIcon = undefined;
|
||||
this.menuOpen = false;
|
||||
return;
|
||||
}
|
||||
const { left, right } = e.target.getBoundingClientRect();
|
||||
const x = ( left + right ) / 2
|
||||
this.menuX = x;
|
||||
this.selectedIcon = icon;
|
||||
this.menuOpen = true;
|
||||
},
|
||||
clickOutsideMenu () {
|
||||
this.menuOpen = false;
|
||||
},
|
||||
menuClickOutsideInclude() {
|
||||
return [
|
||||
document.querySelector('.selected-creature-bar'),
|
||||
document.querySelector('.tabletop-prop-menu')
|
||||
];
|
||||
},
|
||||
openCharacterSheet(tab, elementId) {
|
||||
this.$store.commit(
|
||||
'setTabForCharacterSheet',
|
||||
{ id: this.creatureId, tab }
|
||||
);
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'character-sheet-dialog',
|
||||
elementId,
|
||||
data: {
|
||||
creatureId: this.creatureId,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
creature() {
|
||||
if (!this.creatureId) return;
|
||||
return Creatures.findOne(this.creatureId)
|
||||
},
|
||||
selectedProp() {
|
||||
const propId = this.activeIcon?.propId;
|
||||
if (!propId) return;
|
||||
return CreatureProperties.findOne(propId);
|
||||
},
|
||||
iconGroups() {
|
||||
if (!this.creature) return;
|
||||
const iconGroups = [];
|
||||
|
||||
// Get the standard icons
|
||||
const standardIconsById = {
|
||||
'cast-spell': {standardId: 'cast-spell', groupName: 'Standard Actions', icon: 'mdi-fire' },
|
||||
'make-check': {standardId: 'make-check', groupName: 'Standard Actions', icon: 'mdi-radiobox-marked' },
|
||||
'roll-dice': {standardId: 'roll-dice', groupName: 'Standard Actions', icon: 'mdi-dice-d20' },
|
||||
'tab-stats': {standardId: 'tab-stats', groupName: 'Tabs', icon: 'mdi-chart-box', tab: 'stats', tabName: 'Stats' },
|
||||
'tab-actions': {standardId: 'tab-actions', groupName: 'Tabs', icon: 'mdi-lightning-bolt', tab: 'actions', tabName: 'Actions' },
|
||||
'tab-spells': this.creature?.settings?.hideSpellsTab ? undefined : {standardId: 'tab-spells', groupName: 'Tabs', icon: 'mdi-fire', tab: 'spells', tabName: 'Spells' },
|
||||
'tab-inventory': {standardId: 'tab-inventory', groupName: 'Tabs', icon: 'mdi-cube', tab: 'inventory', tabName: 'Inventory' },
|
||||
'tab-features': {standardId: 'tab-features', groupName: 'Tabs', icon: 'mdi-text', tab: 'features', tabName: 'Features' },
|
||||
'tab-journal': {standardId: 'tab-journal', groupName: 'Tabs', icon: 'mdi-book-open-variant', tab: 'journal', tabName: 'Journal' },
|
||||
'tab-build': {standardId: 'tab-build', groupName: 'Tabs', icon: 'mdi-wrench', tab: 'build', tabName: 'Build' },
|
||||
};
|
||||
|
||||
// Get the folders that could hide a property
|
||||
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);
|
||||
|
||||
// Get the properties that need to be shown as an icon
|
||||
const filter = {
|
||||
'ancestors.id': this.creatureId,
|
||||
'parent.id': {
|
||||
$nin: folderIds,
|
||||
},
|
||||
$and: [
|
||||
{
|
||||
$or: [
|
||||
{ type: 'action' },
|
||||
{ type: 'folder', groupStats: true },
|
||||
{ type: 'attribute' },
|
||||
{ type: 'toggle' },
|
||||
{ type: 'buff' }
|
||||
],
|
||||
},
|
||||
{
|
||||
$or: [
|
||||
{ inactive: { $ne: true } },
|
||||
{ type: 'toggle' },
|
||||
]
|
||||
}
|
||||
],
|
||||
removed: { $ne: true },
|
||||
};
|
||||
if (this.creature.settings?.hideUnusedStats) {
|
||||
filter.hide = { $ne: true };
|
||||
}
|
||||
|
||||
// Get all the properties we wish to display, with just their IDs, and store them
|
||||
const propsById = {};
|
||||
const props = [];
|
||||
CreatureProperties.find(filter, {
|
||||
sort: { order: -1 },
|
||||
fields: { _id: 1, type: 1 },
|
||||
}).forEach(prop => {
|
||||
props.push(prop);
|
||||
propsById[prop._id] = prop;
|
||||
});
|
||||
|
||||
// Using the creature's custom icon groups, collect the props into groups
|
||||
this.creature.tabletopSettings?.iconGroups.forEach(group => {
|
||||
const iconList = [];
|
||||
group.iconIds?.forEach(id => {
|
||||
if (propsById[id]) {
|
||||
const prop = propsById[id];
|
||||
prop._placedInGroup = true;
|
||||
iconList.push({ propId: prop._id });
|
||||
} else if (standardIconsById[id]) {
|
||||
const standardIcon = standardIconsById[id];
|
||||
standardIcon._placedInGroup = true;
|
||||
iconList.push(standardIcon);
|
||||
}
|
||||
});
|
||||
iconGroups.push({
|
||||
name: group.name,
|
||||
iconList,
|
||||
});
|
||||
});
|
||||
|
||||
// Default groups
|
||||
let groupsByName = {};
|
||||
let defaultGroups = [];
|
||||
|
||||
// Add default groups for props that have not yet been collected into custom groups
|
||||
props.forEach(prop => {
|
||||
if (prop._placedInGroup) return;
|
||||
let groupName;
|
||||
switch (prop.type) {
|
||||
case 'buff': groupName = 'Buffs'; break;
|
||||
case 'action': groupName = 'Actions'; break;
|
||||
case 'resource': groupName = 'Resources'; break;
|
||||
case 'folder': groupName = 'Folders'; break;
|
||||
}
|
||||
if (!groupName) return;
|
||||
if (!groupsByName[groupName]) {
|
||||
groupsByName[groupName] = { name: groupName, iconList: [] };
|
||||
if (groupName !== 'Buffs') { // don't add buffs to the default groups, it is handled differently
|
||||
defaultGroups.push(groupsByName[groupName]);
|
||||
}
|
||||
}
|
||||
groupsByName[groupName].iconList.push({ propId: prop._id });
|
||||
});
|
||||
|
||||
// Add default groups for standard icons
|
||||
for (let key in standardIconsById) {
|
||||
const standardIcon = standardIconsById[key];
|
||||
if (!standardIcon) continue;
|
||||
if (standardIcon._placedInGroup) continue;
|
||||
|
||||
const groupName = standardIcon.groupName || 'no';
|
||||
if (!groupsByName[groupName]) {
|
||||
groupsByName[groupName] = { name: groupName, iconList: [] };
|
||||
defaultGroups.push(groupsByName[groupName]);
|
||||
}
|
||||
|
||||
groupsByName[groupName].iconList.push(standardIcon);
|
||||
}
|
||||
|
||||
iconGroups.push(...defaultGroups);
|
||||
|
||||
// Store a specific reference to buffs outside of the list order
|
||||
iconGroups.buffs = groupsByName['Buffs'];
|
||||
|
||||
// Divide the icons into rows
|
||||
iconGroups.forEach(group => {
|
||||
group.rows = splitToNChunks(group.iconList, this.rows);
|
||||
});
|
||||
if (iconGroups.buffs) {
|
||||
iconGroups.buffs.rows = splitToNChunks(iconGroups.buffs.iconList, this.rows);
|
||||
}
|
||||
|
||||
return iconGroups;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.tabletop-prop-menu {
|
||||
top: unset !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.tabletop-prop-menu.rows-1 {
|
||||
bottom: 80px;
|
||||
}
|
||||
.tabletop-prop-menu.rows-2 {
|
||||
bottom: 124px;
|
||||
}
|
||||
.tabletop-prop-menu.rows-3 {
|
||||
bottom: 168px;
|
||||
}
|
||||
.tabletop-prop-menu.rows-4 {
|
||||
bottom: 212px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user