Compare commits

...

45 Commits
1.3.2 ... 1.4.0

Author SHA1 Message Date
Stefan Zermatten
73d1419ee9 Merge pull request #114 from Dumbgenius/misc-enhancements
Miscellaneous enhancements
2017-07-21 13:12:22 +02:00
Stefan Zermatten
681ef614c7 Move earth now level 6 2017-07-21 13:03:31 +02:00
Stefan Zermatten
2bdbcb2e79 Fixed speak with animals
#118
2017-07-21 13:02:50 +02:00
Stefan Zermatten
c119fcfbb8 Freezing sphere now level 6
#118
2017-07-21 13:00:28 +02:00
Stefan Zermatten
deb5db8657 Daylight now level 3 2017-07-21 12:59:54 +02:00
Stefan Zermatten
49522580e3 Replaced line breaks with double line breaks in SRD spells 2017-07-21 12:50:50 +02:00
Stefan Zermatten
5b50f20128 Fixed spell attacks from library assignment of default attack bonus 2017-07-21 12:49:04 +02:00
Stefan Zermatten
52fa97c952 removed default values for library attacks where they aren't needed 2017-07-21 12:48:03 +02:00
Stefan Zermatten
e7a5ce8241 Replaced all mentions of saving throws with DC {DC} saving throw 2017-07-21 12:02:17 +02:00
Stefan Zermatten
d1b9043e1f Added change procedure for updating from srd JSON 2017-07-21 11:53:42 +02:00
Stefan Zermatten
9ffc5649f7 Fixed tooltip suffix icons and moved textarea icons outside of textarea 2017-07-21 11:02:02 +02:00
Stefan Zermatten
15e6c12c03 Replaced shitty paper-tooltip with custom css tooltip 2017-07-21 11:01:18 +02:00
Jacob
e89b877326 Merge branch 'fixbug-116' into misc-enhancements 2017-07-20 16:18:49 +01:00
Jacob
aff2f1f438 Parties are now sorted in both character lists. 2017-07-20 16:18:15 +01:00
Jacob
71d1e9e9e8 Added links in the guide to adam-p's Markdown Cheatsheet
and to the original specifaction for Markdown.
2017-07-20 12:21:01 +01:00
Jacob
0696fd8447 Updated the guide to talk about Markdown support as well 2017-07-20 12:10:17 +01:00
Jacob
b2db33e0f3 Added attacks to SRD cantrips
they scale by level: {floor((Level+1)/6)+1}dX
2017-07-20 11:33:44 +01:00
Jacob
0c2842b84a All "At Higher Levels" are now bold and italic, like in the actual PHB 2017-07-19 12:34:01 +01:00
Jacob
789658cfe7 Spells imported from SRD now have attack bonus set by default 2017-07-19 12:31:50 +01:00
Jacob
3be3da777f Now we can use "attackBonus" in spell attacks, make that the default 2017-07-19 12:19:26 +01:00
Jacob
0e53f157d2 Spell attack bonus and DC are now available in spell attacks. 2017-07-19 12:13:49 +01:00
Jacob
24cc4fd2b1 Background proficiencies now correctly open background dialog
Previously, when clicking on non-skill proficiencies from a character's
background, it would open a blank rectangle as its parent dialog. Now, it
correctly opens the Background dialog.
2017-07-18 23:01:14 +01:00
Jacob
1f0ea689dc Multiple instances of the same proficiency are now merged
For tool/language/weapon/armour proficiencies, on the stats/persona page.
2017-07-18 22:33:28 +01:00
Jacob
11adf9da04 Merge branch 'fixbug-107' into misc-enhancements
# Conflicts:
#	rpg-docs/client/views/character/spells/spellDialog/spellDialog.html
2017-07-18 22:05:31 +01:00
Jacob
5ca81056f9 Merge branch 'feature-swap-stat-modifier' into misc-enhancements 2017-07-18 21:58:29 +01:00
Jacob
6fc469f934 Added setting to swap stats and modifiers
Although Schemas.Character was changed, a database migration should
not be required due to the way characterSettings handles it (since
undefined is a falsy value).
2017-07-18 21:58:00 +01:00
Jacob
0c7948afdd Changed all "1 action" casting times to "action" so it displays properly
i.e. it displays as "Evocation action" rather than "Evocation 1 action"
for example

Also did the same for "1 bonus action" -> "bonus action"
2017-07-18 20:24:12 +01:00
Jacob
42ffc79499 Spell list now displays whether a spell requires GP. 2017-07-18 20:22:44 +01:00
Jacob
0240209410 Removed buffViewList that shouldn't have been there 2017-07-18 20:20:04 +01:00
Jacob
c4c1afa669 Merge branch 'guide-update-1.2.8' into misc-enhancements
# Conflicts:
#	rpg-docs/client/views/guide/guide.css
#	rpg-docs/client/views/guide/guide.html
2017-07-18 19:58:35 +01:00
Jacob
47ac090e9d Merge branch 'testing_dg' into misc-enhancements
# Conflicts:
#	rpg-docs/.meteor/packages
2017-07-18 19:56:26 +01:00
Jacob
aadc83391f Adding an attack to a spell now has attack bonus set by the spell list
Default damage is also changed to 1d10 fire for spells
2017-07-18 19:52:12 +01:00
Jacob
54fb398056 Merge branch 'master' into guide-update-1.2.8 2017-07-18 11:11:37 +01:00
Jacob
073094f6dd Merge branch 'master' into fixbug-107 2017-07-18 11:11:14 +01:00
Jacob
832ed0c1ff A sort of hacky fix to the issue - text wrapping doesn't quite work 2017-07-17 04:43:39 +01:00
Jacob
e65a2db0d2 Changed guide to use Markdown rather than HTML formatting (mostly)
This is both for ease of editing and so that that it can be copied
into the GitHub wiki.

