Compare commits

...

18 Commits
1.3.0 ... 1.3.4

Author SHA1 Message Date
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
Stefan Zermatten
54f055d3ef Merge branch 'bugfix-model' 2017-07-13 16:03:23 +02:00
Stefan Zermatten
2bacb296ba Fixed new effects dialog animation
makes some progress on #72
2017-07-13 16:01:43 +02:00
Stefan Zermatten
8b061f7aa9 Improved look and feel of effect editing 2017-07-13 15:53:03 +02:00
Stefan Zermatten
6a84c83644 Made passive scores show up for all skills with bonuses to passive score
closes #56
2017-07-13 13:42:04 +02:00
Stefan Zermatten
3227cd0934 Add character names to the end of their URL's
Closes #21, makes links to characters human readable
2017-07-13 13:14:04 +02:00
Stefan Zermatten
089feae26f Spell slots are now available in calculations
closes #11
2017-07-13 10:56:42 +02:00
Stefan Zermatten
99c72d1e10 Made names optional for all collections
closes #92
2017-07-13 10:39:43 +02:00
Stefan Zermatten
1e67afbe6f Made DC available in spell descriptions as a variable
closes #101
2017-07-13 10:30:16 +02:00
Stefan Zermatten
1cfec1ca45 Allowed carryMultiplier to be a decimal
closes #100
2017-07-13 09:50:20 +02:00
Stefan Zermatten
09d1ac9ba3 Merge branch 'bugfix-improved-initiative' 2017-07-11 08:53:46 +02:00
Stefan Zermatten
834b9cf384 Added passive perception to improved initiative export
closes #97
2017-07-11 08:52:47 +02:00
Stefan Zermatten
37291b347a Fixed vulnerabilities, resistances, immunities export
closes #96
2017-07-11 08:42:21 +02:00
Stefan Zermatten
efdfbeb59e Moved export to lib 2017-07-11 08:12:52 +02:00
43 changed files with 602 additions and 291 deletions

View File

@@ -48,3 +48,4 @@ es5-shim@4.6.15
differential:vulcanize
reactive-dict
percolate:synced-cron
ongoworks:speakingurl

View File

@@ -85,6 +85,7 @@ npm-mongo@2.2.16_1
oauth@1.1.12
oauth2@1.1.11
observe-sequence@1.0.14
ongoworks:speakingurl@9.0.0
ordered-dict@1.0.9
percolate:migrations@0.9.8
percolate:synced-cron@1.3.2

View File

