Iterated on tabletops

This commit is contained in:
Thaum Rystra
2024-06-12 17:30:37 +02:00
parent a5292cf0f2
commit 621f284cff
12 changed files with 210 additions and 56 deletions

View File

@@ -5,6 +5,7 @@
"blackbox",
"Crits",
"cyrb",
"denormalize",
"denormalized",
"EJSON",
"healthbar",

View File

@@ -243,6 +243,7 @@ const CreatureSchema = new SimpleSchema({
// Tabletop
tabletopId: {
index: 1,
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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({

View File

@@ -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 },

View File

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

View File

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

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

View File

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

View File

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