Iterated on tabletops
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -5,6 +5,7 @@
|
||||
"blackbox",
|
||||
"Crits",
|
||||
"cyrb",
|
||||
"denormalize",
|
||||
"denormalized",
|
||||
"EJSON",
|
||||
"healthbar",
|
||||
|
||||
@@ -243,6 +243,7 @@ const CreatureSchema = new SimpleSchema({
|
||||
|
||||
// Tabletop
|
||||
tabletopId: {
|
||||
index: 1,
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex';
|
||||
import bulkWrite, { addSetOp, addUnsetOp, newOperation } from '/imports/api/engine/shared/bulkWrite';
|
||||
import denormalizeTabletopPropCount from '/imports/api/tabletop/functions/denormalizeTabletopPropCount'
|
||||
|
||||
export default function writeAlteredProperties(computation) {
|
||||
let bulkWriteOperations = [];
|
||||
@@ -35,6 +36,9 @@ export default function writeAlteredProperties(computation) {
|
||||
});
|
||||
bulkWrite(bulkWriteOperations, CreatureProperties);
|
||||
//if (bulkWriteOperations.length) console.log(`Wrote ${bulkWriteOperations.length} props`);
|
||||
|
||||
// Update the relevant tabletop's property count
|
||||
if (computation.creature.tabletopId) denormalizeTabletopPropCount(computation.creature.tabletopId);
|
||||
}
|
||||
|
||||
function addChangedKeysToOp(op, keys, original, changed) {
|
||||
|
||||
@@ -15,6 +15,7 @@ export type Tabletop = {
|
||||
initiativeNumber?: number,
|
||||
activeCreature?: string,
|
||||
},
|
||||
propCount: number,
|
||||
}
|
||||
|
||||
const Tabletops = new Mongo.Collection<Tabletop>('tabletops');
|
||||
@@ -101,6 +102,17 @@ const TabletopSchema = new SimpleSchema({
|
||||
defaultValue: {},
|
||||
},
|
||||
|
||||
// Denormalized fields
|
||||
// Number of properties on all creatures in this tabletop
|
||||
propCount: {
|
||||
type: SimpleSchema.Integer,
|
||||
defaultValue: 0,
|
||||
},
|
||||
// Number of creatures in this tabletop
|
||||
creatureCount: {
|
||||
type: SimpleSchema.Integer,
|
||||
defaultValue: 0,
|
||||
},
|
||||
});
|
||||
|
||||
//@ts-expect-error attachSchema not defined in simpl-schema package
|
||||
@@ -112,5 +124,6 @@ import '/imports/api/tabletop/methods/updateTabletop';
|
||||
import '/imports/api/tabletop/methods/addCreaturesToTabletop';
|
||||
import '/imports/api/tabletop/methods/updateTabletopSharing';
|
||||
import '/imports/api/tabletop/methods/addCreaturesFromLibraryToTabletop';
|
||||
import '/imports/api/tabletop/methods/removeCreatureFromTabletop';
|
||||
|
||||
export default Tabletops;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { debounce } from 'lodash';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
import Tabletops from '/imports/api/tabletop/Tabletops';
|
||||
|
||||
// Store a function per tabletop to debounce the update
|
||||
const queues: Record<string, () => void> = {};
|
||||
/**
|
||||
* Update the propCount field on a tabletop to reflect the sum of all propCounts of creatures in
|
||||
* that tabletop.
|
||||
* Debounced by 1s, per tabletop
|
||||
*/
|
||||
export default function updateTabletopPropCount(tabletopId: string) {
|
||||
if (!tabletopId) return;
|
||||
|
||||
// Server only
|
||||
if (Meteor.isClient) return;
|
||||
|
||||
// If there isn't a debounced function for this tabletop, create one
|
||||
if (!queues[tabletopId]) {
|
||||
queues[tabletopId] = debounce(() => {
|
||||
doUpdateTabletopPropCount(tabletopId);
|
||||
// When this function is actually run, delete the debounced function
|
||||
delete queues[tabletopId];
|
||||
}, 1_000);
|
||||
}
|
||||
|
||||
// Call the debounced function for this tabletop
|
||||
queues[tabletopId]();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the propCount field on a tabletop to reflect the sum of all propCounts of creatures in
|
||||
* that tabletop
|
||||
*/
|
||||
async function doUpdateTabletopPropCount(tabletopId: string) {
|
||||
let propCount = 0;
|
||||
let creatureCount = 0;
|
||||
await Creatures.find({
|
||||
tabletopId
|
||||
}, {
|
||||
fields: { propCount: 1 }
|
||||
}).forEachAsync(creature => {
|
||||
creatureCount += 1;
|
||||
propCount += creature.propCount || 0;
|
||||
});
|
||||
return Tabletops.update(tabletopId, {
|
||||
$set: {
|
||||
propCount,
|
||||
creatureCount,
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import { getFilter, renewDocIds } from '/imports/api/parenting/parentingFunction
|
||||
import { reifyNodeReferences, storeLibraryNodeReferences } from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
||||
import Tabletops from '/imports/api/tabletop/Tabletops';
|
||||
import { assertTabletopHasPropSpace } from '/imports/api/tabletop/methods/shared/tabletopLimits'
|
||||
|
||||
const addCreaturesFromLibraryToTabletop = new ValidatedMethod({
|
||||
|
||||
@@ -40,7 +42,9 @@ const addCreaturesFromLibraryToTabletop = new ValidatedMethod({
|
||||
'You need to be logged in to remove a tabletop');
|
||||
}
|
||||
assertUserHasPaidBenefits(this.userId);
|
||||
assertUserInTabletop(tabletopId, this.userId);
|
||||
const tabletop = Tabletops.findOne(tabletopId);
|
||||
assertUserInTabletop(tabletop, this.userId);
|
||||
assertTabletopHasPropSpace(tabletop);
|
||||
|
||||
for (const nodeId of libraryNodeIds) {
|
||||
const creatureNode = LibraryNodes.findOne({
|
||||
|
||||
@@ -4,6 +4,8 @@ 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';
|
||||
import Tabletops from '/imports/api/tabletop/Tabletops';
|
||||
import { assertTabletopHasPropSpace } from '/imports/api/tabletop/methods/shared/tabletopLimits';
|
||||
|
||||
const addCreaturesToTabletop = new ValidatedMethod({
|
||||
|
||||
@@ -12,6 +14,7 @@ const addCreaturesToTabletop = new ValidatedMethod({
|
||||
validate: new SimpleSchema({
|
||||
'creatureIds': {
|
||||
type: Array,
|
||||
max: 20,
|
||||
},
|
||||
'creatureIds.$': {
|
||||
type: String,
|
||||
@@ -24,7 +27,6 @@ const addCreaturesToTabletop = new ValidatedMethod({
|
||||
}).validator(),
|
||||
|
||||
mixins: [RateLimiterMixin],
|
||||
// @ts-expect-error Rate limit not defined
|
||||
rateLimit: {
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
@@ -36,7 +38,9 @@ const addCreaturesToTabletop = new ValidatedMethod({
|
||||
'You need to be logged in to remove a tabletop');
|
||||
}
|
||||
assertUserHasPaidBenefits(this.userId);
|
||||
assertUserInTabletop(tabletopId, this.userId);
|
||||
const tabletop = Tabletops.findOne(tabletopId);
|
||||
assertUserInTabletop(tabletop, this.userId);
|
||||
assertTabletopHasPropSpace(tabletop);
|
||||
|
||||
Creatures.update({
|
||||
_id: { $in: creatureIds },
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
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;
|
||||
@@ -0,0 +1,89 @@
|
||||
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';
|
||||
import updateTabletopPropCount from '/imports/api/tabletop/functions/denormalizeTabletopPropCount';
|
||||
import { getCreature } from '/imports/api/engine/loadCreatures';
|
||||
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature';
|
||||
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions';
|
||||
|
||||
const removeCreatureFromTabletop = new ValidatedMethod({
|
||||
|
||||
name: 'tabletops.removeCreature',
|
||||
|
||||
validate: new SimpleSchema({
|
||||
tabletopId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
'creatureIds': {
|
||||
type: Array,
|
||||
},
|
||||
'creatureIds.$': {
|
||||
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.removeCreature.denied',
|
||||
'You need to be logged in to remove creatures from tabletop');
|
||||
}
|
||||
assertUserHasPaidBenefits(this.userId);
|
||||
assertUserInTabletop(tabletopId, this.userId);
|
||||
|
||||
const creaturesToRemove: any[] = [];
|
||||
const creatureIdsToClearTabletopId: string[] = [];
|
||||
|
||||
for (const creatureId of creatureIds) {
|
||||
const creature = getCreature(creatureId);
|
||||
// Make sure the creature exists and is in this tabletop
|
||||
if (!creature || creature.tabletopId !== tabletopId) continue;
|
||||
switch (creature.type) {
|
||||
// Remove character creatures from the tabletop
|
||||
case 'pc':
|
||||
creatureIdsToClearTabletopId.push(creatureId);
|
||||
break;
|
||||
// Delete non player characters and monsters
|
||||
case 'npc':
|
||||
case 'monster':
|
||||
creaturesToRemove.push(creature);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear tabletopId from all player characters
|
||||
if (creatureIdsToClearTabletopId.length) Creatures.update({
|
||||
_id: { $in: creatureIdsToClearTabletopId },
|
||||
$or: [
|
||||
{ writers: this.userId },
|
||||
{ owner: this.userId },
|
||||
],
|
||||
}, {
|
||||
$unset: { tabletopId: 1 },
|
||||
}, {
|
||||
multi: true,
|
||||
});
|
||||
|
||||
// Remove all non player characters and monsters
|
||||
for (const creature of creaturesToRemove) {
|
||||
assertOwnership(creature, this.userId)
|
||||
removeCreatureWork(creature._id);
|
||||
}
|
||||
|
||||
if (Meteor.isServer) {
|
||||
updateTabletopPropCount(tabletopId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default removeCreatureFromTabletop;
|
||||
13
app/imports/api/tabletop/methods/shared/tabletopLimits.ts
Normal file
13
app/imports/api/tabletop/methods/shared/tabletopLimits.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
const MAX_PROP_COUNT = 10_000;
|
||||
const MAX_CREATURE_COUNT = 110;
|
||||
|
||||
export function assertTabletopHasPropSpace(tabletop) {
|
||||
if (tabletop.propCount >= MAX_PROP_COUNT) {
|
||||
throw new Meteor.Error('tabletops.denied',
|
||||
'This tabletop is full, either remove some creatures or reduce how many properties each creature has');
|
||||
}
|
||||
if (tabletop.creatureCount >= MAX_CREATURE_COUNT) {
|
||||
throw new Meteor.Error('tabletops.denied',
|
||||
'This tabletop is full, you can\'t add any more creatures to it');
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,7 @@
|
||||
:key="activeCreatureId"
|
||||
:creature-id="activeCreatureId"
|
||||
@active-action-change="activeActionId = $event"
|
||||
@remove="removeCreature(activeCreatureId)"
|
||||
/>
|
||||
</v-slide-y-reverse-transition>
|
||||
</v-footer>
|
||||
@@ -109,6 +110,7 @@ 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';
|
||||
import addCreaturesFromLibraryToTabletop from '/imports/api/tabletop/methods/addCreaturesFromLibraryToTabletop';
|
||||
import removeCreatureFromTabletop from '/imports/api/tabletop/methods/removeCreatureFromTabletop';
|
||||
|
||||
const getProperties = function (creatureId, selector = {}) {
|
||||
return CreatureProperties.find({
|
||||
@@ -254,6 +256,17 @@ export default {
|
||||
if (index > -1) {
|
||||
this.targets.splice(index, 1);
|
||||
}
|
||||
},
|
||||
removeCreature(creatureId) {
|
||||
if (this.activeCreatureId === creatureId) this.activeCreatureId = undefined;
|
||||
removeCreatureFromTabletop.call({
|
||||
tabletopId: this.model._id,
|
||||
creatureIds: [creatureId]
|
||||
}, error => {
|
||||
if (!error) return;
|
||||
console.error(error);
|
||||
snackbar({ text: error.message || error.toString() });
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -43,6 +43,19 @@
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<v-card
|
||||
class="delete-card"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<creature-bar-icon
|
||||
icon="mdi-delete"
|
||||
data-id="trashIcon"
|
||||
@click="$emit('remove')"
|
||||
/>
|
||||
</div>
|
||||
</v-card>
|
||||
<v-card
|
||||
v-if="iconGroups.buffs"
|
||||
class="buffs-card"
|
||||
|
||||
Reference in New Issue
Block a user