Compare commits

..

12 Commits
1.2.0 ... 1.2.6

Author SHA1 Message Date
Stefan Zermatten
8f30cee4d3 Hotfixed column layout with translateZ(0) hack 2017-06-07 09:46:38 +02:00
Stefan Zermatten
d7f7eb2e6a Fixed some spells in the library 2017-06-07 09:35:59 +02:00
Stefan Zermatten
a59cf1162f added return value to remove old soft removed docs 2017-05-29 11:58:19 +02:00
Stefan Zermatten
7bc80da99e Moved cron setup to startup 2017-05-29 11:35:30 +02:00
Stefan Zermatten
2ddc520bb6 Added cron job to remove old documents 2017-05-29 11:16:46 +02:00
Stefan Zermatten
d92bb0f4d4 Replace tap event with click event for mini-FABs
fixes #68, probably
2017-05-16 10:36:40 +02:00
Stefan Zermatten
abcfe57add Fixed results not being used when popping dialogs
Fixes #66
2017-05-11 11:08:00 +02:00
Stefan Zermatten
cdae75beef Merge branch 'bugfix-iamastick' 2017-05-10 16:40:05 +02:00
Stefan Zermatten
e7bcc2224c Replaced effects in-line editing with edit dialogs
Closes #41
2017-05-10 16:39:36 +02:00
Stefan Zermatten
b591c66dd5 Back button now closes dialogs
closes #9
2017-05-10 10:38:28 +02:00
Stefan Zermatten
402bc0e5ed Fixed Readme 2017-05-10 10:37:30 +02:00
Stefan Zermatten
ac772531ee Fixed SRD spells not getting a random ID when inserted
fixes #64
fixes #65
2017-05-08 11:21:30 +02:00
23 changed files with 409 additions and 97 deletions

View File

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

View File

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

View File

@@ -47,3 +47,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

@@ -4,6 +4,7 @@
column-gap: 0px;
column-width: 304px;
padding: 4px;
transform: translateZ(0);
}
.column-container.thin-columns {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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