Moved tabletop characters to left side of the screen

This commit is contained in:
Thaum Rystra
2024-04-12 17:05:20 +02:00
parent 4793b34a55
commit 08640f2bf2
27 changed files with 496 additions and 1370 deletions

View File

@@ -5,6 +5,7 @@
"blackbox", "blackbox",
"Crits", "Crits",
"cyrb", "cyrb",
"denormalized",
"EJSON", "EJSON",
"healthbar", "healthbar",
"healthbars", "healthbars",
@@ -13,7 +14,8 @@
"ngraph", "ngraph",
"ostrio", "ostrio",
"snackbars", "snackbars",
"Spellcasting",
"uncomputed", "uncomputed",
"walkdown" "walkdown"
] ]
} }

View File

@@ -1,13 +1,65 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema' import ColorSchema, { Colored } from '/imports/api/properties/subSchemas/ColorSchema';
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema'; import SharingSchema, { Shared } from '/imports/api/sharing/SharingSchema';
import SharingSchema from '/imports/api/sharing/SharingSchema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
//set up the collection for creatures export type Creature = Colored & Shared & {
let Creatures = new Mongo.Collection('creatures'); // 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? //slowed down by carrying too much?
useVariantEncumbrance: { useVariantEncumbrance: {
type: Boolean, type: Boolean,
@@ -62,7 +114,7 @@ let CreatureSettingsSchema = new SimpleSchema({
}, },
}); });
let IconGroupSchema = new SimpleSchema({ const IconGroupSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
max: STORAGE_LIMITS.name, max: STORAGE_LIMITS.name,
@@ -79,7 +131,7 @@ let IconGroupSchema = new SimpleSchema({
}, },
}); });
let CreatureTabletopSettingsSchema = new SimpleSchema({ const CreatureTabletopSettingsSchema = new SimpleSchema({
iconGroups: { iconGroups: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
@@ -90,7 +142,7 @@ let CreatureTabletopSettingsSchema = new SimpleSchema({
}, },
}); });
let CreatureSchema = new SimpleSchema({ const CreatureSchema = new SimpleSchema({
// Strings // Strings
name: { name: {
type: String, type: String,
@@ -139,11 +191,6 @@ let CreatureSchema = new SimpleSchema({
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
}, },
// Mechanics
deathSave: {
type: deathSaveSchema,
defaultValue: {},
},
// Stats that are computed and denormalised outside of recomputation // Stats that are computed and denormalised outside of recomputation
denormalizedStats: { denormalizedStats: {
type: Object, type: Object,
@@ -159,6 +206,10 @@ let CreatureSchema = new SimpleSchema({
type: SimpleSchema.Integer, type: SimpleSchema.Integer,
defaultValue: 0, defaultValue: 0,
}, },
propCount: {
type: SimpleSchema.Integer,
defaultValue: 0,
},
// Does the character need a recompute? // Does the character need a recompute?
dirty: { dirty: {
type: Boolean, type: Boolean,
@@ -174,11 +225,6 @@ let CreatureSchema = new SimpleSchema({
defaultValue: 'pc', defaultValue: 'pc',
allowedValues: ['pc', 'npc', 'monster'], allowedValues: ['pc', 'npc', 'monster'],
}, },
damageMultipliers: {
type: Object,
blackbox: true,
defaultValue: {}
},
computeErrors: { computeErrors: {
type: Array, type: Array,
optional: true, optional: true,
@@ -196,9 +242,9 @@ let CreatureSchema = new SimpleSchema({
}, },
// Tabletop // Tabletop
tabletop: { tabletopId: {
type: String, type: String,
regEx: SimpleSchema.RegEx.id, regEx: SimpleSchema.RegEx.Id,
optional: true, optional: true,
}, },
initiativeRoll: { initiativeRoll: {
@@ -220,6 +266,7 @@ let CreatureSchema = new SimpleSchema({
CreatureSchema.extend(ColorSchema); CreatureSchema.extend(ColorSchema);
CreatureSchema.extend(SharingSchema); CreatureSchema.extend(SharingSchema);
//@ts-expect-error attachSchema not defined
Creatures.attachSchema(CreatureSchema); Creatures.attachSchema(CreatureSchema);

View File

@@ -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 } });
}
}

View File

@@ -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 } });
}
}

View File

@@ -2,7 +2,7 @@ import buildCreatureComputation from './computation/buildCreatureComputation';
import computeCreatureComputation from './computation/computeCreatureComputation'; import computeCreatureComputation from './computation/computeCreatureComputation';
import writeAlteredProperties from './computation/writeComputation/writeAlteredProperties'; import writeAlteredProperties from './computation/writeComputation/writeAlteredProperties';
import writeScope from './computation/writeComputation/writeScope'; import writeScope from './computation/writeComputation/writeScope';
import writeErrors from './computation/writeComputation/writeErrors'; import writeErrorsAndPropCount from './computation/writeComputation/writeErrorsAndPropCount';
export default async function computeCreature(creatureId) { export default async function computeCreature(creatureId) {
if (Meteor.isClient) return; if (Meteor.isClient) return;
@@ -32,7 +32,7 @@ async function computeComputation(computation, creatureId) {
console.error(logError); console.error(logError);
} finally { } finally {
checkPropertyCount(computation) checkPropertyCount(computation)
writeErrors(creatureId, computation.errors); writeErrorsAndPropCount(creatureId, computation.errors, computation.props.length);
} }
} }

View File

@@ -4,7 +4,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { RefSchema } from '/imports/api/parenting/ChildSchema'; import { RefSchema } from '/imports/api/parenting/ChildSchema';
import { assertDocEditPermission, assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { assertDocEditPermission, assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { compact } from 'lodash'; 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'; import { fetchDocByRefAsync, getCollectionByName, moveDocBetweenRoots, moveDocWithinRoot } from '/imports/api/parenting/parentingFunctions';
const moveBetweenRoots = new ValidatedMethod({ const moveBetweenRoots = new ValidatedMethod({

View File

@@ -1,5 +1,9 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
export interface Colored {
color?: string,
}
const ColorSchema = new SimpleSchema({ const ColorSchema = new SimpleSchema({
color: { color: {
type: String, type: String,

View File

@@ -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;

View File

@@ -2,15 +2,25 @@ import SimpleSchema from 'simpl-schema';
import '/imports/api/sharing/sharing'; import '/imports/api/sharing/sharing';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; 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: { owner: {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
//@ts-expect-error index not defined
index: 1 index: 1
}, },
readers: { readers: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
//@ts-expect-error index not defined
index: 1, index: 1,
maxCount: STORAGE_LIMITS.readersCount, maxCount: STORAGE_LIMITS.readersCount,
}, },
@@ -21,6 +31,7 @@ let SharingSchema = new SimpleSchema({
writers: { writers: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
//@ts-expect-error index not defined
index: 1, index: 1,
maxCount: STORAGE_LIMITS.writersCount, maxCount: STORAGE_LIMITS.writersCount,
}, },
@@ -31,6 +42,7 @@ let SharingSchema = new SimpleSchema({
public: { public: {
type: Boolean, type: Boolean,
defaultValue: false, defaultValue: false,
//@ts-expect-error index not defined
index: 1, index: 1,
}, },
readersCanCopy: { readersCanCopy: {

View File

@@ -1,6 +1,23 @@
import SimpleSchema from 'simpl-schema'; 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({ const InitiativeSchema = new SimpleSchema({
active: { active: {
@@ -23,7 +40,7 @@ const InitiativeSchema = new SimpleSchema({
}); });
// All creatures in a tabletop have a shared time and space. // All creatures in a tabletop have a shared time and space.
let TabletopSchema = new SimpleSchema({ const TabletopSchema = new SimpleSchema({
// Details // Details
name: { name: {
type: String, type: String,
@@ -43,13 +60,38 @@ let TabletopSchema = new SimpleSchema({
owner: String, owner: String,
// The owner will need to included in one of these arrays for specific permissions // 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 // A user should not appear in more than one of the following arrays
gameMasters: [String], gameMasters: {
players: [String], type: Array,
spectators: [String], 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? // Does everyone else have the spectator permission?
public: { public: {
type: Boolean, type: Boolean,
defaultValue: false, optional: true,
//@ts-expect-error Index not defined in simpl-schema package
index: 1, index: 1,
}, },
@@ -61,10 +103,12 @@ let TabletopSchema = new SimpleSchema({
}); });
//@ts-expect-error attachSchema not defined in simpl-schema package
Tabletops.attachSchema(TabletopSchema); Tabletops.attachSchema(TabletopSchema);
import '/imports/api/tabletop/methods/removeTabletop'; import '/imports/api/tabletop/methods/removeTabletop';
import '/imports/api/tabletop/methods/insertTabletop'; import '/imports/api/tabletop/methods/insertTabletop';
import '/imports/api/tabletop/methods/updateTabletop';
import '/imports/api/tabletop/methods/addCreaturesToTabletop'; import '/imports/api/tabletop/methods/addCreaturesToTabletop';
export default Tabletops; export default Tabletops;

View File

@@ -2,7 +2,6 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertUserInTabletop } from './shared/tabletopPermissions'; import { assertUserInTabletop } from './shared/tabletopPermissions';
import { assertAdmin } from '/imports/api/sharing/sharingPermissions';
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers';
import Creatures from '/imports/api/creature/creatures/Creatures'; import Creatures from '/imports/api/creature/creatures/Creatures';
@@ -16,15 +15,16 @@ const addCreaturesToTabletop = new ValidatedMethod({
}, },
'creatureIds.$': { 'creatureIds.$': {
type: String, type: String,
regEx: SimpleSchema.RegEx.id, regEx: SimpleSchema.RegEx.Id,
}, },
tabletopId: { tabletopId: {
type: String, type: String,
regEx: SimpleSchema.RegEx.id, regEx: SimpleSchema.RegEx.Id,
}, },
}).validator(), }).validator(),
mixins: [RateLimiterMixin], mixins: [RateLimiterMixin],
// @ts-expect-error Rate limit not defined
rateLimit: { rateLimit: {
numRequests: 10, numRequests: 10,
timeInterval: 5000, timeInterval: 5000,
@@ -37,16 +37,16 @@ const addCreaturesToTabletop = new ValidatedMethod({
} }
assertUserHasPaidBenefits(this.userId); assertUserHasPaidBenefits(this.userId);
assertUserInTabletop(tabletopId, this.userId); assertUserInTabletop(tabletopId, this.userId);
assertAdmin(this.userId);
Creatures.update({ Creatures.update({
_id: { $in: creatureIds }, _id: { $in: creatureIds },
// You must have write permission for the creatures you
$or: [ $or: [
{ writers: this.userId }, { writers: this.userId },
{ owner: this.userId }, { owner: this.userId },
], ],
}, { }, {
$set: { tabletop: tabletopId }, $set: { tabletopId },
}, { }, {
multi: true, multi: true,
}); });

View File

@@ -1,8 +1,7 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Tabletops from '../Tabletops'; import Tabletops from '../Tabletops';
import { assertAdmin } from '/imports/api/sharing/sharingPermissions'; import { assertUserHasPaidBenefits, getUserTier } from '/imports/api/users/patreon/tiers';
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers';
const insertTabletop = new ValidatedMethod({ const insertTabletop = new ValidatedMethod({
@@ -11,8 +10,9 @@ const insertTabletop = new ValidatedMethod({
validate: null, validate: null,
mixins: [RateLimiterMixin], mixins: [RateLimiterMixin],
// @ts-expect-error Rate limit not defined
rateLimit: { rateLimit: {
numRequests: 5, numRequests: 2,
timeInterval: 5000, timeInterval: 5000,
}, },
@@ -22,13 +22,24 @@ const insertTabletop = new ValidatedMethod({
'You need to be logged in to insert a tabletop'); 'You need to be logged in to insert a tabletop');
} }
assertUserHasPaidBenefits(this.userId); 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({ return Tabletops.insert({
gameMaster: this.userId, owner: this.userId,
gameMasters: [this.userId],
players: [],
spectators: [],
initiative: {
active: false,
roundNumber: 0,
},
}); });
}, },
}); });
export default insertTabletop; export default insertTabletop;

View File

@@ -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;

View File

@@ -2,7 +2,6 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Tabletops from '../Tabletops'; import Tabletops from '../Tabletops';
import { assertAdmin } from '/imports/api/sharing/sharingPermissions';
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers';
import { assertUserIsTabletopOwner } from './shared/tabletopPermissions'; import { assertUserIsTabletopOwner } from './shared/tabletopPermissions';
import Creatures from '/imports/api/creature/creatures/Creatures'; import Creatures from '/imports/api/creature/creatures/Creatures';
@@ -14,11 +13,12 @@ const removeTabletop = new ValidatedMethod({
validate: new SimpleSchema({ validate: new SimpleSchema({
tabletopId: { tabletopId: {
type: String, type: String,
regEx: SimpleSchema.RegEx.id, regEx: SimpleSchema.RegEx.Id,
}, },
}).validator(), }).validator(),
mixins: [RateLimiterMixin], mixins: [RateLimiterMixin],
// @ts-expect-error Rate limit not defined
rateLimit: { rateLimit: {
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
@@ -31,7 +31,6 @@ const removeTabletop = new ValidatedMethod({
} }
assertUserHasPaidBenefits(this.userId); assertUserHasPaidBenefits(this.userId);
assertUserIsTabletopOwner(tabletopId, this.userId); assertUserIsTabletopOwner(tabletopId, this.userId);
assertAdmin(this.userId);
let removed = Tabletops.remove({ let removed = Tabletops.remove({
_id: tabletopId, _id: tabletopId,

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View 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

View File

@@ -9,6 +9,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 0, minimumEntitledCents: 0,
invites: 0, invites: 0,
characterSlots: 5, characterSlots: 5,
tabletopSlots: 0,
fileStorage: 50, fileStorage: 50,
paidBenefits: false, paidBenefits: false,
}, { }, {
@@ -16,6 +17,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 100, minimumEntitledCents: 100,
invites: 0, invites: 0,
characterSlots: 5, characterSlots: 5,
tabletopSlots: 0,
fileStorage: 50, fileStorage: 50,
paidBenefits: false, paidBenefits: false,
}, { }, {
@@ -23,6 +25,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 300, minimumEntitledCents: 300,
invites: 0, invites: 0,
characterSlots: 5, characterSlots: 5,
tabletopSlots: 0,
fileStorage: 50, fileStorage: 50,
paidBenefits: false, paidBenefits: false,
}, { }, {
@@ -31,6 +34,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 500, minimumEntitledCents: 500,
invites: 0, invites: 0,
characterSlots: 20, characterSlots: 20,
tabletopSlots: 4,
fileStorage: 200, fileStorage: 200,
paidBenefits: true, paidBenefits: true,
}, { }, {
@@ -39,6 +43,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 1000, minimumEntitledCents: 1000,
invites: 2, invites: 2,
characterSlots: 50, characterSlots: 50,
tabletopSlots: 10,
fileStorage: 500, fileStorage: 500,
paidBenefits: true, paidBenefits: true,
}, { }, {
@@ -47,6 +52,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 2000, minimumEntitledCents: 2000,
invites: 5, invites: 5,
characterSlots: 120, characterSlots: 120,
tabletopSlots: 24,
fileStorage: 1000, fileStorage: 1000,
paidBenefits: true, paidBenefits: true,
}, { }, {
@@ -55,6 +61,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 5000, minimumEntitledCents: 5000,
invites: 15, invites: 15,
characterSlots: -1, // Unlimited characters characterSlots: -1, // Unlimited characters
tabletopSlots: -1, // Unlimited tabletops
fileStorage: 2000, fileStorage: 2000,
paidBenefits: true, paidBenefits: true,
}, },
@@ -66,6 +73,7 @@ const GUEST_TIER = Object.freeze({
guest: true, guest: true,
invites: 0, invites: 0,
characterSlots: 20, characterSlots: 20,
tabletopSlots: 4,
fileStorage: 200, fileStorage: 200,
paidBenefits: true, paidBenefits: true,
}); });
@@ -76,6 +84,7 @@ const PATREON_DISABLED_TIER = Object.freeze({
name: 'Outlander', name: 'Outlander',
invites: 0, invites: 0,
characterSlots: -1, // Can have infinitely many characters characterSlots: -1, // Can have infinitely many characters
tabletopSlots: -1, // Infinite tabletops
fileStorage: 1000000, // 1TB file storage fileStorage: 1000000, // 1TB file storage
paidBenefits: true, paidBenefits: true,
}); });

View 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>

View File

@@ -58,7 +58,10 @@ export default {
addTabletop(){ addTabletop(){
this.addTabletopLoading = true; this.addTabletopLoading = true;
insertTabletop.call(error => { insertTabletop.call(error => {
if (error) snackbar(error.message); if (error) {
console.error(error)
snackbar({ text: error.reason || error.message || error.toString() });
}
this.addTabletopLoading = false; this.addTabletopLoading = false;
}); });
} }

View File

@@ -59,7 +59,7 @@
<script lang="js"> <script lang="js">
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue'; import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
import CharacterSheet from '/imports/client/ui/creature/character/CharacterSheet.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 { export default {
components: { components: {

View File

@@ -68,6 +68,36 @@
</v-btn> </v-btn>
</div> </div>
</v-row> </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-container>
<v-footer <v-footer
inset inset
@@ -96,13 +126,14 @@
import addCreaturesToTabletop from '/imports/api/tabletop/methods/addCreaturesToTabletop'; import addCreaturesToTabletop from '/imports/api/tabletop/methods/addCreaturesToTabletop';
import TabletopCreatureCard from '/imports/client/ui/tabletop/TabletopCreatureCard.vue'; import TabletopCreatureCard from '/imports/client/ui/tabletop/TabletopCreatureCard.vue';
import TabletopMap from '/imports/client/ui/tabletop/TabletopMap.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 MiniCharacterSheet from '/imports/client/ui/creature/character/MiniCharacterSheet.vue';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js'; import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import ActionCard from '/imports/client/ui/tabletop/TabletopActionCard.vue'; import ActionCard from '/imports/client/ui/tabletop/TabletopActionCard.vue';
import SelectedCreatureBar from '/imports/client/ui/tabletop/selectedCreatureBar/SelectedCreatureBar.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 = {}) { const getProperties = function (creatureId, selector = {}) {
return CreatureProperties.find({ return CreatureProperties.find({
@@ -129,6 +160,7 @@ export default {
ActionCard, ActionCard,
MiniCharacterSheet, MiniCharacterSheet,
SelectedCreatureBar, SelectedCreatureBar,
TabletopCreatureListItem,
}, },
props: { props: {
model: { model: {
@@ -159,7 +191,7 @@ export default {
}, },
}, },
creatures(){ creatures(){
return Creatures.find({ tabletop: this.model._id }); return Creatures.find({ tabletopId: this.model._id });
}, },
actions(){ actions(){
return getProperties(this.activeCreatureId, { type: 'action', actionType: { $ne: 'event'} }); return getProperties(this.activeCreatureId, { type: 'action', actionType: { $ne: 'event'} });
@@ -196,7 +228,10 @@ export default {
tabletopId: this.model._id, tabletopId: this.model._id,
creatureIds: charIds, creatureIds: charIds,
}, error => { }, error => {
if (error) snackbar(error.message); if (error) {
console.error(error)
snackbar({ text: error.message || error.toString() });
}
}); });
}, },
}); });

View 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>

View File

@@ -135,7 +135,7 @@
</template> </template>
<script lang="js"> <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 CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import TabletopActionCard from '/imports/client/ui/tabletop/TabletopActionCard.vue'; import TabletopActionCard from '/imports/client/ui/tabletop/TabletopActionCard.vue';
import CreatureBarIcon from '/imports/client/ui/tabletop/selectedCreatureBar/CreatureBarIcon.vue'; import CreatureBarIcon from '/imports/client/ui/tabletop/selectedCreatureBar/CreatureBarIcon.vue';

View File

@@ -1,5 +1,5 @@
import { serialMap } from '/imports/api/utility/asyncMap'; 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 ParseNode from '/imports/parser/parseTree/ParseNode';
import ResolveFunction from '/imports/parser/types/ResolveFunction'; import ResolveFunction from '/imports/parser/types/ResolveFunction';
import MapFunction from '/imports/parser/types/MapFunction'; import MapFunction from '/imports/parser/types/MapFunction';

View File

@@ -12,8 +12,10 @@ Meteor.publish('tabletops', function () {
} }
return Tabletops.find({ return Tabletops.find({
$or: [ $or: [
{ owner: userId },
{ players: userId }, { players: userId },
{ gameMaster: userId }, { gameMasters: userId },
{ spectators: userId },
], ],
}); });
}); });
@@ -24,12 +26,15 @@ Meteor.publish('tabletop', function (tabletopId) {
return []; return [];
} }
this.autorun(function () { this.autorun(function () {
if (!userId) return [];
const self = this; const self = this;
let tabletopCursor = Tabletops.find({ let tabletopCursor = Tabletops.find({
_id: tabletopId, _id: tabletopId,
$or: [ $or: [
{ owner: userId },
{ players: userId }, { players: userId },
{ gameMaster: userId }, { gameMasters: userId },
{ spectators: userId },
] ]
}); });
let tabletop = tabletopCursor.fetch()[0]; 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 // read permission of this specific creature, so publish as few fields as
// possible // possible
let creatureSummaries = Creatures.find({ let creatureSummaries = Creatures.find({
tabletop: tabletopId, tabletopId,
}, { }, {
fields: { fields: {
_id: 1, _id: 1,
name: 1, name: 1,
picture: 1, picture: 1,
avatarPicture: 1, avatarPicture: 1,
tabletop: 1, tabletopId: 1,
initiativeRoll: 1, initiativeRoll: 1,
settings: 1, settings: 1,
propCount: 1,
}, },
limit: 110,
}); });
const creatureIds = creatureSummaries.map(c => c._id); const creatureIds = creatureSummaries.map(c => c._id);
creatureIds.forEach(creatureId => { creatureIds.forEach(creatureId => {
@@ -59,10 +66,14 @@ Meteor.publish('tabletop', function (tabletopId) {
}); });
const variables = CreatureVariables.find({ const variables = CreatureVariables.find({
_creatureId: { $in: creatureIds } _creatureId: { $in: creatureIds }
}, {
limit: 110,
}); });
let properties = CreatureProperties.find({ let properties = CreatureProperties.find({
'ancestors.id': { $in: creatureIds }, 'ancestors.id': { $in: creatureIds },
removed: { $ne: true }, removed: { $ne: true },
}, {
limit: 10_000,
}); });
const logs = CreatureLogs.find({ const logs = CreatureLogs.find({
tabletopId, tabletopId,