Compare commits

..

34 Commits
1.2.0 ... 1.3.2

Author SHA1 Message Date
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
Thaum Rystra
a165f9b253 Merge branch 'feature-improved-initiative-export'
closes #95
2017-07-10 21:02:10 +02:00
Thaum Rystra
4a6fca98bc Cleaned up export dialog, fixed copying 2017-07-10 20:59:00 +02:00
Stefan Zermatten
35464128a0 Added basic export for improved initiative 2017-07-10 16:26:38 +02:00
Stefan Zermatten
398f8a8a2a Merge branch 'bugfix-style' 2017-06-28 09:33:20 +02:00
Stefan Zermatten
812a1784b2 Improved display of home page on smaller devices 2017-06-28 09:27:24 +02:00
Stefan Zermatten
8fa9cd0148 Prevented spell titles from overflowing their item in spell lists
closes #83
2017-06-28 09:26:52 +02:00
Stefan Zermatten
0e0662cc9a Added slightly specific rule to let headings wrap
closes #90
2017-06-28 09:26:13 +02:00
Stefan Zermatten
ad4e3f5b20 Stopped inventory items being separated from their containers
hopefully fixes #91
2017-06-28 09:25:25 +02:00
Stefan Zermatten
4cd058e1fe Set max of slider before value
Prevents unnecessary capping of temp HP to 100
Fixes #89
2017-06-08 09:48:51 +02:00
Stefan Zermatten
8f30cee4d3 Hotfixed column layout with translateZ(0) hack 2017-06-07 09:46:38 +02:00
Stefan Zermatten
d7f7eb2e6a Fixed some spells in the library 2017-06-07 09:35:59 +02:00
Stefan Zermatten
a59cf1162f added return value to remove old soft removed docs 2017-05-29 11:58:19 +02:00
Stefan Zermatten
7bc80da99e Moved cron setup to startup 2017-05-29 11:35:30 +02:00
Stefan Zermatten
2ddc520bb6 Added cron job to remove old documents 2017-05-29 11:16:46 +02:00
Stefan Zermatten
d92bb0f4d4 Replace tap event with click event for mini-FABs
fixes #68, probably
2017-05-16 10:36:40 +02:00
Stefan Zermatten
abcfe57add Fixed results not being used when popping dialogs
Fixes #66
2017-05-11 11:08:00 +02:00
Stefan Zermatten
cdae75beef Merge branch 'bugfix-iamastick' 2017-05-10 16:40:05 +02:00
Stefan Zermatten
e7bcc2224c Replaced effects in-line editing with edit dialogs
Closes #41
2017-05-10 16:39:36 +02:00
Stefan Zermatten
b591c66dd5 Back button now closes dialogs
closes #9
2017-05-10 10:38:28 +02:00
Stefan Zermatten
402bc0e5ed Fixed Readme 2017-05-10 10:37:30 +02:00
Stefan Zermatten
ac772531ee Fixed SRD spells not getting a random ID when inserted
fixes #64
fixes #65
2017-05-08 11:21:30 +02:00
62 changed files with 960 additions and 143 deletions

View File

@@ -1,13 +1,13 @@
RPG Docs
========
This is the repo for [DiceCloud](dicecloud.com). The currently deployed version should always be the latest release of the master branch.
This is the repo for [DiceCloud](dicecloud.com).
Getting started
---------------
`git clone https://github.com/ThaumRystra/RPG-Docs RPG-Docs`
`cd RPG-Docs`
`git clone https://github.com/ThaumRystra/DiceCloud1 dicecloud`
`cd dicecloud`
`cd rpg-docs`
`bower install`
`meteor`

View File

@@ -691,7 +691,7 @@
"level": 1,
"range": "Self",
"school": "Divination",
"ritual": false,
"ritual": true,
"name": "Comprehend Languages",
"components": {
"verbal": true,
@@ -4240,7 +4240,7 @@
},
{
"castingTime": "1 action",
"description": "A creature of your choice that you can see with range perceives everything as ilariously funny and falls into fits of laughter if this spell affects it. The target must succeed on a wisdom saving throw or fall prone, becoming incapacitated and unable to stand up for this duration. A creature with an Intelligence score of 4 or less isnt affected.\n At the end of each of its turns, and each time it takes damage, the target can make another Wisdom saving throw. The target has an advantage on the saving throw if its triggered by damage. On a success, the spell ends.",
"description": "A creature of your choice that you can see with range perceives everything as hilariously funny and falls into fits of laughter if this spell affects it. The target must succeed on a wisdom saving throw or fall prone, becoming incapacitated and unable to stand up for this duration. A creature with an Intelligence score of 4 or less isnt affected.\n At the end of each of its turns, and each time it takes damage, the target can make another Wisdom saving throw. The target has an advantage on the saving throw if its triggered by damage. On a success, the spell ends.",
"duration": "Concentration, up to 1 minute",
"level": 1,
"range": "30 feet",

View File

@@ -47,3 +47,5 @@ ecmascript@0.6.1
es5-shim@4.6.15
differential:vulcanize
reactive-dict
percolate:synced-cron
ongoworks:speakingurl

View File

@@ -85,8 +85,10 @@ 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
promise@0.8.8
raix:eventemitter@0.1.3
random@1.0.10

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},
@@ -184,6 +185,9 @@ Schemas.Character = new SimpleSchema({
defaultValue: "whitelist",
allowedValues: ["whitelist", "public"],
},
"settings.exportFeatures": {type: Boolean, defaultValue: true},
"settings.exportAttacks": {type: Boolean, defaultValue: true},
"settings.exportDescription": {type: Boolean, defaultValue: true},
});
Characters.attachSchema(Schemas.Character);
@@ -254,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);
});
@@ -534,6 +541,12 @@ 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.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

