Compare commits

...

14 Commits
1.2.4 ... 1.3.0

Author SHA1 Message Date
Thaum Rystra
a165f9b253 Merge branch 'feature-improved-initiative-export'
closes #95
2017-07-10 21:02:10 +02:00
Thaum Rystra
4a6fca98bc Cleaned up export dialog, fixed copying 2017-07-10 20:59:00 +02:00
Stefan Zermatten
35464128a0 Added basic export for improved initiative 2017-07-10 16:26:38 +02:00
Stefan Zermatten
398f8a8a2a Merge branch 'bugfix-style' 2017-06-28 09:33:20 +02:00
Stefan Zermatten
812a1784b2 Improved display of home page on smaller devices 2017-06-28 09:27:24 +02:00
Stefan Zermatten
8fa9cd0148 Prevented spell titles from overflowing their item in spell lists
closes #83
2017-06-28 09:26:52 +02:00
Stefan Zermatten
0e0662cc9a Added slightly specific rule to let headings wrap
closes #90
2017-06-28 09:26:13 +02:00
Stefan Zermatten
ad4e3f5b20 Stopped inventory items being separated from their containers
hopefully fixes #91
2017-06-28 09:25:25 +02:00
Stefan Zermatten
4cd058e1fe Set max of slider before value
Prevents unnecessary capping of temp HP to 100
Fixes #89
2017-06-08 09:48:51 +02:00
Stefan Zermatten
8f30cee4d3 Hotfixed column layout with translateZ(0) hack 2017-06-07 09:46:38 +02:00
Stefan Zermatten
d7f7eb2e6a Fixed some spells in the library 2017-06-07 09:35:59 +02:00
Stefan Zermatten
a59cf1162f added return value to remove old soft removed docs 2017-05-29 11:58:19 +02:00
Stefan Zermatten
7bc80da99e Moved cron setup to startup 2017-05-29 11:35:30 +02:00
Stefan Zermatten
2ddc520bb6 Added cron job to remove old documents 2017-05-29 11:16:46 +02:00
17 changed files with 379 additions and 7 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

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

View File

@@ -87,6 +87,7 @@ oauth2@1.1.11
observe-sequence@1.0.14
ordered-dict@1.0.9
percolate:migrations@0.9.8
percolate:synced-cron@1.3.2
promise@0.8.8
raix:eventemitter@0.1.3
random@1.0.10

View File

@@ -184,6 +184,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);

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

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

@@ -0,0 +1,191 @@
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": [],
"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");
return {
"immunities": multipliers["0"] || [],
"resistances": multipliers["0.5"] || [],
"weaknesses": multipliers["2"] || [],
};
}
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

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

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

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