@@ -1,8 +1,42 @@
Parties = new Mongo.Collection("parties");
Schemas.Party = new SimpleSchema({
//each character/monster can only be in one party at a time
//each party can only be in a single instance at a time
name: {
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.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

@@ -11,10 +11,12 @@ Schemas.Action = new SimpleSchema({
},
name: {
type: String,
optional: true,
trim: false,
},
description: {
type: String,
optional: true,
trim: false,
},
type: {

View File

@@ -12,6 +12,7 @@ Schemas.Attack = new SimpleSchema({
name: {
type: String,
defaultValue: "New Attack",
optional: true,
trim: false,
},
details: {

View File

@@ -8,6 +8,7 @@ Schemas.Buff = new SimpleSchema({
},
name: {
type: String,
optional: true,
trim: false,
},
description: {

View File

@@ -4,6 +4,7 @@ Characters = new Mongo.Collection("characters");
Schemas.Character = new SimpleSchema({
//strings
name: {type: String, defaultValue: "", trim: false, optional: true},
urlName: {type: String, defaultValue: "-", trim: false, optional: true},
alignment: {type: String, defaultValue: "", trim: false, optional: true},
gender: {type: String, defaultValue: "", trim: false, optional: true},
race: {type: String, defaultValue: "", trim: false, optional: true},
@@ -257,7 +258,10 @@ var attributeBase = preventLoop(function(charId, statName){
var result = (base + add) * mul;
if (result < min) result = min;
if (result > max) result = max;
// Don't round carry multiplier
if (statName === "carryMultiplier"){
return result;
}
return Math.floor(result);
});
@@ -537,6 +541,15 @@ if (Meteor.isServer){
Items .remove({charId: character._id});
Containers .remove({charId: character._id});
});
Characters.after.update(function(userId, doc, fieldNames, modifier, options) {
if (_.contains(fieldNames, "name")){
var urlName = getSlug(doc.name, {maintainCase: true}) || "-";
Characters.update(doc._id, {$set: {urlName}});
}
});
Characters.before.insert(function(userId, doc) {
doc.urlName = getSlug(doc.name, {maintainCase: true}) || "-";
});
}
Characters.allow({

View File

@@ -2,7 +2,7 @@ Classes = new Mongo.Collection("classes");
Schemas.Class = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, trim: false},
name: {type: String, optional: true, trim: false},
level: {type: Number},
createdAt: {
type: Date,

View File

@@ -2,7 +2,7 @@ Experiences = new Mongo.Collection("experience");
Schemas.Experience = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, defaultValue: "New Experience", trim: false},
name: {type: String, optional: true, trim: false, defaultValue: "New Experience"},
description: {type: String, optional: true, trim: false},
value: {type: Number, defaultValue: 0},
dateAdded: {

View File

@@ -2,7 +2,7 @@ Features = new Mongo.Collection("features");
Schemas.Feature = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, trim: false},
name: {type: String, optional: true, trim: false},
description: {type: String, optional: true, trim: false},
uses: {type: String, optional: true, trim: false},
used: {type: Number, defaultValue: 0},

View File

@@ -2,7 +2,7 @@ Notes = new Mongo.Collection("notes");
Schemas.Note = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, trim: false},
name: {type: String, optional: true, trim: false},
description: {type: String, optional: true, trim: false},
color: {
type: String,

View File

@@ -2,7 +2,7 @@ SpellLists = new Mongo.Collection("spellLists");
Schemas.SpellLists = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, trim: false},
name: {type: String, optional: true, trim: false},
description: {type: String, optional: true, trim: false},
saveDC: {type: String, optional: true, trim: false},
attackBonus: {type: String, optional: true, trim: false},

View File

@@ -9,6 +9,7 @@ Schemas.Spell = new SimpleSchema({
},
name: {
type: String,
optional: true,
trim: false,
defaultValue: "New Spell",
},

View File

@@ -2,7 +2,7 @@
Containers = new Mongo.Collection("containers");
Schemas.Container = new SimpleSchema({
name: {type: String, trim: false},
name: {type: String, optional: true, trim: false},
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
isCarried: {type: Boolean},
weight: {type: Number, min: 0, defaultValue: 0, decimal: true},

View File

@@ -1,7 +1,7 @@
Items = new Mongo.Collection("items");
Schemas.Item = new SimpleSchema({
name: {type: String, defaultValue: "New Item", trim: false},
name: {type: String, optional: true, trim: false, defaultValue: "New Item"},
plural: {type: String, optional: true, trim: false},
description:{type: String, optional: true, trim: false},
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, //id of owner

View File

@@ -24,7 +24,7 @@ Router.map(function() {
this.route("characterList", {
path: "/characterList",
waitOn: function(){
return subsManager.subscribe("characterList", Meteor.userId());
return subsManager.subscribe("characterList");
},
onAfterAction: function() {
document.title = appName + " - Characters";
@@ -32,11 +32,27 @@ Router.map(function() {
fastRender: true,
});
this.route("characterSheet", {
path: "/character/:_id",
this.route("characterSheetNaked", {
path: "/character/:_id/",
waitOn: function(){
return [
subsManager.subscribe("singleCharacter", this.params._id, Meteor.userId()),
subsManager.subscribe("singleCharacter", this.params._id),
];
},
action: function(){
var _id = this.params._id
var character = Characters.findOne(_id);
var urlName = character && character.urlName;
var path = `\/character\/${_id}\/${urlName || "-"}`;
Router.go(path,{},{replaceState:true});
},
});
this.route("characterSheet", {
path: "/character/:_id/:urlName",
waitOn: function(){
return [
subsManager.subscribe("singleCharacter", this.params._id),
];
},
data: function() {

View File

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

View File

@@ -24,6 +24,10 @@ Template.registerHelper("evaluateString", function(charId, string) {
return evaluateString(charId, string);
});
Template.registerHelper("evaluateSpellString", function(charId, spellListId, string) {
return evaluateSpellString(charId, spellListId, string);
});
Template.registerHelper("evaluateShortString", function(charId, string) {
if (_.isString(string)){
return evaluateString(

View File

@@ -49,7 +49,10 @@ improvedInitiativeJson = function(charId, options){
{"Name": "Cha", "Modifier": skillMod("charismaSave")},
],
"Skills": getSkills(charId),
"Senses": [],
"Senses": [
"passive Perception " +
Characters.calculate.passiveSkill(charId, "perception")
],
"Languages": getLanguages(charId),
"Challenge": "",
"Traits": options.features ? getTraits(charId) : [],
@@ -116,10 +119,11 @@ var getDamageMods = function(charId){
];
// jscs:enable maximumLineLength
multipliers = _.groupBy(multipliers, "value");
var names = o => o.name;
return {
"immunities": multipliers["0"] || [],
"resistances": multipliers["0.5"] || [],
"weaknesses": multipliers["2"] || [],
"immunities": _.map(multipliers["0"], names),
"resistances": _.map(multipliers["0.5"], names),
"vulnerabilities": _.map(multipliers["2"], names),
};
}

View File

@@ -0,0 +1,28 @@
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
// MIT license
var lastTime = 0;
var vendors = ["ms", "moz", "webkit", "o"];
for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x] + "RequestAnimationFrame"];
window.cancelAnimationFrame = window[vendors[x] + "CancelAnimationFrame"] ||
window[vendors[x] + "CancelRequestAnimationFrame"];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};

View File

@@ -20,26 +20,28 @@
<div class="effectEdit layout horizontal flex">
<dicecloud-selector class="statMenu flex" selected={{stat}} selectable="paper-item" style="height: 100%; overflow-y: auto;">
{{#each statGroups}}
<div style="font-weight: bold; margin-top: 16px; padding-left: 8px;">
<div class="statGroupTitle clickable" style="font-weight: bold; margin-top: 16px; padding-left: 8px;">
{{this}}
</div>
{{#each stats}}
<paper-item name={{stat}}>{{name}}</paper-item>
{{/each}}
<iron-collapse opened={{isGroupSelected this ../stat}}>
{{#each stats}}
<paper-item name={{stat}} class="clickable">{{name}}</paper-item>
{{/each}}
</iron-collapse>
{{/each}}
</dicecloud-selector>
{{#if operations}}
<dicecloud-selector class="operationMenu flex" selected={{operation}} style="height: 100%; overflow-y: auto;">
{{#each operations}}
<paper-item name={{operation}}>{{name}}</paper-item>
<paper-item name={{operation}} class="clickable">{{name}}</paper-item>
{{/each}}
</dicecloud-selector>
{{else}} {{#if showMultiplierOperations}}
<dicecloud-selector class="multiplierMenu flex"
selected={{value}}>
<paper-item name="0.5">Resistance</paper-item>
<paper-item name="2">Vulnerability</paper-item>
<paper-item name="0">Immunity</paper-item>
<paper-item name="0.5" class="clickable">Resistance</paper-item>
<paper-item name="2" class="clickable">Vulnerability</paper-item>
<paper-item name="0" class="clickable">Immunity</paper-item>
</dicecloud-selector>
{{else}}
<div class="flex" style="height: 100%;"></div>

View File

@@ -149,47 +149,71 @@ Template.effectEdit.helpers({
effectValue: function(){
return this.calculation || this.value;
},
isGroupSelected: function(groupName, statName){
var stat = statsDict[statName]
return stat && (stat.group === groupName);
},
});
var setStat = function(statName, effectId){
var setter = {stat: statName};
var effect = Effects.findOne(this._id);
var group = statsDict[statName].group;
if (group === "Saving Throws" || group === "Skills"){
// Skills must have a valid skill operation
if (!_.contains(
_.map(skillOperations, ao => ao.operation),
effect.operation
)){
setter.operation = "add";
}
} else if (group !== "Weakness/Resistance"){
// Attributes must have a valid attribute operation
if (!_.contains(
_.map(attributeOperations, ao => ao.operation),
effect.operation
)){
setter.operation = "base";
}
} else {
// Weakness/Resistance must have a mul operation and value
if (effect.operation !== "mul"){
setter.operation = "mul";
}
if (!_.contains([0, 0.5, 2], effect.value)){
setter.value = 0.5;
}
}
Effects.update(effectId, {$set: setter});
};
var scrollAnimationId;
var scrollElementToView = element => {
var scrollFunction = function(){
element.scrollIntoView();
scrollAnimationId = requestAnimationFrame(scrollFunction);
};
return scrollFunction;
}
Template.effectEdit.events({
"click #deleteButton": function(event, instance){
Effects.softRemoveNode(instance.data.id);
GlobalUI.deletedToast(instance.data.id, "Effects", "Effect");
popDialogStack();
},
"click .statGroupTitle": function(event, instance){
var groupName = this.toString();
var firstStat = statGroups[groupName][0].stat;
setStat(firstStat, instance.data.id);
scrollAnimationId = requestAnimationFrame(scrollElementToView(event.target));
_.delay(() => cancelAnimationFrame(scrollAnimationId), 300);
},
"iron-select .statMenu": function(event){
var detail = event.originalEvent.detail;
var statName = detail.item.getAttribute("name");
if (statName == this.stat) return;
var setter = {stat: statName};
var group = Blaze.getData(detail.item).group;
var effect = Effects.findOne(this._id);
if (group === "Saving Throws" || group === "Skills"){
// Skills must have a valid skill operation
if (!_.contains(
_.map(skillOperations, ao => ao.operation),
effect.operation
)){
setter.operation = "add";
}
} else if (group !== "Weakness/Resistance"){
// Attributes must have a valid attribute operation
if (!_.contains(
_.map(attributeOperations, ao => ao.operation),
effect.operation
)){
setter.operation = "base";
}
} else {
// Weakness/Resistance must have a mul operation and value
if (effect.operation !== "mul"){
setter.operation = "mul";
}
if (!_.contains([0, 0.5, 2], effect.value)){
setter.value = 0.5;
}
}
Effects.update(this._id, {$set: setter});
setStat(statName, this._id);
},
"iron-select .operationMenu": function(event){
var detail = event.originalEvent.detail;

View File

@@ -33,7 +33,7 @@ Template.effectsEditList.events({
template: "effectEdit",
data: {id: effectId},
element: event.currentTarget,
returnElement: instance.find(`tr.effect[data-id='${effectId}']`),
returnElement: () => instance.find(`tr.effect[data-id='${effectId}']`),
});
},
"tap .edit-effect": function(event, template){

View File

@@ -36,7 +36,7 @@
</div>
{{/if}}
</div>
<div>{{#markdown}}{{evaluateString charId description}}{{/markdown}}</div>
<div>{{#markdown}}{{evaluateSpellString charId parent.id description}}{{/markdown}}</div>
{{> attacksViewList charId=charId parentId=_id}}
</template>

View File

@@ -61,6 +61,18 @@
<td>Total</td>
<td>{{characterCalculate "skillMod" charId skillName}}</td>
</tr>
{{#each passiveEffects}}
<tr>
<td>{{sourceName}}</td>
<td>Passive Bonus: {{statValue}}</td>
</tr>
{{/each}}
{{#if showPassiveTotal}}
<tr class="paper-font-body2">
<td>Passive Score</td>
<td>{{characterCalculate "passiveSkill" charId skillName}}</td>
</tr>
{{/if}}
</table>
</div>

View File

@@ -122,7 +122,7 @@ Template.skillDialogView.helpers({
var char = Characters.findOne(this.charId);
if (!char) return;
var prof = Characters.calculate.proficiency(this.charId, this.skillName);
var proficiencyBonus =
var proficiencyBonus =
Characters.calculate.attributeValue(this.charId, "proficiencyBonus");
return prof * proficiencyBonus;
},
@@ -189,6 +189,23 @@ Template.skillDialogView.helpers({
enabled: true,
});
},
passiveEffects: function(){
return Effects.find({
charId: this.charId,
stat: this.skillName,
operation: "passiveAdd",
enabled: true,
});
},
showPassiveTotal: function(){
if (this.skillName === "perception") return true;
return Effects.find({
charId: this.charId,
stat: this.skillName,
operation: "passiveAdd",
enabled: true,
}).count();
},
ability: function(){
var opts = {fields: {}};
opts.fields[this.skillName] = 1;

View File

@@ -14,7 +14,7 @@
{{#if conditionalCount}}
*
{{/if}}
{{#if showPassive}}
{{#if isPassiveShown}}
({{characterCalculate "passiveSkill" ../_id skill}})
{{/if}}
</div>

View File

@@ -38,4 +38,16 @@ Template.skillRow.helpers({
operation: "conditional",
}).count();
},
isPassiveShown: function(){
if (this.showPassive === "forced") return true;
if (this.showPassive === "ifNeeded"){
var charId = Template.parentData()._id;
return Effects.find({
charId,
stat: this.skill,
operation: "passiveAdd",
enabled: true,
}).count();
}
},
});

View File

@@ -49,24 +49,24 @@
Skills
</div>
<div flex class="bottom list">
{{> skillRow name="Acrobatics" skill="acrobatics"}}
{{> skillRow name="Animal Handling" skill="animalHandling"}}
{{> skillRow name="Arcana" skill="arcana"}}
{{> skillRow name="Athletics" skill="athletics"}}
{{> skillRow name="Deception" skill="deception"}}
{{> skillRow name="History" skill="history"}}
{{> skillRow name="Insight" skill="insight"}}
{{> skillRow name="Intimidation" skill="intimidation"}}
{{> skillRow name="Investigation" skill="investigation"}}
{{> skillRow name="Medicine" skill="medicine"}}
{{> skillRow name="Nature" skill="nature"}}
{{> skillRow name="Perception" skill="perception" showPassive="true"}}
{{> skillRow name="Performance" skill="performance"}}
{{> skillRow name="Persuasion" skill="persuasion"}}
{{> skillRow name="Religion" skill="religion"}}
{{> skillRow name="Sleight of Hand" skill="sleightOfHand"}}
{{> skillRow name="Stealth" skill="stealth"}}
{{> skillRow name="Survival" skill="survival"}}
{{> skillRow name="Acrobatics" skill="acrobatics" showPassive="ifNeeded"}}
{{> skillRow name="Animal Handling" skill="animalHandling" showPassive="ifNeeded"}}
{{> skillRow name="Arcana" skill="arcana" showPassive="ifNeeded"}}
{{> skillRow name="Athletics" skill="athletics" showPassive="ifNeeded"}}
{{> skillRow name="Deception" skill="deception" showPassive="ifNeeded"}}
{{> skillRow name="History" skill="history" showPassive="ifNeeded"}}
{{> skillRow name="Insight" skill="insight" showPassive="ifNeeded"}}
{{> skillRow name="Intimidation" skill="intimidation" showPassive="ifNeeded"}}
{{> skillRow name="Investigation" skill="investigation" showPassive="ifNeeded"}}
{{> skillRow name="Medicine" skill="medicine" showPassive="ifNeeded"}}
{{> skillRow name="Nature" skill="nature" showPassive="ifNeeded"}}
{{> skillRow name="Perception" skill="perception" showPassive="forced"}}
{{> skillRow name="Performance" skill="performance" showPassive="ifNeeded"}}
{{> skillRow name="Persuasion" skill="persuasion" showPassive="ifNeeded"}}
{{> skillRow name="Religion" skill="religion" showPassive="ifNeeded"}}
{{> skillRow name="Sleight of Hand" skill="sleightOfHand" showPassive="ifNeeded"}}
{{> skillRow name="Stealth" skill="stealth" showPassive="ifNeeded"}}
{{> skillRow name="Survival" skill="survival" showPassive="ifNeeded"}}
</div>
</paper-material>
</div>

View File

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

View File

@@ -10,31 +10,27 @@
{{#if currentUser}}
{{#if characters.count}}
<div class="character-list layout horizontal wrap">
{{# each characters}}
<a class="character-card flex layout vertical end-justified" href="/character/{{_id}}">
<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 charactersWithNoParty}}
{{> characterCard}}
{{/each}}
{{> gridPadding class="character-card flex layout vertical" num=12}}
</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}}
<div layout vertical center center-justified class="padded">
<div>You don't seem to have any characters yet</div>
@@ -47,9 +43,46 @@
</div>
{{/if}}
<div class="fab-buffer"></div>
<paper-fab class="floatyButton addCharacter"
icon="add"
title="Add"></paper-fab>
{{#fabMenu}}
<div>
<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>
</app-header-layout>
</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,27 +1,57 @@
Template.characterList.helpers({
characters(){
characters() {
var userId = Meteor.userId();
return Characters.find(
{
$or: [
{readers: userId},
{writers: userId},
{owner: userId},
]
},
{
fields: {name: 1, picture: 1, color: 1, race: 1, alignment: 1, gender: 1},
sort: {name: 1},
}
{$or: [{readers: userId}, {writers: userId}, {owner: userId}]},
{sort: {name: 1}}
);
},
parties() {
return Parties.find({owner: Meteor.userId()});
},
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){
return name.replace(/[^A-Z]/g, "");
},
})
});
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({
template: "newCharacterDialog",
element: event.currentTarget,
@@ -29,8 +59,23 @@ Template.characterList.events({
if (!character) return;
character.owner = Meteor.userId();
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
*/
.character-name {
.side-list .character-name {
white-space: nowrap;
overflow: hidden;
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">
{{#if characters.count}}
<div class="side-list">
{{#each characters}}
<a href={{pathFor route="characterSheet" data=this}} tabindex="-1" class="side-list-character characterRepresentative">
<paper-item class="short">
<div class="character-name">
{{name}}
</div>
</paper-item>
</a>
{{/each}}
</div>
{{/if}}
<div class="side-list">
{{#each charactersWithNoParty}}
<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}}
{{#each parties}}
<div class="paper-font-subhead partyHead">
<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>

View File

@@ -1,33 +1,50 @@
Template.characterSideList.onCreated(function() {
this.subscribe("characterList");
this.openedParties = new ReactiveVar(new Set());
});
Template.characterSideList.helpers({
characters: function() {
parties() {
var userId = Meteor.userId();
return Parties.find({owner: userId});
},
charactersInParty() {
var userId = Meteor.userId();
return Characters.find(
{
$or: [
{readers: userId},
{writers: userId},
{owner: userId},
]
_id: {$in: this.characters},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
},
{
fields: {name: 1},
sort: {name: 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({
"tap .singleLineItem": function(event, instance) {
//Router.go("characterSheet", {_id: this._id});
$("core-drawer-panel").get(0).closeDrawer();
},
"tap core-item": function() {
Router.go("characterList");
$("core-drawer-panel").get(0).closeDrawer();
"click .partyHead": function(event, instance){
var openedParties = instance.openedParties.get();
if (openedParties.has(this._id)){
openedParties.delete(this._id);
} else {
openedParties.add(this._id);
}
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,11 +4,13 @@ Template.baseDialog.onCreated(function(){
Template.baseDialog.helpers({
editing: function(){
if (!Template.parentData() || !Template.parentData().charId) return true;
return Template.instance().editing.get() &&
canEditCharacter(Template.parentData().charId);
},
showEdit: function() {
if (this.hideEdit) return false;
if (!Template.parentData() || !Template.parentData().charId) return true;
return canEditCharacter(Template.parentData().charId);
},
});

View File

@@ -8,9 +8,10 @@
})();
//evaluates a calculation string
evaluate = function(charId, string){
evaluate = function(charId, string, opts){
var spellListId = opts && opts.spellListId;
if (!string) return string;
string = string.replace(/\b[a-z]+\b/gi, function(sub){
string = string.replace(/\b[a-z,1-9]+\b/gi, function(sub){
//fields
if (Schemas.Character.schema(sub)){
return Characters.calculate.fieldValue(charId, sub);
@@ -43,6 +44,12 @@ evaluate = function(charId, string){
if (sub.toUpperCase() === "LEVEL"){
return Characters.calculate.level(charId);
}
if (spellListId && sub.toUpperCase() === "DC") {
var list = SpellLists.findOne(spellListId);
if (list && list.saveDC){
return evaluate(charId, list.saveDC);
}
}
return sub;
});
try {
@@ -66,6 +73,17 @@ evaluateString = function(charId, string){
return result;
};
evaluateSpellString = function (charId, spellListId, string) {
//define brackets as curly brackets around anything that isn't a curly bracket
if (!string) return string;
var brackets = /\{[^\{\}]*\}/g;
var result = string.replace(brackets, function(exp){
exp = exp.replace(/(\{|\})/g, ""); //remove curly brackets
return evaluate(charId, exp, {spellListId});
});
return result;
}
//returns the value of the effect if it exists,
//otherwise returns the result of the calculation if it exists,
//otherwise returns 0

View File

@@ -249,7 +249,9 @@
},
_updateItems: function() {
var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*');
var nodes = this.selectable
? Polymer.dom(this).querySelectorAll(this.selectable)
: Polymer.dom(this).queryDistributedElements('*');
nodes = Array.prototype.filter.call(nodes, this._bindFilterItem);
this._setItems(nodes);
},

View File

@@ -23,127 +23,13 @@ Meteor.methods({
Migrations.add({
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",
name: "Gives all characters a URL name",
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."
if (char.urlName) return;
var urlName = getSlug(char.name, {maintainCase: true}) || "-";
Characters.update(char._id, {$set: {urlName}});
});
},
down: function(){

View File

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