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({
|
||||
// Strings
|
||||
name: {
|
||||
@@ -177,6 +205,10 @@ let CreatureSchema = new SimpleSchema({
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
},
|
||||
tabletopSettings: {
|
||||
type: CreatureTabletopSettingsSchema,
|
||||
optional: true,
|
||||
},
|
||||
|
||||
// Settings
|
||||
settings: {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { assertEditPermission } from '/imports/api/creature/creatures/creaturePe
|
||||
import { parse, prettifyParseError } from '/imports/parser/parser';
|
||||
import resolve, { toString } from '/imports/parser/resolve';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
|
||||
import { assertUserInTabletop } from '/imports/api/tabletop/methods/shared/tabletopPermissions.js';
|
||||
|
||||
const PER_CREATURE_LOG_LIMIT = 100;
|
||||
|
||||
@@ -42,6 +43,12 @@ let CreatureLogSchema = new SimpleSchema({
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
tabletopId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
optional: true,
|
||||
},
|
||||
creatureName: {
|
||||
type: String,
|
||||
optional: true,
|
||||
@@ -119,6 +126,7 @@ const insertCreatureLog = new ValidatedMethod({
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
tabletop: 1,
|
||||
}
|
||||
});
|
||||
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
|
||||
if (typeof log === 'string') {
|
||||
log = { content: [{ value: log }] };
|
||||
@@ -142,13 +150,20 @@ export function insertCreatureLogWork({ log, creature, method }) {
|
||||
}
|
||||
});
|
||||
log.date = new Date();
|
||||
|
||||
if (tabletopId) log.tabletopId = tabletopId;
|
||||
if (creature && creature.tabletop) log.tabletopId = creature.tabletop;
|
||||
// Insert it
|
||||
let id = CreatureLogs.insert(log);
|
||||
if (Meteor.isServer) {
|
||||
method?.unblock();
|
||||
removeOldLogs(creature._id);
|
||||
logWebhook({ log, creature });
|
||||
if (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;
|
||||
}
|
||||
@@ -170,24 +185,39 @@ const logRoll = new ValidatedMethod({
|
||||
roll: {
|
||||
type: String,
|
||||
},
|
||||
tabletopId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
},
|
||||
}).validator(),
|
||||
run({ roll, creatureId }) {
|
||||
const creature = Creatures.findOne(creatureId, {
|
||||
fields: {
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
}
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
const variables = CreatureVariables.findOne({ _creatureId: creatureId });
|
||||
run({ roll, tabletopId, creatureId }) {
|
||||
if (!creatureId && !tabletopId) throw new Meteor.Error('no-id',
|
||||
'A creature id or tabletop id must be given'
|
||||
);
|
||||
let creature;
|
||||
if (creatureId) {
|
||||
creature = Creatures.findOne(creatureId, {
|
||||
fields: {
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
}
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
}
|
||||
if (tabletopId) {
|
||||
assertUserInTabletop(tabletopId, this.userId);
|
||||
}
|
||||
const variables = CreatureVariables.findOne({ _creatureId: creatureId }) || {};
|
||||
let logContent = []
|
||||
let parsedResult = undefined;
|
||||
try {
|
||||
@@ -228,7 +258,7 @@ const logRoll = new ValidatedMethod({
|
||||
date: new Date(),
|
||||
};
|
||||
|
||||
let id = insertCreatureLogWork({ log, creature, method: this });
|
||||
let id = insertCreatureLogWork({ log, creature, tabletopId, method: this });
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
@@ -16,8 +16,7 @@ export default function computeAction(computation, node) {
|
||||
}
|
||||
});
|
||||
prop.resources.itemsConsumed?.forEach(itemConsumed => {
|
||||
if (!itemConsumed.itemId) return;
|
||||
if (itemConsumed.available < itemConsumed.quantity?.value) {
|
||||
if (!itemConsumed.itemId || itemConsumed.available < itemConsumed.quantity?.value) {
|
||||
prop.insufficientResources = true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,15 +12,17 @@ export const loadedCreatures: Map<string, LoadedCreature> = new Map(); // creatu
|
||||
export function loadCreature(creatureId: string, subscription: Tracker.Computation) {
|
||||
if (!creatureId) throw 'creatureId is required';
|
||||
let creature = loadedCreatures.get(creatureId);
|
||||
if (!creature || !creature.subs.has(subscription)) {
|
||||
subscription.onStop(() => {
|
||||
unloadCreature(creatureId, subscription);
|
||||
});
|
||||
}
|
||||
if (creature) {
|
||||
creature.subs.add(subscription);
|
||||
} else {
|
||||
creature = new LoadedCreature(subscription, creatureId);
|
||||
loadedCreatures.set(creatureId, creature);
|
||||
}
|
||||
subscription.onStop(() => {
|
||||
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
|
||||
if (loadedCreatures.has(creatureId)) {
|
||||
const creature = loadedCreatures.get(creatureId);
|
||||
const props = [];
|
||||
if (!creature) return [];
|
||||
const props: CreatureProperty[] = [];
|
||||
for (const prop of creature.properties.values()) {
|
||||
if (prop.parent?.id === property._id) {
|
||||
if (prop.parentId === property._id) {
|
||||
props.push(prop);
|
||||
}
|
||||
}
|
||||
const cloneProps = EJSON.clone(props);
|
||||
return cloneProps.sort((a, b) => a.order - b.order);
|
||||
return cloneProps.sort((a, b) => a.left - b.left);
|
||||
} else {
|
||||
return CreatureProperties.find({
|
||||
'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-bind="$attrs"
|
||||
:class="buttonClass"
|
||||
v-on="on"
|
||||
@click.stop
|
||||
v-on="noClick ? {} : on"
|
||||
@click="e => { if (!noClick) e.stopPropagation(); }"
|
||||
>
|
||||
<slot />
|
||||
</v-btn>
|
||||
@@ -90,6 +90,7 @@ export default {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
noClick: Boolean,
|
||||
},
|
||||
data(){return {
|
||||
open: false,
|
||||
|
||||
@@ -30,18 +30,17 @@
|
||||
<div
|
||||
v-else
|
||||
key="character-tabs"
|
||||
class="fill-height"
|
||||
class="card-background fill-height"
|
||||
>
|
||||
<v-tabs-items
|
||||
:key=" '' +
|
||||
creature.settings.hideSpellsTab +
|
||||
creature.settings.showTreeTab
|
||||
"
|
||||
:value="$store.getters.tabById($route.params.id)"
|
||||
class="card-background"
|
||||
:value="$store.getters.tabById(creatureId)"
|
||||
@change="e => $store.commit(
|
||||
'setTabForCharacterSheet',
|
||||
{id: $route.params.id, tab: e}
|
||||
{id: creatureId, tab: e}
|
||||
)"
|
||||
>
|
||||
<v-tab-item>
|
||||
@@ -72,7 +71,7 @@
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
<character-sheet-fab
|
||||
v-if="$vuetify.breakpoint.xsOnly"
|
||||
v-if="!embedded && $vuetify.breakpoint.xsOnly"
|
||||
direction="top"
|
||||
fixed
|
||||
bottom
|
||||
@@ -81,15 +80,15 @@
|
||||
:edit-permission="editPermission"
|
||||
/>
|
||||
<v-bottom-navigation
|
||||
v-if="$vuetify.breakpoint.xsOnly && creature && creature.settings"
|
||||
v-if="!embedded && $vuetify.breakpoint.xsOnly && creature && creature.settings"
|
||||
app
|
||||
shift
|
||||
mandatory
|
||||
class="bottom-nav-btns"
|
||||
:value="$store.getters.tabById($route.params.id)"
|
||||
:value="$store.getters.tabById(creatureId)"
|
||||
@change="e => $store.commit(
|
||||
'setTabForCharacterSheet',
|
||||
{id: $route.params.id, tab: e}
|
||||
{id: creatureId, tab: e}
|
||||
)"
|
||||
>
|
||||
<v-btn>
|
||||
@@ -144,6 +143,7 @@ import CreatureLogs from '/imports/api/creature/log/CreatureLogs';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
|
||||
import CharacterSheetFab from '/imports/client/ui/creature/character/CharacterSheetFab.vue';
|
||||
import ActionsTab from '/imports/client/ui/creature/character/characterSheetTabs/ActionsTab.vue';
|
||||
import CharacterSheetInitiative from '/imports/client/ui/creature/character/CharacterSheetInitiative.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -156,13 +156,16 @@ export default {
|
||||
BuildTab,
|
||||
TreeTab,
|
||||
CharacterSheetFab,
|
||||
CharacterSheetInitiative,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
embedded: Boolean,
|
||||
},
|
||||
// @ts-ignore
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['creatureId', 'editPermission'],
|
||||
@@ -250,4 +253,9 @@ export default {
|
||||
min-height: calc(100vh - 96px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog-component .character-sheet .v-window-item {
|
||||
min-height: unset;
|
||||
overflow: unset;
|
||||
}
|
||||
</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">
|
||||
<div class="mini-character-sheet" />
|
||||
<v-card
|
||||
hover
|
||||
@click="$emit('click')"
|
||||
>
|
||||
Character sheet
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import ActionDialog from '/imports/client/ui/creature/actions/ActionDialog.vue';
|
||||
import InsertPropertyDialog from '/imports/client/ui/properties/InsertPropertyDialog.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 CreatureFormDialog from '/imports/client/ui/creature/CreatureFormDialog.vue';
|
||||
import CreaturePropertyDialog from '/imports/client/ui/creature/creatureProperties/CreaturePropertyDialog.vue';
|
||||
@@ -39,6 +40,7 @@ export default {
|
||||
ArchiveDialog,
|
||||
CastSpellWithSlotDialog,
|
||||
CharacterCreationDialog,
|
||||
CharacterSheetDialog,
|
||||
CreatureFormDialog,
|
||||
CreaturePropertyDialog,
|
||||
CreaturePropertyFromLibraryDialog,
|
||||
|
||||
@@ -80,6 +80,7 @@ export default {
|
||||
components: {
|
||||
CreatureFolderList
|
||||
},
|
||||
// @ts-ignore
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'characterList': [],
|
||||
@@ -97,8 +98,8 @@ export default {
|
||||
{ title: 'Home', icon: 'mdi-home', to: '/' },
|
||||
{ title: 'Characters', icon: 'mdi-account-group', to: '/character-list', requireLogin: true },
|
||||
{ title: 'Library', icon: 'mdi-library-shelves', to: '/library', requireLogin: true },
|
||||
//{title: 'Tabletops', icon: 'api', to: '/tabletops', requireLogin: true},
|
||||
//{title: 'Friends', icon: 'people', to: '/friends', requireLogin: true},
|
||||
{title: 'Tabletops', icon: 'mdi-table-furniture', to: '/tabletops', requireLogin: true},
|
||||
{title: 'Friends', icon: 'mdi-account-multiple', to: '/friends', requireLogin: true},
|
||||
{ title: 'Files', icon: 'mdi-file-multiple', to: '/my-files' },
|
||||
{ title: 'Feedback', icon: 'mdi-bug', to: '/feedback' },
|
||||
{ title: 'About', icon: 'mdi-sign-text', to: '/about' },
|
||||
|
||||
@@ -25,8 +25,11 @@
|
||||
:hint="inputHint"
|
||||
:error-messages="inputError"
|
||||
:disabled="!editPermission"
|
||||
:loading="submitLoading"
|
||||
@click:append-outer="submit"
|
||||
@keyup.enter="submit"
|
||||
@keyup.up="decrementHistory"
|
||||
@keyup.down="incrementHistory"
|
||||
/>
|
||||
</v-card>
|
||||
</div>
|
||||
@@ -40,6 +43,7 @@ import { assertEditPermission } from '/imports/api/creature/creatures/creaturePe
|
||||
import { parse, prettifyParseError } from '/imports/parser/parser';
|
||||
import resolve, { toString } from '/imports/parser/resolve';
|
||||
import LogEntry from '/imports/client/ui/log/LogEntry.vue';
|
||||
import { Tracker } from 'meteor/tracker'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -48,22 +52,70 @@ export default {
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: undefined,
|
||||
},
|
||||
tabletopId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
inputHint: undefined,
|
||||
inputError: undefined,
|
||||
input: undefined,
|
||||
history: [],
|
||||
historyIndex: 1,
|
||||
submitLoading: false,
|
||||
}},
|
||||
watch: {
|
||||
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;
|
||||
if (!this.input) return;
|
||||
let result;
|
||||
try {
|
||||
result = parse(value);
|
||||
result = parse(this.input);
|
||||
} catch (e){
|
||||
if (e.constructor.name === 'EndOfInputError'){
|
||||
this.inputError = '...';
|
||||
@@ -83,26 +135,29 @@ export default {
|
||||
return;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submit(){
|
||||
if (this.inputError || !this.input) return;
|
||||
logRoll.call({
|
||||
roll: this.input,
|
||||
creatureId: this.creatureId,
|
||||
}, (error) => {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
this.input = '';
|
||||
incrementHistory() {
|
||||
if (this.historyIndex < this.history.length) {
|
||||
this.historyIndex += 1;
|
||||
}
|
||||
},
|
||||
decrementHistory() {
|
||||
if (this.historyIndex > 0) {
|
||||
this.historyIndex -= 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-ignore
|
||||
meteor: {
|
||||
logs(){
|
||||
return CreatureLogs.find({
|
||||
creatureId: this.creatureId,
|
||||
}, {
|
||||
logs() {
|
||||
const filter = {};
|
||||
if (this.tabletopId) {
|
||||
filter.tabletopId = this.tabletopId;
|
||||
} else if (this.creatureId) {
|
||||
filter.creatureId = this.creatureId;
|
||||
}
|
||||
return CreatureLogs.find(filter, {
|
||||
sort: {date: -1},
|
||||
limit: 20
|
||||
limit: 100
|
||||
});
|
||||
},
|
||||
creature(){
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<v-card
|
||||
class="ma-2 log-entry"
|
||||
>
|
||||
<v-card-title v-if="showName && model.creatureName">
|
||||
{{ model.creatureName }}
|
||||
</v-card-title>
|
||||
<v-card-text
|
||||
v-if="model.text || (model.content && model.content.length)"
|
||||
class="pa-2"
|
||||
@@ -23,6 +26,7 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showName: Boolean,
|
||||
},
|
||||
}
|
||||
</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"
|
||||
:color="model.color || 'primary'"
|
||||
:loading="doActionLoading"
|
||||
:disabled="model.insufficientResources || !context.editPermission"
|
||||
:disabled="model.insufficientResources || !context.editPermission || !!targetingError"
|
||||
:roll-text="rollBonus"
|
||||
:name="model.name"
|
||||
:advantage="model.attackRoll && model.attackRoll.advantage"
|
||||
@@ -36,7 +36,7 @@
|
||||
class="mr-2"
|
||||
:color="model.color || 'primary'"
|
||||
:loading="doActionLoading"
|
||||
:disabled="model.insufficientResources || !context.editPermission"
|
||||
:disabled="model.insufficientResources || !context.editPermission || !!targetingError"
|
||||
@click.stop="doAction"
|
||||
>
|
||||
<property-icon :model="model" />
|
||||
@@ -53,12 +53,20 @@
|
||||
{{ model.name || propertyName }}
|
||||
</div>
|
||||
<div class="action-sub-title layout align-center">
|
||||
<div class="flex">
|
||||
{{ model.actionType }}
|
||||
</div>
|
||||
<div v-if="Number.isFinite(model.usesLeft)">
|
||||
{{ model.usesLeft }} uses
|
||||
<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>
|
||||
</div>
|
||||
@@ -149,6 +157,10 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
targets: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -185,6 +197,16 @@ export default {
|
||||
actionTypeIcon() {
|
||||
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: {
|
||||
children() {
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
:class="insufficient && 'error--text'"
|
||||
>
|
||||
<div
|
||||
v-if="model.quantity && model.quantity.value !== 1"
|
||||
class="mr-2 text-no-wrap text-truncate"
|
||||
style="min-width: 24px; text-align: center;"
|
||||
>
|
||||
{{ model.quantity && model.quantity.value }}
|
||||
{{ model.quantity.value }}
|
||||
</div>
|
||||
<div
|
||||
v-if="model.quantity && (typeof model.quantity.value !== 'string')"
|
||||
@@ -15,6 +16,12 @@
|
||||
>
|
||||
{{ model.statName || model.variableName }}
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -27,28 +27,30 @@
|
||||
:color="model.itemColor"
|
||||
/>
|
||||
<div
|
||||
v-if="quantity !== 1"
|
||||
class="mr-2 text-no-wrap"
|
||||
style="min-width: 24px; text-align: center;"
|
||||
>
|
||||
<template v-if="quantity !== 1">
|
||||
{{ model.available }} / {{ quantity }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ model.available }}
|
||||
</template>
|
||||
{{ quantity }}
|
||||
</div>
|
||||
<div
|
||||
class="text-no-wrap text-truncate flex"
|
||||
>
|
||||
<template v-if="model.itemId">
|
||||
{{ model.itemName }}
|
||||
</template>
|
||||
<span
|
||||
v-else
|
||||
class="error--text"
|
||||
<template v-if="model.itemId">
|
||||
<div
|
||||
class="text-no-wrap text-truncate"
|
||||
>
|
||||
Select item
|
||||
</span>
|
||||
{{ model.itemName }}
|
||||
</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>
|
||||
<v-icon
|
||||
v-if="context.editPermission"
|
||||
|
||||
@@ -19,34 +19,18 @@
|
||||
<v-flex
|
||||
style="height: 24px; flex-basis: 300px; flex-grow: 100;"
|
||||
>
|
||||
<div
|
||||
column
|
||||
align-center
|
||||
<health-bar-progress
|
||||
:model="model"
|
||||
style="cursor: pointer;"
|
||||
class="bar"
|
||||
@click="edit"
|
||||
>
|
||||
<div
|
||||
style="height: 24px; width: 100%; position: relative; transition: background-color 0.5s ease;"
|
||||
:style="{
|
||||
backgroundColor: barBackgroundColor
|
||||
class="value"
|
||||
:class="{
|
||||
'white--text': isTextLight,
|
||||
'black--text': !isTextLight,
|
||||
}"
|
||||
>
|
||||
<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;
|
||||
style="font-size: 15px;
|
||||
line-height: 24px;
|
||||
font-weight: 600;
|
||||
position: absolute;
|
||||
@@ -55,11 +39,10 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
text-align: center;"
|
||||
>
|
||||
{{ model.value }} / {{ model.total }}
|
||||
</div>
|
||||
>
|
||||
{{ model.value }} / {{ model.total }}
|
||||
</div>
|
||||
</div>
|
||||
</health-bar-progress>
|
||||
<v-menu
|
||||
v-model="editing"
|
||||
absolute
|
||||
@@ -85,11 +68,13 @@
|
||||
<script lang="js">
|
||||
import IncrementMenu from '/imports/client/ui/components/IncrementMenu.vue';
|
||||
import isDarkColor from '/imports/client/ui/utility/isDarkColor';
|
||||
import HealthBarProgress from '/imports/client/ui/properties/components/attributes/HealthBarProgress.vue';
|
||||
import chroma from 'chroma-js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IncrementMenu
|
||||
IncrementMenu,
|
||||
HealthBarProgress,
|
||||
},
|
||||
inject: {
|
||||
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: {
|
||||
title: 'Print Character Sheet',
|
||||
},
|
||||
},
|
||||
/* Not ready for prime time <3
|
||||
{
|
||||
}, {
|
||||
path: '/tabletops',
|
||||
name: 'tabletops',
|
||||
component: Tabletops,
|
||||
@@ -218,9 +216,7 @@ RouterFactory.configure(router => {
|
||||
rightDrawer: TabletopRightDrawer,
|
||||
},
|
||||
beforeEnter: ensureLoggedIn,
|
||||
},
|
||||
*/
|
||||
{
|
||||
}, {
|
||||
path: '/friends',
|
||||
components: {
|
||||
default: NotImplemented,
|
||||
|
||||
@@ -7,14 +7,22 @@
|
||||
background: #151515;
|
||||
}
|
||||
|
||||
.card-background .v-tabs-items.theme--dark {
|
||||
background: #151515;
|
||||
}
|
||||
|
||||
.theme--light .card-background {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.card-background .v-tabs-items.theme--light {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.theme--dark .card-raised-background {
|
||||
background: #1d1d1d;
|
||||
}
|
||||
|
||||
.theme--light .card-raised-background {
|
||||
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">
|
||||
<v-container
|
||||
class="tabletop"
|
||||
fluid
|
||||
<div
|
||||
class="tabletop layout column"
|
||||
style="height: 100%;"
|
||||
>
|
||||
<v-row
|
||||
dense
|
||||
class="initiative-row"
|
||||
style="flex-wrap: nowrap; overflow-x: auto;"
|
||||
<tabletop-map
|
||||
class="play-area"
|
||||
style="
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
"
|
||||
/>
|
||||
<v-container
|
||||
fluid
|
||||
>
|
||||
<tabletop-creature-card
|
||||
v-for="creature in creatures"
|
||||
:key="creature._id"
|
||||
:model="creature"
|
||||
/>
|
||||
<v-card
|
||||
class="layout column justify-center align-center"
|
||||
style="height: 150px; min-width: 120px;"
|
||||
data-id="select-creatures"
|
||||
hover
|
||||
@click="addCreature"
|
||||
<v-row
|
||||
dense
|
||||
class="initiative-row flex-grow-0"
|
||||
style="flex-wrap: nowrap; overflow-x: auto; padding-bottom: 50px;"
|
||||
@wheel="transformScroll($event)"
|
||||
>
|
||||
<div class="flex layout justify-center align-center">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
<tabletop-creature-card
|
||||
v-for="creature in creatures"
|
||||
:key="creature._id"
|
||||
:model="creature"
|
||||
:active="activeCreatureId === creature._id"
|
||||
:targeted="targets.includes(creature._id)"
|
||||
:show-target-btn="targets.includes(creature._id) || moreTargets"
|
||||
v-on="(!activeActionId || (targets.includes(creature._id) || moreTargets)) ? {
|
||||
click: () => {
|
||||
if (activeActionId) {
|
||||
if (targets.includes(creature._id)) {
|
||||
untarget(creature._id)
|
||||
} else {
|
||||
if (moreTargets) targets.push(creature._id);
|
||||
}
|
||||
} else {
|
||||
activeCreatureId = creature._id;
|
||||
targets = [];
|
||||
activeActionId = undefined;
|
||||
}
|
||||
}
|
||||
} : {}"
|
||||
@target="targets.push(creature._id)"
|
||||
@untarget="untarget(creature._id)"
|
||||
/>
|
||||
<div
|
||||
class="layout column ma-1 flex-grow-0"
|
||||
>
|
||||
<v-btn
|
||||
data-id="select-creatures"
|
||||
class="mb-2"
|
||||
@click="addCreature"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
Add Character
|
||||
</v-btn>
|
||||
<v-btn disabled>
|
||||
<v-icon left>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
Add Creature
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-card-title>
|
||||
Add<br>creature
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-row>
|
||||
<tabletop-map class="play-area" />
|
||||
<section class="action-row">
|
||||
<mini-character-sheet />
|
||||
<tabletop-action-cards />
|
||||
</section>
|
||||
</v-container>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<v-footer
|
||||
inset
|
||||
class="pa-0"
|
||||
style="
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom:0;
|
||||
right: 0;
|
||||
overflow-x: auto;
|
||||
"
|
||||
>
|
||||
<v-slide-y-reverse-transition mode="out-in">
|
||||
<selected-creature-bar
|
||||
:key="activeCreatureId"
|
||||
:creature-id="activeCreatureId"
|
||||
/>
|
||||
</v-slide-y-reverse-transition>
|
||||
</v-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import addCreaturesToTabletop from '/imports/api/tabletop/methods/addCreaturesToTabletop';
|
||||
import TabletopCreatureCard from '/imports/client/ui/tabletop/TabletopCreatureCard.vue';
|
||||
import TabletopMap from '/imports/client/ui/tabletop/TabletopMap.vue';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
import TabletopActionCards from '/imports/client/ui/tabletop/TabletopActionCards.vue';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import MiniCharacterSheet from '/imports/client/ui/creature/character/MiniCharacterSheet.vue';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import ActionCard from '/imports/client/ui/tabletop/TabletopActionCard.vue';
|
||||
import SelectedCreatureBar from '/imports/client/ui/tabletop/selectedCreatureBar/SelectedCreatureBar.vue';
|
||||
|
||||
const getProperties = function (creatureId, selector = {}) {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': {
|
||||
$eq: creatureId,
|
||||
},
|
||||
inactive: { $ne: true },
|
||||
removed: { $ne: true },
|
||||
overridden: { $ne: true },
|
||||
$nor: [
|
||||
{ hideWhenTotalZero: true, total: 0 },
|
||||
{ hideWhenValueZero: true, value: 0 },
|
||||
],
|
||||
...selector,
|
||||
}, {
|
||||
sort: { order: 1 }
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TabletopCreatureCard,
|
||||
TabletopMap,
|
||||
TabletopActionCards,
|
||||
ActionCard,
|
||||
MiniCharacterSheet,
|
||||
SelectedCreatureBar,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
@@ -58,9 +136,20 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['editPermission'],
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeCreature: undefined,
|
||||
activeCreatureId: undefined,
|
||||
activeActionId: undefined,
|
||||
targets: [],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeCreatureId(id) {
|
||||
this.$root.$emit('active-tabletop-character-change', id);
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
@@ -69,9 +158,29 @@ export default {
|
||||
return [this.model._id];
|
||||
},
|
||||
},
|
||||
creatures() {
|
||||
creatures(){
|
||||
return Creatures.find({ tabletop: this.model._id });
|
||||
},
|
||||
actions(){
|
||||
return getProperties(this.activeCreatureId, { type: 'action', actionType: { $ne: 'event'} });
|
||||
},
|
||||
moreTargets(){
|
||||
const activeAction = CreatureProperties.findOne(this.activeActionId);
|
||||
if (!activeAction) return;
|
||||
if (activeAction.target === 'singleTarget') {
|
||||
return this.targets.length === 0;
|
||||
} else if (activeAction.target === 'multipleTargets') {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
editPermission(){
|
||||
try {
|
||||
assertEditPermission(this.activeCreatureId, Meteor.userId());
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addCreature() {
|
||||
@@ -91,16 +200,53 @@ export default {
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
openCharacterSheetDialog(){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'character-sheet-dialog',
|
||||
elementId: 'mini-character-sheet',
|
||||
data: {
|
||||
creatureId: this.activeCreatureId,
|
||||
},
|
||||
});
|
||||
},
|
||||
clickProperty({_id}){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `${_id}`,
|
||||
data: {_id},
|
||||
});
|
||||
},
|
||||
transformScroll(event) {
|
||||
if (!event.deltaY) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.scrollLeft += event.deltaY;
|
||||
event.preventDefault();
|
||||
},
|
||||
untarget(id){
|
||||
const index = this.targets.indexOf(id);
|
||||
if (index > -1) {
|
||||
this.targets.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.initiative-row>.v-card {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
height: 162px;
|
||||
width: 100px;
|
||||
margin: 4px;
|
||||
}
|
||||
.action-row > div {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
height: 120px;
|
||||
width: 200px;
|
||||
margin: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,37 +1,146 @@
|
||||
<template lang="html">
|
||||
<v-card
|
||||
style="height: 150px; min-width: 120px;"
|
||||
:style="`height: ${height}px; width: ${width}px; overflow: hidden;`"
|
||||
class="tabletop-creature-card"
|
||||
:class="{ active }"
|
||||
:hover="hasClickListener"
|
||||
:elevation="active ? 8 : 2"
|
||||
@mouseover="() => { if (hasClickListener) hover = true; }"
|
||||
@mouseleave="hover = false"
|
||||
v-on="hasClickListener ? {click: () => $emit('click')} : {}"
|
||||
>
|
||||
<v-img
|
||||
:src="model.picture"
|
||||
aspect-ratio="1"
|
||||
:src="model.picture || '/images/ui/missing-portrait.png'"
|
||||
:lazy-src="loadingImg"
|
||||
:height="height"
|
||||
:width="width"
|
||||
class="align-end"
|
||||
:class="{placeholder: !model.picture}"
|
||||
gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)"
|
||||
position="top center"
|
||||
/>
|
||||
<div
|
||||
class="small-title"
|
||||
>
|
||||
{{ model.name }}
|
||||
<v-card-title
|
||||
class="small-title"
|
||||
v-text="model.name"
|
||||
/>
|
||||
<health-bar-progress
|
||||
v-for="bar in healthBars"
|
||||
:key="bar._id"
|
||||
:model="bar"
|
||||
:height="4"
|
||||
style="opacity: 0.7; margin-top: 2px"
|
||||
/>
|
||||
</v-img>
|
||||
<card-highlight :active="hover" />
|
||||
<div class="d-flex justify-center">
|
||||
<v-scale-transition>
|
||||
<v-btn
|
||||
v-if="showTargetBtn"
|
||||
:color="targeted ? 'accent' : ''"
|
||||
:elevation="targeted ? 8 : 2"
|
||||
fab
|
||||
small
|
||||
@click.stop="targeted ? $emit('untarget') : $emit('target')"
|
||||
>
|
||||
<v-icon>{{ targeted ? 'mdi-target' : 'mdi-target' }}</v-icon>
|
||||
</v-btn>
|
||||
</v-scale-transition>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import CardHighlight from '/imports/client/ui/components/CardHighlight.vue';
|
||||
import HealthBarProgress from '/imports/client/ui/properties/components/attributes/HealthBarProgress.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CardHighlight,
|
||||
HealthBarProgress,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 75,
|
||||
},
|
||||
active: Boolean,
|
||||
targeted: Boolean,
|
||||
showTargetBtn: Boolean,
|
||||
},
|
||||
data(){return {
|
||||
hover: false,
|
||||
loadingImg: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAXdJREFUWEftlq1vAkEQxedSB7pYDklBNsgmyCa4Cv5GBI6k8tJKUglULrWcBlvySA4B+/HeAcV07O3t/ObtvNnNHsyc3TEyAGRm+T0Yfs3WFwE0mk3bbbe12WsDtDsda+e5PbZaR4C1c/a9XEowtQC6vZ499ftniaCECiEDoPLnwSBa5WdRWLnZUErIAK+jkeHcY4HkgGBCAmCqr5KyKkgAobP3VXoTAJw9VGBitVhQjpAUeBkOD7Zj4sc5+5rPk0slAOUIkBwQqZAAUD1UYOImALAfbMjE+2xGjWhJASRmGpFtQOwnAzAqTCcTRqTDGhkAP8UGklK9BIDKq9svZUVcShjHZVkmnUApoNjvVHvAfBRFsCGTAJckr2AAAVf4Igqg+D7VdaHJGAVQRm8KIKRCFOBtPE7tK3333ZBBAOXuZyl8Fv1TAF8fBAGYkctWXq3zPdWCANdswJgdgwDM41NVwOeEf4BoE6be/3WO4PSdeARQN7vm+j0BlQ9wWvLB6AAAAABJRU5ErkJggg==',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasClickListener() {
|
||||
return this.$listeners && !!this.$listeners.click;
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
healthBars() {
|
||||
const folderIds = CreatureProperties.find({
|
||||
'ancestors.id': this.model._id,
|
||||
type: 'folder',
|
||||
groupStats: true,
|
||||
hideStatsGroup: true,
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}, { fields: { _id: 1 } }).map(folder => folder._id);
|
||||
|
||||
// Get the properties that need to be shown as a health bar
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.model._id,
|
||||
'parent.id': {
|
||||
$nin: folderIds,
|
||||
},
|
||||
type: 'attribute',
|
||||
attributeType: 'healthBar',
|
||||
healthBarNoDamage: { $ne: true },
|
||||
inactive: { $ne: true } ,
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
sort: {
|
||||
order: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.small-title {
|
||||
font-size: 14px;
|
||||
padding: 4px;
|
||||
font-size: 12px;
|
||||
padding: 4px 4px 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
line-height: normal;
|
||||
}
|
||||
.active {
|
||||
transform: scale(1.2);
|
||||
margin-left: 12px !important;
|
||||
margin-right: 12px !important;
|
||||
transform-origin: top center;
|
||||
}
|
||||
.tabletop-creature-card {
|
||||
transition: all .15s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="css">
|
||||
.tabletop-creature-card .v-btn {
|
||||
transition: all .3s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,52 +1,38 @@
|
||||
<template lang="html">
|
||||
<div class="tabletop-log">
|
||||
<div class="messages layout column justify-end align-end">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message._id"
|
||||
class="message"
|
||||
>
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
<v-textarea
|
||||
v-model="messageContent"
|
||||
@keyup.enter.prevent="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
<character-log
|
||||
:tabletop-id="tabletopId"
|
||||
:creature-id="activeCreatureId"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Messages, { sendMessage } from '/imports/api/tabletop/Messages';
|
||||
import { insertTabletopLog } from '/imports/api/creature/log/CreatureLogs';
|
||||
import CharacterLog from '/imports/client/ui/log/CharacterLog.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CharacterLog,
|
||||
},
|
||||
inject: {
|
||||
context: {
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
tabletopId: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data(){ return {
|
||||
messageContent: '',
|
||||
}},
|
||||
meteor: {
|
||||
messages() {
|
||||
return Messages.find({
|
||||
tabletopId: this.tabletopId,
|
||||
}, {
|
||||
sort: {
|
||||
timeStamp: 1,
|
||||
},
|
||||
});
|
||||
data() {
|
||||
return {
|
||||
activeCreatureId: undefined,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendMessage(){
|
||||
sendMessage.call({
|
||||
content: this.messageContent,
|
||||
tabletopId: this.tabletopId,
|
||||
});
|
||||
this.messageContent = '';
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on('active-tabletop-character-change', (id) => {
|
||||
this.activeCreatureId = id;
|
||||
});
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,134 @@
|
||||
<template lang="html">
|
||||
<div class="tabletop-map" />
|
||||
<template>
|
||||
<div>
|
||||
<canvas
|
||||
ref="map"
|
||||
class="tabletop-map"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import * as THREE from 'three';
|
||||
import { Tracker } from 'meteor/tracker'
|
||||
import TabletopObjects from '/imports/api/tabletop/TabletopObjects.js';
|
||||
import TabletopMaps from '/imports/api/tabletop/TabletopMaps.js';
|
||||
import { OrbitControls } from '/imports/api/tabletop/three/OrbitControls.js';
|
||||
import { DragControls } from 'three/examples/jsm/controls/DragControls.js';
|
||||
import { MapControls } from 'three/examples/jsm/controls/MapControls.js';
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader';
|
||||
|
||||
const maps = [
|
||||
{
|
||||
name: 'first map',
|
||||
position: { x: 0, y: 0 },
|
||||
width: 25,
|
||||
height: 25,
|
||||
texture: '/images/battlemap.webp',
|
||||
}
|
||||
];
|
||||
|
||||
export default {
|
||||
mounted(){
|
||||
const scene = new THREE.Scene();
|
||||
const perspectiveCam = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
|
||||
perspectiveCam.position.z = 5;
|
||||
const orthoCam = new THREE.OrthographicCamera( -2, 2, 2, -2, 0, 1000 );
|
||||
orthoCam.position.z = 500
|
||||
const activeCamera = orthoCam;
|
||||
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ canvas: this.$refs.map });
|
||||
renderer.shadowMap.enabled = true;
|
||||
|
||||
activeCamera.up.set( 0, 0, 1 ); // Use z as upwards
|
||||
const controls = new MapControls( activeCamera, renderer.domElement );
|
||||
|
||||
maps.forEach(map => {
|
||||
const texture = new THREE.TextureLoader().load( map.texture );
|
||||
const material = new THREE.MeshStandardMaterial({ map: texture });
|
||||
material.map.needsUpdate = true;
|
||||
const plane = new THREE.Mesh(new THREE.PlaneGeometry(map.width, map.height), material);
|
||||
plane.overdraw = true;
|
||||
plane.receiveShadow = true;
|
||||
scene.add(plane);
|
||||
});
|
||||
|
||||
// Example model
|
||||
const loader = new STLLoader()
|
||||
loader.load(
|
||||
'/models/example-mini.stl',
|
||||
function (geometry) {
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: 0xb2ffc8,
|
||||
});
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.castShadow = true;
|
||||
scene.add(mesh)
|
||||
const light = new THREE.PointLight()
|
||||
light.position.set(0, 0, 50)
|
||||
light.castShadow = true;
|
||||
scene.add(light)
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
|
||||
directionalLight.position.set(20, 50, 100)
|
||||
directionalLight.castShadow = true;
|
||||
scene.add( directionalLight );
|
||||
const dragControls = new DragControls([mesh], activeCamera, renderer.domElement);
|
||||
dragControls.addEventListener( 'dragstart', function ( event ) {
|
||||
controls.enabled = false;
|
||||
});
|
||||
dragControls.addEventListener( 'dragend', function ( event ) {
|
||||
controls.enabled = true;
|
||||
});
|
||||
},
|
||||
(xhr) => {
|
||||
console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
|
||||
},
|
||||
(error) => {
|
||||
console.log(error)
|
||||
}
|
||||
)
|
||||
//controls.enabled = false;
|
||||
|
||||
//});
|
||||
|
||||
/*
|
||||
const axesHelper = new THREE.AxesHelper( 5 );
|
||||
scene.add( axesHelper );
|
||||
*/
|
||||
|
||||
function resizeCanvasToDisplaySize() {
|
||||
const canvas = renderer.domElement;
|
||||
// look up the size the canvas is being displayed
|
||||
const width = canvas.clientWidth;
|
||||
const height = canvas.clientHeight - 50;
|
||||
|
||||
// adjust displayBuffer size to match
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
// you must pass false here or three.js sadly fights the browser
|
||||
perspectiveCam.aspect = width / height;
|
||||
orthoCam.left= width / -200;
|
||||
orthoCam.right = width / 200;
|
||||
orthoCam.top = height / 200;
|
||||
orthoCam.bottom = height / -200;
|
||||
perspectiveCam.updateProjectionMatrix();
|
||||
orthoCam.updateProjectionMatrix();
|
||||
controls.update();
|
||||
renderer.setSize(width, height, false);
|
||||
}
|
||||
}
|
||||
function animate() {
|
||||
resizeCanvasToDisplaySize();
|
||||
renderer.render( scene, activeCamera );
|
||||
requestAnimationFrame( animate );
|
||||
}
|
||||
animate();
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
<style>
|
||||
.tabletop-map {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template lang="html">
|
||||
<v-btn
|
||||
icon
|
||||
:plain="!selected"
|
||||
large
|
||||
tile
|
||||
:outlined="selected"
|
||||
:color="prop && prop.color"
|
||||
@click.prevent="$emit('click', $event)"
|
||||
@mouseenter="$emit('mouseenter', $event)"
|
||||
@mouseleave="$emit('mouseleave', $event)"
|
||||
>
|
||||
<property-icon
|
||||
v-if="prop"
|
||||
:model="prop"
|
||||
:color="prop.color"
|
||||
/>
|
||||
<v-icon v-else-if="icon">
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<v-icon v-else>
|
||||
mdi-help
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PropertyIcon,
|
||||
},
|
||||
props: {
|
||||
propId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
selected: Boolean,
|
||||
},
|
||||
meteor: {
|
||||
prop() {
|
||||
if (!this.propId) return;
|
||||
return CreatureProperties.findOne(this.propId);
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,432 @@
|
||||
<template lang="html">
|
||||
<div
|
||||
v-if="creatureId"
|
||||
class="selected-creature-bar d-flex pa-3 align-end"
|
||||
style="gap: 8px;"
|
||||
>
|
||||
<!--
|
||||
<tabletop-buff-icons
|
||||
creature-id="creatureId"
|
||||
@select-icon="selectIcon"
|
||||
/>
|
||||
<tabletop-portrait
|
||||
creature-id="creatureId"
|
||||
@select-icon="selectIcon"
|
||||
/>
|
||||
-->
|
||||
<v-menu
|
||||
v-model="menuOpen"
|
||||
v-click-outside="{
|
||||
handler: clickOutsideMenu,
|
||||
include: menuClickOutsideInclude,
|
||||
}"
|
||||
:position-x="menuX"
|
||||
:position-y="menuY"
|
||||
absolute
|
||||
top
|
||||
:nudge-left="150"
|
||||
origin="center bottom"
|
||||
:close-on-click="false"
|
||||
:content-class="`tabletop-prop-menu rows-${rows}`"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<tabletop-action-card
|
||||
v-if="selectedProp && selectedProp.type === 'action'"
|
||||
style="width: 300px;"
|
||||
:style="{
|
||||
width: '300px',
|
||||
opacity: selectedIcon ? 1 : 0.7,
|
||||
transition: 'opacity 0.2s ease',
|
||||
}"
|
||||
:model="selectedProp"
|
||||
/>
|
||||
<v-card
|
||||
v-else-if="activeIcon && activeIcon.tab"
|
||||
style="width: 300px"
|
||||
>
|
||||
<v-card-title>
|
||||
<v-icon left>
|
||||
{{ activeIcon.icon }}
|
||||
</v-icon>
|
||||
{{ activeIcon.tabName }}
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<v-card
|
||||
v-if="iconGroups.buffs"
|
||||
class="buffs-card"
|
||||
>
|
||||
<div
|
||||
v-for="(row, rowIndex) in iconGroups.buffs.rows"
|
||||
:key="rowIndex"
|
||||
class="d-flex"
|
||||
>
|
||||
<template
|
||||
v-for="(icon, iconIndex) in row"
|
||||
>
|
||||
<creature-bar-icon
|
||||
:key="icon.propId || iconIndex"
|
||||
:prop-id="icon.propId"
|
||||
:icon="icon.icon"
|
||||
:selected="selectedIcon === icon"
|
||||
:data-id="icon.propId || icon.standardId"
|
||||
@click="e => selectIcon(e, icon)"
|
||||
@mouseenter="e => hoverIcon(e, icon)"
|
||||
@mouseleave="unHoverIcon(icon)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</v-card>
|
||||
<v-card
|
||||
class="creature-portrait"
|
||||
:width="90"
|
||||
:height="120"
|
||||
>
|
||||
<v-img
|
||||
v-if="creature.picture"
|
||||
:height="120"
|
||||
:src="creature.picture"
|
||||
position="top center"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="fill-height d-flex align-center justify-center"
|
||||
style="opacity: 0.2;"
|
||||
>
|
||||
<v-icon
|
||||
size="90"
|
||||
>
|
||||
mdi-account
|
||||
</v-icon>
|
||||
</div>
|
||||
</v-card>
|
||||
<v-card
|
||||
v-for="group in iconGroups"
|
||||
:key="group.name"
|
||||
>
|
||||
<div
|
||||
v-for="(row, rowIndex) in group.rows"
|
||||
:key="rowIndex"
|
||||
class="d-flex"
|
||||
>
|
||||
<template
|
||||
v-for="(icon, iconIndex) in row"
|
||||
>
|
||||
<creature-bar-icon
|
||||
:key="icon.propId || iconIndex"
|
||||
:prop-id="icon.propId"
|
||||
:icon="icon.icon"
|
||||
:selected="selectedIcon === icon"
|
||||
:data-id="icon.propId || icon.standardId"
|
||||
@click="e => selectIcon(e, icon)"
|
||||
@mouseenter="e => hoverIcon(e, icon)"
|
||||
@mouseleave="unHoverIcon(icon)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</v-card>
|
||||
<!--<tabletop-actions
|
||||
creature-id="creatureId"
|
||||
@select-icon="selectIcon"
|
||||
/>
|
||||
<tabletop-detail-popover />
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import TabletopActionCard from '/imports/client/ui/tabletop/TabletopActionCard.vue';
|
||||
import CreatureBarIcon from '/imports/client/ui/tabletop/selectedCreatureBar/CreatureBarIcon.vue';
|
||||
|
||||
//import TabletopPortrait from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopPortrait.vue';
|
||||
//import TabletopBuffIcons from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopBuffIcons.vue';
|
||||
//import TabletopActions from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopActions.vue';
|
||||
//import TabletopGroupedFolders from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopGroupedFolders.vue';
|
||||
//import TabletopResources from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopResources.vue';
|
||||
//import TabletopCreatureSheetTabs from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopCreatureSheetTabs.vue';
|
||||
//import TabletopDetailPopover from '/imports/client/ui/tabletop/selectedCreatureBar/TabletopDetailPopover.vue';
|
||||
|
||||
function splitToNChunks(inputArray, n) {
|
||||
let result = [];
|
||||
const array = [...inputArray] // Create shallow copy, because splice mutates array
|
||||
for (let i = n; i > 0; i--) {
|
||||
result.push(array.splice(0, Math.ceil(array.length / i)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
//TabletopPortrait,
|
||||
//TabletopBuffIcons,
|
||||
//TabletopActions,
|
||||
//TabletopGroupedFolders,
|
||||
//TabletopResources,
|
||||
//TabletopCreatureSheetTabs,
|
||||
CreatureBarIcon,
|
||||
TabletopActionCard,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rows: 2,
|
||||
hoveredIcon: undefined,
|
||||
selectedIcon: undefined,
|
||||
menuOpen: false,
|
||||
menuX: 200,
|
||||
menuY: window.innerHeight - 216,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
activeIcon() {
|
||||
return this.selectedIcon || this.hoveredIcon;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
menuOpen(val) {
|
||||
if (!val && this.selectIcon) {
|
||||
this.selectedIcon = undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
log(e) {
|
||||
console.log(e);
|
||||
},
|
||||
hoverIcon(e, icon) {
|
||||
if (this.selectedIcon) return;
|
||||
// this.menuX = e.clientX - (e.clientX % 44);
|
||||
const { left, right } = e.target.getBoundingClientRect();
|
||||
const x = ( left + right ) / 2
|
||||
this.menuX = x;
|
||||
this.hoveredIcon = icon;
|
||||
this.menuOpen = true;
|
||||
},
|
||||
unHoverIcon(icon) {
|
||||
if (this.hoveredIcon === icon) {
|
||||
this.hoveredIcon = undefined;
|
||||
if (!this.selectedIcon) {
|
||||
this.menuOpen = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
selectIcon(e, icon) {
|
||||
if (icon.tab) {
|
||||
this.openCharacterSheet(icon.tab, icon.standardId);
|
||||
return;
|
||||
}
|
||||
if (this.selectedIcon === icon) {
|
||||
this.selectedIcon = undefined;
|
||||
this.menuOpen = false;
|
||||
return;
|
||||
}
|
||||
const { left, right } = e.target.getBoundingClientRect();
|
||||
const x = ( left + right ) / 2
|
||||
this.menuX = x;
|
||||
this.selectedIcon = icon;
|
||||
this.menuOpen = true;
|
||||
},
|
||||
clickOutsideMenu () {
|
||||
this.menuOpen = false;
|
||||
},
|
||||
menuClickOutsideInclude() {
|
||||
return [
|
||||
document.querySelector('.selected-creature-bar'),
|
||||
document.querySelector('.tabletop-prop-menu')
|
||||
];
|
||||
},
|
||||
openCharacterSheet(tab, elementId) {
|
||||
this.$store.commit(
|
||||
'setTabForCharacterSheet',
|
||||
{ id: this.creatureId, tab }
|
||||
);
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'character-sheet-dialog',
|
||||
elementId,
|
||||
data: {
|
||||
creatureId: this.creatureId,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
creature() {
|
||||
if (!this.creatureId) return;
|
||||
return Creatures.findOne(this.creatureId)
|
||||
},
|
||||
selectedProp() {
|
||||
const propId = this.activeIcon?.propId;
|
||||
if (!propId) return;
|
||||
return CreatureProperties.findOne(propId);
|
||||
},
|
||||
iconGroups() {
|
||||
if (!this.creature) return;
|
||||
const iconGroups = [];
|
||||
|
||||
// Get the standard icons
|
||||
const standardIconsById = {
|
||||
'cast-spell': {standardId: 'cast-spell', groupName: 'Standard Actions', icon: 'mdi-fire' },
|
||||
'make-check': {standardId: 'make-check', groupName: 'Standard Actions', icon: 'mdi-radiobox-marked' },
|
||||
'roll-dice': {standardId: 'roll-dice', groupName: 'Standard Actions', icon: 'mdi-dice-d20' },
|
||||
'tab-stats': {standardId: 'tab-stats', groupName: 'Tabs', icon: 'mdi-chart-box', tab: 'stats', tabName: 'Stats' },
|
||||
'tab-actions': {standardId: 'tab-actions', groupName: 'Tabs', icon: 'mdi-lightning-bolt', tab: 'actions', tabName: 'Actions' },
|
||||
'tab-spells': this.creature?.settings?.hideSpellsTab ? undefined : {standardId: 'tab-spells', groupName: 'Tabs', icon: 'mdi-fire', tab: 'spells', tabName: 'Spells' },
|
||||
'tab-inventory': {standardId: 'tab-inventory', groupName: 'Tabs', icon: 'mdi-cube', tab: 'inventory', tabName: 'Inventory' },
|
||||
'tab-features': {standardId: 'tab-features', groupName: 'Tabs', icon: 'mdi-text', tab: 'features', tabName: 'Features' },
|
||||
'tab-journal': {standardId: 'tab-journal', groupName: 'Tabs', icon: 'mdi-book-open-variant', tab: 'journal', tabName: 'Journal' },
|
||||
'tab-build': {standardId: 'tab-build', groupName: 'Tabs', icon: 'mdi-wrench', tab: 'build', tabName: 'Build' },
|
||||
};
|
||||
|
||||
// Get the folders that could hide a property
|
||||
const folderIds = CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'folder',
|
||||
groupStats: true,
|
||||
hideStatsGroup: true,
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}, { fields: { _id: 1 } }).map(folder => folder._id);
|
||||
|
||||
// Get the properties that need to be shown as an icon
|
||||
const filter = {
|
||||
'ancestors.id': this.creatureId,
|
||||
'parent.id': {
|
||||
$nin: folderIds,
|
||||
},
|
||||
$and: [
|
||||
{
|
||||
$or: [
|
||||
{ type: 'action' },
|
||||
{ type: 'folder', groupStats: true },
|
||||
{ type: 'attribute' },
|
||||
{ type: 'toggle' },
|
||||
{ type: 'buff' }
|
||||
],
|
||||
},
|
||||
{
|
||||
$or: [
|
||||
{ inactive: { $ne: true } },
|
||||
{ type: 'toggle' },
|
||||
]
|
||||
}
|
||||
],
|
||||
removed: { $ne: true },
|
||||
};
|
||||
if (this.creature.settings?.hideUnusedStats) {
|
||||
filter.hide = { $ne: true };
|
||||
}
|
||||
|
||||
// Get all the properties we wish to display, with just their IDs, and store them
|
||||
const propsById = {};
|
||||
const props = [];
|
||||
CreatureProperties.find(filter, {
|
||||
sort: { order: -1 },
|
||||
fields: { _id: 1, type: 1 },
|
||||
}).forEach(prop => {
|
||||
props.push(prop);
|
||||
propsById[prop._id] = prop;
|
||||
});
|
||||
|
||||
// Using the creature's custom icon groups, collect the props into groups
|
||||
this.creature.tabletopSettings?.iconGroups.forEach(group => {
|
||||
const iconList = [];
|
||||
group.iconIds?.forEach(id => {
|
||||
if (propsById[id]) {
|
||||
const prop = propsById[id];
|
||||
prop._placedInGroup = true;
|
||||
iconList.push({ propId: prop._id });
|
||||
} else if (standardIconsById[id]) {
|
||||
const standardIcon = standardIconsById[id];
|
||||
standardIcon._placedInGroup = true;
|
||||
iconList.push(standardIcon);
|
||||
}
|
||||
});
|
||||
iconGroups.push({
|
||||
name: group.name,
|
||||
iconList,
|
||||
});
|
||||
});
|
||||
|
||||
// Default groups
|
||||
let groupsByName = {};
|
||||
let defaultGroups = [];
|
||||
|
||||
// Add default groups for props that have not yet been collected into custom groups
|
||||
props.forEach(prop => {
|
||||
if (prop._placedInGroup) return;
|
||||
let groupName;
|
||||
switch (prop.type) {
|
||||
case 'buff': groupName = 'Buffs'; break;
|
||||
case 'action': groupName = 'Actions'; break;
|
||||
case 'resource': groupName = 'Resources'; break;
|
||||
case 'folder': groupName = 'Folders'; break;
|
||||
}
|
||||
if (!groupName) return;
|
||||
if (!groupsByName[groupName]) {
|
||||
groupsByName[groupName] = { name: groupName, iconList: [] };
|
||||
if (groupName !== 'Buffs') { // don't add buffs to the default groups, it is handled differently
|
||||
defaultGroups.push(groupsByName[groupName]);
|
||||
}
|
||||
}
|
||||
groupsByName[groupName].iconList.push({ propId: prop._id });
|
||||
});
|
||||
|
||||
// Add default groups for standard icons
|
||||
for (let key in standardIconsById) {
|
||||
const standardIcon = standardIconsById[key];
|
||||
if (!standardIcon) continue;
|
||||
if (standardIcon._placedInGroup) continue;
|
||||
|
||||
const groupName = standardIcon.groupName || 'no';
|
||||
if (!groupsByName[groupName]) {
|
||||
groupsByName[groupName] = { name: groupName, iconList: [] };
|
||||
defaultGroups.push(groupsByName[groupName]);
|
||||
}
|
||||
|
||||
groupsByName[groupName].iconList.push(standardIcon);
|
||||
}
|
||||
|
||||
iconGroups.push(...defaultGroups);
|
||||
|
||||
// Store a specific reference to buffs outside of the list order
|
||||
iconGroups.buffs = groupsByName['Buffs'];
|
||||
|
||||
// Divide the icons into rows
|
||||
iconGroups.forEach(group => {
|
||||
group.rows = splitToNChunks(group.iconList, this.rows);
|
||||
});
|
||||
if (iconGroups.buffs) {
|
||||
iconGroups.buffs.rows = splitToNChunks(iconGroups.buffs.iconList, this.rows);
|
||||
}
|
||||
|
||||
return iconGroups;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.tabletop-prop-menu {
|
||||
top: unset !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.tabletop-prop-menu.rows-1 {
|
||||
bottom: 80px;
|
||||
}
|
||||
.tabletop-prop-menu.rows-2 {
|
||||
bottom: 124px;
|
||||
}
|
||||
.tabletop-prop-menu.rows-3 {
|
||||
bottom: 168px;
|
||||
}
|
||||
.tabletop-prop-menu.rows-4 {
|
||||
bottom: 212px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
import Vue from 'vue';
|
||||
//import Vuetify from 'vuetify/lib';
|
||||
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 SvgIconByName from '/imports/client/ui/icons/SvgIconByName.vue';
|
||||
import themes from '/imports/client/ui/themes';
|
||||
@@ -11,6 +11,7 @@ Vue.use(Vuetify, {
|
||||
directives: {
|
||||
Scroll,
|
||||
Ripple,
|
||||
ClickOutside,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ const store = new Vuex.Store({
|
||||
setTabForCharacterSheet(state, { tab, id }) {
|
||||
// Convert tab names to tab numbers
|
||||
if (typeof tab === 'string') {
|
||||
const tabInput = tab;
|
||||
const creature = Creatures.findOne(id);
|
||||
if (creature?.settings?.hideSpellsTab) {
|
||||
tab = tabsWithoutSpells.indexOf(tab);
|
||||
@@ -63,9 +64,9 @@ const store = new Vuex.Store({
|
||||
tab = tabs.indexOf(tab);
|
||||
}
|
||||
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);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import Tabletops from '/imports/api/tabletop/Tabletops';
|
||||
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 () {
|
||||
var userId = this.userId;
|
||||
@@ -21,6 +24,7 @@ Meteor.publish('tabletop', function (tabletopId) {
|
||||
return [];
|
||||
}
|
||||
this.autorun(function () {
|
||||
const self = this;
|
||||
let tabletopCursor = Tabletops.find({
|
||||
_id: tabletopId,
|
||||
$or: [
|
||||
@@ -32,6 +36,7 @@ Meteor.publish('tabletop', function (tabletopId) {
|
||||
if (!tabletop) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 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
|
||||
// possible
|
||||
@@ -39,22 +44,32 @@ Meteor.publish('tabletop', function (tabletopId) {
|
||||
tabletop: tabletopId,
|
||||
}, {
|
||||
fields: {
|
||||
_id: 1,
|
||||
name: 1,
|
||||
picture: 1,
|
||||
avatarPicture: 1,
|
||||
variables: 1,
|
||||
tabletop: 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,
|
||||
}, {
|
||||
sort: {
|
||||
timeStamp: -1,
|
||||
},
|
||||
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": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
|
||||
"integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg=="
|
||||
"integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA="
|
||||
},
|
||||
"lodash.template": {
|
||||
"version": "4.5.0",
|
||||
@@ -5367,6 +5367,11 @@
|
||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -5540,9 +5545,9 @@
|
||||
}
|
||||
},
|
||||
"vue": {
|
||||
"version": "2.6.14",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
|
||||
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
|
||||
"version": "2.6.10",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
|
||||
"integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
|
||||
},
|
||||
"vue-eslint-parser": {
|
||||
"version": "7.11.0",
|
||||
|
||||
@@ -53,8 +53,9 @@
|
||||
"simpl-schema": "^1.13.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speakingurl": "^14.0.1",
|
||||
"three": "^0.156.1",
|
||||
"vivagraphjs": "^0.12.0",
|
||||
"vue": "2.6.14",
|
||||
"vue": "2.6.10",
|
||||
"vue-meteor-tracker": "^2.0.0",
|
||||
"vue-reactive-provide": "^0.3.0",
|
||||
"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