Added actions to tabletop, used character logs instead.

This commit is contained in:
Stefan Zermatten
2022-04-15 20:26:10 +02:00
parent cbc42f8500
commit 3235d81684
14 changed files with 387 additions and 140 deletions

View File

@@ -4,6 +4,8 @@ import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import {assertEditPermission} from '/imports/api/creature/creatures/creaturePermissions.js';
import {assertUserInTabletop} from '/imports/api/tabletop/methods/shared/tabletopPermissions.js';
import {parse, prettifyParseError} from '/imports/parser/parser.js';
import resolve, { toString } from '/imports/parser/resolve.js';
const PER_CREATURE_LOG_LIMIT = 100;
@@ -40,6 +42,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,
@@ -105,6 +113,7 @@ const insertCreatureLog = new ValidatedMethod({
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
tabletop: 1,
}});
assertEditPermission(creature, this.userId);
// Build the new log
@@ -113,6 +122,25 @@ 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}){
// Build the new log
if (typeof log === 'string'){
@@ -120,12 +148,19 @@ export function insertCreatureLogWork({log, creature, method}){
}
if (!log.content?.length) return;
log.date = new Date();
if (creature) 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;
}
@@ -210,4 +245,4 @@ const logRoll = new ValidatedMethod({
});
export default CreatureLogs;
export { CreatureLogSchema, insertCreatureLog, logRoll};
export { CreatureLogSchema, insertCreatureLog, logRoll, insertTabletopLog};

View File

@@ -214,7 +214,7 @@ if (Meteor.isServer && Meteor.settings.useS3) {
}
} else {
if (Meteor.isServer){
console.log('No S3 details specified, files will be stored in the local filesystem');
// console.log('No S3 details specified, files will be stored in the local filesystem');
}
createS3FilesCollection = function({
collectionName,

View File

@@ -1,6 +1,7 @@
import Tabletops from '/imports/api/tabletop/Tabletops.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import Messages from '/imports/api/tabletop/Messages.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
Meteor.publish('tabletops', function(){
var userId = this.userId;
@@ -47,14 +48,17 @@ Meteor.publish('tabletop', function(tabletopId){
initiativeRoll: 1,
},
});
let recentMessages = Messages.find({
const creatureIds = creatureSummaries.map(c => c._id);
let properties = CreatureProperties.find({
'ancestors.0.id': {$in: creatureIds},
removed: {$ne: true},
});
const logs = CreatureLogs.find({
tabletopId,
}, {
sort: {
timeStamp: -1,
},
limit: 100,
limit: 50,
sort: {date: -1},
});
return [ tabletopCursor, creatureSummaries, recentMessages]
return [ tabletopCursor, creatureSummaries, properties, logs]
})
});

View File

@@ -1,4 +1,4 @@
if<template>
<template>
<div class="character-sheet fill-height">
<v-fade-transition mode="out-in">
<div

View File

@@ -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">

View File

@@ -1,6 +1,7 @@
const AddCreaturePropertyDialog = () => import('/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue');
const ArchiveDialog = () => import('/imports/ui/creature/archive/ArchiveDialog.vue');
const CastSpellWithSlotDialog = () => import('/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue');
const CharacterSheetDialog = () => import('/imports/ui/tabletop/CharacterSheetDialog.vue');
const CreatureFormDialog = () => import('/imports/ui/creature/CreatureFormDialog.vue');
const CreaturePropertyCreationDialog = () => import('/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue');
const CreaturePropertyDialog = () => import('/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue');
@@ -28,6 +29,7 @@ export default {
AddCreaturePropertyDialog,
ArchiveDialog,
CastSpellWithSlotDialog,
CharacterSheetDialog,
CreatureFormDialog,
CreaturePropertyCreationDialog,
CreaturePropertyDialog,

View File

@@ -1,48 +1,20 @@
<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"
/>
</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>
<log-component
:logs="logs"
:edit-permission="editPermission"
@submit="submit"
/>
</template>
<script lang="js">
import CreatureLogs, { logRoll } from '/imports/api/creature/log/CreatureLogs.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { parse, prettifyParseError } from '/imports/parser/parser.js';
import resolve, { toString } from '/imports/parser/resolve.js';
import LogEntry from '/imports/ui/log/LogEntry.vue';
import LogComponent from '/imports/ui/log/LogComponent.vue';
export default {
components: {
LogEntry,
LogComponent,
},
props: {
creatureId: {
@@ -50,49 +22,14 @@ export default {
required: true,
},
},
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;
submit(input){
logRoll.call({
roll: this.input,
roll: input,
creatureId: this.creatureId,
}, (error) => {
if (error) console.error(error);
});
this.input = '';
},
},
meteor: {

View File

@@ -0,0 +1,98 @@
<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/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>

View File

@@ -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>

View File

@@ -0,0 +1,55 @@
<template>
<dialog-base>
<template #replace-toolbar>
<v-tabs
v-if="creature && creature.settings"
v-model="tab"
:color="$vuetify.theme.themes.dark.primary"
>
<v-tab>
Stats
</v-tab>
<v-tab>
Features
</v-tab>
<v-tab>
Inventory
</v-tab>
<v-tab v-if="!creature.settings.hideSpellsTab">
Spells
</v-tab>
<v-tab>
Character
</v-tab>
<v-tab v-if="creature.settings.showTreeTab">
Tree
</v-tab>
</v-tabs>
</template>
<character-sheet
show-menu-button
:creature-id="creatureId"
/>
</dialog-base>
</template>
<script lang="js">
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import CharacterSheet from '/imports/ui/creature/character/CharacterSheet.vue';
export default {
components: {
DialogBase,
CharacterSheet,
},
props: {
creatureId: {
type: String,
required: true,
},
},
data(){return {
tab: 0,
}},
}
</script>

View File

@@ -41,12 +41,6 @@ export default {
actions(){
return getProperties(this.creatureId, 'action');
},
attacks(){
return getProperties(this.creatureId, 'attack');
},
spells(){
return getProperties(this.creatureId, 'spell');
},
}
}
</script>

View File

@@ -7,32 +7,68 @@
dense
class="initiative-row"
style="flex-wrap: nowrap; overflow-x: auto;"
@wheel="transformScroll($event)"
>
<tabletop-creature-card
v-for="creature in creatures"
:key="creature._id"
:model="creature"
:active="activeCreature === creature._id"
@click="activeCreature = creature._id"
/>
<v-card
class="layout column justify-center align-center"
style="height: 150px; min-width: 120px;"
data-id="select-creatures"
hover
@click="addCreature"
<div
class="layout column ma-1"
style="flex-grow: 0;"
>
<div class="flex layout justify-center align-center">
<v-icon>mdi-plus</v-icon>
</div>
<v-card-title>
Add<br>creature
</v-card-title>
</v-card>
<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-row>
<tabletop-map class="play-area" />
<section class="action-row">
<mini-character-sheet />
<tabletop-action-cards />
</section>
<v-row>
<tabletop-map class="play-area" />
</v-row>
<v-footer
app
inset
class="pa-0"
style="background: none; box-shadow: none;"
>
<v-container fluid>
<v-row
dense
class="action-row"
style="flex-wrap: nowrap; overflow-x: auto;"
@wheel="transformScroll($event)"
>
<mini-character-sheet
v-if="activeCreature"
data-id="mini-character-sheet"
@click="openCharacterSheetDialog"
/>
<action-card
v-for="action in actions"
:key="action._id"
:model="action"
:data-id="action._id"
@click="clickProperty({_id: action._id})"
/>
</v-row>
</v-container>
</v-footer>
</v-container>
</template>
@@ -41,15 +77,29 @@ import addCreaturesToTabletop from '/imports/api/tabletop/methods/addCreaturesTo
import TabletopCreatureCard from '/imports/ui/tabletop/TabletopCreatureCard.vue';
import TabletopMap from '/imports/ui/tabletop/TabletopMap.vue';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import TabletopActionCards from '/imports/ui/tabletop/TabletopActionCards.vue';
import MiniCharacterSheet from '/imports/ui/creature/character/MiniCharacterSheet.vue';
import snackbar from '/imports/ui/components/snackbars/SnackbarQueue.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import ActionCard from '/imports/ui/properties/components/actions/ActionCard.vue';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
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: {
TabletopCreatureCard,
TabletopMap,
TabletopActionCards,
ActionCard,
MiniCharacterSheet,
},
props: {
@@ -58,6 +108,10 @@ export default {
required: true,
},
},
reactiveProvide: {
name: 'context',
include: ['editPermission'],
},
data(){ return {
activeCreature: undefined,
}},
@@ -70,6 +124,20 @@ export default {
creatures(){
return Creatures.find({tabletop: this.model._id});
},
actions(){
return getProperties(this.activeCreature, 'action').map(a => {
delete a.summary;
return a;
});
},
editPermission(){
try {
assertEditPermission(this.activeCreature, Meteor.userId());
return true;
} catch (e) {
return false;
}
},
},
methods: {
addCreature(){
@@ -89,6 +157,29 @@ export default {
});
},
});
},
openCharacterSheetDialog(){
this.$store.commit('pushDialogStack', {
component: 'character-sheet-dialog',
elementId: 'mini-character-sheet',
data: {
creatureId: this.activeCreature,
},
});
},
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.deltaX;
event.preventDefault();
}
}
}
@@ -97,8 +188,17 @@ export default {
<style lang="css" scoped>
.initiative-row > .v-card {
flex-grow: 0;
flex-shrink: 0;
height: 162px;
width: 100px;
margin: 4px;
}
.action-row > .v-card {
flex-grow: 0;
flex-shrink: 0;
max-height: 320px;
width: 200px;
margin: 4px;
overflow-y: hidden;
}
</style>

View File

@@ -1,6 +1,11 @@
<template lang="html">
<v-card
style="height: 150px; min-width: 120px;"
:color="active ? 'primary' : ''"
hover
@mouseover="hover = true"
@mouseleave="hover = false"
@click="$emit('click')"
>
<v-img
:src="model.picture"
@@ -12,17 +17,26 @@
>
{{ model.name }}
</div>
<card-highlight :active="hover" />
</v-card>
</template>
<script lang="js">
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
export default {
components: {
CardHighlight,
},
props: {
model: {
type: Object,
required: true,
},
active: Boolean,
},
data(){return {
hover: false,
}},
}
</script>

View File

@@ -1,51 +1,50 @@
<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>
<log-component
:logs="logs"
:edit-permission="context.editPermission"
show-name
@submit="submit"
/>
</template>
<script lang="js">
import Messages, { sendMessage } from '/imports/api/tabletop/Messages.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import insertTabletopLog from '/imports/api/creature/log/CreatureLogs.js';
import LogComponent from '/imports/ui/log/LogComponent.vue';
export default {
components: {
LogComponent,
},
inject: {
context: {
default: {},
},
},
props: {
tabletopId: {
type: String,
required: true,
},
},
data(){ return {
messageContent: '',
}},
meteor: {
messages() {
return Messages.find({
logs() {
return CreatureLogs.find({
tabletopId: this.tabletopId,
}, {
sort: {
timeStamp: 1,
},
sort: {date: -1},
limit: 50
});
}
},
},
methods: {
sendMessage(){
sendMessage.call({
content: this.messageContent,
submit(){
insertTabletopLog.call({
content: this.logContent,
tabletopId: this.tabletopId,
}, (error) => {
if (error) console.error(error);
});
this.messageContent = '';
},
},
}