Vastly improved new character UX

Characters now can limit which libraries they allow
This commit is contained in:
Stefan Zermatten
2022-07-18 13:45:14 +02:00
parent bf9639ae59
commit f8e9131bdd
21 changed files with 686 additions and 503 deletions

View File

@@ -131,15 +131,14 @@ function insertPropertyFromNode(nodeId, ancestors, order){
node.order = order;
}
// Mark root as dirty
node.dirty = true;
// Mark all nodes as dirty
dirtyNodes(nodes);
// Insert the creature properties
CreatureProperties.batchInsert(nodes);
return node;
}
function storeLibraryNodeReferences(nodes){
nodes.forEach(node => {
if (node.libraryNodeId) return;
@@ -147,6 +146,12 @@ function storeLibraryNodeReferences(nodes){
});
}
function dirtyNodes(nodes) {
nodes.forEach(node => {
node.dirty = true;
});
}
// Covert node references into actual nodes
// TODO: check permissions for each library a reference node references
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){

View File

@@ -80,6 +80,27 @@ let CreatureSchema = new SimpleSchema({
optional: true,
max: STORAGE_LIMITS.url,
},
// Libraries
allowedLibraries: {
type: Array,
optional: true,
maxCount: 100,
},
'allowedLibraries.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
allowedLibraryCollections: {
type: Array,
optional: true,
maxCount: 100,
},
'allowedLibraryCollections.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
// Mechanics
deathSave: {
type: deathSaveSchema,
@@ -165,8 +186,8 @@ CreatureSchema.extend(SharingSchema);
Creatures.attachSchema(CreatureSchema);
import '/imports/api/creature/creatures/methods/index.js';
import '/imports/api/engine/actions/doAction.js';
export default Creatures;
export { CreatureSchema };
import '/imports/api/engine/actions/doAction.js';

View File

@@ -10,7 +10,7 @@ export default function defaultCharacterProperties(creatureId){
{
type: 'propertySlot',
name: 'Ruleset',
description: {text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.'},
description: {text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base ruleset, your sheet will be empty.'},
slotTags: ['base'],
tags: [],
quantityExpected: {calculation: '1'},

View File

@@ -1,57 +1,104 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
import Creatures, { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js';
import getUserLibraryIds from '/imports/api/library/getUserLibraryIds.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { insertExperienceForCreature } from '/imports/api/creature/experience/Experiences.js';
import SimpleSchema from 'simpl-schema';
const insertCreature = new ValidatedMethod({
name: 'creatures.insertCreature',
validate: null,
mixins: [RateLimiterMixin],
mixins: [RateLimiterMixin, simpleSchemaMixin],
schema: CreatureSchema.pick(
'name',
'gender',
'alignment',
'allowedLibraries',
'allowedLibraryCollections',
).extend({
'startingLevel': {
type: SimpleSchema.Integer,
min: 0,
},
}),
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
if (!this.userId) {
run({ name, gender, alignment, startingLevel,
allowedLibraries, allowedLibraryCollections }) {
const userId = this.userId
if (!userId) {
throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature');
'You need to be logged in to insert a creature');
}
assertHasCharactersSlots(this.userId);
assertHasCharactersSlots(userId);
// Create the creature document
// Create the creature document
let creatureId = Creatures.insert({
owner: this.userId,
});
owner: userId,
name,
gender,
alignment,
allowedLibraries,
allowedLibraryCollections,
});
// Insert experience to get character to starting level
if (startingLevel) {
insertExperienceForCreature({
experience: {
name: 'Starting level',
levels: startingLevel,
creatureId
},
creatureId,
userId,
});
}
// Insert the default properties
// Not batchInsert because we want the properties cleaned by the schema
let baseId;
let baseId, rulesetSlot;
defaultCharacterProperties(creatureId).forEach(prop => {
let id = CreatureProperties.insert(prop);
if (prop.name === 'Ruleset'){
baseId = id;
rulesetSlot = prop;
}
});
if (Meteor.isServer){
// Insert the 5e ruleset as the default base
insertPropertyFromLibraryNode.call({
nodeIds: ['iHbhfcg3AL5isSWbw'],
parentRef: {id: baseId, collection: 'creatureProperties'},
order: 0.5,
});
// If the user only has a single ruleset subscribed, use it by default
if (Meteor.isServer) {
insertDefaultRuleset(baseId, userId, rulesetSlot);
}
return creatureId;
},
});
// If the user only has a single ruleset subscribed, insert it by default
function insertDefaultRuleset(baseId, userId, slot) {
const libraryIds = getUserLibraryIds(userId);
const filter = getSlotFillFilter({ slot, libraryIds });
const fillCursor = LibraryNodes.find(filter, { fields: { _id: 1 } });
const numRulesets = fillCursor.count();
if (numRulesets === 1) {
const ruleset = fillCursor.fetch()[0]
insertPropertyFromLibraryNode.call({
nodeIds: [ruleset._id],
parentRef: {id: baseId, collection: 'creatureProperties'},
order: 0.5,
});
}
}
export default insertCreature;

View File

@@ -47,7 +47,6 @@ let ExperienceSchema = new SimpleSchema({
Experiences.attachSchema(ExperienceSchema);
const insertExperienceForCreature = function({experience, creatureId, userId}){
assertEditPermission(creatureId, userId);
if (experience.xp){
Creatures.update(creatureId, {
$inc: { 'denormalizedStats.xp': experience.xp },
@@ -93,6 +92,7 @@ const insertExperience = new ValidatedMethod({
}
let insertedIds = [];
creatureIds.forEach(creatureId => {
assertEditPermission(creatureId, userId);
let id = insertExperienceForCreature({experience, creatureId, userId});
insertedIds.push(id);
});
@@ -181,4 +181,4 @@ const recomputeExperiences = new ValidatedMethod({
});
export default Experiences;
export { ExperienceSchema, insertExperience, removeExperience, recomputeExperiences };
export { ExperienceSchema, insertExperience, insertExperienceForCreature, removeExperience, recomputeExperiences };

View File

@@ -5,8 +5,13 @@ import { EJSON } from 'meteor/ejson';
export default function writeScope(creatureId, computation) {
if (!creatureId) throw 'creatureId is required';
const scope = computation.scope;
const variables = computation.variables || {};
let variables = computation.variables;
if (!variables) {
CreatureVariables.insert({ _creatureId: creatureId });
variables = {};
}
delete variables._id;
delete variables._creatureId;
let $set, $unset;
@@ -48,9 +53,9 @@ export default function writeScope(creatureId, computation) {
const update = {};
if ($set) update.$set = $set;
if ($unset) update.$unset = $unset;
CreatureVariables.upsert({_creatureId: creatureId}, update);
CreatureVariables.update({_creatureId: creatureId}, update);
}
if (computation.creature?.dirty) {
Creatures.update({_creatureId: creatureId}, {$unset: { dirty: 1 }});
Creatures.update({_id: creatureId}, {$unset: { dirty: 1 }});
}
}

View File

@@ -0,0 +1,39 @@
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import getUserLibraryIds from './getUserLibraryIds';
import { intersection, union } from 'lodash';
export default function getCreatureLibraryIds(creature, userId) {
if (!userId) return [];
// Get the ids of libraries the user is permitted to view
const userLibIds = getUserLibraryIds(userId);
// If given a creature Id, get the creature document
if (typeof creature === 'string') {
creature = Creatures.findOne(creature, {
fields: {
allowedLibraries: 1,
allowedLibraryCollections: 1,
}
});
if (!creature) return [];
}
// If the creature does not restrict the libraries, let it use them all
if (!creature.allowedLibraryCollections && !creature.allowedLibraries) {
return userLibIds;
}
// Get the ids of the libraries that the creature allows
const allowedCollections = creature.allowedLibraryCollections || [];
let creatureLibIds = creature.allowedLibraries || [];
LibraryCollections.find({
_id: { $in: allowedCollections }
}, { fields: { libraries: 1 } }).forEach(collection => {
creatureLibIds = union(creatureLibIds, collection.libraries);
});
// return all the ids that the creature allows and the user can view
return intersection(userLibIds, creatureLibIds);
}

View File

@@ -0,0 +1,31 @@
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
import Libraries from '/imports/api/library/Libraries.js';
import { union } from 'lodash';
export default function getUserLibraryIds(userId) {
if (!userId) return [];
const user = Meteor.users.findOne(userId);
let subbedIds = user?.subscribedLibraries || [];
const subCollections = user?.subscribedLibraryCollections || [];
LibraryCollections.find({
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ _id: { $in: subCollections }, public: true },
]
}, { fields: { libraries: 1 } }).forEach(collection => {
subbedIds = union(subbedIds, collection.libraries);
});
const libraryIds = Libraries.find({
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ _id: { $in: subbedIds }, public: true },
]
}, {
fields: { _id: 1 }
}).map(lib => lib._id);
return libraryIds;
}

View File

@@ -71,7 +71,7 @@ const userSchema = new SimpleSchema({
subscribedLibraries: {
type: Array,
defaultValue: defaultLibraries,
max: 100,
maxCount: 100,
},
'subscribedLibraries.$': {
type: String,
@@ -80,7 +80,7 @@ const userSchema = new SimpleSchema({
subscribedLibraryCollections: {
type: Array,
defaultValue: defaultLibraryCollections,
max: 100,
maxCount: 100,
},
'subscribedLibraryCollections.$': {
type: String,