Compare commits

...

205 Commits
1.2.5 ... 1.7.3

Author SHA1 Message Date
Stefan Zermatten
74c6a423ee Allowed owners to give their characters to new owners, no UI yet. 2018-08-28 09:59:10 +02:00
Stefan Zermatten
41c90bb69f Updated Meteor 2018-08-23 11:45:38 +02:00
Stefan Zermatten
68432541db Merge pull request #168 from Frogvall/master
Adding advantage/disadvantage arrows to initiative stat card
2018-08-23 11:34:02 +02:00
Frogvall
c417c45db1 Cleaned up a bit 2018-06-20 14:33:34 +02:00
Frogvall
3f3caf63e4 Add css and js implementation of previously naïve approach
Bonus: Learned some meteor :)
2018-06-20 14:25:18 +02:00
Jonas
49c5a1fcb1 Added adv class to skills in stat cards
Added arrows to initiative, for example
2018-06-18 00:51:20 +02:00
Stefan Zermatten
9fb37148fa Merge pull request #167 from mommothazaz123/master
Add raw character JSON output, improve ratelimiter rules of API
2018-06-08 09:26:52 +02:00
Andrew Zhu
a67d7fb4ea Merge branch 'master' into master 2018-06-07 02:06:34 -07:00
Andrew Zhu
8e0f19742b fix some branch conflicts 2018-06-07 02:03:12 -07:00
Andrew Zhu
216e502c8a add permission check to JSONcharacter 2018-06-07 01:38:29 -07:00
Andrew Zhu
1a18d1f816 update README to reflect dir name change 2018-06-07 01:08:41 -07:00
Andrew Zhu
c099e3173b rename /rpg-docs to /app 2018-06-07 01:07:49 -07:00
Andrew Zhu
de93636c7c Revert "add mixmax:smart-disconnect to reduce server load"
This reverts commit 465a61f80f.
2018-06-07 01:03:16 -07:00
Stefan Zermatten
5e263443b3 Updated dependencies 2018-06-04 15:42:23 +02:00
Stefan Zermatten
8c3a891254 Merge branch 'bugfix-163' 2018-06-04 15:29:14 +02:00
Stefan Zermatten
e737067990 Made sure empty party names take up some space so they can be edited 2018-06-04 15:28:08 +02:00
Andrew Zhu
465a61f80f add mixmax:smart-disconnect to reduce server load 2018-05-27 22:58:02 -07:00
Andrew Zhu
bbf42aaf97 make ratelimiter actually match, return time to reset on hitting ratelimit 2018-05-25 01:39:35 -07:00
Andrew Zhu
b20d086a24 add character JSON endpoint, improve ratelimit rules 2018-05-25 01:34:39 -07:00
Stefan Zermatten
52baf297ca Changed rpg-docs folder to app 2018-05-21 14:21:07 +02:00
Thaum Rystra
45e9f491ff Merge branch 'feature-blacklist' 2018-04-02 14:27:24 +02:00
Thaum Rystra
742b26b0de Added rate limiting logging and an error page for hitting the rate limit on opening characters 2018-04-02 14:26:55 +02:00
Stefan Zermatten
164ba78c81 Added blacklist checks and rate limit logging
Needs testing
2018-03-12 09:22:04 +02:00
Thaum Rystra
e27211b24d Merge branch 'enhancement-154' 2018-03-03 20:13:03 +02:00
Thaum Rystra
30987752cc Hotfix - Remove localhost from image link on printed character sheet 2018-03-03 17:28:42 +02:00
Thaum Rystra
058ee2691f Merge branch 'feature-print'
# Conflicts:
#	rpg-docs/package-lock.json
2018-03-03 17:18:16 +02:00
Thaum Rystra
f0cf7f4956 Added QR code and finished page 1 2018-03-03 17:13:36 +02:00
Thaum Rystra
75c8720b04 Moved printed character sheets to their own page
This makes sure the entire printed sheet is rendered before the browser  attempts to print it, solving all manner of errors
2018-03-03 11:13:16 +02:00
Thaum Rystra
f73f2f670f Formatted all existing printed character sheet fields nicely 2018-03-02 21:40:21 +02:00
Thaum Rystra
c6e62e1cfa Added borders to ability scores and AC 2018-03-02 07:25:37 +02:00
Jacob
4e574c0f51 Added "clear" (reset to 0) button to resource cards 2018-02-24 14:49:42 +00:00
Jacob
80b195b7f7 Added reset button to resource cards 2018-02-24 14:42:41 +00:00
Thaum Rystra
67956d9a42 Fixed dropdown boxes being clipped in dialogs, updated Meteor 2018-01-26 18:18:49 +02:00
Stefan Zermatten
64b3ca6066 Added proficiencies to printed sheet; some fixes 2017-12-12 15:54:21 +02:00
Stefan Zermatten
8349f7da9b Merge branch 'master' into feature-print 2017-12-06 09:22:57 +02:00
Stefan Zermatten
0636042878 Added core-js because an error message told me to 2017-11-22 14:34:54 +02:00
Stefan Zermatten
6207ffa516 Updated meteor 2017-11-21 10:53:07 +02:00
Stefan Zermatten
9d33612054 Merge branch 'feature-rate-limiting-api'
closes #141
2017-11-21 08:50:03 +02:00
Stefan Zermatten
face6387a0 Fixed attacks not being added to spells added with multi-add
Fixes #145
2017-11-01 15:55:07 +02:00
Stefan Zermatten
b308595dac Implement both tokens and rate limiting to API
Closes #141, but still needs better UI on failure
2017-10-12 16:24:12 +02:00
Stefan Zermatten
1d2de197a4 Replaced trello with github on home page 2017-09-28 14:04:42 +02:00
Stefan Zermatten
a3d790b47d Merge branch 'feature-manage-user' 2017-09-28 13:47:46 +02:00
Stefan Zermatten
efe6dd87db Added link to change password
closes #78
2017-09-28 13:47:30 +02:00
Stefan Zermatten
5b33a6e783 Fixed username dialog title 2017-09-28 13:22:21 +02:00
Stefan Zermatten
8730fab40b Moved username dialog to own folder 2017-09-28 13:21:14 +02:00
Stefan Zermatten
992776bb40 Merge branch 'feature-onboarding' 2017-09-28 13:18:52 +02:00
Stefan Zermatten
bc9ec4421c Polished onboarding, removed stray logs
closes #102
2017-09-28 13:17:33 +02:00
Stefan Zermatten
4c31ab601c Improved new user experience and fixed errors on character delete 2017-09-28 13:03:54 +02:00
Stefan Zermatten
c4e77c7eae Replaced all paper-tooltip with custom, working version 2017-09-28 11:47:03 +02:00
Stefan Zermatten
2cd6e27f70 Finished basic new user experience 2017-09-28 10:26:45 +02:00
Stefan Zermatten
f6b2dde479 Added basic onboarding steps 2017-09-27 16:19:00 +02:00
Stefan Zermatten
44da62a962 Lowered subscription caching to improve performance 2017-09-26 15:11:03 +02:00
Stefan Zermatten
4e96047e90 Added rate limiting to heavy subscriptions 2017-09-26 14:59:41 +02:00
Stefan Zermatten
212986ac37 Fixed missing charId 2017-09-26 13:54:33 +02:00
Stefan Zermatten
877f516565 Added efficient computation of characters to replace heavy export functionality 2017-09-26 13:36:01 +02:00
Thaum Rystra
750022f0f1 Added missing indices 2017-09-24 04:02:15 +02:00
Stefan Zermatten
614284c73d Updated Meteor and some packages 2017-09-22 12:52:21 +02:00
Stefan Zermatten
6528fc8bab Improved vMix export
closes #138
2017-09-22 11:41:32 +02:00
Stefan Zermatten
020930b2e4 Checked if old hitpoint slider exists before resetting it
closes #135
2017-09-22 11:03:00 +02:00
Stefan Zermatten
dcd76e06e1 Merge branch 'fixbug-137-multiadd-spells-delete-themselves' 2017-09-22 10:43:04 +02:00
Stefan Zermatten
8a58002415 Fixed spells in the library using their library ID's in the spells collection
closes #137
2017-09-22 10:42:34 +02:00
Stefan Zermatten
535fcd77cf Added missing attributes to vmix export 2017-09-13 16:01:05 +02:00
Stefan Zermatten
7c2aed26a4 Fixed vMix export, included vMix parties 2017-09-13 14:01:07 +02:00
Stefan Zermatten
fab052050a Merge branch 'feature-vmix' 2017-09-13 10:01:46 +02:00
Stefan Zermatten
b7bdb141c8 Added basic vMix server side api 2017-09-13 10:01:32 +02:00
Stefan Zermatten
00a050d337 Added basic printing functionality 2017-09-11 09:27:01 +02:00
Stefan Zermatten
0b8fabde14 Merge branch 'enhancement-106' 2017-09-08 10:48:44 +02:00
Stefan Zermatten
3336e177d9 Merge branch 'enhancement-spell-library-style'
closes #106
2017-09-08 10:46:41 +02:00
Stefan Zermatten
2e9440e325 Merge branch 'enhancement-spell-library-style' 2017-09-08 09:47:53 +02:00
Stefan Zermatten
e4ac400cbd Replaced spell library spell selection highlighting with checkboxes
Makes it more intuitive that multiple items can be selected
2017-09-08 09:47:43 +02:00
Stefan Zermatten
f600999c5f Guarded against error when hiding feature description portion of card 2017-09-08 09:46:58 +02:00
Stefan Zermatten
0af905699a Uncommented buffs from spell library 2017-09-08 09:46:00 +02:00
Stefan Zermatten
6e900cfaae Merge pull request #123 from Dumbgenius/spells-improvements
Spells improvements
2017-09-08 08:30:56 +02:00
Stefan Zermatten
1f42d3c622 Merge pull request #128 from Dumbgenius/fix-127
[#127] Arrow library items should give 20 arrows rather than one "Arrows (20)"
2017-09-08 08:29:56 +02:00
Stefan Zermatten
ee453d968f Merge pull request #134 from Dumbgenius/fixbug-attackview-damage-type
attackView now correctly shows damage type
2017-09-07 16:31:26 +02:00
Stefan Zermatten
0850e59b30 Merge pull request #122 from Dumbgenius/feature-22-temp-hp, requires migration
Feature - TempHP as a character attribute,
2017-09-07 16:29:55 +02:00
Stefan Zermatten
f3e44cf033 Moved polymer styling to app-theme 2017-09-07 16:26:09 +02:00
Stefan Zermatten
907f9d15d4 Merge pull request #131 from Dumbgenius/feature-custom-buffs
Feature - custom buffs
2017-09-07 14:42:05 +02:00
Stefan Zermatten
4c3d5d40dd Merge branch 'master' into feature-custom-buffs 2017-09-07 14:41:33 +02:00
Stefan Zermatten
ef0deb20aa Swapped dropdown box for full character list 2017-09-07 14:36:45 +02:00
Stefan Zermatten
b43ee08d75 Fixed some editing bugs on new buffs, improved style 2017-09-07 14:33:59 +02:00
Stefan Zermatten
7e7f1ec997 Merge pull request #132 from Dumbgenius/enhancement-blank-descriptions
Removed blank bit of card on features with no short description
2017-09-07 12:03:32 +02:00
Stefan Zermatten
0f652f5c74 Merge pull request #126 from Dumbgenius/feature-unsharing-characters
Added ability to "unshare" a character.
2017-09-07 12:01:14 +02:00
Stefan Zermatten
3e02875eaf Merge pull request #130 from Dumbgenius/enhancement-85
Added "carried" checkbox to container edit screen (resolves #85)
2017-09-07 11:54:11 +02:00
Stefan Zermatten
b9a5230344 Merge pull request #129 from Dumbgenius/enhancement-84
Added attacks to features (resolves #84)
2017-09-07 11:49:03 +02:00
Stefan Zermatten
28780b96c3 Merge pull request #120 from Dumbgenius/feature-drag-drop-spells
Feature - drag and drop spells
2017-09-07 11:43:34 +02:00
Jacob
bec0b33805 attackView now correctly shows damage type 2017-09-06 18:45:43 +01:00
Jacob
ad9ccbe7ef Removed blank bit of card on features with no short description 2017-09-05 14:31:26 +01:00
Jacob
e2933c2df5 Fixed a couple of places where "Buff" should've been "Condition" 2017-09-05 13:56:15 +01:00
Jacob
87583fdac6 Added buff details to apply buff dialog 2017-09-05 13:48:26 +01:00
Jacob
68e1382aed Buffs/conditions now have a delete button on the list on Stats 2017-09-04 20:36:13 +01:00
Jacob
7b62c82e32 Buff and condition view dialogs now only have the delete button 2017-09-04 20:16:56 +01:00
Jacob
6dd92586a4 Conditions are now separate from buffs, like in #109 2017-09-04 20:16:38 +01:00
Jacob
b3d0db1f02 Merge branch 'master' into feature-custom-buffs 2017-09-04 18:22:04 +01:00
Jacob
5f35c71c9d Added "carried" checkbox to container edit screen
Resolves #85
2017-09-04 15:50:57 +01:00
Jacob
85baf4e5d1 Added attacks to features (resolves #84) 2017-09-04 15:35:52 +01:00
Jacob
15d797131e Fixes #127. Also fixes it for other ammo as well as iron spikes 2017-09-04 15:22:45 +01:00
Stefan Zermatten
06ac9f70c8 Merge pull request #119 from Dumbgenius/srd-fixes
SRD fixes,

Merging this pull request doesn't change the deployed version of DiceCloud, but indicates that the database has been updated using the affected JSON files
2017-09-04 10:50:42 +02:00
Jacob
471a3e274e Refactored code which applies buffs 2017-08-10 02:49:29 +01:00
Jacob
d4031dc4a7 Removed an unnecessary console log 2017-08-09 16:11:09 +01:00
Jacob
18e5ab3f21 Can now set target of buffs
...to "Self only", "Others only", or "Both".
2017-08-09 16:08:19 +01:00
Jacob
c9fe2f17a0 If buff's target is "self", dialog is no longer shown 2017-08-09 15:48:12 +01:00
Jacob
818cb3905f Can now access the name of public characters for buffs
the buffDialog can now access the name of a publicly-available character
so it can display their name as the caster
2017-08-09 15:30:33 +01:00
Jacob
64ef426035 Applying character no longer duplicated in buff apply list 2017-08-09 15:30:25 +01:00
Jacob
2b188f1a8d Merge branch 'master' into feature-custom-buffs
# Conflicts:
#	rpg-docs/client/views/character/inventory/itemDialog/itemDialog.html
#	rpg-docs/client/views/character/spells/spellDialog/spellDialog.html
2017-08-09 15:03:54 +01:00
Jacob
08735ea4f7 Added ability to add/edit buffs 2017-08-09 15:01:20 +01:00
Jacob
bce1b85600 Relocated the buff list to be inline in stats.html 2017-08-09 11:39:18 +01:00
Jacob
0d023e2ba3 Adding conditions now works again 2017-08-09 11:36:45 +01:00
Jacob
dad575de64 Buffs are now split into custom buffs and conditions 2017-08-09 11:20:49 +01:00
Jacob
cb648b4a28 Buffs now display who and what they were applied by. 2017-08-09 11:13:24 +01:00
Jacob
1279137362 Buffs now display attacks and proficiencies in buffDialog 2017-08-09 10:42:34 +01:00
Jacob
3c06529906 buffViewList is now split into "Conditions" and "Buffs" 2017-08-09 10:16:50 +01:00
Jacob
a3b0c6cafd Added a schema/collection pair for CustomBuffs 2017-08-09 10:06:11 +01:00
Jacob
a1d9f7f5bb Added ability to remove self from readers.
Closes #125.
2017-08-09 02:18:46 +01:00
Jacob
3b03e9c71c Spell library now allows you to select multiple spells 2017-07-30 23:27:29 +01:00
Jacob
0eea6f2386 Cantrips now display as "<school> cantrip" rather than "Level 0 <school>"
Also fixed spacing if "(ritual)" tag was not present
2017-07-30 21:20:27 +01:00
Jacob
8f8714d3e5 Altered spacing of headers within baseDialog 2017-07-30 21:14:02 +01:00
Jacob
7c38e8d70a Added newline before At Higher Levels on Healing Word. 2017-07-30 20:22:59 +01:00
Jacob
826859ca3f Added migration to add TempHP to all characters 2017-07-29 22:41:37 +01:00
Jacob
2ca13fbb56 Renamed tempHitPointSlider to extraHitPointSlider in the css 2017-07-29 21:45:02 +01:00
Jacob
6d801e0178 Renamed old "Temporary HP" to "Extra HP" (THP is now the actual attribute) 2017-07-29 21:42:48 +01:00
Jacob
9ddac7d5cd Temporary HP (the attribute) now appears on Stats page 2017-07-29 21:25:05 +01:00
Jacob
7a6f751e30 THP added as a character attribute 2017-07-29 20:09:48 +01:00
Jacob
2389768234 statOrder is now set programatically from an array
This makes it far easier to add new stats, as we don't have to update
every single number manually.
2017-07-29 19:20:26 +01:00
Jacob
c76fe99148 Can now move and copy spells between characters 2017-07-26 21:02:36 +01:00
Jacob
53afaa4f37 CTRL-dragging now copies spells 2017-07-26 19:54:18 +01:00
Jacob
3599b5fbc4 Added basic drag-and-drop functionality between spell lists
Closes #105
2017-07-26 19:23:41 +01:00
Jacob
275fb1ca65 Scrying's tables are now actual Markdown tables 2017-07-21 15:46:33 +01:00
Jacob
d5d937b04a Reincarnate's race table is now an actual Markdown table 2017-07-21 15:38:17 +01:00
Jacob
aa554adbce Teleport's familiarity table is now a table 2017-07-21 15:30:39 +01:00
Jacob
cb739eb207 Removed indents after double-newlines in spells SRD.
They don't actually render in Markdown, and if there was an indent of >=4
spaces it caused the entire next line to be rendered as code (for example
in the Animate Objects spell).
2017-07-21 14:58:29 +01:00
Jacob
66df2ea4aa Animate Objects' stats are now in a table 2017-07-21 14:43:37 +01:00
Stefan Zermatten
73d1419ee9 Merge pull request #114 from Dumbgenius/misc-enhancements
Miscellaneous enhancements
2017-07-21 13:12:22 +02:00
Stefan Zermatten
681ef614c7 Move earth now level 6 2017-07-21 13:03:31 +02:00
Stefan Zermatten
2bdbcb2e79 Fixed speak with animals
#118
2017-07-21 13:02:50 +02:00
Stefan Zermatten
c119fcfbb8 Freezing sphere now level 6
#118
2017-07-21 13:00:28 +02:00
Stefan Zermatten
deb5db8657 Daylight now level 3 2017-07-21 12:59:54 +02:00
Stefan Zermatten
49522580e3 Replaced line breaks with double line breaks in SRD spells 2017-07-21 12:50:50 +02:00
Stefan Zermatten
5b50f20128 Fixed spell attacks from library assignment of default attack bonus 2017-07-21 12:49:04 +02:00
Stefan Zermatten
52fa97c952 removed default values for library attacks where they aren't needed 2017-07-21 12:48:03 +02:00
Stefan Zermatten
e7a5ce8241 Replaced all mentions of saving throws with DC {DC} saving throw 2017-07-21 12:02:17 +02:00
Stefan Zermatten
d1b9043e1f Added change procedure for updating from srd JSON 2017-07-21 11:53:42 +02:00
Stefan Zermatten
9ffc5649f7 Fixed tooltip suffix icons and moved textarea icons outside of textarea 2017-07-21 11:02:02 +02:00
Stefan Zermatten
15e6c12c03 Replaced shitty paper-tooltip with custom css tooltip 2017-07-21 11:01:18 +02:00
Jacob
e89b877326 Merge branch 'fixbug-116' into misc-enhancements 2017-07-20 16:18:49 +01:00
Jacob
aff2f1f438 Parties are now sorted in both character lists. 2017-07-20 16:18:15 +01:00
Jacob
71d1e9e9e8 Added links in the guide to adam-p's Markdown Cheatsheet
and to the original specifaction for Markdown.
2017-07-20 12:21:01 +01:00
Jacob
0696fd8447 Updated the guide to talk about Markdown support as well 2017-07-20 12:10:17 +01:00
Jacob
b2db33e0f3 Added attacks to SRD cantrips
they scale by level: {floor((Level+1)/6)+1}dX
2017-07-20 11:33:44 +01:00
Jacob
0c2842b84a All "At Higher Levels" are now bold and italic, like in the actual PHB 2017-07-19 12:34:01 +01:00
Jacob
789658cfe7 Spells imported from SRD now have attack bonus set by default 2017-07-19 12:31:50 +01:00
Jacob
3be3da777f Now we can use "attackBonus" in spell attacks, make that the default 2017-07-19 12:19:26 +01:00
Jacob
0e53f157d2 Spell attack bonus and DC are now available in spell attacks. 2017-07-19 12:13:49 +01:00
Jacob
24cc4fd2b1 Background proficiencies now correctly open background dialog
Previously, when clicking on non-skill proficiencies from a character's
background, it would open a blank rectangle as its parent dialog. Now, it
correctly opens the Background dialog.
2017-07-18 23:01:14 +01:00
Jacob
1f0ea689dc Multiple instances of the same proficiency are now merged
For tool/language/weapon/armour proficiencies, on the stats/persona page.
2017-07-18 22:33:28 +01:00
Jacob
11adf9da04 Merge branch 'fixbug-107' into misc-enhancements
# Conflicts:
#	rpg-docs/client/views/character/spells/spellDialog/spellDialog.html
2017-07-18 22:05:31 +01:00
Jacob
5ca81056f9 Merge branch 'feature-swap-stat-modifier' into misc-enhancements 2017-07-18 21:58:29 +01:00
Jacob
6fc469f934 Added setting to swap stats and modifiers
Although Schemas.Character was changed, a database migration should
not be required due to the way characterSettings handles it (since
undefined is a falsy value).
2017-07-18 21:58:00 +01:00
Jacob
0c7948afdd Changed all "1 action" casting times to "action" so it displays properly
i.e. it displays as "Evocation action" rather than "Evocation 1 action"
for example

Also did the same for "1 bonus action" -> "bonus action"
2017-07-18 20:24:12 +01:00
Jacob
42ffc79499 Spell list now displays whether a spell requires GP. 2017-07-18 20:22:44 +01:00
Jacob
0240209410 Removed buffViewList that shouldn't have been there 2017-07-18 20:20:04 +01:00
Jacob
c4c1afa669 Merge branch 'guide-update-1.2.8' into misc-enhancements
# Conflicts:
#	rpg-docs/client/views/guide/guide.css
#	rpg-docs/client/views/guide/guide.html
2017-07-18 19:58:35 +01:00
Jacob
47ac090e9d Merge branch 'testing_dg' into misc-enhancements
# Conflicts:
#	rpg-docs/.meteor/packages
2017-07-18 19:56:26 +01:00
Jacob
aadc83391f Adding an attack to a spell now has attack bonus set by the spell list
Default damage is also changed to 1d10 fire for spells
2017-07-18 19:52:12 +01:00
Jacob
54fb398056 Merge branch 'master' into guide-update-1.2.8 2017-07-18 11:11:37 +01:00
Jacob
073094f6dd Merge branch 'master' into fixbug-107 2017-07-18 11:11:14 +01:00
Jacob
bb95fe0d9a Merge branch 'master' into feature-conditions 2017-07-18 11:10:04 +01:00
Jacob
832ed0c1ff A sort of hacky fix to the issue - text wrapping doesn't quite work 2017-07-17 04:43:39 +01:00
Jacob
17a390a8aa Changed Model of Buffs for forwards compatibility
Added a parent attribute and made Buffs a child, so that buffs can be
added to items, spells and features.
2017-07-17 02:41:51 +01:00
Jacob
e65a2db0d2 Changed guide to use Markdown rather than HTML formatting (mostly)
This is both for ease of editing and so that that it can be copied
into the GitHub wiki.

The <iron-icon>s are left in.
2017-07-16 12:07:13 +01:00
Jacob
b3ef43eb70 Added ability to add/remove basic conditions
Conditions are those listed in ./lib/methods/conditions.js
2017-07-16 04:27:32 +01:00
Jacob
be92ef224c Preliminary additions 2017-07-15 19:54:23 +01:00
Jacob
6729bcad64 Updated the guide to reflect more recent updates (1.2.8) 2017-07-15 17:13:06 +01:00
Jacob
7af07e7ba3 Updated the guide to reflect more recent updates. 2017-07-15 17:07:39 +01:00
Jacob
f44914ab84 Added six extra loading hints. 2017-07-15 15:39:51 +01:00
Jacob
59c7eff46a Hit Dice are now called "d6 Hit Dice", etc. rather than just "d6" 2017-07-15 15:17:14 +01:00
Stefan Zermatten
2a332a2965 Merge branch 'feature-parties' 2017-07-14 17:09:47 +02:00
Stefan Zermatten
44a1daf6f8 Grouping characters by party now works
closes #75, finally
2017-07-14 17:09:30 +02:00
Stefan Zermatten
ac23afac5d Stopped characters with poor names from having failed URL's 2017-07-13 17:16:23 +02:00
Stefan Zermatten
a411fb2b43 Fixed migration for failed slug strings 2017-07-13 16:44:23 +02:00
Stefan Zermatten
35b6fe20ae Reset migrations to line up with current DiceCloud server 2017-07-13 16:16:00 +02:00
Stefan Zermatten
54f055d3ef Merge branch 'bugfix-model' 2017-07-13 16:03:23 +02:00
Stefan Zermatten
2bacb296ba Fixed new effects dialog animation
makes some progress on #72
2017-07-13 16:01:43 +02:00
Stefan Zermatten
8b061f7aa9 Improved look and feel of effect editing 2017-07-13 15:53:03 +02:00
Stefan Zermatten
6a84c83644 Made passive scores show up for all skills with bonuses to passive score
closes #56
2017-07-13 13:42:04 +02:00
Stefan Zermatten
3227cd0934 Add character names to the end of their URL's
Closes #21, makes links to characters human readable
2017-07-13 13:14:04 +02:00
Stefan Zermatten
089feae26f Spell slots are now available in calculations
closes #11
2017-07-13 10:56:42 +02:00
Stefan Zermatten
99c72d1e10 Made names optional for all collections
closes #92
2017-07-13 10:39:43 +02:00
Stefan Zermatten
1e67afbe6f Made DC available in spell descriptions as a variable
closes #101
2017-07-13 10:30:16 +02:00
Stefan Zermatten
1cfec1ca45 Allowed carryMultiplier to be a decimal
closes #100
2017-07-13 09:50:20 +02:00
Stefan Zermatten
09d1ac9ba3 Merge branch 'bugfix-improved-initiative' 2017-07-11 08:53:46 +02:00
Stefan Zermatten
834b9cf384 Added passive perception to improved initiative export
closes #97
2017-07-11 08:52:47 +02:00
Stefan Zermatten
37291b347a Fixed vulnerabilities, resistances, immunities export
closes #96
2017-07-11 08:42:21 +02:00
Stefan Zermatten
efdfbeb59e Moved export to lib 2017-07-11 08:12:52 +02:00
Thaum Rystra
a165f9b253 Merge branch 'feature-improved-initiative-export'
closes #95
2017-07-10 21:02:10 +02:00
Thaum Rystra
4a6fca98bc Cleaned up export dialog, fixed copying 2017-07-10 20:59:00 +02:00
Stefan Zermatten
35464128a0 Added basic export for improved initiative 2017-07-10 16:26:38 +02:00
Stefan Zermatten
398f8a8a2a Merge branch 'bugfix-style' 2017-06-28 09:33:20 +02:00
Stefan Zermatten
812a1784b2 Improved display of home page on smaller devices 2017-06-28 09:27:24 +02:00
Stefan Zermatten
8fa9cd0148 Prevented spell titles from overflowing their item in spell lists
closes #83
2017-06-28 09:26:52 +02:00
Stefan Zermatten
0e0662cc9a Added slightly specific rule to let headings wrap
closes #90
2017-06-28 09:26:13 +02:00
Stefan Zermatten
ad4e3f5b20 Stopped inventory items being separated from their containers
hopefully fixes #91
2017-06-28 09:25:25 +02:00
Stefan Zermatten
4cd058e1fe Set max of slider before value
Prevents unnecessary capping of temp HP to 100
Fixes #89
2017-06-08 09:48:51 +02:00
Stefan Zermatten
8f30cee4d3 Hotfixed column layout with translateZ(0) hack 2017-06-07 09:46:38 +02:00
Stefan Zermatten
d7f7eb2e6a Fixed some spells in the library 2017-06-07 09:35:59 +02:00
452 changed files with 10738 additions and 1889 deletions

View File

@@ -8,6 +8,6 @@ Getting started
`git clone https://github.com/ThaumRystra/DiceCloud1 dicecloud`
`cd dicecloud`
`cd rpg-docs`
`cd app`
`bower install`
`meteor`

View File

@@ -13,3 +13,6 @@ notices-for-facebook-graph-api-2
1.3.0-split-minifiers-package
1.4.0-remove-old-dev-bundle-link
1.4.1-add-shell-server-package
1.4.3-split-account-service-packages
1.5-add-dynamic-import-package
1.7-split-underscore-from-meteor-base

View File

@@ -3,10 +3,9 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
iron:router
accounts-password@1.3.3
accounts-ui@1.1.9
random@1.0.10
accounts-password@1.5.1
accounts-ui@1.3.0
random@1.1.0
dburles:collection-helpers
reactive-var@1.0.11
underscore@1.0.10
@@ -18,33 +17,40 @@ dburles:mongo-collection-instances
percolate:migrations
ecwyne:mathjs
useraccounts:polymer
accounts-google@1.0.11
accounts-google@1.3.1
splendido:accounts-meld
email@1.1.18
email@1.2.3
meteorhacks:subs-manager
chuangbo:marked
reywood:iron-router-ga
meteor-base@1.0.4
mobile-experience@1.0.4
mongo@1.1.14
meteor-base@1.4.0
mobile-experience@1.0.5
mongo@1.5.0
blaze-html-templates
session@1.1.7
jquery@1.11.10
tracker@1.1.1
logging@1.1.16
reload@1.1.11
ejson@1.0.13
tracker@1.2.0
logging@1.1.20
reload@1.2.0
ejson@1.1.0
spacebars
check@1.2.4
check@1.3.1
useraccounts:iron-routing
wizonesolutions:canonical
standard-minifier-js@1.2.1
shell-server@0.2.1
standard-minifier-js@2.3.4
shell-server@0.3.1
seba:minifiers-autoprefixer
nikogosovd:multiple-uihooks
templates:array
ecmascript@0.6.1
es5-shim@4.6.15
ecmascript@0.11.1
es5-shim@4.8.0
differential:vulcanize
reactive-dict
reactive-dict@1.2.0
percolate:synced-cron
ongoworks:speakingurl
service-configuration@1.0.11
google-config-ui@1.0.0
dynamic-import@0.4.0
ddp-rate-limiter@1.0.7
rate-limit@1.0.9
iron:router

1
app/.meteor/release Normal file
View File

@@ -0,0 +1 @@
METEOR@1.7.0.3

136
app/.meteor/versions Normal file
View File

@@ -0,0 +1,136 @@
accounts-base@1.4.2
accounts-google@1.3.1
accounts-oauth@1.1.15
accounts-password@1.5.1
accounts-ui@1.3.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.4.1
babel-compiler@7.1.1
babel-runtime@1.2.2
base64@1.0.11
binary-heap@1.0.10
blaze@2.3.2
blaze-html-templates@1.1.2
blaze-tools@1.0.10
boilerplate-generator@1.5.0
caching-compiler@1.1.12
caching-html-compiler@1.1.3
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.2.0
deps@1.0.12
diff-sequence@1.1.0
differential:vulcanize@3.0.0
dynamic-import@0.4.1
ecmascript@0.11.1
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.7.1
ecmascript-runtime-server@0.7.0
ecwyne:mathjs@0.25.0
ejson@1.1.0
email@1.2.3
es5-shim@4.8.0
geojson-utils@1.0.10
google-config-ui@1.0.0
google-oauth@1.2.5
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
http@1.4.1
id-map@1.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.1.1
less@2.7.12
livedata@1.0.18
localstorage@1.2.0
logging@1.1.20
matb33:collection-hooks@0.8.4
mdg:validation-error@0.5.1
meteor@1.9.2
meteor-base@1.4.0
meteorhacks:subs-manager@1.6.4
minifier-css@1.3.1
minifier-js@2.3.5
minimongo@1.4.4
mobile-experience@1.0.5
mobile-status-bar@1.0.14
modern-browsers@0.1.2
modules@0.12.2
modules-runtime@0.10.2
momentjs:moment@2.22.2
mongo@1.5.1
mongo-dev-server@1.1.0
mongo-id@1.0.7
nikogosovd:multiple-uihooks@0.1.8
npm-bcrypt@0.9.3
npm-mongo@3.0.11
oauth@1.2.3
oauth2@1.2.0
observe-sequence@1.0.16
ongoworks:speakingurl@9.0.0
ordered-dict@1.1.0
percolate:migrations@0.9.8
percolate:synced-cron@1.3.2
promise@0.11.1
raix:eventemitter@0.1.3
random@1.1.0
rate-limit@1.0.9
reactive-dict@1.2.0
reactive-var@1.0.11
reload@1.2.0
retry@1.1.0
reywood:iron-router-ga@0.7.1
routepolicy@1.0.13
seba:minifiers-autoprefixer@1.0.1
service-configuration@1.0.11
session@1.1.7
sha@1.0.9
shell-server@0.3.1
socket-stream-client@0.2.2
softwarerero:accounts-t9n@1.3.11
spacebars@1.0.15
spacebars-compiler@1.1.3
splendido:accounts-emails-field@1.2.0
splendido:accounts-meld@1.3.1
srp@1.0.10
standard-minifier-js@2.3.4
templates:array@1.0.3
templating@1.3.2
templating-compiler@1.3.3
templating-runtime@1.3.2
templating-tools@1.1.2
tracker@1.2.0
ui@1.0.13
underscore@1.0.10
url@1.2.0
useraccounts:core@1.14.2
useraccounts:iron-routing@1.14.2
useraccounts:polymer@1.14.2
webapp@1.6.2
webapp-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,42 @@
Parties = new Mongo.Collection("parties");
Schemas.Party = new SimpleSchema({
name: {
type: String,
defaultValue: "New Party",
trim: false,
optional: true,
},
characters: {
type: [String],
regEx: SimpleSchema.RegEx.Id,
index: 1,
defaultValue: [],
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});
Parties.attachSchema(Schemas.Party);
Parties.allow({
insert: function(userId, doc) {
return userId && doc.owner === userId;
},
update: function(userId, doc, fields, modifier) {
return userId && doc.owner === userId;
},
remove: function(userId, doc) {
return userId && doc.owner === userId;
},
fetch: ["owner"],
});
Parties.deny({
update: function(userId, docs, fields, modifier) {
// can't change owners
return _.contains(fields, "owner");
}
});

View File

@@ -11,10 +11,12 @@ Schemas.Action = new SimpleSchema({
},
name: {
type: String,
optional: true,
trim: false,
},
description: {
type: String,
optional: true,
trim: false,
},
type: {

View File

@@ -12,6 +12,7 @@ Schemas.Attack = new SimpleSchema({
name: {
type: String,
defaultValue: "New Attack",
optional: true,
trim: false,
},
details: {

View File

@@ -8,6 +8,7 @@ Schemas.Buff = new SimpleSchema({
},
name: {
type: String,
optional: true,
trim: false,
},
description: {
@@ -22,7 +23,7 @@ Schemas.Buff = new SimpleSchema({
type: {
type: String,
allowedValues: [
"inate",
"inate", //this should be "innate", but changing it could be problematic
"custom",
],
},
@@ -41,12 +42,26 @@ Schemas.Buff = new SimpleSchema({
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
makeParent(Buffs, ["name", "enabled"]); //parents of effects, attacks, proficiencies
Buffs.allow(CHARACTER_SUBSCHEMA_ALLOW);
Buffs.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -4,6 +4,7 @@ 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},
@@ -26,6 +27,7 @@ Schemas.Character = new SimpleSchema({
//stats
hitPoints: {type: Schemas.Attribute},
tempHP: {type: Schemas.Attribute},
experience: {type: Schemas.Attribute},
proficiencyBonus: {type: Schemas.Attribute},
speed: {type: Schemas.Attribute},
@@ -162,9 +164,9 @@ Schemas.Character = new SimpleSchema({
//permissions
party: {type: String, regEx: SimpleSchema.RegEx.Id, optional: true},
owner: {type: String, regEx: SimpleSchema.RegEx.Id},
readers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []},
writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []},
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"),
@@ -183,7 +185,13 @@ Schemas.Character = new SimpleSchema({
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);
@@ -254,7 +262,10 @@ var attributeBase = preventLoop(function(charId, statName){
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);
});
@@ -274,6 +285,7 @@ if (Meteor.isClient) {
//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;
@@ -287,6 +299,7 @@ Characters.calculate = {
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(
@@ -320,6 +333,7 @@ Characters.calculate = {
},
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
@@ -331,6 +345,7 @@ Characters.calculate = {
}),
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);
@@ -382,7 +397,6 @@ Characters.calculate = {
return prof && prof.value || 0;
}),
passiveSkill: memoize(function(charId, skillName){
var skill = Characters.calculate.getField(charId, skillName);
var mod = +Characters.calculate.skillMod(charId, skillName);
var value = 10 + mod;
Effects.find(
@@ -525,6 +539,7 @@ if (Meteor.isServer){
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});
@@ -534,6 +549,19 @@ if (Meteor.isServer){
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({
@@ -555,7 +583,7 @@ Characters.allow({
Characters.deny({
update: function(userId, docs, fields, modifier) {
// can't change owners
return _.contains(fields, "owner");
// can't change owners unless you are the current owner
return _.contains(fields, "owner") && doc.owner !== userId;
}
});

View File

@@ -2,7 +2,7 @@ Classes = new Mongo.Collection("classes");
Schemas.Class = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, trim: false},
name: {type: String, optional: true, trim: false},
level: {type: Number},
createdAt: {
type: Date,

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

@@ -2,7 +2,7 @@ Experiences = new Mongo.Collection("experience");
Schemas.Experience = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, defaultValue: "New Experience", trim: false},
name: {type: String, optional: true, trim: false, defaultValue: "New Experience"},
description: {type: String, optional: true, trim: false},
value: {type: Number, defaultValue: 0},
dateAdded: {

View File

@@ -2,7 +2,7 @@ Features = new Mongo.Collection("features");
Schemas.Feature = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, trim: false},
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},

View File

@@ -2,7 +2,7 @@ Notes = new Mongo.Collection("notes");
Schemas.Note = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, trim: false},
name: {type: String, optional: true, trim: false},
description: {type: String, optional: true, trim: false},
color: {
type: String,

View File

@@ -2,7 +2,7 @@ SpellLists = new Mongo.Collection("spellLists");
Schemas.SpellLists = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
name: {type: String, trim: false},
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},

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

@@ -2,7 +2,7 @@
Containers = new Mongo.Collection("containers");
Schemas.Container = new SimpleSchema({
name: {type: String, trim: false},
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},

View File

@@ -1,7 +1,7 @@
Items = new Mongo.Collection("items");
Schemas.Item = new SimpleSchema({
name: {type: String, defaultValue: "New Item", trim: false},
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

View File

@@ -11,13 +11,11 @@ Schemas.LibraryAttacks = new SimpleSchema({
},
attackBonus: {
type: String,
defaultValue: "strengthMod + proficiencyBonus",
optional: true,
trim: false,
},
damage: {
type: String,
defaultValue: "1d8 + {strengthMod}",
optional: true,
trim: false,
},

View File

@@ -1,6 +1,7 @@
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},

View File

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

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

@@ -0,0 +1,105 @@
Schemas.UserProfile = new SimpleSchema({
username: {
type: String,
optional: true,
},
});
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;
}
};

View File

@@ -13,6 +13,11 @@ Router.plugin("ensureSignedIn", {
Router.plugin("dataNotFound", {notFoundTemplate: "notFound"});
var handleSubError = function(e){
Session.set("error", {reason: e.reason, href: location.href});
Router.go("/error");
};
Router.map(function() {
this.route("/", {
name: "home",
@@ -24,7 +29,7 @@ Router.map(function() {
this.route("characterList", {
path: "/characterList",
waitOn: function(){
return subsManager.subscribe("characterList", Meteor.userId());
return subsManager.subscribe("characterList");
},
onAfterAction: function() {
document.title = appName + " - Characters";
@@ -32,11 +37,31 @@ Router.map(function() {
fastRender: true,
});
this.route("characterSheet", {
path: "/character/:_id",
this.route("characterSheetNaked", {
path: "/character/:_id/",
waitOn: function(){
return [
subsManager.subscribe("singleCharacter", this.params._id, Meteor.userId()),
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() {
@@ -62,6 +87,37 @@ Router.map(function() {
fastRender: true,
});
this.route("printedCharacterSheet", {
path: "/character/:_id/:urlName/print",
waitOn: function(){
return [
subsManager.subscribe(
"singleCharacter", this.params._id, {onError: handleSubError}
),
];
},
data: function() {
var data = Characters.findOne(
{_id: this.params._id},
{fields: {_id: 1, name: 1, color: 1, writers: 1, readers: 1}}
);
return data;
},
onAfterAction: function() {
var char = Characters.findOne({_id: this.params._id}, {fields: {name: 1}});
var name = char && char.name;
if (name){
document.title = name + " - Printing";
}
},
//analytics
trackPageView: false,
onRun: function() {
window.ga && window.ga("send", "pageview", "/print-character");
this.next();
},
});
this.route("library", {
path: "/library",
waitOn: function(){
@@ -108,4 +164,11 @@ Router.map(function() {
document.title = appName;
},
});
this.route("/error", {
name: "error",
onAfterAction: function() {
document.title = `${appName} - Error`;
},
});
});

View File

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

View File

@@ -3,7 +3,8 @@ Template.registerHelper("canEditCharacter", function(charId) {
});
canEditCharacter = function(charId) {
var char = Characters.findOne(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

@@ -24,6 +24,10 @@ 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(

View File

@@ -18,6 +18,9 @@ openParentDialog = function({
} 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,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

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

@@ -4,6 +4,7 @@
column-gap: 0px;
column-width: 304px;
padding: 4px;
transform: translateZ(0);
}
.column-container.thin-columns {
@@ -22,6 +23,7 @@
.card {
background: white;
border-radius: 2px;
position: initial;
}
.card .top {

View File

@@ -1,3 +1,7 @@
body .paper-font-display4, body .paper-font-display3, body .paper-font-title, body .paper-font-caption{
white-space: normal;
}
.white-text {
color: #dedede;
color: rgba(255,255,255,0.87);

View File

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

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

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

@@ -3,8 +3,8 @@
<hr style="margin: 16px 0 16px 0;">
<div class="attacks">
<div class="spaceAfter paper-font-title">Attacks</div>
{{#each attacks}}
{{> attackView}}
{{#each attack in attacks}}
{{> attackView attack=attack charId=charId}}
{{/each}}
</div>
{{/if}}

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>

View File

@@ -0,0 +1,41 @@
Template.customBuffEditList.helpers({
buffs: function(){
var selector = {
"parent.id": this.parentId,
"charId": this.charId,
};
return CustomBuffs.find(selector);
}
});
Template.customBuffEditList.events({
"tap #addBuffButton": function(event, instance){
if (!_.isBoolean(this.enabled)) {
this.enabled = true;
}
const customBuffId = CustomBuffs.insert({
name: this.name || "New Buff",
charId: this.charId,
parent: {
id: this.parentId,
collection: this.parentCollection,
},
});
pushDialogStack({
template: "customBuffEdit",
data: {customBuffId},
element: event.currentTarget,
returnElement: () => instance.find(`tr.buff[data-id='${customBuffId}']`),
});
},
});
Template.customBuffEditListItem.events({
"tap .edit-buff": function(event, template){
pushDialogStack({
template: "customBuffEdit",
data: {customBuffId: this.buff._id},
element: event.currentTarget.parentElement.parentElement,
});
},
});

View File

@@ -0,0 +1,8 @@
<template name="customBuffView">
<div class="flex">{{buff.name}}</div>
<div class="flex">
{{#if canEditCharacter buff.charId}}
<paper-button class="apply-buff-button">Apply{{toSelf}}</paper-button>
{{/if}}
</div>
</template>

View File

@@ -0,0 +1,82 @@
const applyBuff = function(targetId, buff) {
var parent = global[buff.parent.collection].findOne(buff.parent.id);
//insert new buff
newBuffId = Buffs.insert({
charId: targetId,
name: buff.name,
description: buff.description,
lifeTime: {total: buff.lifeTime.total},
type: "custom",
appliedBy: buff.charId,
appliedByDetails: {
name: parent.name,
collection: buff.parent.collection,
},
});
//insert children
Attacks.find({"parent.id": buff._id}).forEach(function(doc){
temp = _.clone(doc);
temp.parent.id = newBuffId;
temp.parent.collection = "Buffs";
delete temp._id;
Attacks.insert(temp);
});
Effects.find({"parent.id": buff._id}).forEach(function(doc){
temp = _.clone(doc);
temp.parent.id = newBuffId;
temp.parent.collection = "Buffs";
delete temp._id;
Effects.insert(temp);
});
Proficiencies.find({"parent.id": buff._id}).forEach(function(doc){
temp = _.clone(doc);
temp.parent.id = newBuffId;
temp.parent.collection = "Buffs";
delete temp._id;
Proficiencies.insert(temp);
});
let target;
if (targetId == buff.charId) {
target = "self";
} else {
target = Characters.findOne(targetId) || {};
target = target && target.name || "target"
}
GlobalUI.toast(`${buff.name || "Buff"} applied to ${target}`);
};
Template.customBuffView.helpers({
toSelf: function() {
if (this.buff.target === "self") {
return " to self";
} else {
return "";
}
}
});
Template.customBuffView.events({
"click .apply-buff-button": function(){
if (this.buff.target !== "self") {
pushDialogStack({
template: "applyBuffDialog",
data: {buff: this.buff},
element: event.currentTarget,
callback: (targetId) => {
if (!targetId) return;
applyBuff(targetId, this.buff);
},
});
} else {
var targetId = this.buff.charId;
applyBuff(targetId, this.buff);
}
},
});

View File

@@ -0,0 +1,14 @@
<template name="customBuffViewList">
{{#if buffs.count}}
<div class="buffs">
<div class="paper-font-title" style="margin-bottom: 8px;">
Buffs
</div>
{{#each buff in buffs}}
<div class="layout horizontal center">
{{> customBuffView buff=buff}}
</div>
{{/each}}
</div>
{{/if}}
</template>

View File

@@ -0,0 +1,9 @@
Template.customBuffViewList.helpers({
buffs: function(){
var selector = {
"parent.id": this.parentId,
"charId": this.charId,
};
return CustomBuffs.find(selector);
}
});

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