Compare commits

...

29 Commits
1.2.5 ... 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
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
55 changed files with 930 additions and 294 deletions

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

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

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

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

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

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

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

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

@@ -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}),
];
});