Compare commits
38 Commits
2.0-beta.5
...
1.8.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
436c5bb785 | ||
|
|
8489ef5ec0 | ||
|
|
c9710bdb09 | ||
|
|
26784f11b6 | ||
|
|
23d43f7d43 | ||
|
|
1ebb0d2527 | ||
|
|
9d86cb8bee | ||
|
|
3343f8a813 | ||
|
|
0260824c2f | ||
|
|
66ee3ff808 | ||
|
|
3f32535666 | ||
|
|
4ea02c4fbb | ||
|
|
b052e8dd19 | ||
|
|
e2822b9f22 | ||
|
|
c46b836985 | ||
|
|
65d1bac0dc | ||
|
|
fcae3056de | ||
|
|
7d364c80c0 | ||
|
|
0ff6c08abd | ||
|
|
1c95336843 | ||
|
|
b36720511b | ||
|
|
261220fdd5 | ||
|
|
64edc52cca | ||
|
|
56f1bd2829 | ||
|
|
3b669fd2f9 | ||
|
|
933878e158 | ||
|
|
0e6ca56316 | ||
|
|
6599fe1ef8 | ||
|
|
f39baf43a1 | ||
|
|
96f4e35e25 | ||
|
|
e17dbf6601 | ||
|
|
3f81d419f7 | ||
|
|
1c00f5aa04 | ||
|
|
f5a32cb50a | ||
|
|
f4d3368fb4 | ||
|
|
1de9fb558a | ||
|
|
06ffc94b4c | ||
|
|
74c6a423ee |
62
README.md
62
README.md
@@ -1,13 +1,71 @@
|
||||
RPG Docs
|
||||
DiceCloud
|
||||
========
|
||||
|
||||
This is the repo for [DiceCloud](dicecloud.com).
|
||||
|
||||
DiceCloud is a free, auditable, real-time character sheet for D&D 5e.
|
||||
|
||||
Philosophy
|
||||
----------
|
||||
|
||||
Setting up your character on DiceCloud takes a little longer than
|
||||
just filling it in on a paper character sheet would. The goal of using an
|
||||
online sheet is to make actually playing the game more streamlined, and
|
||||
ultimately more fun. So putting a little extra effort into setting up a
|
||||
character now pays off over and over again once you're playing.
|
||||
|
||||
The idea is to track where each number comes from, and allow you to easily make
|
||||
changes on the fly. Let's look at a hypothetical example.
|
||||
|
||||
> You need to swim through a sunken section of dungeon to fetch the quest's Thing.
|
||||
> You'll need to take off your magical Plate Armor of +1 Constitution to swim
|
||||
> without sinking, of course.
|
||||
>
|
||||
> Taking it off will take away that disadvantage on
|
||||
> stealth checks, change your armor class, your speed and your constitution, and
|
||||
> which in turn changes your hit points and your constitution saving throw.
|
||||
> Working out all those changes in the middle of a game will drag the game to a
|
||||
> halt.
|
||||
>
|
||||
> Fortunately you have DiceCloud, so it's a matter of dragging
|
||||
> your Plate Armor +1 Con from your "equipment" box to your "backpack" box and
|
||||
> you're done. Your hitpoints change correctly, your saving throws are up to date,
|
||||
> your armor class goes back to reflecting the fact that you have natural armor
|
||||
> from being a dragonborn. Your character sheet keeps up and you
|
||||
> ultimately get more time to play the game. Huzzah!
|
||||
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
`git clone https://github.com/ThaumRystra/DiceCloud1 dicecloud`
|
||||
Running DiceCloud locally, either to host it yourself away from an internet
|
||||
connection, or to contribute to developing it further, is fairly
|
||||
straightforward and it should work on Linux, Windows, and Mac.
|
||||
|
||||
You'll need to have installed:
|
||||
|
||||
- [git](https://www.atlassian.com/git/tutorials/install-git)
|
||||
- [Meteor](https://www.meteor.com/install)
|
||||
- [Bower](https://bower.io/)
|
||||
|
||||
Then, it's just a matter of cloning this repository into a folder, installing the bower dependencies and running
|
||||
`meteor` in the app directory.
|
||||
|
||||
`git clone https://github.com/ThaumRystra/DiceCloud dicecloud`
|
||||
`cd dicecloud`
|
||||
`cd app`
|
||||
`bower install`
|
||||
`meteor`
|
||||
|
||||
You should see this:
|
||||
|
||||
```
|
||||
=> Started proxy.
|
||||
=> Started MongoDB.
|
||||
=> Started your app.
|
||||
|
||||
=> App running at: http://localhost:3000/
|
||||
```
|
||||
|
||||
Now, visiting http://localhost:3000/ should show you an empty instance of
|
||||
DiceCloud running.
|
||||
|
||||
|
||||
2
app/.gitignore
vendored
2
app/.gitignore
vendored
@@ -4,6 +4,8 @@
|
||||
settings.json
|
||||
public/components
|
||||
public/_imports.html
|
||||
private/oldClient
|
||||
nohup.out
|
||||
node_modules
|
||||
dump
|
||||
.cache
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# but you can also edit it by hand.
|
||||
|
||||
accounts-password@1.5.1
|
||||
accounts-ui@1.3.0
|
||||
accounts-ui@1.3.1
|
||||
random@1.1.0
|
||||
dburles:collection-helpers
|
||||
reactive-var@1.0.11
|
||||
@@ -17,7 +17,7 @@ dburles:mongo-collection-instances
|
||||
percolate:migrations
|
||||
ecwyne:mathjs
|
||||
useraccounts:polymer
|
||||
accounts-google@1.3.1
|
||||
accounts-google@1.3.2
|
||||
splendido:accounts-meld
|
||||
email@1.2.3
|
||||
meteorhacks:subs-manager
|
||||
@@ -25,9 +25,9 @@ chuangbo:marked
|
||||
reywood:iron-router-ga
|
||||
meteor-base@1.4.0
|
||||
mobile-experience@1.0.5
|
||||
mongo@1.5.0
|
||||
mongo@1.6.0
|
||||
blaze-html-templates
|
||||
session@1.1.7
|
||||
session@1.1.8
|
||||
jquery@1.11.10
|
||||
tracker@1.2.0
|
||||
logging@1.1.20
|
||||
@@ -37,20 +37,20 @@ spacebars
|
||||
check@1.3.1
|
||||
useraccounts:iron-routing
|
||||
wizonesolutions:canonical
|
||||
standard-minifier-js@2.3.4
|
||||
shell-server@0.3.1
|
||||
standard-minifier-js@2.4.0
|
||||
shell-server@0.4.0
|
||||
seba:minifiers-autoprefixer
|
||||
nikogosovd:multiple-uihooks
|
||||
templates:array
|
||||
ecmascript@0.11.1
|
||||
ecmascript@0.12.0
|
||||
es5-shim@4.8.0
|
||||
differential:vulcanize
|
||||
reactive-dict@1.2.0
|
||||
percolate:synced-cron
|
||||
reactive-dict@1.2.1
|
||||
ongoworks:speakingurl
|
||||
service-configuration@1.0.11
|
||||
google-config-ui@1.0.0
|
||||
dynamic-import@0.4.0
|
||||
google-config-ui@1.0.1
|
||||
dynamic-import@0.5.0
|
||||
ddp-rate-limiter@1.0.7
|
||||
rate-limit@1.0.9
|
||||
iron:router
|
||||
littledata:synced-cron
|
||||
|
||||
@@ -1 +1 @@
|
||||
METEOR@1.7.0.3
|
||||
METEOR@1.8
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
accounts-base@1.4.2
|
||||
accounts-google@1.3.1
|
||||
accounts-oauth@1.1.15
|
||||
accounts-base@1.4.3
|
||||
accounts-google@1.3.2
|
||||
accounts-oauth@1.1.16
|
||||
accounts-password@1.5.1
|
||||
accounts-ui@1.3.0
|
||||
accounts-ui@1.3.1
|
||||
accounts-ui-unstyled@1.4.1
|
||||
aldeed:collection2@2.10.0
|
||||
aldeed:collection2-core@1.2.0
|
||||
@@ -10,16 +10,16 @@ aldeed:schema-deny@1.1.0
|
||||
aldeed:schema-index@1.1.1
|
||||
aldeed:simple-schema@1.5.4
|
||||
allow-deny@1.1.0
|
||||
autoupdate@1.4.1
|
||||
babel-compiler@7.1.1
|
||||
babel-runtime@1.2.2
|
||||
autoupdate@1.5.0
|
||||
babel-compiler@7.2.0
|
||||
babel-runtime@1.3.0
|
||||
base64@1.0.11
|
||||
binary-heap@1.0.10
|
||||
blaze@2.3.2
|
||||
binary-heap@1.0.11
|
||||
blaze@2.3.3
|
||||
blaze-html-templates@1.1.2
|
||||
blaze-tools@1.0.10
|
||||
boilerplate-generator@1.5.0
|
||||
caching-compiler@1.1.12
|
||||
boilerplate-generator@1.6.0
|
||||
caching-compiler@1.2.0
|
||||
caching-html-compiler@1.1.3
|
||||
callback-hook@1.1.0
|
||||
check@1.3.1
|
||||
@@ -35,23 +35,25 @@ ddp-server@2.2.0
|
||||
deps@1.0.12
|
||||
diff-sequence@1.1.0
|
||||
differential:vulcanize@3.0.0
|
||||
dynamic-import@0.4.1
|
||||
ecmascript@0.11.1
|
||||
dynamic-import@0.5.0
|
||||
ecmascript@0.12.0
|
||||
ecmascript-runtime@0.7.0
|
||||
ecmascript-runtime-client@0.7.1
|
||||
ecmascript-runtime-server@0.7.0
|
||||
ecmascript-runtime-client@0.8.0
|
||||
ecmascript-runtime-server@0.7.1
|
||||
ecwyne:mathjs@0.25.0
|
||||
ejson@1.1.0
|
||||
email@1.2.3
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.0
|
||||
geojson-utils@1.0.10
|
||||
google-config-ui@1.0.0
|
||||
google-oauth@1.2.5
|
||||
google-config-ui@1.0.1
|
||||
google-oauth@1.2.6
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.0.11
|
||||
htmljs@1.0.11
|
||||
http@1.4.1
|
||||
id-map@1.1.0
|
||||
inter-process-messaging@0.1.0
|
||||
iron:controller@1.0.12
|
||||
iron:core@1.0.11
|
||||
iron:dynamic-template@1.0.12
|
||||
@@ -63,7 +65,8 @@ iron:url@1.1.0
|
||||
jquery@1.11.11
|
||||
lai:collection-extensions@0.2.1_1
|
||||
launch-screen@1.1.1
|
||||
less@2.7.12
|
||||
less@2.8.0
|
||||
littledata:synced-cron@1.5.1
|
||||
livedata@1.0.18
|
||||
localstorage@1.2.0
|
||||
logging@1.1.20
|
||||
@@ -72,51 +75,51 @@ mdg:validation-error@0.5.1
|
||||
meteor@1.9.2
|
||||
meteor-base@1.4.0
|
||||
meteorhacks:subs-manager@1.6.4
|
||||
minifier-css@1.3.1
|
||||
minifier-js@2.3.5
|
||||
minimongo@1.4.4
|
||||
minifier-css@1.4.0
|
||||
minifier-js@2.4.0
|
||||
minimongo@1.4.5
|
||||
mobile-experience@1.0.5
|
||||
mobile-status-bar@1.0.14
|
||||
modern-browsers@0.1.2
|
||||
modules@0.12.2
|
||||
modules@0.13.0
|
||||
modules-runtime@0.10.2
|
||||
momentjs:moment@2.22.2
|
||||
mongo@1.5.1
|
||||
mongo@1.6.0
|
||||
mongo-decimal@0.1.0
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.7
|
||||
nikogosovd:multiple-uihooks@0.1.8
|
||||
npm-bcrypt@0.9.3
|
||||
npm-mongo@3.0.11
|
||||
npm-mongo@3.1.1
|
||||
oauth@1.2.3
|
||||
oauth2@1.2.0
|
||||
oauth2@1.2.1
|
||||
observe-sequence@1.0.16
|
||||
ongoworks:speakingurl@9.0.0
|
||||
ordered-dict@1.1.0
|
||||
percolate:migrations@0.9.8
|
||||
percolate:synced-cron@1.3.2
|
||||
promise@0.11.1
|
||||
raix:eventemitter@0.1.3
|
||||
random@1.1.0
|
||||
rate-limit@1.0.9
|
||||
reactive-dict@1.2.0
|
||||
reactive-dict@1.2.1
|
||||
reactive-var@1.0.11
|
||||
reload@1.2.0
|
||||
retry@1.1.0
|
||||
reywood:iron-router-ga@0.7.1
|
||||
routepolicy@1.0.13
|
||||
seba:minifiers-autoprefixer@1.0.1
|
||||
routepolicy@1.1.0
|
||||
seba:minifiers-autoprefixer@1.1.1
|
||||
service-configuration@1.0.11
|
||||
session@1.1.7
|
||||
session@1.1.8
|
||||
sha@1.0.9
|
||||
shell-server@0.3.1
|
||||
shell-server@0.4.0
|
||||
socket-stream-client@0.2.2
|
||||
softwarerero:accounts-t9n@1.3.11
|
||||
spacebars@1.0.15
|
||||
spacebars-compiler@1.1.3
|
||||
splendido:accounts-emails-field@1.2.0
|
||||
splendido:accounts-meld@1.3.1
|
||||
srp@1.0.10
|
||||
standard-minifier-js@2.3.4
|
||||
srp@1.0.12
|
||||
standard-minifier-js@2.4.0
|
||||
templates:array@1.0.3
|
||||
templating@1.3.2
|
||||
templating-compiler@1.3.3
|
||||
@@ -129,7 +132,7 @@ url@1.2.0
|
||||
useraccounts:core@1.14.2
|
||||
useraccounts:iron-routing@1.14.2
|
||||
useraccounts:polymer@1.14.2
|
||||
webapp@1.6.2
|
||||
webapp@1.7.0
|
||||
webapp-hashing@1.0.9
|
||||
wizonesolutions:canonical@0.0.5
|
||||
zimme:collection-behaviours@1.1.3
|
||||
|
||||
@@ -65,3 +65,37 @@ makeParent(Buffs, ["name", "enabled"]); //parents of effects, attacks, proficien
|
||||
|
||||
Buffs.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Buffs.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
|
||||
Meteor.methods({
|
||||
applyBuff: function(buffId, targetId){
|
||||
if (!Meteor.call("canWriteCharacter", targetId)){
|
||||
throw new Meteor.Error(
|
||||
"Access denied",
|
||||
"You do not have permission to buff this character"
|
||||
);
|
||||
}
|
||||
let buff = CustomBuffs.findOne(buffId);
|
||||
if (!buff) return;
|
||||
|
||||
var parentCol = Meteor.isClient ?
|
||||
window[buff.parent.collection] : global[buff.parent.collection]
|
||||
var parent = parentCol.findOne(buff.parent.id);
|
||||
|
||||
//insert new buff
|
||||
newBuffId = Buffs.insert({
|
||||
charId: targetId,
|
||||
name: buff.name,
|
||||
description: buff.description,
|
||||
lifeTime: {total: buff.lifeTime.total},
|
||||
type: "custom",
|
||||
|
||||
appliedBy: buff.charId,
|
||||
appliedByDetails: {
|
||||
name: parent && parent.name || "",
|
||||
collection: buff.parent.collection,
|
||||
},
|
||||
});
|
||||
|
||||
Meteor.call("cloneChildren", buffId, {id: newBuffId, collection: "Buffs"})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -582,8 +582,8 @@ Characters.allow({
|
||||
});
|
||||
|
||||
Characters.deny({
|
||||
update: function(userId, docs, fields, modifier) {
|
||||
// can't change owners
|
||||
return _.contains(fields, "owner");
|
||||
update: function(userId, doc, fields, modifier) {
|
||||
// can't change owners unless you are the current owner
|
||||
return _.contains(fields, "owner") && doc.owner !== userId;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,14 +2,38 @@ Libraries = new Mongo.Collection("library");
|
||||
|
||||
Schemas.Library = new SimpleSchema({
|
||||
name: {type: String},
|
||||
owner: {type: String, regEx: SimpleSchema.RegEx.Id},
|
||||
readers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []},
|
||||
writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []},
|
||||
public: {type: Boolean, defaultValue: false},
|
||||
owner: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
readers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: [], index: 1},
|
||||
writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: [], index: 1},
|
||||
public: {type: Boolean, defaultValue: false, index: 1},
|
||||
});
|
||||
|
||||
Libraries.attachSchema(Schemas.Library);
|
||||
|
||||
Libraries.after.remove(function(userId, library) {
|
||||
LibraryItems.remove({library: library._id});
|
||||
LibrarySpells.remove({library: library._id});
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
removeLibrary: function(libraryId) {
|
||||
let library = Libraries.findOne(libraryId);
|
||||
let userId = Meteor.userId();
|
||||
|
||||
if (!library) return;
|
||||
if (library.owner === userId){
|
||||
Libraries.remove(libraryId);
|
||||
} else {
|
||||
if (_.contains(library.readers, userId)){
|
||||
Libraries.update(libraryId, {$pull: {"readers": userId}});
|
||||
}
|
||||
if (_.contains(library.writers, userId)){
|
||||
Libraries.update(libraryId, {$pull: {"writers": userId}});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Libraries.allow({
|
||||
insert(userId, doc) {
|
||||
return userId && doc.owner === userId;
|
||||
@@ -18,16 +42,14 @@ Libraries.allow({
|
||||
return canEdit(userId, doc);
|
||||
},
|
||||
remove(userId, doc) {
|
||||
return canEdit(userId, doc);
|
||||
return userId && doc.owner === userId;
|
||||
},
|
||||
fetch: ["owner", "writers"],
|
||||
});
|
||||
|
||||
Libraries.deny({
|
||||
// For now, only admins can manage libraries
|
||||
insert(userId, doc){
|
||||
var user = Meteor.users.findOne(userId);
|
||||
return !user || !_.contains(user.roles, "admin");
|
||||
return !Meteor.users.findOne(userId);
|
||||
},
|
||||
update(userId, doc, fields, modifier) {
|
||||
// Can't change owners
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Schemas.LibraryAttacks = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
defaultValue: "New Attack",
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
details: {
|
||||
|
||||
@@ -9,7 +9,6 @@ Schemas.LibraryEffects = new SimpleSchema({
|
||||
defaultValue: "add",
|
||||
allowedValues: [
|
||||
"base",
|
||||
"proficiency",
|
||||
"add",
|
||||
"mul",
|
||||
"min",
|
||||
|
||||
@@ -42,3 +42,50 @@ LibraryItems.allow({
|
||||
},
|
||||
fetch: ["library"],
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
updateLibraryItemEffect: function({itemId, effectIndex, field, value, unsetField}){
|
||||
let libraryId = LibraryItems.findOne(itemId).library;
|
||||
let userId = Meteor.userId();
|
||||
if (!Libraries.canEdit(userId, libraryId)) return;
|
||||
let modifier = {
|
||||
$set: {
|
||||
[`effects.${effectIndex}.${field}`]: value,
|
||||
}
|
||||
};
|
||||
if (unsetField){
|
||||
modifier.$unset = {
|
||||
[`effects.${effectIndex}.${unsetField}`]: 1,
|
||||
}
|
||||
}
|
||||
LibraryItems.update(itemId, modifier);
|
||||
},
|
||||
removeLibraryItemEffect: function({itemId, effectIndex}){
|
||||
let libraryId = LibraryItems.findOne(itemId).library;
|
||||
let userId = Meteor.userId();
|
||||
if (!Libraries.canEdit(userId, libraryId)) return;
|
||||
LibraryItems.update(itemId, {$unset : {
|
||||
[`effects.${effectIndex}`] : 1,
|
||||
}});
|
||||
LibraryItems.update(itemId, {$pull : {"effects" : null}});
|
||||
},
|
||||
updateLibraryItemAttack: function({itemId, attackIndex, field, value}){
|
||||
let libraryId = LibraryItems.findOne(itemId).library;
|
||||
let userId = Meteor.userId();
|
||||
if (!Libraries.canEdit(userId, libraryId)) return;
|
||||
LibraryItems.update(itemId, {
|
||||
$set: {
|
||||
[`attacks.${attackIndex}.${field}`]: value,
|
||||
}
|
||||
});
|
||||
},
|
||||
removeLibraryItemAttack: function({itemId, attackIndex}){
|
||||
let libraryId = LibraryItems.findOne(itemId).library;
|
||||
let userId = Meteor.userId();
|
||||
if (!Libraries.canEdit(userId, libraryId)) return;
|
||||
LibraryItems.update(itemId, {$unset : {
|
||||
[`attacks.${attackIndex}`] : 1,
|
||||
}});
|
||||
LibraryItems.update(itemId, {$pull : {"attacks" : null}});
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,10 @@ Schemas.UserProfile = new SimpleSchema({
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
librarySubscriptions: {
|
||||
type: [String],
|
||||
defaultValue: [],
|
||||
},
|
||||
});
|
||||
|
||||
Schemas.User = new SimpleSchema({
|
||||
|
||||
@@ -121,7 +121,7 @@ Router.map(function() {
|
||||
this.route("library", {
|
||||
path: "/library",
|
||||
waitOn: function(){
|
||||
return subsManager.subscribe("standardLibraries");
|
||||
return subsManager.subscribe("customLibraries");
|
||||
},
|
||||
onAfterAction: function() {
|
||||
document.title = appName + " - Library";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "RPG Docs",
|
||||
"version": "0.0.0",
|
||||
"name": "dicecloud",
|
||||
"version": "1",
|
||||
"homepage": "",
|
||||
"authors": [
|
||||
"Stefan Zermatten"
|
||||
|
||||
5
app/client/lib/fixPasswordButton.js
Normal file
5
app/client/lib/fixPasswordButton.js
Normal file
@@ -0,0 +1,5 @@
|
||||
let pwdFormSubmit = AccountsTemplates.atPwdFormEvents["submit #at-pwd-form"]
|
||||
|
||||
Template.atPwdForm.events({
|
||||
"click .at-btn.submit": pwdFormSubmit,
|
||||
});
|
||||
8
app/client/style/iosButtonFix.css
Normal file
8
app/client/style/iosButtonFix.css
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* iOS doens't believe in click events for some elements.
|
||||
* This is here to convince it to allow buttons to be clickable
|
||||
*/
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1,47 +1,5 @@
|
||||
const applyBuff = function(targetId, buff) {
|
||||
var parent = global[buff.parent.collection].findOne(buff.parent.id);
|
||||
|
||||
//insert new buff
|
||||
newBuffId = Buffs.insert({
|
||||
charId: targetId,
|
||||
name: buff.name,
|
||||
description: buff.description,
|
||||
lifeTime: {total: buff.lifeTime.total},
|
||||
type: "custom",
|
||||
|
||||
appliedBy: buff.charId,
|
||||
appliedByDetails: {
|
||||
name: parent.name,
|
||||
collection: buff.parent.collection,
|
||||
},
|
||||
});
|
||||
|
||||
//insert children
|
||||
Attacks.find({"parent.id": buff._id}).forEach(function(doc){
|
||||
temp = _.clone(doc);
|
||||
temp.parent.id = newBuffId;
|
||||
temp.parent.collection = "Buffs";
|
||||
delete temp._id;
|
||||
|
||||
Attacks.insert(temp);
|
||||
});
|
||||
Effects.find({"parent.id": buff._id}).forEach(function(doc){
|
||||
temp = _.clone(doc);
|
||||
temp.parent.id = newBuffId;
|
||||
temp.parent.collection = "Buffs";
|
||||
delete temp._id;
|
||||
|
||||
Effects.insert(temp);
|
||||
});
|
||||
Proficiencies.find({"parent.id": buff._id}).forEach(function(doc){
|
||||
temp = _.clone(doc);
|
||||
temp.parent.id = newBuffId;
|
||||
temp.parent.collection = "Buffs";
|
||||
delete temp._id;
|
||||
|
||||
Proficiencies.insert(temp);
|
||||
});
|
||||
|
||||
Meteor.call("applyBuff", buff._id, targetId)
|
||||
let target;
|
||||
if (targetId == buff.charId) {
|
||||
target = "self";
|
||||
|
||||
@@ -17,6 +17,7 @@ Template.characterSettings.events({
|
||||
"change #hideSpellcasting": function(event, instance){
|
||||
var value = instance.find("#hideSpellcasting").checked;
|
||||
if (this.settings.hideSpellcasting !== value){
|
||||
Session.set(this._id + ".selectedTab", "0");
|
||||
Characters.update(
|
||||
this._id,
|
||||
{$set: {"settings.hideSpellcasting": value}}
|
||||
|
||||
@@ -53,7 +53,7 @@ Template.shareDialog.events({
|
||||
Characters.update(this._id, {$set: {"settings.viewPermission": value}});
|
||||
},
|
||||
"input #userNameOrEmailInput":
|
||||
function(event, instance){
|
||||
_.debounce(function(event, instance){
|
||||
var userName = instance.find("#userNameOrEmailInput").value;
|
||||
instance.userId.set(undefined);
|
||||
Meteor.call("getUserId", userName, function(err, result) {
|
||||
@@ -64,7 +64,7 @@ Template.shareDialog.events({
|
||||
instance.userId.set(result);
|
||||
}
|
||||
});
|
||||
},
|
||||
}, 300),
|
||||
"click #shareButton": function(event, instance){
|
||||
var self = this;
|
||||
var permission = instance.find("#accessLevelMenu").selected;
|
||||
|
||||
@@ -31,9 +31,17 @@
|
||||
Settings
|
||||
</paper-icon-item>
|
||||
<paper-icon-item id="characterExport">
|
||||
<iron-icon icon="content-copy" item-icon></iron-icon>
|
||||
<iron-icon icon="exit-to-app" item-icon></iron-icon>
|
||||
Export to Improved Initiative
|
||||
</paper-icon-item>
|
||||
<paper-icon-item id="characterCopy">
|
||||
<iron-icon icon="content-copy" item-icon></iron-icon>
|
||||
Make a copy
|
||||
</paper-icon-item>
|
||||
<paper-icon-item id="characterDump">
|
||||
<iron-icon icon="file-download" item-icon></iron-icon>
|
||||
Download a backup
|
||||
</paper-icon-item>
|
||||
</paper-menu>
|
||||
</paper-menu-button>
|
||||
{{else}}
|
||||
|
||||
@@ -234,6 +234,18 @@ Template.characterSheet.events({
|
||||
element: event.currentTarget.parentElement.parentElement,
|
||||
});
|
||||
},
|
||||
"click #characterCopy": function(event, instance){
|
||||
Meteor.call("copyCharacter", this._id, (error, char) => {
|
||||
if (error){
|
||||
console.error(error);
|
||||
} else {
|
||||
Router.go(`/character/${char._id}/${char.urlName || "-"}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
"click #characterDump": function(event, instance){
|
||||
saveCharacterDump(this._id);
|
||||
},
|
||||
"click #unshareCharacter": function(event, instance){
|
||||
pushDialogStack({
|
||||
data: this,
|
||||
|
||||
@@ -48,6 +48,25 @@
|
||||
{{/unless}}
|
||||
</iron-collapse>
|
||||
{{/each}}
|
||||
{{#each customLibraries}}
|
||||
<div class="paper-font-body2 category-header clickable">
|
||||
<iron-icon icon="chevron-right" class="{{#if isOpen _id}}open{{/if}}">
|
||||
</iron-icon>
|
||||
{{name}}
|
||||
</div>
|
||||
<iron-collapse opened={{isOpen _id}}>
|
||||
<table style="width: 100%">
|
||||
<tbody>
|
||||
{{#each item in (itemsInLibrary _id)}}
|
||||
{{>libraryItem item=item selected=(isSelected item)}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{#unless ready _id}}
|
||||
<paper-spinner active></paper-spinner>
|
||||
{{/unless}}
|
||||
</iron-collapse>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,13 @@ const categories = [
|
||||
{name: "Tools", key: "tools"},
|
||||
];
|
||||
|
||||
const categoryKeys = [
|
||||
"weapons",
|
||||
"armor",
|
||||
"adventuringGear",
|
||||
"tools",
|
||||
];
|
||||
|
||||
Template.itemLibraryDialog.onCreated(function(){
|
||||
this.selectedItem = new ReactiveVar();
|
||||
this.searchTerm = new ReactiveVar();
|
||||
@@ -14,10 +21,17 @@ Template.itemLibraryDialog.onCreated(function(){
|
||||
this.readyDict = new ReactiveDict();
|
||||
this.searchReady = new ReactiveVar();
|
||||
librarySubs.subscribe("standardLibraries");
|
||||
librarySubs.subscribe("customLibraries");
|
||||
|
||||
this.autorun(() => {
|
||||
// Subscribe to all open categories
|
||||
_.each(this.categoriesOpen.get(), (key) => {
|
||||
var handle = librarySubs.subscribe("standardLibraryItems", key);
|
||||
let handle;
|
||||
if (_.contains(categoryKeys, key)){
|
||||
handle = librarySubs.subscribe("standardLibraryItems", key);
|
||||
} else {
|
||||
handle = librarySubs.subscribe("libraryItems", key);
|
||||
}
|
||||
this.autorun(() => {
|
||||
this.readyDict.set(key, handle.ready());
|
||||
});
|
||||
@@ -70,12 +84,29 @@ Template.itemLibraryDialog.helpers({
|
||||
const searchTerm = Template.instance().searchTerm.get();
|
||||
if (!searchTerm) return;
|
||||
return LibraryItems.find({
|
||||
library: "SRDLibraryGA3XWsd",
|
||||
name: {
|
||||
$regex: new RegExp(".*" + searchTerm + ".*", "gi")
|
||||
},
|
||||
});
|
||||
},
|
||||
customLibraries(){
|
||||
let userId = Meteor.userId();
|
||||
return Libraries.find({
|
||||
$or: [
|
||||
{readers: userId},
|
||||
{writers: userId},
|
||||
{owner: userId},
|
||||
],
|
||||
});
|
||||
},
|
||||
itemsInLibrary(libraryId){
|
||||
return LibraryItems.find({
|
||||
library: libraryId,
|
||||
}, {
|
||||
sort: {name: 1},
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
Template.itemLibraryDialog.events({
|
||||
@@ -93,7 +124,7 @@ Template.itemLibraryDialog.events({
|
||||
},
|
||||
"click .category-header": function(event, template){
|
||||
let cats = template.categoriesOpen.get();
|
||||
const key = this.key;
|
||||
const key = this.key || this._id;
|
||||
// Toggle whether this key is in the array or not
|
||||
if (_.contains(cats, key)){
|
||||
cats = _.without(cats, key);
|
||||
|
||||
@@ -11,16 +11,20 @@
|
||||
<div class="bottom list">
|
||||
{{#each levels}}{{#if showSlots ..}}
|
||||
<div class="item-slot">
|
||||
<div class="item spellSlot layout horizontal center">
|
||||
<div style="margin-right: 16px">
|
||||
<div class="white spellSlot layout horizontal center ">
|
||||
<div class="spellLevelName" style="margin-right: 16px; margin-left: 8px; cursor: pointer;">
|
||||
{{name}}
|
||||
</div>
|
||||
<div class="flex layout horizontal center">
|
||||
<div class="flex layout horizontal center wrap">
|
||||
{{#each slotBubbles ..}}
|
||||
<paper-icon-button class="slotBubble"
|
||||
icon={{icon}}
|
||||
disabled={{disabled}}>
|
||||
</paper-icon-button>
|
||||
{{#unless overflow}}
|
||||
<paper-icon-button class="slotBubble"
|
||||
icon={{icon}}
|
||||
disabled={{disabled}}>
|
||||
</paper-icon-button>
|
||||
{{else}}
|
||||
<div class="paper-font-subhead">+{{overflow}}</div>
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -122,6 +122,7 @@ Template.spells.helpers({
|
||||
return false;
|
||||
},
|
||||
slotBubbles: function(char){
|
||||
const MAX_SLOTS = 10;
|
||||
var baseSlots = Characters.calculate.attributeBase(
|
||||
char._id, "level" + this.level + "SpellSlots"
|
||||
);
|
||||
@@ -130,16 +131,28 @@ Template.spells.helpers({
|
||||
);
|
||||
var slotsUsed = baseSlots - currentSlots;
|
||||
var bubbles = [];
|
||||
var i;
|
||||
for (i = 0; i < currentSlots; i++){
|
||||
var i, overflowFilled, overflowEmpty;
|
||||
var filledSlots = currentSlots;
|
||||
var maxEmptySlots = Math.max(MAX_SLOTS - filledSlots + 1, 1);
|
||||
var emptySlots = slotsUsed;
|
||||
if (baseSlots > MAX_SLOTS){
|
||||
filledSlots = Math.min(MAX_SLOTS, filledSlots);
|
||||
overflowFilled = Math.max(currentSlots - MAX_SLOTS, 0);
|
||||
emptySlots = Math.min(maxEmptySlots, emptySlots);
|
||||
overflowEmpty = Math.max(slotsUsed - maxEmptySlots, 0);
|
||||
}
|
||||
for (i = 0; i < filledSlots; i++){
|
||||
bubbles.push({
|
||||
icon: "radio-button-checked",
|
||||
disabled: i !== currentSlots - 1 || !canEditCharacter(char._id), //last full slot not disabled
|
||||
disabled: i !== filledSlots - 1 || !canEditCharacter(char._id), //last full slot not disabled
|
||||
attribute: "level" + this.level + "SpellSlots",
|
||||
charId: char._id,
|
||||
});
|
||||
}
|
||||
for (i = 0; i < slotsUsed; i++){
|
||||
if (overflowFilled){
|
||||
bubbles.push({overflow: overflowFilled});
|
||||
}
|
||||
for (i = 0; i < emptySlots; i++){
|
||||
bubbles.push({
|
||||
icon: "radio-button-unchecked",
|
||||
disabled: i !== 0 || !canEditCharacter(char._id), //first empty slot not disabled
|
||||
@@ -147,6 +160,9 @@ Template.spells.helpers({
|
||||
charId: char._id,
|
||||
});
|
||||
}
|
||||
if (overflowEmpty){
|
||||
bubbles.push({overflow: overflowEmpty});
|
||||
}
|
||||
return bubbles;
|
||||
},
|
||||
slotStatName: function() {
|
||||
@@ -178,7 +194,7 @@ Template.spells.events({
|
||||
}
|
||||
event.stopPropagation();
|
||||
},
|
||||
"click .spellSlot": function(event, instance) {
|
||||
"click .spellLevelName": function(event, instance) {
|
||||
var name = "Level " + this.level + " Spell Slots";
|
||||
var stat = "level" + this.level + "SpellSlots";
|
||||
var charId = instance.data._id;
|
||||
|
||||
@@ -58,6 +58,13 @@
|
||||
</paper-fab>
|
||||
{{#simpleTooltip class="always"}} New Character {{/simpleTooltip}}
|
||||
</div>
|
||||
<div>
|
||||
<paper-fab icon="file-upload"
|
||||
class="restoreCharacter"
|
||||
mini>
|
||||
</paper-fab>
|
||||
{{#simpleTooltip class="always"}} Restore from backup {{/simpleTooltip}}
|
||||
</div>
|
||||
{{/fabMenu}}
|
||||
</div>
|
||||
</app-header-layout>
|
||||
|
||||
@@ -81,4 +81,13 @@ Template.characterList.events({
|
||||
returnElement: instance.find(`.party[data-id='${partyId}']`),
|
||||
});
|
||||
},
|
||||
"click .restoreCharacter": function(event, instance) {
|
||||
pushDialogStack({
|
||||
template: "characterRestoreDialog",
|
||||
element: event.currentTarget,
|
||||
callback(dump){
|
||||
return;
|
||||
},
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<template name="characterRestoreDialog">
|
||||
<div class="fit layout vertical">
|
||||
<app-header-layout has-scrolling-region class="new-character-dialog flex">
|
||||
<app-header fixed effects="waterfall">
|
||||
<app-toolbar>
|
||||
<div main-title>Restore Character</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="form">
|
||||
<p>
|
||||
Restore a character from a backup file, this will create a new copy of
|
||||
the restored character
|
||||
</p>
|
||||
<paper-input class="fileInput" type="file" label="File"></paper-input><br>
|
||||
{{#if error}}
|
||||
<p style="color: red;">
|
||||
{{error}}
|
||||
</p>
|
||||
{{/if}}
|
||||
{{#if loading}}
|
||||
<paper-spinner active></paper-spinner>
|
||||
{{/if}}
|
||||
</div>
|
||||
</app-header-layout>
|
||||
<div class="buttons layout horizontal end-justified">
|
||||
<paper-button class="cancelButton">
|
||||
Cancel
|
||||
</paper-button>
|
||||
<paper-button class="addButton" disabled={{invalid}}>
|
||||
Restore
|
||||
</paper-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
Template.characterRestoreDialog.onCreated(function(){
|
||||
this.dump = {};
|
||||
this.valid = new ReactiveVar(false);
|
||||
this.error = new ReactiveVar(null);
|
||||
this.loading = new ReactiveVar(false);
|
||||
});
|
||||
|
||||
Template.characterRestoreDialog.helpers({
|
||||
invalid(){
|
||||
return !Template.instance().valid.get();
|
||||
},
|
||||
error(){
|
||||
return Template.instance().error.get();
|
||||
},
|
||||
loading(){
|
||||
return Template.instance().loading.get();
|
||||
},
|
||||
});
|
||||
|
||||
const fail = function(instance){
|
||||
instance.valid.set(false);
|
||||
instance.error.set("Failed to convert file into a valid character");
|
||||
instance.dump = undefined;
|
||||
};
|
||||
|
||||
Template.characterRestoreDialog.events({
|
||||
"input .fileInput": function(event, instance){
|
||||
let input = event.currentTarget.$.input;
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(){
|
||||
let dumpString = reader.result;
|
||||
try {
|
||||
let dump = JSON.parse(dumpString);
|
||||
if (dump && dump.character && dump.collections){
|
||||
instance.valid.set(true);
|
||||
instance.error.set(null);
|
||||
instance.dump = dump;
|
||||
} else {
|
||||
fail(instance);
|
||||
}
|
||||
} catch (e) {
|
||||
fail(instance);
|
||||
}
|
||||
};
|
||||
reader.readAsText(input.files[0]);
|
||||
},
|
||||
"click .cancelButton": function(event, instance){
|
||||
popDialogStack();
|
||||
},
|
||||
"click .addButton": function(event, instance){
|
||||
let dump = instance.dump;
|
||||
if (!dump) return;
|
||||
Meteor.call('restoreCharacter', dump, (e, char) => {
|
||||
instance.loading.set(false);
|
||||
if (!char){
|
||||
instance.error.set(e.message)
|
||||
} else {
|
||||
popDialogStack();
|
||||
Router.go("characterSheet", {
|
||||
_id: char._id,
|
||||
urlName: char.urlName || '-',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,29 @@
|
||||
<template name="itemLibrary">
|
||||
{{#each items}}
|
||||
{{> libraryItem}}
|
||||
{{#each libraries}}
|
||||
<div class="paper-font-subhead library-header layout horizontal center" data-id={{_id}} style="height: 40px;">
|
||||
<iron-icon icon="chevron-right" class="{{#if isOpen _id}}open{{/if}}">
|
||||
</iron-icon>
|
||||
<div class="flex">{{name}}</div>
|
||||
{{#if isOpen _id}}
|
||||
<div class="relative">
|
||||
<paper-icon-button icon="create" class="editLibrary"></paper-icon-button>
|
||||
{{#simpleTooltip}}Edit Library{{/simpleTooltip}}
|
||||
</div>
|
||||
<div class="relative">
|
||||
<paper-icon-button icon="add" class="addItem"></paper-icon-button>
|
||||
{{#simpleTooltip}}Add Item{{/simpleTooltip}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<iron-collapse opened={{isOpen _id}}>
|
||||
{{#each libraryItems}}
|
||||
<paper-item class="short item-name" data-id={{_id}}>
|
||||
{{name}}
|
||||
</paper-item>
|
||||
{{/each}}
|
||||
{{#unless ready _id}}
|
||||
<paper-spinner active></paper-spinner>
|
||||
{{/unless}}
|
||||
</iron-collapse>
|
||||
{{/each}}
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,94 @@
|
||||
const librarySubs = new SubsManager();
|
||||
|
||||
Template.itemLibrary.onCreated(function(){
|
||||
this.selectedTab = new ReactiveVar("0");
|
||||
this.librariesOpen = new ReactiveVar([]);
|
||||
this.readyDict = new ReactiveDict();
|
||||
this.autorun(() => {
|
||||
// Subscribe to all open libraries
|
||||
_.each(this.librariesOpen.get(), (libraryId) => {
|
||||
var handle = librarySubs.subscribe("libraryItems", libraryId);
|
||||
this.autorun(() => {
|
||||
this.readyDict.set(libraryId, handle.ready());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Template.itemLibrary.helpers({
|
||||
items(){
|
||||
return Items.find({charId: {$in: [
|
||||
"SRDLibrary",
|
||||
]}});
|
||||
selectedTab(){
|
||||
return Template.instance().selectedTab.get();
|
||||
},
|
||||
libraries(){
|
||||
let userId = Meteor.userId();
|
||||
return Libraries.find({
|
||||
$or: [
|
||||
{readers: userId},
|
||||
{writers: userId},
|
||||
{owner: userId},
|
||||
],
|
||||
});
|
||||
},
|
||||
libraryItems(){
|
||||
return LibraryItems.find({
|
||||
library: this._id
|
||||
},{
|
||||
sort: {name: 1}
|
||||
});
|
||||
},
|
||||
ready(libraryId){
|
||||
return Template.instance().readyDict.get(libraryId);
|
||||
},
|
||||
isOpen(libraryId){
|
||||
const librariesOpen = Template.instance().librariesOpen.get();
|
||||
return _.contains(librariesOpen, libraryId);
|
||||
},
|
||||
});
|
||||
|
||||
Template.itemLibrary.events({
|
||||
"click .library-header": function(event, template){
|
||||
let libs = template.librariesOpen.get();
|
||||
const libraryId = this._id;
|
||||
// Toggle whether this key is in the array or not
|
||||
if (_.contains(libs, libraryId)){
|
||||
libs = _.without(libs, libraryId);
|
||||
} else {
|
||||
libs.push(libraryId);
|
||||
}
|
||||
template.librariesOpen.set(libs);
|
||||
},
|
||||
"click .editLibrary": function(event, instance){
|
||||
event.stopPropagation();
|
||||
var libraryId = this._id;
|
||||
pushDialogStack({
|
||||
template: "libraryDialog",
|
||||
data: {libraryId},
|
||||
element: event.currentTarget.parentElement.parentElement,
|
||||
returnElement: () => instance.find(`.library-header[data-id='${libraryId}']`),
|
||||
});
|
||||
},
|
||||
"click .addItem": function(event, instance){
|
||||
event.stopPropagation();
|
||||
var libraryId = this._id;
|
||||
var itemId = LibraryItems.insert({
|
||||
name: "New Library Item",
|
||||
library: libraryId,
|
||||
});
|
||||
pushDialogStack({
|
||||
template: "libraryItemDialog",
|
||||
data: {itemId},
|
||||
element: event.currentTarget,
|
||||
returnElement: () => instance.find(`.item-name[data-id='${itemId}']`),
|
||||
});
|
||||
},
|
||||
"click .item-name": function(event, instance){
|
||||
event.stopPropagation();
|
||||
var itemId = this._id;
|
||||
pushDialogStack({
|
||||
template: "libraryItemDialog",
|
||||
data: {itemId},
|
||||
element: event.currentTarget,
|
||||
returnElement: () => instance.find(`.item-name[data-id='${itemId}']`),
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<template name="libraryItemDialog">
|
||||
<div class="fit base-dialog layout vertical">
|
||||
<app-toolbar>
|
||||
<paper-icon-button id="backButton"
|
||||
icon="arrow-back">
|
||||
</paper-icon-button>
|
||||
<div main-title>{{item.name}}</div>
|
||||
<paper-icon-button id="deleteButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
icon="delete">
|
||||
</paper-icon-button>
|
||||
</app-toolbar>
|
||||
<div class="form flex scroll-y" style="position: relative;">
|
||||
<paper-input id="libraryItemLibraryNameInput" class="fullwidth" label="Library name (optional)" value={{item.libraryName}}></paper-input>
|
||||
<paper-input id="libraryItemNameInput" class="fullwidth" label="Item name" value={{item.name}}></paper-input>
|
||||
<div class="layout horizontal center wrap">
|
||||
<paper-input id="libraryItemPluralInput" class="flex" label="Plural name" value={{item.plural}}></paper-input>
|
||||
<paper-input id="libraryItemQuantityInput" class="flex" label="Quantity" type="number" value={{item.quantity}}></paper-input>
|
||||
<paper-checkbox id="incrementCheckbox" class="flex" checked={{item.settings.showIncrement}}>
|
||||
Show Increment
|
||||
</paper-checkbox>
|
||||
</div>
|
||||
<div class="layout horizontal center wrap">
|
||||
<paper-input id="libraryItemValueInput" class="flex" label="Value" type="number" value={{item.value}}></paper-input>
|
||||
<paper-input id="libraryItemWeightInput" class="flex" label="Weight" type="number" value={{item.weight}}></paper-input>
|
||||
<paper-checkbox id="attunementCheckbox" class="flex" checked={{item.requiresAttunement}}>
|
||||
Requires Attunement
|
||||
</paper-checkbox>
|
||||
</div>
|
||||
<paper-textarea id="libraryItemDescriptionInput" label="Description" value={{item.description}}></paper-textarea>
|
||||
<div style="margin-top: 8px;">
|
||||
<div class="paper-font-subhead">Effects</div>
|
||||
{{#each indexedEffects}}
|
||||
<div class="effect layout horizontal center wrap">
|
||||
<paper-dropdown-menu label="Operation" class="operationMenu">
|
||||
<paper-listbox class="dropdown-content" selected={{operationIndex operation}}>
|
||||
<paper-item label="Base Value" name="base"> Base Value </paper-item>
|
||||
<paper-item label="Add" name="add"> Add </paper-item>
|
||||
<paper-item label="Multiply" name="mul"> Multiply </paper-item>
|
||||
<paper-item label="Min" name="min"> Min </paper-item>
|
||||
<paper-item label="Max" name="max"> Max </paper-item>
|
||||
<paper-item label="Advantage" name="advantage"> Advantage </paper-item>
|
||||
<paper-item label="Disadvantage" name="disadvantage"> Disadvantage </paper-item>
|
||||
<paper-item label="PassiveAdd" name="passiveAdd"> PassiveAdd </paper-item>
|
||||
<paper-item label="Fail" name="fail"> Fail </paper-item>
|
||||
<paper-item label="Conditional" name="conditional"> Conditional </paper-item>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
<paper-input class="LibraryItemEffectStat flex" label="Attribute" value={{stat}}></paper-input>
|
||||
<paper-input class="LibraryItemEffectValue flex" label="Value" value={{calculationOrValue}}></paper-input>
|
||||
<paper-icon-button icon="delete" class="deleteEffect"></paper-icon-button>
|
||||
</div>
|
||||
{{/each}}
|
||||
<paper-button id="addEffect" class="red-button">Add Effect</paper-button>
|
||||
</div>
|
||||
<div style="margin-top: 8px;">
|
||||
<div class="paper-font-subhead">Attacks</div>
|
||||
{{#each indexedAttacks}}
|
||||
<div class="effect layout horizontal center wrap">
|
||||
<paper-input class="LibraryItemAttackBonusInput flex" label="Attack Bonus" value={{attackBonus}}></paper-input>
|
||||
<paper-input class="LibraryItemAttackDamageInput flex" label="Damage" value={{damage}}></paper-input>
|
||||
<paper-input class="LibraryItemAttackDetailsInput flex" label="Details" value={{details}}></paper-input>
|
||||
<paper-dropdown-menu label="Damage Type" class="damageTypeMenu">
|
||||
<paper-listbox class="dropdown-content" selected={{damageTypeIndex damageType}}>
|
||||
<paper-item label="Bludgeoning" name="bludgeoning"> Bludgeoning </paper-item>
|
||||
<paper-item label="Piercing" name="piercing"> Piercing </paper-item>
|
||||
<paper-item label="Slashing" name="slashing"> Slashing </paper-item>
|
||||
<paper-item label="Acid" name="acid"> Acid </paper-item>
|
||||
<paper-item label="Cold" name="cold"> Cold </paper-item>
|
||||
<paper-item label="Fire" name="fire"> Fire </paper-item>
|
||||
<paper-item label="Force" name="force"> Force </paper-item>
|
||||
<paper-item label="Lightning" name="lightning"> Lightning </paper-item>
|
||||
<paper-item label="Necrotic" name="necrotic"> Necrotic </paper-item>
|
||||
<paper-item label="Poison" name="poison"> Poison </paper-item>
|
||||
<paper-item label="Psychic" name="psychic"> Psychic </paper-item>
|
||||
<paper-item label="Radiant" name="radiant"> Radiant </paper-item>
|
||||
<paper-item label="Thunder" name="thunder"> Thunder </paper-item>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
<paper-icon-button icon="delete" class="deleteAttack"></paper-icon-button>
|
||||
</div>
|
||||
{{/each}}
|
||||
<paper-button id="addAttack" class="red-button">Add Attack</paper-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,204 @@
|
||||
Template.libraryItemDialog.helpers({
|
||||
item(){
|
||||
return LibraryItems.findOne(this.itemId);
|
||||
},
|
||||
calculationOrValue(){
|
||||
return this.calculation || this.value;
|
||||
},
|
||||
indexedEffects(){
|
||||
let item = LibraryItems.findOne(this.itemId);
|
||||
if (!item) return;
|
||||
return _.map(item.effects, (effect, index) => {
|
||||
if (!effect) return;
|
||||
effect.index = index;
|
||||
return effect;
|
||||
});
|
||||
},
|
||||
indexedAttacks(){
|
||||
let item = LibraryItems.findOne(this.itemId);
|
||||
if (!item) return;
|
||||
return _.map(item.attacks, (attack, index) => {
|
||||
if (!attack) return;
|
||||
attack.index = index;
|
||||
return attack;
|
||||
});
|
||||
},
|
||||
operationIndex(operation){
|
||||
const ref = {
|
||||
base: 0,
|
||||
add: 1,
|
||||
mul: 2,
|
||||
min: 3,
|
||||
max: 4,
|
||||
advantage: 5,
|
||||
disadvantage: 6,
|
||||
passiveAdd: 7,
|
||||
fail: 8,
|
||||
conditional: 9,
|
||||
};
|
||||
return ref[operation];
|
||||
},
|
||||
damageTypeIndex(damageType){
|
||||
const ref = {
|
||||
bludgeoning: 0,
|
||||
piercing: 1,
|
||||
slashing: 2,
|
||||
acid: 3,
|
||||
cold: 4,
|
||||
fire: 5,
|
||||
force: 6,
|
||||
lightning: 7,
|
||||
necrotic: 8,
|
||||
poison: 9,
|
||||
psychic: 10,
|
||||
radiant: 11,
|
||||
thunder: 12,
|
||||
};
|
||||
return ref[damageType];
|
||||
}
|
||||
});
|
||||
|
||||
const bind = function(field){
|
||||
return _.debounce(function(event){
|
||||
const input = event.currentTarget;
|
||||
var value = input.value;
|
||||
LibraryItems.update(this.itemId, {
|
||||
$set: {[field]: value}
|
||||
}, {
|
||||
removeEmptyStrings: false,
|
||||
trimStrings: false,
|
||||
});
|
||||
}, 300);
|
||||
};
|
||||
|
||||
Template.libraryItemDialog.events({
|
||||
"click #backButton": function(){
|
||||
popDialogStack();
|
||||
},
|
||||
"click #deleteButton": function(){
|
||||
LibraryItems.remove(this.itemId);
|
||||
popDialogStack();
|
||||
},
|
||||
"input #libraryItemLibraryNameInput": bind("libraryName"),
|
||||
"input #libraryItemNameInput": bind("name"),
|
||||
"input #libraryItemPluralInput": bind("plural"),
|
||||
"input #libraryItemQuantityInput": bind("quantity"),
|
||||
"input #libraryItemValueInput": bind("value"),
|
||||
"input #libraryItemWeightInput": bind("weight"),
|
||||
"change #attunementCheckbox": function(event){
|
||||
LibraryItems.update(this.itemId, {
|
||||
$set: {requiresAttunement: event.currentTarget.checked}
|
||||
});
|
||||
},
|
||||
"change #incrementCheckbox": function(event){
|
||||
LibraryItems.update(this.itemId, {
|
||||
$set: {"settings.showIncrement": event.currentTarget.checked}
|
||||
});
|
||||
},
|
||||
"input #libraryItemDescriptionInput": bind("description"),
|
||||
|
||||
// Effects
|
||||
"click #addEffect": function(event, template){
|
||||
LibraryItems.update(template.data.itemId, {
|
||||
$push: {
|
||||
effects: {operation: "add"}
|
||||
}
|
||||
});
|
||||
},
|
||||
"iron-select .operationMenu": function(event, template){
|
||||
var detail = event.originalEvent.detail;
|
||||
var opName = detail.item.getAttribute("name");
|
||||
if (opName == this.operation) return;
|
||||
Meteor.call("updateLibraryItemEffect", {
|
||||
itemId: template.data.itemId,
|
||||
effectIndex: this.index,
|
||||
field: "operation",
|
||||
value: opName,
|
||||
});
|
||||
},
|
||||
"input .LibraryItemEffectStat": _.debounce(function(event, template){
|
||||
Meteor.call("updateLibraryItemEffect", {
|
||||
itemId: template.data.itemId,
|
||||
effectIndex: this.index,
|
||||
field: "stat",
|
||||
value: event.currentTarget.value,
|
||||
});
|
||||
}, 300),
|
||||
"input .LibraryItemEffectValue": _.debounce(function(event, template){
|
||||
let value = event.currentTarget.value;
|
||||
if (value && _.isFinite(+value)){
|
||||
Meteor.call("updateLibraryItemEffect", {
|
||||
itemId: template.data.itemId,
|
||||
effectIndex: this.index,
|
||||
field: "value",
|
||||
unsetField: "calculation",
|
||||
value,
|
||||
});
|
||||
} else {
|
||||
Meteor.call("updateLibraryItemEffect", {
|
||||
itemId: template.data.itemId,
|
||||
effectIndex: this.index,
|
||||
field: "calculation",
|
||||
unsetField: "value",
|
||||
value,
|
||||
});
|
||||
}
|
||||
}, 300),
|
||||
"click .deleteEffect": function (event, template) {
|
||||
Meteor.call("removeLibraryItemEffect", {
|
||||
itemId: template.data.itemId,
|
||||
effectIndex: this.index,
|
||||
});
|
||||
},
|
||||
|
||||
// Attacks
|
||||
"click #addAttack": function(event, template){
|
||||
LibraryItems.update(template.data.itemId, {
|
||||
$push: {
|
||||
attacks: {damageType: "slashing"}
|
||||
}
|
||||
});
|
||||
},
|
||||
"iron-select .damageTypeMenu": function(event, template){
|
||||
var detail = event.originalEvent.detail;
|
||||
var damageType = detail.item.getAttribute("name");
|
||||
if (damageType == this.damageType) return;
|
||||
Meteor.call("updateLibraryItemAttack", {
|
||||
itemId: template.data.itemId,
|
||||
attackIndex: this.index,
|
||||
field: "damageType",
|
||||
value: damageType,
|
||||
});
|
||||
},
|
||||
"input .LibraryItemAttackBonusInput": _.debounce(function(event, template){
|
||||
Meteor.call("updateLibraryItemAttack", {
|
||||
itemId: template.data.itemId,
|
||||
attackIndex: this.index,
|
||||
field: "attackBonus",
|
||||
value: event.currentTarget.value,
|
||||
});
|
||||
}, 300),
|
||||
"input .LibraryItemAttackDamageInput": _.debounce(function(event, template){
|
||||
Meteor.call("updateLibraryItemAttack", {
|
||||
itemId: template.data.itemId,
|
||||
attackIndex: this.index,
|
||||
field: "damage",
|
||||
value: event.currentTarget.value,
|
||||
});
|
||||
}, 300),
|
||||
"input .LibraryItemAttackDetailsInput": _.debounce(function(event, template){
|
||||
Meteor.call("updateLibraryItemAttack", {
|
||||
itemId: template.data.itemId,
|
||||
attackIndex: this.index,
|
||||
field: "details",
|
||||
value: event.currentTarget.value,
|
||||
});
|
||||
}, 300),
|
||||
|
||||
"click .deleteAttack": function (event, template) {
|
||||
Meteor.call("removeLibraryItemAttack", {
|
||||
itemId: template.data.itemId,
|
||||
attackIndex: this.index,
|
||||
});
|
||||
},
|
||||
});
|
||||
19
app/client/views/library/library.css
Normal file
19
app/client/views/library/library.css
Normal file
@@ -0,0 +1,19 @@
|
||||
.library .item-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.library .library-header {
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.library .library-header iron-icon {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.library .library-header iron-icon.open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
@@ -1,26 +1,32 @@
|
||||
<template name="library">
|
||||
<div class="fit layout vertical character-sheet">
|
||||
<div class="fit layout vertical library">
|
||||
<app-header fixed effects="waterfall">
|
||||
<app-toolbar class="medium-tall app-grey white-text">
|
||||
<div top-item class="layout horizontal center">
|
||||
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
|
||||
<div class="flex">
|
||||
Library
|
||||
<div class="flex layout horizontal center" style="height: 40px; margin-left: 8px;">
|
||||
Item Library
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
<div bottom-item>
|
||||
<paper-tabs id="characterSheetTabs" selected={{selectedTab}} class="app-grey white-text">
|
||||
<paper-tabs id="libraryTabs" selected={{selectedTab}} class="app-grey white-text">
|
||||
<paper-tab name="items">Items</paper-tab>
|
||||
<paper-tab name="spells">Spells</paper-tab>
|
||||
</paper-tabs>
|
||||
</div>
|
||||
-->
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="flex" style="position: relative;">
|
||||
<iron-pages id="tabPages" class="fit" selected={{selectedTab}}>
|
||||
<!-- <iron-pages id="tabPages" class="fit" selected={{selectedTab}}> -->
|
||||
<div name="items" class="tab-page fit">{{> itemLibrary}}</div>
|
||||
<div name="spells" class="tab-page fit">{{! {{> spellLibrary}} }}</div>
|
||||
</iron-pages>
|
||||
<!-- <div name="spells" class="tab-page fit">{{! {{> spellLibrary}} }}</div>
|
||||
</iron-pages> -->
|
||||
</div>
|
||||
<div class="floatyButton">
|
||||
<paper-fab id="addLibrary" icon="add"></paper-fab>
|
||||
{{#simpleTooltip}}Add Library{{/simpleTooltip}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
29
app/client/views/library/library.js
Normal file
29
app/client/views/library/library.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const librarySubs = new SubsManager();
|
||||
|
||||
Template.library.onCreated(function(){
|
||||
this.selectedTab = new ReactiveVar("0");
|
||||
});
|
||||
|
||||
Template.library.helpers({
|
||||
selectedTab(){
|
||||
return Template.instance().selectedTab.get();
|
||||
},
|
||||
});
|
||||
|
||||
Template.library.events({
|
||||
"iron-select #libraryTabs": function(event, instance){
|
||||
instance.selectedTab.set(event.target.selected);
|
||||
},
|
||||
"click #addLibrary": function(event, instance){
|
||||
var libraryId = Libraries.insert({
|
||||
name: "New Library",
|
||||
owner: Meteor.userId(),
|
||||
});
|
||||
pushDialogStack({
|
||||
template: "libraryDialog",
|
||||
data: {libraryId},
|
||||
element: event.currentTarget,
|
||||
returnElement: () => instance.find(`.library-header[data-id='${libraryId}']`),
|
||||
});
|
||||
},
|
||||
})
|
||||
58
app/client/views/library/libraryDialog/libraryDialog.html
Normal file
58
app/client/views/library/libraryDialog/libraryDialog.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<template name="libraryDialog">
|
||||
<div class="fit base-dialog layout vertical">
|
||||
<app-toolbar>
|
||||
<div main-title>{{library.name}}</div>
|
||||
<paper-icon-button id="deleteButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
icon="delete">
|
||||
</paper-icon-button>
|
||||
</app-toolbar>
|
||||
<div class="form flex scroll-y" style="position: relative;">
|
||||
<paper-input id="libraryNameInput" class="fullwidth" label="Name" value={{library.name}}></paper-input>
|
||||
<div class="layout horizontal center wrap">
|
||||
<paper-input class="flex" id="userNameOrEmailInput" label="Share with username or email" floatinglabel></paper-input>
|
||||
<paper-button id="shareButton"
|
||||
class="red-button"
|
||||
style="width: 80px; height: 37px; margin-top: 16px;"
|
||||
raised
|
||||
disabled={{shareButtonDisabled}}>Share</paper-button>
|
||||
<paper-radio-group id="accessLevelMenu" selected="read">
|
||||
<paper-radio-button name="read">View Only</paper-radio-button>
|
||||
<paper-radio-button name="write">Can Edit</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
</div>
|
||||
<p style="color: red;">{{userFindError}}</p>
|
||||
<div>
|
||||
{{#if readers.length}}
|
||||
<div class="paper-font-subhead">
|
||||
Can View
|
||||
</div>
|
||||
{{#each id in readers}}
|
||||
<div class="layout horizontal center">
|
||||
{{#with id=id}}
|
||||
<paper-icon-button class="deleteShare" icon="delete">
|
||||
</paper-icon-button>
|
||||
{{/with}}
|
||||
<div class="flex">{{username id}}</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{#if writers.length}}
|
||||
<div class="paper-font-subhead">
|
||||
Can Edit
|
||||
</div>
|
||||
{{#each id in writers}}
|
||||
<div class="layout horizontal center">
|
||||
{{#with id=id}}
|
||||
<paper-icon-button class="deleteShare" icon="delete">
|
||||
</paper-icon-button>
|
||||
{{/with}}
|
||||
<div class="flex">{{username id}}</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
95
app/client/views/library/libraryDialog/libraryDialog.js
Normal file
95
app/client/views/library/libraryDialog/libraryDialog.js
Normal file
@@ -0,0 +1,95 @@
|
||||
Template.libraryDialog.onCreated(function(){
|
||||
this.userId = new ReactiveVar();
|
||||
this.autorun(() => {
|
||||
var library = Libraries.findOne(Template.currentData().libraryId, {
|
||||
fields: {readers: 1, writers: 1, owner: 1}
|
||||
});
|
||||
if (!library) return;
|
||||
this.subscribe("userNames", _.union(library.readers, library.writers, [library.owner]));
|
||||
});
|
||||
});
|
||||
|
||||
Template.libraryDialog.helpers({
|
||||
library(){
|
||||
return Libraries.findOne(this.libraryId);
|
||||
},
|
||||
readers: function(){
|
||||
var library = Libraries.findOne(this.libraryId, {fields: {readers: 1}});
|
||||
return library && library.readers;
|
||||
},
|
||||
writers: function(){
|
||||
var library = Libraries.findOne(this.libraryId, {fields: {writers: 1}});
|
||||
return library && library.writers
|
||||
},
|
||||
username: function(id){
|
||||
const user = Meteor.users.findOne(id);
|
||||
return user && user.username || "user: " + id;
|
||||
},
|
||||
shareButtonDisabled: function(){
|
||||
return !Template.instance().userId.get();
|
||||
},
|
||||
userFindError: function(){
|
||||
if (!Template.instance().userId.get()){
|
||||
return "User not found";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.libraryDialog.events({
|
||||
"input #libraryNameInput": _.debounce(function(event){
|
||||
const input = event.currentTarget;
|
||||
var name = input.value;
|
||||
if (!name){
|
||||
input.invalid = true;
|
||||
input.errorMessage = "Name is required";
|
||||
} else {
|
||||
input.invalid = false;
|
||||
Libraries.update(this.libraryId, {
|
||||
$set: {name}
|
||||
}, {
|
||||
removeEmptyStrings: false,
|
||||
trimStrings: false,
|
||||
});
|
||||
}
|
||||
}, 300),
|
||||
"click #deleteButton": function(){
|
||||
Meteor.call("removeLibrary", this.libraryId);
|
||||
popDialogStack();
|
||||
},
|
||||
"input #userNameOrEmailInput":
|
||||
function(event, instance){
|
||||
var userName = instance.find("#userNameOrEmailInput").value;
|
||||
instance.userId.set(undefined);
|
||||
Meteor.call("getUserId", userName, function(err, result) {
|
||||
if (err){
|
||||
console.error(err);
|
||||
} else {
|
||||
console.log(result);
|
||||
instance.userId.set(result);
|
||||
}
|
||||
});
|
||||
},
|
||||
"click #shareButton": function(event, instance){
|
||||
var self = this;
|
||||
var permission = instance.find("#accessLevelMenu").selected;
|
||||
if (!permission) throw "no permission set";
|
||||
var userId = instance.userId.get();
|
||||
if (!userId) return;
|
||||
if (permission === "write"){
|
||||
Libraries.update(self.libraryId, {
|
||||
$addToSet: {writers: userId},
|
||||
$pull: {readers: userId},
|
||||
});
|
||||
} else {
|
||||
Libraries.update(self.libraryId, {
|
||||
$addToSet: {readers: userId},
|
||||
$pull: {writers: userId},
|
||||
});
|
||||
}
|
||||
},
|
||||
"click .deleteShare": function(event, instance) {
|
||||
Libraries.update(instance.data.libraryId, {
|
||||
$pull: {writers: this.id, readers: this.id}
|
||||
});
|
||||
},
|
||||
});
|
||||
99
app/lib/functions/backupRestoreCharacter.js
Normal file
99
app/lib/functions/backupRestoreCharacter.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
let characterCollections = [];
|
||||
Meteor.startup(() => {
|
||||
characterCollections = [
|
||||
Actions,
|
||||
Attacks,
|
||||
Buffs,
|
||||
Classes,
|
||||
Conditions,
|
||||
CustomBuffs,
|
||||
Effects,
|
||||
Experiences,
|
||||
Features,
|
||||
Notes,
|
||||
Proficiencies,
|
||||
SpellLists,
|
||||
Spells,
|
||||
TemporaryHitPoints,
|
||||
Items,
|
||||
Containers,
|
||||
];
|
||||
});
|
||||
|
||||
dumpCharacter = function(charId){
|
||||
let characterDump = {collections: {}};
|
||||
characterDump.character = Characters.findOne(charId);
|
||||
characterCollections.forEach(c => {
|
||||
characterDump.collections[c._name] = c.find({charId}).fetch();
|
||||
});
|
||||
return characterDump;
|
||||
};
|
||||
|
||||
saveCharacterDump = function(charId){
|
||||
let dump = dumpCharacter(charId);
|
||||
let textDump = JSON.stringify(dump, null, 2);
|
||||
let charName = dump.character.name;
|
||||
let blob = new Blob([textDump], {type: "application/json;charset=utf-8"});
|
||||
saveAs(blob, `${charName}.JSON`);
|
||||
};
|
||||
|
||||
giveCharacterDumpNewIds = function(characterDump){
|
||||
// Give the character a new Id
|
||||
const oldCharId = characterDump.character._id;
|
||||
const newCharId = Random.id();
|
||||
characterDump.character._id = newCharId;
|
||||
|
||||
let idMap = {[oldCharId]: newCharId}; // {oldId: newId}
|
||||
|
||||
// Give all documents a new Id, and store the mapping from old to new
|
||||
for (let colName in characterDump.collections){
|
||||
for (let doc of characterDump.collections[colName]){
|
||||
let oldId = doc._id;
|
||||
let newId = Random.id();
|
||||
doc._id = newId;
|
||||
idMap[oldId] = newId;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace all references to old Ids with new ones
|
||||
for (let colName in characterDump.collections){
|
||||
for (let doc of characterDump.collections[colName]){
|
||||
// Replace the character Id with the new one
|
||||
doc.charId = newCharId;
|
||||
// Replace the parent reference id with a new id
|
||||
if (doc.parent && doc.parent.id){
|
||||
let newParentId = idMap[doc.parent.id];
|
||||
if(!newParentId) throw `Can't find the mapping for id ${doc.parent.id}`;
|
||||
doc.parent.id = newParentId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
restoreCharacter = function(characterDump){
|
||||
Characters.direct.insert(characterDump.character);
|
||||
for (collectionName in characterDump.collections){
|
||||
let collection = Meteor.Collection.get(collectionName);
|
||||
for (doc of characterDump.collections[collectionName]){
|
||||
// delete problematic keys that shouldn't ever be available on insert
|
||||
delete doc.restoredAt;
|
||||
delete doc.restoredBy;
|
||||
// Insert the doc with no hooks
|
||||
collection.direct.insert(doc);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Meteor.methods({
|
||||
restoreCharacter(characterDump){
|
||||
characterDump.character.name += " - Restored"
|
||||
characterDump.character.owner = Meteor.userId();
|
||||
characterDump.character.readers = [];
|
||||
characterDump.character.writers = [];
|
||||
giveCharacterDumpNewIds(characterDump);
|
||||
restoreCharacter(characterDump);
|
||||
return characterDump.character
|
||||
},
|
||||
});
|
||||
@@ -3,10 +3,12 @@ var childSchema = new SimpleSchema({
|
||||
"parent.collection": {type: String},
|
||||
"parent.id": {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
"parent.group": {type: String, optional: true},
|
||||
"removed": {type: Boolean, optional: true, index: 1},
|
||||
"removedWith": {
|
||||
optional: true,
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -143,17 +145,17 @@ var checkPermission = function(userId, charId){
|
||||
return true;
|
||||
};
|
||||
|
||||
var cascadeSoftRemove = function(id, removedWithId){
|
||||
var cascadeSoftRemove = function(parentId, removedWithId){
|
||||
_.each(childCollections, function(treeCollection){
|
||||
treeCollection.update(
|
||||
{"parent.id": id},
|
||||
{"parent.id": parentId},
|
||||
{$set: {
|
||||
removed: true,
|
||||
removedWith: removedWithId,
|
||||
}},
|
||||
{multi: true}
|
||||
);
|
||||
treeCollection.find({"parent.id": id}).forEach(function(doc){
|
||||
treeCollection.find({"parent.id": parentId, removed: true}).forEach(function(doc){
|
||||
cascadeSoftRemove(doc._id, removedWithId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,12 +5,14 @@ canEditCharacter = function(charId, userId){
|
||||
return (userId === char.owner || _.contains(char.writers, userId));
|
||||
};
|
||||
|
||||
canViewCharacter = function(charId, userId){
|
||||
canViewCharacter = function(char, userId){
|
||||
userId = userId || Meteor.userId();
|
||||
var char = Characters.findOne(
|
||||
charId,
|
||||
{fields: {owner: 1, writers: 1, readers: 1, "settings.viewPermission": 1}}
|
||||
);
|
||||
if (typeof char !== 'object'){
|
||||
char = Characters.findOne(
|
||||
char,
|
||||
{fields: {owner: 1, writers: 1, readers: 1, "settings.viewPermission": 1}}
|
||||
);
|
||||
}
|
||||
if (!char) return true;
|
||||
return userId === char.owner ||
|
||||
char.settings.viewPermission === "public" ||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
Meteor.methods({
|
||||
"getUserId": function(username){
|
||||
if (!username) return;
|
||||
regex = new RegExp("^" + username + "$", "i")
|
||||
var user = Meteor.users.findOne(
|
||||
{$or: [
|
||||
{username: username},
|
||||
{"emails.address": regex},
|
||||
{"services.google.email": regex},
|
||||
]}
|
||||
);
|
||||
if (Meteor.isClient) return;
|
||||
let user = Accounts.findUserByUsername(username) ||
|
||||
Accounts.findUserByEmail(username);
|
||||
return user && user._id;
|
||||
}
|
||||
});
|
||||
|
||||
26
app/lib/methods/characterCopyPaste.js
Normal file
26
app/lib/methods/characterCopyPaste.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// Uses '/lib/functions/backupRestoreCharacter.js' to do most the work
|
||||
|
||||
Meteor.methods({
|
||||
copyCharacter: function(charId) {
|
||||
const userId = Meteor.userId();
|
||||
let character = Characters.findOne(charId);
|
||||
|
||||
// Need at least view level permission to make a copy for yourself
|
||||
if (!canViewCharacter(character, userId)) return;
|
||||
|
||||
let characterDump = dumpCharacter(charId);
|
||||
giveCharacterDumpNewIds(characterDump);
|
||||
|
||||
// Remove all readers and writers, make this user the new owner
|
||||
characterDump.character.readers = [];
|
||||
characterDump.character.writers = [];
|
||||
characterDump.character.owner = userId;
|
||||
|
||||
// Rename the character so it's obviously a copy
|
||||
characterDump.character.name += " - Copy";
|
||||
|
||||
// Write the character back to the database
|
||||
restoreCharacter(characterDump);
|
||||
return characterDump.character;
|
||||
},
|
||||
});
|
||||
1061
app/package-lock.json
generated
1061
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "rpg-docs",
|
||||
"name": "dicecloud",
|
||||
"version": "0.10.0",
|
||||
"description": "Unofficial Online Realtime D&D 5e App",
|
||||
"scripts": {
|
||||
@@ -11,16 +11,16 @@
|
||||
},
|
||||
"author": "Stefan Zermatten",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0-beta.49",
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"@polymer/polymer": "^1.2.5-npm-test.2",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bcrypt": "^1.0.3",
|
||||
"bower": "^1.7.9",
|
||||
"core-js": "^2.5.7",
|
||||
"fibers": "^2.0.2",
|
||||
"file-saver": "^2.0.0",
|
||||
"meteor-node-stubs": "^0.3.3",
|
||||
"qrcode": "^1.2.0",
|
||||
"source-map-support": "^0.5.6",
|
||||
"qrcode": "^1.3.0",
|
||||
"source-map-support": "^0.5.9",
|
||||
"underscore": "^1.9.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,29 +15,33 @@ Meteor.startup(() => {
|
||||
const now = new Date();
|
||||
const thirtyMinutesAgo = new Date(now.getTime() - 30*60000);
|
||||
_.each(collections, (collection) => {
|
||||
numRemoved += collection.remove({
|
||||
collection.remove({
|
||||
removed: true,
|
||||
removedAt: {$lt: thirtyMinutesAgo} // dates *before* 30 minutes ago
|
||||
}, function(error, result){
|
||||
if (error){
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
return numRemoved;
|
||||
};
|
||||
|
||||
SyncedCron.add({
|
||||
name: "Delete all soft removed items that haven't been restored",
|
||||
name: "deleteSoftRemovedDocs",
|
||||
schedule: function(parser) {
|
||||
return parser.text('every 6 hours');
|
||||
return parser.text('every 2 hours');
|
||||
},
|
||||
job: function() {
|
||||
deleteOldSoftRemovedDocs();
|
||||
}
|
||||
job: deleteOldSoftRemovedDocs,
|
||||
});
|
||||
|
||||
SyncedCron.start();
|
||||
|
||||
// Add a method to manually trigger removal
|
||||
Meteor.methods({
|
||||
deleteOldSoftRemovedDocs() {
|
||||
const user = Meteor.users.findOne(this.userId);
|
||||
if (user && _.contains(user.roles, "admin")){
|
||||
this.unblock();
|
||||
return deleteOldSoftRemovedDocs();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -23,3 +23,18 @@ Meteor.publish("standardLibrarySpells", function(level){
|
||||
sort: {name: 1},
|
||||
});
|
||||
});
|
||||
|
||||
Meteor.publish("customLibraries", function(){
|
||||
userId = this.userId;
|
||||
return Libraries.find({
|
||||
$or: [
|
||||
{readers: userId},
|
||||
{writers: userId},
|
||||
{owner: userId},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
Meteor.publish("libraryItems", function(libraryId){
|
||||
return LibraryItems.find({library: libraryId});
|
||||
});
|
||||
|
||||
@@ -4,5 +4,6 @@ Meteor.publish("user", function(){
|
||||
username: 1,
|
||||
profile: 1,
|
||||
apiKey: 1,
|
||||
librarySubscriptions: 1,
|
||||
}});
|
||||
});
|
||||
|
||||
@@ -2194,9 +2194,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"castingTime": "bonus action",
|
||||
"castingTime": "action",
|
||||
"description": "Choose a manufactured metal object, such as a metal weapon or a suit of heavy or medium metal armor, that you can see within range. You cause the object to glow red-hot. Any creature in physical contact with the object takes 2d8 fire damage when you cast the spell. Until the spell ends, you can use a bonus action on each of your subsequent turns to cause this damage again.\n\nIf a creature is holding or wearing the object and takes the damage from it, the creature must succeed on a DC {DC} Constitution saving throw or drop the object if it can. If it doesn’t drop the object, it has disadvantage on attack rolls and ability checks until the start of your next turn.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, the damage increases by 1d8 for each slot level above 2nd.",
|
||||
"duration": "Instantaneous",
|
||||
"duration": "Concentration, up to 1 minute",
|
||||
"level": 2,
|
||||
"range": "60 feet",
|
||||
"school": "Transmutation",
|
||||
@@ -2204,8 +2204,9 @@
|
||||
"name": "Heat Metal",
|
||||
"components": {
|
||||
"verbal": true,
|
||||
"somatic": false,
|
||||
"concentration": false
|
||||
"somatic": true,
|
||||
"concentration": true,
|
||||
"material": "a piece of iron and a flame"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user