From 08640f2bf2e845a673ef55d5e0704a1ce8f02bee Mon Sep 17 00:00:00 2001 From: Thaum Rystra <9525416+ThaumRystra@users.noreply.github.com> Date: Fri, 12 Apr 2024 17:05:20 +0200 Subject: [PATCH] Moved tabletop characters to left side of the screen --- .vscode/settings.json | 4 +- .../creatures/{Creatures.js => Creatures.ts} | 89 +- .../writeComputation/writeErrors.js | 9 - .../writeErrorsAndPropCount.js | 9 + app/imports/api/engine/computeCreature.js | 4 +- app/imports/api/parenting/organizeMethods.js | 2 +- .../{ColorSchema.js => ColorSchema.ts} | 4 + .../properties/subSchemas/DeathSavesSchema.js | 26 - .../{SharingSchema.js => SharingSchema.ts} | 14 +- .../tabletop/{Tabletops.js => Tabletops.ts} | 56 +- .../methods/addCreaturesToTabletop.js | 10 +- .../api/tabletop/methods/insertTabletop.js | 23 +- .../methods/removeCreatureFromTabletop.js | 53 + .../api/tabletop/methods/removeTabletop.js | 5 +- .../methods/shared/tabletopPermissions.js | 25 - .../methods/shared/tabletopPermissions.ts | 41 + .../api/tabletop/methods/updateTabletop.js | 53 + .../api/tabletop/three/OrbitControls.js | 1253 ----------------- app/imports/api/users/patreon/tiers.js | 9 + .../client/ui/components/HexagonProgress.vue | 58 + app/imports/client/ui/pages/Tabletops.vue | 5 +- .../ui/tabletop/CharacterSheetDialog.vue | 2 +- .../client/ui/tabletop/TabletopComponent.vue | 41 +- .../ui/tabletop/TabletopCreatureListItem.vue | 48 + .../SelectedCreatureBar.vue | 2 +- app/imports/parser/parseTree/array.ts | 2 +- app/imports/server/publications/tabletops.js | 19 +- 27 files changed, 496 insertions(+), 1370 deletions(-) rename app/imports/api/creature/creatures/{Creatures.js => Creatures.ts} (68%) delete mode 100644 app/imports/api/engine/computation/writeComputation/writeErrors.js create mode 100644 app/imports/api/engine/computation/writeComputation/writeErrorsAndPropCount.js rename app/imports/api/properties/subSchemas/{ColorSchema.js => ColorSchema.ts} (84%) delete mode 100644 app/imports/api/properties/subSchemas/DeathSavesSchema.js rename app/imports/api/sharing/{SharingSchema.js => SharingSchema.ts} (69%) rename app/imports/api/tabletop/{Tabletops.js => Tabletops.ts} (53%) create mode 100644 app/imports/api/tabletop/methods/removeCreatureFromTabletop.js delete mode 100644 app/imports/api/tabletop/methods/shared/tabletopPermissions.js create mode 100644 app/imports/api/tabletop/methods/shared/tabletopPermissions.ts create mode 100644 app/imports/api/tabletop/methods/updateTabletop.js delete mode 100644 app/imports/api/tabletop/three/OrbitControls.js create mode 100644 app/imports/client/ui/components/HexagonProgress.vue create mode 100644 app/imports/client/ui/tabletop/TabletopCreatureListItem.vue diff --git a/.vscode/settings.json b/.vscode/settings.json index be0e7dab..8e771ec1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "blackbox", "Crits", "cyrb", + "denormalized", "EJSON", "healthbar", "healthbars", @@ -13,7 +14,8 @@ "ngraph", "ostrio", "snackbars", + "Spellcasting", "uncomputed", "walkdown" ] -} +} \ No newline at end of file diff --git a/app/imports/api/creature/creatures/Creatures.js b/app/imports/api/creature/creatures/Creatures.ts similarity index 68% rename from app/imports/api/creature/creatures/Creatures.js rename to app/imports/api/creature/creatures/Creatures.ts index da152451..d80ce112 100644 --- a/app/imports/api/creature/creatures/Creatures.js +++ b/app/imports/api/creature/creatures/Creatures.ts @@ -1,13 +1,65 @@ import SimpleSchema from 'simpl-schema'; -import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema' -import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema'; -import SharingSchema from '/imports/api/sharing/SharingSchema'; +import ColorSchema, { Colored } from '/imports/api/properties/subSchemas/ColorSchema'; +import SharingSchema, { Shared } from '/imports/api/sharing/SharingSchema'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; -//set up the collection for creatures -let Creatures = new Mongo.Collection('creatures'); +export type Creature = Colored & Shared & { + // Strings + _id: string, + name?: string, + alignment?: string, + gender?: string, + picture?: string, + avatarPicture?: string, -let CreatureSettingsSchema = new SimpleSchema({ + // Libraries + allowedLibraries: string[], + allowedLibraryCollections: string[], + + // Stats that are computed and denormalized outside of recomputation + denormalizedStats: { + xp: number, + milestoneLevels: number, + }, + propCount: number, + // Does the character need a recompute? + dirty?: boolean, + // Version of computation engine that was last used to compute this creature + computeVersion?: string, + type: 'pc' | 'npc' | 'monster', + computeErrors: { + type: string, + details?: any, + }[], + + // Tabletop + tabletopId?: string, + initiativeRoll?: number, + tabletopSettings?: { + iconGroups: { + name?: string, + iconIds: string[], + }[], + }, + + settings: { + useVariantEncumbrance?: true, + hideSpellcasting?: true, + hideRestButtons?: true, + swapStatAndModifier?: true, + hideUnusedStats?: true, + showTreeTab?: true, + hideSpellsTab?: true, + hideCalculationErrors?: true, + hitDiceResetMultiplier?: number, + discordWebhook?: string, + }, +}; + +//set up the collection for creatures +const Creatures = new Mongo.Collection('creatures'); + +const CreatureSettingsSchema = new SimpleSchema({ //slowed down by carrying too much? useVariantEncumbrance: { type: Boolean, @@ -62,7 +114,7 @@ let CreatureSettingsSchema = new SimpleSchema({ }, }); -let IconGroupSchema = new SimpleSchema({ +const IconGroupSchema = new SimpleSchema({ name: { type: String, max: STORAGE_LIMITS.name, @@ -79,7 +131,7 @@ let IconGroupSchema = new SimpleSchema({ }, }); -let CreatureTabletopSettingsSchema = new SimpleSchema({ +const CreatureTabletopSettingsSchema = new SimpleSchema({ iconGroups: { type: Array, defaultValue: [], @@ -90,7 +142,7 @@ let CreatureTabletopSettingsSchema = new SimpleSchema({ }, }); -let CreatureSchema = new SimpleSchema({ +const CreatureSchema = new SimpleSchema({ // Strings name: { type: String, @@ -139,11 +191,6 @@ let CreatureSchema = new SimpleSchema({ regEx: SimpleSchema.RegEx.Id, }, - // Mechanics - deathSave: { - type: deathSaveSchema, - defaultValue: {}, - }, // Stats that are computed and denormalised outside of recomputation denormalizedStats: { type: Object, @@ -159,6 +206,10 @@ let CreatureSchema = new SimpleSchema({ type: SimpleSchema.Integer, defaultValue: 0, }, + propCount: { + type: SimpleSchema.Integer, + defaultValue: 0, + }, // Does the character need a recompute? dirty: { type: Boolean, @@ -174,11 +225,6 @@ let CreatureSchema = new SimpleSchema({ defaultValue: 'pc', allowedValues: ['pc', 'npc', 'monster'], }, - damageMultipliers: { - type: Object, - blackbox: true, - defaultValue: {} - }, computeErrors: { type: Array, optional: true, @@ -196,9 +242,9 @@ let CreatureSchema = new SimpleSchema({ }, // Tabletop - tabletop: { + tabletopId: { type: String, - regEx: SimpleSchema.RegEx.id, + regEx: SimpleSchema.RegEx.Id, optional: true, }, initiativeRoll: { @@ -220,6 +266,7 @@ let CreatureSchema = new SimpleSchema({ CreatureSchema.extend(ColorSchema); CreatureSchema.extend(SharingSchema); +//@ts-expect-error attachSchema not defined Creatures.attachSchema(CreatureSchema); diff --git a/app/imports/api/engine/computation/writeComputation/writeErrors.js b/app/imports/api/engine/computation/writeComputation/writeErrors.js deleted file mode 100644 index f3088fcb..00000000 --- a/app/imports/api/engine/computation/writeComputation/writeErrors.js +++ /dev/null @@ -1,9 +0,0 @@ -import Creatures from '/imports/api/creature/creatures/Creatures'; - -export default function (creatureId, errors = []) { - if (errors.length) { - Creatures.update(creatureId, { $set: { computeErrors: errors } }); - } else { - Creatures.update(creatureId, { $unset: { computeErrors: 1 } }); - } -} diff --git a/app/imports/api/engine/computation/writeComputation/writeErrorsAndPropCount.js b/app/imports/api/engine/computation/writeComputation/writeErrorsAndPropCount.js new file mode 100644 index 00000000..383ad2d3 --- /dev/null +++ b/app/imports/api/engine/computation/writeComputation/writeErrorsAndPropCount.js @@ -0,0 +1,9 @@ +import Creatures from '/imports/api/creature/creatures/Creatures'; + +export default function writeErrorsAndPropCount(creatureId, errors = [], propCount) { + if (errors.length) { + Creatures.update(creatureId, { $set: { computeErrors: errors, propCount } }); + } else { + Creatures.update(creatureId, { $set: { propCount }, $unset: { computeErrors: 1 } }); + } +} diff --git a/app/imports/api/engine/computeCreature.js b/app/imports/api/engine/computeCreature.js index 0a685924..94b5debe 100644 --- a/app/imports/api/engine/computeCreature.js +++ b/app/imports/api/engine/computeCreature.js @@ -2,7 +2,7 @@ import buildCreatureComputation from './computation/buildCreatureComputation'; import computeCreatureComputation from './computation/computeCreatureComputation'; import writeAlteredProperties from './computation/writeComputation/writeAlteredProperties'; import writeScope from './computation/writeComputation/writeScope'; -import writeErrors from './computation/writeComputation/writeErrors'; +import writeErrorsAndPropCount from './computation/writeComputation/writeErrorsAndPropCount'; export default async function computeCreature(creatureId) { if (Meteor.isClient) return; @@ -32,7 +32,7 @@ async function computeComputation(computation, creatureId) { console.error(logError); } finally { checkPropertyCount(computation) - writeErrors(creatureId, computation.errors); + writeErrorsAndPropCount(creatureId, computation.errors, computation.props.length); } } diff --git a/app/imports/api/parenting/organizeMethods.js b/app/imports/api/parenting/organizeMethods.js index 848fc9c9..31d93fce 100644 --- a/app/imports/api/parenting/organizeMethods.js +++ b/app/imports/api/parenting/organizeMethods.js @@ -4,7 +4,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RefSchema } from '/imports/api/parenting/ChildSchema'; import { assertDocEditPermission, assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { compact } from 'lodash'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; import { fetchDocByRefAsync, getCollectionByName, moveDocBetweenRoots, moveDocWithinRoot } from '/imports/api/parenting/parentingFunctions'; const moveBetweenRoots = new ValidatedMethod({ diff --git a/app/imports/api/properties/subSchemas/ColorSchema.js b/app/imports/api/properties/subSchemas/ColorSchema.ts similarity index 84% rename from app/imports/api/properties/subSchemas/ColorSchema.js rename to app/imports/api/properties/subSchemas/ColorSchema.ts index 0597b336..34fd807f 100644 --- a/app/imports/api/properties/subSchemas/ColorSchema.js +++ b/app/imports/api/properties/subSchemas/ColorSchema.ts @@ -1,5 +1,9 @@ import SimpleSchema from 'simpl-schema'; +export interface Colored { + color?: string, +} + const ColorSchema = new SimpleSchema({ color: { type: String, diff --git a/app/imports/api/properties/subSchemas/DeathSavesSchema.js b/app/imports/api/properties/subSchemas/DeathSavesSchema.js deleted file mode 100644 index b6238544..00000000 --- a/app/imports/api/properties/subSchemas/DeathSavesSchema.js +++ /dev/null @@ -1,26 +0,0 @@ -import SimpleSchema from 'simpl-schema'; - -const DeathSavesSchema = new SimpleSchema({ - pass: { - type: SimpleSchema.Integer, - min: 0, - max: 3, - defaultValue: 0, - }, - fail: { - type: SimpleSchema.Integer, - min: 0, - max: 3, - defaultValue: 0, - }, - canDeathSave: { - type: Boolean, - defaultValue: true, - }, - stable: { - type: Boolean, - defaultValue: false, - }, -}); - -export default DeathSavesSchema; diff --git a/app/imports/api/sharing/SharingSchema.js b/app/imports/api/sharing/SharingSchema.ts similarity index 69% rename from app/imports/api/sharing/SharingSchema.js rename to app/imports/api/sharing/SharingSchema.ts index 695d9674..369974b6 100644 --- a/app/imports/api/sharing/SharingSchema.js +++ b/app/imports/api/sharing/SharingSchema.ts @@ -2,15 +2,25 @@ import SimpleSchema from 'simpl-schema'; import '/imports/api/sharing/sharing'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; -let SharingSchema = new SimpleSchema({ +export interface Shared { + owner: string, + readers: string[], + writers: string[], + public: boolean, + readersCanCopy?: true, +} + +const SharingSchema = new SimpleSchema({ owner: { type: String, regEx: SimpleSchema.RegEx.Id, + //@ts-expect-error index not defined index: 1 }, readers: { type: Array, defaultValue: [], + //@ts-expect-error index not defined index: 1, maxCount: STORAGE_LIMITS.readersCount, }, @@ -21,6 +31,7 @@ let SharingSchema = new SimpleSchema({ writers: { type: Array, defaultValue: [], + //@ts-expect-error index not defined index: 1, maxCount: STORAGE_LIMITS.writersCount, }, @@ -31,6 +42,7 @@ let SharingSchema = new SimpleSchema({ public: { type: Boolean, defaultValue: false, + //@ts-expect-error index not defined index: 1, }, readersCanCopy: { diff --git a/app/imports/api/tabletop/Tabletops.js b/app/imports/api/tabletop/Tabletops.ts similarity index 53% rename from app/imports/api/tabletop/Tabletops.js rename to app/imports/api/tabletop/Tabletops.ts index c3fdc16e..8e53f85d 100644 --- a/app/imports/api/tabletop/Tabletops.js +++ b/app/imports/api/tabletop/Tabletops.ts @@ -1,6 +1,23 @@ import SimpleSchema from 'simpl-schema'; -let Tabletops = new Mongo.Collection('tabletops'); +export type Tabletop = { + name?: string, + description?: string, + imageUrl?: string, + owner: string, + gameMasters: string[], + players: string[], + spectators: string[], + public?: true, + initiative: { + active: boolean, + roundNumber: number, + initiativeNumber?: number, + activeCreature?: string, + }, +} + +const Tabletops = new Mongo.Collection('tabletops'); const InitiativeSchema = new SimpleSchema({ active: { @@ -23,7 +40,7 @@ const InitiativeSchema = new SimpleSchema({ }); // All creatures in a tabletop have a shared time and space. -let TabletopSchema = new SimpleSchema({ +const TabletopSchema = new SimpleSchema({ // Details name: { type: String, @@ -43,13 +60,38 @@ let TabletopSchema = new SimpleSchema({ owner: String, // The owner will need to included in one of these arrays for specific permissions // A user should not appear in more than one of the following arrays - gameMasters: [String], - players: [String], - spectators: [String], + gameMasters: { + type: Array, + defaultValue: [], + }, + 'gameMasters.$': { + type: String, + //@ts-expect-error Index not defined in simpl-schema package + index: 1, + }, + players: { + type: Array, + defaultValue: [], + }, + 'players.$': { + type: String, + //@ts-expect-error Index not defined in simpl-schema package + index: 1, + }, + spectators: { + type: Array, + defaultValue: [], + }, + 'spectators.$': { + type: String, + //@ts-expect-error Index not defined in simpl-schema package + index: 1, + }, // Does everyone else have the spectator permission? public: { type: Boolean, - defaultValue: false, + optional: true, + //@ts-expect-error Index not defined in simpl-schema package index: 1, }, @@ -61,10 +103,12 @@ let TabletopSchema = new SimpleSchema({ }); +//@ts-expect-error attachSchema not defined in simpl-schema package Tabletops.attachSchema(TabletopSchema); import '/imports/api/tabletop/methods/removeTabletop'; import '/imports/api/tabletop/methods/insertTabletop'; +import '/imports/api/tabletop/methods/updateTabletop'; import '/imports/api/tabletop/methods/addCreaturesToTabletop'; export default Tabletops; diff --git a/app/imports/api/tabletop/methods/addCreaturesToTabletop.js b/app/imports/api/tabletop/methods/addCreaturesToTabletop.js index babea992..24f84833 100644 --- a/app/imports/api/tabletop/methods/addCreaturesToTabletop.js +++ b/app/imports/api/tabletop/methods/addCreaturesToTabletop.js @@ -2,7 +2,6 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { assertUserInTabletop } from './shared/tabletopPermissions'; -import { assertAdmin } from '/imports/api/sharing/sharingPermissions'; import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; import Creatures from '/imports/api/creature/creatures/Creatures'; @@ -16,15 +15,16 @@ const addCreaturesToTabletop = new ValidatedMethod({ }, 'creatureIds.$': { type: String, - regEx: SimpleSchema.RegEx.id, + regEx: SimpleSchema.RegEx.Id, }, tabletopId: { type: String, - regEx: SimpleSchema.RegEx.id, + regEx: SimpleSchema.RegEx.Id, }, }).validator(), mixins: [RateLimiterMixin], + // @ts-expect-error Rate limit not defined rateLimit: { numRequests: 10, timeInterval: 5000, @@ -37,16 +37,16 @@ const addCreaturesToTabletop = new ValidatedMethod({ } assertUserHasPaidBenefits(this.userId); assertUserInTabletop(tabletopId, this.userId); - assertAdmin(this.userId); Creatures.update({ _id: { $in: creatureIds }, + // You must have write permission for the creatures you $or: [ { writers: this.userId }, { owner: this.userId }, ], }, { - $set: { tabletop: tabletopId }, + $set: { tabletopId }, }, { multi: true, }); diff --git a/app/imports/api/tabletop/methods/insertTabletop.js b/app/imports/api/tabletop/methods/insertTabletop.js index c213f04e..c0827257 100644 --- a/app/imports/api/tabletop/methods/insertTabletop.js +++ b/app/imports/api/tabletop/methods/insertTabletop.js @@ -1,8 +1,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import Tabletops from '../Tabletops'; -import { assertAdmin } from '/imports/api/sharing/sharingPermissions'; -import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; +import { assertUserHasPaidBenefits, getUserTier } from '/imports/api/users/patreon/tiers'; const insertTabletop = new ValidatedMethod({ @@ -11,8 +10,9 @@ const insertTabletop = new ValidatedMethod({ validate: null, mixins: [RateLimiterMixin], + // @ts-expect-error Rate limit not defined rateLimit: { - numRequests: 5, + numRequests: 2, timeInterval: 5000, }, @@ -22,13 +22,24 @@ const insertTabletop = new ValidatedMethod({ 'You need to be logged in to insert a tabletop'); } assertUserHasPaidBenefits(this.userId); - assertAdmin(this.userId); + let tier = getUserTier(this.userId); + const currentTabletopCount = Tabletops.find({ owner: this.userId }).count(); + + if (tier.tabletopSlots !== -1 && tier.tabletopSlots <= currentTabletopCount) { + throw new Meteor.Error('limit-reached', 'You have reached your maximum number of tabletops'); + } return Tabletops.insert({ - gameMaster: this.userId, + owner: this.userId, + gameMasters: [this.userId], + players: [], + spectators: [], + initiative: { + active: false, + roundNumber: 0, + }, }); }, - }); export default insertTabletop; diff --git a/app/imports/api/tabletop/methods/removeCreatureFromTabletop.js b/app/imports/api/tabletop/methods/removeCreatureFromTabletop.js new file mode 100644 index 00000000..74b241c0 --- /dev/null +++ b/app/imports/api/tabletop/methods/removeCreatureFromTabletop.js @@ -0,0 +1,53 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import { assertUserInTabletop } from './shared/tabletopPermissions'; +import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; +import Creatures from '/imports/api/creature/creatures/Creatures'; + +const addCreaturesToTabletop = new ValidatedMethod({ + + name: 'tabletops.addCreatures', + + validate: new SimpleSchema({ + creatureId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + tabletopId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + + mixins: [RateLimiterMixin], + // @ts-expect-error Rate limit not defined + rateLimit: { + numRequests: 10, + timeInterval: 5000, + }, + + run({ tabletopId, creatureIds }) { + if (!this.userId) { + throw new Meteor.Error('tabletops.addCreatures.denied', + 'You need to be logged in to remove a tabletop'); + } + assertUserHasPaidBenefits(this.userId); + assertUserInTabletop(tabletopId, this.userId); + + Creatures.update({ + _id: { $in: creatureIds }, + $or: [ + { writers: this.userId }, + { owner: this.userId }, + ], + }, { + $set: { tabletop: tabletopId }, + }, { + multi: true, + }); + }, + +}); + +export default addCreaturesToTabletop; diff --git a/app/imports/api/tabletop/methods/removeTabletop.js b/app/imports/api/tabletop/methods/removeTabletop.js index d3ac2d33..c828651f 100644 --- a/app/imports/api/tabletop/methods/removeTabletop.js +++ b/app/imports/api/tabletop/methods/removeTabletop.js @@ -2,7 +2,6 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import Tabletops from '../Tabletops'; -import { assertAdmin } from '/imports/api/sharing/sharingPermissions'; import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; import { assertUserIsTabletopOwner } from './shared/tabletopPermissions'; import Creatures from '/imports/api/creature/creatures/Creatures'; @@ -14,11 +13,12 @@ const removeTabletop = new ValidatedMethod({ validate: new SimpleSchema({ tabletopId: { type: String, - regEx: SimpleSchema.RegEx.id, + regEx: SimpleSchema.RegEx.Id, }, }).validator(), mixins: [RateLimiterMixin], + // @ts-expect-error Rate limit not defined rateLimit: { numRequests: 5, timeInterval: 5000, @@ -31,7 +31,6 @@ const removeTabletop = new ValidatedMethod({ } assertUserHasPaidBenefits(this.userId); assertUserIsTabletopOwner(tabletopId, this.userId); - assertAdmin(this.userId); let removed = Tabletops.remove({ _id: tabletopId, diff --git a/app/imports/api/tabletop/methods/shared/tabletopPermissions.js b/app/imports/api/tabletop/methods/shared/tabletopPermissions.js deleted file mode 100644 index 3a233c95..00000000 --- a/app/imports/api/tabletop/methods/shared/tabletopPermissions.js +++ /dev/null @@ -1,25 +0,0 @@ -import Tabletops from '../../Tabletops'; - -export function assertUserInTabletop(tabletopId, userId) { - let tabletop = Tabletops.findOne(tabletopId); - if (!tabletop) { - throw new Meteor.Error('Tabletop does not exist', - 'No tabletop could be found for the given tabletop id'); - } - if (tabletop.gameMaster !== userId && !tabletop.players.includes(userId)) { - throw new Meteor.Error('Not in tabletop', - 'The user is not the gamemaster or a player in the given tabletop'); - } -} - -export function assertUserIsTabletopOwner(tabletopId, userId) { - let tabletop = Tabletops.findOne(tabletopId); - if (!tabletop) { - throw new Meteor.Error('Tabletop does not exist', - 'No tabletop could be found for the given tabletop id'); - } - if (tabletop.gameMaster !== userId) { - throw new Meteor.Error('Not the owner', - 'The user is not the owner of the given tabletop'); - } -} diff --git a/app/imports/api/tabletop/methods/shared/tabletopPermissions.ts b/app/imports/api/tabletop/methods/shared/tabletopPermissions.ts new file mode 100644 index 00000000..fa0a25f2 --- /dev/null +++ b/app/imports/api/tabletop/methods/shared/tabletopPermissions.ts @@ -0,0 +1,41 @@ +import Tabletops, { Tabletop } from '/imports/api/tabletop/Tabletops'; + +function assertTabletopExists(tabletop: Tabletop | undefined): asserts tabletop is Tabletop { + if (!tabletop) { + throw new Meteor.Error('Tabletop does not exist', + 'No tabletop could be found for the given tabletop id'); + } +} + +export function assertUserInTabletop(tabletopId, userId) { + const tabletop = Tabletops.findOne(tabletopId, { + fields: { gameMasters: 1, players: 1 } + }); + assertTabletopExists(tabletop); + if (tabletop.gameMasters.includes(userId) && !tabletop.players.includes(userId)) { + throw new Meteor.Error('Not in tabletop', + 'The user is not the gamemaster or a player in the given tabletop'); + } +} + +export function assertUserGameMasterOfTabletop(tabletopId, userId) { + const tabletop = Tabletops.findOne(tabletopId, { + fields: { gameMasters: 1 }, + }); + assertTabletopExists(tabletop); + if (tabletop.gameMasters.includes(userId)) { + throw new Meteor.Error('not-game-master', + 'The user is not a game master in the given tabletop'); + } +} + +export function assertUserIsTabletopOwner(tabletopId, userId) { + const tabletop = Tabletops.findOne(tabletopId, { + fields: { owner: 1 }, + }); + assertTabletopExists(tabletop); + if (tabletop.owner === userId) { + throw new Meteor.Error('not-owner', + 'The user is not the owner of the given tabletop'); + } +} diff --git a/app/imports/api/tabletop/methods/updateTabletop.js b/app/imports/api/tabletop/methods/updateTabletop.js new file mode 100644 index 00000000..b7bcc7f0 --- /dev/null +++ b/app/imports/api/tabletop/methods/updateTabletop.js @@ -0,0 +1,53 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import Tabletops from '../Tabletops'; +import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; +import { assertUserIsTabletopOwner } from './shared/tabletopPermissions'; + +const removeTabletop = new ValidatedMethod({ + + name: 'tabletops.update', + + validate({ _id, path }) { + if (!_id) return false; + // Allowed fields + let allowedFields = [ + 'name', + 'description', + 'imageUrl', + ]; + if (!allowedFields.includes(path[0])) { + throw new Meteor.Error('tabletops.update.denied', + 'This field can\'t be updated using this method'); + } + }, + + mixins: [RateLimiterMixin], + // @ts-expect-error Rate limit not defined + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + + run({ _id, path, value }) { + if (!this.userId) { + throw new Meteor.Error('tabletops.remove.denied', + 'You need to be logged in to remove a tabletop'); + } + assertUserHasPaidBenefits(this.userId); + assertUserIsTabletopOwner(_id, this.userId); + + if (value === undefined || value === null) { + Tabletops.update(_id, { + $unset: { [path.join('.')]: 1 }, + }); + } else { + Tabletops.update(_id, { + $set: { [path.join('.')]: value }, + }); + } + }, + +}); + +export default removeTabletop; diff --git a/app/imports/api/tabletop/three/OrbitControls.js b/app/imports/api/tabletop/three/OrbitControls.js deleted file mode 100644 index fb6b613c..00000000 --- a/app/imports/api/tabletop/three/OrbitControls.js +++ /dev/null @@ -1,1253 +0,0 @@ -import { - EventDispatcher, - MOUSE, - Quaternion, - Spherical, - TOUCH, - Vector2, - Vector3 -} from 'three'; - -// https://github.com/mrdoob/three.js/blob/master/examples/jsm/controls/OrbitControls.js -// This set of controls performs orbiting, dollying (zooming), and panning. -// Unlike TrackballControls, it maintains the "up" direction object.up. -// -// Orbit - left mouse / touch: one-finger move -// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish -// Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move - -const _changeEvent = { type: 'change' }; -const _startEvent = { type: 'start' }; -const _endEvent = { type: 'end' }; - -class OrbitControls extends EventDispatcher { - - constructor(object, domElement) { - - super(); - - if (domElement === undefined) console.warn('THREE.OrbitControls: The second parameter "domElement" is now mandatory.'); - if (domElement === document) console.error('THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.'); - - this.object = object; - this.domElement = domElement; - this.domElement.style.touchAction = 'none'; // disable touch scroll - - // Set to false to disable this control - this.enabled = true; - - // "target" sets the location of focus, where the object orbits around - this.target = new Vector3(); - - // How far you can dolly in and out ( PerspectiveCamera only ) - this.minDistance = 0; - this.maxDistance = Infinity; - - // How far you can zoom in and out ( OrthographicCamera only ) - this.minZoom = 0; - this.maxZoom = Infinity; - - // How far you can orbit vertically, upper and lower limits. - // Range is 0 to Math.PI radians. - this.minPolarAngle = 0; // radians - this.maxPolarAngle = Math.PI / 2 // radians - - // How far you can orbit horizontally, upper and lower limits. - // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) - this.minAzimuthAngle = - Infinity; // radians - this.maxAzimuthAngle = Infinity; // radians - - // Set to true to enable damping (inertia) - // If damping is enabled, you must call controls.update() in your animation loop - this.enableDamping = false; - this.dampingFactor = 0.05; - - // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. - // Set to false to disable zooming - this.enableZoom = true; - this.zoomSpeed = 1.0; - - // Set to false to disable rotating - this.enableRotate = true; - this.rotateSpeed = 1.0; - - // Set to false to disable panning - this.enablePan = true; - this.panSpeed = 1.0; - this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up - this.keyPanSpeed = 7.0; // pixels moved per arrow key push - - // Set to true to automatically rotate around the target - // If auto-rotate is enabled, you must call controls.update() in your animation loop - this.autoRotate = false; - this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 - - // The four arrow keys - this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; - - // Mouse buttons - this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; - - // Touch fingers - this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; - - // for reset - this.target0 = this.target.clone(); - this.position0 = this.object.position.clone(); - this.zoom0 = this.object.zoom; - - // the target DOM element for key events - this._domElementKeyEvents = null; - - // - // public methods - // - - this.getPolarAngle = function () { - - return spherical.phi; - - }; - - this.getAzimuthalAngle = function () { - - return spherical.theta; - - }; - - this.getDistance = function () { - - return this.object.position.distanceTo(this.target); - - }; - - this.listenToKeyEvents = function (domElement) { - - domElement.addEventListener('keydown', onKeyDown); - this._domElementKeyEvents = domElement; - - }; - - this.saveState = function () { - - scope.target0.copy(scope.target); - scope.position0.copy(scope.object.position); - scope.zoom0 = scope.object.zoom; - - }; - - this.reset = function () { - - scope.target.copy(scope.target0); - scope.object.position.copy(scope.position0); - scope.object.zoom = scope.zoom0; - - scope.object.updateProjectionMatrix(); - scope.dispatchEvent(_changeEvent); - - scope.update(); - - state = STATE.NONE; - - }; - - // this method is exposed, but perhaps it would be better if we can make it private... - this.update = function () { - - const offset = new Vector3(); - - // so camera.up is the orbit axis - const quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)); - const quatInverse = quat.clone().invert(); - - const lastPosition = new Vector3(); - const lastQuaternion = new Quaternion(); - - const twoPI = 2 * Math.PI; - - return function update() { - - const position = scope.object.position; - - offset.copy(position).sub(scope.target); - - // rotate offset to "y-axis-is-up" space - offset.applyQuaternion(quat); - - // angle from z-axis around y-axis - spherical.setFromVector3(offset); - - if (scope.autoRotate && state === STATE.NONE) { - - rotateLeft(getAutoRotationAngle()); - - } - - if (scope.enableDamping) { - - spherical.theta += sphericalDelta.theta * scope.dampingFactor; - spherical.phi += sphericalDelta.phi * scope.dampingFactor; - - } else { - - spherical.theta += sphericalDelta.theta; - spherical.phi += sphericalDelta.phi; - - } - - // restrict theta to be between desired limits - - let min = scope.minAzimuthAngle; - let max = scope.maxAzimuthAngle; - - if (isFinite(min) && isFinite(max)) { - - if (min < - Math.PI) min += twoPI; else if (min > Math.PI) min -= twoPI; - - if (max < - Math.PI) max += twoPI; else if (max > Math.PI) max -= twoPI; - - if (min <= max) { - - spherical.theta = Math.max(min, Math.min(max, spherical.theta)); - - } else { - - spherical.theta = (spherical.theta > (min + max) / 2) ? - Math.max(min, spherical.theta) : - Math.min(max, spherical.theta); - - } - - } - - // restrict phi to be between desired limits - spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)); - - spherical.makeSafe(); - - - spherical.radius *= scale; - - // restrict radius to be between desired limits - spherical.radius = Math.max(scope.minDistance, Math.min(scope.maxDistance, spherical.radius)); - - // move target to panned location - - if (scope.enableDamping === true) { - - scope.target.addScaledVector(panOffset, scope.dampingFactor); - - } else { - - scope.target.add(panOffset); - - } - - offset.setFromSpherical(spherical); - - // rotate offset back to "camera-up-vector-is-up" space - offset.applyQuaternion(quatInverse); - - position.copy(scope.target).add(offset); - - scope.object.lookAt(scope.target); - - if (scope.enableDamping === true) { - - sphericalDelta.theta *= (1 - scope.dampingFactor); - sphericalDelta.phi *= (1 - scope.dampingFactor); - - panOffset.multiplyScalar(1 - scope.dampingFactor); - - } else { - - sphericalDelta.set(0, 0, 0); - - panOffset.set(0, 0, 0); - - } - - scale = 1; - - // update condition is: - // min(camera displacement, camera rotation in radians)^2 > EPS - // using small-angle approximation cos(x/2) = 1 - x^2 / 8 - - if (zoomChanged || - lastPosition.distanceToSquared(scope.object.position) > EPS || - 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) { - - scope.dispatchEvent(_changeEvent); - - lastPosition.copy(scope.object.position); - lastQuaternion.copy(scope.object.quaternion); - zoomChanged = false; - - return true; - - } - - return false; - - }; - - }(); - - this.dispose = function () { - - scope.domElement.removeEventListener('contextmenu', onContextMenu); - - scope.domElement.removeEventListener('pointerdown', onPointerDown); - scope.domElement.removeEventListener('pointercancel', onPointerCancel); - scope.domElement.removeEventListener('wheel', onMouseWheel); - - scope.domElement.removeEventListener('pointermove', onPointerMove); - scope.domElement.removeEventListener('pointerup', onPointerUp); - - - if (scope._domElementKeyEvents !== null) { - - scope._domElementKeyEvents.removeEventListener('keydown', onKeyDown); - - } - - //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? - - }; - - // - // internals - // - - const scope = this; - - const STATE = { - NONE: - 1, - ROTATE: 0, - DOLLY: 1, - PAN: 2, - TOUCH_ROTATE: 3, - TOUCH_PAN: 4, - TOUCH_DOLLY_PAN: 5, - TOUCH_DOLLY_ROTATE: 6 - }; - - let state = STATE.NONE; - - const EPS = 0.000001; - - // current position in spherical coordinates - const spherical = new Spherical(); - const sphericalDelta = new Spherical(); - - let scale = 1; - const panOffset = new Vector3(); - let zoomChanged = false; - - const rotateStart = new Vector2(); - const rotateEnd = new Vector2(); - const rotateDelta = new Vector2(); - - const panStart = new Vector2(); - const panEnd = new Vector2(); - const panDelta = new Vector2(); - - const dollyStart = new Vector2(); - const dollyEnd = new Vector2(); - const dollyDelta = new Vector2(); - - const pointers = []; - const pointerPositions = {}; - - function getAutoRotationAngle() { - - return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; - - } - - function getZoomScale() { - - return Math.pow(0.95, scope.zoomSpeed); - - } - - function rotateLeft(angle) { - - sphericalDelta.theta -= angle; - - } - - function rotateUp(angle) { - - sphericalDelta.phi -= angle; - - } - - const panLeft = function () { - - const v = new Vector3(); - - return function panLeft(distance, objectMatrix) { - - v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix - v.multiplyScalar(- distance); - - panOffset.add(v); - - }; - - }(); - - const panUp = function () { - - const v = new Vector3(); - - return function panUp(distance, objectMatrix) { - - if (scope.screenSpacePanning === true) { - - v.setFromMatrixColumn(objectMatrix, 1); - - } else { - - v.setFromMatrixColumn(objectMatrix, 0); - v.crossVectors(scope.object.up, v); - - } - - v.multiplyScalar(distance); - - panOffset.add(v); - - }; - - }(); - - // deltaX and deltaY are in pixels; right and down are positive - const pan = function () { - - const offset = new Vector3(); - - return function pan(deltaX, deltaY) { - - const element = scope.domElement; - - if (scope.object.isPerspectiveCamera) { - - // perspective - const position = scope.object.position; - offset.copy(position).sub(scope.target); - let targetDistance = offset.length(); - - // half of the fov is center to top of screen - targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0); - - // we use only clientHeight here so aspect ratio does not distort speed - panLeft(2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix); - panUp(2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix); - - } else if (scope.object.isOrthographicCamera) { - - // orthographic - panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.clientWidth, scope.object.matrix); - panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.clientHeight, scope.object.matrix); - - } else { - - // camera neither orthographic nor perspective - console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.'); - scope.enablePan = false; - - } - - }; - - }(); - - function dollyOut(dollyScale) { - - if (scope.object.isPerspectiveCamera) { - - scale /= dollyScale; - - } else if (scope.object.isOrthographicCamera) { - - scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale)); - scope.object.updateProjectionMatrix(); - zoomChanged = true; - - } else { - - console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); - scope.enableZoom = false; - - } - - } - - function dollyIn(dollyScale) { - - if (scope.object.isPerspectiveCamera) { - - scale *= dollyScale; - - } else if (scope.object.isOrthographicCamera) { - - scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale)); - scope.object.updateProjectionMatrix(); - zoomChanged = true; - - } else { - - console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); - scope.enableZoom = false; - - } - - } - - // - // event callbacks - update the object state - // - - function handleMouseDownRotate(event) { - - rotateStart.set(event.clientX, event.clientY); - - } - - function handleMouseDownDolly(event) { - - dollyStart.set(event.clientX, event.clientY); - - } - - function handleMouseDownPan(event) { - - panStart.set(event.clientX, event.clientY); - - } - - function handleMouseMoveRotate(event) { - - rotateEnd.set(event.clientX, event.clientY); - - rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); - - const element = scope.domElement; - - rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height - - rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); - - rotateStart.copy(rotateEnd); - - scope.update(); - - } - - function handleMouseMoveDolly(event) { - - dollyEnd.set(event.clientX, event.clientY); - - dollyDelta.subVectors(dollyEnd, dollyStart); - - if (dollyDelta.y > 0) { - - dollyOut(getZoomScale()); - - } else if (dollyDelta.y < 0) { - - dollyIn(getZoomScale()); - - } - - dollyStart.copy(dollyEnd); - - scope.update(); - - } - - function handleMouseMovePan(event) { - - panEnd.set(event.clientX, event.clientY); - - panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); - - pan(panDelta.x, panDelta.y); - - panStart.copy(panEnd); - - scope.update(); - - } - - function handleMouseWheel(event) { - - if (event.deltaY < 0) { - - dollyIn(getZoomScale()); - - } else if (event.deltaY > 0) { - - dollyOut(getZoomScale()); - - } - - scope.update(); - - } - - function handleKeyDown(event) { - - let needsUpdate = false; - - switch (event.code) { - - case scope.keys.UP: - pan(0, scope.keyPanSpeed); - needsUpdate = true; - break; - - case scope.keys.BOTTOM: - pan(0, - scope.keyPanSpeed); - needsUpdate = true; - break; - - case scope.keys.LEFT: - pan(scope.keyPanSpeed, 0); - needsUpdate = true; - break; - - case scope.keys.RIGHT: - pan(- scope.keyPanSpeed, 0); - needsUpdate = true; - break; - - } - - if (needsUpdate) { - - // prevent the browser from scrolling on cursor keys - event.preventDefault(); - - scope.update(); - - } - - - } - - function handleTouchStartRotate() { - - if (pointers.length === 1) { - - rotateStart.set(pointers[0].pageX, pointers[0].pageY); - - } else { - - const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); - const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); - - rotateStart.set(x, y); - - } - - } - - function handleTouchStartPan() { - - if (pointers.length === 1) { - - panStart.set(pointers[0].pageX, pointers[0].pageY); - - } else { - - const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); - const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); - - panStart.set(x, y); - - } - - } - - function handleTouchStartDolly() { - - const dx = pointers[0].pageX - pointers[1].pageX; - const dy = pointers[0].pageY - pointers[1].pageY; - - const distance = Math.sqrt(dx * dx + dy * dy); - - dollyStart.set(0, distance); - - } - - function handleTouchStartDollyPan() { - - if (scope.enableZoom) handleTouchStartDolly(); - - if (scope.enablePan) handleTouchStartPan(); - - } - - function handleTouchStartDollyRotate() { - - if (scope.enableZoom) handleTouchStartDolly(); - - if (scope.enableRotate) handleTouchStartRotate(); - - } - - function handleTouchMoveRotate(event) { - - if (pointers.length == 1) { - - rotateEnd.set(event.pageX, event.pageY); - - } else { - - const position = getSecondPointerPosition(event); - - const x = 0.5 * (event.pageX + position.x); - const y = 0.5 * (event.pageY + position.y); - - rotateEnd.set(x, y); - - } - - rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); - - const element = scope.domElement; - - rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height - - rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); - - rotateStart.copy(rotateEnd); - - } - - function handleTouchMovePan(event) { - - if (pointers.length === 1) { - - panEnd.set(event.pageX, event.pageY); - - } else { - - const position = getSecondPointerPosition(event); - - const x = 0.5 * (event.pageX + position.x); - const y = 0.5 * (event.pageY + position.y); - - panEnd.set(x, y); - - } - - panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); - - pan(panDelta.x, panDelta.y); - - panStart.copy(panEnd); - - } - - function handleTouchMoveDolly(event) { - - const position = getSecondPointerPosition(event); - - const dx = event.pageX - position.x; - const dy = event.pageY - position.y; - - const distance = Math.sqrt(dx * dx + dy * dy); - - dollyEnd.set(0, distance); - - dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed)); - - dollyOut(dollyDelta.y); - - dollyStart.copy(dollyEnd); - - } - - function handleTouchMoveDollyPan(event) { - - if (scope.enableZoom) handleTouchMoveDolly(event); - - if (scope.enablePan) handleTouchMovePan(event); - - } - - function handleTouchMoveDollyRotate(event) { - - if (scope.enableZoom) handleTouchMoveDolly(event); - - if (scope.enableRotate) handleTouchMoveRotate(event); - - } - - // - // event handlers - FSM: listen for events and reset state - // - - function onPointerDown(event) { - - if (scope.enabled === false) return; - - if (pointers.length === 0) { - - scope.domElement.setPointerCapture(event.pointerId); - - scope.domElement.addEventListener('pointermove', onPointerMove); - scope.domElement.addEventListener('pointerup', onPointerUp); - - } - - // - - addPointer(event); - - if (event.pointerType === 'touch') { - - onTouchStart(event); - - } else { - - onMouseDown(event); - - } - - } - - function onPointerMove(event) { - - if (scope.enabled === false) return; - - if (event.pointerType === 'touch') { - - onTouchMove(event); - - } else { - - onMouseMove(event); - - } - - } - - function onPointerUp(event) { - - removePointer(event); - - if (pointers.length === 0) { - - scope.domElement.releasePointerCapture(event.pointerId); - - scope.domElement.removeEventListener('pointermove', onPointerMove); - scope.domElement.removeEventListener('pointerup', onPointerUp); - - } - - scope.dispatchEvent(_endEvent); - - state = STATE.NONE; - - } - - function onPointerCancel(event) { - - removePointer(event); - - } - - function onMouseDown(event) { - - let mouseAction; - - switch (event.button) { - - case 0: - - mouseAction = scope.mouseButtons.LEFT; - break; - - case 1: - - mouseAction = scope.mouseButtons.MIDDLE; - break; - - case 2: - - mouseAction = scope.mouseButtons.RIGHT; - break; - - default: - - mouseAction = - 1; - - } - - switch (mouseAction) { - - case MOUSE.DOLLY: - - if (scope.enableZoom === false) return; - - handleMouseDownDolly(event); - - state = STATE.DOLLY; - - break; - - case MOUSE.ROTATE: - - if (event.ctrlKey || event.metaKey || event.shiftKey) { - - if (scope.enablePan === false) return; - - handleMouseDownPan(event); - - state = STATE.PAN; - - } else { - - if (scope.enableRotate === false) return; - - handleMouseDownRotate(event); - - state = STATE.ROTATE; - - } - - break; - - case MOUSE.PAN: - - if (event.ctrlKey || event.metaKey || event.shiftKey) { - - if (scope.enableRotate === false) return; - - handleMouseDownRotate(event); - - state = STATE.ROTATE; - - } else { - - if (scope.enablePan === false) return; - - handleMouseDownPan(event); - - state = STATE.PAN; - - } - - break; - - default: - - state = STATE.NONE; - - } - - if (state !== STATE.NONE) { - - scope.dispatchEvent(_startEvent); - - } - - } - - function onMouseMove(event) { - - if (scope.enabled === false) return; - - switch (state) { - - case STATE.ROTATE: - - if (scope.enableRotate === false) return; - - handleMouseMoveRotate(event); - - break; - - case STATE.DOLLY: - - if (scope.enableZoom === false) return; - - handleMouseMoveDolly(event); - - break; - - case STATE.PAN: - - if (scope.enablePan === false) return; - - handleMouseMovePan(event); - - break; - - } - - } - - function onMouseWheel(event) { - - if (scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE) return; - - event.preventDefault(); - - scope.dispatchEvent(_startEvent); - - handleMouseWheel(event); - - scope.dispatchEvent(_endEvent); - - } - - function onKeyDown(event) { - - if (scope.enabled === false || scope.enablePan === false) return; - - handleKeyDown(event); - - } - - function onTouchStart(event) { - - trackPointer(event); - - switch (pointers.length) { - - case 1: - - switch (scope.touches.ONE) { - - case TOUCH.ROTATE: - - if (scope.enableRotate === false) return; - - handleTouchStartRotate(); - - state = STATE.TOUCH_ROTATE; - - break; - - case TOUCH.PAN: - - if (scope.enablePan === false) return; - - handleTouchStartPan(); - - state = STATE.TOUCH_PAN; - - break; - - default: - - state = STATE.NONE; - - } - - break; - - case 2: - - switch (scope.touches.TWO) { - - case TOUCH.DOLLY_PAN: - - if (scope.enableZoom === false && scope.enablePan === false) return; - - handleTouchStartDollyPan(); - - state = STATE.TOUCH_DOLLY_PAN; - - break; - - case TOUCH.DOLLY_ROTATE: - - if (scope.enableZoom === false && scope.enableRotate === false) return; - - handleTouchStartDollyRotate(); - - state = STATE.TOUCH_DOLLY_ROTATE; - - break; - - default: - - state = STATE.NONE; - - } - - break; - - default: - - state = STATE.NONE; - - } - - if (state !== STATE.NONE) { - - scope.dispatchEvent(_startEvent); - - } - - } - - function onTouchMove(event) { - - trackPointer(event); - - switch (state) { - - case STATE.TOUCH_ROTATE: - - if (scope.enableRotate === false) return; - - handleTouchMoveRotate(event); - - scope.update(); - - break; - - case STATE.TOUCH_PAN: - - if (scope.enablePan === false) return; - - handleTouchMovePan(event); - - scope.update(); - - break; - - case STATE.TOUCH_DOLLY_PAN: - - if (scope.enableZoom === false && scope.enablePan === false) return; - - handleTouchMoveDollyPan(event); - - scope.update(); - - break; - - case STATE.TOUCH_DOLLY_ROTATE: - - if (scope.enableZoom === false && scope.enableRotate === false) return; - - handleTouchMoveDollyRotate(event); - - scope.update(); - - break; - - default: - - state = STATE.NONE; - - } - - } - - function onContextMenu(event) { - - if (scope.enabled === false) return; - - event.preventDefault(); - - } - - function addPointer(event) { - - pointers.push(event); - - } - - function removePointer(event) { - - delete pointerPositions[event.pointerId]; - - for (let i = 0; i < pointers.length; i++) { - - if (pointers[i].pointerId == event.pointerId) { - - pointers.splice(i, 1); - return; - - } - - } - - } - - function trackPointer(event) { - - let position = pointerPositions[event.pointerId]; - - if (position === undefined) { - - position = new Vector2(); - pointerPositions[event.pointerId] = position; - - } - - position.set(event.pageX, event.pageY); - - } - - function getSecondPointerPosition(event) { - - const pointer = (event.pointerId === pointers[0].pointerId) ? pointers[1] : pointers[0]; - - return pointerPositions[pointer.pointerId]; - - } - - // - - scope.domElement.addEventListener('contextmenu', onContextMenu); - - scope.domElement.addEventListener('pointerdown', onPointerDown); - scope.domElement.addEventListener('pointercancel', onPointerCancel); - scope.domElement.addEventListener('wheel', onMouseWheel, { passive: false }); - - // force an update at start - - this.update(); - - } - -} - - -// This set of controls performs orbiting, dollying (zooming), and panning. -// Unlike TrackballControls, it maintains the "up" direction object.up. -// This is very similar to OrbitControls, another set of touch behavior -// -// Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate -// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish -// Pan - left mouse, or arrow keys / touch: one-finger move - -class MapControls extends OrbitControls { - - constructor(object, domElement) { - - super(object, domElement); - - this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up - - this.mouseButtons.LEFT = MOUSE.PAN; - this.mouseButtons.RIGHT = MOUSE.ROTATE; - - this.touches.ONE = TOUCH.PAN; - this.touches.TWO = TOUCH.DOLLY_ROTATE; - - } - -} - -export { OrbitControls, MapControls }; diff --git a/app/imports/api/users/patreon/tiers.js b/app/imports/api/users/patreon/tiers.js index 9451cbdd..88a5cb2e 100644 --- a/app/imports/api/users/patreon/tiers.js +++ b/app/imports/api/users/patreon/tiers.js @@ -9,6 +9,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 0, invites: 0, characterSlots: 5, + tabletopSlots: 0, fileStorage: 50, paidBenefits: false, }, { @@ -16,6 +17,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 100, invites: 0, characterSlots: 5, + tabletopSlots: 0, fileStorage: 50, paidBenefits: false, }, { @@ -23,6 +25,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 300, invites: 0, characterSlots: 5, + tabletopSlots: 0, fileStorage: 50, paidBenefits: false, }, { @@ -31,6 +34,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 500, invites: 0, characterSlots: 20, + tabletopSlots: 4, fileStorage: 200, paidBenefits: true, }, { @@ -39,6 +43,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 1000, invites: 2, characterSlots: 50, + tabletopSlots: 10, fileStorage: 500, paidBenefits: true, }, { @@ -47,6 +52,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 2000, invites: 5, characterSlots: 120, + tabletopSlots: 24, fileStorage: 1000, paidBenefits: true, }, { @@ -55,6 +61,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 5000, invites: 15, characterSlots: -1, // Unlimited characters + tabletopSlots: -1, // Unlimited tabletops fileStorage: 2000, paidBenefits: true, }, @@ -66,6 +73,7 @@ const GUEST_TIER = Object.freeze({ guest: true, invites: 0, characterSlots: 20, + tabletopSlots: 4, fileStorage: 200, paidBenefits: true, }); @@ -76,6 +84,7 @@ const PATREON_DISABLED_TIER = Object.freeze({ name: 'Outlander', invites: 0, characterSlots: -1, // Can have infinitely many characters + tabletopSlots: -1, // Infinite tabletops fileStorage: 1000000, // 1TB file storage paidBenefits: true, }); diff --git a/app/imports/client/ui/components/HexagonProgress.vue b/app/imports/client/ui/components/HexagonProgress.vue new file mode 100644 index 00000000..500e1fc4 --- /dev/null +++ b/app/imports/client/ui/components/HexagonProgress.vue @@ -0,0 +1,58 @@ + + + + + \ No newline at end of file diff --git a/app/imports/client/ui/pages/Tabletops.vue b/app/imports/client/ui/pages/Tabletops.vue index da6d4c30..b6b9fd07 100644 --- a/app/imports/client/ui/pages/Tabletops.vue +++ b/app/imports/client/ui/pages/Tabletops.vue @@ -58,7 +58,10 @@ export default { addTabletop(){ this.addTabletopLoading = true; insertTabletop.call(error => { - if (error) snackbar(error.message); + if (error) { + console.error(error) + snackbar({ text: error.reason || error.message || error.toString() }); + } this.addTabletopLoading = false; }); } diff --git a/app/imports/client/ui/tabletop/CharacterSheetDialog.vue b/app/imports/client/ui/tabletop/CharacterSheetDialog.vue index 76618c43..898c3030 100644 --- a/app/imports/client/ui/tabletop/CharacterSheetDialog.vue +++ b/app/imports/client/ui/tabletop/CharacterSheetDialog.vue @@ -59,7 +59,7 @@ + + \ No newline at end of file diff --git a/app/imports/client/ui/tabletop/selectedCreatureBar/SelectedCreatureBar.vue b/app/imports/client/ui/tabletop/selectedCreatureBar/SelectedCreatureBar.vue index 532d2e76..0e8cb681 100644 --- a/app/imports/client/ui/tabletop/selectedCreatureBar/SelectedCreatureBar.vue +++ b/app/imports/client/ui/tabletop/selectedCreatureBar/SelectedCreatureBar.vue @@ -135,7 +135,7 @@