diff --git a/app/imports/api/creature/log/CreatureLogs.js b/app/imports/api/creature/log/CreatureLogs.js index a37569bd..453012e4 100644 --- a/app/imports/api/creature/log/CreatureLogs.js +++ b/app/imports/api/creature/log/CreatureLogs.js @@ -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,24 +186,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 { @@ -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 }; 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/engine/loadCreatures.js b/app/imports/api/engine/loadCreatures.js index aa036a37..829033cb 100644 --- a/app/imports/api/engine/loadCreatures.js +++ b/app/imports/api/engine/loadCreatures.js @@ -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) { @@ -43,7 +45,7 @@ export function getSingleProperty(creatureId, propertyId) { const prop = CreatureProperties.findOne({ _id: propertyId, 'ancestors.id': creatureId, - 'removed': {$ne: true}, + 'removed': { $ne: true }, }, { sort: { order: 1 }, }); @@ -61,7 +63,7 @@ export function getProperties(creatureId) { // console.time(`Cache miss on creature properties: ${creatureId}`) const props = CreatureProperties.find({ 'ancestors.id': creatureId, - 'removed': {$ne: true}, + 'removed': { $ne: true }, }, { sort: { order: 1 }, }).fetch(); @@ -73,7 +75,7 @@ export function getPropertiesOfType(creatureId, propType) { if (loadedCreatures.has(creatureId)) { const creature = loadedCreatures.get(creatureId); const props = [] - for (const prop of creature.properties.values()){ + for (const prop of creature.properties.values()) { if (prop.type === propType) { props.push(prop); } @@ -97,7 +99,7 @@ export function getCreature(creatureId) { if (loadedCreatures.has(creatureId)) { const loadedCreature = loadedCreatures.get(creatureId); const creature = loadedCreature.creature; - if (creature) { + if (creature) { const cloneCreature = EJSON.clone(creature); return cloneCreature; } @@ -118,7 +120,7 @@ export function getVariables(creatureId) { } } // console.time(`Cache miss on variables: ${creatureId}`); - const variables = CreatureVariables.findOne({_creatureId: creatureId}); + const variables = CreatureVariables.findOne({ _creatureId: creatureId }); // console.timeEnd(`Cache miss on variables: ${creatureId}`); return variables; } @@ -148,7 +150,7 @@ export function getProperyAncestors(creatureId, propertyId) { // Fetch from database return CreatureProperties.find({ _id: { $in: ancestorIds }, - removed: {$ne: true}, + removed: { $ne: true }, }, { sort: { order: 1 }, }).fetch(); @@ -164,7 +166,7 @@ export function getPropertyDecendants(creatureId, propertyId) { if (loadedCreatures.has(creatureId)) { const creature = loadedCreatures.get(creatureId); const props = []; - for(const prop of creature.properties.values()){ + for (const prop of creature.properties.values()) { if (prop.ancestors[expectedAncestorPostition]?.id === propertyId) { props.push(prop); } @@ -216,7 +218,7 @@ class LoadedCreature { compute(); }, }); - + // Observe the creature itself self.creatureObserver = Creatures.find({ _id: creatureId, @@ -239,7 +241,7 @@ class LoadedCreature { self.variablesObserver = CreatureVariables.find({ _creatureId: creatureId, }, { - fields: { _creatureId: 0}, + fields: { _creatureId: 0 }, }).observeChanges({ added(id, fields) { fields._id = id; 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/api/tabletop/map/TabletopMap.vue b/app/imports/api/tabletop/map/TabletopMap.vue deleted file mode 100644 index 0c87c5ae..00000000 --- a/app/imports/api/tabletop/map/TabletopMap.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/imports/client/ui/components/RollPopup.vue b/app/imports/client/ui/components/RollPopup.vue index d7cb25b8..0aaca754 100644 --- a/app/imports/client/ui/components/RollPopup.vue +++ b/app/imports/client/ui/components/RollPopup.vue @@ -12,8 +12,8 @@ @@ -90,6 +90,7 @@ export default { type: Number, default: undefined, }, + noClick: Boolean, }, data(){return { open: false, diff --git a/app/imports/client/ui/creature/character/CharacterSheet.vue b/app/imports/client/ui/creature/character/CharacterSheet.vue index 75f4cbd8..568ebe79 100644 --- a/app/imports/client/ui/creature/character/CharacterSheet.vue +++ b/app/imports/client/ui/creature/character/CharacterSheet.vue @@ -30,17 +30,17 @@
@@ -68,11 +68,10 @@ -
@@ -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 { diff --git a/app/imports/client/ui/dialogStack/DialogComponentIndex.js b/app/imports/client/ui/dialogStack/DialogComponentIndex.js index 471d3b6a..bf084614 100644 --- a/app/imports/client/ui/dialogStack/DialogComponentIndex.js +++ b/app/imports/client/ui/dialogStack/DialogComponentIndex.js @@ -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, diff --git a/app/imports/client/ui/layouts/Sidebar.vue b/app/imports/client/ui/layouts/Sidebar.vue index 9037b346..e8bfe92c 100644 --- a/app/imports/client/ui/layouts/Sidebar.vue +++ b/app/imports/client/ui/layouts/Sidebar.vue @@ -80,6 +80,7 @@ export default { components: { CreatureFolderList }, + // @ts-ignore meteor: { $subscribe: { 'characterList': [], 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/properties/components/actions/AttributeConsumedView.vue b/app/imports/client/ui/properties/components/actions/AttributeConsumedView.vue index 26571332..9ae84918 100644 --- a/app/imports/client/ui/properties/components/actions/AttributeConsumedView.vue +++ b/app/imports/client/ui/properties/components/actions/AttributeConsumedView.vue @@ -4,16 +4,23 @@ :class="insufficient && 'error--text'" >
- {{ model.quantity && model.quantity.value }} + {{ model.quantity.value }}
{{ model.statName || model.variableName }}
+
+ ({{ model.available }}) +
diff --git a/app/imports/client/ui/properties/components/actions/ItemConsumedView.vue b/app/imports/client/ui/properties/components/actions/ItemConsumedView.vue index f0eae75a..11be31fb 100644 --- a/app/imports/client/ui/properties/components/actions/ItemConsumedView.vue +++ b/app/imports/client/ui/properties/components/actions/ItemConsumedView.vue @@ -27,28 +27,30 @@ :color="model.itemColor" />
- - + {{ quantity }}
-
- - +
- Select item - + {{ model.itemName }} +
+
+ ({{ model.available }}) +
+ +
+ Select item
- + + + + diff --git a/app/imports/client/ui/tabletop/TabletopActionCards.vue b/app/imports/client/ui/tabletop/TabletopActionCards.vue deleted file mode 100644 index 5fe2efd8..00000000 --- a/app/imports/client/ui/tabletop/TabletopActionCards.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - - - diff --git a/app/imports/client/ui/tabletop/TabletopComponent.vue b/app/imports/client/ui/tabletop/TabletopComponent.vue index 603c8fbd..1e9bbf0a 100644 --- a/app/imports/client/ui/tabletop/TabletopComponent.vue +++ b/app/imports/client/ui/tabletop/TabletopComponent.vue @@ -19,16 +19,31 @@ @@ -67,22 +82,24 @@ @@ -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,8 +155,14 @@ export default { }, data() { return { - activeCreature: undefined, - targets: [], + activeCreatureId: undefined, + activeActionId: undefined, + targets: [], + } + }, + watch: { + activeCreatureId(id) { + this.$root.$emit('active-tabletop-character-change', id); } }, meteor: { @@ -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); } } - } + }, } @@ -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; } diff --git a/app/imports/client/ui/tabletop/TabletopCreatureCard.vue b/app/imports/client/ui/tabletop/TabletopCreatureCard.vue index 656249e6..76e5c3d7 100644 --- a/app/imports/client/ui/tabletop/TabletopCreatureCard.vue +++ b/app/imports/client/ui/tabletop/TabletopCreatureCard.vue @@ -1,15 +1,17 @@ @@ -63,3 +85,9 @@ export default { text-overflow: ellipsis; } + + diff --git a/app/imports/client/ui/tabletop/TabletopLog.vue b/app/imports/client/ui/tabletop/TabletopLog.vue index 2dcd16ea..7f077e31 100644 --- a/app/imports/client/ui/tabletop/TabletopLog.vue +++ b/app/imports/client/ui/tabletop/TabletopLog.vue @@ -1,20 +1,17 @@ diff --git a/app/imports/client/ui/tabletop/TabletopMap.vue b/app/imports/client/ui/tabletop/TabletopMap.vue index 605da292..37546d7e 100644 --- a/app/imports/client/ui/tabletop/TabletopMap.vue +++ b/app/imports/client/ui/tabletop/TabletopMap.vue @@ -1,11 +1,89 @@ -