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,
|
||||
|
||||
@@ -3,6 +3,7 @@ import Libraries from '/imports/api/library/Libraries.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'
|
||||
import getUserLibraryIds from '/imports/api/library/getUserLibraryIds.js';
|
||||
import { LIBRARY_NODE_TREE_FIELDS } from '/imports/server/publications/library.js';
|
||||
|
||||
const FIELDS = {
|
||||
@@ -27,21 +28,17 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){
|
||||
}
|
||||
|
||||
// Get all the ids of libraries the user can access
|
||||
const user = Meteor.users.findOne(userId, {
|
||||
fields: {subscribedLibraries: 1}
|
||||
});
|
||||
const subs = user && user.subscribedLibraries || [];
|
||||
let libraries = Libraries.find({
|
||||
const libraryIds = getUserLibraryIds(userId);
|
||||
const libraries = Libraries.find({
|
||||
$or: [
|
||||
{owner: this.userId},
|
||||
{writers: this.userId},
|
||||
{readers: this.userId},
|
||||
{_id: {$in: subs}},
|
||||
{ owner: userId },
|
||||
{ writers: userId },
|
||||
{ readers: userId },
|
||||
{ _id: { $in: libraryIds }, public: true },
|
||||
]
|
||||
}, {
|
||||
fields: {_id: 1, name: 1},
|
||||
sort: { name: 1 }
|
||||
});
|
||||
let libraryIds = libraries.map(lib => lib._id);
|
||||
|
||||
// Build a filter for nodes in those libraries that match the slot
|
||||
let filter = getSlotFillFilter({slot, libraryIds});
|
||||
@@ -83,7 +80,10 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){
|
||||
self.setData('countAll', LibraryNodes.find(filter).count());
|
||||
});
|
||||
self.autorun(function () {
|
||||
return [LibraryNodes.find(filter, options), libraries];
|
||||
return [
|
||||
LibraryNodes.find(filter, options),
|
||||
libraries
|
||||
];
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,29 +4,28 @@
|
||||
New Character
|
||||
</v-toolbar-title>
|
||||
<v-stepper
|
||||
slot="unwrapped-content"
|
||||
v-model="step"
|
||||
class="no-shadow"
|
||||
flat
|
||||
non-linear
|
||||
>
|
||||
<v-stepper-header class="no-shadow">
|
||||
<v-stepper-header>
|
||||
<v-stepper-step
|
||||
editable
|
||||
:complete="step > 1"
|
||||
step="1"
|
||||
:rules="[() => biographyAlert || true]"
|
||||
>
|
||||
Name
|
||||
Biography
|
||||
<small v-if="biographyAlert">{{ biographyAlert }}</small>
|
||||
</v-stepper-step>
|
||||
<v-divider />
|
||||
<v-stepper-step
|
||||
editable
|
||||
:complete="step > 2"
|
||||
step="2"
|
||||
>
|
||||
Ability Scores
|
||||
</v-stepper-step>
|
||||
<v-divider />
|
||||
<v-stepper-step
|
||||
:complete="step > 3"
|
||||
step="3"
|
||||
>
|
||||
Class
|
||||
Libraries
|
||||
</v-stepper-step>
|
||||
</v-stepper-header>
|
||||
|
||||
@@ -34,195 +33,44 @@
|
||||
<v-stepper-content step="1">
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
outlined
|
||||
label="Name"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="gender"
|
||||
label="Gender"
|
||||
class="mt-1"
|
||||
:error="!name"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="alignment"
|
||||
outlined
|
||||
label="Alignment"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="gender"
|
||||
outlined
|
||||
label="Gender"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model.number="startingLevel"
|
||||
outlined
|
||||
label="Level"
|
||||
type="number"
|
||||
height="20"
|
||||
min="0"
|
||||
@keydown.tab="step++"
|
||||
/>
|
||||
</v-stepper-content>
|
||||
<v-stepper-content step="2">
|
||||
<v-text-field
|
||||
v-model="race"
|
||||
label="Race"
|
||||
<v-switch
|
||||
v-model="allSubscribedLibraries"
|
||||
label="All user libraries"
|
||||
/>
|
||||
<v-layout
|
||||
justify-center
|
||||
align-center
|
||||
>
|
||||
<h3>Point Cost:</h3>
|
||||
<h1
|
||||
class="ml-2"
|
||||
:class="cost > 27 ? 'error--text' : ''"
|
||||
>
|
||||
{{ cost }}
|
||||
</h1>
|
||||
<span class="ml-1">/27</span>
|
||||
</v-layout>
|
||||
<table class="point-buy-table mt-2">
|
||||
<thead>
|
||||
<tr class="font-weight-bold">
|
||||
<td />
|
||||
<td>Base Values</td>
|
||||
<td>Race Bonus</td>
|
||||
<td>Score</td>
|
||||
<td>Modifier</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>Strength</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="baseStrength"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
min="8"
|
||||
max="15"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="strengthBonus"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
/>
|
||||
</td>
|
||||
<td>{{ baseStrength + strengthBonus }}</td>
|
||||
<td>{{ mod(baseStrength + strengthBonus) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dexterity</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="baseDexterity"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
min="8"
|
||||
max="15"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="dexterityBonus"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
/>
|
||||
</td>
|
||||
<td>{{ baseDexterity + dexterityBonus }}</td>
|
||||
<td>{{ mod(baseDexterity + dexterityBonus) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Constitution</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="baseConstitution"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
min="8"
|
||||
max="15"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="constitutionBonus"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
/>
|
||||
</td>
|
||||
<td>{{ baseConstitution + constitutionBonus }}</td>
|
||||
<td>{{ mod(baseConstitution + constitutionBonus) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Intelligence</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="baseIntelligence"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
min="8"
|
||||
max="15"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="intelligenceBonus"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
/>
|
||||
</td>
|
||||
<td>{{ baseIntelligence + intelligenceBonus }}</td>
|
||||
<td>{{ mod(baseIntelligence + intelligenceBonus) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wisdom</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="baseWisdom"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
min="8"
|
||||
max="15"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="wisdomBonus"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
/>
|
||||
</td>
|
||||
<td>{{ baseWisdom + wisdomBonus }}</td>
|
||||
<td>{{ mod(baseWisdom + wisdomBonus) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Charisma</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="baseCharisma"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
min="8"
|
||||
max="15"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="charismaBonus"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
/>
|
||||
</td>
|
||||
<td>{{ baseCharisma + charismaBonus }}</td>
|
||||
<td>{{ mod(baseCharisma + charismaBonus) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</v-stepper-content>
|
||||
<v-stepper-content step="3">
|
||||
<v-text-field
|
||||
v-model="cls"
|
||||
label="Class"
|
||||
/>
|
||||
<v-select
|
||||
v-model="hitDice"
|
||||
:items="hitDiceItems"
|
||||
label="Hit Dice"
|
||||
<library-list
|
||||
selection
|
||||
:disabled="allSubscribedLibraries"
|
||||
:libraries-selected="librariesSelected"
|
||||
:library-collections-selected="libraryCollectionsSelected"
|
||||
:libraries-selected-by-collections="librariesSelectedByCollections"
|
||||
@select-library="selectLibrary"
|
||||
@select-library-collection="selectLibraryCollection"
|
||||
/>
|
||||
</v-stepper-content>
|
||||
</v-stepper-items>
|
||||
@@ -243,15 +91,16 @@
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="step < 3"
|
||||
v-if="step < 2"
|
||||
color="accent"
|
||||
@click="step++"
|
||||
>
|
||||
Next
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:flat="step < 3"
|
||||
:color="step < 3? '' : 'accent'"
|
||||
:disabled="biographyAlert"
|
||||
:text="step < 2"
|
||||
:color="step < 2? '' : 'accent'"
|
||||
@click="submit"
|
||||
>
|
||||
Create
|
||||
@@ -261,109 +110,112 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
const getCost = function(score){
|
||||
const costs = {
|
||||
8: 0,
|
||||
9: 1,
|
||||
10: 2,
|
||||
11: 3,
|
||||
12: 4,
|
||||
13: 5,
|
||||
14: 7,
|
||||
15: 9,
|
||||
};
|
||||
if (costs[score] || costs[score] === 0){
|
||||
return costs[score];
|
||||
} else {
|
||||
return NaN;
|
||||
}
|
||||
};
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import { defer, union, without } from 'lodash';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import insertCreature from '/imports/api/creature/creatures/methods/insertCreature.js';
|
||||
import LibraryList from '/imports/ui/library/LibraryList.vue';
|
||||
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
LibraryList,
|
||||
},
|
||||
data(){return {
|
||||
step: 1,
|
||||
name: 'New Character',
|
||||
gender: '',
|
||||
alignment: '',
|
||||
startingLevel: 1,
|
||||
librariesSelected: [],
|
||||
libraryCollectionsSelected: [],
|
||||
librariesSelectedByCollections: [],
|
||||
allSubscribedLibraries: true,
|
||||
}},
|
||||
computed: {
|
||||
biographyAlert() {
|
||||
if (!this.name) return 'Name required';
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'libraries': [],
|
||||
},
|
||||
data(){return {
|
||||
step: 1,
|
||||
name: 'New Character',
|
||||
gender: '',
|
||||
alignment: '',
|
||||
race: 'Race',
|
||||
baseStrength: 10,
|
||||
baseDexterity: 10,
|
||||
baseConstitution: 10,
|
||||
baseIntelligence: 10,
|
||||
baseWisdom: 10,
|
||||
baseCharisma: 10,
|
||||
strengthBonus: 0,
|
||||
dexterityBonus: 0,
|
||||
constitutionBonus: 0,
|
||||
intelligenceBonus: 0,
|
||||
wisdomBonus: 0,
|
||||
charismaBonus: 0,
|
||||
hitDiceItems: ['d6', 'd8', 'd10', 'd12'],
|
||||
hitDice: 'd8',
|
||||
cls: 'Class',
|
||||
}},
|
||||
computed: {
|
||||
cost(){
|
||||
return [
|
||||
this.baseStrength,
|
||||
this.baseDexterity,
|
||||
this.baseConstitution,
|
||||
this.baseIntelligence,
|
||||
this.baseWisdom,
|
||||
this.baseCharisma,
|
||||
].map(getCost)
|
||||
.reduce((memo, score) => memo + score, 0);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
mod(score){
|
||||
let mod = Math.floor((score - 10) / 2);
|
||||
if (mod >= 0) {
|
||||
return `+${mod}`;
|
||||
} else {
|
||||
return `${mod}`;
|
||||
}
|
||||
},
|
||||
submit(){
|
||||
let char = {
|
||||
name: this.name,
|
||||
gender: this.gender,
|
||||
alignment: this.alignment,
|
||||
race: this.race,
|
||||
baseStrength: this.baseStrength,
|
||||
baseDexterity: this.baseDexterity,
|
||||
baseConstitution: this.baseConstitution,
|
||||
baseIntelligence: this.baseIntelligence,
|
||||
baseWisdom: this.baseWisdom,
|
||||
baseCharisma: this.baseCharisma,
|
||||
strengthBonus: this.strengthBonus,
|
||||
dexterityBonus: this.dexterityBonus,
|
||||
constitutionBonus: this.constitutionBonus,
|
||||
intelligenceBonus: this.intelligenceBonus,
|
||||
wisdomBonus: this.wisdomBonus,
|
||||
charismaBonus: this.charismaBonus,
|
||||
hitDice: this.hitDice,
|
||||
cls: this.cls,
|
||||
};
|
||||
this.$emit('pop', char);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selectLibrary(libraryId, val) {
|
||||
if (val) {
|
||||
this.librariesSelected = union(this.librariesSelected, [libraryId]);
|
||||
} else {
|
||||
this.librariesSelected = without(this.librariesSelected, libraryId);
|
||||
}
|
||||
},
|
||||
selectLibraryCollection(libraryCollectionId, val) {
|
||||
const collection = LibraryCollections.findOne(libraryCollectionId);
|
||||
if (!collection) return;
|
||||
if (val) {
|
||||
this.libraryCollectionsSelected = union(
|
||||
this.libraryCollectionsSelected,
|
||||
[libraryCollectionId]
|
||||
);
|
||||
this.librariesSelectedByCollections = union(
|
||||
this.librariesSelectedByCollections,
|
||||
collection.libraries
|
||||
);
|
||||
} else {
|
||||
this.libraryCollectionsSelected = without(
|
||||
this.libraryCollectionsSelected,
|
||||
libraryCollectionId,
|
||||
);
|
||||
this.librariesSelectedByCollections = without(
|
||||
this.librariesSelectedByCollections,
|
||||
...collection.libraries
|
||||
);
|
||||
}
|
||||
},
|
||||
submit(){
|
||||
let char = {
|
||||
name: this.name,
|
||||
gender: this.gender,
|
||||
alignment: this.alignment,
|
||||
startingLevel: this.startingLevel,
|
||||
};
|
||||
if (!this.allSubscribedLibraries) {
|
||||
char.allowedLibraries = this.librariesSelected;
|
||||
char.allowedLibraryCollections = this.libraryCollectionsSelected;
|
||||
}
|
||||
insertCreature.call(char, (error, creatureId) => {
|
||||
if (error){
|
||||
console.error(error);
|
||||
snackbar({
|
||||
text: error.reason,
|
||||
});
|
||||
} else {
|
||||
this.$store.commit(
|
||||
'setTabForCharacterSheet',
|
||||
{id: creatureId, tab: 4}
|
||||
);
|
||||
this.$emit('pop', creatureId);
|
||||
defer(() => {
|
||||
this.$router.push({ name: 'characterSheet', params: {id: creatureId} });
|
||||
});
|
||||
return creatureId;
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.no-shadow {
|
||||
box-shadow: none;
|
||||
}
|
||||
.point-buy-table {
|
||||
width: 100%;
|
||||
}
|
||||
.point-buy-table td {
|
||||
text-align: center;
|
||||
padding: 0 8px 0 8px;
|
||||
max-width: 50px;
|
||||
}
|
||||
.point-buy-table {
|
||||
width: 100%;
|
||||
}
|
||||
.point-buy-table td {
|
||||
text-align: center;
|
||||
padding: 0 8px 0 8px;
|
||||
max-width: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
:is-selected="selectedCreature === creature._id"
|
||||
v-bind="selection ? {} : {to: creature.url}"
|
||||
:dense="dense"
|
||||
:data-id="dense ? undefined : creature._id"
|
||||
@click="$emit('creature-selected', creature._id)"
|
||||
/>
|
||||
</draggable>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="slots-to-fill">
|
||||
<v-slide-y-transition
|
||||
group
|
||||
class="d-flex flex-row flex-wrap"
|
||||
leave-absolute
|
||||
>
|
||||
<slot-card
|
||||
v-for="slot in slots"
|
||||
@@ -10,6 +10,7 @@
|
||||
:model="slot"
|
||||
class="ma-1"
|
||||
hover
|
||||
style="display: inline-block !important; transition: all 0.3s !important;"
|
||||
@ignore="ignoreSlot(slot._id)"
|
||||
@click="fillSlot(slot._id)"
|
||||
/>
|
||||
@@ -54,7 +55,7 @@ export default {
|
||||
},
|
||||
callback(nodeIds){
|
||||
if (!nodeIds || !nodeIds.length) return;
|
||||
let newPropertyId = insertPropertyFromLibraryNode.call({
|
||||
insertPropertyFromLibraryNode.call({
|
||||
nodeIds,
|
||||
parentRef: {
|
||||
'id': slotId,
|
||||
@@ -66,7 +67,6 @@ export default {
|
||||
snackbar({text: error.reason || error.message || error.toString()});
|
||||
}
|
||||
});
|
||||
return `slot-child-${newPropertyId}`;
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -100,5 +100,4 @@ export default {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
</p>
|
||||
<v-text-field
|
||||
v-if="name"
|
||||
label="Confirmation"
|
||||
outlined
|
||||
v-model="inputName"
|
||||
/>
|
||||
<div class="layout justify-center">
|
||||
|
||||
@@ -2,8 +2,19 @@
|
||||
<v-list-item
|
||||
style="min-height: 60px; min-width: 0;"
|
||||
class="px-0 font-weight-bold"
|
||||
:class="isSelected && !disabled && 'primary--text v-list-item--active'"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<v-list-item-action
|
||||
v-if="selection"
|
||||
>
|
||||
<v-checkbox
|
||||
:disabled="disabled"
|
||||
:input-value="disabled || isSelected"
|
||||
@change="e => $emit('select', e)"
|
||||
@click.stop
|
||||
/>
|
||||
</v-list-item-action>
|
||||
<v-list-item-avatar v-else>
|
||||
<shared-icon :model="model" />
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-title class="d-flex align-center">
|
||||
@@ -56,6 +67,8 @@ export default {
|
||||
open: Boolean,
|
||||
selection: Boolean,
|
||||
dense: Boolean,
|
||||
isSelected: Boolean,
|
||||
disabled: Boolean,
|
||||
},
|
||||
data(){return {
|
||||
renaming: false,
|
||||
|
||||
202
app/imports/ui/library/LibraryList.vue
Normal file
202
app/imports/ui/library/LibraryList.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<v-list
|
||||
expand
|
||||
class="library-list"
|
||||
>
|
||||
<library-list-tile
|
||||
v-for="library in librariesWithoutCollection"
|
||||
:key="library._id"
|
||||
:model="library"
|
||||
:to="{ name: 'singleLibrary', params: { id: library._id }}"
|
||||
:selection="selection"
|
||||
:is-selected="librariesSelected && librariesSelected.includes(library._id)"
|
||||
:selected-by-collection="librariesSelectedByCollections && librariesSelectedByCollections.includes(library._id)"
|
||||
:disabled="disabled"
|
||||
@select="val => $emit('select-library', library._id, val)"
|
||||
/>
|
||||
<v-list-group
|
||||
v-for="libraryCollection in libraryCollections"
|
||||
:key="libraryCollection._id"
|
||||
v-model="openCollections[libraryCollection._id]"
|
||||
group="library-collection"
|
||||
:data-id="`library-collection-${libraryCollection._id}`"
|
||||
>
|
||||
<template #activator>
|
||||
<library-collection-header
|
||||
:open="openCollections[libraryCollection._id]"
|
||||
:model="libraryCollection"
|
||||
:selection="selection"
|
||||
:is-selected="libraryCollectionsSelected && libraryCollectionsSelected.includes(libraryCollection._id)"
|
||||
:disabled="disabled"
|
||||
@select="val => $emit('select-library-collection', libraryCollection._id, val)"
|
||||
/>
|
||||
</template>
|
||||
<library-list-tile
|
||||
v-for="library in libraryCollection.libraryDocuments"
|
||||
:key="library._id"
|
||||
:model="library"
|
||||
:to="{ name: 'singleLibrary', params: { id: library._id }}"
|
||||
:selection="selection"
|
||||
:is-selected="librariesSelected && librariesSelected.includes(library._id)"
|
||||
:selected-by-collection="librariesSelectedByCollections && librariesSelectedByCollections.includes(library._id)"
|
||||
:disabled="disabled"
|
||||
class="ml-4"
|
||||
@select="val => $emit('select-library', library._id, val)"
|
||||
/>
|
||||
</v-list-group>
|
||||
<v-expand-transition>
|
||||
<v-row
|
||||
v-if="!$subReady.libraries"
|
||||
align="center"
|
||||
justify="center"
|
||||
class="pa-4"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="32"
|
||||
/>
|
||||
</v-row>
|
||||
</v-expand-transition>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import { union } from 'lodash';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import LibraryCollections, { insertLibraryCollection } from '/imports/api/library/LibraryCollections.js';
|
||||
import Libraries, { insertLibrary } from '/imports/api/library/Libraries.js';
|
||||
import LibraryListTile from '/imports/ui/library/LibraryListTile.vue'
|
||||
import LibraryCollectionHeader from '/imports/ui/library/LibraryCollectionHeader.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LibraryListTile,
|
||||
LibraryCollectionHeader,
|
||||
},
|
||||
props: {
|
||||
selection: Boolean,
|
||||
disabled: Boolean,
|
||||
librariesSelected: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
libraryCollectionsSelected: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
librariesSelectedByCollections: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data(){ return{
|
||||
loadingInsertLibraryCollection: false,
|
||||
openCollections: [],
|
||||
}},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'libraries': [],
|
||||
},
|
||||
libraryCollections(){
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) return;
|
||||
const subCollections = Meteor.user().subscribedLibraryCollections || [];
|
||||
return LibraryCollections.find({
|
||||
$or: [
|
||||
{ owner: userId },
|
||||
{ writers: userId },
|
||||
{ readers: userId },
|
||||
{ _id: { $in: subCollections }, public: true },
|
||||
]
|
||||
}, {
|
||||
sort: { name: 1 }
|
||||
}).map(libCollection => {
|
||||
libCollection.libraryDocuments = Libraries.find({
|
||||
_id: {$in: libCollection.libraries},
|
||||
$or: [
|
||||
{ owner: userId },
|
||||
{ writers: userId },
|
||||
{ readers: userId },
|
||||
{ public: true },
|
||||
]
|
||||
}, {
|
||||
sort: { name: 1 }
|
||||
}).fetch();
|
||||
return libCollection;
|
||||
});
|
||||
},
|
||||
librariesWithoutCollection() {
|
||||
const userId = Meteor.userId();
|
||||
if (!this.libraryCollections) return;
|
||||
// Collate the IDs of all the libraries in collections
|
||||
let collectedLibraries = [];
|
||||
this.libraryCollections.forEach(libCollection => {
|
||||
collectedLibraries = union(collectedLibraries, libCollection.libraries);
|
||||
});
|
||||
// return the libraries with IDs not in that list
|
||||
return Libraries.find(
|
||||
{
|
||||
_id: {$nin: collectedLibraries},
|
||||
$or: [
|
||||
{ owner: userId },
|
||||
{ writers: userId },
|
||||
{ readers: userId },
|
||||
{ public: true },
|
||||
]
|
||||
},
|
||||
{sort: {name: 1}}
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
insertLibrary() {
|
||||
const self = this;
|
||||
if (this.paidBenefits){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-creation-dialog',
|
||||
elementId: 'insert-library-button',
|
||||
callback(library){
|
||||
if (!library) return;
|
||||
return insertLibrary.call(library, (error, libraryId) => {
|
||||
if (error){
|
||||
console.error(error);
|
||||
snackbar({
|
||||
text: error.reason,
|
||||
});
|
||||
} else {
|
||||
self.$router.push({
|
||||
name: 'singleLibrary',
|
||||
params: { id: libraryId }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'tier-too-low-dialog',
|
||||
elementId: 'insert-library-button',
|
||||
});
|
||||
}
|
||||
},
|
||||
insertLibraryCollection() {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-collection-creation-dialog',
|
||||
elementId: 'insert-library-collection-button',
|
||||
callback(libraryCollection){
|
||||
if (!libraryCollection) return;
|
||||
const id = insertLibraryCollection.call(libraryCollection, error => {
|
||||
if (!error) return;
|
||||
console.error(error);
|
||||
snackbar({
|
||||
text: error.reason,
|
||||
});
|
||||
});
|
||||
return `library-collection-${id}`
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -4,9 +4,21 @@
|
||||
>
|
||||
<v-list-item
|
||||
v-bind="$attrs"
|
||||
:class="isSelected && 'primary--text v-list-item--active'"
|
||||
:class="(isSelected || selectedByCollection) && !disabled && 'primary--text v-list-item--active'"
|
||||
:to="selection ? undefined : to"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<v-list-item-action
|
||||
v-if="selection"
|
||||
>
|
||||
<v-checkbox
|
||||
:disabled="disabled"
|
||||
:input-value="disabled || isSelected"
|
||||
:off-icon="selectedByCollection ? 'mdi-checkbox-intermediate' : undefined"
|
||||
@change="e => $emit('select', e)"
|
||||
@click.stop
|
||||
/>
|
||||
</v-list-item-action>
|
||||
<v-list-item-avatar v-else>
|
||||
<shared-icon :model="model" />
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
@@ -31,7 +43,12 @@ export default {
|
||||
},
|
||||
selection: Boolean,
|
||||
isSelected: Boolean,
|
||||
dense: Boolean,
|
||||
selectedByCollection: Boolean,
|
||||
disabled: Boolean,
|
||||
to: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -74,127 +74,108 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import { defer } from 'lodash';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import insertCreature from '/imports/api/creature/creatures/methods/insertCreature.js';
|
||||
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
|
||||
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
|
||||
import insertCreatureFolder from '/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js';
|
||||
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue';
|
||||
import ArchiveButton from '/imports/ui/creature/creatureList/ArchiveButton.vue';
|
||||
import getCreatureUrlName from '/imports/api/creature/creatures/getCreatureUrlName.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
|
||||
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
|
||||
import insertCreatureFolder from '/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js';
|
||||
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue';
|
||||
import ArchiveButton from '/imports/ui/creature/creatureList/ArchiveButton.vue';
|
||||
import getCreatureUrlName from '/imports/api/creature/creatures/getCreatureUrlName.js';
|
||||
|
||||
const characterTransform = function(char){
|
||||
char.url = `/character/${char._id}/${getCreatureUrlName(char)}`;
|
||||
char.initial = char.name && char.name[0] || '?';
|
||||
return char;
|
||||
};
|
||||
export default {
|
||||
components: {
|
||||
CreatureFolderList,
|
||||
ArchiveButton,
|
||||
const characterTransform = function(char){
|
||||
char.url = `/character/${char._id}/${getCreatureUrlName(char)}`;
|
||||
char.initial = char.name && char.name[0] || '?';
|
||||
return char;
|
||||
};
|
||||
export default {
|
||||
components: {
|
||||
CreatureFolderList,
|
||||
ArchiveButton,
|
||||
},
|
||||
data(){ return{
|
||||
fab: false,
|
||||
loadingInsertFolder: false,
|
||||
renamingFolder: undefined,
|
||||
}},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'characterList': [],
|
||||
},
|
||||
data(){ return{
|
||||
fab: false,
|
||||
loadingInsertFolder: false,
|
||||
renamingFolder: undefined,
|
||||
}},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'characterList': [],
|
||||
},
|
||||
folders(){
|
||||
const userId = Meteor.userId();
|
||||
let folders = CreatureFolders.find(
|
||||
{owner: userId, archived: {$ne: true}},
|
||||
{sort: {order: 1}},
|
||||
).map(folder => {
|
||||
folder.creatures = Creatures.find(
|
||||
{
|
||||
_id: {$in: folder.creatures || []},
|
||||
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
||||
}, {
|
||||
sort: {name: 1},
|
||||
}
|
||||
).map(characterTransform);
|
||||
return folder;
|
||||
});
|
||||
return folders;
|
||||
},
|
||||
CreaturesWithNoParty() {
|
||||
var userId = Meteor.userId();
|
||||
var charArrays = CreatureFolders.find({owner: userId}).map(p => p.creatures);
|
||||
var folderChars = _.uniq(_.flatten(charArrays));
|
||||
return Creatures.find(
|
||||
folders(){
|
||||
const userId = Meteor.userId();
|
||||
let folders = CreatureFolders.find(
|
||||
{owner: userId, archived: {$ne: true}},
|
||||
{sort: {order: 1}},
|
||||
).map(folder => {
|
||||
folder.creatures = Creatures.find(
|
||||
{
|
||||
_id: {$nin: folderChars},
|
||||
_id: {$in: folder.creatures || []},
|
||||
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
||||
},
|
||||
{sort: {name: 1}}
|
||||
).map(characterTransform);
|
||||
},
|
||||
creatureCount(){
|
||||
let userId = Meteor.userId();
|
||||
return Creatures.find({
|
||||
owner: userId,
|
||||
}, {
|
||||
fields: {_id: 1},
|
||||
}).count();
|
||||
},
|
||||
tier(){
|
||||
let userId = Meteor.userId();
|
||||
return getUserTier(userId);
|
||||
},
|
||||
characterSpaceLeft(){
|
||||
let tier = this.tier;
|
||||
let currentCharacterCount = this.creatureCount;
|
||||
if (tier.characterSlots === -1) return Number.POSITIVE_INFINITY;
|
||||
return tier.characterSlots - currentCharacterCount
|
||||
},
|
||||
exceededCharacterSpace(){
|
||||
let tier = this.tier;
|
||||
let currentCharacterCount = this.creatureCount;
|
||||
return tier.characterSlots !== -1 && currentCharacterCount > tier.characterSlots
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
insertCharacter(){
|
||||
insertCreature.call((error, creatureId) => {
|
||||
if (error){
|
||||
console.error(error);
|
||||
snackbar({
|
||||
text: error.reason,
|
||||
});
|
||||
} else {
|
||||
this.$store.commit(
|
||||
'setTabForCharacterSheet',
|
||||
{id: creatureId, tab: 4}
|
||||
);
|
||||
defer(() => {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'character-creation-dialog',
|
||||
elementId: 'new-character-button',
|
||||
data: {
|
||||
creatureId,
|
||||
},
|
||||
});
|
||||
})
|
||||
this.$router.push({ path: `/character/${creatureId}` });
|
||||
}, {
|
||||
sort: {name: 1},
|
||||
}
|
||||
});
|
||||
},
|
||||
insertFolder(){
|
||||
this.loadingInsertFolder = true;
|
||||
insertCreatureFolder.call(error => {
|
||||
this.loadingInsertFolder = false;
|
||||
if (!error) return;
|
||||
console.error(error);
|
||||
snackbar({
|
||||
text: error.reason,
|
||||
});
|
||||
});
|
||||
},
|
||||
).map(characterTransform);
|
||||
return folder;
|
||||
});
|
||||
return folders;
|
||||
},
|
||||
};
|
||||
CreaturesWithNoParty() {
|
||||
var userId = Meteor.userId();
|
||||
var charArrays = CreatureFolders.find({owner: userId}).map(p => p.creatures);
|
||||
var folderChars = _.uniq(_.flatten(charArrays));
|
||||
return Creatures.find(
|
||||
{
|
||||
_id: {$nin: folderChars},
|
||||
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
||||
},
|
||||
{sort: {name: 1}}
|
||||
).map(characterTransform);
|
||||
},
|
||||
creatureCount(){
|
||||
let userId = Meteor.userId();
|
||||
return Creatures.find({
|
||||
owner: userId,
|
||||
}, {
|
||||
fields: {_id: 1},
|
||||
}).count();
|
||||
},
|
||||
tier(){
|
||||
let userId = Meteor.userId();
|
||||
return getUserTier(userId);
|
||||
},
|
||||
characterSpaceLeft(){
|
||||
let tier = this.tier;
|
||||
let currentCharacterCount = this.creatureCount;
|
||||
if (tier.characterSlots === -1) return Number.POSITIVE_INFINITY;
|
||||
return tier.characterSlots - currentCharacterCount
|
||||
},
|
||||
exceededCharacterSpace(){
|
||||
let tier = this.tier;
|
||||
let currentCharacterCount = this.creatureCount;
|
||||
return tier.characterSlots !== -1 && currentCharacterCount > tier.characterSlots
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
insertCharacter() {
|
||||
const self = this;
|
||||
self.$store.commit('pushDialogStack', {
|
||||
component: 'character-creation-dialog',
|
||||
elementId: 'new-character-button',
|
||||
callback: creatureId => creatureId,
|
||||
});
|
||||
},
|
||||
insertFolder(){
|
||||
this.loadingInsertFolder = true;
|
||||
insertCreatureFolder.call(error => {
|
||||
this.loadingInsertFolder = false;
|
||||
if (!error) return;
|
||||
console.error(error);
|
||||
snackbar({
|
||||
text: error.reason,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -10,38 +10,7 @@
|
||||
xl="8"
|
||||
>
|
||||
<v-card :class="{'mb-4': libraryCollections && libraryCollections.length}">
|
||||
<v-list
|
||||
expand
|
||||
class="library-folder-list"
|
||||
>
|
||||
<library-list-tile
|
||||
v-for="library in librariesWithoutCollection"
|
||||
:key="library._id"
|
||||
:model="library"
|
||||
:to="{ name: 'singleLibrary', params: { id: library._id }}"
|
||||
/>
|
||||
<v-list-group
|
||||
v-for="libraryCollection in libraryCollections"
|
||||
:key="libraryCollection._id"
|
||||
v-model="openCollections[libraryCollection._id]"
|
||||
group="library-collection"
|
||||
:data-id="`library-collection-${libraryCollection._id}`"
|
||||
>
|
||||
<template #activator>
|
||||
<library-collection-header
|
||||
:open="openCollections[libraryCollection._id]"
|
||||
:model="libraryCollection"
|
||||
/>
|
||||
</template>
|
||||
<library-list-tile
|
||||
v-for="library in libraryCollection.libraryDocuments"
|
||||
:key="library._id"
|
||||
:model="library"
|
||||
:to="{ name: 'singleLibrary', params: { id: library._id }}"
|
||||
class="ml-4"
|
||||
/>
|
||||
</v-list-group>
|
||||
</v-list>
|
||||
<library-list />
|
||||
<v-expand-transition>
|
||||
<v-row
|
||||
v-if="!$subReady.libraries"
|
||||
@@ -92,13 +61,11 @@ import { getUserTier } from '/imports/api/users/patreon/tiers.js';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import LibraryCollections, { insertLibraryCollection } from '/imports/api/library/LibraryCollections.js';
|
||||
import Libraries, { insertLibrary } from '/imports/api/library/Libraries.js';
|
||||
import LibraryListTile from '/imports/ui/library/LibraryListTile.vue'
|
||||
import LibraryCollectionHeader from '/imports/ui/library/LibraryCollectionHeader.vue';
|
||||
import LibraryList from '/imports/ui/library/LibraryList.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LibraryListTile,
|
||||
LibraryCollectionHeader,
|
||||
LibraryList
|
||||
},
|
||||
data(){ return{
|
||||
loadingInsertLibraryCollection: false,
|
||||
|
||||
@@ -163,7 +163,8 @@ RouterFactory.configure(router => {
|
||||
meta: {
|
||||
title: 'Library Collection',
|
||||
},
|
||||
},{
|
||||
}, {
|
||||
name: 'characterSheet',
|
||||
path: '/character/:id',
|
||||
alias: '/character/:id/:urlName',
|
||||
components: {
|
||||
|
||||
@@ -15,4 +15,4 @@ import '/imports/migrations/methods/index.js'
|
||||
import '/imports/constants/MAINTENANCE_MODE.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/index.js';
|
||||
import '/imports/api/creature/archive/methods/index.js';
|
||||
|
||||
import '/imports/api/creature/creatures/methods/index.js';
|
||||
|
||||
Reference in New Issue
Block a user