Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a332a2965 | ||
|
|
44a1daf6f8 | ||
|
|
ac23afac5d | ||
|
|
a411fb2b43 | ||
|
|
35b6fe20ae | ||
|
|
54f055d3ef | ||
|
|
2bacb296ba | ||
|
|
8b061f7aa9 | ||
|
|
6a84c83644 | ||
|
|
3227cd0934 | ||
|
|
089feae26f | ||
|
|
99c72d1e10 | ||
|
|
1e67afbe6f | ||
|
|
1cfec1ca45 | ||
|
|
09d1ac9ba3 | ||
|
|
834b9cf384 | ||
|
|
37291b347a | ||
|
|
efdfbeb59e | ||
|
|
a165f9b253 | ||
|
|
4a6fca98bc | ||
|
|
35464128a0 | ||
|
|
398f8a8a2a | ||
|
|
812a1784b2 | ||
|
|
8fa9cd0148 | ||
|
|
0e0662cc9a | ||
|
|
ad4e3f5b20 | ||
|
|
4cd058e1fe | ||
|
|
8f30cee4d3 | ||
|
|
d7f7eb2e6a |
@@ -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 isn’t 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 it’s 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 isn’t 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 it’s triggered by damage. On a success, the spell ends.",
|
||||
"duration": "Concentration, up to 1 minute",
|
||||
"level": 1,
|
||||
"range": "30 feet",
|
||||
|
||||
@@ -48,3 +48,4 @@ es5-shim@4.6.15
|
||||
differential:vulcanize
|
||||
reactive-dict
|
||||
percolate:synced-cron
|
||||
ongoworks:speakingurl
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,10 +11,12 @@ Schemas.Action = new SimpleSchema({
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
type: {
|
||||
|
||||
@@ -12,6 +12,7 @@ Schemas.Attack = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
defaultValue: "New Attack",
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
details: {
|
||||
|
||||
@@ -8,6 +8,7 @@ Schemas.Buff = new SimpleSchema({
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
description: {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -9,6 +9,7 @@ Schemas.Spell = new SimpleSchema({
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
defaultValue: "New Spell",
|
||||
},
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
3
rpg-docs/client/globalHelpers/characterPath.js
Normal file
3
rpg-docs/client/globalHelpers/characterPath.js
Normal file
@@ -0,0 +1,3 @@
|
||||
Template.registerHelper("characterPath", function(char) {
|
||||
return `\/character\/${char._id}\/${char.urlName || "-"}`;
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
195
rpg-docs/client/lib/improvedInitiativeJson.js
Normal file
195
rpg-docs/client/lib/improvedInitiativeJson.js
Normal 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: "",
|
||||
}));
|
||||
}
|
||||
28
rpg-docs/client/lib/requestAnimationFramePolyfill.js
Normal file
28
rpg-docs/client/lib/requestAnimationFramePolyfill.js
Normal 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);
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.exportDialog .iiexport {
|
||||
overflow-y: auto;
|
||||
width: 100% !important;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
{{/unless}}
|
||||
</div>
|
||||
<paper-diff-slider class="tempHitPointSlider flex"
|
||||
value={{left}}
|
||||
max={{maximum}}
|
||||
value={{left}}
|
||||
editable pin
|
||||
></paper-diff-slider>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{{#if conditionalCount}}
|
||||
*
|
||||
{{/if}}
|
||||
{{#if showPassive}}
|
||||
{{#if isPassiveShown}}
|
||||
({{characterCalculate "passiveSkill" ../_id skill}})
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}']`),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.partyEdit .inPartyCheckbox {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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}),
|
||||
];
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user