@@ -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

@@ -0,0 +1,195 @@
improvedInitiativeJson = function(charId, options){
options = options || {
features: true,
attacks: true,
description: true,
};
var char = Characters.findOne(charId);
if (!char) return;
var baseValue = function(attributeName){
return Characters.calculate.attributeBase(charId, attributeName);
};
var skillMod = function(skillName){
return Characters.calculate.skillMod(charId, skillName);
};
var damageMods = getDamageMods(charId);
return JSON.stringify({
"Id": char._id,
"Name": char.name,
"Source": "DiceCloud",
"Type": char.race,
"HP": {
"Value": baseValue("hitPoints"),
"Notes": getHitDiceString(charId) || "",
},
"AC": {
"Value": baseValue("armor"),
"Notes": getArmorString(charId) || "",
},
"InitiativeModifier": skillMod("initiative"),
"Speed": ["" + baseValue("speed")],
"Abilities": {
"Str": baseValue("strength"),
"Dex": baseValue("dexterity"),
"Con": baseValue("constitution"),
"Cha": baseValue("charisma"),
"Int": baseValue("intelligence"),
"Wis": baseValue("wisdom"),
},
"DamageVulnerabilities": damageMods.vulnerabilities,
"DamageResistances": damageMods.resistances,
"DamageImmunities": damageMods.immunities,
"ConditionImmunities": [],
"Saves": [
{"Name": "Str", "Modifier": skillMod("strengthSave")},
{"Name": "Dex", "Modifier": skillMod("dexteritySave")},
{"Name": "Con", "Modifier": skillMod("constitutionSave")},
{"Name": "Int", "Modifier": skillMod("intelligenceSave")},
{"Name": "Wis", "Modifier": skillMod("wisdomSave")},
{"Name": "Cha", "Modifier": skillMod("charismaSave")},
],
"Skills": getSkills(charId),
"Senses": [
"passive Perception " +
Characters.calculate.passiveSkill(charId, "perception")
],
"Languages": getLanguages(charId),
"Challenge": "",
"Traits": options.features ? getTraits(charId) : [],
"Actions": options.attacks ? getActions(charId) : [],
"Reactions": [],
"LegendaryActions": [],
"Description": options.description ? char.description : "",
"Player": Meteor.user().username,
}, null, 2);
}
var getHitDiceString = function(charId){
var d6 = Characters.calculate.attributeBase(charId, "d6HitDice");
var d8 = Characters.calculate.attributeBase(charId, "d8HitDice");
var d10 = Characters.calculate.attributeBase(charId, "d10HitDice");
var d12 = Characters.calculate.attributeBase(charId, "d12HitDice");
var con = Characters.calculate.abilityMod(charId,"constitution");
var string = "" +
(d6 ? `${d6}d6 + ` : "") +
(d8 ? `${d8}d8 + ` : "") +
(d10 ? `${d10}d10 + ` : "") +
(d12 ? `${d12}d12 + ` : "") +
con;
}
var getArmorString = function(charId){
var bases = Effects.find({
charId: charId,
stat: "armor",
operation: "base",
enabled: true,
}).map(e => ({
ame: e.name,
value: evaluateEffect(charId, e),
}));
var base = bases.length && _.max(bases, b => b.value).name || "";
var effects = Effects.find({
charId: charId,
stat: "armor",
operation: {$ne: "base"},
enabled: true,
}).map(e => e.name);
var strings = base ? [base] : [];
strings = strings.concat(effects);
return strings.join(", ");
}
var getDamageMods = function(charId){
// jscs:disable maximumLineLength
var multipliers = [
{name: "Acid", value: Characters.calculate.attributeValue(charId, "acidMultiplier")},
{name: "Bludgeoning", value: Characters.calculate.attributeValue(charId, "bludgeoningMultiplier")},
{name: "Cold", value: Characters.calculate.attributeValue(charId, "coldMultiplier")},
{name: "Fire", value: Characters.calculate.attributeValue(charId, "fireMultiplier")},
{name: "Force", value: Characters.calculate.attributeValue(charId, "forceMultiplier")},
{name: "Lightning", value: Characters.calculate.attributeValue(charId, "lightningMultiplier")},
{name: "Necrotic", value: Characters.calculate.attributeValue(charId, "necroticMultiplier")},
{name: "Piercing", value: Characters.calculate.attributeValue(charId, "piercingMultiplier")},
{name: "Poison", value: Characters.calculate.attributeValue(charId, "poisonMultiplier")},
{name: "Psychic", value: Characters.calculate.attributeValue(charId, "psychicMultiplier")},
{name: "Radiant", value: Characters.calculate.attributeValue(charId, "radiantMultiplier")},
{name: "Slashing", value: Characters.calculate.attributeValue(charId, "slashingMultiplier")},
{name: "Thunder", value: Characters.calculate.attributeValue(charId, "thunderMultiplier")},
];
// jscs:enable maximumLineLength
multipliers = _.groupBy(multipliers, "value");
var names = o => o.name;
return {
"immunities": _.map(multipliers["0"], names),
"resistances": _.map(multipliers["0.5"], names),
"vulnerabilities": _.map(multipliers["2"], names),
};
}
var getSkills = function(charId){
var allSkills = [
{name: "acrobatics", attribute: "dexterity"},
{name: "animalHandling", attribute: "wisdom"},
{name: "arcana", attribute: "intelligence"},
{name: "athletics", attribute: "strength"},
{name: "deception", attribute: "charisma"},
{name: "history", attribute: "intelligence"},
{name: "insight", attribute: "wisdom"},
{name: "intimidation", attribute: "charisma"},
{name: "investigation", attribute: "intelligence"},
{name: "medicine", attribute: "wisdom"},
{name: "nature", attribute: "intelligence"},
{name: "perception", attribute: "wisdom"},
{name: "performance", attribute: "charisma"},
{name: "persuasion", attribute: "charisma"},
{name: "religion", attribute: "intelligence"},
{name: "sleightOfHand", attribute: "dexterity"},
{name: "stealth", attribute: "dexterity"},
{name: "survival", attribute: "wisdom"},
];
var skills = [];
_.each(allSkills, skill => {
var value = Characters.calculate.skillMod(charId, skill.name);
var mod = Characters.calculate.abilityMod(charId, skill.attribute);
if (value !== mod){
skills.push({Name: skill.name, Modifier: value});
}
});
return skills;
};
var getLanguages = function(charId){
return Proficiencies.find({
charId,
enabled: true,
type: "language",
}).map(l => l.name);
};
var getTraits = function(charId){
return Features.find(
{charId: charId},
{sort: {color: 1, name: 1}}
).map(f => ({
Name: f.name,
// evaluateShortString helper
Content: evaluateString(
charId, f.description && f.description.split(/^( *[-*_]){3,} *(?:\n+|$)/m)[0]
) || "",
Usage: "",
}));
}
var getActions = function(charId){
return Attacks.find(
{charId, enabled: true},
{sort: {color: 1, name: 1}}
).map(a => ({
Name: a.name,
Content: `+${evaluate(charId, a.attackBonus)} to hit, ` +
`${evaluateString(charId, a.damage)} ${a.damageType} damage, ` +
`${a.details}`,
Usage: "",
}));
}

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