The <iron-icon>s are left in.
2017-07-16 12:07:13 +01:00
Jacob
6729bcad64 Updated the guide to reflect more recent updates (1.2.8) 2017-07-15 17:13:06 +01:00
Jacob
7af07e7ba3 Updated the guide to reflect more recent updates. 2017-07-15 17:07:39 +01:00
Jacob
f44914ab84 Added six extra loading hints. 2017-07-15 15:39:51 +01:00
Jacob
59c7eff46a Hit Dice are now called "d6 Hit Dice", etc. rather than just "d6" 2017-07-15 15:17:14 +01:00
Stefan Zermatten
2a332a2965 Merge branch 'feature-parties' 2017-07-14 17:09:47 +02:00
Stefan Zermatten
44a1daf6f8 Grouping characters by party now works
closes #75, finally
2017-07-14 17:09:30 +02:00
Stefan Zermatten
ac23afac5d Stopped characters with poor names from having failed URL's 2017-07-13 17:16:23 +02:00
Stefan Zermatten
a411fb2b43 Fixed migration for failed slug strings 2017-07-13 16:44:23 +02:00
Stefan Zermatten
35b6fe20ae Reset migrations to line up with current DiceCloud server 2017-07-13 16:16:00 +02:00
49 changed files with 1455 additions and 905 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
// This all gets run in the console by an admin.
// Probably a good idea to reset the server after running big updates
// Only do if the library doesn't exist yet // Only do if the library doesn't exist yet
id = Libraries.insert({ id = Libraries.insert({
_id: "SRDLibraryGA3XWsd", _id: "SRDLibraryGA3XWsd",
@@ -5,6 +7,8 @@ id = Libraries.insert({
name: "SRD Library", name: "SRD Library",
}); });
// First copy-paste the JSON into your console like `items = <pasted JSON>`
// First import, don't do this if the library is already populated
_.each(items, (item) => { _.each(items, (item) => {
item.settings = {category: }; // "adventuringGear", "armor", "weapons", "tools" item.settings = {category: }; // "adventuringGear", "armor", "weapons", "tools"
item.library = "SRDLibraryGA3XWsd" item.library = "SRDLibraryGA3XWsd"
@@ -15,3 +19,38 @@ _.each(spells, (spell) => {
spell.library = "SRDLibraryGA3XWsd" spell.library = "SRDLibraryGA3XWsd"
LibrarySpells.insert(spell) LibrarySpells.insert(spell)
}); });
// Update the library using names as keys
// Make sure you're subscribed to all item categories
handles = _.map(["weapons", "armor", "adventuringGear", "tools"],
category => Meteor.subscribe("standardLibraryItems", category)
);
// Wait until all the handles are ready
handles.map(h => h.ready()); // must reaturn [...true]
_.each(items, (item) => {
var existingItem = LibraryItems.findOne({
library: "SRDLibraryGA3XWsd",
name: item.name,
});
if (!existingItem) return;
_.each(item.attacks, attack => Schemas.LibraryAttacks.clean(attack));
LibraryItems.update(existingItem._id, {$set: item});
});
// Make sure you're subscribed to all spell categories
handles = _.map([0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
category => Meteor.subscribe("standardLibrarySpells", category)
);
// Wait until all the handles are ready
handles.map(h => h.ready()); // must reaturn [...true]
_.each(spells, (spell) => {
var existingSpell = LibrarySpells.findOne({
library: "SRDLibraryGA3XWsd",
name: spell.name,
});
if (!existingSpell) return;
_.each(spell.attacks, attack => Schemas.LibraryAttacks.clean(attack));
LibrarySpells.update(existingSpell._id, {$set: spell});
});

View File

@@ -49,3 +49,4 @@ differential:vulcanize
reactive-dict reactive-dict
percolate:synced-cron percolate:synced-cron
ongoworks:speakingurl ongoworks:speakingurl
service-configuration

View File

@@ -1,8 +1,42 @@
Parties = new Mongo.Collection("parties"); Parties = new Mongo.Collection("parties");
Schemas.Party = new SimpleSchema({ Schemas.Party = new SimpleSchema({
//each character/monster can only be in one party at a time name: {
//each party can only be in a single instance at a time type: String,
defaultValue: "New Party",
trim: false,
optional: true,
},
characters: {
type: [String],
regEx: SimpleSchema.RegEx.Id,
index: 1,
defaultValue: [],
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}); });
Parties.attachSchema(Schemas.Party); Parties.attachSchema(Schemas.Party);
Parties.allow({
insert: function(userId, doc) {
return userId && doc.owner === userId;
},
update: function(userId, doc, fields, modifier) {
return userId && doc.owner === userId;
},
remove: function(userId, doc) {
return userId && doc.owner === userId;
},
fetch: ["owner"],
});
Parties.deny({
update: function(userId, docs, fields, modifier) {
// can't change owners
return _.contains(fields, "owner");
}
});

View File

@@ -4,7 +4,7 @@ Characters = new Mongo.Collection("characters");
Schemas.Character = new SimpleSchema({ Schemas.Character = new SimpleSchema({
//strings //strings
name: {type: String, defaultValue: "", trim: false, optional: true}, name: {type: String, defaultValue: "", trim: false, optional: true},
urlName: {type: String, defaultValue: "", trim: false, optional: true}, urlName: {type: String, defaultValue: "-", trim: false, optional: true},
alignment: {type: String, defaultValue: "", trim: false, optional: true}, alignment: {type: String, defaultValue: "", trim: false, optional: true},
gender: {type: String, defaultValue: "", trim: false, optional: true}, gender: {type: String, defaultValue: "", trim: false, optional: true},
race: {type: String, defaultValue: "", trim: false, optional: true}, race: {type: String, defaultValue: "", trim: false, optional: true},
@@ -185,6 +185,7 @@ Schemas.Character = new SimpleSchema({
defaultValue: "whitelist", defaultValue: "whitelist",
allowedValues: ["whitelist", "public"], allowedValues: ["whitelist", "public"],
}, },
"settings.swapStatAndModifier": {type: Boolean, defaultValue: false},
"settings.exportFeatures": {type: Boolean, defaultValue: true}, "settings.exportFeatures": {type: Boolean, defaultValue: true},
"settings.exportAttacks": {type: Boolean, defaultValue: true}, "settings.exportAttacks": {type: Boolean, defaultValue: true},
"settings.exportDescription": {type: Boolean, defaultValue: true}, "settings.exportDescription": {type: Boolean, defaultValue: true},
@@ -543,10 +544,13 @@ if (Meteor.isServer){
}); });
Characters.after.update(function(userId, doc, fieldNames, modifier, options) { Characters.after.update(function(userId, doc, fieldNames, modifier, options) {
if (_.contains(fieldNames, "name")){ if (_.contains(fieldNames, "name")){
var urlName = getSlug(doc.name, {maintainCase: true}); var urlName = getSlug(doc.name, {maintainCase: true}) || "-";
Characters.update(doc._id, {$set: {urlName}}); Characters.update(doc._id, {$set: {urlName}});
} }
}); });
Characters.before.insert(function(userId, doc) {
doc.urlName = getSlug(doc.name, {maintainCase: true}) || "-";
});
} }
Characters.allow({ Characters.allow({

View File

@@ -11,13 +11,11 @@ Schemas.LibraryAttacks = new SimpleSchema({
}, },
attackBonus: { attackBonus: {
type: String, type: String,
defaultValue: "strengthMod + proficiencyBonus",
optional: true, optional: true,
trim: false, trim: false,
}, },
damage: { damage: {
type: String, type: String,
defaultValue: "1d8 + {strengthMod}",
optional: true, optional: true,
trim: false, trim: false,
}, },

View File

@@ -43,7 +43,7 @@ Router.map(function() {
var _id = this.params._id var _id = this.params._id
var character = Characters.findOne(_id); var character = Characters.findOne(_id);
var urlName = character && character.urlName; var urlName = character && character.urlName;
var path = `\/character\/${_id}\/${urlName}`; var path = `\/character\/${_id}\/${urlName || "-"}`;
Router.go(path,{},{replaceState:true}); Router.go(path,{},{replaceState:true});
}, },
}); });

View File

@@ -0,0 +1,3 @@
Template.registerHelper("characterPath", function(char) {
return `\/character\/${char._id}\/${char.urlName || "-"}`;
});

View File

@@ -7,12 +7,32 @@ Template.attackEditList.helpers({
Template.attackEditList.events({ Template.attackEditList.events({
"tap #addAttackButton": function() { "tap #addAttackButton": function() {
if (typeof this.isSpell !== 'undefined' && this.isSpell) {
var parentSpell = Spells.findOne({"_id": this.parentId})
if (parentSpell && parentSpell.parent.collection == "SpellLists") {
var spellList = SpellLists.findOne({"_id":parentSpell.parent.id});
if (spellList && spellList.attackBonus) {
Attacks.insert({
charId: this.charId,
parent: {
id: this.parentId,
collection: this.parentCollection
},
attackBonus: "attackBonus",
damage: "1d10",
damageType: "fire",
});
return;
}
}
}
Attacks.insert({ Attacks.insert({
charId: this.charId, charId: this.charId,
parent: { parent: {
id: this.parentId, id: this.parentId,
collection: this.parentCollection collection: this.parentCollection
} },
}); });
}, },
}); });

View File

@@ -1,15 +1,15 @@
<template name="attackView"> <template name="attackView">
<div class="attackView layout horizontal"> <div class="attackView layout horizontal">
<div class="paper-font-headline layout horizontal center" style="margin-right: 16px;"> <div class="paper-font-headline layout horizontal center" style="margin-right: 16px;">
{{evaluateSigned charId attackBonus}} {{evaluateAttackBonus charId attack}}
</div> </div>
<div class="layout vertical"> <div class="layout vertical">
<div> <div>
{{evaluateString charId damage}}&nbsp;{{damageType}} {{evaluateDamage charId attack}}&nbsp;{{damageType}}
</div> </div>
{{#if details}} {{#if attack.details}}
<div class="paper-font-caption"> <div class="paper-font-caption">
{{details}} {{attack.details}}
</div> </div>
{{/if}} {{/if}}
</div> </div>

View File

@@ -0,0 +1,28 @@
Template.attackView.helpers ({
evaluateAttackBonus: function(charId, attack) {
if (attack.parent.collection == "Spells") {
var spell = Spells.findOne(attack.parent.id);
if (spell) {
bonus = evaluate(charId, attack.attackBonus, {"spellListId": spell.parent.id});
}
} else {
var bonus = evaluate(charId, attack.attackBonus);
}
if (_.isFinite(bonus)) {
return bonus > 0 ? "+" + bonus : "" + bonus;
} else {
return bonus;
}
},
evaluateDamage: function(charId, attack) {
if (attack.parent.collection == "Spells") {
var spell = Spells.findOne(attack.parent.id);
if (spell) {
return evaluateSpellString(charId, spell.parent.id, attack.damage);
}
} else {
return evaluateString(charId, attack.damage);
}
},
})

View File

@@ -3,8 +3,8 @@
<hr style="margin: 16px 0 16px 0;"> <hr style="margin: 16px 0 16px 0;">
<div class="attacks"> <div class="attacks">
<div class="spaceAfter paper-font-title">Attacks</div> <div class="spaceAfter paper-font-title">Attacks</div>
{{#each attacks}} {{#each attack in attacks}}
{{> attackView}} {{> attackView attack=attack charId=charId}}
{{/each}} {{/each}}
</div> </div>
{{/if}} {{/if}}

View File

@@ -14,6 +14,9 @@
<paper-toggle-button id="variantEncumbrance" checked={{settings.useVariantEncumbrance}}> <paper-toggle-button id="variantEncumbrance" checked={{settings.useVariantEncumbrance}}>
Use variant encumbrance Use variant encumbrance
</paper-toggle-button> </paper-toggle-button>
<paper-toggle-button id="swapStatAndModifier" checked={{settings.swapStatAndModifier}}>
Swap stats and modifiers on Stats page
</paper-toggle-button>
</div> </div>
</app-header-layout> </app-header-layout>
<div class="buttons layout horizontal end-justified"> <div class="buttons layout horizontal end-justified">

View File

@@ -23,6 +23,15 @@ Template.characterSettings.events({
); );
} }
}, },
"change #swapStatAndModifier": function(event, instance){
var value = instance.find("#swapStatAndModifier").checked;
if (this.settings.swapStatAndModifier !== value){
Characters.update(
this._id,
{$set: {"settings.swapStatAndModifier": value}}
);
}
},
"click .doneButton": function(event, instance){ "click .doneButton": function(event, instance){
popDialogStack(); popDialogStack();
}, },

View File

@@ -13,6 +13,7 @@
label="Value" label="Value"
floatinglabel floatinglabel
value={{effectValue}}> value={{effectValue}}>
{{> formulaSuffix}}
</paper-input> </paper-input>
{{else}} {{else}}
<div style="height: 62px;"></div> <div style="height: 62px;"></div>

View File

@@ -53,10 +53,10 @@ var stats = [
{stat: "level7SpellSlots", name: "level 7", group: "Spell Slots"}, {stat: "level7SpellSlots", name: "level 7", group: "Spell Slots"},
{stat: "level8SpellSlots", name: "level 8", group: "Spell Slots"}, {stat: "level8SpellSlots", name: "level 8", group: "Spell Slots"},
{stat: "level9SpellSlots", name: "level 9", group: "Spell Slots"}, {stat: "level9SpellSlots", name: "level 9", group: "Spell Slots"},
{stat: "d6HitDice", name: "d6", group: "Hit Dice"}, {stat: "d6HitDice", name: "d6 Hit Dice", group: "Hit Dice"},
{stat: "d8HitDice", name: "d8", group: "Hit Dice"}, {stat: "d8HitDice", name: "d8 Hit Dice", group: "Hit Dice"},
{stat: "d10HitDice", name: "d10", group: "Hit Dice"}, {stat: "d10HitDice", name: "d10 Hit Dice", group: "Hit Dice"},
{stat: "d12HitDice", name: "d12", group: "Hit Dice"}, {stat: "d12HitDice", name: "d12 Hit Dice", group: "Hit Dice"},
{stat: "acidMultiplier", name: "Acid", group: "Weakness/Resistance"}, {stat: "acidMultiplier", name: "Acid", group: "Weakness/Resistance"},
{stat: "bludgeoningMultiplier", name: "Bludgeoning", group: "Weakness/Resistance"}, {stat: "bludgeoningMultiplier", name: "Bludgeoning", group: "Weakness/Resistance"},
{stat: "coldMultiplier", name: "Cold", group: "Weakness/Resistance"}, {stat: "coldMultiplier", name: "Cold", group: "Weakness/Resistance"},

View File

@@ -68,8 +68,11 @@
{{/if}} {{/if}}
</div> </div>
<!--description--> <!--Description-->
<paper-textarea label="Description" id="featureDescriptionInput" value={{description}}></paper-textarea> <div class="description-input layout horizontal end">
<paper-textarea id="featureDescriptionInput" label="Description" value={{description}}></paper-textarea>
{{> textareaBracketSuffix}}
</div>
{{> effectsEditList parentId=_id parentCollection="Features" charId=charId name=name enabled=enabled}} {{> effectsEditList parentId=_id parentCollection="Features" charId=charId name=name enabled=enabled}}
{{> proficiencyEditList parentId=_id parentCollection="Features" charId=charId enabled=enabled}} {{> proficiencyEditList parentId=_id parentCollection="Features" charId=charId enabled=enabled}}

View File

@@ -19,30 +19,8 @@
Attacks Attacks
</div> </div>
<div class="bottom list"> <div class="bottom list">
{{#each attacks}} {{#each attack in attacks}}
<div class="item-slot"> {{>attackListItem attack=attack charId=_id}}
<div class="flexible attack item">
<div class="layout horizontal">
<div class="paper-font-headline layout horizontal center"
style="margin-right: 16px;">
{{evaluateSigned ../_id attackBonus}}
</div>
<div class="flex layout vertical">
<div class="paper-font-body2">
{{name}}
</div>
<div>
{{evaluateString ../_id damage}}&nbsp;{{damageType}}
</div>
{{#if details}}
<div>
{{details}}
</div>
{{/if}}
</div>
</div>
</div>
</div>
{{/each}} {{/each}}
</div> </div>
</paper-material> </paper-material>
@@ -55,19 +33,19 @@
Proficiencies Proficiencies
</div> </div>
<div flex class="bottom list"> <div flex class="bottom list">
{{#if weaponProfs.count}} {{#if weaponProfs.length}}
<div class="paper-font-subhead">Weapons</div> <div class="paper-font-subhead">Weapons</div>
{{/if}} {{/if}}
{{#each weaponProfs}} {{#each weaponProfs}}
{{> proficiencyListItem}} {{> proficiencyListItem}}
{{/each}} {{/each}}
{{#if armorProfs.count}} {{#if armorProfs.length}}
<div class="paper-font-subhead">Armor</div> <div class="paper-font-subhead">Armor</div>
{{/if}} {{/if}}
{{#each armorProfs}} {{#each armorProfs}}
{{> proficiencyListItem}} {{> proficiencyListItem}}
{{/each}} {{/each}}
{{#if toolProfs.count}} {{#if toolProfs.length}}
<div class="paper-font-subhead">Tools</div> <div class="paper-font-subhead">Tools</div>
{{/if}} {{/if}}
{{#each toolProfs}} {{#each toolProfs}}
@@ -156,3 +134,29 @@
</div> </div>
{{/if}} {{/if}}
</template> </template>
<template name="attackListItem">
<div class="item-slot">
<div class="flexible attack item">
<div class="layout horizontal">
<div class="paper-font-headline layout horizontal center"
style="margin-right: 16px;">
{{evaluateAttackBonus charId attack}}
</div>
<div class="flex layout vertical">
<div class="paper-font-body2">
{{attack.name}}
</div>
<div>
{{evaluateDamage charId attack}}&nbsp;{{attack.damageType}}
</div>
{{#if attack.details}}
<div>
{{attack.details}}
</div>
{{/if}}
</div>
</div>
</div>
</div>
</template>

View File

@@ -1,3 +1,21 @@
var removeDuplicateProficiencies = function(proficiencies) {
dict = {};
proficiencies.forEach(function(prof) {
if (prof.name in dict) { //if we have already gone over another proficiency for the same thing
if (dict[prof.name].value < prof.value) {
dict[prof.name] = prof; //then take the new one if it's higher, otherwise leave it
}
} else {
dict[prof.name] = prof; //if it wasn't already there, store it
}
});
profs = []
_.forEach(dict, function(prof) {
profs.push(prof);
})
return profs;
};
Template.features.helpers({ Template.features.helpers({
features: function(){ features: function(){
var features = Features.find({charId: this._id}, {sort: {color: 1, name: 1}}); var features = Features.find({charId: this._id}, {sort: {color: 1, name: 1}});
@@ -27,13 +45,16 @@ Template.features.helpers({
return !this.alwaysEnabled; return !this.alwaysEnabled;
}, },
weaponProfs: function(){ weaponProfs: function(){
return Proficiencies.find({charId: this._id, type: "weapon"}); var profs = Proficiencies.find({charId: this._id, type: "weapon"});
return removeDuplicateProficiencies(profs);
}, },
armorProfs: function(){ armorProfs: function(){
return Proficiencies.find({charId: this._id, type: "armor"}); var profs = Proficiencies.find({charId: this._id, type: "armor"});
return removeDuplicateProficiencies(profs);
}, },
toolProfs: function(){ toolProfs: function(){
return Proficiencies.find({charId: this._id, type: "tool"}); var profs = Proficiencies.find({charId: this._id, type: "tool"});
return removeDuplicateProficiencies(profs);
}, },
}); });
@@ -61,13 +82,6 @@ Template.features.events({
element: event.currentTarget.parentElement, element: event.currentTarget.parentElement,
}); });
}, },
"click .attack": function(event){
openParentDialog({
parent: this.parent,
charId: this.charId,
element: event.currentTarget,
});
},
"click .useFeature": function(event){ "click .useFeature": function(event){
var featureId = this._id; var featureId = this._id;
Features.update(featureId, {$inc: {used: 1}}); Features.update(featureId, {$inc: {used: 1}});
@@ -133,3 +147,42 @@ Template.resource.events({
}); });
}, },
}); });
Template.attackListItem.helpers({
evaluateAttackBonus: function(charId, attack) {
if (attack.parent.collection == "Spells") {
var spell = Spells.findOne(attack.parent.id);
if (spell) {
bonus = evaluate(charId, attack.attackBonus, {"spellListId": spell.parent.id});
}
} else {
var bonus = evaluate(charId, attack.attackBonus);
}
if (_.isFinite(bonus)) {
return bonus > 0 ? "+" + bonus : "" + bonus;
} else {
return bonus;
}
},
evaluateDamage: function(charId, attack) {
if (attack.parent.collection == "Spells") {
var spell = Spells.findOne(attack.parent.id);
if (spell) {
return evaluateSpellString(charId, spell.parent.id, attack.damage);
}
} else {
return evaluateString(charId, attack.damage);
}
},
});
Template.attackListItem.events({
"click .attack": function(event, instance){
openParentDialog({
parent: instance.data.attack.parent,
charId: instance.data.charId,
element: event.currentTarget,
});
},
});

View File

@@ -20,9 +20,11 @@
</div> </div>
<hr class="vertMargin"> <hr class="vertMargin">
<div class="description-input layout horizontal end">
<paper-textarea label="Description" id="containerDescriptionInput" value={{description}}> <paper-textarea label="Description" id="containerDescriptionInput" value={{description}}>
</paper-textarea> </paper-textarea>
{{> textareaBracketSuffix}}
</div>
</template> </template>
<template name="containerView"> <template name="containerView">

View File

@@ -61,12 +61,10 @@
</div> </div>
<!--Description--> <!--Description-->
<paper-textarea id="itemDescriptionInput" label="Description" value={{description}}> <div class="description-input layout horizontal end">
<div suffix> <paper-textarea id="itemDescriptionInput" label="Description" value={{description}}></paper-textarea>
<paper-tooltip position="left" animation-delay="0">This field accepts formulae in {curly brackets}</paper-tooltip> {{> textareaBracketSuffix}}
<iron-icon icon="dicecloud:code-braces"></iron-icon> </div>
</div>
</paper-textarea>
<!--Effects--> <!--Effects-->
{{> effectsEditList parentId=_id parentCollection="Items" charId=charId enabled=equipped name=name}} {{> effectsEditList parentId=_id parentCollection="Items" charId=charId enabled=equipped name=name}}
<!--Attacks--> <!--Attacks-->

View File

@@ -7,6 +7,24 @@ var colorMap = {
backstory: "j", backstory: "j",
}; };
var removeDuplicateProficiencies = function(proficiencies) {
dict = {};
proficiencies.forEach(function(prof) {
if (prof.name in dict) { //if we have already gone over another proficiency for the same thing
if (dict[prof.name].value < prof.value) {
dict[prof.name] = prof; //then take the new one if it's higher, otherwise leave it
}
} else {
dict[prof.name] = prof; //if it wasn't already there, store it
}
});
profs = []
_.forEach(dict, function(prof) {
profs.push(prof);
})
return profs;
};
Template.persona.helpers({ Template.persona.helpers({
characterDetails: function(){ characterDetails: function(){
var char = Characters.findOne( var char = Characters.findOne(
@@ -33,7 +51,8 @@ Template.persona.helpers({
}; };
}, },
languages: function(){ languages: function(){
return Proficiencies.find({charId: this._id, type: "language"}); var profs = Proficiencies.find({charId: this._id, type: "language"});
return removeDuplicateProficiencies(profs);
}, },
}); });

View File

@@ -15,6 +15,22 @@ Template.proficiencyListItem.helpers({
Template.proficiencyListItem.events({ Template.proficiencyListItem.events({
"click .proficiency": function(event, instance){ "click .proficiency": function(event, instance){
if (this.parent.collection == "Characters") {
if (this.parent.group == "background") {
pushDialogStack({
template: "backgroundDialog",
data: {
"charId": this.charId,
"field":"background",
"title":"Background",
"color":"j",
},
element: event.currentTarget,
})
return;
}
}
openParentDialog({ openParentDialog({
parent: this.parent, parent: this.parent,
charId: this.charId, charId: this.charId,

View File

@@ -111,8 +111,10 @@
</paper-checkbox> </paper-checkbox>
</div> </div>
<!--Description--> <div class="description-input layout horizontal end">
<paper-textarea id="descriptionInput" label="Description" value="{{description}}"> <paper-textarea id="descriptionInput" label="Description" style="width: calc(100% - 24px)" value={{description}}></paper-textarea>
</paper-textarea> {{> textareaBracketSuffix}}
{{> attackEditList parentId=_id parentCollection="Spells" charId=charId enabled=true name=name}} </div>
{{> attackEditList parentId=_id parentCollection="Spells" charId=charId enabled=true name=name isSpell=true}}
</template> </template>

View File

@@ -40,8 +40,11 @@
{{> formulaSuffix}} {{> formulaSuffix}}
</paper-input> </paper-input>
<!--Description--> <!--Description-->
<paper-textarea id="spellListDescriptionInput" label="Description" value={{description}}> <div class="description-input layout horizontal end">
</paper-textarea> <paper-textarea id="spellListDescriptionInput" label="Description" value={{description}}>
</paper-textarea>
{{> textareaBracketSuffix}}
</div>
</div> </div>
{{/baseDialog}} {{/baseDialog}}
{{/with}} {{/with}}

View File

@@ -11,6 +11,12 @@ var spellLevels = [
{name: "Level 9", level: 9}, {name: "Level 9", level: 9},
]; ];
var materialNeedsGp = function(string) {
if (!string) return false;
gpRegExp = /\b[0-9]+ ?(cp|sp|gp)\b/i;
return gpRegExp.test(string);
}
const showUnprepared = (listId) => { const showUnprepared = (listId) => {
return Session.get(`showUnprepared.${listId}`); return Session.get(`showUnprepared.${listId}`);
} }
@@ -70,6 +76,7 @@ Template.spells.helpers({
} }
if (this.components.material){ if (this.components.material){
components += components ? ", M" : "M"; components += components ? ", M" : "M";
if (materialNeedsGp(this.components.material)) {components += "gp";}
} }
if (this.components.concentration){ if (this.components.concentration){
components += components ? ", C" : "C"; components += components ? ", C" : "C";
@@ -268,6 +275,7 @@ Template.spells.events({
Spells.insert(spell); Spells.insert(spell);
// Copy over attacks and effects // Copy over attacks and effects
_.each(result.attacks, (attack) => { _.each(result.attacks, (attack) => {
if (!("attackBonus" in attack)) {attack.attackBonus = "attackBonus"} //if no attack bonus provided, use spell list's
attack.charId = charId; attack.charId = charId;
attack.parent = {id: spellId, collection: "Spells"}; attack.parent = {id: spellId, collection: "Spells"};
Attacks.insert(attack); Attacks.insert(attack);
@@ -277,6 +285,31 @@ Template.spells.events({
effect.parent = {id: spellId, collection: "Spells"}; effect.parent = {id: spellId, collection: "Spells"};
Effects.insert(effect); Effects.insert(effect);
}); });
/******[UNCOMMENT ONCE BUFFS ARE ADDED]*******
_.each(result.buffs, (buff) => {
buff.charId = charId;
buff.parent = {id: spellId, collection: "Spells"};
buffId = Buffs.insert(buff);
_.each(buff.attacks, (attack) => {
if (!(attackBonus in attack)) {attack.attackBonus = "attackBonus"} //if no attack bonus provided, use spell list's
attack.charId = charId;
attack.parent = {id: buffId, collection: "Buffs"};
Attacks.insert(attack);
});
_.each(buff.effects, (effect) => {
effect.charId = charId;
effect.parent = {id: buffId, collection: "Buffs"};
Effects.insert(effect);
});
_.each(buff.proficiencies, (prof) => {
prof.charId = charId;
prof.parent = {id: buffId, collection: "Buffs"};
Proficiencies.insert(prof);
});
});
*******[UNCOMMENT ONCE BUFFS ARE ADDED]******/
}, },
returnElement: () => $(`[data-id='${spellId}']`).get(0), returnElement: () => $(`[data-id='${spellId}']`).get(0),
}) })

View File

@@ -2,12 +2,21 @@
<div> <div>
<paper-material class="ability-mini-card layout horizontal"> <paper-material class="ability-mini-card layout horizontal">
<div class="numbers"> <div class="numbers">
{{#if swap}}
<div class="paper-font-display1 stat">
{{abilityMod}}
</div>
<div class="paper-font-subhead modifier">
{{characterCalculate "attributeValue" ../_id ability}}
</div>
{{else}}
<div class="paper-font-display1 stat"> <div class="paper-font-display1 stat">
{{characterCalculate "attributeValue" ../_id ability}} {{characterCalculate "attributeValue" ../_id ability}}
</div> </div>
<div class="paper-font-subhead modifier"> <div class="paper-font-subhead modifier">
{{abilityMod}} {{abilityMod}}
</div> </div>
{{/if}}
</div> </div>
<div class="paper-font-subhead title flex layout horizontal center"> <div class="paper-font-subhead title flex layout horizontal center">
{{title}} {{title}}

View File

@@ -5,5 +5,10 @@ Template.abilityMiniCard.helpers({
Template.parentData()._id, this.ability Template.parentData()._id, this.ability
) )
); );
} },
swap: function() {
var character = Characters.findOne({"_id": Template.parentData()._id})
if (character) {return character.settings.swapStatAndModifier;}
else {return false;}
},
}); });

View File

@@ -8,8 +8,16 @@
position: relative; position: relative;
} }
.character-card .image { .partyHeader {
display: inline-block;
}
.partyHeader iron-icon {
visibility: hidden;
}
.partyHeader:hover iron-icon{
visibility: initial;
} }
.character-card .initials { .character-card .initials {

View File

@@ -10,31 +10,27 @@
{{#if currentUser}} {{#if currentUser}}
{{#if characters.count}} {{#if characters.count}}
<div class="character-list layout horizontal wrap"> <div class="character-list layout horizontal wrap">
{{# each characters}} {{# each charactersWithNoParty}}
<a class="character-card flex layout vertical end-justified" href="{{pathFor route="characterSheet" data=this}}"> {{> characterCard}}
<iron-image class="fit {{colorClass}}"
sizing="cover" preload fade src={{picture}}>
</iron-image>
{{#unless picture}}
<div class="fit initials layout vertical center center-justified">
{{initials name}}
</div>
{{/unless}}
<paper-item>
<paper-item-body two-lines>
<div class="name white87">
{{name}}
</div>
<div secondary style="color: #8a8a8a; color: rgba(255,255,255,0.87);">
{{alignment}} {{gender}} {{race}}
</div>
</paper-item-body>
</paper-item>
<paper-ripple></paper-ripple>
</a>
{{/each}} {{/each}}
{{> gridPadding class="character-card flex layout vertical" num=12}} {{> gridPadding class="character-card flex layout vertical" num=12}}
</div> </div>
{{# each party in parties}}
<div class="party" data-id={{party._id}}>
{{#with party}}
<div class="partyHeader clickable paper-font-title padded">
{{name}}
<iron-icon icon="create"></iron-icon>
</div>
{{/with}}
<div class="character-list layout horizontal wrap">
{{# each charactersInParty party._id}}
{{> characterCard}}
{{/each}}
{{> gridPadding class="character-card flex layout vertical" num=12}}
</div>
</div>
{{/each}}
{{else}} {{else}}
<div layout vertical center center-justified class="padded"> <div layout vertical center center-justified class="padded">
<div>You don't seem to have any characters yet</div> <div>You don't seem to have any characters yet</div>
@@ -47,9 +43,46 @@
</div> </div>
{{/if}} {{/if}}
<div class="fab-buffer"></div> <div class="fab-buffer"></div>
<paper-fab class="floatyButton addCharacter" {{#fabMenu}}
icon="add" <div>
title="Add"></paper-fab> <paper-fab icon="social:group"
class="addParty"
mini>
</paper-fab>
<paper-tooltip position="left"> New Party </paper-tooltip>
</div>
<div>
<paper-fab icon="face"
class="addCharacter"
mini>
</paper-fab>
<paper-tooltip position="left"> New Character </paper-tooltip>
</div>
{{/fabMenu}}
</div> </div>
</app-header-layout> </app-header-layout>
</template> </template>
<template name="characterCard">
<a class="character-card flex layout vertical end-justified" href="{{characterPath this}}">
<iron-image class="fit {{colorClass}}"
sizing="cover" preload fade src={{picture}}>
</iron-image>
{{#unless picture}}
<div class="fit initials layout vertical center center-justified">
{{initials name}}
</div>
{{/unless}}
<paper-item>
<paper-item-body two-lines>
<div class="name white87">
{{name}}
</div>
<div secondary style="color: #8a8a8a; color: rgba(255,255,255,0.87);">
{{alignment}} {{gender}} {{race}}
</div>
</paper-item-body>
</paper-item>
<paper-ripple></paper-ripple>
</a>
</template>

View File

@@ -1,35 +1,60 @@
Template.characterList.helpers({ Template.characterList.helpers({
characters(){ characters() {
var userId = Meteor.userId(); var userId = Meteor.userId();
return Characters.find( return Characters.find(
{ {$or: [{readers: userId}, {writers: userId}, {owner: userId}]},
$or: [ {sort: {name: 1}}
{readers: userId},
{writers: userId},
{owner: userId},
]
},
{
fields: {
name: 1,
urlName: 1,
picture: 1,
color: 1,
race: 1,
alignment: 1,
gender: 1,
},
sort: {name: 1},
}
); );
}, },
parties() {
return Parties.find(
{owner: Meteor.userId()},
{sort: {name: 1}},
);
},
charactersInParty(partyId) {
var userId = Meteor.userId();
var party = Parties.findOne(partyId);
return Characters.find(
{
_id: {$in: party.characters},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
},
{sort: {name: 1}}
);
},
charactersWithNoParty() {
var userId = Meteor.userId();
var charArrays = Parties.find({owner: userId}).map(p => p.characters);
var partyChars = _.uniq(_.flatten(charArrays));
return Characters.find(
{
_id: {$nin: partyChars},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
},
{sort: {name: 1}}
);
},
});
Template.characterCard.helpers({
initials(name){ initials(name){
return name.replace(/[^A-Z]/g, ""); return name.replace(/[^A-Z]/g, "");
}, },
}) });
Template.characterList.events({ Template.characterList.events({
"tap .addCharacter": function(event, template) { "click .partyHeader": function(event, instance){
pushDialogStack({
template: "partyDialog",
data: {
_id: this._id,
startEditing: true,
},
element: event.currentTarget.parentElement,
});
},
"click .addCharacter": function(event, instance) {
pushDialogStack({ pushDialogStack({
template: "newCharacterDialog", template: "newCharacterDialog",
element: event.currentTarget, element: event.currentTarget,
@@ -37,8 +62,23 @@ Template.characterList.events({
if (!character) return; if (!character) return;
character.owner = Meteor.userId(); character.owner = Meteor.userId();
let _id = Characters.insert(character); let _id = Characters.insert(character);
Router.go("characterSheet", {_id}); let urlName = getSlug(character.name, {maintainCase: true}) || "-"
Router.go("characterSheet", {_id, urlName});
}, },
}) })
}, },
"click .addParty": function(event, instance) {
var partyId = Parties.insert({
owner: Meteor.userId(),
});
pushDialogStack({
template: "partyDialog",
data: {
_id: partyId,
startEditing: true,
},
element: event.currentTarget,
returnElement: instance.find(`.party[data-id='${partyId}']`),
});
},
}); });

View File

@@ -2,8 +2,21 @@
prevent character names from wrapping prevent character names from wrapping
*/ */
.character-name { .side-list .character-name {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.side-list .partyHead {
font-weight: 500;
cursor: pointer;
}
.side-list .partyHead iron-icon {
transition: transform 0.3s ease;
}
.side-list .partyHead iron-icon.open {
transform: rotate(90deg);
}

View File

@@ -1,15 +1,31 @@
<template name="characterSideList"> <template name="characterSideList">
{{#if characters.count}} <div class="side-list">
<div class="side-list"> {{#each charactersWithNoParty}}
{{#each characters}} <a href={{characterPath this}} tabindex="-1" class="side-list-character characterRepresentative">
<a href={{pathFor route="characterSheet" data=this}} tabindex="-1" class="side-list-character characterRepresentative"> <paper-item class="short">
<paper-item class="short"> <div class="character-name">
<div class="character-name"> {{name}}
{{name}} </div>
</div> </paper-item>
</paper-item> </a>
</a> {{/each}}
{{/each}} {{#each parties}}
</div> <div class="paper-font-subhead partyHead">
{{/if}} <iron-icon icon="chevron-right" class="{{#if isOpen _id}}open{{/if}}">
</iron-icon>
{{name}}
</div>
<iron-collapse opened={{isOpen _id}}>
{{#each charactersInParty}}
<a href={{characterPath this}} tabindex="-1" class="side-list-character characterRepresentative">
<paper-item class="short">
<div class="character-name">
{{name}}
</div>
</paper-item>
</a>
{{/each}}
</iron-collapse>
{{/each}}
</div>
</template> </template>

View File

@@ -1,33 +1,52 @@
Template.characterSideList.onCreated(function() { Template.characterSideList.onCreated(function() {
this.subscribe("characterList"); this.subscribe("characterList");
this.openedParties = new ReactiveVar(new Set());
}); });
Template.characterSideList.helpers({ Template.characterSideList.helpers({
characters: function() { parties() {
return Parties.find(
{owner: Meteor.userId()},
{sort: {name: 1}},
);
},
charactersInParty() {
var userId = Meteor.userId(); var userId = Meteor.userId();
return Characters.find( return Characters.find(
{ {
$or: [ _id: {$in: this.characters},
{readers: userId}, $or: [{readers: userId}, {writers: userId}, {owner: userId}],
{writers: userId},
{owner: userId},
]
}, },
{ {sort: {name: 1}}
fields: {name: 1, urlName: 1},
sort: {name: 1},
}
); );
}, },
charactersWithNoParty() {
var userId = Meteor.userId();
var charArrays = Parties.find({owner: userId}).map(p => p.characters);
var partyChars = _.uniq(_.flatten(charArrays));
return Characters.find(
{
_id: {$nin: partyChars},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
},
{sort: {name: 1}}
);
},
isOpen(id) {
var openedParties = Template.instance().openedParties.get();
console.log(openedParties);
return openedParties.has(id);
},
}); });
Template.characterSideList.events({ Template.characterSideList.events({
"tap .singleLineItem": function(event, instance) { "click .partyHead": function(event, instance){
//Router.go("characterSheet", {_id: this._id}); var openedParties = instance.openedParties.get();
$("core-drawer-panel").get(0).closeDrawer(); if (openedParties.has(this._id)){
}, openedParties.delete(this._id);
"tap core-item": function() { } else {
Router.go("characterList"); openedParties.add(this._id);
$("core-drawer-panel").get(0).closeDrawer(); }
instance.openedParties.set(openedParties);
}, },
}); });

View File

@@ -0,0 +1,3 @@
.partyEdit .inPartyCheckbox {
margin-bottom: 8px;
}

View File

@@ -0,0 +1,32 @@
<template name="partyDialog">
{{#with party}}
{{#baseDialog title=name hideColor=true startEditing=true}}
{{> partyDetails}}
{{else}}
{{> partyEdit}}
{{/baseDialog}}
{{/with}}
</template>
<template name="partyDetails">
<div class="fit layout vertical partyDetails" style="padding: 24px;">
<div>
{{#each character in getCharacters}}
<div>{{character.name}}</div>
{{/each}}
</div>
</div>
</template>
<template name="partyEdit">
<div class="layout vertical partyEdit" style="padding: 24px;">
<paper-input class="partyNameInput" value={{name}} label="Party name">
</paper-input>
{{#each allCharacters}}
<paper-checkbox checked={{charInParty _id}}
class="inPartyCheckbox">
{{name}}
</paper-checkbox>
{{/each}}
</div>
</template>

View File

@@ -0,0 +1,62 @@
Template.partyDialog.helpers({
party(){
return Parties.findOne(this._id);
}
});
Template.partyDetails.helpers({
getCharacters (){
var userId = Meteor.userId();
return Characters.find(
{
_id: {$in: this.characters},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
},
{sort: {name: 1}}
);
}
});
Template.partyEdit.helpers({
allCharacters() {
var userId = Meteor.userId();
return Characters.find(
{$or: [{readers: userId}, {writers: userId}, {owner: userId}]},
{sort: {name: 1}}
);
},
charInParty(charId) {
return _.contains(Template.parentData().characters, charId);
},
});
Template.partyDialog.events({
"click #deleteButton": function(event, instance){
Parties.remove(instance.data._id);
popDialogStack();
},
"click #doneEditingButton": function(event, instance){
popDialogStack();
},
});
Template.partyEdit.events({
"change .inPartyCheckbox": function(event, instance){
var currentCharacters = this.characters;
var checked = event.currentTarget.checked;
var charId = this._id;
var partyId = instance.data._id;
if (checked){
Parties.update(partyId, {$addToSet: {characters: charId}});
} else {
Parties.update(partyId, {$pull: {characters: charId}});
}
},
"input .partyNameInput": function(event, instance){
var name = event.currentTarget.value;
Parties.update(this._id, {$set: {name}}, {
removeEmptyStrings: false,
trimStrings: false,
});
},
});

View File

@@ -4,4 +4,8 @@
.wallOfText p{ .wallOfText p{
margin-top: 8px; margin-top: 8px;
}
.wallOfText a{
color: #d13b2e;
} }

View File

@@ -7,92 +7,127 @@
</app-toolbar> </app-toolbar>
</app-header> </app-header>
<div class="layout vertical center"> <div class="layout vertical center">
<paper-material class="wallOfText card" style="padding: 32px; max-width: 800px;"> <paper-material class="wallOfText card" style="padding: 32px; max-width: 800px;"> {{#markdown}}
<h2>Character Sheet Philosophy</h2>
<p>Setting up your character on DiceCloud is going to take you a little longer than just filling it in on a paper character sheet would have. The goal of using an online sheet is to make actually playing the game more streamlined, and ultimately more fun. So putting a little extra effort into setting up your character now will pay off over and over again once you're playing.</p>
<p>The idea is to track where each number comes from, and allow you to easily make changes on the fly.</p>
<p>Lets look at a hypothetical example.</p>
<p>You need to swim through a sunken section of dungeon to fetch the quest's Thing.<br>You'll need to take off your magical Plate Armor of +1 Constitution to swim without sinking, of course. Taking it off will change your armor class, your speed and your constitution, which in turn changes your hitpoints and your constitution saving throw. Working out all those changes in the middle of a game will drag the game to a hault. <br> Fortunately you have a digital character sheet, so it's a matter of dragging your Plate Armor +1 Con from your "equipment" box to your "backpack" box and you're done. Your hitpoints change correctly, your saving throws are up to date, your armor class goes back to reflecting the fact that you have natural armor from being a dragonborn. Your character sheet keeps up and you ultimately get more time to play the game. Huzzah!</p>
<h2>Creating a Character</h2>
<ul>
<li>In the <a href={{pathFor route="characterList"}}>character list</a>, click the plus button, floating in the bottom right corner.</li>
<li>Give your character a name, gender and race, these can be changed later if you change your mind. Then click the Add button</li>
<li>Your new character should open, with most of its attributes and abilities at zero.</li>
</ul>
<h2>Adding Racial Effects</h2> ## Character Sheet Philosophy
<p>You have already given your character a race, but you haven't yet specified what that race does for your character, so lets do that.</p>
<ul>
<li>Click the Journal tab</li>
<li>In the card that displays your level, click on your race to open the racial dialog box</li>
<li>Click the edit button in the top corner of the racial dialog</li>
</ul>
<p>In the edit mode of the racial dialog you can change your race's name and add effects and proficiencies your race gives you. We will only be adding the base traits our race gives us, specific features can go in the features tab so we can more easily reference them later.</p>
<p>Lets add some of the effects all races will give.</p>
<ul>
<li>Click the Add Effect button, a new effect should appear</li>
<li>In the Stat Group dropdown box, choose "Stats"</li>
<li>The second dropdown lets us choose which stat to effect, choose "Speed"</li>
<li>The third dropdown lets us choose how to effect that stat, choose "Base Value", since our character's base speed comes from their race</li>
<li>Finally, input the value for our characters speed, it'll probably be 30 unless you chose a slower race, such as a dwarf</li>
<li>Close the Race dialog and navigate to the Stats tab</li>
<li>The speed card should now correctly display the character's speed</li>
<li>Click the speed card to see how that value was calculated</li>
<li>Currently there is only one number effecting the total, the speed from our race, but as more effects from different sources start impacting our character's speed, they will show up here.</li>
</ul>
<h2>Adding your ability scores</h2> Setting up your character on DiceCloud is going to take you a little longer than just filling it in on a paper character sheet would have. The goal of using an online sheet is to make actually playing the game more streamlined, and ultimately more fun. So putting a little extra effort into setting up your character now will pay off over and over again once you're playing.
<p>Your character currently doesn't have any ability scores, so lets fix that. Whether you roll your abilities or point-buy them, lets add a feature to represent where they came from</p>
<ul>
<li>Navigate to the <emd>Features</emd> tab</li>
<li>Click the plus button in the bottom right to add a new feature</li>
<li>Give the Feature a name, like <em>Point Buy</em></li>
<li>Leave the feature as always enabled, don't limit its uses, and leave the description blank</li>
<li>Click the <em>Add Effect</em> button</li>
<li>For <em>Stat Group</em> choose <em>Ability Scores</em></li>
<li>For <em>Stat</em> choose <em>Strength</em></li>
<li>For the operation choose <em>Base Value</em></li>
<li>Input your character's rolled or point-bought strength, without the racial modfier</li>
<li>Repeat for the rest of your ability scores</li>
</ul>
<p>You can now check that your ability scores appear on your <em>Stats</em> page and that your skills that use them have their values calculated accordingly.</p>
<p>We didn't include your character's racial ability modifiers in the feature, so you should go back to your character's racial dialog and add them in there as effects. Remember to use the add operation, rather than base value, since your race adds to your ability scores.</p>
<p>By separating the source of your character's stats you can easily check how your character got their ability scores and stats, even after 20 levels, without getting confused or making mistakes.</p>
<h2>Adding a Class</h2> The idea is to track where each number comes from, and allow you to easily make changes on the fly.
<p>Currently your character is at level 0, because they don't have any class levels. Let's fix that.</p> Let's look at a hypothetical example.
<ul>
<li>Click the plus button in the card that currently says "Level 0"</li> You need to swim through a sunken section of dungeon to fetch the quest's Thing.
<li>A new class has now been added, name the class in the Class Name input and leave the level as 1</li> You'll need to take off your magical Plate Armor of +1 Constitution to swim without sinking, of course. Taking it off will change your armor class, your speed and your constitution, which in turn changes your hit points and your constitution saving throw. Working out all those changes in the middle of a game will drag the game to a halt.
</ul> Fortunately you have a digital character sheet, so it's a matter of dragging your Plate Armor +1 Con from your "equipment" box to your "backpack" box and you're done. Your hitpoints change correctly, your saving throws are up to date, your armor class goes back to reflecting the fact that you have natural armor from being a dragonborn. Your character sheet keeps up and you ultimately get more time to play the game. Huzzah!
<p>We now have a class, lets add the saving throw proficiencies it gives us.</p>
<ul> ---
<li>Click the Add Proficiency button</li>
<li>Click the dropdown box that currently has "Skill" selected, and choose "Saving Throw" instead</li> ## Creating a Character
<li>In the second dropdown choose the first saving throw your class gives you</li>
<li>The third dropdown lets us specify if we have half or double our proficiency bonus for this proficiency, leave it at the default "proficient" for now</li> - In the [character list]({{pathFor route="characterList"}}), click the plus button, floating in the bottom right corner.
</ul> - Give your character a name, gender and race - these can all be changed later if you change your mind. Then click the Add button.
<p>If you navigate back to the stat page, you will see that you now have a proficiency bonus, based on your class level, and the saving throw you are proficienct in will take your proficiency bonus into account.</p> - Your new character should open, with your ability scores at a default of 10, but most other attributes at zero.
<p>One of the most important things your class gives you is your hitpoints, so lets go add those now.</p>
<ul>
<li>Navigate to the class dialog box by clicking on your class name in the journal tab and hitting the edit button</li> ## Adding Racial Effects
<li>Click the Add Effect button</li> You have already given your character a race, but you haven't yet specified what that race does for your character, so let's do that.
<li>Choose the <em>Stats</em> stat group, and choose the <em>Hitpoints</em> stat</li>
<li>Choose the <em>Base Value</em> operation</li> - Click the Journal tab.
</ul> - In the card that displays your level, click on your race to open the racial dialog box.
<p>Now we need to decide how many hitpoints our class gives us. We will assume that we take the constant hitpoints per level, since it's both the rule used for league play and it's statistically advantageous over rolling for hitpoints every level.</p> - Click the edit button (the pencil icon) in the top corner of the racial dialog.
<p>We could work out our hit points every level and change the effect each time, but we can do one better, we can input the calculation directly into the value field and have the character sheet figure it out for us</p>
<p>Let's assume we are rolling a fighter, so in the class name you typed in "Fighter" (with the capital F, but without the quote marks). A fighter gets 10 hp at first level and 6 hitpoints every level after that.</p> In the edit mode of the racial dialog you can change your race's name and add effects and proficiencies your race gives you. We will only be adding the base traits our race gives us, specific features can go in the features tab so we can more easily reference them later.
<p>Lets rather split that into 4 bonus hitpoints at first level, and 6 hitpoints for every fighter level your character has. We can the write this as <em>4 + 6*FighterLevel</em> where the * represents multiplication.</p>
<p><em>Note, we don't add the constitution modifier here, that's already taken care of by default, since all characters add their constitution modifier to their hit points</em></p> Let's add some of the effects all races will give.
<ul>
<li>In the value field input <em>4 + 6 * FighterLevel</em>, the spaces aren't needed, but you must spell your class name exactly as it is spelt in the class name input box, capital letters and all, in our case "Fighter"</li> - Click the Add Effect button; a new window will open - this is the effect edit dialog.
<li>Create a new effect that effects the base value of <em>d10 Hit Dice</em> with the value of <em>FighterLevel</em>, since we also get our fighters level worth of hit dice</li> - In the left menu, scroll down to "Stats" and choose "Speed".
<li>Check how your changes are reflected in the <em>Stats</em> tab</li> - The right menu let's us choose how to effect that stat. Choose "Base Value", since our character's base speed comes from their race.
<li>Change your level and check that the <em>Stats</em> tab gets updated accordingly</li> - Finally, input the value for our characters speed, it'll probably be 30 unless you chose a slower race, such as a dwarf.
</ul> - Close the Race dialog and navigate to the Stats tab.
<p>You can try all sorts of calculations in your effects and in certain other places too. For example if you had some feature that is used a number of times equal to your wisdom modifier or 1, whichever is lower, you could limit its uses to <em>min(1, wisdomMod)</em> and the character sheet will figure it out for you, and update itself if you wisdom modifier happens to change later.</p> - The speed card should now correctly display the character's speed.
</paper-material> - Click the speed card to see how that value was calculated.
- Currently there is only one number effecting the total, the speed from our race, but as more effects from different sources start impacting our character's speed, they will show up here.
You can now also add any other *stat changes* given yo you by your race, for example a human's +1 to each ability score, or an elf's +2 Dexterity.
## Adding your ability scores
Your character's ability scores are currently all 10 by default - which means that they're no better than your average commoner! Whether you roll your abilities, point-buy them, or just use the standard set of values, you'll need to update them.
- Navigate to the *Features* tab.
- Select the *Base Ability Scores* feature, which was added automatically.
- Click the edit button (the pencil icon) in the top right corner.
- Click the pencil icon to the right of your character's Strength to open the effect edit dialog.
- Input your character's rolled or point-bought strength, *without* the racial modifier.
- Notice that the operation is *Base Value* by default - this is what we want, as it is the character's *base* Strength score.
- Repeat for the rest of your ability scores.
You can now check that your ability scores appear on your *Stats* page and that your skills that use them have their values calculated accordingly.
We didn't include your character's racial ability modifiers in the feature, so you should go back to your character's racial dialog and add them in there as effects. Remember to use the add operation, rather than base value, since your race adds to your ability scores.
By separating the source of your character's stats you can easily check how your character got their ability scores and stats, even after 20 levels, without getting confused or making mistakes.</p>
## Adding a Class
Currently your character is at level 0, because they don't have any class levels. Let's fix that.
- Click the plus button in the card that currently says "Level 0"
- A new class has now been added, name the class in the Class Name input and leave the level as 1.
We now have a class, let's add the saving throw proficiencies it gives us.
- Click the Add Proficiency button
- Click the dropdown box that currently has "Skill" selected, and choose "Saving Throw" instead
- In the second dropdown choose the first saving throw your class gives you
- The third dropdown let's us specify if we have half or double our proficiency bonus for this proficiency, leave it at the default "proficient" for now
If you navigate back to the stat page, you will see that you now have a proficiency bonus, based on your class level, and the saving throw you are proficienct in will take your proficiency bonus into account.
One of the most important things your class gives you is your hit points, so let's go add those now.
- Navigate to the class dialog box by clicking on your class name in the journal tab and hitting the edit button
- Click the Add Effect button
- Scroll down to *Stats* on the left, and choose the *Hit Points* stat.
- Choose the *Base Value* operation.
Now we need to decide how many hit points our class gives us. We will assume that we take the constant hit points per level, since it's both the rule used for league play and it's statistically advantageous over rolling for hit points every level.
We could work out our hit points every level and change the effect each time, but we can do one better: we can input the calculation directly into the value field and have the character sheet figure it out for us.
Let's assume we are creating a fighter, so in the class name you typed in "Fighter" (with the capital F, but without the quote marks). A fighter gets 10 hp at first level and 6 hitpoints every level after that.
Let's rather split that into 4 bonus hit points at first level, and 6 hit points for every fighter level your character has. We can the write this as `4 + 6*FighterLevel` where the `*` represents multiplication.
*Note that we __don't add the constitution modifier here__; that's already taken care of by default, since all characters add their constitution modifier to their hit points automatically.*
- In the value field input `4 + 6*FighterLevel` - the spaces aren't needed, but you must spell your class name exactly as it is spelt in the class name input box, capital letters and all, in our case "Fighter"
- Create a new effect that sets the base value of *d10 Hit Dice* to *FighterLevel*, since we also get a number of hit dice equal to our fighter's level.
- Check how your changes are reflected in the *Stats* tab.
- Change your level and check that the *Stats* tab gets updated accordingly.
This method of including calculations in other stats allows you to take full advantage of having a digital character sheet, as it means that you can change any one thing in your character sheet and everthing else will update automatically.
---
## Additional Tips
Any input field with a <iron-icon icon="lightbulb-outline"></iron-icon> light bulb icon is a *formula field*: it will compute any and all variables and functions within it. For example, the "Value" field in the effect edit dialog is a formula field, so you could set the value to `3`, or `FighterLevel*2`, or any formula you can think of.
Any input field with a <iron-icon icon="dicecloud:code-braces"></iron-icon> curly brackets icon is a *smart input field*: you can also use formulas here, but they must be enclosed within {curly brackets}. For example, the "Damage" field of a spell or weapon is a smart input field, so you could type `1d8 + {strengthMod}` for the damage, and if your strength modifier was +3, it would display as "1d8 + 3".
The full list of functions and variables can be found on the GitHub wiki, [here](https://github.com/ThaumRystra/DiceCloud1/wiki/Function-and-Variable-List).
Any description field, as well as some others like your background or description, can be formatted with Markdown.
For example, having \*asterisks\* around something makes it *italic*, and a \*\*pair\*\* makes it **bold**.
If you need to display an actual asterisk, you can escape it with a backslash, like this: \\\*.
You can read more about Markdown [here](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet "Markdown Cheatsheet") and [here](https://daringfireball.net/projects/markdown/syntax "Markdown's origianal specification").
In addition, using three or more hyphens on their own line (like this: " --- "; this is the Markdown for a horizontal rule) will cut off the description of a feature card or any of the cards on the Persona page, so that the full description is only displayed - this is useful when having the full feature text would be annoyingly long, so you can simply display a summary on the card and have it expand into the full text.
{{/markdown}}</paper-material>
</div> </div>
</app-header-layout> </app-header-layout>
</template> </template>

View File

@@ -1,7 +1,13 @@
var hints = [ var hints = [
"Drag and drop items to move them between containers", "Drag and drop items to move them between containers.",
"Hold Ctrl while dragging items around to only move some of them", "Hold Ctrl while dragging items around to only move some of them.",
"Magic items are considered priceless, don't give them a gold value", "Magic items are considered priceless, don't give them a gold value.",
"You can use formulae in {curly brackets} in any field with a {} icon.",
"You can disable the 'Spells' tab from the charecter menu in the top right.",
"You can share your character with others from the menu in the top right.",
"Your spells, features, and items are ordered by their colour, which you can set with the paint bucket.",
"You can only have three magic items attuned to you at once. Choose carefully!",
"Click the '+' underneath 'Hit Points' to add additional health bars for temporary HP, wild shapes, familiars and more.",
]; ];
Template.loading.helpers({ Template.loading.helpers({

View File

@@ -4,11 +4,13 @@ Template.baseDialog.onCreated(function(){
Template.baseDialog.helpers({ Template.baseDialog.helpers({
editing: function(){ editing: function(){
if (!Template.parentData() || !Template.parentData().charId) return true;
return Template.instance().editing.get() && return Template.instance().editing.get() &&
canEditCharacter(Template.parentData().charId); canEditCharacter(Template.parentData().charId);
}, },
showEdit: function() { showEdit: function() {
if (this.hideEdit) return false; if (this.hideEdit) return false;
if (!Template.parentData() || !Template.parentData().charId) return true;
return canEditCharacter(Template.parentData().charId); return canEditCharacter(Template.parentData().charId);
}, },
}); });

View File

@@ -0,0 +1,8 @@
.textarea-bracket-suffix {
margin-bottom: 12px;
}
.description-input > paper-textarea {
width: 100%;
width: 100% - 24px;
}

View File

@@ -1,13 +1,35 @@
<template name="formulaSuffix"> <template name="formulaSuffix">
<div suffix> <div suffix style="position: relative">
<paper-tooltip position="left" animation-delay="0">This is a formula field</paper-tooltip>
<iron-icon icon="lightbulb-outline"></iron-icon> <iron-icon icon="lightbulb-outline"></iron-icon>
{{# simpleTooltip}}
This is a formula field
{{/ simpleTooltip}}
</div> </div>
</template> </template>
<template name="bracketSuffix"> <template name="bracketSuffix">
<div suffix> <div suffix style="position: relative">
<paper-tooltip position="left" animation-delay="0">This field accepts formulae in {curly brackets}</paper-tooltip>
<iron-icon icon="dicecloud:code-braces"></iron-icon> <iron-icon icon="dicecloud:code-braces"></iron-icon>
{{# simpleTooltip}}
This field accepts formulae in {curly brackets}
{{/ simpleTooltip}}
</div>
</template>
<template name="textareaBracketSuffix">
<div class="textarea-bracket-suffix">
<div class="markdown" style="position: relative">
<iron-icon icon="dicecloud:markdown"></iron-icon>
{{# simpleTooltip}}
This field accepts markdown formatting
{{/ simpleTooltip}}
</div>
<div class="brackets" style="position: relative">
<!--<paper-tooltip position="left" animation-delay="0">This field accepts formulae in {curly brackets}</paper-tooltip>-->
<iron-icon icon="dicecloud:code-braces"></iron-icon>
{{# simpleTooltip}}
This field accepts formulae in {curly brackets}
{{/ simpleTooltip}}
</div>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,18 @@
.simple-tooltip:hover .tooltip {
opacity: 0.9;
}
.tooltip {
opacity: 0;
transition: opacity 200ms ease-in;
font-size: 12px;
line-height: 1;
background-color: #616161;
color: white;
padding: 8px;
border-radius: 2px;
position: absolute;
right: calc(100% + 8px);
pointer-events: none;
white-space: nowrap;
}

View File

@@ -0,0 +1,7 @@
<template name="simpleTooltip">
<div class="simple-tooltip fit">
<div class="tooltip">
{{> Template.contentBlock}}
</div>
</div>
</template>

View File

@@ -49,6 +49,12 @@ evaluate = function(charId, string, opts){
if (list && list.saveDC){ if (list && list.saveDC){
return evaluate(charId, list.saveDC); return evaluate(charId, list.saveDC);
} }
}
if (spellListId && sub.toUpperCase() === "ATTACKBONUS") {
var list = SpellLists.findOne(spellListId);
if (list && list.attackBonus){
return evaluate(charId, list.attackBonus);
}
} }
return sub; return sub;
}); });

View File

@@ -11,5 +11,8 @@
<g id="patreon"> <g id="patreon">
<path d="M 0,11.704583 C 0,6.129988 4.5275823,0.90539433 10.497051,0.15752807 c 4.277795,-0.49807892 7.513064,1.14423533 9.752176,3.28462863 2.088042,1.9893242 3.332492,4.529078 3.631638,7.5160563 0.248292,2.991465 -0.397865,5.579082 -2.138897,8.017126 C 20.000935,21.368511 16.566733,24.001 12.288938,24.001 H 6.4675474 V 12.512279 c 0.050855,-2.5382585 0.8974395,-4.7295066 3.9786486,-5.7735279 2.687831,-0.7972254 5.822887,0.6955156 6.768189,3.5329199 0.987184,3.036337 -0.448719,5.076516 -2.138897,6.320966 -1.705135,1.244449 -4.337624,1.244449 -6.072674,0.04936 v 3.933777 c 1.136757,0.553421 2.587617,0.702994 3.63463,0.643165 3.769246,-0.538464 6.715839,-2.677362 7.957297,-5.923101 1.28633,-3.425228 0.38889,-7.4188334 -2.288471,-9.9017494 C 15.075488,2.746641 11.530602,2.1034761 7.761356,3.9432271 5.1139094,5.2893863 3.2741585,8.0265768 2.8254387,11.018042 V 23.999504 H 0.04487198 L 0,11.704583 z" style="fill-rule:nonzero"/> <path d="M 0,11.704583 C 0,6.129988 4.5275823,0.90539433 10.497051,0.15752807 c 4.277795,-0.49807892 7.513064,1.14423533 9.752176,3.28462863 2.088042,1.9893242 3.332492,4.529078 3.631638,7.5160563 0.248292,2.991465 -0.397865,5.579082 -2.138897,8.017126 C 20.000935,21.368511 16.566733,24.001 12.288938,24.001 H 6.4675474 V 12.512279 c 0.050855,-2.5382585 0.8974395,-4.7295066 3.9786486,-5.7735279 2.687831,-0.7972254 5.822887,0.6955156 6.768189,3.5329199 0.987184,3.036337 -0.448719,5.076516 -2.138897,6.320966 -1.705135,1.244449 -4.337624,1.244449 -6.072674,0.04936 v 3.933777 c 1.136757,0.553421 2.587617,0.702994 3.63463,0.643165 3.769246,-0.538464 6.715839,-2.677362 7.957297,-5.923101 1.28633,-3.425228 0.38889,-7.4188334 -2.288471,-9.9017494 C 15.075488,2.746641 11.530602,2.1034761 7.761356,3.9432271 5.1139094,5.2893863 3.2741585,8.0265768 2.8254387,11.018042 V 23.999504 H 0.04487198 L 0,11.704583 z" style="fill-rule:nonzero"/>
</g> </g>
<g id="markdown">
<path d="M2,16V8H4L7,11L10,8H12V16H10V10.83L7,13.83L4,10.83V16H2M16,8H19V12H21.5L17.5,16.5L13.5,12H16V8Z" />
</g>
</defs></svg> </defs></svg>
</iron-iconset-svg> </iron-iconset-svg>

View File

@@ -23,142 +23,12 @@ Meteor.methods({
Migrations.add({ Migrations.add({
version: 1, version: 1,
name: "converts effect proficiencies to proficiency objects, removes types from assets",
up: function() {
//convert proficiency effects to proficiency objects
Effects.find({operation: "proficiency"}).forEach(function(effect){
var type = "skill";
if (_.contains(SAVES, effect.stat)) type = "save";
Proficiencies.insert({
charId: effect.charId,
name: effect.stat,
value: effect.value,
parent: _.clone(effect.parent),
type: type,
enabled: effect.enabled,
}, function(err){
if (!err) Effects.remove(effect._id);
});
});
//store type as a parent group if it's needed
Effects.find({"parent.collection": "Characters"}).forEach(function(e){
Effects.update(e._id, {$set: {"parent.group": e.type}});
});
Attacks.find({"parent.collection": "Characters"}).forEach(function(a){
Attacks.update(a._id, {$set: {"parent.group": a.type}});
});
//remove type
Effects.update({}, {$unset: {type: ""}}, {validate: false, multi: true});
Attacks.update({}, {$unset: {type: ""}}, {validate: false, multi: true});
//remove languages and proficiencies
Characters.update(
{},
{$unset: {languages: "", proficiencies: ""}},
{validate: false, multi: true}
);
},
});
Migrations.add({
version: 2,
name: "Converts attacks from damage dice and damage bonus to a string with curly bracket calculations, adds settings.showIncrement to items",
up: function() {
//update attacks
Attacks.find({}).forEach(function(attack) {
if (!attack.damage && attack.damageDice && attack.damageBonus){
var newDamage = attack.damageDice +
" + {" + attack.damageBonus + "}";
Attacks.update(
attack._id,
{
$unset: {
damageBonus: "",
damageDice: "",
},
$set: {
damage: newDamage
},
},
{validate: false});
}
});
//update Items
Items.update(
{settings: undefined},
{$set: {"settings.showIncrement" : false}},
{validate: false, multi: true}
);
},
});
Migrations.add({
version: 3,
name: "Converts attacks from damage dice and damage bonus to a string with curly bracket calculations, adds settings.showIncrement to items",
up: function() {
//update characters
Characters.update(
{"settings.useVariantEncumbrance": undefined},
{$set: {"settings.useVariantEncumbrance" : false}},
{validate: false, multi: true}
);
Characters.update(
{"settings.useStandardEncumbrance": undefined},
{$set: {"settings.useStandardEncumbrance" : true}},
{validate: false, multi: true}
);
},
});
Migrations.add({
version: 4,
name: "Adds an effect to give characters a base carry capacity",
up: function() {
//update characters
Characters.find({}).forEach(function(char){
Characters.update(char._id, {
$set: {
carryMultiplier: {
adjustment: 0,
reset: "longRest",
}
}
});
var effect = Effects.findOne({
charId: char._id, name: "Natural Carrying Capacity",
});
if (effect) return;
Effects.insert({
charId: char._id,
name: "Natural Carrying Capacity",
stat: "carryMultiplier",
operation: "base",
value: "1",
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
effect = Effects.findOne({
charId: char._id, name: "Natural Carrying Capacity",
});
if (!effect) throw "Carry capacity effect should be set by now."
});
},
down: function(){
return;
},
});
Migrations.add({
version: 5,
name: "Gives all characters a URL name", name: "Gives all characters a URL name",
up: function() { up: function() {
//update characters //update characters
Characters.find({}).forEach(function(char){ Characters.find({}).forEach(function(char){
if (char.urlName) return; if (char.urlName) return;
var urlName = getSlug(char.name, {maintainCase: true}); var urlName = getSlug(char.name, {maintainCase: true}) || "-";
Characters.update(char._id, {$set: {urlName}}); Characters.update(char._id, {$set: {urlName}});
}); });
}, },

View File

@@ -4,27 +4,24 @@ Meteor.publish("characterList", function(){
this.ready(); this.ready();
return; return;
} }
return Characters.find( return [
{ Characters.find(
$or: [ {$or: [{readers: userId}, {writers: userId}, {owner: userId}]},
{readers: userId}, {
{writers: userId}, fields: {
{owner: userId}, name: 1,
] urlName: 1,
}, race: 1,
{ alignment: 1,
fields: { gender: 1,
name: 1, readers: 1,
urlName: 1, writers:1,
race: 1, owner: 1,
alignment: 1, color: 1,
gender: 1, picture: 1,
readers: 1, }
writers:1,
owner: 1,
color: 1,
picture: 1,
} }
} ),
); Parties.find({owner: userId}),
];
}); });