Compare commits

..

15 Commits
1.6.2 ... 1.7.1

Author SHA1 Message Date
Thaum Rystra
45e9f491ff Merge branch 'feature-blacklist' 2018-04-02 14:27:24 +02:00
Thaum Rystra
742b26b0de Added rate limiting logging and an error page for hitting the rate limit on opening characters 2018-04-02 14:26:55 +02:00
Stefan Zermatten
164ba78c81 Added blacklist checks and rate limit logging
Needs testing
2018-03-12 09:22:04 +02:00
Thaum Rystra
e27211b24d Merge branch 'enhancement-154' 2018-03-03 20:13:03 +02:00
Thaum Rystra
30987752cc Hotfix - Remove localhost from image link on printed character sheet 2018-03-03 17:28:42 +02:00
Thaum Rystra
058ee2691f Merge branch 'feature-print'
# Conflicts:
#	rpg-docs/package-lock.json
2018-03-03 17:18:16 +02:00
Thaum Rystra
f0cf7f4956 Added QR code and finished page 1 2018-03-03 17:13:36 +02:00
Thaum Rystra
75c8720b04 Moved printed character sheets to their own page
This makes sure the entire printed sheet is rendered before the browser  attempts to print it, solving all manner of errors
2018-03-03 11:13:16 +02:00
Thaum Rystra
f73f2f670f Formatted all existing printed character sheet fields nicely 2018-03-02 21:40:21 +02:00
Thaum Rystra
c6e62e1cfa Added borders to ability scores and AC 2018-03-02 07:25:37 +02:00
Jacob
4e574c0f51 Added "clear" (reset to 0) button to resource cards 2018-02-24 14:49:42 +00:00
Jacob
80b195b7f7 Added reset button to resource cards 2018-02-24 14:42:41 +00:00
Stefan Zermatten
64b3ca6066 Added proficiencies to printed sheet; some fixes 2017-12-12 15:54:21 +02:00
Stefan Zermatten
8349f7da9b Merge branch 'master' into feature-print 2017-12-06 09:22:57 +02:00
Stefan Zermatten
00a050d337 Added basic printing functionality 2017-09-11 09:27:01 +02:00
42 changed files with 2486 additions and 267 deletions

View File

@@ -0,0 +1,9 @@
Blacklist = new Mongo.Collection("blacklist");
Schemas.Blacklist = new SimpleSchema({
userId: {
type: String,
},
});
Blacklist.attachSchema(Schemas.Blacklist);

View File

@@ -42,12 +42,21 @@ var ifKeyValid = function(apiKey, response, callback){
};
var isKeyValid = function(apiKey){
return !!Meteor.users.findOne({apiKey});
var user = Meteor.users.findOne({apiKey});
if (!user) return false;
var blackListed = Blacklist.findOne({userId: user._id});
return !blackListed;
};
var rateLimiter = new RateLimiter();
rateLimiter.addRule({apiKey: String}, 2, 10000);
var isRateLimited = function(apiKey){
return !rateLimiter.check({apiKey}).allowed;
const limited = !rateLimiter.check({apiKey}).allowed
if (limited) {
console.log(`Rate limit hit by API key ${apiKey}`);
return true;
} else {
return false;
}
};

View File

