diff --git a/app/imports/api/creature/log/CreatureLogs.js b/app/imports/api/creature/log/CreatureLogs.js index 522d1f2e..d8063cb5 100644 --- a/app/imports/api/creature/log/CreatureLogs.js +++ b/app/imports/api/creature/log/CreatureLogs.js @@ -125,33 +125,16 @@ 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 }] }; } if (!log.content?.length) return; log.date = new Date(); - if (creature) log.tabletopId = creature.tabletop; + if (tabletopId) log.tabletopId = tabletopId; + if (creature && creature.tabletop) log.tabletopId = creature.tabletop; + console.log(log.tabletopId); // Insert it let id = CreatureLogs.insert(log); if (Meteor.isServer) { @@ -185,24 +168,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 { @@ -243,11 +241,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 }; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js index 663922a5..86d23e80 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js @@ -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; } }); diff --git a/app/imports/api/log/LogComponent.vue b/app/imports/api/log/LogComponent.vue deleted file mode 100644 index d3036749..00000000 --- a/app/imports/api/log/LogComponent.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/imports/client/ui/log/CharacterLog.vue b/app/imports/client/ui/log/CharacterLog.vue index f4e9582a..6f32d184 100644 --- a/app/imports/client/ui/log/CharacterLog.vue +++ b/app/imports/client/ui/log/CharacterLog.vue @@ -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" /> @@ -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; } }, - }, - methods: { - submit(input){ - logRoll.call({ - roll: input, - creatureId: this.creatureId, - }, (error) => { - if (error) console.error(error); - }); + 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(){ diff --git a/app/imports/client/ui/tabletop/TabletopActionCard.vue b/app/imports/client/ui/tabletop/TabletopActionCard.vue index b7ddecd9..92b6e0bd 100644 --- a/app/imports/client/ui/tabletop/TabletopActionCard.vue +++ b/app/imports/client/ui/tabletop/TabletopActionCard.vue @@ -53,7 +53,11 @@ :loading="doActionLoading" :disabled="model.insufficientResources || !context.editPermission || !!targetingError" v-on="active ? { - 'click.stop': doAction + click: e => { + if (!active) return; + e.stopPropagation(); + doAction({}); + } } : {}" > @@ -224,12 +228,14 @@ 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){ + 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; @@ -280,6 +286,7 @@ export default { } }, error => { this.doActionLoading = false; + this.$emit('deactivate'); if (error) { console.error(error); snackbar({ text: error.reason }); diff --git a/app/imports/client/ui/tabletop/TabletopComponent.vue b/app/imports/client/ui/tabletop/TabletopComponent.vue index 0b5fe967..1e9bbf0a 100644 --- a/app/imports/client/ui/tabletop/TabletopComponent.vue +++ b/app/imports/client/ui/tabletop/TabletopComponent.vue @@ -28,20 +28,22 @@ :model="creature" :active="activeCreatureId === creature._id" :targeted="targets.includes(creature._id)" - :show-target-btn="!!activeActionId" - @click="() => { - if (activeActionId) { - if (targets.includes(creature._id)) { - untarget(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 { - targets.push(creature._id); + activeCreatureId = creature._id; + targets = []; + activeActionId = undefined; } - } else { - activeCreatureId = creature._id; - targets = []; - activeActionId = undefined; } - }" + } : {}" @target="targets.push(creature._id)" @untarget="untarget(creature._id)" /> @@ -95,6 +97,7 @@ :key="action._id" :model="action" :active="activeActionId === action._id" + :targets="targets" @activate="activeActionId = action._id" @deactivate="activeActionId = undefined; targets = [];" /> @@ -157,6 +160,11 @@ export default { targets: [], } }, + watch: { + activeCreatureId(id) { + this.$root.$emit('active-tabletop-character-change', id); + } + }, meteor: { $subscribe: { 'tabletop'() { @@ -169,6 +177,15 @@ export default { 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()); diff --git a/app/imports/client/ui/tabletop/TabletopCreatureCard.vue b/app/imports/client/ui/tabletop/TabletopCreatureCard.vue index 51677e49..76e5c3d7 100644 --- a/app/imports/client/ui/tabletop/TabletopCreatureCard.vue +++ b/app/imports/client/ui/tabletop/TabletopCreatureCard.vue @@ -2,12 +2,12 @@ { if (hasClickListener) hover = true; }" @mouseleave="hover = false" - @click="$emit('click')" + v-on="hasClickListener ? {click: () => $emit('click')} : {}" > - - {{ targeted ? 'mdi-target' : 'mdi-target' }} - + + + {{ targeted ? 'mdi-target' : 'mdi-target' }} + + @@ -60,6 +62,11 @@ export default { hover: false, } }, + computed: { + hasClickListener() { + return this.$listeners && !!this.$listeners.click; + }, + }, // @ts-ignore meteor: { variables() { diff --git a/app/imports/client/ui/tabletop/TabletopLog.vue b/app/imports/client/ui/tabletop/TabletopLog.vue index bdb671e6..7f077e31 100644 --- a/app/imports/client/ui/tabletop/TabletopLog.vue +++ b/app/imports/client/ui/tabletop/TabletopLog.vue @@ -1,17 +1,17 @@ - diff --git a/app/imports/server/publications/tabletops.js b/app/imports/server/publications/tabletops.js index 88e8bd2e..58f10a48 100644 --- a/app/imports/server/publications/tabletops.js +++ b/app/imports/server/publications/tabletops.js @@ -66,7 +66,7 @@ Meteor.publish('tabletop', function (tabletopId) { const logs = CreatureLogs.find({ tabletopId, }, { - limit: 50, + limit: 100, sort: { date: -1 }, }); return [tabletopCursor, creatureSummaries, properties, logs, variables]