Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b8fabde14 | ||
|
|
3336e177d9 | ||
|
|
2e9440e325 | ||
|
|
e4ac400cbd | ||
|
|
f600999c5f | ||
|
|
0af905699a | ||
|
|
6e900cfaae | ||
|
|
1f42d3c622 | ||
|
|
ee453d968f | ||
|
|
0850e59b30 | ||
|
|
f3e44cf033 | ||
|
|
907f9d15d4 | ||
|
|
4c3d5d40dd | ||
|
|
ef0deb20aa | ||
|
|
b43ee08d75 | ||
|
|
7e7f1ec997 | ||
|
|
0f652f5c74 | ||
|
|
3e02875eaf | ||
|
|
b9a5230344 | ||
|
|
28780b96c3 | ||
|
|
bec0b33805 | ||
|
|
ad9ccbe7ef | ||
|
|
e2933c2df5 | ||
|
|
87583fdac6 | ||
|
|
68e1382aed | ||
|
|
7b62c82e32 | ||
|
|
6dd92586a4 | ||
|
|
b3d0db1f02 | ||
|
|
5f35c71c9d | ||
|
|
85baf4e5d1 | ||
|
|
15d797131e | ||
|
|
06ac9f70c8 | ||
|
|
471a3e274e | ||
|
|
d4031dc4a7 | ||
|
|
18e5ab3f21 | ||
|
|
c9fe2f17a0 | ||
|
|
818cb3905f | ||
|
|
64ef426035 | ||
|
|
2b188f1a8d | ||
|
|
08735ea4f7 | ||
|
|
bce1b85600 | ||
|
|
0d023e2ba3 | ||
|
|
dad575de64 | ||
|
|
cb648b4a28 | ||
|
|
1279137362 | ||
|
|
3c06529906 | ||
|
|
a3b0c6cafd | ||
|
|
a1d9f7f5bb | ||
|
|
3b03e9c71c | ||
|
|
0eea6f2386 | ||
|
|
8f8714d3e5 | ||
|
|
7c38e8d70a | ||
|
|
826859ca3f | ||
|
|
2ca13fbb56 | ||
|
|
6d801e0178 | ||
|
|
9ddac7d5cd | ||
|
|
7a6f751e30 | ||
|
|
2389768234 | ||
|
|
c76fe99148 | ||
|
|
53afaa4f37 | ||
|
|
3599b5fbc4 | ||
|
|
275fb1ca65 | ||
|
|
d5d937b04a | ||
|
|
aa554adbce | ||
|
|
cb739eb207 | ||
|
|
66df2ea4aa | ||
|
|
73d1419ee9 | ||
|
|
681ef614c7 | ||
|
|
2bdbcb2e79 | ||
|
|
c119fcfbb8 | ||
|
|
deb5db8657 | ||
|
|
49522580e3 | ||
|
|
5b50f20128 | ||
|
|
52fa97c952 | ||
|
|
e7a5ce8241 | ||
|
|
d1b9043e1f | ||
|
|
9ffc5649f7 | ||
|
|
15e6c12c03 | ||
|
|
e89b877326 | ||
|
|
aff2f1f438 | ||
|
|
71d1e9e9e8 | ||
|
|
0696fd8447 | ||
|
|
b2db33e0f3 | ||
|
|
0c2842b84a | ||
|
|
789658cfe7 | ||
|
|
3be3da777f | ||
|
|
0e53f157d2 | ||
|
|
24cc4fd2b1 | ||
|
|
1f0ea689dc | ||
|
|
11adf9da04 | ||
|
|
5ca81056f9 | ||
|
|
6fc469f934 | ||
|
|
0c7948afdd | ||
|
|
42ffc79499 | ||
|
|
0240209410 | ||
|
|
c4c1afa669 | ||
|
|
47ac090e9d | ||
|
|
aadc83391f | ||
|
|
54fb398056 | ||
|
|
073094f6dd | ||
|
|
bb95fe0d9a | ||
|
|
832ed0c1ff | ||
|
|
17a390a8aa | ||
|
|
e65a2db0d2 | ||
|
|
b3ef43eb70 | ||
|
|
be92ef224c | ||
|
|
6729bcad64 | ||
|
|
7af07e7ba3 | ||
|
|
f44914ab84 | ||
|
|
59c7eff46a | ||
|
|
2a332a2965 | ||
|
|
44a1daf6f8 |
@@ -21,32 +21,40 @@
|
||||
"weight": 1
|
||||
},
|
||||
{
|
||||
"name": "Arrows (20)",
|
||||
"plural": "Arrows (20)",
|
||||
"libraryName": "Arrows (20)",
|
||||
"name": "Arrow",
|
||||
"plural": "Arrows",
|
||||
"description": "",
|
||||
"value": 1,
|
||||
"weight": 1
|
||||
"value": 0.05,
|
||||
"weight": 0.05,
|
||||
"quantity": 20
|
||||
},
|
||||
{
|
||||
"name": "Blowgun needles (5)",
|
||||
"plural": "Blowgun needles (5)",
|
||||
"libraryName": "Blowgun needles (5)",
|
||||
"name": "Blowgun needle",
|
||||
"plural": "Blowgun needles",
|
||||
"description": "",
|
||||
"value": 1,
|
||||
"weight": 1
|
||||
"value": 0.2,
|
||||
"weight": 0.2,
|
||||
"quantity": 5
|
||||
},
|
||||
{
|
||||
"name": "Crossbow bolts (20)",
|
||||
"plural": "Crossbow bolts (20)",
|
||||
"libraryName": "Crossbow bolts (20)",
|
||||
"name": "Crossbow bolt",
|
||||
"plural": "Crossbow bolts",
|
||||
"description": "",
|
||||
"value": 1,
|
||||
"weight": 1.5
|
||||
"value": 0.05,
|
||||
"weight": 0.075,
|
||||
"quantity": 20
|
||||
},
|
||||
{
|
||||
"name": "Sling bullets (20)",
|
||||
"plural": "Sling bullets (20)",
|
||||
"libraryName": "Sling bullets (20)",
|
||||
"name": "Sling bullet",
|
||||
"plural": "Sling bullets",
|
||||
"description": "",
|
||||
"value": 0.04,
|
||||
"weight": 1.5
|
||||
"value": 0.002,
|
||||
"weight": 0.075,
|
||||
"quantity": 20
|
||||
},
|
||||
{
|
||||
"name": "Antitoxin (vial)",
|
||||
@@ -651,11 +659,13 @@
|
||||
"weight": 3
|
||||
},
|
||||
{
|
||||
"name": "Spikes, iron (10)",
|
||||
"plural": "Spikes, iron (10)",
|
||||
"libraryName": "Spikes, iron (10)",
|
||||
"name": "Spike, iron",
|
||||
"plural": "Spikes, iron",
|
||||
"description": "",
|
||||
"value": 1,
|
||||
"weight": 5
|
||||
"value": 0.1,
|
||||
"weight": 0.5,
|
||||
"quantity": 10
|
||||
},
|
||||
{
|
||||
"name": "Spyglass",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
// This all gets run in the console by an admin.
|
||||
// Probably a good idea to reset the server after running big updates
|
||||
// Only do if the library doesn't exist yet
|
||||
id = Libraries.insert({
|
||||
_id: "SRDLibraryGA3XWsd",
|
||||
@@ -5,6 +7,8 @@ id = Libraries.insert({
|
||||
name: "SRD Library",
|
||||
});
|
||||
|
||||
// First copy-paste the JSON into your console like `items = <pasted JSON>`
|
||||
// First import, don't do this if the library is already populated
|
||||
_.each(items, (item) => {
|
||||
item.settings = {category: }; // "adventuringGear", "armor", "weapons", "tools"
|
||||
item.library = "SRDLibraryGA3XWsd"
|
||||
@@ -15,3 +19,38 @@ _.each(spells, (spell) => {
|
||||
spell.library = "SRDLibraryGA3XWsd"
|
||||
LibrarySpells.insert(spell)
|
||||
});
|
||||
|
||||
// Update the library using names as keys
|
||||
// Make sure you're subscribed to all item categories
|
||||
handles = _.map(["weapons", "armor", "adventuringGear", "tools"],
|
||||
category => Meteor.subscribe("standardLibraryItems", category)
|
||||
);
|
||||
// Wait until all the handles are ready
|
||||
handles.map(h => h.ready()); // must reaturn [...true]
|
||||
|
||||
_.each(items, (item) => {
|
||||
var existingItem = LibraryItems.findOne({
|
||||
library: "SRDLibraryGA3XWsd",
|
||||
name: item.name,
|
||||
});
|
||||
if (!existingItem) return;
|
||||
_.each(item.attacks, attack => Schemas.LibraryAttacks.clean(attack));
|
||||
LibraryItems.update(existingItem._id, {$set: item});
|
||||
});
|
||||
|
||||
// Make sure you're subscribed to all spell categories
|
||||
handles = _.map([0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
category => Meteor.subscribe("standardLibrarySpells", category)
|
||||
);
|
||||
// Wait until all the handles are ready
|
||||
handles.map(h => h.ready()); // must reaturn [...true]
|
||||
|
||||
_.each(spells, (spell) => {
|
||||
var existingSpell = LibrarySpells.findOne({
|
||||
library: "SRDLibraryGA3XWsd",
|
||||
name: spell.name,
|
||||
});
|
||||
if (!existingSpell) return;
|
||||
_.each(spell.attacks, attack => Schemas.LibraryAttacks.clean(attack));
|
||||
LibrarySpells.update(existingSpell._id, {$set: spell});
|
||||
});
|
||||
|
||||
@@ -49,3 +49,4 @@ differential:vulcanize
|
||||
reactive-dict
|
||||
percolate:synced-cron
|
||||
ongoworks:speakingurl
|
||||
service-configuration
|
||||
|
||||
@@ -1,8 +1,42 @@
|
||||
Parties = new Mongo.Collection("parties");
|
||||
|
||||
Schemas.Party = new SimpleSchema({
|
||||
//each character/monster can only be in one party at a time
|
||||
//each party can only be in a single instance at a time
|
||||
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");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ Schemas.Buff = new SimpleSchema({
|
||||
type: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
"inate",
|
||||
"inate", //this should be "innate", but changing it could be problematic
|
||||
"custom",
|
||||
],
|
||||
},
|
||||
@@ -42,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);
|
||||
|
||||
@@ -27,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},
|
||||
@@ -185,6 +186,7 @@ Schemas.Character = new SimpleSchema({
|
||||
defaultValue: "whitelist",
|
||||
allowedValues: ["whitelist", "public"],
|
||||
},
|
||||
"settings.swapStatAndModifier": {type: Boolean, defaultValue: false},
|
||||
"settings.exportFeatures": {type: Boolean, defaultValue: true},
|
||||
"settings.exportAttacks": {type: Boolean, defaultValue: true},
|
||||
"settings.exportDescription": {type: Boolean, defaultValue: true},
|
||||
@@ -532,6 +534,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});
|
||||
|
||||
42
rpg-docs/Model/Character/Conditions.js
Normal file
42
rpg-docs/Model/Character/Conditions.js
Normal 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);
|
||||
53
rpg-docs/Model/Character/CustomBuffs.js
Normal file
53
rpg-docs/Model/Character/CustomBuffs.js
Normal 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);
|
||||
@@ -83,3 +83,170 @@ Spells.after.update(function (userId, spell, fieldNames) {
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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});
|
||||
};
|
||||
|
||||
@@ -7,12 +7,32 @@ Template.attackEditList.helpers({
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}} {{damageType}}
|
||||
{{evaluateDamage charId attack}} {{attack.damageType}}
|
||||
</div>
|
||||
{{#if details}}
|
||||
{{#if attack.details}}
|
||||
<div class="paper-font-caption">
|
||||
{{details}}
|
||||
{{attack.details}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -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}}
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
@@ -1,15 +1,23 @@
|
||||
<template name="buffDialog">
|
||||
{{#with buff}}
|
||||
{{#baseDialog title=name class=colorClass hideEdit=true}}
|
||||
{{#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 class="pre-wrap">{{evaluateString charId description}}</div>
|
||||
<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>
|
||||
|
||||
@@ -1,5 +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 + ".";
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 can’t see and automatically fails any ability check that requires sight.\n\nAttack rolls against the creature have advantage, and the creature’s attack rolls have disadvantage.",
|
||||
},
|
||||
},
|
||||
|
||||
deaf: {
|
||||
buff: {
|
||||
name: "Deaf",
|
||||
description: "A deafened creature can’t 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 can’t willingly move closer to the source of its fear.",
|
||||
}
|
||||
},
|
||||
|
||||
grappled: {
|
||||
buff:{
|
||||
name: "Grappled",
|
||||
description: "A grappled creature’s speed becomes 0, and it can’t 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 can’t 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 creature’s location can be detected by any noise it makes or any tracks it leaves.\n\nAttack rolls against the creature have disadvantage, and the creature’s attack rolls have advantage.",
|
||||
}
|
||||
},
|
||||
|
||||
paralyzed: {
|
||||
buff: {
|
||||
name: "Paralyzed",
|
||||
description: "A paralyzed creature is **incapacitated** and can’t 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 can’t 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 creature’s 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 creature’s speed becomes 0, and it can’t benefit from any bonus to its speed.\n\nAttack rolls against the creature have advantage, and the creature’s attack rolls have disadvantage.\n\nThe creature has disadvantage on Dexterity saving throws.",
|
||||
},
|
||||
},
|
||||
|
||||
stunned: {
|
||||
buff: {
|
||||
name: "Stunned",
|
||||
description: "A stunned creature is **incapacitated**, can’t 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**, can’t move or speak, and is unaware of its surroundings.\n\nThe creature drops whatever it’s 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 creature’s 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",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
@@ -0,0 +1,6 @@
|
||||
Template.conditionViewDialog.events({
|
||||
"click #deleteButton": function(event, instance){
|
||||
Conditions.remove(instance.data.condition._id);
|
||||
popDialogStack();
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
@@ -0,0 +1,9 @@
|
||||
Template.customBuffViewList.helpers({
|
||||
buffs: function(){
|
||||
var selector = {
|
||||
"parent.id": this.parentId,
|
||||
"charId": this.charId,
|
||||
};
|
||||
return CustomBuffs.find(selector);
|
||||
}
|
||||
});
|
||||
@@ -14,6 +14,9 @@
|
||||
<paper-toggle-button id="variantEncumbrance" checked={{settings.useVariantEncumbrance}}>
|
||||
Use variant encumbrance
|
||||
</paper-toggle-button>
|
||||
<paper-toggle-button id="swapStatAndModifier" checked={{settings.swapStatAndModifier}}>
|
||||
Swap stats and modifiers on Stats page
|
||||
</paper-toggle-button>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
<div class="buttons layout horizontal end-justified">
|
||||
|
||||
@@ -23,6 +23,15 @@ Template.characterSettings.events({
|
||||
);
|
||||
}
|
||||
},
|
||||
"change #swapStatAndModifier": function(event, instance){
|
||||
var value = instance.find("#swapStatAndModifier").checked;
|
||||
if (this.settings.swapStatAndModifier !== value){
|
||||
Characters.update(
|
||||
this._id,
|
||||
{$set: {"settings.swapStatAndModifier": value}}
|
||||
);
|
||||
}
|
||||
},
|
||||
"click .doneButton": function(event, instance){
|
||||
popDialogStack();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<!-- shamelessly nicked and renamed from deleteCharacterConfirmation.html -->
|
||||
<template name="unshareCharacterConfirmation">
|
||||
<div class="fit layout vertical">
|
||||
<app-header-layout has-scrolling-region class="feedback flex">
|
||||
<app-header fixed effects="waterfall">
|
||||
<app-toolbar>
|
||||
<div main-title>Unshare Character</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="form flex">
|
||||
Removing (unsharing) a character does not delete it.<br>
|
||||
However, you will be no longer be able to access or view it, unless it is publicly visible.<br>
|
||||
The character's owner or anyone with write permissions for the character can return read access.<br><br>
|
||||
To continue type "{{name}}" into the box below.<br>
|
||||
<paper-input id="nameInput" label="type the characters's name here" style="width: 100%;"></paper-input><br>
|
||||
<paper-button id="unshareButton" style={{getStyle}} disabled={{cantUnshare}}>Unshare Character</paper-button>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
<div class="buttons layout horizontal end-justified">
|
||||
<paper-button class="cancelButton"> Cancel </paper-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
Template.unshareCharacterConfirmation.onCreated(function() {
|
||||
this.canUnshare = new ReactiveVar(false);
|
||||
});
|
||||
|
||||
Template.unshareCharacterConfirmation.helpers({
|
||||
cantUnshare: function() {
|
||||
return !Template.instance().canUnshare.get();
|
||||
},
|
||||
getStyle: function() {
|
||||
if (Template.instance().canUnshare.get()) {
|
||||
return "background: #d23f31; color: white;";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template.unshareCharacterConfirmation.events({
|
||||
"change #nameInput, input #nameInput": function(event, instance) {
|
||||
var can = instance.find("#nameInput").value === this.name;
|
||||
instance.canUnshare.set(can);
|
||||
},
|
||||
"click #unshareButton": function(event, instance) {
|
||||
if (instance.find("#nameInput").value === this.name) {
|
||||
setTimeout(popDialogStack, 100); //weird things happen without the delay.
|
||||
Router.go("/characterList");
|
||||
Meteor.call("removeMeFromReaders", this._id);
|
||||
}
|
||||
},
|
||||
"click .cancelButton": function(event, instance){
|
||||
popDialogStack();
|
||||
},
|
||||
});
|
||||
@@ -30,6 +30,16 @@
|
||||
</paper-icon-item>
|
||||
</paper-menu>
|
||||
</paper-menu-button>
|
||||
{{else}}
|
||||
<paper-menu-button class="character-menu" horizontal-align="right">
|
||||
<paper-icon-button icon="more-vert" class="dropdown-trigger"></paper-icon-button>
|
||||
<paper-menu class="dropdown-content black87">
|
||||
<paper-icon-item id="unshareCharacter">
|
||||
<iron-icon icon="delete" item-icon></iron-icon>
|
||||
Unshare
|
||||
</paper-icon-item>
|
||||
</paper-menu>
|
||||
</paper-menu-button>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div bottom-item>
|
||||
|
||||
@@ -210,4 +210,11 @@ Template.characterSheet.events({
|
||||
element: event.currentTarget.parentElement.parentElement,
|
||||
});
|
||||
},
|
||||
"click #unshareCharacter": function(event, instance){
|
||||
pushDialogStack({
|
||||
data: this,
|
||||
template: "unshareCharacterConfirmation",
|
||||
element: event.currentTarget.parentElement.parentElement,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
label="Value"
|
||||
floatinglabel
|
||||
value={{effectValue}}>
|
||||
{{> formulaSuffix}}
|
||||
</paper-input>
|
||||
{{else}}
|
||||
<div style="height: 62px;"></div>
|
||||
|
||||
@@ -7,32 +7,36 @@ var stats = [
|
||||
{stat: "intelligence", name: "Intelligence", group: "Ability Scores"},
|
||||
{stat: "wisdom", name: "Wisdom", group: "Ability Scores"},
|
||||
{stat: "charisma", name: "Charisma", group: "Ability Scores"},
|
||||
{name: "Strength Save", stat: "strengthSave", group: "Saving Throws"},
|
||||
{name: "Dexterity Save", stat: "dexteritySave", group: "Saving Throws"},
|
||||
{name: "Constitution Save", stat: "constitutionSave", group: "Saving Throws"},
|
||||
{name: "Intelligence Save", stat: "intelligenceSave", group: "Saving Throws"},
|
||||
{name: "Wisdom Save", stat: "wisdomSave", group: "Saving Throws"},
|
||||
{name: "Charisma Save", stat: "charismaSave", group: "Saving Throws"},
|
||||
{name: "Acrobatics", stat: "acrobatics", group: "Skills"},
|
||||
{name: "Animal Handling", stat: "animalHandling", group: "Skills"},
|
||||
{name: "Arcana", stat: "arcana", group: "Skills"},
|
||||
{name: "Athletics", stat: "athletics", group: "Skills"},
|
||||
{name: "Deception", stat: "deception", group: "Skills"},
|
||||
{name: "History", stat: "history", group: "Skills"},
|
||||
{name: "Insight", stat: "insight", group: "Skills"},
|
||||
{name: "Intimidation", stat: "intimidation", group: "Skills"},
|
||||
{name: "Investigation", stat: "investigation", group: "Skills"},
|
||||
{name: "Medicine", stat: "medicine", group: "Skills"},
|
||||
{name: "Nature", stat: "nature", group: "Skills"},
|
||||
{name: "Perception", stat: "perception", group: "Skills"},
|
||||
{name: "Performance", stat: "performance", group: "Skills"},
|
||||
{name: "Persuasion", stat: "persuasion", group: "Skills"},
|
||||
{name: "Religion", stat: "religion", group: "Skills"},
|
||||
{name: "Sleight of Hand", stat: "sleightOfHand", group: "Skills"},
|
||||
{name: "Stealth", stat: "stealth", group: "Skills"},
|
||||
{name: "Survival", stat: "survival", group: "Skills"},
|
||||
{name: "Initiative", stat: "initiative", group: "Skills"},
|
||||
|
||||
{stat: "strengthSave", name: "Strength Save", group: "Saving Throws"},
|
||||
{stat: "dexteritySave", name: "Dexterity Save", group: "Saving Throws"},
|
||||
{stat: "constitutionSave", name: "Constitution Save", group: "Saving Throws"},
|
||||
{stat: "intelligenceSave", name: "Intelligence Save", group: "Saving Throws"},
|
||||
{stat: "wisdomSave", name: "Wisdom Save", group: "Saving Throws"},
|
||||
{stat: "charismaSave", name: "Charisma Save", group: "Saving Throws"},
|
||||
|
||||
{stat: "acrobatics", name: "Acrobatics", group: "Skills"},
|
||||
{stat: "animalHandling", name: "Animal Handling", group: "Skills"},
|
||||
{stat: "arcana", name: "Arcana", group: "Skills"},
|
||||
{stat: "athletics", name: "Athletics", group: "Skills"},
|
||||
{stat: "deception", name: "Deception", group: "Skills"},
|
||||
{stat: "history", name: "History", group: "Skills"},
|
||||
{stat: "insight", name: "Insight", group: "Skills"},
|
||||
{stat: "intimidation", name: "Intimidation", group: "Skills"},
|
||||
{stat: "investigation", name: "Investigation", group: "Skills"},
|
||||
{stat: "medicine", name: "Medicine", group: "Skills"},
|
||||
{stat: "nature", name: "Nature", group: "Skills"},
|
||||
{stat: "perception", name: "Perception", group: "Skills"},
|
||||
{stat: "performance", name: "Performance", group: "Skills"},
|
||||
{stat: "persuasion", name: "Persuasion", group: "Skills"},
|
||||
{stat: "religion", name: "Religion", group: "Skills"},
|
||||
{stat: "sleightOfHand", name: "Sleight of Hand", group: "Skills"},
|
||||
{stat: "stealth", name: "Stealth", group: "Skills"},
|
||||
{stat: "survival", name: "Survival", group: "Skills"},
|
||||
{stat: "initiative", name: "Initiative", group: "Skills"},
|
||||
|
||||
{stat: "hitPoints", name: "Hit Points", group: "Stats"},
|
||||
{stat: "tempHP", name: "Temporary Hit Points", group: "Stats"},
|
||||
{stat: "armor", name: "Armor", group: "Stats"},
|
||||
{stat: "dexterityArmor", name: "Dexterity Armor Bonus", group: "Stats"},
|
||||
{stat: "speed", name: "Speed", group: "Stats"},
|
||||
@@ -44,6 +48,7 @@ var stats = [
|
||||
{stat: "expertiseDice", name: "Expertise Dice", group: "Stats"},
|
||||
{stat: "superiorityDice", name: "Superiority Dice", group: "Stats"},
|
||||
{stat: "carryMultiplier", name: "Carry Capacity Multiplier", group: "Stats"},
|
||||
|
||||
{stat: "level1SpellSlots", name: "level 1", group: "Spell Slots"},
|
||||
{stat: "level2SpellSlots", name: "level 2", group: "Spell Slots"},
|
||||
{stat: "level3SpellSlots", name: "level 3", group: "Spell Slots"},
|
||||
@@ -53,10 +58,12 @@ var stats = [
|
||||
{stat: "level7SpellSlots", name: "level 7", group: "Spell Slots"},
|
||||
{stat: "level8SpellSlots", name: "level 8", group: "Spell Slots"},
|
||||
{stat: "level9SpellSlots", name: "level 9", group: "Spell Slots"},
|
||||
{stat: "d6HitDice", name: "d6", group: "Hit Dice"},
|
||||
{stat: "d8HitDice", name: "d8", group: "Hit Dice"},
|
||||
{stat: "d10HitDice", name: "d10", group: "Hit Dice"},
|
||||
{stat: "d12HitDice", name: "d12", group: "Hit Dice"},
|
||||
|
||||
{stat: "d6HitDice", name: "d6 Hit Dice", group: "Hit Dice"},
|
||||
{stat: "d8HitDice", name: "d8 Hit Dice", group: "Hit Dice"},
|
||||
{stat: "d10HitDice", name: "d10 Hit Dice", group: "Hit Dice"},
|
||||
{stat: "d12HitDice", name: "d12 Hit Dice", group: "Hit Dice"},
|
||||
|
||||
{stat: "acidMultiplier", name: "Acid", group: "Weakness/Resistance"},
|
||||
{stat: "bludgeoningMultiplier", name: "Bludgeoning", group: "Weakness/Resistance"},
|
||||
{stat: "coldMultiplier", name: "Cold", group: "Weakness/Resistance"},
|
||||
|
||||
@@ -5,12 +5,14 @@ var stats = {
|
||||
"intelligence":{"name":"Intelligence"},
|
||||
"wisdom":{"name":"Wisdom"},
|
||||
"charisma":{"name":"Charisma"},
|
||||
|
||||
"strengthSave":{"name":"Strength Save"},
|
||||
"dexteritySave":{"name":"Dexterity Save"},
|
||||
"constitutionSave":{"name":"Constitution Save"},
|
||||
"intelligenceSave":{"name":"Intelligence Save"},
|
||||
"wisdomSave":{"name":"Wisdom Save"},
|
||||
"charismaSave":{"name":"Charisma Save"},
|
||||
|
||||
"acrobatics":{"name":"Acrobatics"},
|
||||
"animalHandling":{"name":"Animal Handling"},
|
||||
"arcana":{"name":"Arcana"},
|
||||
@@ -30,7 +32,9 @@ var stats = {
|
||||
"stealth":{"name":"Stealth"},
|
||||
"survival":{"name":"Survival"},
|
||||
"initiative":{"name":"Initiative"},
|
||||
|
||||
"hitPoints":{"name":"Hit Points"},
|
||||
"tempHP":{"name":"Temporary Hit Points"},
|
||||
"armor":{"name":"Armor"},
|
||||
"dexterityArmor":{"name":"Dexterity Armor Bonus"},
|
||||
"speed":{"name":"Speed"},
|
||||
@@ -42,6 +46,7 @@ var stats = {
|
||||
"expertiseDice":{"name":"Expertise Dice"},
|
||||
"superiorityDice":{"name":"Superiority Dice"},
|
||||
"carryMultiplier": {"name": "Carry Capacity Multiplier"},
|
||||
|
||||
"level1SpellSlots":{"name":"level 1 Spell Slots"},
|
||||
"level2SpellSlots":{"name":"level 2 Spell Slots"},
|
||||
"level3SpellSlots":{"name":"level 3 Spell Slots"},
|
||||
@@ -51,10 +56,12 @@ var stats = {
|
||||
"level7SpellSlots":{"name":"level 7 Spell Slots"},
|
||||
"level8SpellSlots":{"name":"level 8 Spell Slots"},
|
||||
"level9SpellSlots":{"name":"level 9 Spell Slots"},
|
||||
|
||||
"d6HitDice":{"name":"d6 Hit Dice"},
|
||||
"d8HitDice":{"name":"d8 Hit Dice"},
|
||||
"d10HitDice":{"name":"d10 Hit Dice"},
|
||||
"d12HitDice":{"name":"d12 Hit Dice"},
|
||||
|
||||
"acidMultiplier":{"name":"Acid damage", "group": "Weakness/Resistance"},
|
||||
"bludgeoningMultiplier":{
|
||||
"name":"Bludgeoning damage", "group": "Weakness/Resistance",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
|
||||
{{> effectsViewList charId=charId parentId=_id}}
|
||||
{{> proficiencyViewList charId=charId parentId=_id}}
|
||||
{{> attacksViewList charId=charId parentId=_id}}
|
||||
{{> customBuffViewList charId=charId parentId=_id}}
|
||||
|
||||
</template>
|
||||
|
||||
<template name="featureEdit">
|
||||
@@ -68,9 +71,14 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<!--description-->
|
||||
<paper-textarea label="Description" id="featureDescriptionInput" value={{description}}></paper-textarea>
|
||||
<!--Description-->
|
||||
<div class="description-input layout horizontal end">
|
||||
<paper-textarea id="featureDescriptionInput" label="Description" value={{description}}></paper-textarea>
|
||||
{{> textareaBracketSuffix}}
|
||||
</div>
|
||||
|
||||
{{> effectsEditList parentId=_id parentCollection="Features" charId=charId name=name enabled=enabled}}
|
||||
{{> proficiencyEditList parentId=_id parentCollection="Features" charId=charId enabled=enabled}}
|
||||
{{> attackEditList parentId=_id parentCollection="Features" charId=charId enabled=enabled name=name}}
|
||||
{{> customBuffEditList parentId=_id parentCollection="Features" charId=charId}}
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card.featureCard .bottom {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.containerMain.featureDescription {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
@@ -19,30 +19,8 @@
|
||||
Attacks
|
||||
</div>
|
||||
<div class="bottom list">
|
||||
{{#each attacks}}
|
||||
<div class="item-slot">
|
||||
<div class="flexible attack item">
|
||||
<div class="layout horizontal">
|
||||
<div class="paper-font-headline layout horizontal center"
|
||||
style="margin-right: 16px;">
|
||||
{{evaluateSigned ../_id attackBonus}}
|
||||
</div>
|
||||
<div class="flex layout vertical">
|
||||
<div class="paper-font-body2">
|
||||
{{name}}
|
||||
</div>
|
||||
<div>
|
||||
{{evaluateString ../_id damage}} {{damageType}}
|
||||
</div>
|
||||
{{#if details}}
|
||||
<div>
|
||||
{{details}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#each attack in attacks}}
|
||||
{{>attackListItem attack=attack charId=_id}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</paper-material>
|
||||
@@ -55,19 +33,19 @@
|
||||
Proficiencies
|
||||
</div>
|
||||
<div flex class="bottom list">
|
||||
{{#if weaponProfs.count}}
|
||||
{{#if weaponProfs.length}}
|
||||
<div class="paper-font-subhead">Weapons</div>
|
||||
{{/if}}
|
||||
{{#each weaponProfs}}
|
||||
{{> proficiencyListItem}}
|
||||
{{/each}}
|
||||
{{#if armorProfs.count}}
|
||||
{{#if armorProfs.length}}
|
||||
<div class="paper-font-subhead">Armor</div>
|
||||
{{/if}}
|
||||
{{#each armorProfs}}
|
||||
{{> proficiencyListItem}}
|
||||
{{/each}}
|
||||
{{#if toolProfs.count}}
|
||||
{{#if toolProfs.length}}
|
||||
<div class="paper-font-subhead">Tools</div>
|
||||
{{/if}}
|
||||
{{#each toolProfs}}
|
||||
@@ -100,9 +78,10 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if description}}
|
||||
{{#if hasCharacters (evaluateShortString charId description)}}
|
||||
<div class="bottom flex">
|
||||
{{#markdown}}{{evaluateShortString charId description}}{{/markdown}}
|
||||
{{> customBuffViewList charId=charId parentId=_id}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if hasUses}}
|
||||
@@ -156,3 +135,29 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
<template name="attackListItem">
|
||||
<div class="item-slot">
|
||||
<div class="flexible attack item">
|
||||
<div class="layout horizontal">
|
||||
<div class="paper-font-headline layout horizontal center"
|
||||
style="margin-right: 16px;">
|
||||
{{evaluateAttackBonus charId attack}}
|
||||
</div>
|
||||
<div class="flex layout vertical">
|
||||
<div class="paper-font-body2">
|
||||
{{attack.name}}
|
||||
</div>
|
||||
<div>
|
||||
{{evaluateDamage charId attack}} {{attack.damageType}}
|
||||
</div>
|
||||
{{#if attack.details}}
|
||||
<div>
|
||||
{{attack.details}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
var removeDuplicateProficiencies = function(proficiencies) {
|
||||
dict = {};
|
||||
proficiencies.forEach(function(prof) {
|
||||
if (prof.name in dict) { //if we have already gone over another proficiency for the same thing
|
||||
if (dict[prof.name].value < prof.value) {
|
||||
dict[prof.name] = prof; //then take the new one if it's higher, otherwise leave it
|
||||
}
|
||||
} else {
|
||||
dict[prof.name] = prof; //if it wasn't already there, store it
|
||||
}
|
||||
});
|
||||
profs = []
|
||||
_.forEach(dict, function(prof) {
|
||||
profs.push(prof);
|
||||
})
|
||||
return profs;
|
||||
};
|
||||
|
||||
Template.features.helpers({
|
||||
features: function(){
|
||||
var features = Features.find({charId: this._id}, {sort: {color: 1, name: 1}});
|
||||
@@ -27,13 +45,19 @@ Template.features.helpers({
|
||||
return !this.alwaysEnabled;
|
||||
},
|
||||
weaponProfs: function(){
|
||||
return Proficiencies.find({charId: this._id, type: "weapon"});
|
||||
var profs = Proficiencies.find({charId: this._id, type: "weapon"});
|
||||
return removeDuplicateProficiencies(profs);
|
||||
},
|
||||
armorProfs: function(){
|
||||
return Proficiencies.find({charId: this._id, type: "armor"});
|
||||
var profs = Proficiencies.find({charId: this._id, type: "armor"});
|
||||
return removeDuplicateProficiencies(profs);
|
||||
},
|
||||
toolProfs: function(){
|
||||
return Proficiencies.find({charId: this._id, type: "tool"});
|
||||
var profs = Proficiencies.find({charId: this._id, type: "tool"});
|
||||
return removeDuplicateProficiencies(profs);
|
||||
},
|
||||
hasCharacters: function(string){
|
||||
return string && string.match(/\S/);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -61,13 +85,6 @@ Template.features.events({
|
||||
element: event.currentTarget.parentElement,
|
||||
});
|
||||
},
|
||||
"click .attack": function(event){
|
||||
openParentDialog({
|
||||
parent: this.parent,
|
||||
charId: this.charId,
|
||||
element: event.currentTarget,
|
||||
});
|
||||
},
|
||||
"click .useFeature": function(event){
|
||||
var featureId = this._id;
|
||||
Features.update(featureId, {$inc: {used: 1}});
|
||||
@@ -133,3 +150,42 @@ Template.resource.events({
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Template.attackListItem.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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.attackListItem.events({
|
||||
"click .attack": function(event, instance){
|
||||
openParentDialog({
|
||||
parent: instance.data.attack.parent,
|
||||
charId: instance.data.charId,
|
||||
element: event.currentTarget,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -17,12 +17,17 @@
|
||||
</paper-input>
|
||||
<paper-input id="valueInput" label="Value" type="number" value={{value}}>
|
||||
</paper-input>
|
||||
<paper-toggle-button id="carriedToggle" checked={{isCarried}}>
|
||||
Carried
|
||||
</paper-toggle-button>
|
||||
</div>
|
||||
|
||||
<hr class="vertMargin">
|
||||
|
||||
<paper-textarea label="Description" id="containerDescriptionInput" value={{description}}>
|
||||
</paper-textarea>
|
||||
<div class="description-input layout horizontal end">
|
||||
<paper-textarea label="Description" id="containerDescriptionInput" value={{description}}>
|
||||
</paper-textarea>
|
||||
{{> textareaBracketSuffix}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="containerView">
|
||||
|
||||
@@ -54,4 +54,8 @@ Template.containerEdit.events({
|
||||
trimStrings: false,
|
||||
});
|
||||
},
|
||||
"change #carriedToggle": function(event, instance){
|
||||
var carried = !this.isCarried;
|
||||
Containers.update(this._id, {$set: {isCarried: carried}});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,16 +28,16 @@
|
||||
<div class="bottom green" style="padding: 0;">
|
||||
{{> carryCapacityBar}}
|
||||
</div>
|
||||
{{#if encumberedBuffs.count}}
|
||||
{{#if encumberedConditions.count}}
|
||||
<div class="bottom list">
|
||||
{{#each encumberedBuffs}}
|
||||
{{#each condition in encumberedConditions}}
|
||||
<div class="item-slot">
|
||||
<div class="item buff layout horizontal center">
|
||||
<div class="item condition layout horizontal center">
|
||||
<div class="flex">
|
||||
<iron-icon icon="work"
|
||||
style="margin-right: 16px">
|
||||
</iron-icon>
|
||||
{{name}}
|
||||
{{condition.name}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,9 +68,8 @@ Template.inventory.helpers({
|
||||
return weight;
|
||||
},
|
||||
encumberedBuffs: function(){
|
||||
return Buffs.find({
|
||||
return Conditions.find({
|
||||
charId: this._id,
|
||||
type: "inate",
|
||||
name: {$in: [
|
||||
"Encumbered",
|
||||
"Heavily encumbered",
|
||||
@@ -157,7 +156,7 @@ Template.inventory.events({
|
||||
return;
|
||||
}
|
||||
// Make the library item into a regular item
|
||||
let item = _.omit(result, "library", "attacks", "effects");
|
||||
let item = _.omit(result, "libraryName", "library", "attacks", "effects");
|
||||
delete item.settings.category;
|
||||
// Update the item to match library item
|
||||
Items.update(itemId, {$set: item});
|
||||
@@ -201,12 +200,10 @@ Template.inventory.events({
|
||||
element: event.currentTarget.parentElement,
|
||||
});
|
||||
},
|
||||
"click .buff": function(event, instance){
|
||||
var buffId = this._id;
|
||||
var charId = Template.parentData()._id;
|
||||
"click .condition": function(event, instance){
|
||||
pushDialogStack({
|
||||
template: "buffDialog",
|
||||
data: {buffId: buffId, charId: charId},
|
||||
template: "conditionViewDialogDialog",
|
||||
data: {condition: this.condition},
|
||||
element: event.currentTarget,
|
||||
});
|
||||
},
|
||||
@@ -334,21 +331,23 @@ Template.layout.events({
|
||||
Session.set("inventory.dragItemId", null);
|
||||
},
|
||||
"drop .characterRepresentative": function(event, instance) {
|
||||
var itemId = event.originalEvent.dataTransfer.getData("dicecloud-id/items");
|
||||
if (event.ctrlKey){
|
||||
//split the stack to the container
|
||||
pushDialogStack({
|
||||
template: "splitStackDialog",
|
||||
data: {
|
||||
id: itemId,
|
||||
parentCollection: "Characters",
|
||||
parentId: this._id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
//move item to the character
|
||||
Meteor.call("moveItemToCharacter", itemId, this._id);
|
||||
if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/items")){
|
||||
var itemId = event.originalEvent.dataTransfer.getData("dicecloud-id/items");
|
||||
if (event.ctrlKey){
|
||||
//split the stack to the container
|
||||
pushDialogStack({
|
||||
template: "splitStackDialog",
|
||||
data: {
|
||||
id: itemId,
|
||||
parentCollection: "Characters",
|
||||
parentId: this._id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
//move item to the character
|
||||
Meteor.call("moveItemToCharacter", itemId, this._id);
|
||||
}
|
||||
Session.set("inventory.dragItemId", null);
|
||||
}
|
||||
Session.set("inventory.dragItemId", null);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
{{/if}}
|
||||
{{> effectsViewList charId=charId parentId=_id}}
|
||||
{{> attacksViewList charId=charId parentId=_id}}
|
||||
{{> customBuffViewList charId=charId parentId=_id}}
|
||||
</template>
|
||||
|
||||
<template name="itemEdit">
|
||||
@@ -61,16 +62,17 @@
|
||||
</div>
|
||||
|
||||
<!--Description-->
|
||||
<paper-textarea id="itemDescriptionInput" label="Description" value={{description}}>
|
||||
<div suffix>
|
||||
<paper-tooltip position="left" animation-delay="0">This field accepts formulae in {curly brackets}</paper-tooltip>
|
||||
<iron-icon icon="dicecloud:code-braces"></iron-icon>
|
||||
</div>
|
||||
</paper-textarea>
|
||||
<div class="description-input layout horizontal end">
|
||||
<paper-textarea id="itemDescriptionInput" label="Description" value={{description}}></paper-textarea>
|
||||
{{> textareaBracketSuffix}}
|
||||
</div>
|
||||
|
||||
<!--Effects-->
|
||||
{{> effectsEditList parentId=_id parentCollection="Items" charId=charId enabled=equipped name=name}}
|
||||
<!--Attacks-->
|
||||
{{> attackEditList parentId=_id parentCollection="Items" charId=charId enabled=equipped name=name}}
|
||||
<!-- Buffs -->
|
||||
{{> customBuffEditList parentId=_id parentCollection="Items" charId=charId}}
|
||||
</template>
|
||||
|
||||
<template name="containerDropdown">
|
||||
|
||||
@@ -61,15 +61,15 @@
|
||||
<template name="libraryItem">
|
||||
<tr class="item library-item {{#if selected}}selected{{/if}}">
|
||||
<td class="itemName">
|
||||
{{item.name}}
|
||||
{{itemName}}
|
||||
<paper-ripple></paper-ripple>
|
||||
</td>
|
||||
<td>
|
||||
{{item.weight}} lb.
|
||||
{{itemWeight}} lb.
|
||||
<paper-ripple></paper-ripple>
|
||||
</td>
|
||||
<td>
|
||||
{{valueString item.value}}
|
||||
{{valueString itemValue}}
|
||||
<paper-ripple></paper-ripple>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -107,3 +107,24 @@ Template.itemLibraryDialog.events({
|
||||
template.searchTerm.set(value);
|
||||
},
|
||||
});
|
||||
|
||||
Template.libraryItem.helpers({
|
||||
itemName: function(){
|
||||
return this.item.libraryName || this.item.name;
|
||||
},
|
||||
itemWeight: function(){
|
||||
if (this.item.quantity) {
|
||||
return this.item.weight * this.item.quantity;
|
||||
} else {
|
||||
return this.item.weight;
|
||||
}
|
||||
},
|
||||
itemValue: function(){
|
||||
if (this.item.quantity) {
|
||||
return this.item.value * this.item.quantity;
|
||||
} else {
|
||||
return this.item.value;
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -7,6 +7,24 @@ var colorMap = {
|
||||
backstory: "j",
|
||||
};
|
||||
|
||||
var removeDuplicateProficiencies = function(proficiencies) {
|
||||
dict = {};
|
||||
proficiencies.forEach(function(prof) {
|
||||
if (prof.name in dict) { //if we have already gone over another proficiency for the same thing
|
||||
if (dict[prof.name].value < prof.value) {
|
||||
dict[prof.name] = prof; //then take the new one if it's higher, otherwise leave it
|
||||
}
|
||||
} else {
|
||||
dict[prof.name] = prof; //if it wasn't already there, store it
|
||||
}
|
||||
});
|
||||
profs = []
|
||||
_.forEach(dict, function(prof) {
|
||||
profs.push(prof);
|
||||
})
|
||||
return profs;
|
||||
};
|
||||
|
||||
Template.persona.helpers({
|
||||
characterDetails: function(){
|
||||
var char = Characters.findOne(
|
||||
@@ -33,7 +51,8 @@ Template.persona.helpers({
|
||||
};
|
||||
},
|
||||
languages: function(){
|
||||
return Proficiencies.find({charId: this._id, type: "language"});
|
||||
var profs = Proficiencies.find({charId: this._id, type: "language"});
|
||||
return removeDuplicateProficiencies(profs);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,22 @@ Template.proficiencyListItem.helpers({
|
||||
|
||||
Template.proficiencyListItem.events({
|
||||
"click .proficiency": function(event, instance){
|
||||
if (this.parent.collection == "Characters") {
|
||||
if (this.parent.group == "background") {
|
||||
pushDialogStack({
|
||||
template: "backgroundDialog",
|
||||
data: {
|
||||
"charId": this.charId,
|
||||
"field":"background",
|
||||
"title":"Background",
|
||||
"color":"j",
|
||||
},
|
||||
element: event.currentTarget,
|
||||
})
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
openParentDialog({
|
||||
parent: this.parent,
|
||||
charId: this.charId,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<template name="spellDetails">
|
||||
<div class="paper-font-body2">
|
||||
Level {{level}} {{school}} {{#if ritual}}ritual{{/if}}, {{preparedString}}
|
||||
{{schoolAndLevel}}{{#if ritual}} (ritual){{/if}}, {{preparedString}}
|
||||
</div>
|
||||
<div style="margin: 16px 0 16px 0;">
|
||||
{{#if castingTime}}
|
||||
@@ -38,6 +38,7 @@
|
||||
</div>
|
||||
<div>{{#markdown}}{{evaluateSpellString charId parent.id description}}{{/markdown}}</div>
|
||||
{{> attacksViewList charId=charId parentId=_id}}
|
||||
{{> customBuffViewList charId=charId parentId=_id}}
|
||||
</template>
|
||||
|
||||
<template name="spellEdit">
|
||||
@@ -112,7 +113,11 @@
|
||||
</div>
|
||||
|
||||
<!--Description-->
|
||||
<paper-textarea id="descriptionInput" label="Description" value="{{description}}">
|
||||
</paper-textarea>
|
||||
{{> attackEditList parentId=_id parentCollection="Spells" charId=charId enabled=true name=name}}
|
||||
<div class="description-input layout horizontal end">
|
||||
<paper-textarea id="descriptionInput" label="Description" style="width: calc(100% - 24px)" value={{description}}></paper-textarea>
|
||||
{{> textareaBracketSuffix}}
|
||||
</div>
|
||||
|
||||
{{> customBuffEditList parentId=_id parentCollection="Spells" charId=charId}}
|
||||
{{> attackEditList parentId=_id parentCollection="Spells" charId=charId enabled=true name=name isSpell=true}}
|
||||
</template>
|
||||
|
||||
@@ -29,6 +29,13 @@ Template.spellDialog.events({
|
||||
});
|
||||
|
||||
Template.spellDetails.helpers({
|
||||
schoolAndLevel: function(){
|
||||
if (this.level == 0) {
|
||||
return this.school + " cantrip";
|
||||
} else {
|
||||
return "Level " + this.level + " " + this.school;
|
||||
}
|
||||
},
|
||||
getComponents: function(){
|
||||
var components = "";
|
||||
if (this.components.concentration) components += "C";
|
||||
|
||||
@@ -40,8 +40,11 @@
|
||||
{{> formulaSuffix}}
|
||||
</paper-input>
|
||||
<!--Description-->
|
||||
<paper-textarea id="spellListDescriptionInput" label="Description" value={{description}}>
|
||||
</paper-textarea>
|
||||
<div class="description-input layout horizontal end">
|
||||
<paper-textarea id="spellListDescriptionInput" label="Description" value={{description}}>
|
||||
</paper-textarea>
|
||||
{{> textareaBracketSuffix}}
|
||||
</div>
|
||||
</div>
|
||||
{{/baseDialog}}
|
||||
{{/with}}
|
||||
|
||||
@@ -83,7 +83,9 @@
|
||||
{{#each spells ../_id ../../_id}}
|
||||
{{#if showSpell ../../_id}}
|
||||
<div class="item-slot">
|
||||
<div class="tall spell item layout horizontal center" data-id={{_id}}>
|
||||
<div class="tall spell item layout horizontal center spellItem"
|
||||
data-id={{_id}}
|
||||
draggable={{canEditCharacter charId}}>
|
||||
<iron-icon icon="social:whatshot"
|
||||
style="color: {{hexColor color}};
|
||||
margin-right: 16px;"
|
||||
|
||||
@@ -11,6 +11,12 @@ var spellLevels = [
|
||||
{name: "Level 9", level: 9},
|
||||
];
|
||||
|
||||
var materialNeedsGp = function(string) {
|
||||
if (!string) return false;
|
||||
gpRegExp = /\b[0-9]+ ?(cp|sp|gp)\b/i;
|
||||
return gpRegExp.test(string);
|
||||
}
|
||||
|
||||
const showUnprepared = (listId) => {
|
||||
return Session.get(`showUnprepared.${listId}`);
|
||||
}
|
||||
@@ -70,6 +76,7 @@ Template.spells.helpers({
|
||||
}
|
||||
if (this.components.material){
|
||||
components += components ? ", M" : "M";
|
||||
if (materialNeedsGp(this.components.material)) {components += "gp";}
|
||||
}
|
||||
if (this.components.concentration){
|
||||
components += components ? ", C" : "C";
|
||||
@@ -246,8 +253,8 @@ Template.spells.events({
|
||||
pushDialogStack({
|
||||
template: "spellLibraryDialog",
|
||||
element: event.currentTarget,
|
||||
callback: (result) => {
|
||||
if (!result) return;
|
||||
callback: (resultArray) => {
|
||||
if (!resultArray) return;
|
||||
if (!listId){
|
||||
listId = SpellLists.insert({
|
||||
name: "New SpellList",
|
||||
@@ -256,26 +263,56 @@ Template.spells.events({
|
||||
attackBonus: "intelligenceMod + proficiencyBonus",
|
||||
});
|
||||
}
|
||||
// Make the library spell into a regular spell
|
||||
let spell = _.omit(result, "library", "attacks", "effects");
|
||||
spell._id = spellId;
|
||||
spell.charId = charId;
|
||||
spell.parent = {
|
||||
id: listId,
|
||||
collection: "SpellLists",
|
||||
};
|
||||
spell.prepared = "prepared";
|
||||
Spells.insert(spell);
|
||||
// Copy over attacks and effects
|
||||
_.each(result.attacks, (attack) => {
|
||||
attack.charId = charId;
|
||||
attack.parent = {id: spellId, collection: "Spells"};
|
||||
Attacks.insert(attack);
|
||||
});
|
||||
_.each(result.effects, (effect) => {
|
||||
effect.charId = charId;
|
||||
effect.parent = {id: spellId, collection: "Spells"};
|
||||
Effects.insert(effect);
|
||||
|
||||
//loop through all returned spells
|
||||
_.each(resultArray, (rawSpell, index) =>{
|
||||
// Make the library spell into a regular spell
|
||||
let spell = _.omit(rawSpell, "library", "attacks", "effects");
|
||||
if (index == 0) {
|
||||
spell._id = spellId; //only do this for the first spell added
|
||||
}
|
||||
spell.charId = charId;
|
||||
spell.parent = {
|
||||
id: listId,
|
||||
collection: "SpellLists",
|
||||
};
|
||||
spell.prepared = "prepared";
|
||||
Spells.insert(spell);
|
||||
// Copy over attacks and effects
|
||||
_.each(rawSpell.attacks, (attack) => {
|
||||
if (!("attackBonus" in attack)) {attack.attackBonus = "attackBonus"} //if no attack bonus provided, use spell list's
|
||||
attack.charId = charId;
|
||||
attack.parent = {id: spellId, collection: "Spells"};
|
||||
Attacks.insert(attack);
|
||||
});
|
||||
_.each(rawSpell.effects, (effect) => {
|
||||
effect.charId = charId;
|
||||
effect.parent = {id: spellId, collection: "Spells"};
|
||||
Effects.insert(effect);
|
||||
});
|
||||
|
||||
_.each(rawSpell.buffs, (buff) => {
|
||||
buff.charId = charId;
|
||||
buff.parent = {id: spellId, collection: "Spells"};
|
||||
buffId = Buffs.insert(buff);
|
||||
|
||||
_.each(buff.attacks, (attack) => {
|
||||
if (!(attackBonus in attack)) {attack.attackBonus = "attackBonus"} //if no attack bonus provided, use spell list's
|
||||
attack.charId = charId;
|
||||
attack.parent = {id: buffId, collection: "Buffs"};
|
||||
Attacks.insert(attack);
|
||||
});
|
||||
_.each(buff.effects, (effect) => {
|
||||
effect.charId = charId;
|
||||
effect.parent = {id: buffId, collection: "Buffs"};
|
||||
Effects.insert(effect);
|
||||
});
|
||||
_.each(buff.proficiencies, (prof) => {
|
||||
prof.charId = charId;
|
||||
prof.parent = {id: buffId, collection: "Buffs"};
|
||||
Proficiencies.insert(prof);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
returnElement: () => $(`[data-id='${spellId}']`).get(0),
|
||||
@@ -300,3 +337,49 @@ Template.spells.events({
|
||||
event.stopPropagation();
|
||||
},
|
||||
});
|
||||
|
||||
Template.layout.events({
|
||||
"dragstart .spellItem": function(event, instance){
|
||||
event.originalEvent.dataTransfer.setData("dicecloud-id/spells", this._id);
|
||||
Session.set("spellLists.dragSpellId", this._id);
|
||||
},
|
||||
"dragend .spellItem": function(event, instance){
|
||||
Session.set("spellLists.dragSpellId", null);
|
||||
},
|
||||
|
||||
"dragover .spellList, dragenter .spellList": function(event, instance){
|
||||
if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/spells")){
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
"drop .spellList": function(event, instance){
|
||||
var spellId = event.originalEvent.dataTransfer.getData("dicecloud-id/spells");
|
||||
if (event.ctrlKey){
|
||||
//copy spell to new list
|
||||
Meteor.call("copySpellToList", spellId, this._id);
|
||||
} else {
|
||||
//move spell to new list
|
||||
Meteor.call("moveSpellToList", spellId, this._id);
|
||||
}
|
||||
Session.set("spellLists.dragSpellId", null);
|
||||
},
|
||||
|
||||
"dragover .characterRepresentative, dragenter .characterRepresentative": function(event, instance){
|
||||
if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/spells")){
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
"drop .characterRepresentative": function(event, instance) {
|
||||
if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/spells")){ //to prevent conflicts with item drag/drop
|
||||
var spellId = event.originalEvent.dataTransfer.getData("dicecloud-id/spells");
|
||||
if (event.ctrlKey){
|
||||
//copy spell to character
|
||||
Meteor.call("copySpellToCharacter", spellId, this._id);
|
||||
} else {
|
||||
//move spell to character
|
||||
Meteor.call("moveSpellToCharacter", spellId, this._id);
|
||||
}
|
||||
Session.set("spellLists.dragSpellId", null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
.spell-library-dialog .spell.selected {
|
||||
background-color: #e4e4e4;
|
||||
}
|
||||
|
||||
.spell-library-dialog .category-header {
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -13,11 +9,3 @@
|
||||
.spell-library-dialog .category-header iron-icon.open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.spell-library-dialog table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.spell-library-dialog .library-spell td {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -13,15 +13,18 @@
|
||||
<div class="spells" style="padding:8px">
|
||||
{{#if searchTerm}}
|
||||
{{#if searchSpells.count}}
|
||||
<table style="width: 100%">
|
||||
<tbody>
|
||||
{{#each spell in searchSpells}}
|
||||
{{>librarySpell spell=spell selected=(isSelected spell)}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
{{#each spell in searchSpells}}
|
||||
{{>librarySpell spell=spell selected=(isSelected spell)}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}{{#if searchReady}}
|
||||
No spells match "{{searchTerm}}"
|
||||
<p>
|
||||
No spells match "{{searchTerm}}"
|
||||
</p>
|
||||
<p class="paper-font-caption">
|
||||
DiceCloud only includes content provided by Wizards of the Coast in the official system reference document. If the spell you are looking for is not available in the system reference document, you will need to add it manually.
|
||||
</p>
|
||||
{{/if}}{{/if}}
|
||||
{{#unless searchReady}}
|
||||
<div class="layout vertical center" style="width: 100%; padding: 16px;">
|
||||
@@ -36,13 +39,11 @@
|
||||
{{name}}
|
||||
</div>
|
||||
<iron-collapse opened={{isOpen key}}>
|
||||
<table style="width: 100%">
|
||||
<tbody>
|
||||
{{#each spell in (spellsInCategory key)}}
|
||||
{{>librarySpell spell=spell selected=(isSelected spell)}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
{{#each spell in (spellsInCategory key)}}
|
||||
{{>librarySpell spell=spell selected=(isSelected spell)}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#unless ready key}}
|
||||
<paper-spinner active></paper-spinner>
|
||||
{{/unless}}
|
||||
@@ -59,10 +60,10 @@
|
||||
</template>
|
||||
|
||||
<template name="librarySpell">
|
||||
<tr class="spell library-spell {{#if selected}}selected{{/if}}">
|
||||
<td class="spellName">
|
||||
<div style="margin: ">
|
||||
<paper-checkbox class="spell library-spell" checked={{selected}} style="padding: 2px 0 2px 16px; width: 100%;">
|
||||
{{spell.name}}
|
||||
<paper-ripple></paper-ripple>
|
||||
</td>
|
||||
</tr>
|
||||
</paper-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,7 +14,7 @@ const categories = [
|
||||
];
|
||||
|
||||
Template.spellLibraryDialog.onCreated(function(){
|
||||
this.selectedSpell = new ReactiveVar();
|
||||
this.selectedSpells = new ReactiveVar([]); //this holds an array of the selected spells by ID
|
||||
this.searchTerm = new ReactiveVar();
|
||||
this.categoriesOpen = new ReactiveVar([]);
|
||||
this.readyDict = new ReactiveDict();
|
||||
@@ -59,8 +59,12 @@ Template.spellLibraryDialog.helpers({
|
||||
});
|
||||
},
|
||||
isSelected(spell){
|
||||
const selected = Template.instance().selectedSpell.get();
|
||||
return selected && selected._id === spell._id;
|
||||
const selected = Template.instance().selectedSpells.get();
|
||||
return _.contains(selected, spell._id);
|
||||
},
|
||||
selectedCount(){
|
||||
const selected = Template.instance().selectedSpells.get();
|
||||
return selected && selected.length;
|
||||
},
|
||||
isOpen(key){
|
||||
const cats = Template.instance().categoriesOpen.get();
|
||||
@@ -89,10 +93,26 @@ Template.spellLibraryDialog.events({
|
||||
popDialogStack();
|
||||
},
|
||||
"click .okButton": function(event, template){
|
||||
popDialogStack(template.selectedSpell.get());
|
||||
const selectedIds = template.selectedSpells.get();
|
||||
var returnSpells = [];
|
||||
_.each(selectedIds, (id) => {
|
||||
let spell = LibrarySpells.findOne(id);
|
||||
if (spell) {
|
||||
returnSpells.push(spell)
|
||||
}
|
||||
});
|
||||
popDialogStack(returnSpells);
|
||||
},
|
||||
"click .library-spell": function(event, template){
|
||||
template.selectedSpell.set(this.spell);
|
||||
let selected = template.selectedSpells.get();
|
||||
const spellId = this.spell._id;
|
||||
// Toggle whether this spellId is in the array or not
|
||||
if (_.contains(selected, spellId)){
|
||||
selected = _.without(selected, spellId);
|
||||
} else {
|
||||
selected.push(spellId);
|
||||
}
|
||||
template.selectedSpells.set(selected);
|
||||
},
|
||||
"click #backButton": function(event, template){
|
||||
popDialogStack();
|
||||
|
||||
@@ -2,12 +2,21 @@
|
||||
<div>
|
||||
<paper-material class="ability-mini-card layout horizontal">
|
||||
<div class="numbers">
|
||||
{{#if swap}}
|
||||
<div class="paper-font-display1 stat">
|
||||
{{abilityMod}}
|
||||
</div>
|
||||
<div class="paper-font-subhead modifier">
|
||||
{{characterCalculate "attributeValue" ../_id ability}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="paper-font-display1 stat">
|
||||
{{characterCalculate "attributeValue" ../_id ability}}
|
||||
</div>
|
||||
<div class="paper-font-subhead modifier">
|
||||
{{abilityMod}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="paper-font-subhead title flex layout horizontal center">
|
||||
{{title}}
|
||||
|
||||
@@ -5,5 +5,10 @@ Template.abilityMiniCard.helpers({
|
||||
Template.parentData()._id, this.ability
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
swap: function() {
|
||||
var character = Characters.findOne({"_id": Template.parentData()._id})
|
||||
if (character) {return character.settings.swapStatAndModifier;}
|
||||
else {return false;}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- data just needs charId -->
|
||||
<template name="addTHPDialog">
|
||||
<template name="addEHPDialog">
|
||||
<div class="fit layout vertical">
|
||||
<app-header-layout has-scrolling-region class="new-character-dialog flex">
|
||||
<app-header fixed effects="waterfall">
|
||||
@@ -1,8 +1,8 @@
|
||||
Template.addTHPDialog.onRendered(function(){
|
||||
Template.addEHPDialog.onRendered(function(){
|
||||
this.find("#quantityInput").focus();
|
||||
});
|
||||
|
||||
Template.addTHPDialog.events({
|
||||
Template.addEHPDialog.events({
|
||||
"tap .addButton": function(event, instance){
|
||||
popDialogStack();
|
||||
var max = +instance.find("#quantityInput").value;
|
||||
@@ -3,10 +3,15 @@
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.healthCard .bottom-border {
|
||||
border-bottom: rgba(0,0,0,0.24) solid 1px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.healthCard #stableButton {
|
||||
color: #b71c1c;
|
||||
transition: color 0.4s ease;
|
||||
width: 100%
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.healthCard #stableButton:before {
|
||||
|
||||
@@ -5,33 +5,52 @@
|
||||
Hit Points
|
||||
</div>
|
||||
<paper-icon-button class="white54"
|
||||
id="addTempHP"
|
||||
id="addExtraHP"
|
||||
icon="add"
|
||||
disabled={{#unless canEditCharacter _id}}true{{/unless}}>
|
||||
</paper-icon-button>
|
||||
</div>
|
||||
<div class="right flex layout vertical center-justified" style="min-width: 180px;">
|
||||
<!-- main HP slider -->
|
||||
<div class="layout horizontal">
|
||||
<paper-diff-slider id="hitPointSlider"
|
||||
editable pin
|
||||
disabled={{#unless canEditCharacter _id}}true{{/unless}}>
|
||||
</paper-diff-slider>
|
||||
</div>
|
||||
{{#each tempHitPoints}}
|
||||
<div>
|
||||
{{name}}
|
||||
{{#if characterCalculate "attributeBase" _id "tempHP"}}
|
||||
<!-- main THP slider -->
|
||||
<div class="layout horizontal center {{#if extraHitPoints.count}}bottom-border{{/if}}">
|
||||
<div class="self-center">
|
||||
Temporary Hit Points
|
||||
</div>
|
||||
<paper-diff-slider id="temporaryHitPointSlider"
|
||||
class="flex"
|
||||
editable pin
|
||||
disabled={{#unless canEditCharacter _id}}true{{/unless}}
|
||||
max={{characterCalculate "attributeBase" _id "tempHP"}}
|
||||
value={{characterCalculate "attributeValue" _id "tempHP"}}
|
||||
>
|
||||
</paper-diff-slider>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#each extraHitPoints}}
|
||||
<div class="layout horizontal center">
|
||||
<div class="self-center">
|
||||
{{name}}
|
||||
</div>
|
||||
<div style="height: 40px; width: 40px;">
|
||||
{{#unless left}}
|
||||
<paper-icon-button class="deleteTHP" icon="delete"></paper-icon-button>
|
||||
<paper-icon-button class="deleteEHP" icon="delete"></paper-icon-button>
|
||||
{{/unless}}
|
||||
</div>
|
||||
<paper-diff-slider class="tempHitPointSlider flex"
|
||||
<paper-diff-slider class="extraHitPointSlider flex"
|
||||
max={{maximum}}
|
||||
value={{left}}
|
||||
editable pin
|
||||
></paper-diff-slider>
|
||||
>
|
||||
</paper-diff-slider>
|
||||
</div>
|
||||
{{/each}}
|
||||
<div class="paper-font-caption">
|
||||
|
||||
@@ -5,7 +5,7 @@ Template.healthCard.binding({
|
||||
"#hitPointSlider": {
|
||||
max: () => Characters.calculate.attributeBase(currentId() , "hitPoints"),
|
||||
value: () => Characters.calculate.attributeValue(currentId() , "hitPoints"),
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Reset the old value between characters so that we don't get red health lost
|
||||
@@ -16,13 +16,14 @@ Template.healthCard.onRendered(function(){
|
||||
const id = Template.currentData()._id;
|
||||
if (oldId !== id){
|
||||
this.find("#hitPointSlider").resetOldValue();
|
||||
this.find("#temporaryHitPointSlider").resetOldValue();
|
||||
oldId = id;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.healthCard.helpers({
|
||||
tempHitPoints: function(){
|
||||
extraHitPoints: function(){
|
||||
return TemporaryHitPoints.find({charId: this._id});
|
||||
},
|
||||
showDeathSave: function(){
|
||||
@@ -93,17 +94,23 @@ Template.healthCard.events({
|
||||
}}
|
||||
);
|
||||
},
|
||||
"change .tempHitPointSlider": function(event){
|
||||
"change #temporaryHitPointSlider": function(event){ //this is the actual THP stat
|
||||
var value = event.currentTarget.value;
|
||||
var base = Characters.calculate.attributeBase(this._id, "tempHP");
|
||||
var adjustment = value - base;
|
||||
Characters.update(this._id, {$set: {"tempHP.adjustment": adjustment}});
|
||||
},
|
||||
"change .extraHitPointSlider": function(event){ //this is the extra bars
|
||||
var value = event.currentTarget.value;
|
||||
var used = this.maximum - value;
|
||||
TemporaryHitPoints.update(this._id, {$set: {"used": used}});
|
||||
},
|
||||
"click .deleteTHP": function(event){
|
||||
"click .deleteEHP": function(event){
|
||||
TemporaryHitPoints.remove(this._id);
|
||||
},
|
||||
"click #addTempHP": function(event){
|
||||
"click #addExtraHP": function(event){
|
||||
pushDialogStack({
|
||||
template: "addTHPDialog",
|
||||
template: "addEHPDialog",
|
||||
data: {charId: this._id},
|
||||
element: event.currentTarget.parentElement,
|
||||
});
|
||||
|
||||
@@ -42,6 +42,34 @@
|
||||
</div>
|
||||
</paper-material>
|
||||
</div>
|
||||
<!--Condtions-->
|
||||
<div>
|
||||
<paper-material class="card">
|
||||
<div class="top white subhead layout horizontal center">
|
||||
<div class="flex">Conditions</div>
|
||||
{{#if canEditCharacter _id}}
|
||||
<paper-icon-button class="black54" id="addCondition" icon="add"></paper-icon-button>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div flex class="bottom list">
|
||||
<div class="conditionsList">
|
||||
{{#each condition in conditions}}
|
||||
{{>conditionView condition=condition}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if buffs.count}}
|
||||
<div class="layout horizontal">
|
||||
<div class="paper-font-subhead flex">Buffs</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="buffsList">
|
||||
{{#each buff in buffs}}
|
||||
{{>buffListItem buff=buff}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</paper-material>
|
||||
</div>
|
||||
<!--Skills-->
|
||||
<div>
|
||||
<paper-material class="card">
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
Template.stats.helpers({
|
||||
conditions: function() {
|
||||
return Conditions.find({charId: this._id});
|
||||
},
|
||||
buffs: function() {
|
||||
var selector = {
|
||||
"charId": this._id,
|
||||
};
|
||||
return Buffs.find(selector);
|
||||
},
|
||||
})
|
||||
|
||||
Template.stats.events({
|
||||
"click .stat-card": function(event, instance){
|
||||
var charId = instance.data._id;
|
||||
@@ -65,4 +77,17 @@ Template.stats.events({
|
||||
element: event.currentTarget.parentElement.parentElement,
|
||||
});
|
||||
},
|
||||
"click #addCondition": function(event, template){
|
||||
pushDialogStack({
|
||||
template: "conditionLibraryDialog",
|
||||
element: event.currentTarget,
|
||||
callback: (result) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
else Meteor.call("giveCondition", this._id, result)
|
||||
},
|
||||
//returnElement: () => $(`[data-id='${itemId}']`).get(0),
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,8 +8,16 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.character-card .image {
|
||||
.partyHeader {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.partyHeader iron-icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.partyHeader:hover iron-icon{
|
||||
visibility: initial;
|
||||
}
|
||||
|
||||
.character-card .initials {
|
||||
|
||||
@@ -10,31 +10,27 @@
|
||||
{{#if currentUser}}
|
||||
{{#if characters.count}}
|
||||
<div class="character-list layout horizontal wrap">
|
||||
{{# each characters}}
|
||||
<a class="character-card flex layout vertical end-justified" href="{{characterPath this}}">
|
||||
<iron-image class="fit {{colorClass}}"
|
||||
sizing="cover" preload fade src={{picture}}>
|
||||
</iron-image>
|
||||
{{#unless picture}}
|
||||
<div class="fit initials layout vertical center center-justified">
|
||||
{{initials name}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
<paper-item>
|
||||
<paper-item-body two-lines>
|
||||
<div class="name white87">
|
||||
{{name}}
|
||||
</div>
|
||||
<div secondary style="color: #8a8a8a; color: rgba(255,255,255,0.87);">
|
||||
{{alignment}} {{gender}} {{race}}
|
||||
</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
<paper-ripple></paper-ripple>
|
||||
</a>
|
||||
{{# each charactersWithNoParty}}
|
||||
{{> characterCard}}
|
||||
{{/each}}
|
||||
{{> gridPadding class="character-card flex layout vertical" num=12}}
|
||||
</div>
|
||||
{{# each party in parties}}
|
||||
<div class="party" data-id={{party._id}}>
|
||||
{{#with party}}
|
||||
<div class="partyHeader clickable paper-font-title padded">
|
||||
{{name}}
|
||||
<iron-icon icon="create"></iron-icon>
|
||||
</div>
|
||||
{{/with}}
|
||||
<div class="character-list layout horizontal wrap">
|
||||
{{# each charactersInParty party._id}}
|
||||
{{> characterCard}}
|
||||
{{/each}}
|
||||
{{> gridPadding class="character-card flex layout vertical" num=12}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<div layout vertical center center-justified class="padded">
|
||||
<div>You don't seem to have any characters yet</div>
|
||||
@@ -47,9 +43,46 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="fab-buffer"></div>
|
||||
<paper-fab class="floatyButton addCharacter"
|
||||
icon="add"
|
||||
title="Add"></paper-fab>
|
||||
{{#fabMenu}}
|
||||
<div>
|
||||
<paper-fab icon="social:group"
|
||||
class="addParty"
|
||||
mini>
|
||||
</paper-fab>
|
||||
<paper-tooltip position="left"> New Party </paper-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<paper-fab icon="face"
|
||||
class="addCharacter"
|
||||
mini>
|
||||
</paper-fab>
|
||||
<paper-tooltip position="left"> New Character </paper-tooltip>
|
||||
</div>
|
||||
{{/fabMenu}}
|
||||
</div>
|
||||
</app-header-layout>
|
||||
</template>
|
||||
|
||||
<template name="characterCard">
|
||||
<a class="character-card flex layout vertical end-justified" href="{{characterPath this}}">
|
||||
<iron-image class="fit {{colorClass}}"
|
||||
sizing="cover" preload fade src={{picture}}>
|
||||
</iron-image>
|
||||
{{#unless picture}}
|
||||
<div class="fit initials layout vertical center center-justified">
|
||||
{{initials name}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
<paper-item>
|
||||
<paper-item-body two-lines>
|
||||
<div class="name white87">
|
||||
{{name}}
|
||||
</div>
|
||||
<div secondary style="color: #8a8a8a; color: rgba(255,255,255,0.87);">
|
||||
{{alignment}} {{gender}} {{race}}
|
||||
</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
<paper-ripple></paper-ripple>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@@ -1,35 +1,60 @@
|
||||
Template.characterList.helpers({
|
||||
characters(){
|
||||
characters() {
|
||||
var userId = Meteor.userId();
|
||||
return Characters.find(
|
||||
{
|
||||
$or: [
|
||||
{readers: userId},
|
||||
{writers: userId},
|
||||
{owner: userId},
|
||||
]
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
name: 1,
|
||||
urlName: 1,
|
||||
picture: 1,
|
||||
color: 1,
|
||||
race: 1,
|
||||
alignment: 1,
|
||||
gender: 1,
|
||||
},
|
||||
sort: {name: 1},
|
||||
}
|
||||
{$or: [{readers: userId}, {writers: userId}, {owner: userId}]},
|
||||
{sort: {name: 1}}
|
||||
);
|
||||
},
|
||||
parties() {
|
||||
return Parties.find(
|
||||
{owner: Meteor.userId()},
|
||||
{sort: {name: 1}},
|
||||
);
|
||||
},
|
||||
charactersInParty(partyId) {
|
||||
var userId = Meteor.userId();
|
||||
var party = Parties.findOne(partyId);
|
||||
return Characters.find(
|
||||
{
|
||||
_id: {$in: party.characters},
|
||||
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
||||
},
|
||||
{sort: {name: 1}}
|
||||
);
|
||||
},
|
||||
charactersWithNoParty() {
|
||||
var userId = Meteor.userId();
|
||||
var charArrays = Parties.find({owner: userId}).map(p => p.characters);
|
||||
var partyChars = _.uniq(_.flatten(charArrays));
|
||||
return Characters.find(
|
||||
{
|
||||
_id: {$nin: partyChars},
|
||||
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
||||
},
|
||||
{sort: {name: 1}}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Template.characterCard.helpers({
|
||||
initials(name){
|
||||
return name.replace(/[^A-Z]/g, "");
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
Template.characterList.events({
|
||||
"tap .addCharacter": function(event, template) {
|
||||
"click .partyHeader": function(event, instance){
|
||||
pushDialogStack({
|
||||
template: "partyDialog",
|
||||
data: {
|
||||
_id: this._id,
|
||||
startEditing: true,
|
||||
},
|
||||
element: event.currentTarget.parentElement,
|
||||
});
|
||||
},
|
||||
"click .addCharacter": function(event, instance) {
|
||||
pushDialogStack({
|
||||
template: "newCharacterDialog",
|
||||
element: event.currentTarget,
|
||||
@@ -37,8 +62,23 @@ Template.characterList.events({
|
||||
if (!character) return;
|
||||
character.owner = Meteor.userId();
|
||||
let _id = Characters.insert(character);
|
||||
Router.go("characterSheet", {_id});
|
||||
let urlName = getSlug(character.name, {maintainCase: true}) || "-"
|
||||
Router.go("characterSheet", {_id, urlName});
|
||||
},
|
||||
})
|
||||
},
|
||||
"click .addParty": function(event, instance) {
|
||||
var partyId = Parties.insert({
|
||||
owner: Meteor.userId(),
|
||||
});
|
||||
pushDialogStack({
|
||||
template: "partyDialog",
|
||||
data: {
|
||||
_id: partyId,
|
||||
startEditing: true,
|
||||
},
|
||||
element: event.currentTarget,
|
||||
returnElement: instance.find(`.party[data-id='${partyId}']`),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
.characterPicker .character-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.characterPicker .partyHead {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.characterPicker .partyHead iron-icon {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.characterPicker .partyHead iron-icon.open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<template name="characterPicker">
|
||||
<dicecloud-selector class="characterPicker" selected={{selected}} selectable="paper-item" style="height: 100%; overflow-y: auto;">
|
||||
{{#if selfId}}{{#if includeSelf}}
|
||||
<paper-item class="short clickable" name={{selfId}}>
|
||||
<div class="character-name">
|
||||
Self
|
||||
</div>
|
||||
</paper-item>
|
||||
{{/if}}{{/if}}
|
||||
{{#each charactersWithNoParty}}
|
||||
<paper-item class="short clickable" name={{_id}}>
|
||||
<div class="character-name">
|
||||
{{name}}
|
||||
</div>
|
||||
</paper-item>
|
||||
{{/each}}
|
||||
{{#each parties}}
|
||||
<div class="paper-font-subhead partyHead clickable">
|
||||
<iron-icon icon="chevron-right" class="{{#if isOpen _id}}open{{/if}}">
|
||||
</iron-icon>
|
||||
{{name}}
|
||||
</div>
|
||||
<iron-collapse opened={{isOpen _id}}>
|
||||
{{#each charactersInParty}}
|
||||
<paper-item class="short clickable" name={{_id}}>
|
||||
<div class="character-name">
|
||||
{{name}}
|
||||
</div>
|
||||
</paper-item>
|
||||
{{/each}}
|
||||
</iron-collapse>
|
||||
{{/each}}
|
||||
</dicecloud-selector>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
Template.characterPicker.onCreated(function() {
|
||||
this.subscribe("characterList");
|
||||
this.openedParties = new ReactiveVar(new Set());
|
||||
});
|
||||
|
||||
Template.characterPicker.helpers({
|
||||
parties() {
|
||||
return Parties.find(
|
||||
{owner: Meteor.userId()},
|
||||
{sort: {name: 1}},
|
||||
);
|
||||
},
|
||||
charactersInParty() {
|
||||
var userId = Meteor.userId();
|
||||
var selector = {
|
||||
_id: {$in: this.characters, $ne: this.selfId},
|
||||
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
||||
};
|
||||
if (this.writableOnly) {
|
||||
selector.$or = [{writers: userId}, {owner: userId}];
|
||||
}
|
||||
return Characters.find(selector,{sort: {name: 1}});
|
||||
},
|
||||
charactersWithNoParty() {
|
||||
var userId = Meteor.userId();
|
||||
var charArrays = Parties.find({owner: userId}).map(p => p.characters);
|
||||
var partyChars = _.uniq(_.flatten(charArrays));
|
||||
var selector = {
|
||||
_id: {$nin: partyChars, $ne: this.selfId},
|
||||
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
||||
};
|
||||
if (this.writableOnly) {
|
||||
selector.$or = [{writers: userId}, {owner: userId}];
|
||||
}
|
||||
return Characters.find(selector, {sort: {name: 1}});
|
||||
},
|
||||
isOpen(id) {
|
||||
var openedParties = Template.instance().openedParties.get();
|
||||
return openedParties.has(id);
|
||||
},
|
||||
});
|
||||
|
||||
Template.characterPicker.events({
|
||||
"click .partyHead": function(event, instance){
|
||||
var openedParties = instance.openedParties.get();
|
||||
if (openedParties.has(this._id)){
|
||||
openedParties.delete(this._id);
|
||||
} else {
|
||||
openedParties.add(this._id);
|
||||
}
|
||||
instance.openedParties.set(openedParties);
|
||||
},
|
||||
});
|
||||
@@ -2,8 +2,21 @@
|
||||
prevent character names from wrapping
|
||||
*/
|
||||
|
||||
.character-name {
|
||||
.side-list .character-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.side-list .partyHead {
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.side-list .partyHead iron-icon {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.side-list .partyHead iron-icon.open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
<template name="characterSideList">
|
||||
{{#if characters.count}}
|
||||
<div class="side-list">
|
||||
{{#each characters}}
|
||||
<a href={{characterPath this}} tabindex="-1" class="side-list-character characterRepresentative">
|
||||
<paper-item class="short">
|
||||
<div class="character-name">
|
||||
{{name}}
|
||||
</div>
|
||||
</paper-item>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="side-list">
|
||||
{{#each charactersWithNoParty}}
|
||||
<a href={{characterPath this}} tabindex="-1" class="side-list-character characterRepresentative">
|
||||
<paper-item class="short">
|
||||
<div class="character-name">
|
||||
{{name}}
|
||||
</div>
|
||||
</paper-item>
|
||||
</a>
|
||||
{{/each}}
|
||||
{{#each parties}}
|
||||
<div class="paper-font-subhead partyHead">
|
||||
<iron-icon icon="chevron-right" class="{{#if isOpen _id}}open{{/if}}">
|
||||
</iron-icon>
|
||||
{{name}}
|
||||
</div>
|
||||
<iron-collapse opened={{isOpen _id}}>
|
||||
{{#each charactersInParty}}
|
||||
<a href={{characterPath this}} tabindex="-1" class="side-list-character characterRepresentative">
|
||||
<paper-item class="short">
|
||||
<div class="character-name">
|
||||
{{name}}
|
||||
</div>
|
||||
</paper-item>
|
||||
</a>
|
||||
{{/each}}
|
||||
</iron-collapse>
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,33 +1,51 @@
|
||||
Template.characterSideList.onCreated(function() {
|
||||
this.subscribe("characterList");
|
||||
this.openedParties = new ReactiveVar(new Set());
|
||||
});
|
||||
|
||||
Template.characterSideList.helpers({
|
||||
characters: function() {
|
||||
parties() {
|
||||
return Parties.find(
|
||||
{owner: Meteor.userId()},
|
||||
{sort: {name: 1}},
|
||||
);
|
||||
},
|
||||
charactersInParty() {
|
||||
var userId = Meteor.userId();
|
||||
return Characters.find(
|
||||
{
|
||||
$or: [
|
||||
{readers: userId},
|
||||
{writers: userId},
|
||||
{owner: userId},
|
||||
]
|
||||
_id: {$in: this.characters},
|
||||
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
||||
},
|
||||
{
|
||||
fields: {name: 1, urlName: 1},
|
||||
sort: {name: 1},
|
||||
}
|
||||
{sort: {name: 1}}
|
||||
);
|
||||
},
|
||||
charactersWithNoParty() {
|
||||
var userId = Meteor.userId();
|
||||
var charArrays = Parties.find({owner: userId}).map(p => p.characters);
|
||||
var partyChars = _.uniq(_.flatten(charArrays));
|
||||
return Characters.find(
|
||||
{
|
||||
_id: {$nin: partyChars},
|
||||
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
||||
},
|
||||
{sort: {name: 1}}
|
||||
);
|
||||
},
|
||||
isOpen(id) {
|
||||
var openedParties = Template.instance().openedParties.get();
|
||||
return openedParties.has(id);
|
||||
},
|
||||
});
|
||||
|
||||
Template.characterSideList.events({
|
||||
"tap .singleLineItem": function(event, instance) {
|
||||
//Router.go("characterSheet", {_id: this._id});
|
||||
$("core-drawer-panel").get(0).closeDrawer();
|
||||
},
|
||||
"tap core-item": function() {
|
||||
Router.go("characterList");
|
||||
$("core-drawer-panel").get(0).closeDrawer();
|
||||
"click .partyHead": function(event, instance){
|
||||
var openedParties = instance.openedParties.get();
|
||||
if (openedParties.has(this._id)){
|
||||
openedParties.delete(this._id);
|
||||
} else {
|
||||
openedParties.add(this._id);
|
||||
}
|
||||
instance.openedParties.set(openedParties);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.partyEdit .inPartyCheckbox {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<template name="partyDialog">
|
||||
{{#with party}}
|
||||
{{#baseDialog title=name hideColor=true startEditing=true}}
|
||||
{{> partyDetails}}
|
||||
{{else}}
|
||||
{{> partyEdit}}
|
||||
{{/baseDialog}}
|
||||
{{/with}}
|
||||
</template>
|
||||
|
||||
<template name="partyDetails">
|
||||
<div class="fit layout vertical partyDetails" style="padding: 24px;">
|
||||
<div>
|
||||
{{#each character in getCharacters}}
|
||||
<div>{{character.name}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="partyEdit">
|
||||
<div class="layout vertical partyEdit" style="padding: 24px;">
|
||||
<paper-input class="partyNameInput" value={{name}} label="Party name">
|
||||
</paper-input>
|
||||
{{#each allCharacters}}
|
||||
<paper-checkbox checked={{charInParty _id}}
|
||||
class="inPartyCheckbox">
|
||||
{{name}}
|
||||
</paper-checkbox>
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,62 @@
|
||||
Template.partyDialog.helpers({
|
||||
party(){
|
||||
return Parties.findOne(this._id);
|
||||
}
|
||||
});
|
||||
|
||||
Template.partyDetails.helpers({
|
||||
getCharacters (){
|
||||
var userId = Meteor.userId();
|
||||
return Characters.find(
|
||||
{
|
||||
_id: {$in: this.characters},
|
||||
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
||||
},
|
||||
{sort: {name: 1}}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Template.partyEdit.helpers({
|
||||
allCharacters() {
|
||||
var userId = Meteor.userId();
|
||||
return Characters.find(
|
||||
{$or: [{readers: userId}, {writers: userId}, {owner: userId}]},
|
||||
{sort: {name: 1}}
|
||||
);
|
||||
},
|
||||
charInParty(charId) {
|
||||
return _.contains(Template.parentData().characters, charId);
|
||||
},
|
||||
});
|
||||
|
||||
Template.partyDialog.events({
|
||||
"click #deleteButton": function(event, instance){
|
||||
Parties.remove(instance.data._id);
|
||||
popDialogStack();
|
||||
},
|
||||
"click #doneEditingButton": function(event, instance){
|
||||
popDialogStack();
|
||||
},
|
||||
});
|
||||
|
||||
Template.partyEdit.events({
|
||||
"change .inPartyCheckbox": function(event, instance){
|
||||
var currentCharacters = this.characters;
|
||||
var checked = event.currentTarget.checked;
|
||||
var charId = this._id;
|
||||
var partyId = instance.data._id;
|
||||
if (checked){
|
||||
Parties.update(partyId, {$addToSet: {characters: charId}});
|
||||
} else {
|
||||
Parties.update(partyId, {$pull: {characters: charId}});
|
||||
}
|
||||
},
|
||||
"input .partyNameInput": function(event, instance){
|
||||
var name = event.currentTarget.value;
|
||||
Parties.update(this._id, {$set: {name}}, {
|
||||
removeEmptyStrings: false,
|
||||
trimStrings: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -4,4 +4,8 @@
|
||||
|
||||
.wallOfText p{
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.wallOfText a{
|
||||
color: #d13b2e;
|
||||
}
|
||||
@@ -7,92 +7,127 @@
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="layout vertical center">
|
||||
<paper-material class="wallOfText card" style="padding: 32px; max-width: 800px;">
|
||||
<h2>Character Sheet Philosophy</h2>
|
||||
<p>Setting up your character on DiceCloud is going to take you a little longer than just filling it in on a paper character sheet would have. The goal of using an online sheet is to make actually playing the game more streamlined, and ultimately more fun. So putting a little extra effort into setting up your character now will pay off over and over again once you're playing.</p>
|
||||
<p>The idea is to track where each number comes from, and allow you to easily make changes on the fly.</p>
|
||||
<p>Lets look at a hypothetical example.</p>
|
||||
<p>You need to swim through a sunken section of dungeon to fetch the quest's Thing.<br>You'll need to take off your magical Plate Armor of +1 Constitution to swim without sinking, of course. Taking it off will change your armor class, your speed and your constitution, which in turn changes your hitpoints and your constitution saving throw. Working out all those changes in the middle of a game will drag the game to a hault. <br> Fortunately you have a digital character sheet, so it's a matter of dragging your Plate Armor +1 Con from your "equipment" box to your "backpack" box and you're done. Your hitpoints change correctly, your saving throws are up to date, your armor class goes back to reflecting the fact that you have natural armor from being a dragonborn. Your character sheet keeps up and you ultimately get more time to play the game. Huzzah!</p>
|
||||
<h2>Creating a Character</h2>
|
||||
<ul>
|
||||
<li>In the <a href={{pathFor route="characterList"}}>character list</a>, click the plus button, floating in the bottom right corner.</li>
|
||||
<li>Give your character a name, gender and race, these can be changed later if you change your mind. Then click the Add button</li>
|
||||
<li>Your new character should open, with most of its attributes and abilities at zero.</li>
|
||||
</ul>
|
||||
<paper-material class="wallOfText card" style="padding: 32px; max-width: 800px;"> {{#markdown}}
|
||||
|
||||
<h2>Adding Racial Effects</h2>
|
||||
<p>You have already given your character a race, but you haven't yet specified what that race does for your character, so lets do that.</p>
|
||||
<ul>
|
||||
<li>Click the Journal tab</li>
|
||||
<li>In the card that displays your level, click on your race to open the racial dialog box</li>
|
||||
<li>Click the edit button in the top corner of the racial dialog</li>
|
||||
</ul>
|
||||
<p>In the edit mode of the racial dialog you can change your race's name and add effects and proficiencies your race gives you. We will only be adding the base traits our race gives us, specific features can go in the features tab so we can more easily reference them later.</p>
|
||||
<p>Lets add some of the effects all races will give.</p>
|
||||
<ul>
|
||||
<li>Click the Add Effect button, a new effect should appear</li>
|
||||
<li>In the Stat Group dropdown box, choose "Stats"</li>
|
||||
<li>The second dropdown lets us choose which stat to effect, choose "Speed"</li>
|
||||
<li>The third dropdown lets us choose how to effect that stat, choose "Base Value", since our character's base speed comes from their race</li>
|
||||
<li>Finally, input the value for our characters speed, it'll probably be 30 unless you chose a slower race, such as a dwarf</li>
|
||||
<li>Close the Race dialog and navigate to the Stats tab</li>
|
||||
<li>The speed card should now correctly display the character's speed</li>
|
||||
<li>Click the speed card to see how that value was calculated</li>
|
||||
<li>Currently there is only one number effecting the total, the speed from our race, but as more effects from different sources start impacting our character's speed, they will show up here.</li>
|
||||
</ul>
|
||||
## Character Sheet Philosophy
|
||||
|
||||
<h2>Adding your ability scores</h2>
|
||||
<p>Your character currently doesn't have any ability scores, so lets fix that. Whether you roll your abilities or point-buy them, lets add a feature to represent where they came from</p>
|
||||
<ul>
|
||||
<li>Navigate to the <emd>Features</emd> tab</li>
|
||||
<li>Click the plus button in the bottom right to add a new feature</li>
|
||||
<li>Give the Feature a name, like <em>Point Buy</em></li>
|
||||
<li>Leave the feature as always enabled, don't limit its uses, and leave the description blank</li>
|
||||
<li>Click the <em>Add Effect</em> button</li>
|
||||
<li>For <em>Stat Group</em> choose <em>Ability Scores</em></li>
|
||||
<li>For <em>Stat</em> choose <em>Strength</em></li>
|
||||
<li>For the operation choose <em>Base Value</em></li>
|
||||
<li>Input your character's rolled or point-bought strength, without the racial modfier</li>
|
||||
<li>Repeat for the rest of your ability scores</li>
|
||||
</ul>
|
||||
<p>You can now check that your ability scores appear on your <em>Stats</em> page and that your skills that use them have their values calculated accordingly.</p>
|
||||
<p>We didn't include your character's racial ability modifiers in the feature, so you should go back to your character's racial dialog and add them in there as effects. Remember to use the add operation, rather than base value, since your race adds to your ability scores.</p>
|
||||
<p>By separating the source of your character's stats you can easily check how your character got their ability scores and stats, even after 20 levels, without getting confused or making mistakes.</p>
|
||||
Setting up your character on DiceCloud is going to take you a little longer than just filling it in on a paper character sheet would have. The goal of using an online sheet is to make actually playing the game more streamlined, and ultimately more fun. So putting a little extra effort into setting up your character now will pay off over and over again once you're playing.
|
||||
|
||||
<h2>Adding a Class</h2>
|
||||
<p>Currently your character is at level 0, because they don't have any class levels. Let's fix that.</p>
|
||||
<ul>
|
||||
<li>Click the plus button in the card that currently says "Level 0"</li>
|
||||
<li>A new class has now been added, name the class in the Class Name input and leave the level as 1</li>
|
||||
</ul>
|
||||
<p>We now have a class, lets add the saving throw proficiencies it gives us.</p>
|
||||
<ul>
|
||||
<li>Click the Add Proficiency button</li>
|
||||
<li>Click the dropdown box that currently has "Skill" selected, and choose "Saving Throw" instead</li>
|
||||
<li>In the second dropdown choose the first saving throw your class gives you</li>
|
||||
<li>The third dropdown lets us specify if we have half or double our proficiency bonus for this proficiency, leave it at the default "proficient" for now</li>
|
||||
</ul>
|
||||
<p>If you navigate back to the stat page, you will see that you now have a proficiency bonus, based on your class level, and the saving throw you are proficienct in will take your proficiency bonus into account.</p>
|
||||
<p>One of the most important things your class gives you is your hitpoints, so lets go add those now.</p>
|
||||
<ul>
|
||||
<li>Navigate to the class dialog box by clicking on your class name in the journal tab and hitting the edit button</li>
|
||||
<li>Click the Add Effect button</li>
|
||||
<li>Choose the <em>Stats</em> stat group, and choose the <em>Hitpoints</em> stat</li>
|
||||
<li>Choose the <em>Base Value</em> operation</li>
|
||||
</ul>
|
||||
<p>Now we need to decide how many hitpoints our class gives us. We will assume that we take the constant hitpoints per level, since it's both the rule used for league play and it's statistically advantageous over rolling for hitpoints every level.</p>
|
||||
<p>We could work out our hit points every level and change the effect each time, but we can do one better, we can input the calculation directly into the value field and have the character sheet figure it out for us</p>
|
||||
<p>Let's assume we are rolling a fighter, so in the class name you typed in "Fighter" (with the capital F, but without the quote marks). A fighter gets 10 hp at first level and 6 hitpoints every level after that.</p>
|
||||
<p>Lets rather split that into 4 bonus hitpoints at first level, and 6 hitpoints for every fighter level your character has. We can the write this as <em>4 + 6*FighterLevel</em> where the * represents multiplication.</p>
|
||||
<p><em>Note, we don't add the constitution modifier here, that's already taken care of by default, since all characters add their constitution modifier to their hit points</em></p>
|
||||
<ul>
|
||||
<li>In the value field input <em>4 + 6 * FighterLevel</em>, the spaces aren't needed, but you must spell your class name exactly as it is spelt in the class name input box, capital letters and all, in our case "Fighter"</li>
|
||||
<li>Create a new effect that effects the base value of <em>d10 Hit Dice</em> with the value of <em>FighterLevel</em>, since we also get our fighters level worth of hit dice</li>
|
||||
<li>Check how your changes are reflected in the <em>Stats</em> tab</li>
|
||||
<li>Change your level and check that the <em>Stats</em> tab gets updated accordingly</li>
|
||||
</ul>
|
||||
<p>You can try all sorts of calculations in your effects and in certain other places too. For example if you had some feature that is used a number of times equal to your wisdom modifier or 1, whichever is lower, you could limit its uses to <em>min(1, wisdomMod)</em> and the character sheet will figure it out for you, and update itself if you wisdom modifier happens to change later.</p>
|
||||
</paper-material>
|
||||
The idea is to track where each number comes from, and allow you to easily make changes on the fly.
|
||||
Let's look at a hypothetical example.
|
||||
|
||||
You need to swim through a sunken section of dungeon to fetch the quest's Thing.
|
||||
You'll need to take off your magical Plate Armor of +1 Constitution to swim without sinking, of course. Taking it off will change your armor class, your speed and your constitution, which in turn changes your hit points and your constitution saving throw. Working out all those changes in the middle of a game will drag the game to a halt.
|
||||
Fortunately you have a digital character sheet, so it's a matter of dragging your Plate Armor +1 Con from your "equipment" box to your "backpack" box and you're done. Your hitpoints change correctly, your saving throws are up to date, your armor class goes back to reflecting the fact that you have natural armor from being a dragonborn. Your character sheet keeps up and you ultimately get more time to play the game. Huzzah!
|
||||
|
||||
---
|
||||
|
||||
## Creating a Character
|
||||
|
||||
- In the [character list]({{pathFor route="characterList"}}), click the plus button, floating in the bottom right corner.
|
||||
- Give your character a name, gender and race - these can all be changed later if you change your mind. Then click the Add button.
|
||||
- Your new character should open, with your ability scores at a default of 10, but most other attributes at zero.
|
||||
|
||||
|
||||
## Adding Racial Effects
|
||||
You have already given your character a race, but you haven't yet specified what that race does for your character, so let's do that.
|
||||
|
||||
- Click the Journal tab.
|
||||
- In the card that displays your level, click on your race to open the racial dialog box.
|
||||
- Click the edit button (the pencil icon) in the top corner of the racial dialog.
|
||||
|
||||
In the edit mode of the racial dialog you can change your race's name and add effects and proficiencies your race gives you. We will only be adding the base traits our race gives us, specific features can go in the features tab so we can more easily reference them later.
|
||||
|
||||
Let's add some of the effects all races will give.
|
||||
|
||||
- Click the Add Effect button; a new window will open - this is the effect edit dialog.
|
||||
- In the left menu, scroll down to "Stats" and choose "Speed".
|
||||
- The right menu let's us choose how to effect that stat. Choose "Base Value", since our character's base speed comes from their race.
|
||||
- Finally, input the value for our characters speed, it'll probably be 30 unless you chose a slower race, such as a dwarf.
|
||||
- Close the Race dialog and navigate to the Stats tab.
|
||||
- The speed card should now correctly display the character's speed.
|
||||
- Click the speed card to see how that value was calculated.
|
||||
- Currently there is only one number effecting the total, the speed from our race, but as more effects from different sources start impacting our character's speed, they will show up here.
|
||||
|
||||
You can now also add any other *stat changes* given yo you by your race, for example a human's +1 to each ability score, or an elf's +2 Dexterity.
|
||||
|
||||
## Adding your ability scores
|
||||
|
||||
Your character's ability scores are currently all 10 by default - which means that they're no better than your average commoner! Whether you roll your abilities, point-buy them, or just use the standard set of values, you'll need to update them.
|
||||
|
||||
- Navigate to the *Features* tab.
|
||||
- Select the *Base Ability Scores* feature, which was added automatically.
|
||||
- Click the edit button (the pencil icon) in the top right corner.
|
||||
- Click the pencil icon to the right of your character's Strength to open the effect edit dialog.
|
||||
- Input your character's rolled or point-bought strength, *without* the racial modifier.
|
||||
- Notice that the operation is *Base Value* by default - this is what we want, as it is the character's *base* Strength score.
|
||||
- Repeat for the rest of your ability scores.
|
||||
|
||||
You can now check that your ability scores appear on your *Stats* page and that your skills that use them have their values calculated accordingly.
|
||||
|
||||
We didn't include your character's racial ability modifiers in the feature, so you should go back to your character's racial dialog and add them in there as effects. Remember to use the add operation, rather than base value, since your race adds to your ability scores.
|
||||
|
||||
By separating the source of your character's stats you can easily check how your character got their ability scores and stats, even after 20 levels, without getting confused or making mistakes.</p>
|
||||
|
||||
## Adding a Class
|
||||
|
||||
Currently your character is at level 0, because they don't have any class levels. Let's fix that.
|
||||
|
||||
- Click the plus button in the card that currently says "Level 0"
|
||||
- A new class has now been added, name the class in the Class Name input and leave the level as 1.
|
||||
|
||||
We now have a class, let's add the saving throw proficiencies it gives us.
|
||||
|
||||
- Click the Add Proficiency button
|
||||
- Click the dropdown box that currently has "Skill" selected, and choose "Saving Throw" instead
|
||||
- In the second dropdown choose the first saving throw your class gives you
|
||||
- The third dropdown let's us specify if we have half or double our proficiency bonus for this proficiency, leave it at the default "proficient" for now
|
||||
|
||||
If you navigate back to the stat page, you will see that you now have a proficiency bonus, based on your class level, and the saving throw you are proficienct in will take your proficiency bonus into account.
|
||||
|
||||
One of the most important things your class gives you is your hit points, so let's go add those now.
|
||||
|
||||
- Navigate to the class dialog box by clicking on your class name in the journal tab and hitting the edit button
|
||||
- Click the Add Effect button
|
||||
- Scroll down to *Stats* on the left, and choose the *Hit Points* stat.
|
||||
- Choose the *Base Value* operation.
|
||||
|
||||
Now we need to decide how many hit points our class gives us. We will assume that we take the constant hit points per level, since it's both the rule used for league play and it's statistically advantageous over rolling for hit points every level.
|
||||
|
||||
We could work out our hit points every level and change the effect each time, but we can do one better: we can input the calculation directly into the value field and have the character sheet figure it out for us.
|
||||
|
||||
Let's assume we are creating a fighter, so in the class name you typed in "Fighter" (with the capital F, but without the quote marks). A fighter gets 10 hp at first level and 6 hitpoints every level after that.
|
||||
|
||||
Let's rather split that into 4 bonus hit points at first level, and 6 hit points for every fighter level your character has. We can the write this as `4 + 6*FighterLevel` where the `*` represents multiplication.
|
||||
|
||||
*Note that we __don't add the constitution modifier here__; that's already taken care of by default, since all characters add their constitution modifier to their hit points automatically.*
|
||||
|
||||
- In the value field input `4 + 6*FighterLevel` - the spaces aren't needed, but you must spell your class name exactly as it is spelt in the class name input box, capital letters and all, in our case "Fighter"
|
||||
- Create a new effect that sets the base value of *d10 Hit Dice* to *FighterLevel*, since we also get a number of hit dice equal to our fighter's level.
|
||||
- Check how your changes are reflected in the *Stats* tab.
|
||||
- Change your level and check that the *Stats* tab gets updated accordingly.
|
||||
|
||||
This method of including calculations in other stats allows you to take full advantage of having a digital character sheet, as it means that you can change any one thing in your character sheet and everthing else will update automatically.
|
||||
|
||||
---
|
||||
|
||||
## Additional Tips
|
||||
|
||||
Any input field with a <iron-icon icon="lightbulb-outline"></iron-icon> light bulb icon is a *formula field*: it will compute any and all variables and functions within it. For example, the "Value" field in the effect edit dialog is a formula field, so you could set the value to `3`, or `FighterLevel*2`, or any formula you can think of.
|
||||
|
||||
Any input field with a <iron-icon icon="dicecloud:code-braces"></iron-icon> curly brackets icon is a *smart input field*: you can also use formulas here, but they must be enclosed within {curly brackets}. For example, the "Damage" field of a spell or weapon is a smart input field, so you could type `1d8 + {strengthMod}` for the damage, and if your strength modifier was +3, it would display as "1d8 + 3".
|
||||
|
||||
The full list of functions and variables can be found on the GitHub wiki, [here](https://github.com/ThaumRystra/DiceCloud1/wiki/Function-and-Variable-List).
|
||||
|
||||
Any description field, as well as some others like your background or description, can be formatted with Markdown.
|
||||
For example, having \*asterisks\* around something makes it *italic*, and a \*\*pair\*\* makes it **bold**.
|
||||
If you need to display an actual asterisk, you can escape it with a backslash, like this: \\\*.
|
||||
You can read more about Markdown [here](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet "Markdown Cheatsheet") and [here](https://daringfireball.net/projects/markdown/syntax "Markdown's origianal specification").
|
||||
|
||||
In addition, using three or more hyphens on their own line (like this: " --- "; this is the Markdown for a horizontal rule) will cut off the description of a feature card or any of the cards on the Persona page, so that the full description is only displayed - this is useful when having the full feature text would be annoyingly long, so you can simply display a summary on the card and have it expand into the full text.
|
||||
|
||||
|
||||
{{/markdown}}</paper-material>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
var hints = [
|
||||
"Drag and drop items to move them between containers",
|
||||
"Hold Ctrl while dragging items around to only move some of them",
|
||||
"Magic items are considered priceless, don't give them a gold value",
|
||||
"Drag and drop items to move them between containers.",
|
||||
"Hold Ctrl while dragging items around to only move some of them.",
|
||||
"Magic items are considered priceless, don't give them a gold value.",
|
||||
"You can use formulae in {curly brackets} in any field with a {} icon.",
|
||||
"You can disable the 'Spells' tab from the charecter menu in the top right.",
|
||||
"You can share your character with others from the menu in the top right.",
|
||||
"Your spells, features, and items are ordered by their colour, which you can set with the paint bucket.",
|
||||
"You can only have three magic items attuned to you at once. Choose carefully!",
|
||||
"Click the '+' underneath 'Hit Points' to add additional health bars for temporary HP, wild shapes, familiars and more.",
|
||||
];
|
||||
|
||||
Template.loading.helpers({
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.base-dialog h1,
|
||||
.base-dialog h2,
|
||||
.base-dialog h3,
|
||||
.base-dialog h4,
|
||||
.base-dialog h5,
|
||||
.base-dialog h6{
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.base-dialog p{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -15,10 +15,12 @@
|
||||
{{/unless}}
|
||||
{{#unless hideColor}}
|
||||
{{> colorDropdown}}
|
||||
{{/unless}}
|
||||
{{#unless editOnly}}
|
||||
<paper-icon-button id="doneEditingButton"
|
||||
icon="done">
|
||||
</paper-icon-button>
|
||||
{{/unless}}
|
||||
<paper-icon-button id="doneEditingButton"
|
||||
icon="done">
|
||||
</paper-icon-button>
|
||||
{{else}}
|
||||
{{#if showEdit}}
|
||||
<paper-icon-button id="editButton"
|
||||
|
||||
@@ -4,11 +4,13 @@ Template.baseDialog.onCreated(function(){
|
||||
|
||||
Template.baseDialog.helpers({
|
||||
editing: function(){
|
||||
if (!Template.parentData() || !Template.parentData().charId) return true;
|
||||
return Template.instance().editing.get() &&
|
||||
canEditCharacter(Template.parentData().charId);
|
||||
},
|
||||
showEdit: function() {
|
||||
if (this.hideEdit) return false;
|
||||
if (!Template.parentData() || !Template.parentData().charId) return true;
|
||||
return canEditCharacter(Template.parentData().charId);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.textarea-bracket-suffix {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.description-input > paper-textarea {
|
||||
width: 100%;
|
||||
width: 100% - 24px;
|
||||
}
|
||||
@@ -1,13 +1,35 @@
|
||||
<template name="formulaSuffix">
|
||||
<div suffix>
|
||||
<paper-tooltip position="left" animation-delay="0">This is a formula field</paper-tooltip>
|
||||
<div suffix style="position: relative">
|
||||
<iron-icon icon="lightbulb-outline"></iron-icon>
|
||||
{{# simpleTooltip}}
|
||||
This is a formula field
|
||||
{{/ simpleTooltip}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="bracketSuffix">
|
||||
<div suffix>
|
||||
<paper-tooltip position="left" animation-delay="0">This field accepts formulae in {curly brackets}</paper-tooltip>
|
||||
<div suffix style="position: relative">
|
||||
<iron-icon icon="dicecloud:code-braces"></iron-icon>
|
||||
{{# simpleTooltip}}
|
||||
This field accepts formulae in {curly brackets}
|
||||
{{/ simpleTooltip}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="textareaBracketSuffix">
|
||||
<div class="textarea-bracket-suffix">
|
||||
<div class="markdown" style="position: relative">
|
||||
<iron-icon icon="dicecloud:markdown"></iron-icon>
|
||||
{{# simpleTooltip}}
|
||||
This field accepts markdown formatting
|
||||
{{/ simpleTooltip}}
|
||||
</div>
|
||||
<div class="brackets" style="position: relative">
|
||||
<!--<paper-tooltip position="left" animation-delay="0">This field accepts formulae in {curly brackets}</paper-tooltip>-->
|
||||
<iron-icon icon="dicecloud:code-braces"></iron-icon>
|
||||
{{# simpleTooltip}}
|
||||
This field accepts formulae in {curly brackets}
|
||||
{{/ simpleTooltip}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.simple-tooltip:hover .tooltip {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease-in;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
background-color: #616161;
|
||||
color: white;
|
||||
padding: 8px;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
right: calc(100% + 8px);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<template name="simpleTooltip">
|
||||
<div class="simple-tooltip fit">
|
||||
<div class="tooltip">
|
||||
{{> Template.contentBlock}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,71 +1,82 @@
|
||||
statOrder = {
|
||||
"strength": 1,
|
||||
"dexterity": 2,
|
||||
"constitution": 3,
|
||||
"intelligence": 4,
|
||||
"wisdom": 5,
|
||||
"charisma": 6,
|
||||
"strengthSave": 7,
|
||||
"dexteritySave": 8,
|
||||
"constitutionSave": 9,
|
||||
"intelligenceSave": 10,
|
||||
"wisdomSave": 11,
|
||||
"charismaSave": 12,
|
||||
"acrobatics": 13,
|
||||
"animalHandling": 14,
|
||||
"arcana": 15,
|
||||
"athletics": 16,
|
||||
"deception": 17,
|
||||
"history": 18,
|
||||
"insight": 19,
|
||||
"intimidation": 20,
|
||||
"investigation": 21,
|
||||
"medicine": 22,
|
||||
"nature": 23,
|
||||
"perception": 24,
|
||||
"performance": 25,
|
||||
"persuasion": 26,
|
||||
"religion": 27,
|
||||
"sleightOfHand": 28,
|
||||
"stealth": 29,
|
||||
"survival": 30,
|
||||
"initiative": 31,
|
||||
"hitPoints": 32,
|
||||
"armor": 33,
|
||||
"dexterityArmor": 34,
|
||||
"speed": 35,
|
||||
"proficiencyBonus": 36,
|
||||
"ki": 37,
|
||||
"sorceryPoints": 38,
|
||||
"rages": 39,
|
||||
"rageDamage": 40,
|
||||
"expertiseDice": 41,
|
||||
"superiorityDice": 42,
|
||||
"carryMultiplier": 43,
|
||||
"level1SpellSlots": 44,
|
||||
"level2SpellSlots": 45,
|
||||
"level3SpellSlots": 46,
|
||||
"level4SpellSlots": 47,
|
||||
"level5SpellSlots": 48,
|
||||
"level6SpellSlots": 49,
|
||||
"level7SpellSlots": 50,
|
||||
"level8SpellSlots": 51,
|
||||
"level9SpellSlots": 52,
|
||||
"d6HitDice": 53,
|
||||
"d8HitDice": 54,
|
||||
"d10HitDice": 55,
|
||||
"d12HitDice": 56,
|
||||
"acidMultiplier": 57,
|
||||
"bludgeoningMultiplier": 58,
|
||||
"coldMultiplier": 59,
|
||||
"fireMultiplier": 60,
|
||||
"forceMultiplier": 61,
|
||||
"lightningMultiplier": 62,
|
||||
"necroticMultiplier": 63,
|
||||
"piercingMultiplier": 64,
|
||||
"poisonMultiplier": 65,
|
||||
"psychicMultiplier": 66,
|
||||
"radiantMultiplier": 67,
|
||||
"slashingMultiplier": 68,
|
||||
"thunderMultiplier": 69,
|
||||
};
|
||||
var statsInOrder = [
|
||||
"strength",
|
||||
"dexterity",
|
||||
"constitution",
|
||||
"intelligence",
|
||||
"wisdom",
|
||||
"charisma",
|
||||
|
||||
"strengthSave",
|
||||
"dexteritySave",
|
||||
"constitutionSave",
|
||||
"intelligenceSave",
|
||||
"wisdomSave",
|
||||
"charismaSave",
|
||||
|
||||
"acrobatics",
|
||||
"animalHandling",
|
||||
"arcana",
|
||||
"athletics",
|
||||
"deception",
|
||||
"history",
|
||||
"insight",
|
||||
"intimidation",
|
||||
"investigation",
|
||||
"medicine",
|
||||
"nature",
|
||||
"perception",
|
||||
"performance",
|
||||
"persuasion",
|
||||
"religion",
|
||||
"sleightOfHand",
|
||||
"stealth",
|
||||
"survival",
|
||||
"initiative",
|
||||
|
||||
"hitPoints",
|
||||
"armor",
|
||||
"dexterityArmor",
|
||||
"speed",
|
||||
"proficiencyBonus",
|
||||
"ki",
|
||||
"sorceryPoints",
|
||||
"rages",
|
||||
"rageDamage",
|
||||
"expertiseDice",
|
||||
"superiorityDice",
|
||||
"carryMultiplier",
|
||||
|
||||
"level1SpellSlots",
|
||||
"level2SpellSlots",
|
||||
"level3SpellSlots",
|
||||
"level4SpellSlots",
|
||||
"level5SpellSlots",
|
||||
"level6SpellSlots",
|
||||
"level7SpellSlots",
|
||||
"level8SpellSlots",
|
||||
"level9SpellSlots",
|
||||
|
||||
"d6HitDice",
|
||||
"d8HitDice",
|
||||
"d10HitDice",
|
||||
"d12HitDice",
|
||||
|
||||
"acidMultiplier",
|
||||
"bludgeoningMultiplier",
|
||||
"coldMultiplier",
|
||||
"fireMultiplier",
|
||||
"forceMultiplier",
|
||||
"lightningMultiplier",
|
||||
"necroticMultiplier",
|
||||
"piercingMultiplier",
|
||||
"poisonMultiplier",
|
||||
"psychicMultiplier",
|
||||
"radiantMultiplier",
|
||||
"slashingMultiplier",
|
||||
"thunderMultiplier",
|
||||
];
|
||||
|
||||
statOrder = {};
|
||||
_.each(statsInOrder, function(element, index){
|
||||
statOrder[element] = index;
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user