@@ -13,6 +13,11 @@ Router.plugin("ensureSignedIn", {
Router.plugin("dataNotFound", {notFoundTemplate: "notFound"});
var handleSubError = function(e){
Session.set("error", {reason: e.reason, href: location.href});
Router.go("/error");
};
Router.map(function() {
this.route("/", {
name: "home",
@@ -36,7 +41,9 @@ Router.map(function() {
path: "/character/:_id/",
waitOn: function(){
return [
subsManager.subscribe("singleCharacter", this.params._id),
subsManager.subscribe(
"singleCharacter", this.params._id, {onError: handleSubError}
),
];
},
action: function(){
@@ -52,7 +59,9 @@ Router.map(function() {
path: "/character/:_id/:urlName",
waitOn: function(){
return [
subsManager.subscribe("singleCharacter", this.params._id),
subsManager.subscribe(
"singleCharacter", this.params._id, {onError: handleSubError}
),
];
},
data: function() {
@@ -78,6 +87,37 @@ Router.map(function() {
fastRender: true,
});
this.route("printedCharacterSheet", {
path: "/character/:_id/:urlName/print",
waitOn: function(){
return [
subsManager.subscribe(
"singleCharacter", this.params._id, {onError: handleSubError}
),
];
},
data: function() {
var data = Characters.findOne(
{_id: this.params._id},
{fields: {_id: 1, name: 1, color: 1, writers: 1, readers: 1}}
);
return data;
},
onAfterAction: function() {
var char = Characters.findOne({_id: this.params._id}, {fields: {name: 1}});
var name = char && char.name;
if (name){
document.title = name + " - Printing";
}
},
//analytics
trackPageView: false,
onRun: function() {
window.ga && window.ga("send", "pageview", "/print-character");
this.next();
},
});
this.route("library", {
path: "/library",
waitOn: function(){
@@ -124,4 +164,11 @@ Router.map(function() {
document.title = appName;
},
});
this.route("/error", {
name: "error",
onAfterAction: function() {
document.title = `${appName} - Error`;
},
});
});

View File

@@ -0,0 +1,174 @@
// jscs:disable
// https://github.com/chunksnbits/jquery-quickfit
(function ($) {
var Quickfit, QuickfitHelper, defaults, pluginName;
pluginName = 'quickfit';
defaults = {
min: 8,
max: 12,
tolerance: 0.02,
truncate: false,
width: null,
sampleNumberOfLetters: 10,
sampleFontSize: 12
};
QuickfitHelper = (function () {
var sharedInstance = null;
QuickfitHelper.instance = function (options) {
if (!sharedInstance) {
sharedInstance = new QuickfitHelper(options);
}
return sharedInstance;
};
function QuickfitHelper(options) {
this.options = options;
this.item = $('<span id="meassure"></span>');
this.item.css({
position: 'absolute',
left: '-1000px',
top: '-1000px',
'font-size': "" + this.options.sampleFontSize + "px"
});
$('body').append(this.item);
this.meassures = {};
}
QuickfitHelper.prototype.getMeassure = function (letter) {
var currentMeassure;
currentMeassure = this.meassures[letter];
if (!currentMeassure) {
currentMeassure = this.setMeassure(letter);
}
return currentMeassure;
};
QuickfitHelper.prototype.setMeassure = function (letter) {
var currentMeassure, index, sampleLetter, text, _ref;
text = '';
sampleLetter = letter === ' ' ? '&nbsp;' : letter;
for (index = 0, _ref = this.options.sampleNumberOfLetters - 1; 0 <= _ref ? index <= _ref : index >= _ref; 0 <= _ref ? index++ : index--) {
text += sampleLetter;
}
this.item.html(text);
currentMeassure = this.item.width() / this.options.sampleNumberOfLetters / this.options.sampleFontSize;
this.meassures[letter] = currentMeassure;
return currentMeassure;
};
return QuickfitHelper;
})();
Quickfit = (function () {
function Quickfit(element, options) {
this.$element = element;
this.options = $.extend({}, defaults, options);
this.$element = $(this.$element);
this._defaults = defaults;
this._name = pluginName;
this.quickfitHelper = QuickfitHelper.instance(this.options);
}
Quickfit.prototype.fit = function () {
var elementWidth;
if (!this.options.width) {
elementWidth = this.$element.width();
this.options.width = elementWidth - this.options.tolerance * elementWidth;
}
if (this.text = this.$element.attr('data-quickfit')) {
this.previouslyTruncated = true;
} else {
this.text = this.$element.text();
}
this.calculateFontSize();
if (this.options.truncate) this.truncate();
return {
$element: this.$element,
size: this.fontSize
};
};
Quickfit.prototype.calculateFontSize = function () {
var letter, textWidth, i;
textWidth = 0;
for (i = 0; i < this.text.length; ++i) {
letter = this.text.charAt(i);
textWidth += this.quickfitHelper.getMeassure(letter);
}
this.targetFontSize = parseInt(this.options.width / textWidth);
return this.fontSize = Math.max(this.options.min, Math.min(this.options.max, this.targetFontSize));
};
Quickfit.prototype.truncate = function () {
var index, lastLetter, letter, textToAdd, textWidth;
if (this.fontSize > this.targetFontSize) {
textToAdd = '';
textWidth = 3 * this.quickfitHelper.getMeassure('.') * this.fontSize;
index = 0;
while (textWidth < this.options.width && index < this.text.length) {
letter = this.text[index++];
if (lastLetter) textToAdd += lastLetter;
textWidth += this.fontSize * this.quickfitHelper.getMeassure(letter);
lastLetter = letter;
}
if (textToAdd.length + 1 === this.text.length) {
textToAdd = this.text;
} else {
textToAdd += '...';
}
this.textWasTruncated = true;
return this.$element.attr('data-quickfit', this.text).html(textToAdd);
} else {
if (this.previouslyTruncated) {
return this.$element.html(this.text);
}
}
};
return Quickfit;
})();
return $.fn.quickfit = function (options) {
var measurements = [];
// Separate measurements from repaints
// First calculate all measurements...
var $elements = this.each(function () {
var measurement = new Quickfit(this, options).fit();
measurements.push(measurement);
return measurement.$element;
});
// ... then apply the measurements.
for (var i = 0; i < measurements.length; i++) {
var measurement = measurements[i];
measurement.$element.css({ fontSize: measurement.size + 'px' });
}
return $elements;
};
})(jQuery, window);

View File

@@ -0,0 +1,12 @@
Session.setDefault("isPrinting", false);
if (window.matchMedia) {
var mediaQueryList = window.matchMedia("print");
mediaQueryList.addListener(function(mql) {
if (mql.matches) {
Session.set("isPrinting", true);
Tracker.flush();
} else {
Session.set("isPrinting", false);
}
});
}

View File

@@ -0,0 +1,17 @@
removeDuplicateProficiencies = function(proficiencies) {
dict = {};
proficiencies.forEach(function(prof) {
if (prof.name in dict) { //if we have already gone over another proficiency for the same thing
if (dict[prof.name].value < prof.value) {
dict[prof.name] = prof; //then take the new one if it's higher, otherwise leave it
}
} else {
dict[prof.name] = prof; //if it wasn't already there, store it
}
});
profs = []
_.forEach(dict, function(prof) {
profs.push(prof);
})
return profs;
};

View File

@@ -20,6 +20,12 @@
<iron-icon icon="social:share" item-icon></iron-icon>
Share
</paper-icon-item>
<a href={{printUrl}}>
<paper-icon-item id="printButton">
<iron-icon icon="print" item-icon></iron-icon>
Print
</paper-icon-item>
</a>
<paper-icon-item id="characterSettings">
<iron-icon icon="settings" item-icon></iron-icon>
Settings

View File

@@ -165,6 +165,12 @@ var getTab = function(charId){
};
Template.characterSheet.helpers({
printing: function(){
return Session.get("isPrinting");
},
printUrl: function(){
return `/character/${this._id}/${this.urlName || "-"}/print`
},
selectedTab: function(){
return getTab(this._id);
},
@@ -181,8 +187,8 @@ Template.characterSheet.helpers({
const step = Session.get("newUserExperienceStep");
if (selected == tab) return false;
return (tab === 1 && step === 0) ||
(tab === 5 && step === 1) ||
(tab === 0 && step === 2);
(tab === 5 && step === 1) ||
(tab === 0 && step === 2);
},
});

View File

@@ -133,6 +133,12 @@
<div class="right clickable flex layout horizontal center">
{{title}}
</div>
<div class="layout horizontal center">
<div class="layout vertical">
<paper-button class="resourceResetMax" disabled={{cantIncrement}}>Reset</paper-button>
<paper-button class="resourceResetZero" disabled={{cantDecrement}}>Clear</paper-button>
</div>
</div>
</paper-material>
</div>
{{/if}}

View File

@@ -1,21 +1,3 @@
var removeDuplicateProficiencies = function(proficiencies) {
dict = {};
proficiencies.forEach(function(prof) {
if (prof.name in dict) { //if we have already gone over another proficiency for the same thing
if (dict[prof.name].value < prof.value) {
dict[prof.name] = prof; //then take the new one if it's higher, otherwise leave it
}
} else {
dict[prof.name] = prof; //if it wasn't already there, store it
}
});
profs = []
_.forEach(dict, function(prof) {
profs.push(prof);
})
return profs;
};
Template.features.helpers({
features: function(){
var features = Features.find({charId: this._id}, {sort: {color: 1, name: 1}});
@@ -129,6 +111,17 @@ Template.resource.helpers({
});
Template.resource.events({
"click .resourceResetMax": function(event){
var modifier = {$set: {}};
modifier.$set[this.name + ".adjustment"] = 0;
Characters.update(this.char._id, modifier, {validate: false});
},
"click .resourceResetZero": function(event){
var base = Characters.calculate.attributeBase(this.char._id, this.name);
var modifier = {$set: {}};
modifier.$set[this.name + ".adjustment"] = -base;
Characters.update(this.char._id, modifier, {validate: false});
},
"click .resourceUp": function(event){
var value = Characters.calculate.attributeValue(this.char._id, this.name);
var base = Characters.calculate.attributeBase(this.char._id, this.name);

View File

@@ -0,0 +1,3 @@
.printedAbility .title.paper-font-subhead {
font-size: 2.5mm !important;
}

View File

@@ -0,0 +1,13 @@
<template name="printedAbility">
<div class="printedAbility layout vertical center double-border">
<div class="paper-font-subhead title flex layout horizontal center">
{{title}}
</div>
<div class="paper-font-display1 stat">
{{characterCalculate "attributeValue" ../_id ability}}
</div>
<div class="paper-font-subhead modifier">
{{abilityMod}}
</div>
</div>
</template>

View File

@@ -0,0 +1,9 @@
Template.printedAbility.helpers({
abilityMod: function() {
return signedString(
Characters.calculate.abilityMod(
Template.parentData()._id, this.ability
)
);
}
});

View File

@@ -0,0 +1,23 @@
<template name="printedAttack">
<div class="printedAttack" style="margin-bottom: 2mm">
<div class="layout horizontal">
<div class="paper-font-headline layout horizontal center"
style="margin-right: 1mm; min-width: 32px; text-align: right;">
{{evaluateAttackBonus charId attack}}
</div>
<div class="flex layout vertical">
<div class="paper-font-body2">
{{attack.name}}
</div>
<div>
{{evaluateDamage charId attack}}&nbsp;{{attack.damageType}}
</div>
{{#if attack.details}}
<div>
{{attack.details}}
</div>
{{/if}}
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,30 @@
Template.printedAttack.helpers({
evaluateAttackBonus: function(charId, attack) {
if (attack.parent.collection == "Spells") {
var spell = Spells.findOne(attack.parent.id);
if (spell) {
bonus = evaluate(charId, attack.attackBonus, {
"spellListId": spell.parent.id
});
}
} else {
var bonus = evaluate(charId, attack.attackBonus);
}
if (_.isFinite(bonus)) {
return bonus > 0 ? "+" + bonus : "" + bonus;
} else {
return bonus;
}
},
evaluateDamage: function(charId, attack) {
if (attack.parent.collection == "Spells") {
var spell = Spells.findOne(attack.parent.id);
if (spell) {
return evaluateSpellString(charId, spell.parent.id, attack.damage);
}
} else {
return evaluateString(charId, attack.damage);
}
},
});

View File

@@ -0,0 +1,190 @@
.printed .page {
width: 100%;
padding: 6mm;
page-break-inside: avoid;
page-break-after: always;
}
.printed .shrink-to-fit {
white-space: nowrap;
overflow: hidden;
}
.printed p {
margin-bottom: 1mm;
}
.printed .double-border {
position: relative;
padding: 11px 10px;
}
.printed .double-border > * {
position: relative;
}
.printed .double-border:before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border: 16px solid transparent;
border-image-source: url(/png/doubleLineImageBorder.png);
border-image-slice: 110 126 fill;
border-image-repeat: stretch;
box-sizing: content-box;
}
.printed .double-border.printedAbility {
padding: 11px 6px 0;
margin-bottom: 3mm;
}
.printed .double-border.printedAbility:last-of-type {
margin-bottom: 0;
}
.printed .printedAbility .modifier {
position: relative;
top: 4px;
z-index: 1;
padding: 2px 18px;
background-image: url(/png/upwardPointingBorder.png);
background-position: center;
background-size: contain;
background-repeat: no-repeat;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
.printed .octogon-border {
position: relative;
padding: 0 20px;
}
.printed .octogon-border:before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border: 22px solid transparent;
border-image: url(/png/octogonBorder.png) 124 118 fill;
z-index: -1;
}
.printed iron-icon {
width: 16px;
min-width: 16px;
height: 16px;
min-height: 16px;
}
.printed .proficiencies, .printed .attacks, .printed .background {
overflow: hidden;
}
.printed .shield-background {
background: url(/png/shieldBorder.png);
background-size: cover;
background-repeat: no-repeat;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
padding: 4px 8px 8px;
width: 80px;
height: 91px;
position: relative;
}
.printed .shield-background .paper-font-subhead {
width: 64px;
text-align: center;
line-height: 1.1;
}
.printed {
font-size: 3mm;
}
.printed .paper-font-body2 {
font-size: 3mm;
line-height: 4mm;
}
.printed .paper-font-subhead {
font-size: 3mm !important;
line-height: 3.5mm !important;
font-weight: bold !important;
text-transform: uppercase !important;
}
.printed .paper-font-subhead.modifier {
font-size: 4mm !important;
line-height: 6mm !important;
}
.printed .paper-font-display1 {
font-size: 7mm !important;
line-height: 12mm !important;
}
.printed .paper-font-headline {
font-size: 5mm !important;
line-height: 6mm !important;
}
.printed .lined-background {
background-image: url(/png/horizontalLine.png);
background-size: 100% 4mm;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
@media screen {
.printed .page {
width: 210mm;
height: 297mm;
background: white;
margin: 8px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12),
0 3px 1px -2px rgba(0, 0, 0, 0.2);
}
.printed .page-holder {
width: calc(210mm + 16px);
}
.printed {
overflow: auto;
padding-left:
}
}
@media print {
app-drawer {
display: none;
}
app-header {
display: none;
}
.printed {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 99;
background: #fff;
}
.printed .page-holder {
height: 100%
}
.printed .page {
height: 100%;
}
}

View File

@@ -0,0 +1,276 @@
<template name="printedCharacterSheet">
<div class="fit printed-character-sheet layout vertical">
<app-header fixed effects="waterfall">
<app-toolbar class="medium {{colorClass}} layout horizontal center" style="z-index: 2;">
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
<paper-icon-button icon="arrow-back" class="backButton"></paper-icon-button>
<div class="flex character-name">
{{name}}
</div>
<div style="position: relative;">
<paper-icon-button icon="print" class="printButton"></paper-icon-button>
{{#simpleTooltip}} Print {{/simpleTooltip}}
</div>
</app-toolbar>
</app-header>
<div class="printed flex">
<div class="page-holder">
<div class="page">
<div class="layout vertical" style="height: 100%;">
<div class="layout horizontal center" style="margin-bottom: 4mm">
<img src="/crown-dice-logo-cropped-transparent.png" style="width: 60px; margin-right: 2mm">
<div class="characterName paper-font-title" style="margin-right: 4mm">
{{name}}
</div>
<div class="paper-font-body2">
<div>
{{#each classes}}
<span style="margin-right: 2mm;">
{{name}}&nbsp;{{level}}
</span>
{{/each}}
</div>
<div>
{{character.alignment}} {{character.gender}} {{character.race}}
</div>
</div>
<div class="flex layout vertical end" style="margin-right: 2mm;">
<div class="paper-font-body2 " style="font-size: 5mm !important;">
dicecloud.com
</div>
<div>
{{characterUrl}}
</div>
</div>
<canvas id="qrCode"></canvas>
</div>
<div class="columns layout horizontal flex">
<div class="col1 flex layout vertical">
<div class="layout vertical center-justified" style="min-height: 100px; margin-bottom: 4mm;">
<div class="initiative" style="margin-bottom: 2mm;">
{{> printedLongStat stat="" name="Inspiration" prefix=""}}
</div>
<div class="proficiencyBonus">
{{> printedLongStat stat="proficiencyBonus" name="Proficiency Bonus" prefix="+"}}
</div>
</div>
<div class="layout horizontal">
<div class="abilities layout vertical justified" style="margin-right: 4mm;">
{{> printedAbility ability="strength" title="Strength"}}
{{> printedAbility ability="dexterity" title="Dexterity"}}
{{> printedAbility ability="constitution" title="Constitution"}}
{{> printedAbility ability="intelligence" title="Intelligence"}}
{{> printedAbility ability="wisdom" title="Wisdom"}}
{{> printedAbility ability="charisma" title="Charisma"}}
</div>
<div class="flex layout vertical">
<div class="saves double-border" style="margin-bottom: 2mm">
<div>
{{> printedSkillRow name="Strength" skill="strengthSave"}}
{{> printedSkillRow name="Dexterity" skill="dexteritySave"}}
{{> printedSkillRow name="Constitution" skill="constitutionSave"}}
{{> printedSkillRow name="Intelligence" skill="intelligenceSave"}}
{{> printedSkillRow name="Wisdom" skill="wisdomSave"}}
{{> printedSkillRow name="Charisma" skill="charismaSave"}}
</div>
<div class="paper-font-subhead layout vertical center">
Saving Throws
</div>
</div>
<div class="skills double-border">
<div>
{{> printedSkillRow name="Acrobatics" skill="acrobatics"}}
{{> printedSkillRow name="Animal Handling" skill="animalHandling"}}
{{> printedSkillRow name="Arcana" skill="arcana"}}
{{> printedSkillRow name="Athletics" skill="athletics"}}
{{> printedSkillRow name="Deception" skill="deception"}}
{{> printedSkillRow name="History" skill="history"}}
{{> printedSkillRow name="Insight" skill="insight"}}
{{> printedSkillRow name="Intimidation" skill="intimidation"}}
{{> printedSkillRow name="Investigation" skill="investigation"}}
{{> printedSkillRow name="Medicine" skill="medicine"}}
{{> printedSkillRow name="Nature" skill="nature"}}
{{> printedSkillRow name="Perception" skill="perception" showPassive="true"}}
{{> printedSkillRow name="Performance" skill="performance"}}
{{> printedSkillRow name="Persuasion" skill="persuasion"}}
{{> printedSkillRow name="Religion" skill="religion"}}
{{> printedSkillRow name="Sleight of Hand" skill="sleightOfHand"}}
{{> printedSkillRow name="Stealth" skill="stealth"}}
{{> printedSkillRow name="Survival" skill="survival"}}
</div>
<div class="paper-font-subhead layout vertical center">
Skills
</div>
</div>
</div>
</div>
<div class="proficiencies flex double-border" style="margin-top: 4mm">
<div class="paper-font-subhead layout vertical center" style="margin-bottom: 2mm;">
Proficiencies
</div>
<div class="layout horizontal">
<div class="flex" style="margin-right: 2mm">
{{#if armorProfs.length}}
<div class="paper-font-subhead" style="margin-bottom: 1mm;">Armor</div>
{{/if}}
{{#each armorProfs}}
{{> printedProficiency}}
{{/each}}
{{#if weaponProfs.length}}
<div class="paper-font-subhead" style="margin: 2mm 0 1mm;">Weapons</div>
{{/if}}
{{#each weaponProfs}}
{{> printedProficiency}}
{{/each}}
</div>
{{#if toolProfs.length}}
<div class="flex">
<div class="paper-font-subhead" style="margin-bottom: 1mm;">Tools</div>
{{#each toolProfs}}
{{> printedProficiency}}
{{/each}}
</div>
{{/if}}
</div>
</div>
</div>
<div class="col2 flex layout vertical" style="margin-left: 4mm; margin-right: 4mm;">
<div class="layout horizontal center justified" style="min-height: 100px; margin-bottom: 4mm;">
<div class="armor">
{{> printedSquareStat stat="armor" name="Armor Class" class="shield-background"}}
</div>
<div class="inititive">
{{> printedSquareStat stat="initiative" name="Initiative" isSkill="true" class="double-border"}}
</div>
<div class="speed">
{{> printedSquareStat stat="speed" name="Speed" class="double-border"}}
</div>
</div>
<div class="hitpoints layout vertical double-border" style="margin-bottom: 2mm;">
<div>
Hit point maximum:
<span class="paper-font-subhead">
{{characterCalculate "attributeBase" _id "hitPoints"}}
</span>
</div>
<div class="flex" style="width: 3cm; height: 2cm;">
<!-- Space for writing -->
</div>
<div class="layout vertical center paper-font-subhead">
Hit Points
</div>
</div>
<div class="tempHitpoints layout vertical double-border" style="margin-bottom: 2mm;">
<div style="width: 3cm; height: 1.5cm;">
<!-- Space for writing -->
</div>
<div class="layout vertical center paper-font-subhead">
Temporary Hit Points
</div>
</div>
<div class="layout horizontal" style="margin-bottom: 4mm;">
<div class="hitDice double-border flex layout vertical" style="margin-right: 2mm;">
<div>
Total:
<span class="paper-font-subhead" style="text-transform: none !important;">
{{hitDiceTotal}}
</span>
</div>
<div class="flex" style="min-height: 1cm;">
<!-- Space for writing -->
</div>
<div class="paper-font-subhead layout vertical center">
Hit Dice
</div>
</div>
<div class="deathSaves layout vertical center double-border">
<div class="" style="margin-bottom: 1mm;">
Successes
</div>
<div class="layout horizontal center">
<iron-icon icon="radio-button-unchecked"></iron-icon>
<iron-icon icon="radio-button-unchecked"></iron-icon>
<iron-icon icon="radio-button-unchecked"></iron-icon>
</div>
<div class="" style="margin: 1mm 0;">
Failures
</div>
<div class="layout horizontal center">
<iron-icon icon="radio-button-unchecked"></iron-icon>
<iron-icon icon="radio-button-unchecked"></iron-icon>
<iron-icon icon="radio-button-unchecked"></iron-icon>
</div>
<div class="paper-font-subhead layout vertical center" style="margin-top: 2mm;">
Death Saves
</div>
</div>
</div>
<div class="attacks double-border flex">
<div class="paper-font-subhead layout vertical center" style="margin-bottom: 2mm;">
Attacks
</div>
{{#each attack in attacks}}
{{> printedAttack attack=attack charId=_id}}
{{/each}}
</div>
</div>
<div class="col3 flex layout vertical">
<div class="Languages double-border" style="min-height: 100px; margin-bottom: 4mm;">
<div class="paper-font-subhead layout vertical center" style="margin-bottom: 2mm;">
Languages
</div>
<div class="layout horizontal">
<div class="flex" style="margin-right: 2mm;">
{{#each languageProfs.left}}
{{> printedProficiency}}
{{/each}}
</div>
{{#if languageProfs.right.length}}
<div class="flex">
{{#each languageProfs.right}}
{{> printedProficiency}}
{{/each}}
</div>
{{/if}}
</div>
</div>
<div class="traits double-border">
{{#markdown}}{{evaluateShortString character._id character.personality}}{{/markdown}}
<div class="paper-font-subhead layout vertical center">
Personality traits
</div>
</div>
<div class="ideals double-border" style="margin-top: 2mm">
{{#markdown}}{{evaluateShortString character._id character.ideals}}{{/markdown}}
<div class="paper-font-subhead layout vertical center">
Ideals
</div>
</div>
<div class="bonds double-border" style="margin-top: 2mm">
{{#markdown}}{{evaluateShortString character._id character.bonds}}{{/markdown}}
<div class="paper-font-subhead layout vertical center">
Bonds
</div>
</div>
<div class="flaws double-border" style="margin-top: 2mm">
{{#markdown}}{{evaluateShortString character._id character.flaws}}{{/markdown}}
<div class="paper-font-subhead layout vertical center">
Flaws
</div>
</div>
<div class="background double-border flex layout vertical" style="margin-top: 2mm">
<div class="paper-font-subhead layout vertical center" style="margin-bottom: 4mm">
Notes
</div>
<div class="flex lined-background">
<!-- lined space for writing -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,80 @@
import QRCode from "qrcode"
Template.printedCharacterSheet.onRendered(function(){
// Quickfit is only called once on rendering, text will not resize reactively
this.$(".shrink-to-fit").quickfit({
min: 7,
max: 36,
truncate: true,
});
let url = `https://dicecloud.com/character/${this.data._id}`;
let canvas = this.find("#qrCode");
QRCode.toCanvas(canvas, url, {
margin: 0,
width: 200,
}, function(error){
$(canvas).css("width", "60px").css("height", "60px");
if (error) console.error(error)
});
});
Template.printedCharacterSheet.helpers({
character(){
return Characters.findOne(this._id);
},
classes: function(){
return Classes.find({charId: this._id}, {sort: {createdAt: 1}});
},
weaponProfs: function(){
var profs = Proficiencies.find({charId: this._id, type: "weapon"});
return removeDuplicateProficiencies(profs);
},
armorProfs: function(){
var profs = Proficiencies.find({charId: this._id, type: "armor"});
return removeDuplicateProficiencies(profs);
},
toolProfs: function(){
var profs = Proficiencies.find({charId: this._id, type: "tool"});
return removeDuplicateProficiencies(profs);
},
languageProfs: function(){
var profs = Proficiencies.find({charId: this._id, type: "language"});
profs = removeDuplicateProficiencies(profs);
if (profs.length > 3){
var halfway = Math.floor(profs.length / 2);
var left = profs.slice(0, halfway);
var right = profs.slice(halfway);
return {left, right};
} else {
return {left: profs, right: []};
}
},
attacks: function(){
return Attacks.find(
{charId: this._id, enabled: true},
{sort: {color: 1, name: 1}});
},
hitDiceTotal: function(){
let d6 = Characters.calculate.attributeValue(this._id, "d6HitDice");
let d8 = Characters.calculate.attributeValue(this._id, "d8HitDice");
let d10 = Characters.calculate.attributeValue(this._id, "d10HitDice");
let d12 = Characters.calculate.attributeValue(this._id, "d12HitDice");
d6 = d6 ? d6 + "d6" : "";
d8 = d8 ? d8 + "d8" : "";
d10 = d10 ? d10 + "d10" : "";
d12 = d12 ? d12 + "d12" : "";
return [d6, d8, d10, d12].filter(Boolean).join(" ");
},
characterUrl: function(){
return `/character/${this._id}`
},
});
Template.printedCharacterSheet.events({
"click .printButton": function(event, instance){
print();
},
"click .backButton": function(event, instance){
history && history.back();
},
});

View File

@@ -0,0 +1,20 @@
.printedLongStat .title {
white-space: nowrap;
margin-left: 2mm;
}
.printedLongStat .numbers {
z-index: 1;
min-width: 74px;
min-height: 45px;
}
.printed .printedLongStat.double-border{
padding: 0;
}
.printed .printedLongStat.double-border:before {
top: 4px;
bottom: 4px;
left: 33px;
}

View File

@@ -0,0 +1,16 @@
<template name="printedLongStat">
<div class="printedLongStat layout horizontal double-border">
<div class="numbers paper-font-display1 octogon-border">
{{#if stat}}
{{#if isSkill}}
{{prefix}}{{skillMod}}
{{else}}
{{prefix}}{{characterCalculate "attributeValue" ../_id stat}}
{{/if}}
{{/if}}
</div>
<div class="paper-font-subhead title flex layout horizontal center">
{{name}}
</div>
</div>
</template>

View File

@@ -0,0 +1,9 @@
Template.printedLongStat.helpers({
skillMod: function() {
return signedString(
Characters.calculate.skillMod(
Template.parentData()._id, this.stat
)
);
},
});

View File

@@ -0,0 +1,3 @@
.printedProficiency iron-icon {
margin-right: 2mm;
}

View File

@@ -0,0 +1,6 @@
<template name="printedProficiency">
<div class="printedProficiency layout horizontal center">
<iron-icon icon="{{profIcon}}"></iron-icon>
<div>{{getName}}</div>
</div>
</template>

View File

@@ -0,0 +1,40 @@
Template.printedProficiency.helpers({
profIcon: function(){
var prof = this.value;
if (prof > 0 && prof < 1) return "image:brightness-2";
if (prof === 1) return "image:brightness-1";
if (prof > 1) return "av:album";
return "radio-button-off";
},
getName: function(){
if (this.type === "skill") return skills[this.name];
if (this.type === "save") return saves[this.name];
return this.name;
},
});
Template.printedProficiency.events({
"click .proficiency": function(event, instance){
if (this.parent.collection == "Characters") {
if (this.parent.group == "background") {
pushDialogStack({
template: "backgroundDialog",
data: {
"charId": this.charId,
"field":"background",
"title":"Background",
"color":"j",
},
element: event.currentTarget,
})
return;
}
}
openParentDialog({
parent: this.parent,
charId: this.charId,
element: event.currentTarget,
});
}
});

View File

@@ -0,0 +1,10 @@
.printedSkillRow {
height: 24px;
min-width: 140px;
}
.printedSkillRow .skill-mod {
width: 36px;
text-align: center;
font-size: 3.5mm;
}

View File

@@ -0,0 +1,21 @@
<template name="printedSkillRow">
<div class="printedSkillRow layout horizontal center">
<iron-icon icon="{{profIcon}}"></iron-icon>
{{#if failSkill}}
<div class="fail skill-mod">fail</div>
{{else}}
<div class="{{advantage}} skill-mod">
{{skillMod}}
</div>
{{/if}}
<div flex>
{{name}}
{{#if conditionalCount}}
*
{{/if}}
{{#if showPassive}}
({{characterCalculate "passiveSkill" ../_id skill}})
{{/if}}
</div>
</div>
</template>

View File

@@ -0,0 +1,41 @@
Template.printedSkillRow.helpers({
skillMod: function() {
return signedString(
Characters.calculate.skillMod(
Template.parentData()._id, this.skill
)
);
},
profIcon: function(){
var charId = Template.parentData()._id;
var prof = Characters.calculate.proficiency(charId, this.skill);
if (prof > 0 && prof < 1) return "image:brightness-2";
if (prof === 1) return "image:brightness-1";
if (prof > 1) return "av:album";
return "radio-button-unchecked";
},
failSkill: function(){
var charId = Template.parentData()._id;
return Effects.find({
charId: charId,
stat: this.skill,
enabled: true,
operation: "fail",
}).count();
},
advantage: function(){
var charId = Template.parentData()._id;
var advantage = Characters.calculate.advantage(charId, this.skill);
if (advantage > 0) return "advantage";
if (advantage < 0) return "disadvantage";
},
conditionalCount: function(){
var charId = Template.parentData()._id;
return Effects.find({
charId: charId,
stat: this.skill,
enabled: true,
operation: "conditional",
}).count();
},
});

View File

@@ -0,0 +1,7 @@
.printedSquareStat {
min-width: 67px;
}
.printedSquareStat .title.paper-font-subhead {
font-size: 2.5mm !important;
}

View File

@@ -0,0 +1,14 @@
<template name="printedSquareStat">
<div class="printedSquareStat layout vertical center {{class}}">
<div class="numbers paper-font-display1">
{{#if isSkill}}
{{prefix}}{{skillMod}}
{{else}}
{{prefix}}{{characterCalculate "attributeValue" ../_id stat}}
{{/if}}
</div>
<div class="paper-font-subhead title">
{{name}}
</div>
</div>
</template>

View File

@@ -0,0 +1,9 @@
Template.printedSquareStat.helpers({
skillMod: function() {
return signedString(
Characters.calculate.skillMod(
Template.parentData()._id, this.stat
)
);
},
});

View File

@@ -23,4 +23,17 @@
<meta name="msapplication-TileColor" content="#b91d1d">
<meta name="msapplication-TileImage" content="/mstile-144x144.png?v=lk6WXp6Pmj">
<meta name="theme-color" content="#d12929">
<style type="text/css" media="print">
@page {
margin: 0mm;
}
html {
margin: 0px;
}
* {
-webkit-transition: none !important;
transition: none !important;
}
</style>
</head>

View File

@@ -0,0 +1,20 @@
<template name="error">
<app-header-layout has-scrolling-region fullbleed>
<app-header class="app-grey white-text" fixed>
<app-toolbar>
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
</app-toolbar>
</app-header>
<div class="fit layout vertical center center-justified">
<div class="paper-font-subhead"
style="margin-left: 16px;
margin-right: 16px;
text-align: center;">
{{#if errorMessage}}
<div>{{errorMessage}}</div>
<paper-button class="try-again">Try Again</paper-button>
{{/if}}
</div>
</div>
</app-header-layout>
</template>

View File

@@ -0,0 +1,17 @@
Template.error.onRendered(function(){
const error = Session.get("error") || {};
if (error.href) window.history.replaceState("", "", error.href);
});
Template.error.helpers({
errorMessage: function(){
const error = Session.get("error") || {};
return error.reason;
},
});
Template.error.events({
"click .try-again": function(event, instance){
window.location.reload();
},
});

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,8 @@
"babel-runtime": "^6.23.0",
"bcrypt": "^1.0.3",
"bower": "^1.7.9",
"core-js": "^2.5.1"
"core-js": "^2.5.1",
"meteor-node-stubs": "^0.3.2",
"qrcode": "^1.2.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,8 @@
logRateError = function(reply, ruleInput){
// reply = {allowed, timeToReset, numInvocationsLeft}
// ruleInput = {userId, clientAddress, type, name, connectionId}
console.log(
`Limit hit for ${ruleInput.type} "${ruleInput.name}" ` +
`by user ${ruleInput.userId} from ${ruleInput.clientAddress}`
);
}

View File

@@ -38,9 +38,13 @@ Meteor.publish("singleCharacter", function(characterId){
DDPRateLimiter.addRule({
name: "singleCharacter",
type: "subscription",
userId(){ return true; },
userId: null,
connectionId(){ return true; },
}, 8, 5000);
}, 8, 10000, function(reply, ruleInput){
if(!reply.allowed){
logRateError(reply, ruleInput);
}
});
Meteor.publish("singleCharacterName", function(characterId){
userId = this.userId;