Vastly improved new character UX
Characters now can limit which libraries they allow
This commit is contained in:
@@ -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){
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 }});
|
||||
}
|
||||
}
|
||||
|
||||
39
app/imports/api/library/getCreatureLibraryIds.js
Normal file
39
app/imports/api/library/getCreatureLibraryIds.js
Normal 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);
|
||||
}
|
||||
31
app/imports/api/library/getUserLibraryIds.js
Normal file
31
app/imports/api/library/getUserLibraryIds.js
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user