Iterated on tabletops
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -5,6 +5,7 @@
|
|||||||
"blackbox",
|
"blackbox",
|
||||||
"Crits",
|
"Crits",
|
||||||
"cyrb",
|
"cyrb",
|
||||||
|
"denormalize",
|
||||||
"denormalized",
|
"denormalized",
|
||||||
"EJSON",
|
"EJSON",
|
||||||
"healthbar",
|
"healthbar",
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ const CreatureSchema = new SimpleSchema({
|
|||||||
|
|
||||||
// Tabletop
|
// Tabletop
|
||||||
tabletopId: {
|
tabletopId: {
|
||||||
|
index: 1,
|
||||||
type: String,
|
type: String,
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
optional: true,
|
optional: true,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||||
import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex';
|
import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex';
|
||||||
import bulkWrite, { addSetOp, addUnsetOp, newOperation } from '/imports/api/engine/shared/bulkWrite';
|
import bulkWrite, { addSetOp, addUnsetOp, newOperation } from '/imports/api/engine/shared/bulkWrite';
|
||||||
|
import denormalizeTabletopPropCount from '/imports/api/tabletop/functions/denormalizeTabletopPropCount'
|
||||||
|
|
||||||
export default function writeAlteredProperties(computation) {
|
export default function writeAlteredProperties(computation) {
|
||||||
let bulkWriteOperations = [];
|
let bulkWriteOperations = [];
|
||||||
@@ -35,6 +36,9 @@ export default function writeAlteredProperties(computation) {
|
|||||||
});
|
});
|
||||||
bulkWrite(bulkWriteOperations, CreatureProperties);
|
bulkWrite(bulkWriteOperations, CreatureProperties);
|
||||||
//if (bulkWriteOperations.length) console.log(`Wrote ${bulkWriteOperations.length} props`);
|
//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) {
|
function addChangedKeysToOp(op, keys, original, changed) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type Tabletop = {
|
|||||||
initiativeNumber?: number,
|
initiativeNumber?: number,
|
||||||
activeCreature?: string,
|
activeCreature?: string,
|
||||||
},
|
},
|
||||||
|
propCount: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tabletops = new Mongo.Collection<Tabletop>('tabletops');
|
const Tabletops = new Mongo.Collection<Tabletop>('tabletops');
|
||||||
@@ -101,6 +102,17 @@ const TabletopSchema = new SimpleSchema({
|
|||||||
defaultValue: {},
|
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
|
//@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/addCreaturesToTabletop';
|
||||||
import '/imports/api/tabletop/methods/updateTabletopSharing';
|
import '/imports/api/tabletop/methods/updateTabletopSharing';
|
||||||
import '/imports/api/tabletop/methods/addCreaturesFromLibraryToTabletop';
|
import '/imports/api/tabletop/methods/addCreaturesFromLibraryToTabletop';
|
||||||
|
import '/imports/api/tabletop/methods/removeCreatureFromTabletop';
|
||||||
|
|
||||||
export default Tabletops;
|
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 { reifyNodeReferences, storeLibraryNodeReferences } from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode';
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
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({
|
const addCreaturesFromLibraryToTabletop = new ValidatedMethod({
|
||||||
|
|
||||||
@@ -40,7 +42,9 @@ const addCreaturesFromLibraryToTabletop = new ValidatedMethod({
|
|||||||
'You need to be logged in to remove a tabletop');
|
'You need to be logged in to remove a tabletop');
|
||||||
}
|
}
|
||||||
assertUserHasPaidBenefits(this.userId);
|
assertUserHasPaidBenefits(this.userId);
|
||||||
assertUserInTabletop(tabletopId, this.userId);
|
const tabletop = Tabletops.findOne(tabletopId);
|
||||||
|
assertUserInTabletop(tabletop, this.userId);
|
||||||
|
assertTabletopHasPropSpace(tabletop);
|
||||||
|
|
||||||
for (const nodeId of libraryNodeIds) {
|
for (const nodeId of libraryNodeIds) {
|
||||||
const creatureNode = LibraryNodes.findOne({
|
const creatureNode = LibraryNodes.findOne({
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
|||||||
import { assertUserInTabletop } from './shared/tabletopPermissions';
|
import { assertUserInTabletop } from './shared/tabletopPermissions';
|
||||||
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';
|
||||||
|
import Tabletops from '/imports/api/tabletop/Tabletops';
|
||||||
|
import { assertTabletopHasPropSpace } from '/imports/api/tabletop/methods/shared/tabletopLimits';
|
||||||
|
|
||||||
const addCreaturesToTabletop = new ValidatedMethod({
|
const addCreaturesToTabletop = new ValidatedMethod({
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@ const addCreaturesToTabletop = new ValidatedMethod({
|
|||||||
validate: new SimpleSchema({
|
validate: new SimpleSchema({
|
||||||
'creatureIds': {
|
'creatureIds': {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
max: 20,
|
||||||
},
|
},
|
||||||
'creatureIds.$': {
|
'creatureIds.$': {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -24,7 +27,6 @@ const addCreaturesToTabletop = new ValidatedMethod({
|
|||||||
}).validator(),
|
}).validator(),
|
||||||
|
|
||||||
mixins: [RateLimiterMixin],
|
mixins: [RateLimiterMixin],
|
||||||
// @ts-expect-error Rate limit not defined
|
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
numRequests: 10,
|
numRequests: 10,
|
||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
@@ -36,7 +38,9 @@ const addCreaturesToTabletop = new ValidatedMethod({
|
|||||||
'You need to be logged in to remove a tabletop');
|
'You need to be logged in to remove a tabletop');
|
||||||
}
|
}
|
||||||
assertUserHasPaidBenefits(this.userId);
|
assertUserHasPaidBenefits(this.userId);
|
||||||
assertUserInTabletop(tabletopId, this.userId);
|
const tabletop = Tabletops.findOne(tabletopId);
|
||||||
|
assertUserInTabletop(tabletop, this.userId);
|
||||||
|
assertTabletopHasPropSpace(tabletop);
|
||||||
|
|
||||||
Creatures.update({
|
Creatures.update({
|
||||||
_id: { $in: creatureIds },
|
_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"
|
:key="activeCreatureId"
|
||||||
:creature-id="activeCreatureId"
|
:creature-id="activeCreatureId"
|
||||||
@active-action-change="activeActionId = $event"
|
@active-action-change="activeActionId = $event"
|
||||||
|
@remove="removeCreature(activeCreatureId)"
|
||||||
/>
|
/>
|
||||||
</v-slide-y-reverse-transition>
|
</v-slide-y-reverse-transition>
|
||||||
</v-footer>
|
</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 SelectedCreatureBar from '/imports/client/ui/tabletop/selectedCreatureBar/SelectedCreatureBar.vue';
|
||||||
import TabletopCreatureListItem from '/imports/client/ui/tabletop/TabletopCreatureListItem.vue';
|
import TabletopCreatureListItem from '/imports/client/ui/tabletop/TabletopCreatureListItem.vue';
|
||||||
import addCreaturesFromLibraryToTabletop from '/imports/api/tabletop/methods/addCreaturesFromLibraryToTabletop';
|
import addCreaturesFromLibraryToTabletop from '/imports/api/tabletop/methods/addCreaturesFromLibraryToTabletop';
|
||||||
|
import removeCreatureFromTabletop from '/imports/api/tabletop/methods/removeCreatureFromTabletop';
|
||||||
|
|
||||||
const getProperties = function (creatureId, selector = {}) {
|
const getProperties = function (creatureId, selector = {}) {
|
||||||
return CreatureProperties.find({
|
return CreatureProperties.find({
|
||||||
@@ -254,6 +256,17 @@ export default {
|
|||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
this.targets.splice(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-title>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-menu>
|
</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-card
|
||||||
v-if="iconGroups.buffs"
|
v-if="iconGroups.buffs"
|
||||||
class="buffs-card"
|
class="buffs-card"
|
||||||
|
|||||||
Reference in New Issue
Block a user