Compare commits

..

38 Commits

Author SHA1 Message Date
Stefan Zermatten
436c5bb785 Fixed bug in view permission causing 500 errors for Avrae 2019-02-12 09:53:29 +02:00
Stefan Zermatten
8489ef5ec0 Fixed restoring characters not working correctly for sub documents 2019-02-11 13:04:14 +02:00
Stefan Zermatten
c9710bdb09 Fixed restored characters not belonging to the user restoring them 2019-02-11 12:10:04 +02:00
Stefan Zermatten
26784f11b6 Merge branch 'feature-backup-restore' 2019-02-11 12:01:52 +02:00
Stefan Zermatten
23d43f7d43 Added character restore functionality 2019-02-11 11:58:45 +02:00
Stefan Zermatten
1ebb0d2527 Got character copying working 2019-02-11 11:11:51 +02:00
Stefan Zermatten
9d86cb8bee Added the copy character method 2019-02-11 10:21:11 +02:00
Stefan Zermatten
3343f8a813 Allowed canViewCharacter to take in a character instead of a charId to save a database read 2019-02-11 10:17:43 +02:00
Stefan Zermatten
0260824c2f Made gave backup and restore the ability to change ids for all docs 2019-02-11 10:09:18 +02:00
Stefan Zermatten
66ee3ff808 Merge remote-tracking branch 'origin/master' into feature-backup-restore 2019-02-11 09:45:05 +02:00
Stefan Zermatten
3f32535666 fixed link to dicecloud repo 2019-01-31 10:11:32 +02:00
Stefan Zermatten
4ea02c4fbb Fixed hidden link to localhost 2019-01-31 10:10:56 +02:00
Stefan Zermatten
b052e8dd19 Update README.md 2019-01-31 10:09:19 +02:00
Stefan Zermatten
e2822b9f22 added naive backup restore 2019-01-28 11:35:56 +02:00
Stefan Zermatten
c46b836985 Merge pull request #192 from Frogvall/master
Updated heat metal according to SRD/PHB
2018-11-14 10:14:19 +02:00
Frogvall
65d1bac0dc Updated heat metal according to SRD/PHB 2018-11-14 06:40:22 +01:00
Stefan Zermatten
fcae3056de Merge branch 'bugfix-67' 2018-11-13 12:57:28 +02:00
Stefan Zermatten
7d364c80c0 Added hotfix to fix password button until the relevant package can be updated, fixes #67 2018-11-13 12:57:02 +02:00
Stefan Zermatten
0ff6c08abd Merge branch 'bugfix-150' 2018-11-13 12:01:52 +02:00
Stefan Zermatten
1c95336843 Spell slot bubbles are limited to 10, overflow is shown numerically
fixes #150
2018-11-13 12:01:40 +02:00
Stefan Zermatten
b36720511b Merge branch 'bugfix-155' 2018-11-13 10:39:26 +02:00
Stefan Zermatten
261220fdd5 Fixes #155, buffs can now only be applied if you have write access
refactored applying buffs to be a method, not a client side operation. You can now only applied if you have write access to the receiving character.
2018-11-13 10:39:14 +02:00
Stefan Zermatten
64edc52cca Merge branch 'bugfix-170' 2018-11-13 10:08:08 +02:00
Stefan Zermatten
56f1bd2829 Fixes #170 and maybe some other more subtle problems regarding soft removes not cascading properly, orphaning objects 2018-11-13 10:07:55 +02:00
Stefan Zermatten
3b669fd2f9 Merge branch 'bugfix-177' 2018-11-13 09:32:24 +02:00
Stefan Zermatten
933878e158 fixes #177 2018-11-13 09:31:57 +02:00
Stefan Zermatten
0e6ca56316 Merge branch 'bugfix-191' 2018-11-13 09:28:06 +02:00
Stefan Zermatten
6599fe1ef8 fixes #191 2018-11-13 09:26:36 +02:00
Stefan Zermatten
f39baf43a1 Merge branch 'bugfix-187' 2018-11-13 08:59:34 +02:00
Stefan Zermatten
96f4e35e25 fixes #187 2018-11-13 08:58:42 +02:00
Stefan Zermatten
e17dbf6601 Upgrade to meteor 1.8 2018-10-10 14:28:48 +02:00
Stefan Zermatten
3f81d419f7 updated meteor 2018-10-10 14:00:46 +02:00
Stefan Zermatten
1c00f5aa04 Hotfix iOS sign in issues (maybe) 2018-10-04 10:05:02 +02:00
Stefan Zermatten
f5a32cb50a Cobbled together some semblance of an item library UI 2018-10-02 15:43:10 +02:00
Stefan Zermatten
f4d3368fb4 Updated cron job to clean database of soft removed documents 2018-09-25 10:58:01 +02:00
Stefan Zermatten
1de9fb558a Merge pull request #182 from mommothazaz123/patch-1
fix error in Characters.deny()
2018-09-14 10:06:43 +02:00
Andrew Zhu
06ffc94b4c fix error in Characters.deny()
Renamed `docs` to `doc`, since `doc.owner` was undefined.
2018-09-07 14:07:55 -07:00
Stefan Zermatten
74c6a423ee Allowed owners to give their characters to new owners, no UI yet. 2018-08-28 09:59:10 +02:00
714 changed files with 24342 additions and 21914 deletions

View File

@@ -0,0 +1,13 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="HtmlUnknownAttribute" enabled="false" level="WARNING" enabled_by_default="false">
<option name="myValues">
<value>
<list size="0" />
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
</profile>
</component>

View File

@@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Project Default" />
<option name="USE_PROJECT_PROFILE" value="true" />
<version value="1.0" />
</settings>
</component>

56
.jscsrc Normal file
View File

@@ -0,0 +1,56 @@
{
"requireOperatorBeforeLineBreak": true,
"requireCamelCaseOrUpperCaseIdentifiers": true,
"maximumLineLength": {
"value": 80,
"allowComments": true,
"allowRegex": true
},
"validateIndentation": "\t",
"validateQuoteMarks": "\"",
"disallowMultipleLineStrings": true,
"disallowMixedSpacesAndTabs": "smart",
"disallowTrailingWhitespace": true,
"disallowSpaceAfterPrefixUnaryOperators": true,
"disallowMultipleVarDecl": false,
"disallowNewlineBeforeBlockStatements": true,
"disallowKeywordsOnNewLine": ["else"],
"requireSpaceAfterKeywords": [
"if",
"else",
"for",
"while",
"do",
"switch",
"return",
"try",
"catch"
],
"requireSpaceBeforeBinaryOperators": [
"=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=",
"&=", "|=", "^=", "+=",
"+", "-", "*", "/", "%", "<<", ">>", ">>>", "&",
"|", "^", "&&", "||", "===", "==", ">=",
"<=", "<", ">", "!=", "!=="
],
"requireSpaceAfterBinaryOperators": true,
"requireSpacesInConditionalExpression": true,
"requireSpacesInForStatement": true,
"requireTrailingComma": {
"ignoreSingleValue": true,
"ignoreSingleLine": true
},
"requireLineFeedAtFileEnd": true,
"disallowSpacesInAnonymousFunctionExpression": {
"beforeOpeningRoundBrace": true
},
"disallowSpacesInsideObjectBrackets": "all",
"disallowSpacesInsideArrayBrackets": "all",
"disallowSpacesInsideParentheses": true,
"disallowMultipleLineBreaks": true,
"disallowNewlineBeforeBlockStatements": true
}

4
.jshintrc Normal file
View File

@@ -0,0 +1,4 @@
{
"undef": false,
"esversion": 6
}

View File

@@ -1,14 +0,0 @@
FROM ubuntu:latest
RUN apt-get update --quiet \
&& apt-get install --quiet --yes \
bsdtar \
curl \
git
RUN ln --symbolic --force $(which bsdtar) $(which tar)
RUN useradd --create-home --shell /bin/bash dicecloud
USER dicecloud
WORKDIR /home/dicecloud
RUN curl https://install.meteor.com/?release=1.8.0.2 | sh
ENV PATH="${PATH}:/home/dicecloud/.meteor"
COPY dev.sh ./dev.sh
ENTRYPOINT ./dev.sh

View File

