Merge branch 'feature-tabletop' into develop
This commit is contained in:
@@ -62,6 +62,34 @@ let CreatureSettingsSchema = new SimpleSchema({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let IconGroupSchema = new SimpleSchema({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
max: STORAGE_LIMITS.name,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
iconIds: {
|
||||||
|
type: Array,
|
||||||
|
max: 4,
|
||||||
|
defaultValue: [],
|
||||||
|
},
|
||||||
|
'iconIds.$': {
|
||||||
|
type: String,
|
||||||
|
max: STORAGE_LIMITS.variableName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let CreatureTabletopSettingsSchema = new SimpleSchema({
|
||||||
|
iconGroups: {
|
||||||
|
type: Array,
|
||||||
|
defaultValue: [],
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
|
'iconGroups.$': {
|
||||||
|
type: IconGroupSchema,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
let CreatureSchema = new SimpleSchema({
|
let CreatureSchema = new SimpleSchema({
|
||||||
// Strings
|
// Strings
|
||||||
name: {
|
name: {
|
||||||
@@ -177,6 +205,10 @@ let CreatureSchema = new SimpleSchema({
|
|||||||
type: SimpleSchema.Integer,
|
type: SimpleSchema.Integer,
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
|
tabletopSettings: {
|
||||||
|
type: CreatureTabletopSettingsSchema,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
settings: {
|
settings: {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { assertEditPermission } from '/imports/api/creature/creatures/creaturePe
|
|||||||
import { parse, prettifyParseError } from '/imports/parser/parser';
|
import { parse, prettifyParseError } from '/imports/parser/parser';
|
||||||
import resolve, { toString } from '/imports/parser/resolve';
|
import resolve, { toString } from '/imports/parser/resolve';
|
||||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
|
||||||
|
import { assertUserInTabletop } from '/imports/api/tabletop/methods/shared/tabletopPermissions.js';
|
||||||
|
|
||||||
const PER_CREATURE_LOG_LIMIT = 100;
|
const PER_CREATURE_LOG_LIMIT = 100;
|
||||||
|
|
||||||
@@ -42,6 +43,12 @@ let CreatureLogSchema = new SimpleSchema({
|
|||||||
regEx: SimpleSchema.RegEx.Id,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
index: 1,
|
index: 1,
|
||||||
},
|
},
|
||||||
|
tabletopId: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
index: 1,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
creatureName: {
|
creatureName: {
|
||||||
type: String,
|
type: String,
|
||||||
optional: true,
|
optional: true,
|
||||||
@@ -119,6 +126,7 @@ const insertCreatureLog = new ValidatedMethod({
|
|||||||
'settings.discordWebhook': 1,
|
'settings.discordWebhook': 1,
|
||||||
name: 1,
|
name: 1,
|
||||||
avatarPicture: 1,
|
avatarPicture: 1,
|
||||||
|
tabletop: 1,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
assertEditPermission(creature, this.userId);
|
assertEditPermission(creature, this.userId);
|
||||||
@@ -128,7 +136,7 @@ const insertCreatureLog = new ValidatedMethod({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function insertCreatureLogWork({ log, creature, method }) {
|
export function insertCreatureLogWork({ log, creature, tabletopId, method }) {
|
||||||
// Build the new log
|
// Build the new log
|
||||||
if (typeof log === 'string') {
|
if (typeof log === 'string') {
|
||||||
log = { content: [{ value: log }] };
|
log = { content: [{ value: log }] };
|
||||||
@@ -142,13 +150,20 @@ export function insertCreatureLogWork({ log, creature, method }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
log.date = new Date();
|
log.date = new Date();
|
||||||
|
if (tabletopId) log.tabletopId = tabletopId;
|
||||||
|
if (creature && creature.tabletop) log.tabletopId = creature.tabletop;
|
||||||
// Insert it
|
// Insert it
|
||||||
let id = CreatureLogs.insert(log);
|
let id = CreatureLogs.insert(log);
|
||||||
if (Meteor.isServer) {
|
if (Meteor.isServer) {
|
||||||
method?.unblock();
|
method?.unblock();
|
||||||
removeOldLogs(creature._id);
|
if (creature) {
|
||||||
logWebhook({ log, creature });
|
removeOldLogs(creature._id);
|
||||||
|
logWebhook({ log, creature });
|
||||||
|
}
|
||||||
|
if (log.tabletopId) {
|
||||||
|
// Todo remove old tabletop logs
|
||||||
|
// Log webhook if it's different to creature webhook
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -170,24 +185,39 @@ const logRoll = new ValidatedMethod({
|
|||||||
roll: {
|
roll: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
tabletopId: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
creatureId: {
|
creatureId: {
|
||||||
type: String,
|
type: String,
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
optional: true,
|
||||||
},
|
},
|
||||||
}).validator(),
|
}).validator(),
|
||||||
run({ roll, creatureId }) {
|
run({ roll, tabletopId, creatureId }) {
|
||||||
const creature = Creatures.findOne(creatureId, {
|
if (!creatureId && !tabletopId) throw new Meteor.Error('no-id',
|
||||||
fields: {
|
'A creature id or tabletop id must be given'
|
||||||
readers: 1,
|
);
|
||||||
writers: 1,
|
let creature;
|
||||||
owner: 1,
|
if (creatureId) {
|
||||||
'settings.discordWebhook': 1,
|
creature = Creatures.findOne(creatureId, {
|
||||||
name: 1,
|
fields: {
|
||||||
avatarPicture: 1,
|
readers: 1,
|
||||||
}
|
writers: 1,
|
||||||
});
|
owner: 1,
|
||||||
assertEditPermission(creature, this.userId);
|
'settings.discordWebhook': 1,
|
||||||
const variables = CreatureVariables.findOne({ _creatureId: creatureId });
|
name: 1,
|
||||||
|
avatarPicture: 1,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assertEditPermission(creature, this.userId);
|
||||||
|
}
|
||||||
|
if (tabletopId) {
|
||||||
|
assertUserInTabletop(tabletopId, this.userId);
|
||||||
|
}
|
||||||
|
const variables = CreatureVariables.findOne({ _creatureId: creatureId }) || {};
|
||||||
let logContent = []
|
let logContent = []
|
||||||
let parsedResult = undefined;
|
let parsedResult = undefined;
|
||||||
try {
|
try {
|
||||||
@@ -228,7 +258,7 @@ const logRoll = new ValidatedMethod({
|
|||||||
date: new Date(),
|
date: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let id = insertCreatureLogWork({ log, creature, method: this });
|
let id = insertCreatureLogWork({ log, creature, tabletopId, method: this });
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ export default function computeAction(computation, node) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
prop.resources.itemsConsumed?.forEach(itemConsumed => {
|
prop.resources.itemsConsumed?.forEach(itemConsumed => {
|
||||||
if (!itemConsumed.itemId) return;
|
if (!itemConsumed.itemId || itemConsumed.available < itemConsumed.quantity?.value) {
|
||||||
if (itemConsumed.available < itemConsumed.quantity?.value) {
|
|
||||||
prop.insufficientResources = true;
|
prop.insufficientResources = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,15 +12,17 @@ export const loadedCreatures: Map<string, LoadedCreature> = new Map(); // creatu
|
|||||||
export function loadCreature(creatureId: string, subscription: Tracker.Computation) {
|
export function loadCreature(creatureId: string, subscription: Tracker.Computation) {
|
||||||
if (!creatureId) throw 'creatureId is required';
|
if (!creatureId) throw 'creatureId is required';
|
||||||
let creature = loadedCreatures.get(creatureId);
|
let creature = loadedCreatures.get(creatureId);
|
||||||
|
if (!creature || !creature.subs.has(subscription)) {
|
||||||
|
subscription.onStop(() => {
|
||||||
|
unloadCreature(creatureId, subscription);
|
||||||
|
});
|
||||||
|
}
|
||||||
if (creature) {
|
if (creature) {
|
||||||
creature.subs.add(subscription);
|
creature.subs.add(subscription);
|
||||||
} else {
|
} else {
|
||||||
creature = new LoadedCreature(subscription, creatureId);
|
creature = new LoadedCreature(subscription, creatureId);
|
||||||
loadedCreatures.set(creatureId, creature);
|
loadedCreatures.set(creatureId, creature);
|
||||||
}
|
}
|
||||||
subscription.onStop(() => {
|
|
||||||
unloadCreature(creatureId, subscription);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function unloadCreature(creatureId, subscription) {
|
function unloadCreature(creatureId, subscription) {
|
||||||
@@ -188,14 +190,15 @@ export function getPropertyChildren(creatureId, property) {
|
|||||||
// This propertyId will always appear in the parent of the children
|
// This propertyId will always appear in the parent of the children
|
||||||
if (loadedCreatures.has(creatureId)) {
|
if (loadedCreatures.has(creatureId)) {
|
||||||
const creature = loadedCreatures.get(creatureId);
|
const creature = loadedCreatures.get(creatureId);
|
||||||
const props = [];
|
if (!creature) return [];
|
||||||
|
const props: CreatureProperty[] = [];
|
||||||
for (const prop of creature.properties.values()) {
|
for (const prop of creature.properties.values()) {
|
||||||
if (prop.parent?.id === property._id) {
|
if (prop.parentId === property._id) {
|
||||||
props.push(prop);
|
props.push(prop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const cloneProps = EJSON.clone(props);
|
const cloneProps = EJSON.clone(props);
|
||||||
return cloneProps.sort((a, b) => a.order - b.order);
|
return cloneProps.sort((a, b) => a.left - b.left);
|
||||||
} else {
|
} else {
|
||||||
return CreatureProperties.find({
|
return CreatureProperties.find({
|
||||||
'parent.id': property._id,
|
'parent.id': property._id,
|
||||||
|
|||||||
49
app/imports/api/tabletop/TabletopMaps.js
Normal file
49
app/imports/api/tabletop/TabletopMaps.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import ChildSchema from '/imports/api/parenting/ChildSchema.js';
|
||||||
|
|
||||||
|
let TabletopMaps = new Mongo.Collection('tabletopmaps');
|
||||||
|
|
||||||
|
let TabletopMapschema = new SimpleSchema({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
texture: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: Object,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
'position.x': {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
'position.y': {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
rotation: {
|
||||||
|
type: Number,
|
||||||
|
max: 360,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
// If this map was copied from a library map, this ID will be set
|
||||||
|
libraryMapId: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const schema = new SimpleSchema({});
|
||||||
|
schema.extend(ChildSchema);
|
||||||
|
schema.extend(TabletopMapschema);
|
||||||
|
TabletopMaps.attachSchema(schema);
|
||||||
|
|
||||||
|
export default TabletopMaps;
|
||||||
43
app/imports/api/tabletop/TabletopObjects.js
Normal file
43
app/imports/api/tabletop/TabletopObjects.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import ChildSchema from '/imports/api/parenting/ChildSchema.js';
|
||||||
|
|
||||||
|
let TabletopObjects = new Mongo.Collection('tabletopObjects');
|
||||||
|
|
||||||
|
let TabletopObjectSchema = new SimpleSchema({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
texture: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: Object,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
'position.x': {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
'position.y': {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
rotation: {
|
||||||
|
type: Number,
|
||||||
|
max: 360,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const schema = new SimpleSchema({});
|
||||||
|
schema.extend(ChildSchema);
|
||||||
|
schema.extend(TabletopObjectSchema);
|
||||||
|
TabletopObjects.attachSchema(schema);
|
||||||
|
|
||||||
|
export default TabletopObjects;
|
||||||
1253
app/imports/api/tabletop/three/OrbitControls.js
Normal file
1253
app/imports/api/tabletop/three/OrbitControls.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,8 +12,8 @@
|
|||||||
<v-btn
|
<v-btn
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:class="buttonClass"
|
:class="buttonClass"
|
||||||
v-on="on"
|
v-on="noClick ? {} : on"
|
||||||
@click.stop
|
@click="e => { if (!noClick) e.stopPropagation(); }"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -90,6 +90,7 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
noClick: Boolean,
|
||||||
},
|
},
|
||||||
data(){return {
|
data(){return {
|
||||||
open: false,
|
open: false,
|
||||||
|
|||||||
@@ -30,18 +30,17 @@
|
|||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
key="character-tabs"
|
key="character-tabs"
|
||||||
class="fill-height"
|
class="card-background fill-height"
|
||||||
>
|
>
|
||||||
<v-tabs-items
|
<v-tabs-items
|
||||||
:key=" '' +
|
:key=" '' +
|
||||||
creature.settings.hideSpellsTab +
|
creature.settings.hideSpellsTab +
|
||||||
creature.settings.showTreeTab
|
creature.settings.showTreeTab
|
||||||
"
|
"
|
||||||
:value="$store.getters.tabById($route.params.id)"
|
:value="$store.getters.tabById(creatureId)"
|
||||||
class="card-background"
|
|
||||||
@change="e => $store.commit(
|
@change="e => $store.commit(
|
||||||
'setTabForCharacterSheet',
|
'setTabForCharacterSheet',
|
||||||
{id: $route.params.id, tab: e}
|
{id: creatureId, tab: e}
|
||||||
)"
|
)"
|
||||||
>
|
>
|
||||||
<v-tab-item>
|
<v-tab-item>
|
||||||
@@ -72,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</v-fade-transition>
|
</v-fade-transition>
|
||||||
<character-sheet-fab
|
<character-sheet-fab
|
||||||
v-if="$vuetify.breakpoint.xsOnly"
|
v-if="!embedded && $vuetify.breakpoint.xsOnly"
|
||||||
direction="top"
|
direction="top"
|
||||||
fixed
|
fixed
|
||||||
bottom
|
bottom
|
||||||
@@ -81,15 +80,15 @@
|
|||||||
:edit-permission="editPermission"
|
:edit-permission="editPermission"
|
||||||
/>
|
/>
|
||||||
<v-bottom-navigation
|
<v-bottom-navigation
|
||||||
v-if="$vuetify.breakpoint.xsOnly && creature && creature.settings"
|
v-if="!embedded && $vuetify.breakpoint.xsOnly && creature && creature.settings"
|
||||||
app
|
app
|
||||||
shift
|
shift
|
||||||
mandatory
|
mandatory
|
||||||
class="bottom-nav-btns"
|
class="bottom-nav-btns"
|
||||||
:value="$store.getters.tabById($route.params.id)"
|
:value="$store.getters.tabById(creatureId)"
|
||||||
@change="e => $store.commit(
|
@change="e => $store.commit(
|
||||||
'setTabForCharacterSheet',
|
'setTabForCharacterSheet',
|
||||||
{id: $route.params.id, tab: e}
|
{id: creatureId, tab: e}
|
||||||
)"
|
)"
|
||||||
>
|
>
|
||||||
<v-btn>
|
<v-btn>
|
||||||
@@ -144,6 +143,7 @@ import CreatureLogs from '/imports/api/creature/log/CreatureLogs';
|
|||||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
|
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
|
||||||
import CharacterSheetFab from '/imports/client/ui/creature/character/CharacterSheetFab.vue';
|
import CharacterSheetFab from '/imports/client/ui/creature/character/CharacterSheetFab.vue';
|
||||||
import ActionsTab from '/imports/client/ui/creature/character/characterSheetTabs/ActionsTab.vue';
|
import ActionsTab from '/imports/client/ui/creature/character/characterSheetTabs/ActionsTab.vue';
|
||||||
|
import CharacterSheetInitiative from '/imports/client/ui/creature/character/CharacterSheetInitiative.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -156,13 +156,16 @@ export default {
|
|||||||
BuildTab,
|
BuildTab,
|
||||||
TreeTab,
|
TreeTab,
|
||||||
CharacterSheetFab,
|
CharacterSheetFab,
|
||||||
|
CharacterSheetInitiative,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
creatureId: {
|
creatureId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
embedded: Boolean,
|
||||||
},
|
},
|
||||||
|
// @ts-ignore
|
||||||
reactiveProvide: {
|
reactiveProvide: {
|
||||||
name: 'context',
|
name: 'context',
|
||||||
include: ['creatureId', 'editPermission'],
|
include: ['creatureId', 'editPermission'],
|
||||||
@@ -250,4 +253,9 @@ export default {
|
|||||||
min-height: calc(100vh - 96px);
|
min-height: calc(100vh - 96px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-component .character-sheet .v-window-item {
|
||||||
|
min-height: unset;
|
||||||
|
overflow: unset;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="d-flex character-sheet-initiative"
|
||||||
|
:style="{
|
||||||
|
left: `${$vuetify.application.left}px`,
|
||||||
|
right: `${$vuetify.application.right}px`,
|
||||||
|
bottom: $vuetify.breakpoint.xsOnly ? '60px' : '4px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<v-card
|
||||||
|
v-for="c in [1,2,3,4,5]"
|
||||||
|
:key="c"
|
||||||
|
:width="48"
|
||||||
|
:height="64"
|
||||||
|
class="mx-1"
|
||||||
|
>
|
||||||
|
<v-progress-linear :value="62" />
|
||||||
|
<v-img src="https://picsum.photos/200/300" />
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="js">
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.character-sheet-initiative {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
padding-right: 56px;
|
||||||
|
z-index: 5;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
transition: .2s cubic-bezier(0.4, 0, 0.2, 1) transform,
|
||||||
|
.2s cubic-bezier(0.4, 0, 0.2, 1) background-color,
|
||||||
|
.2s cubic-bezier(0.4, 0, 0.2, 1) left,
|
||||||
|
.2s cubic-bezier(0.4, 0, 0.2, 1) right,
|
||||||
|
280ms cubic-bezier(0.4, 0, 0.2, 1) box-shadow,
|
||||||
|
.25s cubic-bezier(0.4, 0, 0.2, 1) max-width,
|
||||||
|
.25s cubic-bezier(0.4, 0, 0.2, 1) width;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<div class="mini-character-sheet" />
|
<v-card
|
||||||
|
hover
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
|
Character sheet
|
||||||
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import ActionDialog from '/imports/client/ui/creature/actions/ActionDialog.vue';
|
import ActionDialog from '/imports/client/ui/creature/actions/ActionDialog.vue';
|
||||||
import InsertPropertyDialog from '/imports/client/ui/properties/InsertPropertyDialog.vue';
|
import InsertPropertyDialog from '/imports/client/ui/properties/InsertPropertyDialog.vue';
|
||||||
import CharacterCreationDialog from '/imports/client/ui/creature/character/CharacterCreationDialog.vue';
|
import CharacterCreationDialog from '/imports/client/ui/creature/character/CharacterCreationDialog.vue';
|
||||||
|
import CharacterSheetDialog from '/imports/client/ui/tabletop/CharacterSheetDialog.vue';
|
||||||
import CastSpellWithSlotDialog from '/imports/client/ui/properties/components/spells/CastSpellWithSlotDialog.vue';
|
import CastSpellWithSlotDialog from '/imports/client/ui/properties/components/spells/CastSpellWithSlotDialog.vue';
|
||||||
import CreatureFormDialog from '/imports/client/ui/creature/CreatureFormDialog.vue';
|
import CreatureFormDialog from '/imports/client/ui/creature/CreatureFormDialog.vue';
|
||||||
import CreaturePropertyDialog from '/imports/client/ui/creature/creatureProperties/CreaturePropertyDialog.vue';
|
import CreaturePropertyDialog from '/imports/client/ui/creature/creatureProperties/CreaturePropertyDialog.vue';
|
||||||
@@ -39,6 +40,7 @@ export default {
|
|||||||
ArchiveDialog,
|
ArchiveDialog,
|
||||||
CastSpellWithSlotDialog,
|
CastSpellWithSlotDialog,
|
||||||
CharacterCreationDialog,
|
CharacterCreationDialog,
|
||||||
|
CharacterSheetDialog,
|
||||||
CreatureFormDialog,
|
CreatureFormDialog,
|
||||||
CreaturePropertyDialog,
|
CreaturePropertyDialog,
|
||||||
CreaturePropertyFromLibraryDialog,
|
CreaturePropertyFromLibraryDialog,
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
CreatureFolderList
|
CreatureFolderList
|
||||||
},
|
},
|
||||||
|
// @ts-ignore
|
||||||
meteor: {
|
meteor: {
|
||||||
$subscribe: {
|
$subscribe: {
|
||||||
'characterList': [],
|
'characterList': [],
|
||||||
@@ -97,8 +98,8 @@ export default {
|
|||||||
{ title: 'Home', icon: 'mdi-home', to: '/' },
|
{ title: 'Home', icon: 'mdi-home', to: '/' },
|
||||||
{ title: 'Characters', icon: 'mdi-account-group', to: '/character-list', requireLogin: true },
|
{ title: 'Characters', icon: 'mdi-account-group', to: '/character-list', requireLogin: true },
|
||||||
{ title: 'Library', icon: 'mdi-library-shelves', to: '/library', requireLogin: true },
|
{ title: 'Library', icon: 'mdi-library-shelves', to: '/library', requireLogin: true },
|
||||||
//{title: 'Tabletops', icon: 'api', to: '/tabletops', requireLogin: true},
|
{title: 'Tabletops', icon: 'mdi-table-furniture', to: '/tabletops', requireLogin: true},
|
||||||
//{title: 'Friends', icon: 'people', to: '/friends', requireLogin: true},
|
{title: 'Friends', icon: 'mdi-account-multiple', to: '/friends', requireLogin: true},
|
||||||
{ title: 'Files', icon: 'mdi-file-multiple', to: '/my-files' },
|
{ title: 'Files', icon: 'mdi-file-multiple', to: '/my-files' },
|
||||||
{ title: 'Feedback', icon: 'mdi-bug', to: '/feedback' },
|
{ title: 'Feedback', icon: 'mdi-bug', to: '/feedback' },
|
||||||
{ title: 'About', icon: 'mdi-sign-text', to: '/about' },
|
{ title: 'About', icon: 'mdi-sign-text', to: '/about' },
|
||||||
|
|||||||
@@ -25,8 +25,11 @@
|
|||||||
:hint="inputHint"
|
:hint="inputHint"
|
||||||
:error-messages="inputError"
|
:error-messages="inputError"
|
||||||
:disabled="!editPermission"
|
:disabled="!editPermission"
|
||||||
|
:loading="submitLoading"
|
||||||
@click:append-outer="submit"
|
@click:append-outer="submit"
|
||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
|
@keyup.up="decrementHistory"
|
||||||
|
@keyup.down="incrementHistory"
|
||||||
/>
|
/>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,6 +43,7 @@ import { assertEditPermission } from '/imports/api/creature/creatures/creaturePe
|
|||||||
import { parse, prettifyParseError } from '/imports/parser/parser';
|
import { parse, prettifyParseError } from '/imports/parser/parser';
|
||||||
import resolve, { toString } from '/imports/parser/resolve';
|
import resolve, { toString } from '/imports/parser/resolve';
|
||||||
import LogEntry from '/imports/client/ui/log/LogEntry.vue';
|
import LogEntry from '/imports/client/ui/log/LogEntry.vue';
|
||||||
|
import { Tracker } from 'meteor/tracker'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -48,22 +52,70 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
creatureId: {
|
creatureId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
default: undefined,
|
||||||
|
},
|
||||||
|
tabletopId: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data(){return {
|
data(){return {
|
||||||
inputHint: undefined,
|
inputHint: undefined,
|
||||||
inputError: undefined,
|
inputError: undefined,
|
||||||
input: undefined,
|
input: undefined,
|
||||||
|
history: [],
|
||||||
|
historyIndex: 1,
|
||||||
|
submitLoading: false,
|
||||||
}},
|
}},
|
||||||
watch: {
|
watch: {
|
||||||
input(value){
|
input(value){
|
||||||
this.input = value;
|
this.input = value;
|
||||||
|
this.recalculate();
|
||||||
|
},
|
||||||
|
creatureId() {
|
||||||
|
Tracker.afterFlush(() => this.recalculate())
|
||||||
|
},
|
||||||
|
historyIndex(i) {
|
||||||
|
if (typeof this.history[i] === 'string') {
|
||||||
|
this.input = this.history[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submit() {
|
||||||
|
if (!this.input) return;
|
||||||
|
if (this.submitLoading) return;
|
||||||
|
const log = {
|
||||||
|
roll: this.input,
|
||||||
|
};
|
||||||
|
if (this.tabletopId) log.tabletopId = this.tabletopId;
|
||||||
|
if (this.creatureId) log.creatureId = this.creatureId;
|
||||||
|
this.submitLoading = true;
|
||||||
|
logRoll.call(log, (error) => {
|
||||||
|
this.submitLoading = false;
|
||||||
|
if (!error) {
|
||||||
|
this.addHistory(this.input);
|
||||||
|
this.input = '';
|
||||||
|
this.inputError = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.inputError = error.message || error.toString();
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addHistory(string) {
|
||||||
|
// Don't add duplicates back to back in history
|
||||||
|
if (string === this.history[this.history.length - 1]) return;
|
||||||
|
this.history.push(string);
|
||||||
|
if (this.history.length > 50) this.history.shift();
|
||||||
|
this.historyIndex = this.history.length;
|
||||||
|
},
|
||||||
|
recalculate() {
|
||||||
this.inputHint = this.inputError = undefined;
|
this.inputHint = this.inputError = undefined;
|
||||||
if (!this.input) return;
|
if (!this.input) return;
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
result = parse(value);
|
result = parse(this.input);
|
||||||
} catch (e){
|
} catch (e){
|
||||||
if (e.constructor.name === 'EndOfInputError'){
|
if (e.constructor.name === 'EndOfInputError'){
|
||||||
this.inputError = '...';
|
this.inputError = '...';
|
||||||
@@ -83,26 +135,29 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
incrementHistory() {
|
||||||
methods: {
|
if (this.historyIndex < this.history.length) {
|
||||||
submit(){
|
this.historyIndex += 1;
|
||||||
if (this.inputError || !this.input) return;
|
}
|
||||||
logRoll.call({
|
|
||||||
roll: this.input,
|
|
||||||
creatureId: this.creatureId,
|
|
||||||
}, (error) => {
|
|
||||||
if (error) console.error(error);
|
|
||||||
});
|
|
||||||
this.input = '';
|
|
||||||
},
|
},
|
||||||
|
decrementHistory() {
|
||||||
|
if (this.historyIndex > 0) {
|
||||||
|
this.historyIndex -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
// @ts-ignore
|
||||||
meteor: {
|
meteor: {
|
||||||
logs(){
|
logs() {
|
||||||
return CreatureLogs.find({
|
const filter = {};
|
||||||
creatureId: this.creatureId,
|
if (this.tabletopId) {
|
||||||
}, {
|
filter.tabletopId = this.tabletopId;
|
||||||
|
} else if (this.creatureId) {
|
||||||
|
filter.creatureId = this.creatureId;
|
||||||
|
}
|
||||||
|
return CreatureLogs.find(filter, {
|
||||||
sort: {date: -1},
|
sort: {date: -1},
|
||||||
limit: 20
|
limit: 100
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
creature(){
|
creature(){
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
<v-card
|
<v-card
|
||||||
class="ma-2 log-entry"
|
class="ma-2 log-entry"
|
||||||
>
|
>
|
||||||
|
<v-card-title v-if="showName && model.creatureName">
|
||||||
|
{{ model.creatureName }}
|
||||||
|
</v-card-title>
|
||||||
<v-card-text
|
<v-card-text
|
||||||
v-if="model.text || (model.content && model.content.length)"
|
v-if="model.text || (model.content && model.content.length)"
|
||||||
class="pa-2"
|
class="pa-2"
|
||||||
@@ -23,6 +26,7 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
showName: Boolean,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
30
app/imports/client/ui/properties/components/PropertyCard.vue
Normal file
30
app/imports/client/ui/properties/components/PropertyCard.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template
|
||||||
|
lang="html"
|
||||||
|
functional
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="model.type"
|
||||||
|
v-if="model && components[model.type]"
|
||||||
|
/>
|
||||||
|
<v-card v-else-if="model">
|
||||||
|
<v-card-title class="text--error">
|
||||||
|
A property card for the {{ model.type }} isn't defined. You should report this error.
|
||||||
|
</v-card-title>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="js">
|
||||||
|
import ActionCard from '/imports/client/ui/properties/components/actions/ActionCard.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
action: ActionCard,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
model: {
|
||||||
|
type: Object,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
class="mr-2"
|
class="mr-2"
|
||||||
:color="model.color || 'primary'"
|
:color="model.color || 'primary'"
|
||||||
:loading="doActionLoading"
|
:loading="doActionLoading"
|
||||||
:disabled="model.insufficientResources || !context.editPermission"
|
:disabled="model.insufficientResources || !context.editPermission || !!targetingError"
|
||||||
:roll-text="rollBonus"
|
:roll-text="rollBonus"
|
||||||
:name="model.name"
|
:name="model.name"
|
||||||
:advantage="model.attackRoll && model.attackRoll.advantage"
|
:advantage="model.attackRoll && model.attackRoll.advantage"
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
class="mr-2"
|
class="mr-2"
|
||||||
:color="model.color || 'primary'"
|
:color="model.color || 'primary'"
|
||||||
:loading="doActionLoading"
|
:loading="doActionLoading"
|
||||||
:disabled="model.insufficientResources || !context.editPermission"
|
:disabled="model.insufficientResources || !context.editPermission || !!targetingError"
|
||||||
@click.stop="doAction"
|
@click.stop="doAction"
|
||||||
>
|
>
|
||||||
<property-icon :model="model" />
|
<property-icon :model="model" />
|
||||||
@@ -53,12 +53,20 @@
|
|||||||
{{ model.name || propertyName }}
|
{{ model.name || propertyName }}
|
||||||
</div>
|
</div>
|
||||||
<div class="action-sub-title layout align-center">
|
<div class="action-sub-title layout align-center">
|
||||||
<div class="flex">
|
<div
|
||||||
{{ model.actionType }}
|
v-if="targetingError"
|
||||||
</div>
|
class="flex error--text"
|
||||||
<div v-if="Number.isFinite(model.usesLeft)">
|
>
|
||||||
{{ model.usesLeft }} uses
|
{{ targetingError }}
|
||||||
</div>
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex">
|
||||||
|
{{ model.actionType }}
|
||||||
|
</div>
|
||||||
|
<div v-if="Number.isFinite(model.usesLeft)">
|
||||||
|
{{ model.usesLeft }} uses
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,6 +157,10 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
targets: {
|
||||||
|
type: Array,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -185,6 +197,16 @@ export default {
|
|||||||
actionTypeIcon() {
|
actionTypeIcon() {
|
||||||
return `$vuetify.icons.${this.model.actionType}`;
|
return `$vuetify.icons.${this.model.actionType}`;
|
||||||
},
|
},
|
||||||
|
targetingError(){
|
||||||
|
// Can always do an action without a target
|
||||||
|
if (!this.targets || !this.targets.length) return undefined;
|
||||||
|
if (this.targets.length > 1 && this.model.target !== 'multipleTargets'){
|
||||||
|
return 'Single target';
|
||||||
|
} else if (this.model.target === 'self' && this.targets[0] !== this.model.ancestors[0]._id){
|
||||||
|
return 'Can only target self';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
meteor: {
|
meteor: {
|
||||||
children() {
|
children() {
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
:class="insufficient && 'error--text'"
|
:class="insufficient && 'error--text'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
v-if="model.quantity && model.quantity.value !== 1"
|
||||||
class="mr-2 text-no-wrap text-truncate"
|
class="mr-2 text-no-wrap text-truncate"
|
||||||
style="min-width: 24px; text-align: center;"
|
style="min-width: 24px; text-align: center;"
|
||||||
>
|
>
|
||||||
{{ model.quantity && model.quantity.value }}
|
{{ model.quantity.value }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="model.quantity && (typeof model.quantity.value !== 'string')"
|
v-if="model.quantity && (typeof model.quantity.value !== 'string')"
|
||||||
@@ -15,6 +16,12 @@
|
|||||||
>
|
>
|
||||||
{{ model.statName || model.variableName }}
|
{{ model.statName || model.variableName }}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="(typeof model.available) == 'number'"
|
||||||
|
class="text--disabled text-no-wrap text-truncate ml-1 flex-shrink-0"
|
||||||
|
>
|
||||||
|
({{ model.available }})
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -27,28 +27,30 @@
|
|||||||
:color="model.itemColor"
|
:color="model.itemColor"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
v-if="quantity !== 1"
|
||||||
class="mr-2 text-no-wrap"
|
class="mr-2 text-no-wrap"
|
||||||
style="min-width: 24px; text-align: center;"
|
style="min-width: 24px; text-align: center;"
|
||||||
>
|
>
|
||||||
<template v-if="quantity !== 1">
|
{{ quantity }}
|
||||||
{{ model.available }} / {{ quantity }}
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ model.available }}
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<template v-if="model.itemId">
|
||||||
class="text-no-wrap text-truncate flex"
|
<div
|
||||||
>
|
class="text-no-wrap text-truncate"
|
||||||
<template v-if="model.itemId">
|
|
||||||
{{ model.itemName }}
|
|
||||||
</template>
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
class="error--text"
|
|
||||||
>
|
>
|
||||||
Select item
|
{{ model.itemName }}
|
||||||
</span>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="(typeof model.available) == 'number'"
|
||||||
|
class="text--disabled text-no-wrap text-truncate ml-1 flex-shrink-0"
|
||||||
|
>
|
||||||
|
({{ model.available }})
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="error--text text-no-wrap text-truncate flex"
|
||||||
|
>
|
||||||
|
Select item
|
||||||
</div>
|
</div>
|
||||||
<v-icon
|
<v-icon
|
||||||
v-if="context.editPermission"
|
v-if="context.editPermission"
|
||||||
|
|||||||
@@ -19,34 +19,18 @@
|
|||||||
<v-flex
|
<v-flex
|
||||||
style="height: 24px; flex-basis: 300px; flex-grow: 100;"
|
style="height: 24px; flex-basis: 300px; flex-grow: 100;"
|
||||||
>
|
>
|
||||||
<div
|
<health-bar-progress
|
||||||
column
|
:model="model"
|
||||||
align-center
|
|
||||||
style="cursor: pointer;"
|
style="cursor: pointer;"
|
||||||
class="bar"
|
|
||||||
@click="edit"
|
@click="edit"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style="height: 24px; width: 100%; position: relative; transition: background-color 0.5s ease;"
|
class="value"
|
||||||
:style="{
|
:class="{
|
||||||
backgroundColor: barBackgroundColor
|
'white--text': isTextLight,
|
||||||
|
'black--text': !isTextLight,
|
||||||
}"
|
}"
|
||||||
>
|
style="font-size: 15px;
|
||||||
<div
|
|
||||||
class="filler"
|
|
||||||
style="height: 100%; transform-origin: left; transition: all 0.5s ease;"
|
|
||||||
:style="{
|
|
||||||
backgroundColor: barColor,
|
|
||||||
transform: `scaleX(${fillFraction})`,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="value"
|
|
||||||
:class="{
|
|
||||||
'white--text': isTextLight,
|
|
||||||
'black--text': !isTextLight,
|
|
||||||
}"
|
|
||||||
style="font-size: 15px;
|
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -55,11 +39,10 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
text-align: center;"
|
text-align: center;"
|
||||||
>
|
>
|
||||||
{{ model.value }} / {{ model.total }}
|
{{ model.value }} / {{ model.total }}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</health-bar-progress>
|
||||||
<v-menu
|
<v-menu
|
||||||
v-model="editing"
|
v-model="editing"
|
||||||
absolute
|
absolute
|
||||||
@@ -85,11 +68,13 @@
|
|||||||
<script lang="js">
|
<script lang="js">
|
||||||
import IncrementMenu from '/imports/client/ui/components/IncrementMenu.vue';
|
import IncrementMenu from '/imports/client/ui/components/IncrementMenu.vue';
|
||||||
import isDarkColor from '/imports/client/ui/utility/isDarkColor';
|
import isDarkColor from '/imports/client/ui/utility/isDarkColor';
|
||||||
|
import HealthBarProgress from '/imports/client/ui/properties/components/attributes/HealthBarProgress.vue';
|
||||||
import chroma from 'chroma-js';
|
import chroma from 'chroma-js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
IncrementMenu
|
IncrementMenu,
|
||||||
|
HealthBarProgress,
|
||||||
},
|
},
|
||||||
inject: {
|
inject: {
|
||||||
theme: {
|
theme: {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<template lang="html">
|
||||||
|
<div
|
||||||
|
class="bar"
|
||||||
|
@click="e => $emit('click', e)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="width: 100%; position: relative; transition: background-color 0.5s ease;"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: barBackgroundColor,
|
||||||
|
height: `${height}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="filler"
|
||||||
|
style="height: 100%; transform-origin: left; transition: all 0.5s ease;"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: barColor,
|
||||||
|
transform: `scaleX(${fillFraction})`,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="js">
|
||||||
|
import chroma from 'chroma-js';
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
model: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 24,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fillFraction() {
|
||||||
|
let fraction = this.model.value / this.model.total;
|
||||||
|
if (fraction < 0) fraction = 0;
|
||||||
|
if (fraction > 1) fraction = 1;
|
||||||
|
return fraction;
|
||||||
|
},
|
||||||
|
color() {
|
||||||
|
return this.model.color || this.$vuetify.theme.currentTheme.primary
|
||||||
|
},
|
||||||
|
barColor() {
|
||||||
|
const fraction = this.model.value / this.model.total;
|
||||||
|
if (!Number.isFinite(fraction)) return this.color;
|
||||||
|
if (fraction > 0.5) {
|
||||||
|
return this.color;
|
||||||
|
} else if (this.model.healthBarColorMid && this.model.healthBarColorLow) {
|
||||||
|
return chroma.mix(this.model.healthBarColorLow, this.model.healthBarColorMid, fraction * 2).hex();
|
||||||
|
} else if (this.model.healthBarColorMid) {
|
||||||
|
return this.model.healthBarColorMid;
|
||||||
|
}
|
||||||
|
return this.color;
|
||||||
|
},
|
||||||
|
barBackgroundColor() {
|
||||||
|
return chroma(this.barColor)
|
||||||
|
.darken(1.5)
|
||||||
|
.desaturate(1.5)
|
||||||
|
.hex();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -202,9 +202,7 @@ RouterFactory.configure(router => {
|
|||||||
meta: {
|
meta: {
|
||||||
title: 'Print Character Sheet',
|
title: 'Print Character Sheet',
|
||||||
},
|
},
|
||||||
},
|
}, {
|
||||||
/* Not ready for prime time <3
|
|
||||||
{
|
|
||||||
path: '/tabletops',
|
path: '/tabletops',
|
||||||
name: 'tabletops',
|
name: 'tabletops',
|
||||||
component: Tabletops,
|
component: Tabletops,
|
||||||
@@ -218,9 +216,7 @@ RouterFactory.configure(router => {
|
|||||||
rightDrawer: TabletopRightDrawer,
|
rightDrawer: TabletopRightDrawer,
|
||||||
},
|
},
|
||||||
beforeEnter: ensureLoggedIn,
|
beforeEnter: ensureLoggedIn,
|
||||||
},
|
}, {
|
||||||
*/
|
|
||||||
{
|
|
||||||
path: '/friends',
|
path: '/friends',
|
||||||
components: {
|
components: {
|
||||||
default: NotImplemented,
|
default: NotImplemented,
|
||||||
|
|||||||
@@ -7,14 +7,22 @@
|
|||||||
background: #151515;
|
background: #151515;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-background .v-tabs-items.theme--dark {
|
||||||
|
background: #151515;
|
||||||
|
}
|
||||||
|
|
||||||
.theme--light .card-background {
|
.theme--light .card-background {
|
||||||
background: #f6f6f6;
|
background: #f6f6f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-background .v-tabs-items.theme--light {
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
.theme--dark .card-raised-background {
|
.theme--dark .card-raised-background {
|
||||||
background: #1d1d1d;
|
background: #1d1d1d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme--light .card-raised-background {
|
.theme--light .card-raised-background {
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
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">
|
<template lang="html">
|
||||||
<v-container
|
<div
|
||||||
class="tabletop"
|
class="tabletop layout column"
|
||||||
fluid
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<v-row
|
<tabletop-map
|
||||||
dense
|
class="play-area"
|
||||||
class="initiative-row"
|
style="
|
||||||
style="flex-wrap: nowrap; overflow-x: auto;"
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<v-container
|
||||||
|
fluid
|
||||||
>
|
>
|
||||||
<tabletop-creature-card
|
<v-row
|
||||||
v-for="creature in creatures"
|
dense
|
||||||
:key="creature._id"
|
class="initiative-row flex-grow-0"
|
||||||
:model="creature"
|
style="flex-wrap: nowrap; overflow-x: auto; padding-bottom: 50px;"
|
||||||
/>
|
@wheel="transformScroll($event)"
|
||||||
<v-card
|
|
||||||
class="layout column justify-center align-center"
|
|
||||||
style="height: 150px; min-width: 120px;"
|
|
||||||
data-id="select-creatures"
|
|
||||||
hover
|
|
||||||
@click="addCreature"
|
|
||||||
>
|
>
|
||||||
<div class="flex layout justify-center align-center">
|
<tabletop-creature-card
|
||||||
<v-icon>mdi-plus</v-icon>
|
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>
|
</div>
|
||||||
<v-card-title>
|
</v-row>
|
||||||
Add<br>creature
|
</v-container>
|
||||||
</v-card-title>
|
<v-footer
|
||||||
</v-card>
|
inset
|
||||||
</v-row>
|
class="pa-0"
|
||||||
<tabletop-map class="play-area" />
|
style="
|
||||||
<section class="action-row">
|
background: none;
|
||||||
<mini-character-sheet />
|
box-shadow: none;
|
||||||
<tabletop-action-cards />
|
position: absolute;
|
||||||
</section>
|
left: 0;
|
||||||
</v-container>
|
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>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import addCreaturesToTabletop from '/imports/api/tabletop/methods/addCreaturesToTabletop';
|
import addCreaturesToTabletop from '/imports/api/tabletop/methods/addCreaturesToTabletop';
|
||||||
import TabletopCreatureCard from '/imports/client/ui/tabletop/TabletopCreatureCard.vue';
|
import TabletopCreatureCard from '/imports/client/ui/tabletop/TabletopCreatureCard.vue';
|
||||||
import TabletopMap from '/imports/client/ui/tabletop/TabletopMap.vue';
|
import TabletopMap from '/imports/client/ui/tabletop/TabletopMap.vue';
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
import TabletopActionCards from '/imports/client/ui/tabletop/TabletopActionCards.vue';
|
|
||||||
import MiniCharacterSheet from '/imports/client/ui/creature/character/MiniCharacterSheet.vue';
|
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 {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
TabletopCreatureCard,
|
TabletopCreatureCard,
|
||||||
TabletopMap,
|
TabletopMap,
|
||||||
TabletopActionCards,
|
ActionCard,
|
||||||
MiniCharacterSheet,
|
MiniCharacterSheet,
|
||||||
|
SelectedCreatureBar,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
model: {
|
model: {
|
||||||
@@ -58,9 +136,20 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
reactiveProvide: {
|
||||||
|
name: 'context',
|
||||||
|
include: ['editPermission'],
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeCreature: undefined,
|
activeCreatureId: undefined,
|
||||||
|
activeActionId: undefined,
|
||||||
|
targets: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
activeCreatureId(id) {
|
||||||
|
this.$root.$emit('active-tabletop-character-change', id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
meteor: {
|
meteor: {
|
||||||
@@ -69,9 +158,29 @@ export default {
|
|||||||
return [this.model._id];
|
return [this.model._id];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
creatures() {
|
creatures(){
|
||||||
return Creatures.find({ tabletop: this.model._id });
|
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: {
|
methods: {
|
||||||
addCreature() {
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
.initiative-row>.v-card {
|
.initiative-row>.v-card {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
height: 162px;
|
height: 162px;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
.action-row > div {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 120px;
|
||||||
|
width: 200px;
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,37 +1,146 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-card
|
<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
|
<v-img
|
||||||
:src="model.picture"
|
:src="model.picture || '/images/ui/missing-portrait.png'"
|
||||||
aspect-ratio="1"
|
: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"
|
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>
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<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 {
|
export default {
|
||||||
|
components: {
|
||||||
|
CardHighlight,
|
||||||
|
HealthBarProgress,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
model: {
|
model: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
.small-title {
|
.small-title {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
padding: 4px;
|
padding: 4px 4px 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,52 +1,38 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<div class="tabletop-log">
|
<character-log
|
||||||
<div class="messages layout column justify-end align-end">
|
:tabletop-id="tabletopId"
|
||||||
<div
|
:creature-id="activeCreatureId"
|
||||||
v-for="message in messages"
|
/>
|
||||||
:key="message._id"
|
|
||||||
class="message"
|
|
||||||
>
|
|
||||||
{{ message.content }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<v-textarea
|
|
||||||
v-model="messageContent"
|
|
||||||
@keyup.enter.prevent="sendMessage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<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 {
|
export default {
|
||||||
|
components: {
|
||||||
|
CharacterLog,
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
context: {
|
||||||
|
default: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
tabletopId: {
|
tabletopId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
default: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data(){ return {
|
data() {
|
||||||
messageContent: '',
|
return {
|
||||||
}},
|
activeCreatureId: undefined,
|
||||||
meteor: {
|
|
||||||
messages() {
|
|
||||||
return Messages.find({
|
|
||||||
tabletopId: this.tabletopId,
|
|
||||||
}, {
|
|
||||||
sort: {
|
|
||||||
timeStamp: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
mounted() {
|
||||||
sendMessage(){
|
this.$root.$on('active-tabletop-character-change', (id) => {
|
||||||
sendMessage.call({
|
this.activeCreatureId = id;
|
||||||
content: this.messageContent,
|
});
|
||||||
tabletopId: this.tabletopId,
|
|
||||||
});
|
|
||||||
this.messageContent = '';
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,11 +1,134 @@
|
|||||||
<template lang="html">
|
<template>
|
||||||
<div class="tabletop-map" />
|
<div>
|
||||||
|
<canvas
|
||||||
|
ref="map"
|
||||||
|
class="tabletop-map"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<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 {
|
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>
|
</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>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
//import Vuetify from 'vuetify/lib';
|
//import Vuetify from 'vuetify/lib';
|
||||||
import Vuetify from 'vuetify/lib/framework';
|
import Vuetify from 'vuetify/lib/framework';
|
||||||
import { Scroll, Ripple } from 'vuetify/lib/directives';
|
import { Scroll, Ripple, ClickOutside } from 'vuetify/lib/directives';
|
||||||
import SVG_ICONS from '/imports/constants/SVG_ICONS';
|
import SVG_ICONS from '/imports/constants/SVG_ICONS';
|
||||||
import SvgIconByName from '/imports/client/ui/icons/SvgIconByName.vue';
|
import SvgIconByName from '/imports/client/ui/icons/SvgIconByName.vue';
|
||||||
import themes from '/imports/client/ui/themes';
|
import themes from '/imports/client/ui/themes';
|
||||||
@@ -11,6 +11,7 @@ Vue.use(Vuetify, {
|
|||||||
directives: {
|
directives: {
|
||||||
Scroll,
|
Scroll,
|
||||||
Ripple,
|
Ripple,
|
||||||
|
ClickOutside,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const store = new Vuex.Store({
|
|||||||
setTabForCharacterSheet(state, { tab, id }) {
|
setTabForCharacterSheet(state, { tab, id }) {
|
||||||
// Convert tab names to tab numbers
|
// Convert tab names to tab numbers
|
||||||
if (typeof tab === 'string') {
|
if (typeof tab === 'string') {
|
||||||
|
const tabInput = tab;
|
||||||
const creature = Creatures.findOne(id);
|
const creature = Creatures.findOne(id);
|
||||||
if (creature?.settings?.hideSpellsTab) {
|
if (creature?.settings?.hideSpellsTab) {
|
||||||
tab = tabsWithoutSpells.indexOf(tab);
|
tab = tabsWithoutSpells.indexOf(tab);
|
||||||
@@ -63,9 +64,9 @@ const store = new Vuex.Store({
|
|||||||
tab = tabs.indexOf(tab);
|
tab = tabs.indexOf(tab);
|
||||||
}
|
}
|
||||||
if (!(tab > -1)) {
|
if (!(tab > -1)) {
|
||||||
throw 'Could not find requested tab';
|
console.warn(`could not find a tab called ${tabInput}`);
|
||||||
|
tab = 0;
|
||||||
}
|
}
|
||||||
console.log('resolved: ', tab);
|
|
||||||
}
|
}
|
||||||
Vue.set(state.characterSheetTabs, id, tab);
|
Vue.set(state.characterSheetTabs, id, tab);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import Tabletops from '/imports/api/tabletop/Tabletops';
|
import Tabletops from '/imports/api/tabletop/Tabletops';
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||||
import Messages from '/imports/api/tabletop/Messages';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||||
|
import CreatureLogs from '/imports/api/creature/log/CreatureLogs';
|
||||||
|
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
||||||
|
import { loadCreature } from '/imports/api/engine/loadCreatures';
|
||||||
|
|
||||||
Meteor.publish('tabletops', function () {
|
Meteor.publish('tabletops', function () {
|
||||||
var userId = this.userId;
|
var userId = this.userId;
|
||||||
@@ -21,6 +24,7 @@ Meteor.publish('tabletop', function (tabletopId) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
this.autorun(function () {
|
this.autorun(function () {
|
||||||
|
const self = this;
|
||||||
let tabletopCursor = Tabletops.find({
|
let tabletopCursor = Tabletops.find({
|
||||||
_id: tabletopId,
|
_id: tabletopId,
|
||||||
$or: [
|
$or: [
|
||||||
@@ -32,6 +36,7 @@ Meteor.publish('tabletop', function (tabletopId) {
|
|||||||
if (!tabletop) {
|
if (!tabletop) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warning, this leaks data to users of the same tabletop who may not have
|
// Warning, this leaks data to users of the same tabletop who may not have
|
||||||
// read permission of this specific creature, so publish as few fields as
|
// read permission of this specific creature, so publish as few fields as
|
||||||
// possible
|
// possible
|
||||||
@@ -39,22 +44,32 @@ Meteor.publish('tabletop', function (tabletopId) {
|
|||||||
tabletop: tabletopId,
|
tabletop: tabletopId,
|
||||||
}, {
|
}, {
|
||||||
fields: {
|
fields: {
|
||||||
|
_id: 1,
|
||||||
name: 1,
|
name: 1,
|
||||||
picture: 1,
|
picture: 1,
|
||||||
avatarPicture: 1,
|
avatarPicture: 1,
|
||||||
variables: 1,
|
|
||||||
tabletop: 1,
|
tabletop: 1,
|
||||||
initiativeRoll: 1,
|
initiativeRoll: 1,
|
||||||
|
settings: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
let recentMessages = Messages.find({
|
const creatureIds = creatureSummaries.map(c => c._id);
|
||||||
|
creatureIds.forEach(creatureId => {
|
||||||
|
loadCreature(creatureId, self);
|
||||||
|
});
|
||||||
|
const variables = CreatureVariables.find({
|
||||||
|
_creatureId: { $in: creatureIds }
|
||||||
|
});
|
||||||
|
let properties = CreatureProperties.find({
|
||||||
|
'ancestors.id': { $in: creatureIds },
|
||||||
|
removed: { $ne: true },
|
||||||
|
});
|
||||||
|
const logs = CreatureLogs.find({
|
||||||
tabletopId,
|
tabletopId,
|
||||||
}, {
|
}, {
|
||||||
sort: {
|
|
||||||
timeStamp: -1,
|
|
||||||
},
|
|
||||||
limit: 100,
|
limit: 100,
|
||||||
|
sort: { date: -1 },
|
||||||
});
|
});
|
||||||
return [tabletopCursor, creatureSummaries, recentMessages]
|
return [tabletopCursor, creatureSummaries, properties, logs, variables]
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
13
app/package-lock.json
generated
13
app/package-lock.json
generated
@@ -3463,7 +3463,7 @@
|
|||||||
"lodash.omit": {
|
"lodash.omit": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
|
||||||
"integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg=="
|
"integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA="
|
||||||
},
|
},
|
||||||
"lodash.template": {
|
"lodash.template": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
@@ -5367,6 +5367,11 @@
|
|||||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"three": {
|
||||||
|
"version": "0.156.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.156.1.tgz",
|
||||||
|
"integrity": "sha512-kP7H0FK9d/k6t/XvQ9FO6i+QrePoDcNhwl0I02+wmUJRNSLCUIDMcfObnzQvxb37/0Uc9TDT0T1HgsRRrO6SYQ=="
|
||||||
|
},
|
||||||
"to-regex-range": {
|
"to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
@@ -5540,9 +5545,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vue": {
|
"vue": {
|
||||||
"version": "2.6.14",
|
"version": "2.6.10",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
|
||||||
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
|
"integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
|
||||||
},
|
},
|
||||||
"vue-eslint-parser": {
|
"vue-eslint-parser": {
|
||||||
"version": "7.11.0",
|
"version": "7.11.0",
|
||||||
|
|||||||
@@ -53,8 +53,9 @@
|
|||||||
"simpl-schema": "^1.13.1",
|
"simpl-schema": "^1.13.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"speakingurl": "^14.0.1",
|
"speakingurl": "^14.0.1",
|
||||||
|
"three": "^0.156.1",
|
||||||
"vivagraphjs": "^0.12.0",
|
"vivagraphjs": "^0.12.0",
|
||||||
"vue": "2.6.14",
|
"vue": "2.6.10",
|
||||||
"vue-meteor-tracker": "^2.0.0",
|
"vue-meteor-tracker": "^2.0.0",
|
||||||
"vue-reactive-provide": "^0.3.0",
|
"vue-reactive-provide": "^0.3.0",
|
||||||
"vue-router": "^3.6.5",
|
"vue-router": "^3.6.5",
|
||||||
|
|||||||
BIN
app/public/images/battlemap.webp
Normal file
BIN
app/public/images/battlemap.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 217 KiB |
BIN
app/public/images/ui/missing-portrait.png
Normal file
BIN
app/public/images/ui/missing-portrait.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
app/public/models/example-mini.stl
Normal file
BIN
app/public/models/example-mini.stl
Normal file
Binary file not shown.
Reference in New Issue
Block a user