Moved tabletop characters to left side of the screen
This commit is contained in:
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -5,6 +5,7 @@
|
||||
"blackbox",
|
||||
"Crits",
|
||||
"cyrb",
|
||||
"denormalized",
|
||||
"EJSON",
|
||||
"healthbar",
|
||||
"healthbars",
|
||||
@@ -13,7 +14,8 @@
|
||||
"ngraph",
|
||||
"ostrio",
|
||||
"snackbars",
|
||||
"Spellcasting",
|
||||
"uncomputed",
|
||||
"walkdown"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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<Creature>('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);
|
||||
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
}
|
||||
@@ -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 } });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
|
||||
export interface Colored {
|
||||
color?: string,
|
||||
}
|
||||
|
||||
const ColorSchema = new SimpleSchema({
|
||||
color: {
|
||||
type: String,
|
||||
@@ -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;
|
||||
@@ -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: {
|
||||
@@ -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<Tabletop>('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;
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
53
app/imports/api/tabletop/methods/updateTabletop.js
Normal file
53
app/imports/api/tabletop/methods/updateTabletop.js
Normal file
@@ -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;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
});
|
||||
|
||||
58
app/imports/client/ui/components/HexagonProgress.vue
Normal file
58
app/imports/client/ui/components/HexagonProgress.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div
|
||||
class="hexagon-progress"
|
||||
:style="fillStyle"
|
||||
>
|
||||
<div class="hexagon-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
percent: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fillStyle() {
|
||||
return {
|
||||
'--p': `${this.percent}%`
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.hexagon-progress {
|
||||
position: relative;
|
||||
clip-path: polygon(
|
||||
50% 0%,
|
||||
100% 25%,
|
||||
100% 75%,
|
||||
50% 100%,
|
||||
0% 75%,
|
||||
0% 25%
|
||||
);
|
||||
background: conic-gradient(red var(--p),#0000 0);
|
||||
background-color: #5e1010; /* adjust the color as needed */
|
||||
}
|
||||
|
||||
.hexagon-content {
|
||||
position: absolute;
|
||||
inset: 4px;
|
||||
background-color: #252525;
|
||||
clip-path: polygon(
|
||||
50% 0%,
|
||||
100% 25%,
|
||||
100% 75%,
|
||||
50% 100%,
|
||||
0% 75%,
|
||||
0% 25%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<script lang="js">
|
||||
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
|
||||
import CharacterSheet from '/imports/client/ui/creature/character/CharacterSheet.vue';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -68,6 +68,36 @@
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-row>
|
||||
<div class="d-flex flex-column align-start ml-2">
|
||||
<div class="d-flex flex-column">
|
||||
<tabletop-creature-list-item
|
||||
v-for="creature in creatures"
|
||||
:key="creature._id"
|
||||
:model="creature"
|
||||
:title="creature.name"
|
||||
:active="activeCreatureId === creature._id"
|
||||
:targeted="targets.includes(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 {
|
||||
activeCreatureId = creature._id;
|
||||
targets = [];
|
||||
activeActionId = undefined;
|
||||
}
|
||||
}
|
||||
} : {}"
|
||||
@target="targets.push(creature._id)"
|
||||
@untarget="untarget(creature._id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-container>
|
||||
<v-footer
|
||||
inset
|
||||
@@ -96,13 +126,14 @@
|
||||
import addCreaturesToTabletop from '/imports/api/tabletop/methods/addCreaturesToTabletop';
|
||||
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 Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
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';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import ActionCard from '/imports/client/ui/tabletop/TabletopActionCard.vue';
|
||||
import SelectedCreatureBar from '/imports/client/ui/tabletop/selectedCreatureBar/SelectedCreatureBar.vue';
|
||||
import TabletopCreatureListItem from '/imports/client/ui/tabletop/TabletopCreatureListItem.vue';
|
||||
|
||||
const getProperties = function (creatureId, selector = {}) {
|
||||
return CreatureProperties.find({
|
||||
@@ -129,6 +160,7 @@ export default {
|
||||
ActionCard,
|
||||
MiniCharacterSheet,
|
||||
SelectedCreatureBar,
|
||||
TabletopCreatureListItem,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
@@ -159,7 +191,7 @@ export default {
|
||||
},
|
||||
},
|
||||
creatures(){
|
||||
return Creatures.find({ tabletop: this.model._id });
|
||||
return Creatures.find({ tabletopId: this.model._id });
|
||||
},
|
||||
actions(){
|
||||
return getProperties(this.activeCreatureId, { type: 'action', actionType: { $ne: 'event'} });
|
||||
@@ -196,7 +228,10 @@ export default {
|
||||
tabletopId: this.model._id,
|
||||
creatureIds: charIds,
|
||||
}, error => {
|
||||
if (error) snackbar(error.message);
|
||||
if (error) {
|
||||
console.error(error)
|
||||
snackbar({ text: error.message || error.toString() });
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
48
app/imports/client/ui/tabletop/TabletopCreatureListItem.vue
Normal file
48
app/imports/client/ui/tabletop/TabletopCreatureListItem.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<hexagon-progress
|
||||
:percent="66"
|
||||
style="z-index: 1; width: 60px; height: 60px; margin-right: -16px"
|
||||
>
|
||||
<hexagon-progress
|
||||
:percent="30"
|
||||
style="width: 100%; height: 100%"
|
||||
>
|
||||
<v-img
|
||||
contain
|
||||
src="/images/paragons/kira.png"
|
||||
/>
|
||||
</hexagon-progress>
|
||||
</hexagon-progress>
|
||||
<v-card
|
||||
class="flex-grow-1"
|
||||
style="margin: 16px 16px 16px 0px;"
|
||||
>
|
||||
<div style="margin: 8px 8px 8px 24px;">
|
||||
{{ title }}
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import HexagonProgress from '/imports/client/ui/components/HexagonProgress.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HexagonProgress,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Title'
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -135,7 +135,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import TabletopActionCard from '/imports/client/ui/tabletop/TabletopActionCard.vue';
|
||||
import CreatureBarIcon from '/imports/client/ui/tabletop/selectedCreatureBar/CreatureBarIcon.vue';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { serialMap } from '/imports/api/utility/asyncMap';
|
||||
import constant, { ConstantValueType } from '/imports/parser/parseTree/constant';
|
||||
import constant from '/imports/parser/parseTree/constant';
|
||||
import ParseNode from '/imports/parser/parseTree/ParseNode';
|
||||
import ResolveFunction from '/imports/parser/types/ResolveFunction';
|
||||
import MapFunction from '/imports/parser/types/MapFunction';
|
||||
|
||||
@@ -12,8 +12,10 @@ Meteor.publish('tabletops', function () {
|
||||
}
|
||||
return Tabletops.find({
|
||||
$or: [
|
||||
{ owner: userId },
|
||||
{ players: userId },
|
||||
{ gameMaster: userId },
|
||||
{ gameMasters: userId },
|
||||
{ spectators: userId },
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -24,12 +26,15 @@ Meteor.publish('tabletop', function (tabletopId) {
|
||||
return [];
|
||||
}
|
||||
this.autorun(function () {
|
||||
if (!userId) return [];
|
||||
const self = this;
|
||||
let tabletopCursor = Tabletops.find({
|
||||
_id: tabletopId,
|
||||
$or: [
|
||||
{ owner: userId },
|
||||
{ players: userId },
|
||||
{ gameMaster: userId },
|
||||
{ gameMasters: userId },
|
||||
{ spectators: userId },
|
||||
]
|
||||
});
|
||||
let tabletop = tabletopCursor.fetch()[0];
|
||||
@@ -41,17 +46,19 @@ Meteor.publish('tabletop', function (tabletopId) {
|
||||
// read permission of this specific creature, so publish as few fields as
|
||||
// possible
|
||||
let creatureSummaries = Creatures.find({
|
||||
tabletop: tabletopId,
|
||||
tabletopId,
|
||||
}, {
|
||||
fields: {
|
||||
_id: 1,
|
||||
name: 1,
|
||||
picture: 1,
|
||||
avatarPicture: 1,
|
||||
tabletop: 1,
|
||||
tabletopId: 1,
|
||||
initiativeRoll: 1,
|
||||
settings: 1,
|
||||
propCount: 1,
|
||||
},
|
||||
limit: 110,
|
||||
});
|
||||
const creatureIds = creatureSummaries.map(c => c._id);
|
||||
creatureIds.forEach(creatureId => {
|
||||
@@ -59,10 +66,14 @@ Meteor.publish('tabletop', function (tabletopId) {
|
||||
});
|
||||
const variables = CreatureVariables.find({
|
||||
_creatureId: { $in: creatureIds }
|
||||
}, {
|
||||
limit: 110,
|
||||
});
|
||||
let properties = CreatureProperties.find({
|
||||
'ancestors.id': { $in: creatureIds },
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
limit: 10_000,
|
||||
});
|
||||
const logs = CreatureLogs.find({
|
||||
tabletopId,
|
||||
|
||||
Reference in New Issue
Block a user