diff --git a/app/imports/api/campaign/Campaigns.js b/app/imports/api/campaign/Campaigns.js
deleted file mode 100644
index 2e9b8e49..00000000
--- a/app/imports/api/campaign/Campaigns.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import SimpleSchema from 'simpl-schema';
-
-let Campaigns = new Mongo.Collection('campaigns');
-
-let CampaignSchema = new SimpleSchema({
-
-});
-
-Campaigns.attachSchema(CampaignSchema);
-
-export default Campaigns;
diff --git a/app/imports/api/campaign/Encounter.js b/app/imports/api/campaign/Encounter.js
deleted file mode 100644
index 679f86ec..00000000
--- a/app/imports/api/campaign/Encounter.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import SimpleSchema from 'simpl-schema';
-
-let Encounters = new Mongo.Collection('encounters');
-
-const CreatureInitiativeSchema = new SimpleSchema({
- name: {
- type: String,
- optional: true,
- },
- initiativeRoll: {
- type: SimpleSchema.Integer,
- },
-});
-
-const InitiativeSchema = new SimpleSchema({
- // An ordered list of all creatures in the initiative order
- creatures: {
- type: Array,
- defaultValue: [],
- },
- 'creatures.$': {
- type: CreatureInitiativeSchema,
- },
- active: {
- type: Boolean,
- defaultValue: false,
- },
- roundNumber: {
- type: SimpleSchema.Integer,
- defaultValue: 0,
- },
- initiativeNumber: {
- type: SimpleSchema.Integer,
- optional: true,
- },
-});
-
-// A creature can be in one ecounter at a time.
-// All creatures in an encounter have a shared time and space.
-let EncounterSchema = new SimpleSchema({
- name: {
- type: String,
- optional: true,
- },
- initiative: {
- type: InitiativeSchema,
- defaultValue: {},
- },
-});
-
-Encounters.attachSchema(EncounterSchema);
-
-export default Encounters;
diff --git a/app/imports/api/creature/Creatures.js b/app/imports/api/creature/Creatures.js
index 197941b5..26d82c54 100644
--- a/app/imports/api/creature/Creatures.js
+++ b/app/imports/api/creature/Creatures.js
@@ -5,7 +5,7 @@ import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
-import { getUserTier } from '/imports/api/users/patreon/tiers.js';
+import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
import '/imports/api/creature/removeCreature.js';
import '/imports/api/creature/restCreature.js';
@@ -107,6 +107,17 @@ let CreatureSchema = new SimpleSchema({
defaultValue: {}
},
+ // Tabletop
+ tabletop: {
+ type: String,
+ regEx: SimpleSchema.RegEx.id,
+ optional: true,
+ },
+ initiativeRoll: {
+ type: SimpleSchema.Integer,
+ optional: true,
+ },
+
// Settings
settings: {
type: CreatureSettingsSchema,
@@ -136,11 +147,7 @@ const insertCreature = new ValidatedMethod({
throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature');
}
- let tier = getUserTier(this.userId);
- if (!tier.paidBenefits){
- throw new Meteor.Error('Creatures.methods.insert.denied',
- `The ${tier.name} tier does not allow you to insert a creature`);
- }
+ assertUserHasPaidBenefits(this.userId);
// Create the creature document
let charId = Creatures.insert({
diff --git a/app/imports/api/campaign/Parties.js b/app/imports/api/creature/Parties.js
similarity index 100%
rename from app/imports/api/campaign/Parties.js
rename to app/imports/api/creature/Parties.js
diff --git a/app/imports/api/tabletop/Tabletops.js b/app/imports/api/tabletop/Tabletops.js
new file mode 100644
index 00000000..3e108494
--- /dev/null
+++ b/app/imports/api/tabletop/Tabletops.js
@@ -0,0 +1,189 @@
+import SimpleSchema from 'simpl-schema';
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
+import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
+import Creatures from '/imports/api/creature/Creatures.js';
+
+let Tabletops = new Mongo.Collection('tabletops');
+
+const InitiativeSchema = new SimpleSchema({
+ active: {
+ type: Boolean,
+ defaultValue: false,
+ },
+ roundNumber: {
+ type: SimpleSchema.Integer,
+ defaultValue: 0,
+ },
+ initiativeNumber: {
+ type: SimpleSchema.Integer,
+ optional: true,
+ },
+ activeCreature: {
+ type: String,
+ regEx: SimpleSchema.RegEx.id,
+ optional: true,
+ },
+});
+
+// All creatures in a tabletop have a shared time and space.
+let TabletopSchema = new SimpleSchema({
+ name: {
+ type: String,
+ optional: true,
+ },
+ initiative: {
+ type: InitiativeSchema,
+ defaultValue: {},
+ },
+ gameMaster: {
+ type: String,
+ regEx: SimpleSchema.RegEx.id,
+ },
+ players: {
+ type: Array,
+ defaultValue: [],
+ },
+ 'players.$': {
+ type: String,
+ regEx: SimpleSchema.RegEx.id,
+ },
+});
+
+Tabletops.attachSchema(TabletopSchema);
+
+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');
+ }
+}
+
+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');
+ }
+}
+
+const insertTabletop = new ValidatedMethod({
+
+ name: 'tabletops.insert',
+
+ validate: null,
+
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+
+ run() {
+ if (!this.userId) {
+ throw new Meteor.Error('tabletops.insert.denied',
+ 'You need to be logged in to insert a tabletop');
+ }
+ assertUserHasPaidBenefits(this.userId);
+
+ return Tabletops.insert({
+ gameMaster: this.userId,
+ });
+ },
+
+});
+
+const removeTabletop = new ValidatedMethod({
+
+ name: 'tabletops.remove',
+
+ validate: new SimpleSchema({
+ tabletopId: {
+ type: String,
+ regEx: SimpleSchema.RegEx.id,
+ },
+ }).validator(),
+
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+
+ run({tabletopId}) {
+ if (!this.userId) {
+ throw new Meteor.Error('tabletops.remove.denied',
+ 'You need to be logged in to remove a tabletop');
+ }
+ assertUserHasPaidBenefits(this.userId);
+ assertUserIsTabletopOwner(tabletopId, this.userId);
+ let removed = Tabletops.remove({
+ _id: tabletopId,
+ });
+ Creatures.update({
+ tabletop: tabletopId,
+ }, {
+ $unset: {tabletop: 1},
+ });
+ return removed;
+ },
+
+});
+
+const addCreaturesToTabletop = new ValidatedMethod({
+
+ name: 'tabletops.addCreatures',
+
+ validate: new SimpleSchema({
+ 'creatureIds': {
+ type: Array,
+ },
+ 'creatureIds.$': {
+ type: String,
+ regEx: SimpleSchema.RegEx.id,
+ },
+ tabletopId: {
+ type: String,
+ regEx: SimpleSchema.RegEx.id,
+ },
+ }).validator(),
+
+ mixins: [RateLimiterMixin],
+ 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 Tabletops;
+export { insertTabletop, removeTabletop, addCreaturesToTabletop };
diff --git a/app/imports/api/users/patreon/tiers.js b/app/imports/api/users/patreon/tiers.js
index ccb3df1c..8b16bfec 100644
--- a/app/imports/api/users/patreon/tiers.js
+++ b/app/imports/api/users/patreon/tiers.js
@@ -77,5 +77,13 @@ export function getUserTier(user){
}
}
+export function assertUserHasPaidBenefits(user){
+ let tier = getUserTier(user);
+ if (!tier.paidBenefits){
+ throw new Meteor.Error('Creatures.methods.insert.denied',
+ `The ${tier.name} tier does not allow you to insert a creature`);
+ }
+}
+
export default TIERS;
export { GUEST_TIER };
diff --git a/app/imports/server/publications/characterList.js b/app/imports/server/publications/characterList.js
index ede11228..426019e3 100644
--- a/app/imports/server/publications/characterList.js
+++ b/app/imports/server/publications/characterList.js
@@ -1,5 +1,5 @@
import Creatures from '/imports/api/creature/Creatures.js';
-import Parties from '/imports/api/campaign/Parties.js';
+import Parties from '/imports/api/creature/Parties.js';
Meteor.publish('characterList', function(){
this.autorun(function (){
diff --git a/app/imports/server/publications/index.js b/app/imports/server/publications/index.js
index c58b5496..963baecd 100644
--- a/app/imports/server/publications/index.js
+++ b/app/imports/server/publications/index.js
@@ -5,3 +5,4 @@ import '/imports/server/publications/singleCharacter.js';
import '/imports/server/publications/experiences.js';
import '/imports/server/publications/users.js';
import '/imports/server/publications/icons.js';
+import '/imports/server/publications/tabletops.js';
diff --git a/app/imports/server/publications/tabletops.js b/app/imports/server/publications/tabletops.js
new file mode 100644
index 00000000..7aa30abb
--- /dev/null
+++ b/app/imports/server/publications/tabletops.js
@@ -0,0 +1,51 @@
+import Tabletops from '/imports/api/tabletop/Tabletops.js';
+import Creatures from '/imports/api/creature/Creatures.js';
+
+Meteor.publish('tabletops', function(){
+ var userId = this.userId;
+ if (!userId) {
+ return this.ready();
+ }
+ return Tabletops.find({
+ $or: [
+ {players: userId},
+ {gameMaster: userId},
+ ],
+ });
+});
+
+Meteor.publish('tabletop', function(tabletopId){
+ var userId = this.userId;
+ if (!userId) {
+ return this.ready();
+ }
+ this.autorun(function (){
+ let tabletopCursor = Tabletops.find({
+ _id: tabletopId,
+ $or: [
+ {players: userId},
+ {gameMaster: userId},
+ ]
+ });
+ let tabletop = tabletopCursor.fetch()[0];
+ if (!tabletop){
+ return this.ready();
+ }
+ // Warning, this leaks data to users of the same tabletop who may not have
+ // read permission of this specific creature, so publish as few fields as
+ // possible
+ let creatureSummaries = Creatures.find({
+ tabletop: tabletopId,
+ }, {
+ fields: {
+ name: 1,
+ picture: 1,
+ avatarPicture: 1,
+ variables: 1,
+ tabletop: 1,
+ initiativeRoll: 1,
+ },
+ });
+ return [ tabletopCursor, creatureSummaries]
+ })
+});
diff --git a/app/imports/ui/creature/CreatureListTile.vue b/app/imports/ui/creature/CreatureListTile.vue
new file mode 100644
index 00000000..c6402838
--- /dev/null
+++ b/app/imports/ui/creature/CreatureListTile.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+ {{ model.initial }}
+
+
+
+
+ {{ model.name }}
+
+
+ {{ model.alignment }} {{ model.gender }} {{ model.race }}
+
+
+
+
+
+
+
+
+
diff --git a/app/imports/ui/creature/character/MiniCharacterSheet.vue b/app/imports/ui/creature/character/MiniCharacterSheet.vue
new file mode 100644
index 00000000..19f6b871
--- /dev/null
+++ b/app/imports/ui/creature/character/MiniCharacterSheet.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/app/imports/ui/dialogStack/DialogComponentIndex.js b/app/imports/ui/dialogStack/DialogComponentIndex.js
index 2008373c..783439d7 100644
--- a/app/imports/ui/dialogStack/DialogComponentIndex.js
+++ b/app/imports/ui/dialogStack/DialogComponentIndex.js
@@ -11,6 +11,7 @@ import LibraryEditDialog from '/imports/ui/library/LibraryEditDialog.vue';
import LibraryNodeCreationDialog from '/imports/ui/library/LibraryNodeCreationDialog.vue';
import LibraryNodeDialog from '/imports/ui/library/LibraryNodeDialog.vue';
import MoveLibraryNodeDialog from '/imports/ui/library/MoveLibraryNodeDialog.vue'
+import SelectCreaturesDialog from '/imports/ui/tabletop/SelectCreaturesDialog.vue';
import ShareDialog from '/imports/ui/sharing/ShareDialog.vue';
import TierTooLowDialog from '/imports/ui/user/TierTooLowDialog.vue';
import UsernameDialog from '/imports/ui/user/UsernameDialog.vue';
@@ -29,6 +30,7 @@ export default {
LibraryNodeCreationDialog,
LibraryNodeDialog,
MoveLibraryNodeDialog,
+ SelectCreaturesDialog,
ShareDialog,
TierTooLowDialog,
UsernameDialog,
diff --git a/app/imports/ui/layouts/Sidebar.vue b/app/imports/ui/layouts/Sidebar.vue
index 5c7d4e05..515a136d 100644
--- a/app/imports/ui/layouts/Sidebar.vue
+++ b/app/imports/ui/layouts/Sidebar.vue
@@ -115,7 +115,7 @@
diff --git a/app/imports/ui/pages/Tabletops.vue b/app/imports/ui/pages/Tabletops.vue
new file mode 100644
index 00000000..3a7db28f
--- /dev/null
+++ b/app/imports/ui/pages/Tabletops.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+ {{ tabletop.name || 'Unnamed Tabletop' }}
+
+
+
+
+
+ You don't own or belong to any tabletops yet
+
+
+ add
+
+
+
+
+
+
+
diff --git a/app/imports/ui/router.js b/app/imports/ui/router.js
index d9ca93f9..8e0fc7c5 100644
--- a/app/imports/ui/router.js
+++ b/app/imports/ui/router.js
@@ -20,6 +20,9 @@ import InviteSuccess from '/imports/ui/pages/InviteSuccess.vue' ;
import InviteError from '/imports/ui/pages/InviteError.vue' ;
import NotImplemented from '/imports/ui/pages/NotImplemented.vue';
import PatreonLevelTooLow from '/imports/ui/pages/PatreonLevelTooLow.vue';
+import Tabletops from '/imports/ui/pages/Tabletops.vue';
+import Tabletop from '/imports/ui/pages/Tabletop.vue';
+import TabletopToolbar from '/imports/ui/tabletop/TabletopToolbar.vue';
let userSubscription = Meteor.subscribe('user');
@@ -143,6 +146,19 @@ RouterFactory.configure(factory => {
meta: {
title: 'Character Sheet',
},
+ },{
+ path: '/tabletops',
+ name: 'tabletops',
+ component: Tabletops,
+ beforeEnter: ensureLoggedIn,
+ },{
+ path: '/tabletop/:id',
+ name: 'tabletop',
+ components: {
+ default: Tabletop,
+ toolbar: TabletopToolbar,
+ },
+ beforeEnter: ensureLoggedIn,
},{
path: '/friends',
components: {
diff --git a/app/imports/ui/tabletop/SelectCreaturesDialog.vue b/app/imports/ui/tabletop/SelectCreaturesDialog.vue
new file mode 100644
index 00000000..5b69cda5
--- /dev/null
+++ b/app/imports/ui/tabletop/SelectCreaturesDialog.vue
@@ -0,0 +1,63 @@
+
+
+
+ Add Characters
+
+
+
+
+
+
+
+ Add characters
+
+
+
+
+
+
+
+
diff --git a/app/imports/ui/tabletop/TabletopActionCards.vue b/app/imports/ui/tabletop/TabletopActionCards.vue
new file mode 100644
index 00000000..f7a38809
--- /dev/null
+++ b/app/imports/ui/tabletop/TabletopActionCards.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
diff --git a/app/imports/ui/tabletop/TabletopComponent.vue b/app/imports/ui/tabletop/TabletopComponent.vue
new file mode 100644
index 00000000..ef482ef1
--- /dev/null
+++ b/app/imports/ui/tabletop/TabletopComponent.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+ add
+
+
+ Add creature
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/imports/ui/tabletop/TabletopCreatureCard.vue b/app/imports/ui/tabletop/TabletopCreatureCard.vue
new file mode 100644
index 00000000..24081ede
--- /dev/null
+++ b/app/imports/ui/tabletop/TabletopCreatureCard.vue
@@ -0,0 +1,24 @@
+
+
+
+ {{ model.name }}
+
+
+
+
+
+
diff --git a/app/imports/ui/tabletop/TabletopLog.vue b/app/imports/ui/tabletop/TabletopLog.vue
new file mode 100644
index 00000000..c93fabb6
--- /dev/null
+++ b/app/imports/ui/tabletop/TabletopLog.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/app/imports/ui/tabletop/TabletopMap.vue b/app/imports/ui/tabletop/TabletopMap.vue
new file mode 100644
index 00000000..e95e1939
--- /dev/null
+++ b/app/imports/ui/tabletop/TabletopMap.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/app/imports/ui/tabletop/TabletopToolbar.vue b/app/imports/ui/tabletop/TabletopToolbar.vue
new file mode 100644
index 00000000..d5b9913f
--- /dev/null
+++ b/app/imports/ui/tabletop/TabletopToolbar.vue
@@ -0,0 +1,29 @@
+
+
+
+
+ Tabletop
+
+
+
+
+
+
+
+