@@ -4,6 +4,7 @@
column-gap: 0px;
column-width: 304px;
padding: 4px;
transform: translateZ(0);
}
.column-container.thin-columns {
@@ -22,6 +23,7 @@
.card {
background: white;
border-radius: 2px;
position: initial;
}
.card .top {

View File

@@ -1,3 +1,7 @@
body .paper-font-display4, body .paper-font-display3, body .paper-font-title, body .paper-font-caption{
white-space: normal;
}
.white-text {
color: #dedede;
color: rgba(255,255,255,0.87);

View File

@@ -24,6 +24,10 @@
<iron-icon icon="settings" item-icon></iron-icon>
Settings
</paper-icon-item>
<paper-icon-item id="characterExport">
<iron-icon icon="content-copy" item-icon></iron-icon>
Export to Improved Initiative
</paper-icon-item>
</paper-menu>
</paper-menu-button>
{{/if}}

View File

@@ -203,4 +203,11 @@ Template.characterSheet.events({
element: event.currentTarget.parentElement.parentElement,
});
},
"click #characterExport": function(event, instance){
pushDialogStack({
data: this,
template: "exportDialog",
element: event.currentTarget.parentElement.parentElement,
});
},
});

View File

@@ -13,3 +13,13 @@
.effectEdit .deleteEffect {
flex-shrink: 0;
}
.effectEdit .effect-table-view {
align-self: center;
color: #757575;
color: rgba(0,0,0,0.54);
}
.effectEdit .iron-selected {
color: #ad2a1f;
}