@@ -45,26 +45,27 @@ 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, and running
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`
`meteor npm install`
`bower install`
`meteor`
You should see this:
```
=> Started proxy.
=> [HMR] Dev server listening on port 3003.
=> Started MongoDB.
=> Started your app.
=> App running at: http://localhost:3000/
```
Now, visiting [](http://localhost:3000/) should show you an empty instance of
Now, visiting http://localhost:3000/ should show you an empty instance of
DiceCloud running.

1
app/.bowerrc Normal file
View File

@@ -0,0 +1 @@
{"directory":"public/components/"}

2
app/.gitignore vendored
View File

@@ -1,7 +1,6 @@
.meteor/local
.meteor/meteorite
.demeteorized
.cache
settings.json
public/components
public/_imports.html
@@ -9,3 +8,4 @@ private/oldClient
nohup.out
node_modules
dump
.cache

View File

@@ -16,4 +16,3 @@ notices-for-facebook-graph-api-2
1.4.3-split-account-service-packages
1.5-add-dynamic-import-package
1.7-split-underscore-from-meteor-base
1.8.3-split-jquery-from-blaze

View File

@@ -3,49 +3,54 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password@1.6.0
accounts-password@1.5.1
accounts-ui@1.3.1
random@1.2.0
random@1.1.0
dburles:collection-helpers
reactive-var@1.0.11
underscore@1.0.10
aldeed:collection2
matb33:collection-hooks
zimme:collection-softremovable
momentjs:moment
dburles:mongo-collection-instances
percolate:migrations
accounts-google@1.3.3
ecwyne:mathjs
useraccounts:polymer
accounts-google@1.3.2
splendido:accounts-meld
email@1.2.3
meteorhacks:subs-manager
chuangbo:marked
reywood:iron-router-ga
meteor-base@1.4.0
mobile-experience@1.1.0
mongo@1.10.0
session@1.2.0
mobile-experience@1.0.5
mongo@1.6.0
blaze-html-templates
session@1.1.8
jquery@1.11.10
tracker@1.2.0
logging@1.1.20
reload@1.3.0
ejson@1.1.1
reload@1.2.0
ejson@1.1.0
spacebars
check@1.3.1
standard-minifier-js@2.6.0
shell-server@0.5.0
useraccounts:iron-routing
wizonesolutions:canonical
standard-minifier-js@2.4.0
shell-server@0.4.0
seba:minifiers-autoprefixer
nikogosovd:multiple-uihooks
templates:array
ecmascript@0.14.3
ecmascript@0.12.0
es5-shim@4.8.0
reactive-dict@1.3.0
percolate:synced-cron
differential:vulcanize
reactive-dict@1.2.1
ongoworks:speakingurl
service-configuration@1.0.11
google-config-ui@1.0.1
dynamic-import@0.5.2
dynamic-import@0.5.0
ddp-rate-limiter@1.0.7
rate-limit@1.0.9
meteortesting:mocha
mdg:validated-method
akryum:vue-router2
static-html
aldeed:collection2@3.0.0
aldeed:schema-index
akryum:vue-component
accounts-patreon
bozhao:link-accounts
iron:router
littledata:synced-cron

View File

@@ -1 +1 @@
METEOR@1.10.2
METEOR@1.8

View File

@@ -1,126 +1,139 @@
accounts-base@1.6.0
accounts-google@1.3.3
accounts-oauth@1.2.0
accounts-password@1.6.0
accounts-patreon@0.1.0
accounts-base@1.4.3
accounts-google@1.3.2
accounts-oauth@1.1.16
accounts-password@1.5.1
accounts-ui@1.3.1
accounts-ui-unstyled@1.4.2
akryum:npm-check@0.1.2
akryum:vue-component@0.15.2
akryum:vue-component-dev-client@0.4.7
akryum:vue-component-dev-server@0.1.4
akryum:vue-router2@0.2.3
aldeed:collection2@3.0.6
aldeed:schema-index@3.0.0
accounts-ui-unstyled@1.4.1
aldeed:collection2@2.10.0
aldeed:collection2-core@1.2.0
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.6.0
babel-compiler@7.5.3
babel-runtime@1.5.0
base64@1.0.12
autoupdate@1.5.0
babel-compiler@7.2.0
babel-runtime@1.3.0
base64@1.0.11
binary-heap@1.0.11
blaze@2.3.4
blaze@2.3.3
blaze-html-templates@1.1.2
blaze-tools@1.0.10
boilerplate-generator@1.7.0
bozhao:link-accounts@2.1.1
caching-compiler@1.2.2
boilerplate-generator@1.6.0
caching-compiler@1.2.0
caching-html-compiler@1.1.3
callback-hook@1.3.0
callback-hook@1.1.0
check@1.3.1
chuangbo:marked@0.3.5_1
coffeescript@1.0.17
dburles:collection-helpers@1.1.0
dburles:mongo-collection-instances@0.3.5
ddp@1.4.0
ddp-client@2.3.3
ddp-common@1.4.0
ddp-rate-limiter@1.0.7
ddp-server@2.3.1
ddp-server@2.2.0
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.5.2
ecmascript@0.14.3
diff-sequence@1.1.0
differential:vulcanize@3.0.0
dynamic-import@0.5.0
ecmascript@0.12.0
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.10.0
ecmascript-runtime-server@0.9.0
ejson@1.1.1
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.1
fetch@0.1.0
geojson-utils@1.0.10
google-config-ui@1.0.1
google-oauth@1.3.0
google-oauth@1.2.6
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
http@1.4.2
http@1.4.1
id-map@1.1.0
inter-process-messaging@0.1.1
inter-process-messaging@0.1.0
iron:controller@1.0.12
iron:core@1.0.11
iron:dynamic-template@1.0.12
iron:layout@1.0.12
iron:location@1.0.11
iron:middleware-stack@1.1.0
iron:router@1.1.2
iron:url@1.1.0
jquery@1.11.11
lai:collection-extensions@0.2.1_1
launch-screen@1.2.0
launch-screen@1.1.1
less@2.8.0
littledata:synced-cron@1.5.1
livedata@1.0.18
lmieulet:meteor-coverage@1.1.4
localstorage@1.2.0
logging@1.1.20
mdg:validated-method@1.2.0
meteor@1.9.3
matb33:collection-hooks@0.8.4
mdg:validation-error@0.5.1
meteor@1.9.2
meteor-base@1.4.0
meteorhacks:picker@1.0.3
meteorhacks:subs-manager@1.6.4
meteortesting:browser-tests@1.3.3
meteortesting:mocha@1.1.5
meteortesting:mocha-core@7.0.1
minifier-css@1.5.0
minifier-js@2.6.0
minimongo@1.6.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.5
modules@0.15.0
modules-runtime@0.12.0
momentjs:moment@2.24.0
mongo@1.10.0
mongo-decimal@0.1.1
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.13.0
modules-runtime@0.10.2
momentjs:moment@2.22.2
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.7.0
oauth@1.3.0
oauth2@1.3.0
npm-mongo@3.1.1
oauth@1.2.3
oauth2@1.2.1
observe-sequence@1.0.16
ongoworks:speakingurl@9.0.0
ordered-dict@1.1.0
patreon-oauth@0.1.0
percolate:migrations@0.9.8
percolate:synced-cron@1.3.2
promise@0.11.2
promise@0.11.1
raix:eventemitter@0.1.3
random@1.2.0
random@1.1.0
rate-limit@1.0.9
reactive-dict@1.3.0
reactive-dict@1.2.1
reactive-var@1.0.11
reload@1.3.0
reload@1.2.0
retry@1.1.0
reywood:iron-router-ga@0.7.1
routepolicy@1.1.0
seba:minifiers-autoprefixer@1.1.2
seba:minifiers-autoprefixer@1.1.1
service-configuration@1.0.11
session@1.2.0
session@1.1.8
sha@1.0.9
shell-server@0.5.0
socket-stream-client@0.3.0
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
srp@1.1.0
standard-minifier-js@2.6.0
static-html@1.2.2
splendido:accounts-emails-field@1.2.0
splendido:accounts-meld@1.3.1
srp@1.0.12
standard-minifier-js@2.4.0
templates:array@1.0.3
templating@1.3.2
templating-compiler@1.3.3
templating-runtime@1.3.2
templating-tools@1.1.2
tmeasday:check-npm-versions@0.3.2
tracker@1.2.0
ui@1.0.13
underscore@1.0.10
url@1.3.0
webapp@1.9.1
url@1.2.0
useraccounts:core@1.14.2
useraccounts:iron-routing@1.14.2
useraccounts:polymer@1.14.2
webapp@1.7.0
webapp-hashing@1.0.9
wizonesolutions:canonical@0.0.5
zimme:collection-behaviours@1.1.3
zimme:collection-softremovable@1.0.5

View File

@@ -0,0 +1,7 @@
Instances = new Mongo.Collection("instances");
Schemas.Instance = new SimpleSchema({
//an instance is a single flow of time all parties in an instance are in-sync time wise
});
Instances.attachSchema(Schemas.Instance);

View File

@@ -1,8 +1,6 @@
import SimpleSchema from 'simpl-schema';
Parties = new Mongo.Collection("parties");
let Parties = new Mongo.Collection("parties");
let partySchema = new SimpleSchema({
Schemas.Party = new SimpleSchema({
name: {
type: String,
defaultValue: "New Party",
@@ -10,13 +8,10 @@ let partySchema = new SimpleSchema({
optional: true,
},
characters: {
type: Array,
defaultValue: [],
},
characters: {
type: String,
type: [String],
regEx: SimpleSchema.RegEx.Id,
index: 1,
defaultValue: [],
},
owner: {
type: String,
@@ -24,7 +19,7 @@ let partySchema = new SimpleSchema({
},
});
Parties.attachSchema(partySchema);
Parties.attachSchema(Schemas.Party);
Parties.allow({
insert: function(userId, doc) {
@@ -45,5 +40,3 @@ Parties.deny({
return _.contains(fields, "owner");
}
});
export default Parties;

View File

@@ -0,0 +1,40 @@
Actions = new Mongo.Collection("actions");
/*
* Actions are given to a character by items and features
*/
Schemas.Action = new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
name: {
type: String,
optional: true,
trim: false,
},
description: {
type: String,
optional: true,
trim: false,
},
type: {
type: String,
allowedValues: ["action, bonus, reaction, free"],
defaultValue: "action",
},
//the immediate impact of doing this action (eg. -1 rages)
adjustments: {
type: [Schemas.Adjustment],
defaultValue: [],
},
});
Actions.attachSchema(Schemas.Action);
Actions.attachBehaviour("softRemovable");
makeChild(Actions);
Actions.allow(CHARACTER_SUBSCHEMA_ALLOW);
Actions.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -0,0 +1,87 @@
Attacks = new Mongo.Collection("attacks");
/*
* Attacks are given to a character by items and features
*/
Schemas.Attack = new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
name: {
type: String,
defaultValue: "New Attack",
optional: true,
trim: false,
},
details: {
type: String,
optional: true,
trim: false,
},
attackBonus: {
type: String,
defaultValue: "strengthMod + proficiencyBonus",
optional: true,
trim: false,
},
damage: {
type: String,
defaultValue: "1d8 + {strengthMod}",
optional: true,
trim: false,
},
damageType: {
type: String,
allowedValues: [
"bludgeoning",
"piercing",
"slashing",
"acid",
"cold",
"fire",
"force",
"lightning",
"necrotic",
"poison",
"psychic",
"radiant",
"thunder",
],
defaultValue: "slashing",
},
//the id of the feature, buff or item that created this effect
parent: {
type: Schemas.Parent
},
color: {
type: String,
allowedValues: _.pluck(colorOptions, "key"),
defaultValue: "q",
},
enabled: {
type: Boolean,
defaultValue: true,
},
});
Attacks.attachSchema(Schemas.Attack);
Attacks.attachBehaviour("softRemovable");
makeChild(Attacks, ["name", "enabled"]); //children of lots of things
Attacks.after.insert(function (userId, attack) {
//Check to see if this attack's parent is a spell, if so, mirror prepared state to enabled
if (attack.parent.collection === "Spells") {
var parentSpell = Spells.findOne(attack.parent.id);
if (parentSpell.prepared === "unprepared") {
Attacks.update(attack._id, {$set: {enabled: false}});
} else if (parentSpell.prepared === "prepared" || "always") {
Attacks.update(attack._id, {$set: {enabled: true}});
}
}
});
Attacks.allow(CHARACTER_SUBSCHEMA_ALLOW);
Attacks.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -0,0 +1,101 @@
Buffs = new Mongo.Collection("buffs");
Schemas.Buff = new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
name: {
type: String,
optional: true,
trim: false,
},
description: {
type: String,
optional: true,
trim: false,
},
enabled: {
type: Boolean,
defaultValue: true,
},
type: {
type: String,
allowedValues: [
"inate", //this should be "innate", but changing it could be problematic
"custom",
],
},
"lifeTime.total": {
type: Number,
defaultValue: 0, //0 is infinite
min: 0,
},
"lifeTime.spent": {
type: Number,
defaultValue: 0,
min: 0,
},
color: {
type: String,
allowedValues: _.pluck(colorOptions, "key"),
defaultValue: "q",
},
appliedBy: { //the charId of whoever applied the buff
type: String,
regEx: SimpleSchema.RegEx.Id,
},
appliedByDetails: {//the name and collection of the thing that applied the buff
type: Object,
optional: true,
},
"appliedByDetails.name": {
type: String,
},
"appliedByDetails.collection": {
type: String,
},
});
Buffs.attachSchema(Schemas.Buff);
Buffs.attachBehaviour("softRemovable");
makeParent(Buffs, ["name", "enabled"]); //parents of effects, attacks, proficiencies
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"})
}
})

View File

@@ -0,0 +1,589 @@
//set up the collection for characters
Characters = new Mongo.Collection("characters");
Schemas.Character = new SimpleSchema({
//strings
name: {type: String, defaultValue: "", trim: false, optional: true},
urlName: {type: String, defaultValue: "-", trim: false, optional: true},
alignment: {type: String, defaultValue: "", trim: false, optional: true},
gender: {type: String, defaultValue: "", trim: false, optional: true},
race: {type: String, defaultValue: "", trim: false, optional: true},
picture: {type: String, defaultValue: "", trim: true, optional: true},
description: {type: String, defaultValue: "", trim: false, optional: true},
personality: {type: String, defaultValue: "", trim: false, optional: true},
ideals: {type: String, defaultValue: "", trim: false, optional: true},
bonds: {type: String, defaultValue: "", trim: false, optional: true},
flaws: {type: String, defaultValue: "", trim: false, optional: true},
backstory: {type: String, defaultValue: "", trim: false, optional: true},
//attributes
//ability scores
strength: {type: Schemas.Attribute},
dexterity: {type: Schemas.Attribute},
constitution: {type: Schemas.Attribute},
intelligence: {type: Schemas.Attribute},
wisdom: {type: Schemas.Attribute},
charisma: {type: Schemas.Attribute},
//stats
hitPoints: {type: Schemas.Attribute},
tempHP: {type: Schemas.Attribute},
experience: {type: Schemas.Attribute},
proficiencyBonus: {type: Schemas.Attribute},
speed: {type: Schemas.Attribute},
weight: {type: Schemas.Attribute},
age: {type: Schemas.Attribute},
ageRate: {type: Schemas.Attribute},
armor: {type: Schemas.Attribute},
carryMultiplier: {type: Schemas.Attribute},
//resources
level1SpellSlots: {type: Schemas.Attribute},
level2SpellSlots: {type: Schemas.Attribute},
level3SpellSlots: {type: Schemas.Attribute},
level4SpellSlots: {type: Schemas.Attribute},
level5SpellSlots: {type: Schemas.Attribute},
level6SpellSlots: {type: Schemas.Attribute},
level7SpellSlots: {type: Schemas.Attribute},
level8SpellSlots: {type: Schemas.Attribute},
level9SpellSlots: {type: Schemas.Attribute},
ki: {type: Schemas.Attribute},
sorceryPoints: {type: Schemas.Attribute},
rages: {type: Schemas.Attribute},
superiorityDice: {type: Schemas.Attribute},
expertiseDice: {type: Schemas.Attribute},
//specific features
rageDamage: {type: Schemas.Attribute},
//hit dice
d6HitDice: {type: Schemas.Attribute},
d8HitDice: {type: Schemas.Attribute},
d10HitDice: {type: Schemas.Attribute},
d12HitDice: {type: Schemas.Attribute},
//vulnerabilities
acidMultiplier: {type: Schemas.Attribute},
bludgeoningMultiplier: {type: Schemas.Attribute},
coldMultiplier: {type: Schemas.Attribute},
fireMultiplier: {type: Schemas.Attribute},
forceMultiplier: {type: Schemas.Attribute},
lightningMultiplier: {type: Schemas.Attribute},
necroticMultiplier: {type: Schemas.Attribute},
piercingMultiplier: {type: Schemas.Attribute},
poisonMultiplier: {type: Schemas.Attribute},
psychicMultiplier: {type: Schemas.Attribute},
radiantMultiplier: {type: Schemas.Attribute},
slashingMultiplier: {type: Schemas.Attribute},
thunderMultiplier: {type: Schemas.Attribute},
//skills
//saves
strengthSave: {type: Schemas.Skill},
"strengthSave.ability": {type: String, defaultValue: "strength"},
dexteritySave: {type: Schemas.Skill},
"dexteritySave.ability": {type: String, defaultValue: "dexterity"},
constitutionSave:{type: Schemas.Skill},
"constitutionSave.ability": {type: String, defaultValue: "constitution"},
intelligenceSave:{type: Schemas.Skill},
"intelligenceSave.ability": {type: String, defaultValue: "intelligence"},
wisdomSave: {type: Schemas.Skill},
"wisdomSave.ability": {type: String, defaultValue: "wisdom"},
charismaSave: {type: Schemas.Skill},
"charismaSave.ability": {type: String, defaultValue: "charisma"},
//skill skills
acrobatics: {type: Schemas.Skill},
"acrobatics.ability": {type: String, defaultValue: "dexterity"},
animalHandling: {type: Schemas.Skill},
"animalHandling.ability": {type: String, defaultValue: "wisdom"},
arcana: {type: Schemas.Skill},
"arcana.ability": {type: String, defaultValue: "intelligence"},
athletics: {type: Schemas.Skill},
"athletics.ability": {type: String, defaultValue: "strength"},
deception: {type: Schemas.Skill},
"deception.ability": {type: String, defaultValue: "charisma"},
history: {type: Schemas.Skill},
"history.ability": {type: String, defaultValue: "intelligence"},
insight: {type: Schemas.Skill},
"insight.ability": {type: String, defaultValue: "wisdom"},
intimidation: {type: Schemas.Skill},
"intimidation.ability": {type: String, defaultValue: "charisma"},
investigation: {type: Schemas.Skill},
"investigation.ability": {type: String, defaultValue: "intelligence"},
medicine: {type: Schemas.Skill},
"medicine.ability": {type: String, defaultValue: "wisdom"},
nature: {type: Schemas.Skill},
"nature.ability": {type: String, defaultValue: "intelligence"},
perception: {type: Schemas.Skill},
"perception.ability": {type: String, defaultValue: "wisdom"},
performance: {type: Schemas.Skill},
"performance.ability": {type: String, defaultValue: "charisma"},
persuasion: {type: Schemas.Skill},
"persuasion.ability": {type: String, defaultValue: "charisma"},
religion: {type: Schemas.Skill},
"religion.ability": {type: String, defaultValue: "intelligence"},
sleightOfHand: {type: Schemas.Skill},
"sleightOfHand.ability": {type: String, defaultValue: "dexterity"},
stealth: {type: Schemas.Skill},
"stealth.ability": {type: String, defaultValue: "dexterity"},
survival: {type: Schemas.Skill},
"survival.ability": {type: String, defaultValue: "wisdom"},
//Mechanical Skills
initiative: {type: Schemas.Skill},
"initiative.ability": {type: String, defaultValue: "dexterity"},
dexterityArmor: {type: Schemas.Skill},
"dexterityArmor.ability": {type: String, defaultValue: "dexterity"},
//mechanics
deathSave: {type: Schemas.DeathSave},
//permissions
party: {type: String, regEx: SimpleSchema.RegEx.Id, optional: true},
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},
color: {
type: String,
allowedValues: _.pluck(colorOptions, "key"),
defaultValue: "q",
},
//TODO add per-character settings
//how many experiences to load at a time in XP table
"settings.experiencesInc": {type: Number, defaultValue: 20},
//slowed down by carrying too much?
"settings.useVariantEncumbrance": {type: Boolean, defaultValue: false},
"settings.useStandardEncumbrance": {type: Boolean, defaultValue: true},
//hide spellcasting
"settings.hideSpellcasting": {type: Boolean, defaultValue: false},
//show to anyone with link
"settings.viewPermission": {
type: String,
defaultValue: "whitelist",
allowedValues: ["whitelist", "public"],
index: 1,
},
"settings.swapStatAndModifier": {type: Boolean, defaultValue: false},
"settings.exportFeatures": {type: Boolean, defaultValue: true},
"settings.exportAttacks": {type: Boolean, defaultValue: true},
"settings.exportDescription": {type: Boolean, defaultValue: true},
"settings.newUserExperience": {type: Boolean, optional: true},
});
Characters.attachSchema(Schemas.Character);
var attributeBase = preventLoop(function(charId, statName){
check(statName, String);
//if it's a damage multiplier, we treat it specially
if (_.contains(DAMAGE_MULTIPLIERS, statName)){
var invulnerabilityCount = Effects.find({
charId: charId,
stat: statName,
enabled: true,
operation: "mul",
value: 0,
}).count();
if (invulnerabilityCount) return 0;
var resistCount = Effects.find({
charId: charId,
stat: statName,
enabled: true,
operation: "mul",
value: 0.5,
}).count();
var vulnCount = Effects.find({
charId: charId,
stat: statName,
enabled: true,
operation: "mul",
value: 2,
}).count();
if (!resistCount && !vulnCount){
return 1;
} else if (resistCount && !vulnCount){
return 0.5;
} else if (!resistCount && vulnCount){
return 2;
} else {
return 1;
}
}
var value;
var base = 0;
var add = 0;
var mul = 1;
var min = Number.NEGATIVE_INFINITY;
var max = Number.POSITIVE_INFINITY;
Effects.find({
charId: charId,
stat: statName,
enabled: true,
operation: {$in: ["base", "add", "mul", "min", "max"]},
}).forEach(function(effect) {
value = evaluateEffect(charId, effect);
if (effect.operation === "base"){
if (value > base) base = value;
} else if (effect.operation === "add"){
add += value;
} else if (effect.operation === "mul"){
mul *= value;
} else if (effect.operation === "min"){
if (value > min) min = value;
} else if (effect.operation === "max"){
if (value < max) max = value;
}
});
var result = (base + add) * mul;
if (result < min) result = min;
if (result > max) result = max;
// Don't round carry multiplier
if (statName === "carryMultiplier"){
return result;
}
return Math.floor(result);
});
if (Meteor.isClient) {
Template.registerHelper("characterCalculate", function(func, charId, input) {
try {
return Characters.calculate[func](charId, input);
} catch (e){
if (!Characters.calculate[func]){
throw new Error(func + "is not a function name");
} else {
throw e;
}
}
});
}
//create a local memoize with a argument concatenating hash function
var memoize = function(f) {
if (Meteor.isServer) return f;
return Tracker.memoize(f, function() {
return _.reduce(arguments, function(memo, arg) {
return memo + arg;
}, "");
});
};
//memoize funcitons that have finds and slow loops
Characters.calculate = {
getField: function(charId, fieldName) {
var fieldSelector = {};
fieldSelector[fieldName] = 1;
var char = Characters.findOne(charId, {fields: fieldSelector});
if (!char) return;
var field = char[fieldName];
if (field === undefined){
throw new Meteor.Error(
"getField failed",
"getField could not find field " +
fieldName +
" in character " +
char._id
);
}
return field;
},
fieldValue: function(charId, fieldName) {
if (!Schemas.Character.schema(fieldName)){
throw new Meteor.Error(
"Field not found",
"Character's schema does not contain a field called: " + fieldName
);
}
//duck typing to get the right value function
//.ability implies skill
if (Schemas.Character.schema(fieldName + ".ability")){
return Characters.calculate.skillMod(charId, fieldName);
}
//adjustment implies attribute
if (Schemas.Character.schema(fieldName + ".adjustment")){
return Characters.calculate.attributeValue(charId, fieldName);
}
//fall back to just returning the field itself
return Characters.calculate.getField(charId, fieldName);
},
attributeValue: memoize(function(charId, attributeName){
var attribute = Characters.calculate.getField(charId, attributeName);
if (!attribute) return;
//base value
var value = Characters.calculate.attributeBase(charId, attributeName);
//plus adjustment
value += attribute.adjustment;
return value;
}),
attributeBase: memoize(function(charId, attributeName){
return attributeBase(charId, attributeName);
}),
skillMod: memoize(preventLoop(function(charId, skillName){
var skill = Characters.calculate.getField(charId, skillName);
if (!skill) return;
//get the final value of the ability score
var ability = Characters.calculate.attributeValue(charId, skill.ability);
//base modifier
var mod = +getMod(ability);
//multiply proficiency bonus by largest value in proficiency array
var prof = Characters.calculate.proficiency(charId, skillName);
//add multiplied proficiency bonus to modifier
mod += prof * Characters.calculate.attributeValue(charId, "proficiencyBonus");
//apply all effects
var value;
var add = 0;
var mul = 1;
var min = Number.NEGATIVE_INFINITY;
var max = Number.POSITIVE_INFINITY;
Effects.find({
charId: charId,
stat: skillName,
enabled: true,
operation: {$in: ["base", "add", "mul", "min", "max"]},
}).forEach(function(effect) {
value = evaluateEffect(charId, effect);
if (effect.operation === "add"){
add += value;
} else if (effect.operation === "mul"){
mul *= value;
} else if (effect.operation === "min"){
if (value > min) min = value;
} else if (effect.operation === "max"){
if (value < max) max = value;
}
});
var result = (mod + add) * mul;
if (result < min) result = min;
if (result > max) result = max;
return Math.floor(result);
})),
proficiency: memoize(function(charId, skillName){
//return largest value in proficiency array
var prof = Proficiencies.findOne(
{charId: charId, name: skillName, enabled: true},
{sort: {value: -1}}
);
return prof && prof.value || 0;
}),
passiveSkill: memoize(function(charId, skillName){
var mod = +Characters.calculate.skillMod(charId, skillName);
var value = 10 + mod;
Effects.find(
{charId: charId, stat: skillName, enabled: true, operation: "passiveAdd"}
).forEach(function(effect){
value += evaluateEffect(charId, effect);
});
var advantage = Characters.calculate.advantage(charId, skillName);
value += 5 * advantage;
return Math.floor(value);
}),
advantage: memoize(function(charId, skillName){
var advantage = Effects.find(
{charId: charId, stat: skillName, enabled: true, operation: "advantage"}
).count();
var disadvantage = Effects.find(
{charId: charId, stat: skillName, enabled: true, operation: "disadvantage"}
).count();
if (advantage && !disadvantage) return 1;
if (disadvantage && !advantage) return -1;
return 0;
}),
abilityMod: function(charId, attribute){
return getMod(
Characters.calculate.attributeValue(charId, attribute)
);
},
passiveAbility: function(charId, attribute){
var mod = +getMod(Characters.calculate.attributeValue(charId, attribute));
return 10 + mod;
},
xpLevel: function(charId){
var xp = Characters.calculate.experience(charId);
for (var i = 0; i < 19; i++){
if (xp < XP_TABLE[i]){
return i;
}
}
if (xp > 355000) return 20;
return 0;
},
level: memoize(function(charId){
var level = 0;
Classes.find({charId: charId}).forEach(function(cls){
level += cls.level;
});
return level;
}),
experience: memoize(function(charId){
var xp = 0;
Experiences.find(
{charId: charId},
{fields: {value: 1}}
).forEach(function(e){
xp += e.value;
});
return xp;
}),
};
var deprecated = function() {
//var err = new Error("this function has been deprecated");
var name = "";
if (Template.instance()){
name = Template.instance().view.name;
}
var logString = "this function has been deprecated \n";
if (name){
logString += "View: " + name + "\n\n";
}
//logString += err.stack + "\n\n---------------------\n\n";
console.log(logString);
};
//functions and calculated values.
//These functions can only rely on this._id since no other
//field is likely to be attached to all returned characters
Characters.helpers({
//returns the value stored in the field requested
//will set up dependencies on just that field
getField : function(fieldName){
deprecated();
return Characters.calculate.getField(this._id, fieldName);
},
//returns the value of a field
fieldValue : function(fieldName){
deprecated();
return Characters.calculate.fieldValue(this._id, fieldName);
},
attributeValue: function(attributeName){
deprecated();
return Characters.calculate.attributeValue(this._id, attributeName);
},
attributeBase: function(attributeName){
deprecated();
return Characters.calculate.attributeBase(this._id, attributeName);
},
skillMod: function(skillName){
deprecated();
return Characters.calculate.skillMod(this._id, skillName);
},
proficiency: function(skillName){
deprecated();
return Characters.calculate.proficiency(this._id, skillName);
},
passiveSkill: function(skillName){
deprecated();
return Characters.calculate.passiveSkill(this._id, skillName);
},
advantage: function(skillName){
deprecated();
return Characters.calculate.advantage(this._id, skillName);
},
abilityMod: function(attribute){
deprecated();
return Characters.calculate.abilityMod(this._id, attribute);
},
passiveAbility: function(attribute){
deprecated();
return Characters.calculate.passiveAbility(this._id, attribute);
},
xpLevel: function(){
deprecated();
return Characters.calculate.xpLevel(this._id);
},
level: function(){
deprecated();
return Characters.calculate.level(this._id);
},
experience: function(){
deprecated();
return Characters.calculate.experience(this._id);
},
});
//clean up all data related to that character before removing it
if (Meteor.isServer){
Characters.after.remove(function(userId, character) {
Actions .remove({charId: character._id});
Attacks .remove({charId: character._id});
Buffs .remove({charId: character._id});
Classes .remove({charId: character._id});
CustomBuffs .remove({charId: character._id});
Effects .remove({charId: character._id});
Experiences .remove({charId: character._id});
Features .remove({charId: character._id});
Notes .remove({charId: character._id});
Proficiencies .remove({charId: character._id});
SpellLists .remove({charId: character._id});
Items .remove({charId: character._id});
Containers .remove({charId: character._id});
});
Characters.after.update(function(userId, doc, fieldNames, modifier, options) {
if (_.contains(fieldNames, "name")){
var urlName = getSlug(doc.name, {maintainCase: true}) || "-";
Characters.update(doc._id, {$set: {urlName}});
}
});
Characters.before.insert(function(userId, doc) {
doc.urlName = getSlug(doc.name, {maintainCase: true}) || "-";
// The first character a user creates should have the new user experience
if (!Characters.find({owner: userId}).count()){
doc.settings.newUserExperience = true;
}
});
}
Characters.allow({
insert: function(userId, doc) {
// the user must be logged in, and the document must be owned by the user
return (userId && doc.owner === userId);
},
update: function(userId, doc, fields, modifier) {
// can only change documents you have write access to
return doc.owner === userId ||
_.contains(doc.writers, userId);
},
remove: function(userId, doc) {
// can only remove your own documents
return doc.owner === userId;
},
fetch: ["owner", "writers"],
});
Characters.deny({
update: function(userId, doc, fields, modifier) {
// can't change owners unless you are the current owner
return _.contains(fields, "owner") && doc.owner !== userId;
}
});

View File

@@ -0,0 +1,32 @@
Classes = new Mongo.Collection("classes");
Schemas.Class = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, optional: true, trim: false},
level: {type: Number},
createdAt: {
type: Date,
autoValue: function() {
if (this.isInsert) {
return new Date();
} else if (this.isUpsert) {
return {$setOnInsert: new Date()};
} else {
this.unset();
}
},
},
color: {
type: String,
allowedValues: _.pluck(colorOptions, "key"),
defaultValue: "q",
},
});
Classes.attachSchema(Schemas.Class);
Classes.attachBehaviour("softRemovable");
makeParent(Classes, "name"); //parents of effects and attacks
Classes.allow(CHARACTER_SUBSCHEMA_ALLOW);
Classes.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -0,0 +1,42 @@
Conditions = new Mongo.Collection("conditions");
Schemas.Conditions = new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
name: {
type: String,
optional: true,
trim: false,
},
description: {
type: String,
optional: true,
trim: false,
},
"lifeTime.total": {
type: Number,
defaultValue: 0, //0 is infinite
min: 0,
},
"lifeTime.spent": {
type: Number,
defaultValue: 0,
min: 0,
},
color: {
type: String,
allowedValues: _.pluck(colorOptions, "key"),
defaultValue: "q",
},
});
Conditions.attachSchema(Schemas.Conditions);
Conditions.attachBehaviour("softRemovable");
makeParent(Conditions, ["name"]); //parents of effects, attacks, proficiencies
Conditions.allow(CHARACTER_SUBSCHEMA_ALLOW);
Conditions.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -0,0 +1,53 @@
CustomBuffs = new Mongo.Collection("customBuffs");
Schemas.CustomBuff = new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
name: {
type: String,
optional: true,
trim: false,
},
description: {
type: String,
optional: true,
trim: false,
},
target: {
type: String,
allowedValues: [
"self",
"others",
"both"
],
defaultValue: "self",
},
enabled: {
type: Boolean,
autoValue: function(){
return false;
//enabled is ALWAYS false on these, so that its children are also not enabled, so that the buff templates have no effects.
},
},
"lifeTime.total": {
type: Number,
defaultValue: 0, //0 is infinite
min: 0,
},
//the id of the feature, buff or item that creates this buff
parent: {
type: Schemas.Parent,
},
});
CustomBuffs.attachSchema(Schemas.CustomBuff);
CustomBuffs.attachBehaviour("softRemovable");
makeParent(CustomBuffs, ["name", "enabled"]); //parents of effects, attacks, proficiencies. Since this represents a template, "enabled" is always false.
makeChild(CustomBuffs); //children of lots of things
CustomBuffs.allow(CHARACTER_SUBSCHEMA_ALLOW);
CustomBuffs.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -0,0 +1,132 @@
Effects = new Mongo.Collection("effects");
/*
* Effects are reason-value attached to skills and abilities
* that modify their final value or presentation in some way
*/
Schemas.Effect = new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
name: {
type: String,
optional: true, //TODO make necessary if there is no owner
trim: false,
},
operation: {
type: String,
defaultValue: "add",
allowedValues: [
"base",
"proficiency",
"add",
"mul",
"min",
"max",
"advantage",
"disadvantage",
"passiveAdd",
"fail",
"conditional",
],
},
value: {
type: Number,
decimal: true,
optional: true,
},
calculation: {
type: String,
optional: true,
trim: false,
},
//the thing that created this effect
parent: {
type: Schemas.Parent
},
//which stat the effect is applied to
stat: {
type: String,
optional: true,
},
enabled: {
type: Boolean,
defaultValue: true,
},
});
Effects.attachSchema(Schemas.Effect);
Effects.attachBehaviour("softRemovable");
makeChild(Effects, ["enabled"]); //children of lots of things
Effects.allow(CHARACTER_SUBSCHEMA_ALLOW);
Effects.deny(CHARACTER_SUBSCHEMA_DENY);
//give characters default character effects
Characters.after.insert(function(userId, char) {
if (Meteor.isServer) {
Effects.insert({
charId: char._id,
name: "Constitution modifier for each level",
stat: "hitPoints",
operation: "add",
calculation: "level * constitutionMod",
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
Effects.insert({
charId: char._id,
name: "Proficiency bonus by level",
stat: "proficiencyBonus",
operation: "add",
calculation: "floor(level / 4 + 1.75)",
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
Effects.insert({
charId: char._id,
name: "Dexterity Armor Bonus",
stat: "armor",
operation: "add",
calculation: "dexterityArmor",
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
Effects.insert({
charId: char._id,
name: "Natural Armor",
stat: "armor",
operation: "base",
value: 10,
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
Effects.insert({
charId: char._id,
name: "Natural Carrying Capacity",
stat: "carryMultiplier",
operation: "base",
value: "1",
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
}
});

View File

@@ -0,0 +1,27 @@
Experiences = new Mongo.Collection("experience");
Schemas.Experience = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, optional: true, trim: false, defaultValue: "New Experience"},
description: {type: String, optional: true, trim: false},
value: {type: Number, defaultValue: 0},
dateAdded: {
type: Date,
autoValue: function() {
if (this.isInsert) {
return new Date();
} else if (this.isUpsert) {
return {$setOnInsert: new Date()};
} else {
this.unset();
}
},
},
});
Experiences.attachSchema(Schemas.Experience);
Experiences.attachBehaviour("softRemovable");
Experiences.allow(CHARACTER_SUBSCHEMA_ALLOW);
Experiences.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -0,0 +1,115 @@
Features = new Mongo.Collection("features");
Schemas.Feature = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, optional: true, trim: false},
description: {type: String, optional: true, trim: false},
uses: {type: String, optional: true, trim: false},
used: {type: Number, defaultValue: 0},
reset: {
type: String,
allowedValues: ["manual", "longRest", "shortRest"],
defaultValue: "manual",
},
enabled: {type: Boolean, defaultValue: true},
alwaysEnabled:{type: Boolean, defaultValue: true},
color: {type: String,
allowedValues: _.pluck(colorOptions, "key"),
defaultValue: "q",
},
});
Features.attachSchema(Schemas.Feature);
Features.helpers({
usesLeft: function(){
return evaluate(this.charId, this.uses) - this.used;
},
usesValue: function(){
return evaluate(this.charId, this.uses);
},
});
Features.attachBehaviour("softRemovable");
makeParent(Features, ["name", "enabled"]); //parents of effects and attacks
Features.allow(CHARACTER_SUBSCHEMA_ALLOW);
Features.deny(CHARACTER_SUBSCHEMA_DENY);
//give characters default feature of base ability scores of 10
Characters.after.insert(function(userId, char) {
if (Meteor.isServer){
var featureId = Features.insert({
name: "Base Ability Scores",
charId: char._id,
enabled: true,
alwaysEnabled: true,
});
Effects.insert({
stat: "strength",
charId: char._id,
parent: {
id: featureId,
collection: "Features",
},
operation: "base",
value: 10,
enabled: true,
});
Effects.insert({
stat: "dexterity",
charId: char._id,
parent: {
id: featureId,
collection: "Features",
},
operation: "base",
value: 10,
enabled: true,
});
Effects.insert({
stat: "constitution",
charId: char._id,
parent: {
id: featureId,
collection: "Features",
},
operation: "base",
value: 10,
enabled: true,
});
Effects.insert({
stat: "intelligence",
charId: char._id,
parent: {
id: featureId,
collection: "Features",
},
operation: "base",
value: 10,
enabled: true,
});
Effects.insert({
stat: "wisdom",
charId: char._id,
parent: {
id: featureId,
collection: "Features",
},
operation: "base",
value: 10,
enabled: true,
});
Effects.insert({
stat: "charisma",
charId: char._id,
parent: {
id: featureId,
collection: "Features",
},
operation: "base",
value: 10,
enabled: true,
});
}
});

View File

@@ -0,0 +1,19 @@
Notes = new Mongo.Collection("notes");
Schemas.Note = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, optional: true, trim: false},
description: {type: String, optional: true, trim: false},
color: {
type: String,
allowedValues:_.pluck(colorOptions, "key"),
defaultValue: "q",
},
});
Notes.attachSchema(Schemas.Note);
Notes.attachBehaviour("softRemovable");
Notes.allow(CHARACTER_SUBSCHEMA_ALLOW);
Notes.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -0,0 +1,37 @@
Proficiencies = new Mongo.Collection("proficiencies");
Schemas.Proficiency = new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
name: {
type: String,
trim: false,
optional: true,
},
value: {
type: Number,
allowedValues: [0, 0.5, 1, 2],
defaultValue: 1,
decimal: true,
},
type: {
type: String,
allowedValues: ["skill", "save", "weapon", "armor", "tool", "language"],
defaultValue: "skill",
},
enabled: {
type: Boolean,
defaultValue: true,
},
});
Proficiencies.attachSchema(Schemas.Proficiency);
Proficiencies.attachBehaviour("softRemovable");
makeChild(Proficiencies, ["enabled"]);
Proficiencies.allow(CHARACTER_SUBSCHEMA_ALLOW);
Proficiencies.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -0,0 +1,37 @@
SpellLists = new Mongo.Collection("spellLists");
Schemas.SpellLists = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, optional: true, trim: false},
description: {type: String, optional: true, trim: false},
saveDC: {type: String, optional: true, trim: false},
attackBonus: {type: String, optional: true, trim: false},
maxPrepared: {type: String, optional: true, trim: false},
color: {
type: String,
allowedValues: _.pluck(colorOptions, "key"),
defaultValue: "q",
},
"settings.showUnprepared": {type: Boolean, defaultValue: true},
});
SpellLists.attachSchema(Schemas.SpellLists);
SpellLists.helpers({
numPrepared: function(){
var num = 0;
Spells.find(
{charId: this.charId, listId: this._id, prepared: 1},
{fields: {prepareCost: 1}}
).forEach(function(spell){
num += spell.prepareCost;
});
return num;
}
});
SpellLists.attachBehaviour("softRemovable");
makeParent(SpellLists); //parents of spells
SpellLists.allow(CHARACTER_SUBSCHEMA_ALLOW);
SpellLists.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -0,0 +1,252 @@
Spells = new Mongo.Collection("spells");
Schemas.Spell = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
prepared: {
type: String,
defaultValue: "prepared",
allowedValues: ["prepared", "unprepared", "always"],
},
name: {
type: String,
optional: true,
trim: false,
defaultValue: "New Spell",
},
description: {
type: String,
optional: true,
trim: false,
},
castingTime: {
type: String,
optional: true,
defaultValue: "action",
trim: false,
},
range: {
type: String,
optional: true,
trim: false,
},
duration: {
type: String,
optional: true,
trim: false,
defaultValue: "Instantaneous",
},
"components.verbal": {type: Boolean, defaultValue: false},
"components.somatic": {type: Boolean, defaultValue: false},
"components.concentration": {type: Boolean, defaultValue: false},
"components.material": {type: String, optional: true},
ritual: {
type: Boolean,
defaultValue: false,
},
level: {
type: Number,
defaultValue: 1,
},
school: {
type: String,
defaultValue: "Abjuration",
allowedValues: magicSchools,
},
color: {
type: String,
allowedValues: _.pluck(colorOptions, "key"),
defaultValue: "q",
},
});
Spells.attachSchema(Schemas.Spell);
Spells.attachBehaviour("softRemovable");
makeChild(Spells); //children of spell lists
makeParent(Spells, ["name", "enabled"]); //parents of attacks
Spells.after.update(function (userId, spell, fieldNames) {
//Update prepared state of spell and child attacks to be enabled or not
if (_.contains(fieldNames, "prepared")) {
var childAttacks = Attacks.find({"parent.id": spell._id}).fetch();
if (spell.prepared === "unprepared") {
_.each(childAttacks, function(attack){
Attacks.update(attack._id, {$set: {enabled: false}});
});
} else if (spell.prepared === "prepared" || "always") {
_.each(childAttacks, function(attack){
Attacks.update(attack._id, {$set: {enabled: true}});
});
}
}
});
Spells.allow(CHARACTER_SUBSCHEMA_ALLOW);
Spells.deny(CHARACTER_SUBSCHEMA_DENY);
var checkMovePermission = function(spellId, parent, destinationOnly) {
var spell = Spells.findOne(spellId);
if (!spell)
throw new Meteor.Error("No such spell",
"An spell could not be found to move");
//handle permissions
var permission;
if (!destinationOnly) { //if we're not modifying the origin, only the destination
permission = Meteor.call("canWriteCharacter", spell.charId);
if (!permission){
throw new Meteor.Error("Access denied",
"Not permitted to move spells from this character");
}
}
if (parent.collection === "Characters"){
permission = Meteor.call("canWriteCharacter", parent.id);
if (!permission){
throw new Meteor.Error("Access denied",
"Not permitted to move spells to this character");
}
} else {
var parentCollectionObject = global[parent.collection];
var parentObject = null;
if (parentCollectionObject)
parentObject = parentCollectionObject.findOne(
parent.id, {fields: {_id: 1, charId: 1}}
);
if (!parentObject) throw new Meteor.Error(
"Invalid parent",
"The destination parent " + parent.id +
" does not exist in the collection " + parent.collection
);
if (parentObject.charId){
permission = Meteor.call("canWriteCharacter", parentObject.charId);
if (!permission){
throw new Meteor.Error("Access denied",
"Not permitted to move spells to this character");
}
}
}
};
var moveSpell = function(spellId, targetCollection, targetId) {
var spell = Spells.findOne(spellId);
if (!spell) return;
targetCollection = targetCollection || spell.parent.collection;
targetId = targetId || spell.parent.id;
if (Meteor.isServer) {
checkMovePermission(spellId, {collection: targetCollection, id: targetId}, false);
}
if (targetCollection == "Characters") { //then we are copying the spell to a different character.
targetList = SpellLists.findOne({"charId": targetId});
targetListId = targetList && targetList._id;
if (!targetListId) {
targetListId = SpellLists.insert({ //create a spell list if we don't already have one
name: "New SpellList",
charId: targetId,
saveDC: "8 + intelligenceMod + proficiencyBonus",
attackBonus: "intelligenceMod + proficiencyBonus",
});
}
Spells.update(
spellId,
{$set: {
charId: targetId,
"parent.collection": "SpellLists",
"parent.id": targetListId,
}}
);
}
else { //we are moving the spell within the same character
//update the spell provided the update will actually change something
if (
spell.parent.collection !== targetCollection ||
spell.parent.id !== targetId
){
Spells.update(
spellId,
{$set: {
"parent.collection": targetCollection,
"parent.id": targetId,
}}
);
}
}
};
var copySpell = function(spellId, targetCollection, targetId) {
var spell = Spells.findOne(spellId);
if (!spell) return;
targetCollection = targetCollection || spell.parent.collection;
targetId = targetId || spell.parent.id;
if (Meteor.isServer) {
checkMovePermission(spellId, {collection: targetCollection, id: targetId}, true); //we're only reading from the source character
}
if (targetCollection == "Characters") { //then we are copying the spell to a different character.
targetList = SpellLists.findOne({"charId": targetId});
targetListId = targetList && targetList._id;
if (!targetListId) {
targetListId = SpellLists.insert({ //create a spell list if we don't already have one
name: "New SpellList",
charId: targetId,
saveDC: "8 + intelligenceMod + proficiencyBonus",
attackBonus: "intelligenceMod + proficiencyBonus",
});
}
newSpell = _.clone(spell);
delete newSpell._id;
newSpellId = Spells.insert(newSpell); //add a new copy of the spell
Spells.update(
newSpellId,
{$set: {
charId: targetId,
"parent.collection": "SpellLists",
"parent.id": targetListId,
}}
);
}
else { //else we are copying the spell within the same character
newSpell = _.clone(spell);
delete newSpell._id;
newSpellId = Spells.insert(newSpell); //add a new copy of the spell
Spells.update(
newSpellId,
{$set: {
"parent.collection": targetCollection,
"parent.id": targetId,
}}
);
}
};
Meteor.methods({
moveSpellToList: function(spellId, spellListId) {
check(spellId, String);
check(spellListId, String);
moveSpell(spellId, "SpellLists", spellListId);
},
copySpellToList: function(spellId, spellListId) {
check(spellId, String);
check(spellListId, String);
copySpell(spellId, "SpellLists", spellListId);
},
moveSpellToCharacter: function(spellId, charId) {
check(spellId, String);
check(charId, String);
moveSpell(spellId, "Characters", charId);
},
copySpellToCharacter: function(spellId, charId) {
check(spellId, String);
check(charId, String);
copySpell(spellId, "Characters", charId);
},
});

View File

@@ -0,0 +1,21 @@
/*
* Adjustments make instantaneous changes to the value of some attribute
* Damage, healing and resource cost/recovery are all adjustments
*/
Schemas.Adjustment = new SimpleSchema({
//which stat the adjustment is applied to
stat: {
type: String,
optional: true,
},
//the value added to the stat
value: {
type: Number,
decimal: true,
optional: true,
},
calculation: {
type: String,
optional: true,
},
});

View File

@@ -0,0 +1,13 @@
Schemas.Attribute = new SimpleSchema({
//the temporary shift of the attribute
//should be zero after reset
adjustment: {
type: Number,
defaultValue: 0,
},
reset: {
type: String,
defaultValue: "longRest",
allowedValues: ["longRest", "shortRest"],
},
});

View File

@@ -1,14 +1,12 @@
import SimpleSchema from 'simpl-schema';
const DeathSavesSchema = new SimpleSchema({
Schemas.DeathSave = new SimpleSchema({
pass: {
type: SimpleSchema.Integer,
type: Number,
min: 0,
max: 3,
defaultValue: 0,
},
fail: {
type: SimpleSchema.Integer,
type: Number,
min: 0,
max: 3,
defaultValue: 0,
@@ -22,5 +20,3 @@ const DeathSavesSchema = new SimpleSchema({
defaultValue: false,
},
});
export default DeathSavesSchema;

View File

@@ -0,0 +1,4 @@
Schemas.Skill = new SimpleSchema({
//attribute name that this skill used as base mod for roll
ability: {type: String, defaultValue: ""},
});

View File

@@ -0,0 +1,41 @@
TemporaryHitPoints = new Mongo.Collection("temporaryHitPoints");
Schemas.TemporaryHitPoints = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, optional: true},
maximum: {type: Number, defaultValue: 0, min: 0, max: 500},
used: {type: Number, defaultValue: 0, min: 0, max: 500},
deleteOnZero:{type: Boolean, defaultValue: false},
dateAdded: {
type: Date,
autoValue: function() {
if (this.isInsert) {
return new Date();
} else if (this.isUpsert) {
return {$setOnInsert: new Date()};
} else {
this.unset();
}
},
},
});
TemporaryHitPoints.attachSchema(Schemas.TemporaryHitPoints);
TemporaryHitPoints.helpers({
left: function(){
return this.maximum - this.used;
}
});
//remove the temporary hit points when they hit zero
TemporaryHitPoints.after.update(
function(userId, thp, fieldNames, modifier, options){
if (thp.used >= thp.maximum && thp.deleteOnZero){
TemporaryHitPoints.remove(thp._id);
}
}, {fetchPrevious: false}
);
TemporaryHitPoints.allow(CHARACTER_SUBSCHEMA_ALLOW);
TemporaryHitPoints.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -0,0 +1,56 @@
//set up the collection for containers
Containers = new Mongo.Collection("containers");
Schemas.Container = new SimpleSchema({
name: {type: String, optional: true, trim: false},
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
isCarried: {type: Boolean},
weight: {type: Number, min: 0, defaultValue: 0, decimal: true},
value: {type: Number, min: 0, defaultValue: 0, decimal: true},
description:{type: String, optional: true, trim: false},
color: {
type: String,
allowedValues: _.pluck(colorOptions, "key"),
defaultValue: "q",
},
});
Containers.attachSchema(Schemas.Container);
Containers.helpers({
contentsValue: function(){
var value = 0;
Items.find(
{"parent.id": this._id},
{fields: {quantity: 1, value: 1}}
).forEach(function(item){
value += item.totalValue();
});
return value;
},
totalValue: function(){
return this.contentsValue() + this.value;
},
contentsWeight: function(){
var weight = 0;
Items.find(
{"parent.id": this._id},
{fields: {quantity: 1, weight: 1}}
).forEach(function(item){
weight += item.totalWeight();
});
return weight;
},
totalWeight: function(){
return this.contentsWeight() + this.weight;
},
moveToCharacter: function(characterId){
if (this.charId === characterId) return;
Items.update(this._id, {$set: {charId: characterId}});
},
});
Containers.attachBehaviour("softRemovable");
makeParent(Containers); //parents of items
Containers.allow(CHARACTER_SUBSCHEMA_ALLOW);

View File

@@ -0,0 +1,268 @@
Items = new Mongo.Collection("items");
Schemas.Item = new SimpleSchema({
name: {type: String, optional: true, trim: false, defaultValue: "New Item"},
plural: {type: String, optional: true, trim: false},
description:{type: String, optional: true, trim: false},
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, //id of owner
quantity: {type: Number, min: 0, defaultValue: 1},
weight: {type: Number, min: 0, defaultValue: 0, decimal: true},
value: {type: Number, min: 0, defaultValue: 0, decimal: true},
enabled: {type: Boolean, defaultValue: false},
requiresAttunement: {type: Boolean, defaultValue: false},
"settings.showIncrement": {type: Boolean, defaultValue: false},
color: {
type: String,
allowedValues: _.pluck(colorOptions, "key"),
defaultValue: "q",
},
});
Items.attachSchema(Schemas.Item);
var checkMovePermission = function(itemId, parent) {
var item = Items.findOne(itemId);
if (!item)
throw new Meteor.Error("No such item",
"An item could not be found to move");
//handle permissions
var permission = Meteor.call("canWriteCharacter", item.charId);
if (!permission){
throw new Meteor.Error("Access denied",
"Not permitted to move items from this character");
}
if (parent.collection === "Characters"){
permission = Meteor.call("canWriteCharacter", parent.id);
if (!permission){
throw new Meteor.Error("Access denied",
"Not permitted to move items to this character");
}
} else {
var parentCollectionObject = global[parent.collection];
var parentObject = null;
if (parentCollectionObject)
parentObject = parentCollectionObject.findOne(
parent.id, {fields: {_id: 1, charId: 1}}
);
if (!parentObject) throw new Meteor.Error(
"Invalid parent",
"The destination parent " + parent.id +
" does not exist in the collection " + parent.collection
);
if (parentObject.charId){
permission = Meteor.call("canWriteCharacter", parentObject.charId);
if (!permission){
throw new Meteor.Error("Access denied",
"Not permitted to move items to this character");
}
}
}
};
var moveItem = function(itemId, enable, parentCollection, parentId) {
var item = Items.findOne(itemId);
if (!item) return;
parentCollection = parentCollection || item.parent.collection;
parentId = parentId || item.parent.id;
if (Meteor.isServer) {
checkMovePermission(itemId, {collection: parentCollection, id: parentId});
}
//update the item provided the update will actually change something
if (
item.parent.collection !== parentCollection ||
item.parent.id !== parentId ||
item.enabled !== enable
){
Items.update(
itemId,
{$set: {
"parent.collection": parentCollection,
"parent.id": parentId,
enabled: enable,
}}
);
}
};
Meteor.methods({
moveItemToParent: function(itemId, parent) {
check(itemId, String);
check(parent, {collection: String, id: String});
moveItem(itemId, false, parent.collection, parent.id);
},
moveItemToCharacter: function(itemId, charId) {
check(itemId, String);
check(charId, String);
moveItem(itemId, false, "Characters", charId);
},
moveItemToContainer: function(itemId, containerId) {
check(itemId, String);
check(containerId, String);
moveItem(itemId, false, "Containers", containerId);
},
equipItem: function(itemId, charId){
check(itemId, String);
check(charId, String);
moveItem(itemId, true, "Characters", charId);
},
unequipItem: function(itemId, charId){
check(itemId, String);
check(charId, String);
moveItem(itemId, false, "Characters", charId);
},
splitItemToParent: function(itemId, moveQuantity, parent){
check(itemId, String);
check(moveQuantity, Number);
check(parent, {id: String, collection: String});
//get the item
var item = Items.findOne(itemId);
if (!item) return;
//don't bother moving nothing
if (moveQuantity <= 0 || item.quantity <= 0){
return;
}
//ensure we are only moving up to the current stack size
if (item.quantity < moveQuantity){
moveQuantity = this.quantity;
}
if (Meteor.isServer) {
checkMovePermission(itemId, parent);
}
//create a new item stack
var newStack = _.omit(EJSON.clone(item), "_id");
newStack.parent = parent;
newStack.quantity = moveQuantity;
//find out if we have an exact replica in the destination
var query = _.omit(newStack, ["parent", "quantity"]);
query["parent.collection"] = newStack.parent.collection;
query["parent.id"] = newStack.parent.id;
query._id = {$ne: itemId}; //make sure we don't join it to itself
var existingStack = Items.findOne(query);
if (existingStack){
//increase the existing stack's size
Items.update(
existingStack._id,
{$inc: {quantity: moveQuantity}}
);
} else {
//insert the new stack
Items.insert(newStack, function(err, id){
if (err) throw err;
//copy the children also
Meteor.call("cloneChildren", item._id, {collection: "Items", id: id});
});
}
//reduce the old stack's size
var oldQuantity = item.quantity - moveQuantity;
if (oldQuantity === 0){
Items.remove(itemId);
} else {
Items.update(itemId, {$set: {quantity: oldQuantity}});
}
},
});
Items.helpers({
totalValue: function(){
return this.value * this.quantity;
},
totalWeight: function(){
return this.weight * this.quantity;
},
pluralName: function(){
if (this.plural && this.quantity !== 1){
return this.plural;
} else {
return this.name;
}
},
});
Items.before.update(function(userId, doc, fieldNames, modifier, options){
if (
modifier && modifier.$set && modifier.$set.enabled && //we are equipping this item
!(
modifier.$set["parent.collection"] === "Characters" &&
modifier.$set["parent.id"]
) //and we haven"t specified a character to equip to
){
//equip it to the current character
modifier.$set["parent.collection"] = "Characters";
modifier.$set["parent.id"] = doc.charId;
}
});
Items.attachBehaviour("softRemovable");
makeChild(Items); //children of containers
makeParent(Items, ["name", "enabled"]); //parents of effects and attacks
Items.allow(CHARACTER_SUBSCHEMA_ALLOW);
//give characters default items
Characters.after.insert(function(userId, char) {
if (Meteor.isServer){
var containerId = Containers.insert({
name: "Coin Pouch",
charId: char._id,
isCarried: true,
description: "A sturdy pouch for coins",
color: "d",
});
Items.insert({
name: "Gold piece",
plural: "Gold pieces",
charId: char._id,
quantity: 0,
weight: 0.02,
value: 1,
color: "n",
parent: {
id: containerId,
collection: "Containers",
},
settings: {
showIncrement: true,
},
});
Items.insert({
name: "Silver piece",
plural: "Silver pieces",
charId: char._id,
quantity: 0,
weight: 0.02,
value: 0.1,
color: "q",
parent: {
id: containerId,
collection: "Containers",
},
settings: {
showIncrement: true,
},
});
Items.insert({
name: "Copper piece",
plural: "Copper pieces",
charId: char._id,
quantity: 0,
weight: 0.02,
value: 0.01,
color: "s",
parent: {
id: containerId,
collection: "Containers",
},
settings: {
showIncrement: true,
},
});
}
});

View File

@@ -0,0 +1,69 @@
Libraries = new Mongo.Collection("library");
Schemas.Library = new SimpleSchema({
name: {type: String},
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;
},
update(userId, doc, fields, modifier) {
return canEdit(userId, doc);
},
remove(userId, doc) {
return userId && doc.owner === userId;
},
fetch: ["owner", "writers"],
});
Libraries.deny({
insert(userId, doc){
return !Meteor.users.findOne(userId);
},
update(userId, doc, fields, modifier) {
// Can't change owners
return _.contains(fields, "owner")
},
fetch: [],
});
const canEdit = function(userId, library){
if (!userId || !library) return;
return library.owner === userId || _.contains(library.writers, userId);
};
Libraries.canEdit = function(userId, libraryId){
const library = Libraries.findOne(libraryId);
return canEdit(userId, library);
};

View File

@@ -0,0 +1,41 @@
Schemas.LibraryAttacks = new SimpleSchema({
name: {
type: String,
optional: true,
trim: false,
},
details: {
type: String,
optional: true,
trim: false,
},
attackBonus: {
type: String,
optional: true,
trim: false,
},
damage: {
type: String,
optional: true,
trim: false,
},
damageType: {
type: String,
allowedValues: [
"bludgeoning",
"piercing",
"slashing",
"acid",
"cold",
"fire",
"force",
"lightning",
"necrotic",
"poison",
"psychic",
"radiant",
"thunder",
],
defaultValue: "slashing",
},
});

View File

@@ -0,0 +1,39 @@
Schemas.LibraryEffects = new SimpleSchema({
name: {
type: String,
optional: true, //TODO make necessary if there is no owner
trim: false,
},
operation: {
type: String,
defaultValue: "add",
allowedValues: [
"base",
"add",
"mul",
"min",
"max",
"advantage",
"disadvantage",
"passiveAdd",
"fail",
"conditional",
],
},
// Effects either have a value OR a calculation
value: {
type: Number,
decimal: true,
optional: true,
},
calculation: {
type: String,
optional: true,
trim: false,
},
//which stat the effect is applied to
stat: {
type: String,
optional: true,
},
});

View File

@@ -0,0 +1,91 @@
LibraryItems = new Mongo.Collection("libraryItems");
Schemas.LibraryItems = new SimpleSchema({
libraryName:{type: String, optional: true, trim: false},
name: {type: String, defaultValue: "New Item", trim: false},
plural: {type: String, optional: true, trim: false},
description:{type: String, optional: true, trim: false},
quantity: {type: Number, min: 0, defaultValue: 1},
weight: {type: Number, min: 0, defaultValue: 0, decimal: true},
value: {type: Number, min: 0, defaultValue: 0, decimal: true},
requiresAttunement: {type: Boolean, defaultValue: false},
library: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
"settings.category": {
type: String,
optional: true,
allowedValues: [
"adventuringGear", "armor", "weapons", "tools",
],
},
"settings.showIncrement": {
type: Boolean,
defaultValue: false,
},
effects: {type: [Schemas.LibraryEffects], defaultValue: []},
attacks: {type: [Schemas.LibraryAttacks], defaultValue: []},
});
LibraryItems.attachSchema(Schemas.LibraryItems);
LibraryItems.allow({
insert(userId, doc) {
return Libraries.canEdit(userId, doc.library);
},
update(userId, doc, fields, modifier) {
return Libraries.canEdit(userId, doc.library);
},
remove(userId, doc) {
return Libraries.canEdit(userId, doc.library);
},
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}});
},
})

View File

@@ -0,0 +1,66 @@
LibrarySpells = new Mongo.Collection("librarySpells");
Schemas.LibrarySpells = new SimpleSchema({
name: {
type: String,
trim: false,
defaultValue: "New Spell",
},
description: {
type: String,
optional: true,
trim: false,
},
castingTime: {
type: String,
optional: true,
defaultValue: "action",
trim: false,
},
range: {
type: String,
optional: true,
trim: false,
},
duration: {
type: String,
optional: true,
trim: false,
defaultValue: "Instantaneous",
},
"components.verbal": {type: Boolean, defaultValue: false},
"components.somatic": {type: Boolean, defaultValue: false},
"components.concentration": {type: Boolean, defaultValue: false},
"components.material": {type: String, optional: true},
ritual: {
type: Boolean,
defaultValue: false,
},
level: {
type: Number,
defaultValue: 1,
},
school: {
type: String,
defaultValue: "Abjuration",
allowedValues: magicSchools,
},
library: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
effects: {type: [Schemas.LibraryEffects], defaultValue: []},
attacks: {type: [Schemas.LibraryAttacks], defaultValue: []},
});
LibrarySpells.attachSchema(Schemas.LibrarySpells);
LibrarySpells.allow({
insert(userId, doc) {
return Libraries.canEdit(userId, doc.library);
},
update(userId, doc, fields, modifier) {
return Libraries.canEdit(userId, doc.library);
},
remove(userId, doc) {
return Libraries.canEdit(userId, doc.library);
},
fetch: ["library"],
});

View File

@@ -1,5 +1,3 @@
import SimpleSchema from 'simpl-schema';
Blacklist = new Mongo.Collection("blacklist");
Schemas.Blacklist = new SimpleSchema({

View File

@@ -1,5 +1,3 @@
import SimpleSchema from 'simpl-schema';
ChangeLogs = new Mongo.Collection("changeLogs");
Schemas.ChangeLog = new SimpleSchema({

View File

@@ -1,5 +1,3 @@
import SimpleSchema from 'simpl-schema';
Reports = new Mongo.Collection("reports");
Schemas.Report = new SimpleSchema({
@@ -24,7 +22,7 @@ Schemas.Report = new SimpleSchema({
},
//the immediate impact of doing this action (eg. -1 rages)
severity: {
type: SimpleSchema.Integer,
type: Number,
defaultValue: 5,
min: 1,
max: 10,

109
app/Model/Users/Users.js Normal file
View File

@@ -0,0 +1,109 @@
Schemas.UserProfile = new SimpleSchema({
username: {
type: String,
optional: true,
},
librarySubscriptions: {
type: [String],
defaultValue: [],
},
});
Schemas.User = new SimpleSchema({
username: {
type: String,
optional: true,
},
profile: {
type: Schemas.UserProfile,
optional: true,
},
emails: {
type: Array,
optional: true,
},
"emails.$": {
type: Object,
},
"emails.$.address": {
type: String,
regEx: SimpleSchema.RegEx.Email,
},
"emails.$.verified": {
type: Boolean,
},
registered_emails: {
type: Array,
optional: true,
},
"registered_emails.$": {
type: Object,
blackbox: true,
},
createdAt: {
type: Date
},
services: {
type: Object,
optional: true,
blackbox: true,
},
roles: {
type: Object,
optional: true,
blackbox: true,
},
roles: {
type: Array,
optional: true,
},
"roles.$": {
type: String
},
// In order to avoid an 'Exception in setInterval callback' from Meteor
heartbeat: {
type: Date,
optional: true,
},
apiKey: {
type: String,
index: 1,
optional: true,
},
});
Meteor.users.attachSchema(Schemas.User);
Meteor.users.allow({
update: function(userId, doc, fields, modifier) {
if (
doc._id === userId &&
_.contains(fields, "username") &&
_.contains(fields, "profile") &&
fields.length === 2 &&
_.keys(modifier).length === 1 &&
modifier.$set &&
modifier.$set["profile.username"] &&
modifier.$set.username &&
_.keys(modifier.$set).length === 2
){
var expectedUsername = modifier.$set["profile.username"];
expectedUsername = expectedUsername.toLowerCase().replace(/\s+/gm, "");
if (modifier.$set.username !== expectedUsername){
return false;
}
var foundUser = Meteor.call("getUserId", expectedUsername);
return !foundUser || foundUser === userId;
}
}
});
if (Meteor.isServer) Meteor.methods({
generateMyApiKey() {
var user = Meteor.users.findOne(this.userId);
if (!user) return;
if (user && user.apiKey) return;
var apiKey = Random.id(30);
Meteor.users.update(this.userId, {$set: {apiKey}});
},
});

91
app/Routes/API.js Normal file
View File

@@ -0,0 +1,91 @@
Router.map(function() {
this.route("vmixCharacter", {
path: "/vmix-character/:_id/",
where: "server",
action: function() {
this.response.setHeader("Content-Type", "application/json");
var query = this.params.query;
var key = query && query.key;
ifKeyValid(key, this.response, "vmixCharacter", () =>
this.response.end(vMixCharacter(this.params._id))
);
},
});
this.route("vmixParty", {
path: "/vmix-party/:_id/",
where: "server",
action: function() {
this.response.setHeader("Content-Type", "application/json");
var query = this.params.query;
var key = query && query.key;
ifKeyValid(key, this.response, "vmixParty", () =>
this.response.end(vMixParty(this.params._id))
);
},
});
this.route("jsonCharacterSheet", {
path: "/character/:_id/json",
where: "server",
action: function() {
this.response.setHeader("Content-Type", "application/json");
var query = this.params.query;
var key = query && query.key;
ifKeyValid(key, this.response, "jsonCharacterSheet", () => {
if (canViewCharacter(this.params._id, userIdFromKey(key))){
this.response.end(JSONExport(this.params._id))
} else {
this.response.writeHead(403, "You do not have permission to view this character");
this.response.end();
}
}
);
},
});
});
var ifKeyValid = function(apiKey, response, method, callback){
if (!apiKey){
response.writeHead(403, "You must use an api key to access this api");
response.end();
} else if (!isKeyValid(apiKey)){
response.writeHead(403, "API key is invalid");
response.end();
} else if (isRateLimited(apiKey, method)){
response.writeHead(429, "Too many requests");
response.end(JSON.stringify({
"timeToReset": rateLimiter.check({apiKey: apiKey, method: method}).timeToReset
}));
} else {
rateLimiter.increment({apiKey: apiKey, method: method})
callback();
}
};
var isKeyValid = function(apiKey){
var user = Meteor.users.findOne({apiKey});
if (!user) return false;
var blackListed = Blacklist.findOne({userId: user._id});
return !blackListed;
};
var userIdFromKey = function(apiKey){
var user = Meteor.users.findOne({apiKey}); // we know user exists from isKeyValid
return user._id;
}
var rateLimiter = new RateLimiter();
rateLimiter.addRule({apiKey: String}, 5, 5000);
rateLimiter.addRule({apiKey: String, method: "vmixCharacter"}, 2, 10000);
rateLimiter.addRule({apiKey: String, method: "vmixParty"}, 2, 10000);
rateLimiter.addRule({apiKey: String, method: "jsonCharacterSheet"}, 5, 5000);
var isRateLimited = function(apiKey, method){
const limited = !rateLimiter.check({apiKey: apiKey, method: method}).allowed
if (limited) {
console.log(`Rate limit hit by API key ${apiKey}`);
return true;
} else {
return false;
}
};

174
app/Routes/Routes.js Normal file
View File

@@ -0,0 +1,174 @@
Router.configure({
loadingTemplate: "loading",
layoutTemplate: "layout",
trackPageView: true,
});
Router.plugin("ensureSignedIn", {
only: [
"profile",
"characterList",
]
});
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",
onAfterAction: function() {
document.title = appName;
},
});
this.route("characterList", {
path: "/characterList",
waitOn: function(){
return subsManager.subscribe("characterList");
},
onAfterAction: function() {
document.title = appName + " - Characters";
},
fastRender: true,
});
this.route("characterSheetNaked", {
path: "/character/:_id/",
waitOn: function(){
return [
subsManager.subscribe(
"singleCharacter", this.params._id, {onError: handleSubError}
),
];
},
action: function(){
var _id = this.params._id
var character = Characters.findOne(_id);
var urlName = character && character.urlName;
var path = `\/character\/${_id}\/${urlName || "-"}`;
Router.go(path,{},{replaceState:true});
},
});
this.route("characterSheet", {
path: "/character/:_id/:urlName",
waitOn: function(){
return [
subsManager.subscribe(
"singleCharacter", this.params._id, {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;
}
},
//analytics
trackPageView: false,
onRun: function() {
window.ga && window.ga("send", "pageview", "/character");
this.next();
},
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(){
return subsManager.subscribe("customLibraries");
},
onAfterAction: function() {
document.title = appName + " - Library";
},
fastRender: true,
});
this.route("loading", {
path: "/loading"
});
this.route("profile", {
path: "/account",
onAfterAction: function() {
document.title = appName + " Account";
},
});
this.route("/changelog", {
name: "changeLog",
waitOn: function() {
return [
subsManager.subscribe("changeLog"),
]
},
data: {
changeLogs: function() {
return ChangeLogs.find({}, {sort: {version: -1}});
}
},
onAfterAction: function() {
document.title = appName;
},
fastRender: true,
});
this.route("/guide", {
name: "guide",
onAfterAction: function() {
document.title = appName;
},
});
this.route("/error", {
name: "error",
onAfterAction: function() {
document.title = `${appName} - Error`;
},
});
});

30
app/bower.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "dicecloud",
"version": "1",
"homepage": "",
"authors": [
"Stefan Zermatten"
],
"license": "none",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"polymer": "Polymer/polymer#^1.0.0",
"iron-elements": "PolymerElements/iron-elements#^1.0.0",
"platinum-elements": "PolymerElements/platinum-elements#^1.0.1",
"neon-elements": "PolymerElements/neon-elements#^1.0.0",
"paper-elements": "PolymerElements/paper-elements#^1.0.7",
"app-elements": "PolymerElements/app-elements#^0.10.1",
"marked-element": "PolymerElements/marked-element#^1.2.0",
"paper-swatch-picker": "PolymerElements/paper-swatch-picker#~1.0.2"
},
"resolutions": {
"webcomponentsjs": "0.7.24"
}
}

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,38 @@
this.GlobalUI = (function() {
function GlobalUI() {}
var toast;
GlobalUI.toast = function(opts) {
if (!toast) toast = $("#global-toast")[0];
if (_.isObject(opts)){
toast.text = opts.text;
Session.set("global.ui.toastTemplate", opts.template);
Session.set("global.ui.toastData", opts.data);
} else {
toast.text = opts;
Session.set("global.ui.toastTemplate");
Session.set("global.ui.toastData");
}
return toast.show();
};
GlobalUI.deletedToast = function(id, collection, itemName) {
GlobalUI.toast({
text: itemName ? itemName + " deleted" : "Deleted item from" + collection,
template: "undoToast",
data: {
id: id,
collection: collection,
},
});
};
return GlobalUI;
})();
Template.layout.helpers({
globalToastTemplate: function() {
return Session.get("global.ui.toastTemplate");
},
globalToastData: function() {
return Session.get("global.ui.toastData");
},
});

View File

@@ -0,0 +1,3 @@
Template.registerHelper("canCast", function() {
return Characters.find({_id: this._id, spells: {$size: 0}}).count() === 0;
});

View File

@@ -0,0 +1,11 @@
Template.registerHelper("canEditCharacter", function(charId) {
return canEditCharacter(charId);
});
canEditCharacter = function(charId) {
var char = Characters.findOne(charId);
if (!char) return false;
var userId = Meteor.userId();
return char.owner === userId ||
_.contains(char.writers, userId);
};

View File

@@ -0,0 +1,3 @@
Template.registerHelper("characterPath", function(char) {
return `\/character\/${char._id}\/${char.urlName || "-"}`;
});

View File

@@ -0,0 +1,11 @@
Template.registerHelper("colorClass", function(color) {
if (color) {
return getColorClass(color);
} else if (this.color) {
return getColorClass(this.color);
}
});
Template.registerHelper("hexColor", function(color) {
return getHexColor(color);
});

View File

@@ -0,0 +1,9 @@
Template.registerHelper("detailHero", function(suffix, givenId) {
var id = givenId || this._id;
if (suffix) {
id += suffix;
}
if (Session.equals("global.ui.detailHeroId", id)) {
return "hero";
}
});

View File

@@ -0,0 +1,37 @@
Template.registerHelper("evaluate", function(charId, string) {
return evaluate(charId, string);
});
Template.registerHelper("evaluateSigned", function(charId, string) {
var number = evaluate(charId, string);
if (_.isFinite(number)) {
return number > 0 ? "+" + number : "" + number;
} else {
return number;
}
});
Template.registerHelper("evaluateSignedSpaced", function(charId, string) {
var number = evaluate(charId, string);
if (_.isFinite(number)) {
return number > 0 ? "+ " + number : "- " + (-1 * number);
} else {
return number;
}
});
Template.registerHelper("evaluateString", function(charId, string) {
return evaluateString(charId, string);
});
Template.registerHelper("evaluateSpellString", function(charId, spellListId, string) {
return evaluateSpellString(charId, spellListId, string);
});
Template.registerHelper("evaluateShortString", function(charId, string) {
if (_.isString(string)){
return evaluateString(
charId, string.split(/^( *[-*_]){3,} *(?:\n+|$)/m)[0]
);
}
});

View File

@@ -0,0 +1,26 @@
openParentDialog = function({
parent, charId, element, returnElement, callback,
}) {
let template;
let data;
if (parent.collection === "Characters" && parent.group === "racial") {
template = "raceDialog";
data = {charId: parent.id};
} else if (parent.collection === "Features") {
template = "featureDialog";
data = {featureId: parent.id};
} else if (parent.collection === "Classes") {
template = "classDialog";
data = {classId: parent.id};
} else if (parent.collection === "Items") {
template = "itemDialog";
data = {itemId: parent.id};
} else if (parent.collection === "Spells") {
template = "spellDialog";
data = {spellId: parent.id};
} else if (parent.collection === "Buffs") {
template = "buffDialog";
data = {buffId: parent.id};
}
pushDialogStack({template, data, element, returnElement, callback});
};

View File

@@ -0,0 +1,6 @@
Template.registerHelper("round", function(value, decimalPlaces) {
decimalPlaces = +decimalPlaces || 2;
var num = +value;
var tens = Math.pow(10, decimalPlaces);
return Math.round(num * tens) / tens;
});

View File

@@ -0,0 +1,3 @@
Template.registerHelper("session", function(key) {
return Session.get(key);
});

View File

@@ -0,0 +1,3 @@
Template.registerHelper("signedString", function(number) {
return number >= 0 ? "+" + number : "" + number;
});

View File

@@ -0,0 +1,63 @@
Template.registerHelper("valueString", function(value) {
var intValue = Math.round(value * 100);
var cp = intValue % 10;
intValue -= cp;
cp = Math.round(cp);
sp = intValue % 100;
intValue -= sp;
sp = Math.round(sp / 10)
gp = Math.floor(value);
var resultArray = [];
if (gp > 0) {
resultArray.push(gp + " gp");
}
if (sp > 0) {
resultArray.push(sp + " sp");
}
if (cp > 0) {
resultArray.push(cp + " cp");
}
//build string with correct spacing
var result = "";
for (var i = 0, l = resultArray.length; i < l; i++) {
//add a space between values
if (i !== 0) {
result += " ";
}
result += resultArray[i];
}
return result;
});
Template.registerHelper("longValueString", function(value) {
var resultArray = [];
//sp
var gp = Math.floor(value);
if (gp > 0) {
resultArray.push(gp + " gp");
}
//sp
var sp = Math.floor(10 * (value % 1));
if (sp > 0 || resultArray.length) {
resultArray.push(sp + " sp");
}
//cp
var cp = 10 * ((value * 10) % 1);
cp = Math.round(cp * 1000) / 1000;
if (cp > 0 || resultArray.length) {
resultArray.push(cp + " cp");
}
//build string with correct spacing
var result = "";
for (var i = 0; i < resultArray.length; i++) {
//add a space between values
if (i !== 0) {
result += " ";
}
result += resultArray[i];
}
return result;
});

View File

@@ -1,3 +0,0 @@
<body>
<div id="app"></div>
</body>

View File

@@ -0,0 +1,24 @@
/**
* Take in a map like this:
* {
* "#someId": {
* proprty1() { return someReactiveValue()}
* }
* }
* and bind the properties to the DOM on autorun.
*
* Useful for polymer components where you need to set the order of property updating
* or alter properties that don't bind well to their attributes
*/
Blaze.Template.prototype.binding = function(bindingMap){
this.onRendered(function(){
_.each(bindingMap, (propertyMap, cssPattern) => {
node = this.find(cssPattern);
_.each(propertyMap, (func, property) => {
this.autorun(() => {
node[property] = func && func.call && func.call(this, node);
});
});
});
});
};

View File

@@ -0,0 +1,5 @@
let pwdFormSubmit = AccountsTemplates.atPwdFormEvents["submit #at-pwd-form"]
Template.atPwdForm.events({
"click .at-btn.submit": pwdFormSubmit,
});

View File

@@ -0,0 +1,195 @@
improvedInitiativeJson = function(charId, options){
options = options || {
features: true,
attacks: true,
description: true,
};
var char = Characters.findOne(charId);
if (!char) return;
var baseValue = function(attributeName){
return Characters.calculate.attributeBase(charId, attributeName);
};
var skillMod = function(skillName){
return Characters.calculate.skillMod(charId, skillName);
};
var damageMods = getDamageMods(charId);
return JSON.stringify({
"Id": char._id,
"Name": char.name,
"Source": "DiceCloud",
"Type": char.race,
"HP": {
"Value": baseValue("hitPoints"),
"Notes": getHitDiceString(charId) || "",
},
"AC": {
"Value": baseValue("armor"),
"Notes": getArmorString(charId) || "",
},
"InitiativeModifier": skillMod("initiative"),
"Speed": ["" + baseValue("speed")],
"Abilities": {
"Str": baseValue("strength"),
"Dex": baseValue("dexterity"),
"Con": baseValue("constitution"),
"Cha": baseValue("charisma"),
"Int": baseValue("intelligence"),
"Wis": baseValue("wisdom"),
},
"DamageVulnerabilities": damageMods.vulnerabilities,
"DamageResistances": damageMods.resistances,
"DamageImmunities": damageMods.immunities,
"ConditionImmunities": [],
"Saves": [
{"Name": "Str", "Modifier": skillMod("strengthSave")},
{"Name": "Dex", "Modifier": skillMod("dexteritySave")},
{"Name": "Con", "Modifier": skillMod("constitutionSave")},
{"Name": "Int", "Modifier": skillMod("intelligenceSave")},
{"Name": "Wis", "Modifier": skillMod("wisdomSave")},
{"Name": "Cha", "Modifier": skillMod("charismaSave")},
],
"Skills": getSkills(charId),
"Senses": [
"passive Perception " +
Characters.calculate.passiveSkill(charId, "perception")
],
"Languages": getLanguages(charId),
"Challenge": "",
"Traits": options.features ? getTraits(charId) : [],
"Actions": options.attacks ? getActions(charId) : [],
"Reactions": [],
"LegendaryActions": [],
"Description": options.description ? char.description : "",
"Player": Meteor.user().username,
}, null, 2);
}
var getHitDiceString = function(charId){
var d6 = Characters.calculate.attributeBase(charId, "d6HitDice");
var d8 = Characters.calculate.attributeBase(charId, "d8HitDice");
var d10 = Characters.calculate.attributeBase(charId, "d10HitDice");
var d12 = Characters.calculate.attributeBase(charId, "d12HitDice");
var con = Characters.calculate.abilityMod(charId,"constitution");
var string = "" +
(d6 ? `${d6}d6 + ` : "") +
(d8 ? `${d8}d8 + ` : "") +
(d10 ? `${d10}d10 + ` : "") +
(d12 ? `${d12}d12 + ` : "") +
con;
}
var getArmorString = function(charId){
var bases = Effects.find({
charId: charId,
stat: "armor",
operation: "base",
enabled: true,
}).map(e => ({
ame: e.name,
value: evaluateEffect(charId, e),
}));
var base = bases.length && _.max(bases, b => b.value).name || "";
var effects = Effects.find({
charId: charId,
stat: "armor",
operation: {$ne: "base"},
enabled: true,
}).map(e => e.name);
var strings = base ? [base] : [];
strings = strings.concat(effects);
return strings.join(", ");
}
var getDamageMods = function(charId){
// jscs:disable maximumLineLength
var multipliers = [
{name: "Acid", value: Characters.calculate.attributeValue(charId, "acidMultiplier")},
{name: "Bludgeoning", value: Characters.calculate.attributeValue(charId, "bludgeoningMultiplier")},
{name: "Cold", value: Characters.calculate.attributeValue(charId, "coldMultiplier")},
{name: "Fire", value: Characters.calculate.attributeValue(charId, "fireMultiplier")},
{name: "Force", value: Characters.calculate.attributeValue(charId, "forceMultiplier")},
{name: "Lightning", value: Characters.calculate.attributeValue(charId, "lightningMultiplier")},
{name: "Necrotic", value: Characters.calculate.attributeValue(charId, "necroticMultiplier")},
{name: "Piercing", value: Characters.calculate.attributeValue(charId, "piercingMultiplier")},
{name: "Poison", value: Characters.calculate.attributeValue(charId, "poisonMultiplier")},
{name: "Psychic", value: Characters.calculate.attributeValue(charId, "psychicMultiplier")},
{name: "Radiant", value: Characters.calculate.attributeValue(charId, "radiantMultiplier")},
{name: "Slashing", value: Characters.calculate.attributeValue(charId, "slashingMultiplier")},
{name: "Thunder", value: Characters.calculate.attributeValue(charId, "thunderMultiplier")},
];
// jscs:enable maximumLineLength
multipliers = _.groupBy(multipliers, "value");
var names = o => o.name;
return {
"immunities": _.map(multipliers["0"], names),
"resistances": _.map(multipliers["0.5"], names),
"vulnerabilities": _.map(multipliers["2"], names),
};
}
var getSkills = function(charId){
var allSkills = [
{name: "acrobatics", attribute: "dexterity"},
{name: "animalHandling", attribute: "wisdom"},
{name: "arcana", attribute: "intelligence"},
{name: "athletics", attribute: "strength"},
{name: "deception", attribute: "charisma"},
{name: "history", attribute: "intelligence"},
{name: "insight", attribute: "wisdom"},
{name: "intimidation", attribute: "charisma"},
{name: "investigation", attribute: "intelligence"},
{name: "medicine", attribute: "wisdom"},
{name: "nature", attribute: "intelligence"},
{name: "perception", attribute: "wisdom"},
{name: "performance", attribute: "charisma"},
{name: "persuasion", attribute: "charisma"},
{name: "religion", attribute: "intelligence"},
{name: "sleightOfHand", attribute: "dexterity"},
{name: "stealth", attribute: "dexterity"},
{name: "survival", attribute: "wisdom"},
];
var skills = [];
_.each(allSkills, skill => {
var value = Characters.calculate.skillMod(charId, skill.name);
var mod = Characters.calculate.abilityMod(charId, skill.attribute);
if (value !== mod){
skills.push({Name: skill.name, Modifier: value});
}
});
return skills;
};
var getLanguages = function(charId){
return Proficiencies.find({
charId,
enabled: true,
type: "language",
}).map(l => l.name);
};
var getTraits = function(charId){
return Features.find(
{charId: charId},
{sort: {color: 1, name: 1}}
).map(f => ({
Name: f.name,
// evaluateShortString helper
Content: evaluateString(
charId, f.description && f.description.split(/^( *[-*_]){3,} *(?:\n+|$)/m)[0]
) || "",
Usage: "",
}));
}
var getActions = function(charId){
return Attacks.find(
{charId, enabled: true},
{sort: {color: 1, name: 1}}
).map(a => ({
Name: a.name,
Content: `+${evaluate(charId, a.attackBonus)} to hit, ` +
`${evaluateString(charId, a.damage)} ${a.damageType} damage, ` +
`${a.details}`,
Usage: "",
}));
}

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

@@ -0,0 +1,28 @@
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
// MIT license
var lastTime = 0;
var vendors = ["ms", "moz", "webkit", "o"];
for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x] + "RequestAnimationFrame"];
window.cancelAnimationFrame = window[vendors[x] + "CancelAnimationFrame"] ||
window[vendors[x] + "CancelRequestAnimationFrame"];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};

View File

@@ -1,3 +0,0 @@
import '/imports/ui/vueSetup.js';
import '/imports/ui/styles/stylesIndex.js';
import '/imports/ui/styles/stylesIndex.js';

View File

@@ -0,0 +1,17 @@
@keyframes bounce {
from {
transform: translate(0px,0px);
}
to {
transform: translate(0px,-16px);
}
}
.bounce{
animation-name: bounce;
animation-duration: 0.3s;
animation-direction: alternate;
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
animation-delay: 0s;
animation-iteration-count: infinite;
}

View File

@@ -0,0 +1,87 @@
/*Column layout*/
.column-container {
column-fill: balance;
column-gap: 0px;
column-width: 304px;
padding: 4px;
transform: translateZ(0);
}
.column-container.thin-columns {
column-count: 4;
column-width: 240px;
}
.column-container > div {
padding: 4px;
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
break-inside: avoid;
}
/*Cards*/
.card {
background: white;
border-radius: 2px;
position: initial;
}
.card .top {
cursor: pointer;
padding: 16px;
border-radius: 2px 2px 0 0;
}
.card .top.white {
cursor: auto;
padding: 16px;
border-bottom: rgba(0,0,0,0.12) solid 1px;
}
.card .bottom {
padding: 16px;
border-radius: 0 0 2px 2px;
}
.card .bottom.list {
padding: 16px 0;
}
.card .bottom.list .paper-font-subhead {
color: rgba(0,0,0,0.54);
font-size: 14px;
font-weight: 500;
letter-spacing: 0.010em;
padding: 12px 16px 12px 16px;
}
.card .bottom.text {
white-space: pre-wrap;
}
.card .left {
padding: 16px;
border-radius: 2px 0 0 2px;
text-align: center;
min-width: 72px;
}
.card .right {
padding: 16px;
border-radius: 0 2px 2px 0;
}
.card .left paper-icon-button {
display: block;
height: 32px;
padding: 0;
width: 32px;
}
.card .left paper-icon-button[disabled] {
color: rgba(255, 255, 255, 0.2);
}
.card img, .card iron-image {
max-width: 100%;
}

View File

@@ -0,0 +1,87 @@
.red {
background-color: #F44336;
}
.pink {
background-color: #E91E63;
}
.purple {
background-color: #9C27B0;
}
.deep-purple {
background-color: #673AB7;
}
.indigo {
background-color: #3F51B5;
}
.blue {
background-color: #2196F3;
}
.light-blue {
background-color: #03A9F4;
}
.cyan {
background-color: #00BCD4;
}
.teal {
background-color: #009688;
}
.green {
background-color: #4CAF50;
}
.light-green {
background-color: #8BC34A;
}
.lime {
background-color: #CDDC39;
}
.yellow {
background-color: #FFEB3B;
}
.amber {
background-color: #FFC107;
}
.orange {
background-color: #FF9800;
}
.deep-orange {
background-color: #FF5722;
}
.brown {
background-color: #795548;
}
.grey {
background-color: #9E9E9E;
}
.blue-grey {
background-color: #607D8B;
}
.app-grey {
background-color: #424242;
}
.white {
background-color: #ffffff;
}
.black {
background-color: #262626;
}

View 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;
}

View File

@@ -0,0 +1,43 @@
/*
List items
*/
.item-slot {
background-color: rgb(232, 232, 232);
background-color: rgba(0, 0, 0, 0.1);
}
.item {
background: white;
cursor: pointer;
font-size: 16px;
height: 40px;
margin: 1px 0 1px 0;
padding: 0 16px 0 16px;
position: relative;
transition: box-shadow 0.3s ease, opacity 0.5s ease-in-out;
}
.item > .itemName {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item.small {
height: 32px;
}
.item.tall {
height: 56px;
}
.item.flexible {
height: auto;
padding-top: 16px;
padding-bottom: 16px;
}
.item iron-icon, .item paper-icon-button {
color: #747474;
color: rgba(0,0,0,0.54);
}

115
app/client/style/main.css Normal file
View File

@@ -0,0 +1,115 @@
/*apply a natural box layout model to all elements*/
*, *:before, *:after {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
root {
display: block;
}
body {
font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial;
margin: 0;
overflow-x: hidden;
background-color: #E0E0E0;
}
/*Paragraphs*/
p {
margin-bottom: 8px;
}
//Horizontal rule
hr {
background-color: #444;
opacity: 0.12;
border-width: 0;
color: #444;
height: 1px;
line-height: 0;
margin: 16px 0;
text-align: center;
}
.avatar {
display: inline-block;
box-sizing: border-box;
width: 40px;
height: 40px;
border-radius: 50%;
font-size: 26px;
font-color: rgba(255, 255, 255, 0.58) !important;
}
/*
temporary fix for https://github.com/PolymerElements/paper-item/issues/71
*/
paper-icon-item::shadow #contentIcon {
flex-shrink: 0;
}
/*FABs*/
.floatyButton {
position: fixed;
bottom: 24px;
right: 24px;
/* stop the fab from flashing during animation */
transform: scale(1) translateZ(0px);
z-index: 3;
}
paper-fab {
background-color: #d13b2e;
}
paper-fab.keyboard-focus {
background: #630c05;
}
.red-button:not([disabled]) {
background: #d23f31;
color: #fff;
margin-top: 16px;
}
/*Buttons*/
paper-button {
color: #000;
color: rgba(0,0,0,0.87);
font-size: 14px;
font-weight: 400;
letter-spacing: 0.010;
text-transform: uppercase;
}
dicecloud-selector paper-item {
white-space: nowrap;
overflow: hidden;
}
/*Style shortcuts*/
.scroll-y {
overflow-y: auto;
}
.clickable, core-item, paper-tab {
cursor: pointer;
}
.pre-wrap, .prewrap{
white-space: pre-wrap;
}
.padded {
padding: 8px;
}
.fullwidth {
width: 100%;
}
.fab-buffer {
height: 100px;
}

View File

@@ -0,0 +1,15 @@
td {
padding: 8px;
}
.strengthTable{
width: 100%;
}
.strengthTable td:nth-child(2){
text-align: right;
}
.summaryTable td:nth-child(3){
text-align: right;
}

View File

@@ -0,0 +1,68 @@
body .paper-font-display4, body .paper-font-display3, body .paper-font-title, body .paper-font-caption{
white-space: normal;
}
.white-text {
color: #dedede;
color: rgba(255,255,255,0.87);
}
.white-text .paper-font-display2{
color: #8a8a8a;
color: rgba(255,255,255,0.54);
}
.white-text .paper-font-display1, .white-text.paper-font-display1{
color: #8a8a8a;
color: rgba(255,255,255,0.54);
}
.white-text h1, .white-text .paper-font-headline, .white-text.paper-font-headline{
color: #dedede;
color: rgba(255,255,255,0.87);
}
.white-text h2, .white-text .paper-font-title, .white-text.paper-font-title{
color: #dedede;
color: rgba(255,255,255,0.87);
}
.white-text h3, .white-text .paper-font-subhead{
color: #dedede;
color: rgba(255,255,255,0.87);
}
.white-text .paper-font-body2{
color: #dedede;
color: rgba(255,255,255,0.87);
}
.white-text p, .white-text .paper-font-body1{
color: #dedede;
color: rgba(255,255,255,0.87);
}
.white-text .paper-font-caption{
color: #8a8a8a;
color: rgba(255,255,255,0.54);
}
.black54 {
color: #757575;
color: rgba(0,0,0,0.54);
}
.white54 {
color: #8a8a8a;
color: rgba(255,255,255,0.54);
}
.black87 {
color: #212121;
color: rgba(0,0,0,0.87);
}
.white87 {
color: #dedede;
color: rgba(255,255,255,0.87);
}

View File

@@ -0,0 +1,33 @@
<template name="attackEdit">
<div class="layout horizontal">
<div class="layout vertical flex">
<div class="layout horizontal wrap">
<!--attackBonus-->
<paper-input class="attackBonusInput flex" label="Attack Bonus" value={{attackBonus}}
style="flex-basis: 200px; margin-right: 16px;">
{{> formulaSuffix}}
</paper-input>
<!--details-->
<paper-input class="detailInput" label="Details" value={{details}} style="flex-basis: 200px;">
</paper-input>
</div>
<div class="layout horizontal wrap">
<!--damageBonus-->
<paper-input class="damageInput flex" label="Damage" value={{damage}}
style="flex-basis: 200px; margin-right: 16px;">
{{> bracketSuffix}}
</paper-input>
<!--DamageType-->
<paper-dropdown-menu label="Damage Type" dynamic-align>
<dicecloud-selector class="dropdown-content damageTypeDropdown" selected={{damageType}}>
{{#each damageTypes}}
<paper-item name={{this}} class="containerMenuItem">{{this}}</paper-item>
{{/each}}
</dicecloud-selector>
</paper-dropdown-menu>
</div>
</div>
<!--Delete Button-->
<paper-icon-button class="deleteAttack" icon="delete"></paper-icon-button>
</div>
</template>

View File

@@ -0,0 +1,46 @@
var damageTypes = [
"bludgeoning",
"piercing",
"slashing",
"acid",
"cold",
"fire",
"force",
"lightning",
"necrotic",
"poison",
"psychic",
"radiant",
"thunder",
];
Template.attackEdit.events({
"click .deleteAttack": function(event, instance) {
Attacks.softRemoveNode(this._id);
GlobalUI.deletedToast(this._id, "Attacks", "Attack");
},
"change .attackBonusInput": function(event) {
var value = event.currentTarget.value;
Attacks.update(this._id, {$set: {attackBonus: value}});
},
"change .damageInput": function(event) {
var value = event.currentTarget.value;
Attacks.update(this._id, {$set: {damage: value}});
},
"change .detailInput": function(event) {
var value = event.currentTarget.value;
Attacks.update(this._id, {$set: {details: value}});
},
"iron-select .damageTypeDropdown": function(event) {
var detail = event.originalEvent.detail;
var value = detail.item.getAttribute("name");
if (value == this.damageType) return;
Attacks.update(this._id, {$set: {damageType: value}});
},
});
Template.attackEdit.helpers({
damageTypes: function() {
return damageTypes;
},
});

View File

@@ -0,0 +1,13 @@
<!--needs to be given charId, parentId and parentCollection-->
<template name="attackEditList">
{{#if attacks.count}}
<hr style="margin: 16px 0 16px 0;">
<div id="attacks">
<div class="paper-font-title">Attacks</div>
{{#each attacks}}
{{>attackEdit}}
{{/each}}
</div>
{{/if}}
<paper-button id="addAttackButton" class="red-button" raised>Add Attack</paper-button>
</template>

View File

@@ -0,0 +1,38 @@
Template.attackEditList.helpers({
attacks: function() {
var cursor = Attacks.find({"parent.id": this.parentId, charId: this.charId});
return cursor;
}
});
Template.attackEditList.events({
"tap #addAttackButton": function() {
if (typeof this.isSpell !== 'undefined' && this.isSpell) {
var parentSpell = Spells.findOne({"_id": this.parentId})
if (parentSpell && parentSpell.parent.collection == "SpellLists") {
var spellList = SpellLists.findOne({"_id":parentSpell.parent.id});
if (spellList && spellList.attackBonus) {
Attacks.insert({
charId: this.charId,
parent: {
id: this.parentId,
collection: this.parentCollection
},
attackBonus: "attackBonus",
damage: "1d10",
damageType: "fire",
});
return;
}
}
}
Attacks.insert({
charId: this.charId,
parent: {
id: this.parentId,
collection: this.parentCollection
},
});
},
});

View File

@@ -0,0 +1,17 @@
<template name="attackView">
<div class="attackView layout horizontal">
<div class="paper-font-headline layout horizontal center" style="margin-right: 16px;">
{{evaluateAttackBonus charId attack}}
</div>
<div class="layout vertical">
<div>
{{evaluateDamage charId attack}}&nbsp;{{attack.damageType}}
</div>
{{#if attack.details}}
<div class="paper-font-caption">
{{attack.details}}
</div>
{{/if}}
</div>
</div>
</template>

View File

@@ -0,0 +1,28 @@
Template.attackView.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,11 @@
<template name="attacksViewList">
{{#if attacks.count}}
<hr style="margin: 16px 0 16px 0;">
<div class="attacks">
<div class="spaceAfter paper-font-title">Attacks</div>
{{#each attack in attacks}}
{{> attackView attack=attack charId=charId}}
{{/each}}
</div>
{{/if}}
</template>

View File

@@ -0,0 +1,5 @@
Template.attacksViewList.helpers({
attacks: function() {
return Attacks.find({"parent.id": this.parentId, charId: this.charId});
}
});

View File

@@ -0,0 +1,33 @@
<!-- data is the CustomBuff -->
<template name="applyBuffDialog">
<div class="fit layout vertical applyBuffDialog">
<app-header fixed effects="waterfall">
<app-toolbar>
Apply Buff
</app-toolbar>
</app-header>
<div class="flex layout horizontal" style="height:100%">
<div class="flex" style="margin-right: 16px; height: 100%; max-width: 240px; overflow-y: auto;">
{{> characterPicker selfId=buff.charId includeSelf=canApplyToSelf writableOnly=true}}
</div>
<div class="flex buff-description" style="height: 100%; overflow-y: auto">
<hr style="margin: 16px 0 16px 0;">
{{#if buff.description}}
<div>{{#markdown}}{{evaluateString buff.charId buff.description}}{{/markdown}}</div>
<hr style="margin: 16px 0 16px 0;">
{{/if}}
{{> effectsViewList charId=buff.charId parentId=buff._id}}
{{> proficiencyViewList charId=buff.charId parentId=buff._id}}
{{> attacksViewList charId=buff.charId parentId=buff._id}}
</div>
</div>
<div class="buttons layout horizontal end-justified">
<paper-button id="cancelButton">
Cancel
</paper-button>
<paper-button id="applyButton" disabled={{cantApply}}>
Apply
</paper-button>
</div>
</div>
</template>

View File

@@ -0,0 +1,32 @@
Template.applyBuffDialog.onCreated(function(){
this.selectedTarget = new ReactiveVar("default");
});
Template.applyBuffDialog.helpers({
cantApply: function() {
return this.buff.target === "others" && Template.instance().selectedTarget.get() === "default"; //this is the only case where we can't apply a buff
},
canApplyToSelf: function() {
return this.buff.target !== "others"; //i.e. it is "self" or "both"
},
});
Template.applyBuffDialog.events({
"iron-select .characterPicker": function(event){
var detail = event.originalEvent.detail;
var value = detail.item.getAttribute("name");
Template.instance().selectedTarget.set(value);
},
"click #applyButton": function(event, instance){
var targetId = Template.instance().selectedTarget.get();
if (targetId === "default") {
if (this.buff.target === "others") return; //since we have "Select a character" selected
targetId = this.buff.charId; //otherwise, the default is to target self
}
popDialogStack(targetId);
},
"click #cancelButton": function(event, instance){
popDialogStack();
},
});

View File

@@ -0,0 +1,23 @@
<template name="buffDialog">
{{#with buff}}
{{#baseDialog title=name class="white" hideColor=true startEditing=true editOnly=true}}
{{> buffDetails}}
{{else}}
{{> buffDetails}}
{{/baseDialog}}
{{/with}}
</template>
<template name="buffDetails">
<div>
{{appliedBy}}
</div>
<hr style="margin: 16px 0 16px 0;">
{{#if description}}
<div>{{#markdown}}{{evaluateString charId description}}{{/markdown}}</div>
<hr style="margin: 16px 0 16px 0;">
{{/if}}
{{> effectsViewList charId=charId parentId=_id}}
{{> proficiencyViewList charId=charId parentId=_id}}
{{> attacksViewList charId=charId parentId=_id}}
</template>

View File

@@ -0,0 +1,50 @@
Template.buffDialog.onCreated(function(){
var buff = Buffs.findOne(this.buffId);
Meteor.subscribe("singleCharacterName", buff.charId); //so we can access the names of public characters
});
Template.buffDialog.helpers({
buff: function(){
return Buffs.findOne(this.buffId);
},
});
Template.buffDialog.events({
"click #deleteButton": function(event, instance){
Buffs.softRemoveNode(instance.data.buffId);
popDialogStack();
},
});
const typeDict = {
"Features": "feature",
"Items": "item",
"Spells": "spell",
}; //really, we should only need these three
Template.buffDetails.helpers({
appliedBy: function() {
if (this.type == "inate") {
return "Innate.";
} else {
var myName = Characters.findOne(this.charId).name;
var applierCharacter = Characters.findOne(this.appliedBy) || {name: "???"}
// "???" indicates that either we do not have read access to the buff-giver, or that the buff-giver does not exist.
if (applierCharacter.name === myName) {
var charName = "your "
} else {
if (applierCharacter.name && applierCharacter.name[applierCharacter.name.length - 1] === 's') {
var charName = applierCharacter.name + "' ";
} else {
var charName = applierCharacter.name + "'s ";
}
}
var type = typeDict[this.appliedByDetails.collection] + " ";
var applierThing = this.appliedByDetails.name;
return "Applied by " + charName + type + applierThing + ".";
}
},
});

View File

@@ -0,0 +1,15 @@
<template name="buffListItem">
<div class="item buffListItem layout horizontal center">
<div class="flex">
{{buff.name}}
</div>
{{#if canEditCharacter buff.charId}}
<paper-icon-button class="deleteButton"
role="button"
tabindex="0"
icon="delete">
</paper-icon-button>
{{/if}}
</div>
</template>

View File

@@ -0,0 +1,21 @@
Template.buffListItem.helpers({
name: function() {
return this.buff.name
}
});
Template.buffListItem.events({
"click .buffListItem": function(event){
var buffId = this.buff._id;
var charId = this.buff.charId;
pushDialogStack({
template: "buffDialog",
data: {buffId: buffId, charId: charId},
element: event.currentTarget,
});
},
"tap .deleteButton": function(event){
event.stopPropagation();
Buffs.remove(this.buff._id);
},
});

View File

@@ -0,0 +1,11 @@
.condition-library-dialog .item.selected {
background-color: #e4e4e4;
}
.condition-library-dialog table {
border-collapse: collapse;
}
.condition-library-dialog .library-condition td, tr {
position: relative;
}

View File

@@ -0,0 +1,34 @@
<template name="conditionLibraryDialog">
<div class="fit condition-library-dialog layout vertical">
<app-toolbar class="app-grey white-text">
<paper-icon-button id="backButton"
icon="arrow-back">
</paper-icon-button>
<div main-title>Conditions</div>
</app-toolbar>
<div class="flex scroll-y">
<div class="conditions" style="padding:8px">
<table style="width: 100%">
<tbody>
{{#each condition in conditions}}
{{>libraryCondition condition=condition selected=(isSelected condition)}}
{{/each}}
</tbody>
</table>
</div>
</div>
<div class="layout horizontal end-justified">
<paper-button class="cancelButton">Cancel</paper-button>
<paper-button class="okButton">OK</paper-button>
</div>
</div>
</template>
<template name="libraryCondition">
<tr class="item library-condition {{#if selected}}selected{{/if}}">
<td class="conditionName">
{{conditionName condition}}
<paper-ripple></paper-ripple>
</td>
</tr>
</template>

View File

@@ -0,0 +1,166 @@
Template.conditionLibraryDialog.onCreated(function(){
this.selectedCondition = new ReactiveVar();
});
Template.conditionLibraryDialog.helpers({
conditions: function(){
return Object.keys(LIBRARY_CONDITIONS)
},
isSelected(condition){
const selected = Template.instance().selectedCondition.get();
return selected && selected === condition;
},
});
Template.conditionLibraryDialog.events({
"click .cancelButton": function(event, template){
popDialogStack();
},
"click .okButton": function(event, template){
popDialogStack(template.selectedCondition.get());
},
"click .library-condition": function(event, template){
template.selectedCondition.set(this.condition);
},
"click #backButton": function(event, template){
popDialogStack();
},
});
Template.libraryCondition.helpers({
conditionName: function(name){
return LIBRARY_CONDITIONS[name].buff.name;
},
})
LIBRARY_CONDITIONS = {
//Conditions
blind: {
buff: {
name: "Blind",
description: "A blinded creature cant see and automatically fails any ability check that requires sight.\n\nAttack rolls against the creature have advantage, and the creatures attack rolls have disadvantage.",
},
},
deaf: {
buff: {
name: "Deaf",
description: "A deafened creature cant hear and automatically fails any ability check that requires hearing.",
},
},
frightened: {
buff: {
name: "Frightened",
description: "A frightened creature has disadvantage on ability checks and attack rolls while the source of its fear is within line of sight.\n\nThe creature cant willingly move closer to the source of its fear.",
}
},
grappled: {
buff:{
name: "Grappled",
description: "A grappled creatures speed becomes 0, and it cant benefit from any bonus to its speed.\n\nThe condition ends if the grappler is incapacitated.\n\nThe condition also ends if an effect removes the grappled creature from the reach of the grappler or grappling effect, such as when a creature is hurled away by the thunder wave spell.",
},
},
incapacitated: {
buff: {
name: "Incapacitated",
description: "An incapacitated creature cant take actions or reactions.",
}
},
invisible: {
buff: {
name: "Invisible",
description: "An invisible creature is impossible to see without the aid of magic or a special sense. For the purpose of hiding, the creature is heavily obscured. The creatures location can be detected by any noise it makes or any tracks it leaves.\n\nAttack rolls against the creature have disadvantage, and the creatures attack rolls have advantage.",
}
},
paralyzed: {
buff: {
name: "Paralyzed",
description: "A paralyzed creature is **incapacitated** and cant move or speak.\n\nAttack rolls against the creature have advantage.\n\nAny attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.",
},
},
petrified: {
buff: {
name: "Petrified",
description: "A petrified creature is transformed, along with any nonmagical object it is wearing or carrying, into a solid inanimate substance (usually stone). Its weight increases by a factor of ten, and it ceases aging.\n\nA petrified creature is **incapacitated** and cant move or speak, and is unaware of its surroundings.\n\nAttack rolls against the creature have advantage.\n\nThe creature is immune to poison and disease, although a poison or disease already in its system is suspended, not neutralized.",
},
},
poisoned: {
buff: {
name: "Poisoned",
description: "A poisoned creature has disadvantage on attack rolls and ability checks.",
},
},
prone: {
buff: {
name: "Prone",
description: "A prone creatures only movement option is to crawl, unless it stands up and thereby ends the condition.\n\nThe creature has disadvantage on attack rolls.\n\nAn attack roll against the creature has advantage if the attacker is within 5 feet of the creature. Otherwise, the attack roll has disadvantage.",
}
},
restrained: {
buff: {
name: "Restrained",
description: "A restrained creatures speed becomes 0, and it cant benefit from any bonus to its speed.\n\nAttack rolls against the creature have advantage, and the creatures attack rolls have disadvantage.\n\nThe creature has disadvantage on Dexterity saving throws.",
},
},
stunned: {
buff: {
name: "Stunned",
description: "A stunned creature is **incapacitated**, cant move, and can speak only falteringly\n\nThe creature automatically fails Strength and Dexterity saving throws.\n\nAttack rolls against the creature have advantage.",
},
},
unconscious: {
buff: {
name: "Unconscious",
description: "An unconscious creature is **incapacitated**, cant move or speak, and is unaware of its surroundings.\n\nThe creature drops whatever its holding and falls **prone**.\n\nThe creature automatically fails Strength and Dexterity saving throws.\n\nAttack rolls against the creature have advantage.\n\nAny attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.",
},
},
exhaustion1: {
buff: {
name: "Exhaustion - 1",
description: "Disadvantage on ability checks\n\nFinishing a long rest reduces a creatures exhaustion level by 1, provided that the creature has also ingested some food and drink.",
},
},
exhaustion2: {
buff: {
name: "Exhaustion - 2",
description: "Speed halved",
},
},
exhaustion3: {
buff: {
name: "Exhaustion - 3",
description: "Disadvantage on attack rolls and saving throws",
},
},
exhaustion4: {
buff: {
name: "Exhaustion - 4",
description: "Hit point maximum halved",
},
},
exhaustion5: {
buff: {
name: "Exhaustion - 5",
description: "Speed reduced to 0",
},
},
exhaustion6: {
buff: {
name: "Exhaustion - 6",
description: "You have died of exhaustion",
},
},
};

View File

@@ -0,0 +1,15 @@
<template name="conditionView">
<div class="item conditionView layout horizontal center">
<div class="flex">
{{condition.name}}
</div>
{{#if canEditCharacter condition.charId}}
<paper-icon-button class="deleteButton"
role="button"
tabindex="0"
icon="delete">
</paper-icon-button>
{{/if}}
</div>
</template>

View File

@@ -0,0 +1,15 @@
Template.conditionView.events({
"click .conditionView": function(event){
var condition = this.condition;
var charId = Template.parentData()._id;
pushDialogStack({
template: "conditionViewDialog",
data: {condition: condition},
element: event.currentTarget,
});
},
"tap .deleteButton": function(event){
event.stopPropagation();
Conditions.remove(this.condition._id);
},
});

View File

@@ -0,0 +1,14 @@
<template name="conditionViewDialog">
{{#baseDialog title=condition.name class="white" hideColor=true startEditing=true editOnly=true}}}
{{> conditionDetails condition=condition}}
{{else}}
{{> conditionDetails condition=condition}}
{{/baseDialog}}
</template>
<template name="conditionDetails">
{{#if condition.description}}
<div>{{#markdown}}{{evaluateString condition.charId condition.description}}{{/markdown}}</div>
{{/if}}
{{> effectsViewList charId=condition.charId parentId=condition._id}}
</template>

View File

@@ -0,0 +1,6 @@
Template.conditionViewDialog.events({
"click #deleteButton": function(event, instance){
Conditions.remove(instance.data.condition._id);
popDialogStack();
},
});

View File

@@ -0,0 +1,29 @@
<template name="customBuffEdit">
{{#baseEditDialog title=buff.name hideColor=true}}
<!--name-->
<paper-input id="buffNameInput" class="fullwidth" label="Name" value={{buff.name}}></paper-input>
<div class="layout horizontal center wrap justified">
<paper-dropdown-menu class=flex label="Target" style="flex-basis: 150px; max-width: 200px;">
<dicecloud-selector selected={{buff.target}} class="dropdown-content target-dropdown">
<paper-item name="self" style="width: 150px;">
Self only
</paper-item>
<paper-item name="others">
Others only
</paper-item>
<paper-item name="both">
Both
</paper-item>
</dicecloud-selector>
</paper-dropdown-menu>
</div>
<!--description-->
<paper-textarea label="Description" id="buffDescriptionInput" value={{buff.description}}></paper-textarea>
{{> effectsEditList parentId=buff._id parentCollection="CustomBuffs" charId=buff.charId name=name enabled=false}}
{{> attackEditList parentId=buff._id parentCollection="CustomBuffs" charId=buff.charId name=name enabled=false}}
{{> proficiencyEditList parentId=buff._id parentCollection="CustomBuffs" charId=buff.charId enabled=false}}
{{/baseEditDialog}}
</template>

View File

@@ -0,0 +1,47 @@
Template.customBuffEdit.helpers({
buff(){
return CustomBuffs.findOne(this.customBuffId);
},
});
const debounce = (f) => _.debounce(f, 300);
Template.customBuffEdit.events({
"input #buffNameInput": 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;
CustomBuffs.update(this.customBuffId, {
$set: {name: name}
}, {
removeEmptyStrings: false,
trimStrings: false,
});
}
}),
"input #buffDescriptionInput": debounce(function(event){
var description = event.currentTarget.value;
CustomBuffs.update(this.customBuffId, {
$set: {description: description}
}, {
removeEmptyStrings: false,
trimStrings: false,
});
}),
"iron-select .target-dropdown": function(event){
var detail = event.originalEvent.detail;
var value = detail.item.getAttribute("name");
const buff = CustomBuffs.findOne(this.customBuffId);
if (value === buff.target) return;
CustomBuffs.update(this.customBuffId, {$set: {target: value}});
},
"click #deleteButton": function(event, instance){
CustomBuffs.softRemoveNode(instance.data.customBuffId);
GlobalUI.deletedToast(instance.data.customBuffId, "Buffs", "Buff");
popDialogStack();
},
});

View File

@@ -0,0 +1,30 @@
<!--needs to be given charId, parentId and parentCollection-->
<template name="customBuffEditList">
{{#if buffs.count}}
<div class="buffs">
<div class="paper-font-title" style="margin-bottom: 8px;">
Buffs
</div>
<table class="wideTable" style="width: 100%;">
{{#each buff in buffs}}
{{> customBuffEditListItem buff=buff}}
{{/each}}
</table>
</div>
{{/if}}
<paper-button id="addBuffButton"
class="red-button"
raised>
Add Buff
</paper-button>
</template>
<template name="customBuffEditListItem">
<div class="buff layout horizontal center" data-id={{buff._id}}>
{{> customBuffView buff=buff}}
<div>
<paper-icon-button class="edit-buff" icon="create">
</paper-icon-button>
</div>
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More