Merge commit 'ace284e265a65006263fcecab7865c5e1ebd899f' into feature-tabletop
This commit is contained in:
@@ -137,26 +137,7 @@ const insertCreatureLog = new ValidatedMethod({
|
||||
},
|
||||
});
|
||||
|
||||
const insertTabletopLog = new ValidatedMethod({
|
||||
name: 'creatureLogs.methods.insertTabletopLog',
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
validate: new SimpleSchema({
|
||||
log: CreatureLogSchema.omit('date'),
|
||||
}).validator(),
|
||||
run({ log }) {
|
||||
const tabletopId = log.tabletopId;
|
||||
assertUserInTabletop(tabletopId, this.userId);
|
||||
// Build the new log
|
||||
let id = insertCreatureLogWork({ log, method: this })
|
||||
return id;
|
||||
},
|
||||
});
|
||||
|
||||
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 }] };
|
||||
@@ -170,8 +151,8 @@ export function insertCreatureLogWork({ log, creature, method }) {
|
||||
}
|
||||
});
|
||||
log.date = new Date();
|
||||
if (creature) log.tabletopId = creature.tabletop;
|
||||
|
||||
if (tabletopId) log.tabletopId = tabletopId;
|
||||
if (creature && creature.tabletop) log.tabletopId = creature.tabletop;
|
||||
// Insert it
|
||||
let id = CreatureLogs.insert(log);
|
||||
if (Meteor.isServer) {
|
||||
@@ -205,13 +186,24 @@ 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, {
|
||||
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,
|
||||
@@ -222,7 +214,11 @@ const logRoll = new ValidatedMethod({
|
||||
}
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
const variables = CreatureVariables.findOne({ _creatureId: creatureId });
|
||||
}
|
||||
if (tabletopId) {
|
||||
assertUserInTabletop(tabletopId, this.userId);
|
||||
}
|
||||
const variables = CreatureVariables.findOne({ _creatureId: creatureId }) || {};
|
||||
let logContent = []
|
||||
let parsedResult = undefined;
|
||||
try {
|
||||
@@ -263,11 +259,11 @@ const logRoll = new ValidatedMethod({
|
||||
date: new Date(),
|
||||
};
|
||||
|
||||
let id = insertCreatureLogWork({ log, creature, method: this });
|
||||
let id = insertCreatureLogWork({ log, creature, tabletopId, method: this });
|
||||
|
||||
return id;
|
||||
},
|
||||
});
|
||||
|
||||
export default CreatureLogs;
|
||||
export { CreatureLogSchema, insertCreatureLog, logRoll, insertTabletopLog, PER_CREATURE_LOG_LIMIT };
|
||||
export { CreatureLogSchema, insertCreatureLog, logRoll, PER_CREATURE_LOG_LIMIT };
|
||||
|
||||
@@ -9,8 +9,7 @@ export default function computeAction(computation, node) {
|
||||
computeResources(computation, node);
|
||||
if (!prop.resources) return;
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,15 +10,17 @@ export const loadedCreatures = new Map(); // creatureId => {creature, properties
|
||||
export function loadCreature(creatureId, subscription) {
|
||||
if (!creatureId) throw 'creatureId is required';
|
||||
let creature = loadedCreatures.get(creatureId);
|
||||
if (!creature || !creature.subs.has(subscription)) {
|
||||
subscription.onStop(() => {
|
||||
unloadCreature(creatureId, subscription);
|
||||
});
|
||||
}
|
||||
if (loadedCreatures.has(creatureId)) {
|
||||
creature.subs.add(subscription);
|
||||
} else {
|
||||
creature = new LoadedCreature(subscription, creatureId);
|
||||
loadedCreatures.set(creatureId, creature);
|
||||
}
|
||||
subscription.onStop(() => {
|
||||
unloadCreature(creatureId, subscription);
|
||||
});
|
||||
}
|
||||
|
||||
function unloadCreature(creatureId, subscription) {
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
<template lang="html">
|
||||
<div
|
||||
style="height: 100%; overflow: hidden;"
|
||||
class="character-log layout column justify-end"
|
||||
>
|
||||
<v-slide-y-reverse-transition
|
||||
group
|
||||
hide-on-leave
|
||||
class="card-raised-background flex layout column reverse align-end pa-3"
|
||||
style="overflow: auto;"
|
||||
>
|
||||
<log-entry
|
||||
v-for="log in logs"
|
||||
:key="log._id"
|
||||
:model="log"
|
||||
:show-name="showName"
|
||||
/>
|
||||
</v-slide-y-reverse-transition>
|
||||
<v-card>
|
||||
<v-text-field
|
||||
v-model="input"
|
||||
class="mx-2 mb-2"
|
||||
persistent-hint
|
||||
style="flex-grow: 0"
|
||||
append-outer-icon="mdi-send"
|
||||
:hint="inputHint"
|
||||
:error-messages="inputError"
|
||||
:disabled="!editPermission"
|
||||
@click:append-outer="submit"
|
||||
@keyup.enter="submit"
|
||||
/>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import LogEntry from '/imports/client/ui/log/LogEntry.vue';
|
||||
import { parse, prettifyParseError } from '/imports/parser/parser.js';
|
||||
import resolve, { toString } from '/imports/parser/resolve.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LogEntry,
|
||||
},
|
||||
props: {
|
||||
logs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
editPermission: Boolean,
|
||||
showName: Boolean,
|
||||
},
|
||||
data(){return {
|
||||
inputHint: undefined,
|
||||
inputError: undefined,
|
||||
input: undefined,
|
||||
}},
|
||||
watch: {
|
||||
input(value){
|
||||
this.input = value;
|
||||
this.inputHint = this.inputError = undefined;
|
||||
if (!this.input) return;
|
||||
let result;
|
||||
try {
|
||||
result = parse(value);
|
||||
} catch (e){
|
||||
if (e.constructor.name === 'EndOfInputError'){
|
||||
this.inputError = '...';
|
||||
} else {
|
||||
let error = prettifyParseError(e);
|
||||
this.inputError = error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let {result: compiled} = resolve('compile', result, this.creature.variables);
|
||||
this.inputHint = toString(compiled);
|
||||
return;
|
||||
} catch (e){
|
||||
console.warn(e);
|
||||
this.inputError = 'Compilation error';
|
||||
return;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submit(){
|
||||
if (this.inputError || !this.input) return;
|
||||
this.$emit('submit', this.input);
|
||||
this.input = '';
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -1,89 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<canvas
|
||||
ref="map"
|
||||
class="tabletop-map"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as THREE from 'three';
|
||||
import { Tracker } from 'meteor/tracker'
|
||||
import { MapControls } from '/imports/client/ui/tabletop/three/OrbitControls.js';
|
||||
|
||||
const maps = [
|
||||
{
|
||||
name: 'first map',
|
||||
position: { x: 0, y: 0 },
|
||||
width: 5,
|
||||
height: 5,
|
||||
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, 1, 1000 );
|
||||
orthoCam.position.z = 5
|
||||
const activeCamera = orthoCam;
|
||||
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({canvas: this.$refs.map});
|
||||
|
||||
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.MeshBasicMaterial({ map: texture });
|
||||
material.map.needsUpdate = true;
|
||||
const plane = new THREE.Mesh(new THREE.PlaneGeometry(map.width, map.height), material);
|
||||
plane.overdraw = true;
|
||||
scene.add(plane);
|
||||
});
|
||||
|
||||
/*
|
||||
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>
|
||||
.tabletop-map {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -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,17 +30,17 @@
|
||||
<div
|
||||
v-else
|
||||
key="character-tabs"
|
||||
class="card-background"
|
||||
class="card-background fill-height"
|
||||
>
|
||||
<v-tabs-items
|
||||
:key=" '' +
|
||||
creature.settings.hideSpellsTab +
|
||||
creature.settings.showTreeTab
|
||||
"
|
||||
: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-tab-item>
|
||||
@@ -68,11 +68,10 @@
|
||||
<tree-tab :creature-id="creatureId" />
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
<character-sheet-initiative />
|
||||
</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>
|
||||
@@ -164,7 +163,9 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
embedded: Boolean,
|
||||
},
|
||||
// @ts-ignore
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['creatureId', 'editPermission'],
|
||||
@@ -250,7 +251,11 @@ export default {
|
||||
<style>
|
||||
.character-sheet .v-window-item {
|
||||
min-height: calc(100vh - 96px);
|
||||
padding-bottom: 70px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog-component .character-sheet .v-window-item {
|
||||
min-height: unset;
|
||||
overflow: unset;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Load commonly used dialogs immediately
|
||||
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';
|
||||
@@ -37,6 +38,7 @@ export default {
|
||||
ArchiveDialog,
|
||||
CastSpellWithSlotDialog,
|
||||
CharacterCreationDialog,
|
||||
CharacterSheetDialog,
|
||||
CreatureFormDialog,
|
||||
CreaturePropertyDialog,
|
||||
CreaturePropertyFromLibraryDialog,
|
||||
|
||||
@@ -80,6 +80,7 @@ export default {
|
||||
components: {
|
||||
CreatureFolderList
|
||||
},
|
||||
// @ts-ignore
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'characterList': [],
|
||||
|
||||
@@ -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.js';
|
||||
import resolve, { toString } from '/imports/parser/resolve.js';
|
||||
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,24 +135,29 @@ export default {
|
||||
return;
|
||||
}
|
||||
},
|
||||
incrementHistory() {
|
||||
if (this.historyIndex < this.history.length) {
|
||||
this.historyIndex += 1;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit(input){
|
||||
logRoll.call({
|
||||
roll: input,
|
||||
creatureId: this.creatureId,
|
||||
}, (error) => {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
},
|
||||
decrementHistory() {
|
||||
if (this.historyIndex > 0) {
|
||||
this.historyIndex -= 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-ignore
|
||||
meteor: {
|
||||
logs() {
|
||||
return CreatureLogs.find({
|
||||
creatureId: this.creatureId,
|
||||
}, {
|
||||
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(){
|
||||
|
||||
@@ -4,16 +4,23 @@
|
||||
:class="insufficient && 'error--text'"
|
||||
>
|
||||
<div
|
||||
v-if="model.quantity && model.quantity.value !== 1"
|
||||
class="mr-2"
|
||||
style="width: 24px; text-align: center;"
|
||||
>
|
||||
{{ model.quantity && model.quantity.value }}
|
||||
{{ model.quantity.value }}
|
||||
</div>
|
||||
<div
|
||||
class="text-no-wrap text-truncate"
|
||||
>
|
||||
{{ 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>
|
||||
<template v-if="model.itemId">
|
||||
<div
|
||||
class="text-no-wrap text-truncate"
|
||||
>
|
||||
{{ model.itemName }}
|
||||
</div>
|
||||
<div
|
||||
class="text-no-wrap text-truncate flex"
|
||||
v-if="(typeof model.available) == 'number'"
|
||||
class="text--disabled text-no-wrap text-truncate ml-1 flex-shrink-0"
|
||||
>
|
||||
<template v-if="model.itemId">
|
||||
{{ model.itemName }}
|
||||
({{ model.available }})
|
||||
</div>
|
||||
</template>
|
||||
<span
|
||||
<div
|
||||
v-else
|
||||
class="error--text"
|
||||
class="error--text text-no-wrap text-truncate flex"
|
||||
>
|
||||
Select item
|
||||
</span>
|
||||
</div>
|
||||
<v-icon
|
||||
v-if="context.editPermission"
|
||||
|
||||
@@ -7,10 +7,18 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,60 @@
|
||||
<template>
|
||||
<dialog-base>
|
||||
<template #replace-toolbar>
|
||||
<template #toolbar-extension>
|
||||
<v-tabs
|
||||
v-if="creature && creature.settings"
|
||||
v-model="tab"
|
||||
:value="$store.getters.tabById(creatureId)"
|
||||
:color="$vuetify.theme.themes.dark.primary"
|
||||
class="flex"
|
||||
style="min-width: 0"
|
||||
centered
|
||||
grow
|
||||
max="100px"
|
||||
@change="e => $store.commit(
|
||||
'setTabForCharacterSheet',
|
||||
{id: creatureId, tab: e}
|
||||
)"
|
||||
>
|
||||
<v-tab>
|
||||
Stats
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
Features
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
Inventory
|
||||
Actions
|
||||
</v-tab>
|
||||
<v-tab v-if="!creature.settings.hideSpellsTab">
|
||||
Spells
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
Character
|
||||
Inventory
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
Features
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
Journal
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
Build
|
||||
</v-tab>
|
||||
<v-tab v-if="creature.settings.showTreeTab">
|
||||
Tree
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</template>
|
||||
<template #unwrapped-content>
|
||||
<character-sheet
|
||||
show-menu-button
|
||||
embedded
|
||||
:creature-id="creatureId"
|
||||
/>
|
||||
</template>
|
||||
</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: {
|
||||
@@ -48,6 +67,13 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
// @ts-ignore
|
||||
meteor: {
|
||||
creature() {
|
||||
if (!this.creatureId) return;
|
||||
return Creatures.findOne(this.creatureId);
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
tab: 0,
|
||||
}},
|
||||
395
app/imports/client/ui/tabletop/TabletopActionCard.vue
Normal file
395
app/imports/client/ui/tabletop/TabletopActionCard.vue
Normal file
@@ -0,0 +1,395 @@
|
||||
<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;
|
||||
}
|
||||
},
|
||||
// @ts-ignore
|
||||
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,49 +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.js';
|
||||
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');
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -19,16 +19,31 @@
|
||||
<v-row
|
||||
dense
|
||||
class="initiative-row flex-grow-0"
|
||||
style="flex-wrap: nowrap; overflow-x: auto;"
|
||||
style="flex-wrap: nowrap; overflow-x: auto; padding-bottom: 50px;"
|
||||
@wheel="transformScroll($event)"
|
||||
>
|
||||
<tabletop-creature-card
|
||||
v-for="creature in creatures"
|
||||
:key="creature._id"
|
||||
:model="creature"
|
||||
:active="activeCreature === creature._id"
|
||||
:active="activeCreatureId === creature._id"
|
||||
:targeted="targets.includes(creature._id)"
|
||||
@click="activeCreature = creature._id; targets = []"
|
||||
: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)"
|
||||
/>
|
||||
@@ -67,22 +82,24 @@
|
||||
<v-container fluid>
|
||||
<v-row
|
||||
dense
|
||||
class="action-row"
|
||||
style="flex-wrap: nowrap; overflow-x: auto;"
|
||||
class="action-row overflow-x-auto align-end"
|
||||
style="flex-wrap: nowrap; padding-top: 100px;"
|
||||
@wheel="transformScroll($event)"
|
||||
>
|
||||
<mini-character-sheet
|
||||
v-if="activeCreature"
|
||||
v-if="activeCreatureId"
|
||||
data-id="mini-character-sheet"
|
||||
:creature-id="activeCreatureId"
|
||||
@click="openCharacterSheetDialog"
|
||||
/>
|
||||
<action-card
|
||||
v-for="action in actions"
|
||||
:key="action._id"
|
||||
:model="action"
|
||||
:data-id="action._id"
|
||||
:active="activeActionId === action._id"
|
||||
:targets="targets"
|
||||
@click="clickProperty({_id: action._id})"
|
||||
@activate="activeActionId = action._id"
|
||||
@deactivate="activeActionId = undefined; targets = [];"
|
||||
/>
|
||||
</v-row>
|
||||
</v-container>
|
||||
@@ -95,9 +112,29 @@ import addCreaturesToTabletop from '/imports/api/tabletop/methods/addCreaturesTo
|
||||
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.js';
|
||||
import TabletopActionCards from '/imports/client/ui/tabletop/TabletopActionCards.vue';
|
||||
import MiniCharacterSheet from '/imports/client/ui/creature/character/MiniCharacterSheet.vue';
|
||||
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';
|
||||
|
||||
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: {
|
||||
@@ -118,10 +155,16 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeCreature: undefined,
|
||||
activeCreatureId: undefined,
|
||||
activeActionId: undefined,
|
||||
targets: [],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeCreatureId(id) {
|
||||
this.$root.$emit('active-tabletop-character-change', id);
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'tabletop'() {
|
||||
@@ -132,28 +175,20 @@ export default {
|
||||
return Creatures.find({ tabletop: this.model._id });
|
||||
},
|
||||
actions(){
|
||||
return getProperties(this.activeCreature, 'action').map(a => {
|
||||
delete a.summary;
|
||||
return a;
|
||||
});
|
||||
return getProperties(this.activeCreatureId, { type: 'action', actionType: { $ne: 'event'} });
|
||||
},
|
||||
editPermission(){
|
||||
try {
|
||||
assertEditPermission(this.activeCreature, Meteor.userId());
|
||||
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;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
actions(){
|
||||
return getProperties(this.activeCreature, 'action').map(a => {
|
||||
delete a.summary;
|
||||
return a;
|
||||
});
|
||||
},
|
||||
editPermission(){
|
||||
try {
|
||||
assertEditPermission(this.activeCreature, Meteor.userId());
|
||||
assertEditPermission(this.activeCreatureId, Meteor.userId());
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
@@ -184,7 +219,7 @@ export default {
|
||||
component: 'character-sheet-dialog',
|
||||
elementId: 'mini-character-sheet',
|
||||
data: {
|
||||
creatureId: this.activeCreature,
|
||||
creatureId: this.activeCreatureId,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -199,7 +234,7 @@ export default {
|
||||
if (!event.deltaY) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.scrollLeft += event.deltaY + event.deltaX;
|
||||
event.currentTarget.scrollLeft += event.deltaY;
|
||||
event.preventDefault();
|
||||
},
|
||||
untarget(id){
|
||||
@@ -208,7 +243,7 @@ export default {
|
||||
this.targets.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -220,12 +255,11 @@ export default {
|
||||
width: 100px;
|
||||
margin: 4px;
|
||||
}
|
||||
.action-row > .v-card {
|
||||
.action-row > div {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
max-height: 320px;
|
||||
height: 120px;
|
||||
width: 200px;
|
||||
margin: 4px;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<template lang="html">
|
||||
<v-card
|
||||
style="height: 100px; width: 70px;"
|
||||
:color="active ? 'accent' : ''"
|
||||
hover
|
||||
@mouseover="hover = true"
|
||||
class="tabletop-creature-card"
|
||||
:class="{ active }"
|
||||
:hover="hasClickListener"
|
||||
:elevation="active ? 8 : 2"
|
||||
@mouseover="() => { if (hasClickListener) hover = true; }"
|
||||
@mouseleave="hover = false"
|
||||
@click="$emit('click')"
|
||||
v-on="hasClickListener ? {click: () => $emit('click')} : {}"
|
||||
>
|
||||
<v-progress-linear
|
||||
v-if="model.variables.hitPoints"
|
||||
:value="model.variables.hitPoints.value * 100 / model.variables.hitPoints.total"
|
||||
v-if="variables.hitPoints"
|
||||
:value="variables.hitPoints.value * 100 / variables.hitPoints.total"
|
||||
/>
|
||||
<v-img
|
||||
:src="model.picture"
|
||||
@@ -22,20 +24,27 @@
|
||||
{{ model.name }}
|
||||
</div>
|
||||
<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
|
||||
style="position: fixed;"
|
||||
@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 CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
|
||||
import CardHighlight from '/imports/client/ui/components/CardHighlight.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CardHighlight,
|
||||
@@ -47,10 +56,23 @@ export default {
|
||||
},
|
||||
active: Boolean,
|
||||
targeted: Boolean,
|
||||
showTargetBtn: Boolean,
|
||||
},
|
||||
data(){return {
|
||||
hover: false,
|
||||
}},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasClickListener() {
|
||||
return this.$listeners && !!this.$listeners.click;
|
||||
},
|
||||
},
|
||||
// @ts-ignore
|
||||
meteor: {
|
||||
variables() {
|
||||
return CreatureVariables.findOne({ _creatureId: this.model._id }) || {};
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -63,3 +85,9 @@ export default {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="css">
|
||||
.tabletop-creature-card .v-btn {
|
||||
transition: all .3s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
<template lang="html">
|
||||
<log-component
|
||||
:logs="logs"
|
||||
:edit-permission="context.editPermission"
|
||||
show-name
|
||||
@submit="submit"
|
||||
<character-log
|
||||
:tabletop-id="tabletopId"
|
||||
:creature-id="activeCreatureId"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import insertTabletopLog from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import LogComponent from '/imports/client/ui/log/LogComponent.vue';
|
||||
import { insertTabletopLog } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import CharacterLog from '/imports/client/ui/log/CharacterLog.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LogComponent,
|
||||
CharacterLog,
|
||||
},
|
||||
inject: {
|
||||
context: {
|
||||
@@ -24,29 +21,19 @@ export default {
|
||||
props: {
|
||||
tabletopId: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
logs() {
|
||||
return CreatureLogs.find({
|
||||
tabletopId: this.tabletopId,
|
||||
}, {
|
||||
sort: {date: -1},
|
||||
limit: 50
|
||||
data() {
|
||||
return {
|
||||
activeCreatureId: undefined,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on('active-tabletop-character-change', (id) => {
|
||||
this.activeCreatureId = id;
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submit(){
|
||||
insertTabletopLog.call({
|
||||
content: this.logContent,
|
||||
tabletopId: this.tabletopId,
|
||||
}, (error) => {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,11 +1,89 @@
|
||||
<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 { MapControls } from '/imports/api/tabletop/three/OrbitControls.js';
|
||||
|
||||
const maps = [
|
||||
{
|
||||
name: 'first map',
|
||||
position: { x: 0, y: 0 },
|
||||
width: 5,
|
||||
height: 5,
|
||||
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, 1, 1000 );
|
||||
orthoCam.position.z = 5
|
||||
const activeCamera = orthoCam;
|
||||
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({canvas: this.$refs.map});
|
||||
|
||||
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.MeshBasicMaterial({ map: texture });
|
||||
material.map.needsUpdate = true;
|
||||
const plane = new THREE.Mesh(new THREE.PlaneGeometry(map.width, map.height), material);
|
||||
plane.overdraw = true;
|
||||
scene.add(plane);
|
||||
});
|
||||
|
||||
/*
|
||||
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>
|
||||
.tabletop-map {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,8 @@ import Tabletops from '/imports/api/tabletop/Tabletops.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
|
||||
import { loadCreature } from '/imports/api/engine/loadCreatures.js';
|
||||
|
||||
Meteor.publish('tabletops', function () {
|
||||
var userId = this.userId;
|
||||
@@ -22,6 +24,7 @@ Meteor.publish('tabletop', function(tabletopId){
|
||||
return [];
|
||||
}
|
||||
this.autorun(function () {
|
||||
const self = this;
|
||||
let tabletopCursor = Tabletops.find({
|
||||
_id: tabletopId,
|
||||
$or: [
|
||||
@@ -33,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
|
||||
@@ -40,25 +44,31 @@ Meteor.publish('tabletop', function(tabletopId){
|
||||
tabletop: tabletopId,
|
||||
}, {
|
||||
fields: {
|
||||
_id: 1,
|
||||
name: 1,
|
||||
picture: 1,
|
||||
avatarPicture: 1,
|
||||
variables: 1,
|
||||
tabletop: 1,
|
||||
initiativeRoll: 1,
|
||||
},
|
||||
});
|
||||
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.0.id': {$in: creatureIds},
|
||||
'ancestors.id': { $in: creatureIds },
|
||||
removed: { $ne: true },
|
||||
});
|
||||
const logs = CreatureLogs.find({
|
||||
tabletopId,
|
||||
}, {
|
||||
limit: 50,
|
||||
limit: 100,
|
||||
sort: { date: -1 },
|
||||
});
|
||||
return [ tabletopCursor, creatureSummaries, properties, logs]
|
||||
return [tabletopCursor, creatureSummaries, properties, logs, variables]
|
||||
})
|
||||
});
|
||||
|
||||
12
app/package-lock.json
generated
12
app/package-lock.json
generated
@@ -3426,15 +3426,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"signal-exit": {
|
||||
<<<<<<< HEAD
|
||||
"version": "3.0.2",
|
||||
"resolved": "",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
=======
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||
>>>>>>> version-2
|
||||
},
|
||||
"simpl-schema": {
|
||||
"version": "1.13.1",
|
||||
@@ -3748,9 +3742,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"three": {
|
||||
"version": "0.139.2",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.139.2.tgz",
|
||||
"integrity": "sha512-gV7q7QY8rogu7HLFZR9cWnOQAUedUhu2WXAnpr2kdXZP9YDKsG/0ychwQvWkZN5PlNw9mv5MoCTin6zNTXoONg=="
|
||||
"version": "0.148.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.148.0.tgz",
|
||||
"integrity": "sha512-8uzVV+qhTPi0bOFs/3te3RW6hb3urL8jYEl6irjCWo/l6sr8MPNMcClFev/MMYeIxr0gmDcoXTy/8LXh/LXkfw=="
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
|
||||
@@ -52,8 +52,9 @@
|
||||
"simpl-schema": "^1.13.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speakingurl": "^14.0.1",
|
||||
"three": "^0.148.0",
|
||||
"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",
|
||||
|
||||
Reference in New Issue
Block a user