View File

@@ -1,45 +1,55 @@
<template name="effectEdit">
<div class="effectEdit layout horizontal center">
<paper-dropdown-menu label="Stat" class="statDropDown" dynamic-align>
<dicecloud-selector class="statMenu dropdown-content" selected={{stat}} selectable="paper-item">
{{#each statGroups}}
<div style="font-weight: bold; margin-top: 16px; padding-left: 8px;">
{{this}}
</div>
{{#each stats}}
<paper-item name={{stat}}>{{name}}</paper-item>
{{/each}}
{{/each}}
</dicecloud-selector>
</paper-dropdown-menu>
{{#if operations}}
<paper-dropdown-menu class="operationDropDown" label="Operation" dynamic-align>
<dicecloud-selector class="dropdown-content operationMenu" selected={{operation}}>
{{#each operations}}
<paper-item name={{operation}}>{{name}}</paper-item>
{{#with effect}}
{{#baseEditDialog hideColor=true title="Effect"}}
<div class="fit layout vertical effectEdit" style="padding: 24px;">
<table class="paper-font-display1 effect-table-view">
<tr>
{{> effectView}}
</tr>
</table>
<hr class="vertMargin" style="width: 100%;">
{{#if showEffectValueInput}}
<paper-input class="effectValueInput"
label="Value"
floatinglabel
value={{effectValue}}>
</paper-input>
{{else}}
<div style="height: 62px;"></div>
{{/if}}
<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 class="statGroupTitle clickable" style="font-weight: bold; margin-top: 16px; padding-left: 8px;">
{{this}}
</div>
<iron-collapse opened={{isGroupSelected this ../stat}}>
{{#each stats}}
<paper-item name={{stat}} class="clickable">{{name}}</paper-item>
{{/each}}
</iron-collapse>
{{/each}}
</dicecloud-selector>
</paper-dropdown-menu>
{{/if}}
{{#if effectValueTemplate}}
{{> Template.dynamic template=effectValueTemplate}}
{{else}}
<div class="flex"></div>
{{/if}}
<paper-icon-button class="deleteEffect"
icon="delete">
</paper-icon-button>
<br>
</div>
</template>
<template name="regularEffectValue">
<paper-input class="effectValueInput flex"
label="Value"
floatinglabel
value={{effectValue}}
style="flex-basis: 100px;">
</paper-input>
{{#if operations}}
<dicecloud-selector class="operationMenu flex" selected={{operation}} style="height: 100%; overflow-y: auto;">
{{#each operations}}
<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" 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>
{{/if}} {{/if}}
</div>
</div>
{{/baseEditDialog}}
{{/with}}
</template>
<template name="multiplierEffectValue">
@@ -51,5 +61,4 @@
<paper-item name="0">Immunity</paper-item>
</dicecloud-selector>
</paper-dropdown-menu>
<div class="flex"></div>
</template>

View File

@@ -97,7 +97,19 @@ var skillOperations = [
{name: "Conditional Benefit", operation: "conditional"},
];
Template.effectEdit.onRendered(function(){
_.defer(() => {
const statElement = this.find(".statMenu .iron-selected");
statElement && statElement.scrollIntoView();
const opElement = this.find(".operationMenu .iron-selected");
opElement && opElement.scrollIntoView();
});
});
Template.effectEdit.helpers({
effect: function(){
return Effects.findOne(this.id);
},
statGroups: function(){
return statGroupNames;
},
@@ -115,46 +127,101 @@ Template.effectEdit.helpers({
return attributeOperations;
}
},
effectValueTemplate: function(){
//resistance/vulnerability template
showMultiplierOperations: function(){
var stat = statsDict[this.stat];
return stat && stat.group === "Weakness/Resistance"
},
showEffectValueInput: function(){
var stat = statsDict[this.stat];
var group = stat && stat.group;
if (group === "Weakness/Resistance") return "multiplierEffectValue";
if (
group === "Weakness/Resistance"
) return false;
var op = this.operation;
if (!op) return null;
//operations that don't need templates
if (op === "advantage" || op === "disadvantage" || op === "fail") return null;
//default template
return "regularEffectValue";
if (
!op ||
op === "advantage" ||
op === "disadvantage" ||
op === "fail"
) return false;
return true;
},
});
Template.regularEffectValue.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 .deleteEffect": function(event){
Effects.softRemoveNode(this._id);
GlobalUI.deletedToast(this._id, "Effects", "Effect");
"click #deleteButton": function(event, instance){
Effects.softRemoveNode(instance.data.id);
GlobalUI.deletedToast(instance.data.id, "Effects", "Effect");
popDialogStack();
},
"iron-select .statDropDown": function(event){
"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;
Effects.update(this._id, {$set: {stat: statName}});
setStat(statName, this._id);
},
"iron-select .operationDropDown": function(event){
"iron-select .operationMenu": function(event){
var detail = event.originalEvent.detail;
var opName = detail.item.getAttribute("name");
if (opName == this.operation) return;
Effects.update(this._id, {$set: {operation: opName}});
},
"iron-select .damageMultiplierDropDown": function(event){
"iron-select .multiplierMenu": function(event){
var detail = event.originalEvent.detail;
var value = +detail.item.getAttribute("name");
if (value == this.value) return;
@@ -164,15 +231,25 @@ Template.effectEdit.events({
operation: "mul",
}});
},
"change .effectValueInput": function(event){
"change .effectValueInput, input .effectValueInput":
_.debounce(function(event){
var value = event.currentTarget.value;
var numValue = +value;
var numValue = value === "" ? NaN : +value;
if (_.isFinite(numValue)){
if (this.value === numValue) return;
Effects.update(this._id, {$set: {value: numValue, calculation: ""}});
Effects.update(this._id, {
$set: {value: numValue},
$unset: {calculation: ""},
});
} else if (_.isString(value)){
if (this.calculation === value) return;
Effects.update(this._id, {$set: {value: "", calculation: value}});
Effects.update(this._id, {
$set: {calculation: value},
$unset: {value: ""},
}, {
removeEmptyStrings: false,
trimStrings: false,
});
}
},
}, 400),
});

View File

@@ -1,6 +1,4 @@
<template name="effectView">
<tr>
<td>{{statName}}</td>
<td>{{operationName}}{{statValue}}</td>
</tr>
<td>{{statName}}</td>
<td>{{operationName}}{{statValue}}</td>
</template>

View File

@@ -1,4 +1,3 @@
//TODO add dexterity armor
var stats = {
"strength":{"name":"Strength"},
"dexterity":{"name":"Dexterity"},
@@ -131,8 +130,10 @@ Template.effectView.helpers({
return stats[this.stat] && stats[this.stat].name || "No Stat";
},
operationName: function(){
if (this.operation === "proficiency" ||
this.operation === "conditional") return null;
if (
this.operation === "proficiency" ||
this.operation === "conditional"
) return null;
if (stats[this.stat] && stats[this.stat].group === "Weakness/Resistance")
return null;
if (this.operation === "add" && evaluateEffect(this.charId, this) < 0)
@@ -141,9 +142,11 @@ Template.effectView.helpers({
operations[this.operation].name || "No Operation";
},
statValue: function(){
if (this.operation === "advantage" ||
this.operation === "disadvantage" ||
this.operation === "fail"){
if (
this.operation === "advantage" ||
this.operation === "disadvantage" ||
this.operation === "fail"
){
return null;
}
if (this.operation === "proficiency"){

View File

@@ -0,0 +1,3 @@
.effectsEditList .effect {
background: white;
}

View File

@@ -1,11 +1,19 @@
<!--needs to be given charId, parentId and parentCollection-->
<template name="effectsEditList">
{{#if effects.count}}
<div id="effects">
{{#if effects.length}}
<div class="effectsEditList">
<div class="paper-font-title">Effects</div>
{{#each effects}}
{{>effectEdit}}
{{/each}}
<table class="wideTable" style="width: 100%;">
{{#each effects}}
<tr class="effect" data-id={{_id}}>
{{>effectView}}
<td>
<paper-icon-button class="edit-effect" icon="create">
</paper-icon-button>
</td>
</tr>
{{/each}}
</table>
</div>
{{/if}}
<paper-button id="addEffectButton" class="red-button" raised>Add Effect</paper-button>

View File

@@ -8,17 +8,17 @@ Template.effectsEditList.helpers({
if (this.parentGroup){
selector["parent.group"] = this.parentGroup;
}
var cursor = Effects.find(selector);
return cursor;
var effects = Effects.find(selector).fetch();
return _.sortBy(effects, effect => statOrder[effect.stat] || 999);
}
});
Template.effectsEditList.events({
"tap #addEffectButton": function(){
"tap #addEffectButton": function(event, instance){
if (!_.isBoolean(this.enabled)) {
this.enabled = true;
}
Effects.insert({
const effectId = Effects.insert({
name: this.name,
charId: this.charId,
parent: {
@@ -29,5 +29,18 @@ Template.effectsEditList.events({
operation: "add",
enabled: this.enabled,
});
pushDialogStack({
template: "effectEdit",
data: {id: effectId},
element: event.currentTarget,
returnElement: () => instance.find(`tr.effect[data-id='${effectId}']`),
});
},
"tap .edit-effect": function(event, template){
pushDialogStack({
template: "effectEdit",
data: {id: this._id},
element: event.currentTarget.parentElement.parentElement,
});
},
});

View File

@@ -1,11 +1,11 @@
<!--needs to be given charId, (parentId or stat) and type-->
<template name="effectsViewList">
{{#if effects.count}}
{{#if effects.length}}
<div class="effects">
<div class="spaceAfter paper-font-title">Effects</div>
<table class="wideTable">
{{#each effects}}
{{>effectView}}
<tr>{{>effectView}}</tr>
{{/each}}
</table>
</div>

View File

@@ -2,11 +2,14 @@ Template.effectsViewList.helpers({
effects: function(){
var selector = {
"parent.id": this.parentId,
"charId": this.charId
"charId": this.charId,
};
if (this.parentGroup){
selector["parent.group"] = this.parentGroup;
}
return Effects.find(selector, {fields: {parent: 0}});
let effects = Effects.find(selector, {
fields: {parent: 0},
}).fetch();
return _.sortBy(effects, effect => statOrder[effect.stat] || 999);
}
});

View File

@@ -0,0 +1,4 @@
.exportDialog .iiexport {
overflow-y: auto;
width: 100% !important;
}

View File

@@ -0,0 +1,31 @@
<template name="exportDialog">
<div class="exportDialog fit layout vertical">
{{#with character}}
<app-header fixed effects="waterfall">
<app-toolbar>
<div main-title>Export Character to Improved Initiative</div>
</app-toolbar>
</app-header>
<div class="form flex layout vertical">
<paper-toggle-button id="exportFeatures" checked={{settings.exportFeatures}}>
Features
</paper-toggle-button>
<paper-toggle-button id="exportAttacks" checked={{settings.exportAttacks}}>
Attacks
</paper-toggle-button>
<paper-toggle-button id="exportDescription" checked={{settings.exportDescription}}>
Description
</paper-toggle-button>
<div class="paper-font-title padded">JSON</div>
<textarea class="flex iiexport">{{improvedInitiativeJson}}</textarea>
<paper-button id="copyExportButton" class="red-button" raised>
<iron-icon icon="content-copy"></iron-icon>
Copy to Clipboard
</paper-button>
</div>
<div class="buttons layout horizontal end-justified">
<paper-button class="doneButton"> Done </paper-button>
</div>
{{/with}}
</div>
</template>

View File

@@ -0,0 +1,60 @@
Template.exportDialog.helpers({
character: function(){
return Characters.findOne(this._id);
},
improvedInitiativeJson: function(){
var options = {
features: this.settings.exportFeatures,
attacks: this.settings.exportAttacks,
description: this.settings.exportDescription,
}
return improvedInitiativeJson(this._id, options);
},
});
Template.exportDialog.events({
"change #exportFeatures": function(event, template){
Characters.update(this._id, {$set: {
"settings.exportFeatures": event.target.checked,
}});
},
"change #exportAttacks": function(event, template){
Characters.update(this._id, {$set: {
"settings.exportAttacks": event.target.checked,
}});
},
"change #exportDescription": function(event, template){
Characters.update(this._id, {$set: {
"settings.exportDescription": event.target.checked,
}});
},
"click #copyExportButton": function(event, template){
var copyTextarea = template.find(".iiexport");
copyTextarea.select();
var msg;
try {
var successful = document.execCommand("copy");
var msg = successful ? "JSON copied to clipboard" : "Unable to copy JSON";
} catch (err) {
msg = "Unable to copy JSON";
} finally {
clearSelection();
GlobalUI.toast(msg);
}
},
"click .doneButton": function(event, instance){
popDialogStack();
},
});
var clearSelection = function(){
if (window.getSelection) {
if (window.getSelection().empty) { // Chrome
window.getSelection().empty();
} else if (window.getSelection().removeAllRanges) { // Firefox
window.getSelection().removeAllRanges();
}
} else if (document.selection) { // IE?
document.selection.empty();
}
};

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

@@ -78,6 +78,20 @@
margin-left: 16px;
}
.spell.item > div > div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.spell.item > div {
min-width: 0;
}
.spell.item iron-icon {
flex-shrink: 0;
}
.spellLevel {
-webkit-backface-visibility: hidden;
-webkit-transform: translateX(0);

View File

@@ -258,6 +258,7 @@ Template.spells.events({
}
// Make the library spell into a regular spell
let spell = _.omit(result, "library", "attacks", "effects");
spell._id = spellId;
spell.charId = charId;
spell.parent = {
id: listId,

View File

@@ -28,8 +28,8 @@
{{/unless}}
</div>
<paper-diff-slider class="tempHitPointSlider flex"
value={{left}}
max={{maximum}}
value={{left}}
editable pin
></paper-diff-slider>
</div>

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

@@ -11,7 +11,7 @@
{{#if characters.count}}
<div class="character-list layout horizontal wrap">
{{# each characters}}
<a class="character-card flex layout vertical end-justified" href="/character/{{_id}}">
<a class="character-card flex layout vertical end-justified" href="{{pathFor route="characterSheet" data=this}}">
<iron-image class="fit {{colorClass}}"
sizing="cover" preload fade src={{picture}}>
</iron-image>

View File

@@ -10,7 +10,15 @@ Template.characterList.helpers({
]
},
{
fields: {name: 1, picture: 1, color: 1, race: 1, alignment: 1, gender: 1},
fields: {
name: 1,
urlName: 1,
picture: 1,
color: 1,
race: 1,
alignment: 1,
gender: 1,
},
sort: {name: 1},
}
);

View File

@@ -14,7 +14,7 @@ Template.characterSideList.helpers({
]
},
{
fields: {name: 1},
fields: {name: 1, urlName: 1},
sort: {name: 1},
}
);

View File

@@ -21,10 +21,15 @@
max-width: 300px;
padding: 16px;
text-align: center;
min-width: 0;
}
.intro .section .columns {
max-width: 100%;
}
.intro paper-button {
min-width: 200px;
flex-basis: 200px;
}

View File

@@ -64,7 +64,7 @@
</div>
<div class="section white-text" style="background: #282828">
<div class="columns layout horizontal around-justified wrap">
<div>
<div class="layout vertical center">
<div class="paper-font-headline">
Guide
</div>
@@ -78,7 +78,7 @@
</paper-button>
</a>
</div>
<div>
<div class="layout vertical center">
<div class="paper-font-headline">
Discuss
</div>
@@ -91,7 +91,7 @@
</paper-button>
</a>
</div>
<div>
<div class="layout vertical center">
<div class="paper-font-headline">
Get involved
</div>

View File

@@ -27,7 +27,7 @@
{{/if}}
{{/if}}
</app-toolbar>
<div class="form flex scroll-y">
<div class="form flex scroll-y" style="position: relative;">
{{#unless editing}}
{{> UI.contentBlock}}
{{else}}

View File

@@ -0,0 +1,23 @@
<template name="baseEditDialog">
<div class="fit base-dialog layout vertical">
<app-toolbar class={{class}}>
<paper-icon-button id="backButton"
icon="arrow-back">
</paper-icon-button>
<div main-title>{{title}}</div>
{{#unless hideDelete}}
<paper-icon-button id="deleteButton"
role="button"
tabindex="0"
icon="delete">
</paper-icon-button>
{{/unless}}
{{#unless hideColor}}
{{> colorDropdown}}
{{/unless}}
</app-toolbar>
<div class="form flex scroll-y" style="position: relative;">
{{> UI.contentBlock}}
</div>
</div>
</template>

View File

@@ -0,0 +1,5 @@
Template.baseEditDialog.events({
"tap #backButton": function(){
popDialogStack();
},
});

View File

@@ -13,14 +13,80 @@ pushDialogStack = function({template, data, element, returnElement, callback}){
returnElement,
callback,
});
updateHistory();
};
var currentResult;
popDialogStack = function(result){
if (history && history.state && history.state.openDialogs){
currentResult = result;
history.back();
} else {
popDialogStackAction(result);
}
}
window.onpopstate = function(event){
let state = event.state;
let numDialogs = dialogs._array.length;
if (_.isFinite(state.openDialogs) && numDialogs > state.openDialogs){
popDialogStackAction(currentResult);
currentResult = undefined;
}
}
popDialogStackAction = function(result){
const dialog = dialogs.pop();
updateHistory();
if (!dialog) return;
dialog.callback && dialog.callback(result);
};
let updateHistory = function(){
// history should looks like: [{openDialogs: 0}, {openDialogs: n}] where
// n is the number of open dialogs
// If we can't access the history object, give up
if (!history) return;
// Make sure that there is a state tracking open dialogs
// replace the state without bashing it in the process
if (!history.state || !_.isFinite(history.state.openDialogs)){
let newState = _.clone(history.state) || {};
newState.openDialogs = 0;
history.replaceState(newState, "");
}
const numDialogs = dialogs._array.length;
const stateDialogs = history.state.openDialogs;
// If the number of dialogs and state dialogs are equal, we don't need to do
// anything
if (numDialogs === stateDialogs) return;
if (stateDialogs > 0){
// On a dialog count
if (numDialogs === 0){
// but shouldn't be
history.back();
} else {
// but should replace with correct count
let newState = _.clone(history.state) || {};
newState.openDialogs = dialogs._array.length;
history.replaceState(newState, "");
}
} else if (numDialogs > 0 && stateDialogs === 0){
// On the zero state, push a dialog count
history.pushState({openDialogs: numDialogs}, "");
} else {
console.warn(
"History could not be updated correctly, unexpected case",
{stateDialogs, numDialogs},
)
}
};
Template.dialogStack.helpers({
dialogStackClass(){
if (!dialogs.get().length) return "hide";

View File

@@ -25,10 +25,10 @@ Template.fabMenu.helpers({
});
Template.fabMenu.events({
"tap .expand-menu": function(event, instance) {
"click .expand-menu": function(event, instance) {
instance.active.set(!instance.active.get());
},
"tap .mini-holder paper-fab": function(event, instance) {
"click .mini-holder paper-fab": function(event, instance) {
instance.active.set(false);
},
});

View File

@@ -0,0 +1,71 @@
statOrder = {
"strength": 1,
"dexterity": 2,
"constitution": 3,
"intelligence": 4,
"wisdom": 5,
"charisma": 6,
"strengthSave": 7,
"dexteritySave": 8,
"constitutionSave": 9,
"intelligenceSave": 10,
"wisdomSave": 11,
"charismaSave": 12,
"acrobatics": 13,
"animalHandling": 14,
"arcana": 15,
"athletics": 16,
"deception": 17,
"history": 18,
"insight": 19,
"intimidation": 20,
"investigation": 21,
"medicine": 22,
"nature": 23,
"perception": 24,
"performance": 25,
"persuasion": 26,
"religion": 27,
"sleightOfHand": 28,
"stealth": 29,
"survival": 30,
"initiative": 31,
"hitPoints": 32,
"armor": 33,
"dexterityArmor": 34,
"speed": 35,
"proficiencyBonus": 36,
"ki": 37,
"sorceryPoints": 38,
"rages": 39,
"rageDamage": 40,
"expertiseDice": 41,
"superiorityDice": 42,
"carryMultiplier": 43,
"level1SpellSlots": 44,
"level2SpellSlots": 45,
"level3SpellSlots": 46,
"level4SpellSlots": 47,
"level5SpellSlots": 48,
"level6SpellSlots": 49,
"level7SpellSlots": 50,
"level8SpellSlots": 51,
"level9SpellSlots": 52,
"d6HitDice": 53,
"d8HitDice": 54,
"d10HitDice": 55,
"d12HitDice": 56,
"acidMultiplier": 57,
"bludgeoningMultiplier": 58,
"coldMultiplier": 59,
"fireMultiplier": 60,
"forceMultiplier": 61,
"lightningMultiplier": 62,
"necroticMultiplier": 63,
"piercingMultiplier": 64,
"poisonMultiplier": 65,
"psychicMultiplier": 66,
"radiantMultiplier": 67,
"slashingMultiplier": 68,
"thunderMultiplier": 69,
};

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

@@ -0,0 +1,45 @@
Meteor.startup(() => {
const collections = [
Attacks, Buffs, Classes, Effects, Experiences,
Features, Notes, Proficiencies, SpellLists, Spells,
Containers, Items,
];
/**
* Deletes all soft removed documents that were removed more than 30 minutes ago
* and were not restored
* @return {Number} Number of documents removed
*/
const deleteOldSoftRemovedDocs = function(){
let numRemoved = 0;
const now = new Date();
const thirtyMinutesAgo = new Date(now.getTime() - 30*60000);
_.each(collections, (collection) => {
numRemoved += collection.remove({
removed: true,
removedAt: {$lt: thirtyMinutesAgo} // dates *before* 30 minutes ago
});
});
return numRemoved;
};
SyncedCron.add({
name: "Delete all soft removed items that haven't been restored",
schedule: function(parser) {
return parser.text('every 6 hours');
},
job: function() {
deleteOldSoftRemovedDocs();
}
});
// Add a method to manually trigger removal
Meteor.methods({
deleteOldSoftRemovedDocs() {
const user = Meteor.users.findOne(this.userId);
if (user && _.contains(user.roles, "admin")){
return deleteOldSoftRemovedDocs();
}
},
});
});

View File

@@ -125,7 +125,7 @@ Migrations.add({
}
});
var effect = Effects.findOne({
charId: char._id, name: "Natural Carrying Capacity"
charId: char._id, name: "Natural Carrying Capacity",
});
if (effect) return;
Effects.insert({
@@ -141,7 +141,7 @@ Migrations.add({
},
});
effect = Effects.findOne({
charId: char._id, name: "Natural Carrying Capacity"
charId: char._id, name: "Natural Carrying Capacity",
});
if (!effect) throw "Carry capacity effect should be set by now."
});
@@ -150,3 +150,19 @@ Migrations.add({
return;
},
});
Migrations.add({
version: 5,
name: "Gives all characters a URL name",
up: function() {
//update characters
Characters.find({}).forEach(function(char){
if (char.urlName) return;
var urlName = getSlug(char.name, {maintainCase: true});
Characters.update(char._id, {$set: {urlName}});
});
},
down: function(){
return;
},
});

View File

@@ -15,6 +15,7 @@ Meteor.publish("characterList", function(){
{
fields: {
name: 1,
urlName: 1,
race: 1,
alignment: 1,
gender: 1,