Compare commits

...

17 Commits
0.2.3 ... 0.2.7

Author SHA1 Message Date
Stefan Zermatten
2404845d51 Styling and rounding fixes for detail boxes 2015-05-12 11:34:56 +02:00
Stefan Zermatten
bf032bcdf3 Now only show edit and add buttons to writers, not readers 2015-05-12 11:34:37 +02:00
Stefan Zermatten
ff8ae89722 Improved feedback form style 2015-05-12 10:36:15 +02:00
Stefan Zermatten
80ca7307ce Added change log 2015-05-12 10:10:15 +02:00
Stefan Zermatten
a539b0bc6c limited how much info gets published to users about themselves 2015-05-12 09:33:32 +02:00
Stefan Zermatten
c6b3cad9c8 Improved character side list style 2015-05-12 09:32:40 +02:00
Stefan Zermatten
95b7b66390 Added quick feedback form 2015-05-12 09:32:28 +02:00
Stefan Zermatten
43c4122fe3 Fixed stack dragging within the same container 2015-05-11 17:10:58 +02:00
Stefan Zermatten
3f4dcc146a fixed charId's being out of date after re-parenting 2015-05-11 17:10:28 +02:00
Stefan Zermatten
e4600decd0 Added more things to except list for need sign in 2015-05-11 16:51:17 +02:00
Stefan Zermatten
f6df716870 Rewrote how item drag and drop works. Need to update charId's to keep up. 2015-05-11 16:51:02 +02:00
Stefan Zermatten
b99da301cd Updated meteor 2015-05-11 12:16:38 +02:00
Stefan Zermatten
0a01885300 Finished implementing useraccounts 2015-05-11 12:15:00 +02:00
Stefan Zermatten
5cb1515235 Began implementing useraccounts and permissions properly 2015-05-08 12:59:38 +02:00
Stefan Zermatten
7430c2c795 Prevented evaluate crashing when given duff abilityMod strings 2015-05-06 12:12:18 +02:00
Stefan Zermatten
39f7548b8d Fixed singlecharacter publication returning undefined rather than an empty array 2015-05-06 12:11:37 +02:00
Stefan Zermatten
c4a8c4b7ba Fixed typos in the guide, floating action buttons are on the bottom RIGHT not left 2015-05-04 10:01:47 +02:00
46 changed files with 794 additions and 279 deletions

View File

@@ -20,3 +20,7 @@ mike:mocha
dburles:mongo-collection-instances
percolate:migrations
ecwyne:mathjs
useraccounts:polymer
accounts-google
splendido:accounts-meld
email

View File

@@ -1,9 +1,11 @@
accounts-base@1.2.0
accounts-google@1.0.4
accounts-oauth@1.1.5
accounts-password@1.1.1
accounts-ui@1.1.5
accounts-ui-unstyled@1.1.7
aldeed:collection2@2.3.3
aldeed:simple-schema@1.3.2
aldeed:simple-schema@1.3.3
amplify@1.0.0
autoupdate@1.2.1
base64@1.0.3
@@ -24,6 +26,7 @@ ejson@1.0.6
email@1.0.6
fastclick@1.0.3
geojson-utils@1.0.3
google@1.1.5
html-tools@1.0.4
htmljs@1.0.4
http@1.1.0
@@ -44,20 +47,22 @@ less@1.0.14
livedata@1.0.13
localstorage@1.0.3
logging@1.0.7
matb33:collection-hooks@0.7.11
matb33:collection-hooks@0.7.13
meteor@1.1.6
meteor-platform@1.2.2
mike:mocha@0.5.3
mike:mocha@0.5.4
minifiers@1.1.5
minimongo@1.0.8
mobile-status-bar@1.0.3
momentjs:moment@2.10.3
mongo@1.1.0
npm-bcrypt@0.7.8_2
oauth@1.1.4
oauth2@1.1.3
observe-sequence@1.0.6
ordered-dict@1.0.3
package-version-parser@3.0.3
percolate:migrations@0.7.3
percolate:migrations@0.7.5
practicalmeteor:chai@1.9.2_3
practicalmeteor:loglevel@1.1.0_3
random@1.0.3
@@ -72,16 +77,21 @@ sanjo:meteor-version@1.0.0
service-configuration@1.0.4
session@1.1.0
sha@1.0.3
softwarerero:accounts-t9n@1.0.9
spacebars@1.0.6
spacebars-compiler@1.0.6
splendido:accounts-emails-field@1.2.0
splendido:accounts-meld@1.3.0
srp@1.0.3
templating@1.1.1
tracker@1.0.7
ui@1.0.6
underscore@1.0.3
url@1.0.4
useraccounts:core@1.9.1
useraccounts:polymer@1.9.1
velocity:chokidar@0.12.6_1
velocity:core@0.6.0
velocity:core@0.6.1
velocity:html-reporter@0.5.3
velocity:meteor-internals@1.1.0_7
velocity:shim@0.1.0

View File

@@ -61,7 +61,6 @@ Effects.attachSchema(Schemas.Effect);
if (Meteor.isServer) Characters.after.insert(function(userId, char) {
Effects.insert({
charId: char._id,
type: "inate",
name: "Constitution modifier for each level",
stat: "hitPoints",
operation: "add",
@@ -69,11 +68,11 @@ if (Meteor.isServer) Characters.after.insert(function(userId, char) {
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
Effects.insert({
charId: char._id,
type: "inate",
name: "Proficiency bonus by level",
stat: "proficiencyBonus",
operation: "add",
@@ -81,11 +80,11 @@ if (Meteor.isServer) Characters.after.insert(function(userId, char) {
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
Effects.insert({
charId: char._id,
type: "inate",
name: "Dexterity Armor Bonus",
stat: "armor",
operation: "add",
@@ -93,11 +92,11 @@ if (Meteor.isServer) Characters.after.insert(function(userId, char) {
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
Effects.insert({
charId: char._id,
type: "inate",
name: "Natural Armor",
stat: "armor",
operation: "base",
@@ -105,6 +104,7 @@ if (Meteor.isServer) Characters.after.insert(function(userId, char) {
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
});

View File

@@ -19,6 +19,156 @@ Schemas.Item = new SimpleSchema({
Items.attachSchema(Schemas.Item);
var checkMovePermission = function(itemId, parent) {
var item = Items.findOne(itemId);
if (!item)
throw new Meteor.Error("No such item",
"An item could not be found to move");
//handle permissions
var permission = Meteor.call("canWriteCharacter", item.charId);
if (!permission){
throw new Meteor.Error("Access denied",
"Not permitted to move items from this character");
}
if (parent.collection === "Characters"){
permission = Meteor.call("canWriteCharacter", parent.id);
if (!permission){
throw new Meteor.Error("Access denied",
"Not permitted to move items to this character");
}
} else {
var parentCollectionObject = global[parent.collection];
var parentObject = null;
if (parentCollectionObject)
parentObject = parentCollectionObject.findOne(
parent.id, {fields: {_id: 1, charId: 1}}
);
if (!parentObject) throw new Meteor.Error(
"Invalid parent",
"The destination parent " + parent.id +
" does not exist in the collection " + parent.collection
);
if (parentObject.charId){
permission = Meteor.call("canWriteCharacter", parentObject.charId);
if (!permission){
throw new Meteor.Error("Access denied",
"Not permitted to move items to this character");
}
}
}
};
var moveItem = function(itemId, enable, parentCollection, parentId) {
var item = Items.findOne(itemId);
if (!item) return;
parentCollection = parentCollection || item.parent.collection;
parentId = parentId || item.parent.id;
if (Meteor.isServer) {
checkMovePermission(itemId, {collection: parentCollection, id: parentId});
}
//update the item provided the update will actually change something
if (
item.parent.collection !== parentCollection ||
item.parent.id !== parentId ||
item.enabled !== enable
){
Items.update(
itemId,
{$set: {
"parent.collection": parentCollection,
"parent.id": parentId,
enabled: enable,
}}
);
}
};
Meteor.methods({
moveItemToParent: function(itemId, parent) {
check(itemId, String);
check(parent, {collection: String, id: String});
moveItem(itemId, false, parent.collection, parent.id);
},
moveItemToCharacter: function(itemId, charId) {
check(itemId, String);
check(charId, String);
moveItem(itemId, false, "Characters", charId);
},
moveItemToContainer: function(itemId, containerId) {
check(itemId, String);
check(containerId, String);
moveItem(itemId, false, "Containers", containerId);
},
equipItem: function(itemId, charId){
check(itemId, String);
check(charId, String);
moveItem(itemId, true, "Characters", charId);
},
unequipItem: function(itemId, charId){
check(itemId, String);
check(charId, String);
moveItem(itemId, false, "Characters", charId);
},
splitItemToParent: function(itemId, moveQuantity, parent){
check(itemId, String);
check(moveQuantity, Number);
check(parent, {id: String, collection: String});
//get the item
var item = Items.findOne(itemId);
if (!item) return;
//don't bother moving nothing
if (moveQuantity <= 0 || item.quantity <= 0){
return;
}
//ensure we are only moving up to the current stack size
if (item.quantity < moveQuantity){
moveQuantity = this.quantity;
}
if (Meteor.isServer) {
checkMovePermission(itemId, parent);
}
//create a new item stack
var newStack = _.omit(EJSON.clone(item), "_id");
newStack.parent = parent;
newStack.quantity = moveQuantity;
//find out if we have an exact replica in the destination
var query = _.omit(newStack, ["parent", "quantity"]);
query["parent.collection"] = newStack.parent.collection;
query["parent.id"] = newStack.parent.id;
query._id = {$ne: itemId}; //make sure we don't join it to itself
var existingStack = Items.findOne(query);
if (existingStack){
//increase the existing stack's size
Items.update(
existingStack._id,
{$inc: {quantity: moveQuantity}}
);
} else {
//insert the new stack
Items.insert(newStack, function(err, id){
if (err) throw err;
//copy the children also
Meteor.call("cloneChildren", item._id, {collection: "Items", id: id});
});
}
//reduce the old stack's size
var oldQuantity = item.quantity - moveQuantity;
if (oldQuantity === 0){
Items.remove(itemId);
} else {
Items.update(itemId, {$set: {quantity: oldQuantity}});
}
},
});
Items.helpers({
totalValue: function(){
return this.value * this.quantity;
@@ -33,103 +183,6 @@ Items.helpers({
return this.name;
}
},
equip: function(characterId){
var charId = characterId || this.charId;
if (!charId || !Characters.findOne(charId)) throw "Invalid character";
if (this.parent.collection === "Characters" &&
this.parent.id === charId &&
this.enabled) {
return;
}
Items.update(
this._id,
{$set: {
"parent.collection": "Characters",
"parent.id": charId,
enabled: true,
}}
);
},
unequip: function(){
if (!this.enabled) return;
Items.update(this._id, {$set: {enabled: false}});
},
moveToContainer: function(containerId){
if (!containerId || !Containers.findOne(containerId)){
throw "Invalid container";
}
if (this.parent.collection === "Containers" &&
this.parent.id === containerId &&
!this.enabled) return;
Items.update(
this._id,
{$set: {
"parent.collection": "Containers",
"parent.id": containerId,
enabled: false,
}}
);
},
moveToCharacter: function(characterId){
if (!characterId || !Characters.findOne(characterId)) {
throw "Invalid character";
}
if (this.parent.collection === "Characters" &&
this.parent.id === characterId &&
!this.enabled) return;
Items.update(
this._id,
{$set: {
"parent.collection": "Characters",
"parent.id": characterId,
charId: characterId,
enabled: false,
}}
);
},
splitToParent: function(parent, moveQuantity){
check(parent, {id: String, collection: String});
check(moveQuantity, Number);
var parentCollection = Meteor.isClient ?
window[parent.collection] : global[parent.collection];
if (!parent.id || !parentCollection.findOne(parent.id)){
throw "Invalid parent";
}
var oldStack = this;
//we can only move as much as we have, leaving 0 behind at worst
if (oldStack.quantity < moveQuantity) moveQuantity = oldStack.quantity;
var oldQuantity = oldStack.quantity - moveQuantity;
var newStack = _.pick(oldStack, Schemas.Item.objectKeys());
newStack.parent = parent;
newStack.quantity = moveQuantity;
var existingStack = Items.findOne(_.omit(newStack, "quantity"));
var updateStackSize = function(){
if (oldQuantity > 0){
Items.update(oldStack._id, {$set: {quantity: oldQuantity}});
} else {
Items.remove(oldStack._id);
}
};
if (existingStack){
Items.update(
existingStack._id,
{$inc: {quantity: moveQuantity}},
{},
function(){
updateStackSize();
}
);
} else {
Items.insert(newStack, function(err, id){
if (err) throw err;
updateStackSize();
//copy the children also
Meteor.call("cloneChildren", oldStack._id, {collection: "Items", id: id});
});
}
},
});
Items.before.update(function(userId, doc, fieldNames, modifier, options){

View File

@@ -0,0 +1,27 @@
ChangeLogs = new Mongo.Collection("changeLogs");
Schemas.ChangeLog = new SimpleSchema({
version: {
type: String,
},
changes: {
type: [String],
},
});
ChangeLogs.attachSchema(Schemas.ChangeLog);
ChangeLogs.allow({
insert: function(userId, doc) {
var user = Meteor.users.findOne(userId);
if (user) return _.contains(user.roles, "admin");
},
update: function(userId, doc, fields, modifier) {
var user = Meteor.users.findOne(userId);
if (user) return _.contains(user.roles, "admin");
},
remove: function(userId, doc) {
var user = Meteor.users.findOne(userId);
if (user) return _.contains(user.roles, "admin");
},
});

View File

@@ -0,0 +1,60 @@
Reports = new Mongo.Collection("reports");
Schemas.Report = new SimpleSchema({
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
title: {
type: String,
trim: false,
optional: true,
},
description: {
type: String,
trim: false,
optional: true,
},
type: {
type: String,
allowedValues: ["bug", "change", "feature", "general"],
defaultValue: "bug",
},
//the immediate impact of doing this action (eg. -1 rages)
severity: {
type: Number,
defaultValue: 5,
min: 1,
max: 10,
},
metaData: {
type: Object,
blackbox: true,
},
});
Reports.attachSchema(Schemas.Report);
Meteor.methods({
insertReport: function(report) {
check(report, {
title: String,
description: String,
type: String,
severity: Number,
metaData: Object,
});
report.owner = this.userId;
Reports.insert(report);
},
deleteReport: function(id) {
var user = Meteor.users.findOne(this.userId);
if (!_.contains(user.roles, "admin")){
throw new Meteor.Error(
"not admin",
"The user must be an administrator to delete feedback"
);
}
Reports.remove(id);
},
});

View File

@@ -1,44 +1,23 @@
Schema = {};
Schema.User = new SimpleSchema({
username: {
type: String,
regEx: /^[a-z0-9A-Z_]{3,15}$/,
optional: true,
},
emails: {
type: [Object],
// this must be optional if you also use other login services like facebook,
// but if you use only accounts-password, then it can be required
optional: true,
},
"emails.$.address": {
type: String,
regEx: SimpleSchema.RegEx.Email,
},
"emails.$.verified": {
type: Boolean
},
createdAt: {
type: Date
},
services: {
type: Object,
optional: true,
blackbox: true,
},
roles: {
type: [String],
optional: true,
},
});
Meteor.users.attachSchema(Schema.User);
Meteor.users.allow({
update: function(userId, doc, fields, modifier) {
return userId === doc._id &&
fields.length === 1 &&
fields[0] === "username";
if (
doc._id === userId &&
_.contains(fields, "username") &&
_.contains(fields, "profile") &&
fields.length === 2 &&
_.keys(modifier).length === 1 &&
modifier.$set &&
modifier.$set["profile.username"] &&
modifier.$set.username &&
_.keys(modifier.$set).length === 2
){
var expectedUsername = modifier.$set["profile.username"];
expectedUsername = expectedUsername.toLowerCase().replace(/\s+/gm, "");
if (modifier.$set.username !== expectedUsername){
return false;
}
var foundUser = Meteor.call("getUserId", expectedUsername);
return !foundUser || foundUser === userId;
}
}
});

View File

@@ -3,9 +3,29 @@ Router.configure({
layoutTemplate: "layout",
});
Router.plugin("ensureSignedIn", {
except: [
"home",
"atSignIn",
"atSignUp",
"atForgotPassword",
"atResetPwd",
"atEnrollAccount",
"atVerifyEmail",
"atresendVerificationEmail",
"loginButtons",
"notFound",
]
});
Router.plugin("dataNotFound", {notFoundTemplate: "notFound"});
Router.map(function() {
this.route("/", {
name: "home",
onAfterAction: function() {
document.title = appName;
},
});
this.route("characterList", {
@@ -33,7 +53,7 @@ Router.map(function() {
data: function() {
var data = Characters.findOne(
{_id: this.params._id},
{fields: {_id: 1, name: 1, color: 1}}
{fields: {_id: 1, name: 1, color: 1, writers: 1, readers: 1}}
);
return data;
},
@@ -56,4 +76,21 @@ Router.map(function() {
document.title = appName + " Account";
},
});
this.route("/changelog", {
name: "changeLog",
waitOn: function() {
return [
Meteor.subscribe("changeLog"),
]
},
data: {
changeLogs: function() {
return ChangeLogs.find({}, {sort: {version: -1}});
}
},
onAfterAction: function() {
document.title = appName;
},
});
});

View File

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

View File

@@ -5,18 +5,24 @@
<div flex>
{{name}}
</div>
<div>
{{> colorDropdown}}
</div>
<paper-menu-button>
<paper-icon-button icon="more-vert" noink></paper-icon-button>
<paper-dropdown class="dropdown" halign="right">
<core-menu class="menu" style="color: black; color: rgba(0,0,0,0.87);">
<paper-item id="deleteCharacter"><core-icon icon="delete"></core-icon>Delete</paper-item>
<paper-item id="shareCharacter"><core-icon icon="social:share"></core-icon>Share</paper-item>
</core-menu>
</paper-dropdown>
</paper-menu-button>
{{#if canEditCharacter _id}}
<div>
{{> colorDropdown}}
</div>
<paper-menu-button>
<paper-icon-button icon="more-vert" noink></paper-icon-button>
<paper-dropdown class="dropdown" halign="right">
<core-menu class="menu" style="color: black; color: rgba(0,0,0,0.87);">
<paper-item id="deleteCharacter">
<core-icon icon="delete"></core-icon>Delete
</paper-item>
<paper-item id="shareCharacter">
<core-icon icon="social:share"></core-icon>Share
</paper-item>
</core-menu>
</paper-dropdown>
</paper-menu-button>
{{/if}}
<div class="bottom fit" horizontal layout>
<paper-tabs flex horizontal center layout id="characterSheetTabs" selected={{selectedTab}} class="{{colorClass}}">
<paper-tab name="stats">Stats</paper-tab>

View File

@@ -13,7 +13,7 @@ var getTab = function(charId){
Template.characterSheet.helpers({
selectedTab: function(){
return getTab(this._id);
}
},
});
Template.characterSheet.events({

View File

@@ -104,14 +104,16 @@
</div>
<div class="fab-buffer"></div>
</div>
<paper-fab id="addFeature"
class="floatyButton"
icon="add"
title="Add"
role="button"
tabindex="0"
aria-label="Add"
hero-id="main"></paper-fab>
{{#if canEditCharacter _id}}
<paper-fab id="addFeature"
class="floatyButton"
icon="add"
title="Add"
role="button"
tabindex="0"
aria-label="Add"
hero-id="main"></paper-fab>
{{/if}}
</div>
</template>

View File

@@ -77,10 +77,12 @@
</div>
<div class="fab-buffer"></div>
</div>
<paper-fab-menu id="inventoryAddMenu" icon="add" closeIcon="close" duration="0.3">
<paper-fab-menu-item id="addItem" icon="note-add" color="#d23f31" tooltip="Item"></paper-fab-menu-item>
<paper-fab-menu-item id="addContainer" icon="work" color="#d23f31" tooltip="Container"></paper-fab-menu-item>
</paper-fab-menu>
{{#if canEditCharacter _id}}
<paper-fab-menu id="inventoryAddMenu" icon="add" closeIcon="close" duration="0.3">
<paper-fab-menu-item id="addItem" icon="note-add" color="#d23f31" tooltip="Item"></paper-fab-menu-item>
<paper-fab-menu-item id="addContainer" icon="work" color="#d23f31" tooltip="Container"></paper-fab-menu-item>
</paper-fab-menu>
{{/if}}
</div>
</template>

View File

@@ -174,71 +174,93 @@ Template.inventoryItem.helpers({
Template.layout.events({
"dragstart .inventoryItem": function(event, instance){
event.originalEvent.dataTransfer.setData("dicecloud-id/items", this._id);
Session.set("inventory.dragItemId", this._id);
Session.set("inventory.dragItemOriginalContainer", this.container);
Session.set("inventory.dragItemOriginalCharacter", this.charId);
},
"dragover .itemContainer, dragenter .itemContainer":
function(event, instance){
if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/items")){
event.preventDefault();
}
},
"dragover .equipmentContainer, dragenter .equipmentContainer":
function(event, instance){
if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/items")){
event.preventDefault();
}
},
"dragover .carriedContainer, dragenter .carriedContainer":
function(event, instance){
if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/items")){
event.preventDefault();
}
},
"dragover .characterRepresentative, dragenter .characterRepresentative":
function(event, instance){
if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/items")){
event.preventDefault();
}
},
"dragend .inventoryItem": function(event, instance){
resetInvetorySession(); //this is a valid drop zone
Session.set("inventory.dragItemId", null);
},
"dragover .itemContainer": function(event, instance){
event.preventDefault();
},
"dragover .equipmentContainer": function(event, instance){
event.preventDefault();
},
"dragover .carriedContainer": function(event, instance){
event.preventDefault();
},
"drop .itemContainer": function(event, instacne){
var item = Items.findOne(Session.get("inventory.dragItemId"));
"drop .itemContainer": function(event, instance){
var itemId = event.originalEvent.dataTransfer.getData("dicecloud-id/items");
if (event.ctrlKey){
//split the stack to the container
GlobalUI.showDialog({
template: "splitStackDialog",
data: {
id: item._id,
id: itemId,
parentCollection: "Containers",
parentId: this._id,
},
});
} else {
//move item to the container
item.moveToContainer(this._id);
Meteor.call("moveItemToContainer", itemId, this._id);
}
resetInvetorySession();
Session.set("inventory.dragItemId", null);
},
"drop .equipmentContainer": function(event, instance){
var charId = Session.get("inventory.dragItemOriginalCharacter");
var item = Items.findOne(Session.get("inventory.dragItemId"));
item.equip(charId);
resetInvetorySession();
var itemId = event.originalEvent.dataTransfer.getData("dicecloud-id/items");
Meteor.call("equipItem", itemId, this._id);
Session.set("inventory.dragItemId", null);
},
"drop .carriedContainer": function(event, instance){
var charId = Session.get("inventory.dragItemOriginalCharacter");
var item = Items.findOne(Session.get("inventory.dragItemId"));
var itemId = event.originalEvent.dataTransfer.getData("dicecloud-id/items");
if (event.ctrlKey){
//split the stack to the container
GlobalUI.showDialog({
template: "splitStackDialog",
data: {
id: item._id,
id: itemId,
parentCollection: "Characters",
parentId: this._id,
},
});
} else {
//move item to the character
item.moveToCharacter(this._id);
Meteor.call("moveItemToCharacter", itemId, this._id);
}
resetInvetorySession();
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
GlobalUI.showDialog({
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);
},
});
var resetInvetorySession = function(){
_.defer(function(){
Session.set("inventory.dragItemId", null);
Session.set("inventory.dragItemOriginalContainer", null);
Session.set("inventory.dragItemOriginalCharacter", null);
});
};

View File

@@ -10,7 +10,7 @@
<template name="itemDetails">
<div layout horizontal wrap center justified class="headline">
{{#if weight}}<div class="sideMargin">{{totalWeight}}lbs</div>{{/if}}
{{#if weight}}<div class="sideMargin">{{round totalWeight}}lbs</div>{{/if}}
{{#if value}}<div>{{valueString totalValue}}</div>{{/if}}
</div>
<div layout horizontal wrap class="caption">

View File

@@ -81,13 +81,10 @@ Template.itemEdit.events({
},
"change #equippedInput": function(event){
var equipped = Template.instance().find("#equippedInput").checked;
var item = Items.findOne(this._id);
if (item){
if (equipped){
item.equip();
} else {
item.unequip();
}
if (equipped){
Meteor.call("equipItem", this._id, this.charId);
} else {
Meteor.call("unequipItem", this._id, this.charId);
}
},
"change #attunementCheckbox": function(event){
@@ -107,7 +104,6 @@ Template.containerDropdown.events({
var detail = event.originalEvent.detail;
if (!detail.isSelected) return;
var containerId = detail.item.getAttribute("name");
var item = Items.findOne(Template.currentData()._id);
item.moveToContainer(containerId);
Meteor.call("moveItemToContainer", Template.currentData()._id, containerId);
}
});

View File

@@ -7,13 +7,12 @@ Template.splitStackDialog.helpers({
Template.splitStackDialog.events({
"tap #moveButton": function(event, instance){
var item = Items.findOne(this.id);
if (item){
item.splitToParent(
{collection: this.parentCollection , id: this.parentId},
+instance.find("#quantityInput").value
);
}
Meteor.call(
"splitItemToParent",
this.id,
+instance.find("#quantityInput").value,
{collection: this.parentCollection , id: this.parentId}
);
},
"tap #oneButton":function(event, instance){
instance.find("#quantityInput").value = 1;

View File

@@ -1,7 +1,7 @@
<template name="experienceDialog">
{{#with experience}}
{{#baseDialog title=name class=colorClass hideColor="true" startEditing=../startEditing}}
<div horizontal layout center-justified>
<div horizontal layout center-justified class= "display2">
{{value}}
</div>
{{#if description}}

View File

@@ -68,6 +68,7 @@
<div class="fab-buffer"></div>
</div>
</div>
{{#if canEditCharacter _id}}
<paper-fab id="addNote"
class="floatyButton"
icon="add"
@@ -75,4 +76,5 @@
role="button"
tabindex="0"
hero-id="main"></paper-fab>
{{/if}}
</template>

View File

@@ -1,5 +1,8 @@
<template name="raceDialog">
{{#baseDialog title="Race" class=colorClass hideColor="true" hideDelete="true" startEditing=startEditing}}
<div horizontal layout center-justified class= "display2">
{{race}}
</div>
{{> effectsViewList charId=charId parentId=charId parentGroup="racial"}}
{{> proficiencyViewList charId=charId parentId=charId parentGroup="racial"}}
{{else}}

View File

@@ -91,8 +91,10 @@
<div class="fab-buffer"></div>
</div>
</div>
<paper-fab-menu id="inventoryAddMenu" icon="add" closeIcon="close" duration="0.3">
<paper-fab-menu-item id="addSpell" icon="note-add" color="#d23f31" tooltip="Spell"></paper-fab-menu-item>
<paper-fab-menu-item id="addSpellList" icon="work" color="#d23f31" tooltip="Spell List"></paper-fab-menu-item>
</paper-fab-menu>
{{#if canEditCharacter _id}}
<paper-fab-menu id="inventoryAddMenu" icon="add" closeIcon="close" duration="0.3">
<paper-fab-menu-item id="addSpell" icon="note-add" color="#d23f31" tooltip="Spell"></paper-fab-menu-item>
<paper-fab-menu-item id="addSpellList" icon="work" color="#d23f31" tooltip="Spell List"></paper-fab-menu-item>
</paper-fab-menu>
{{/if}}
</template>

View File

@@ -140,7 +140,12 @@ Template.attributeDialogView.helpers({
return char.attributeValue(this.statName);
},
sourceName: function(){
if (this.parent.collection === "Characters") return this.name;
if (this.parent.group === "racial"){
return this.getParent().race;
}
if (this.parent.collection === "Characters"){
return this.name;
}
return this.getParent().name;
},
operationName: function(){

View File

@@ -2,10 +2,14 @@
color: black;
color: rgba(0, 0, 0, 0.870588);
cursor: pointer;
font-size: 16px;
height: 40px;
overflow: hidden;
padding: 12px 0 12px 16px;
padding: 8px 0 8px 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.singleLineItem core-icon {
height: 8px;
margin-right: 8px;
width: 8px;
}

View File

@@ -3,7 +3,11 @@
{{#if characters.count}}
<div>
{{#each characters}}
<div class="singleLineItem">{{name}}</div>
<div class="singleLineItem characterRepresentative"
layout horizontal center>
<core-icon icon="image:brightness-1"></core-icon>
<div>{{name}}</div>
</div>
{{/each}}
</div>
{{/if}}

View File

@@ -0,0 +1,30 @@
<template name="feedback">
<div class="feedback" style="min-width: 300px; min-height: 370px">
<div>
<paper-input id="feedbackTitle" label="Title" floatinglabel></paper-input>
</div>
<div>
<paper-dropdown-menu class="typeDropdown" label="Operation" flex>
<paper-dropdown layered class="dropdown">
<core-menu class="menu typeMenu" selected="general">
<paper-item name="general">General Feedback</paper-item>
<paper-item name="bug">Bug</paper-item>
<paper-item name="change">Suggested Change</paper-item>
<paper-item name="feature">Feature Request</paper-item>
</core-menu>
</paper-dropdown>
</paper-dropdown-menu>
</div>
<div layout horizontal center>
<div>Importance</div>
<paper-slider id="severity" max=10 min=1 value=5 snap></paper-slider>
</div>
<paper-input-decorator label="Description" floatinglabel layout vertical>
<paper-autogrow-textarea rows=10 maxRows=10>
<textarea id="feedbackDescription"></textarea>
</paper-autogrow-textarea>
</paper-input-decorator>
</div>
<paper-button id="cancelButton" affirmative>Cancel</paper-button>
<paper-button id="sendButton" affirmative>Send </paper-button>
</template>

View File

@@ -0,0 +1,14 @@
Template.feedback.events({
"tap #sendButton": function(event, instance) {
var report = {};
report.title = instance.find("#feedbackTitle").value;
report.severity = instance.find("#severity").value;
report.type = instance.find(".typeMenu").selected;
report.description = instance.find("#feedbackDescription").value;
report.metaData = {
url: window.location.href,
session: _.pairs(Session.keys),
};
Meteor.call("insertReport", report);
}
});

View File

@@ -11,7 +11,7 @@
<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 left corner.</li>
<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>
@@ -41,7 +41,7 @@
<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 left to add a new feature</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>

View File

@@ -1,18 +1,20 @@
<template name="layout">
<core-drawer-panel>
<core-header-panel drawer navigation flex mode="seamed" class="white">
<div id="accountSummary">
{{> loginButtons}}
{{#if currentUser}}
<div id="profileLink" style="text-decoration: underline; cursor: pointer;">
My account
{{profileLink}}
</div>
{{else}}
<a href="/sign-in" style="color: white;">Sign in</a>
{{/if}}
</div>
<div id="navPanel">
<core-item id="homeNav" icon="home" label="Home"></core-item>
{{> characterSideList}}
<core-item id="feedback" icon="bug-report" label="Send Feedback"></core-item>
<core-item id="changeLog" icon="list" label="Change Log"></core-item>
</div>
</core-header-panel>
<core-animated-pages main

View File

@@ -1,3 +1,7 @@
Template.layout.onCreated(function() {
this.subscribe("user");
});
Template.layout.rendered = function() {
$(window).on("popstate", GlobalUI.popStateHandler);
};
@@ -9,7 +13,11 @@ Template.layout.destroyed = function() {
Template.layout.helpers({
notSelected: function(){
return Session.get("global.ui.detailShow") ? "not-selected" : null;
}
},
profileLink: function() {
var user = Meteor.user();
return user.profile && user.profile.username || user.username || "My Account";
},
});
Template.layout.events({
@@ -19,4 +27,14 @@ Template.layout.events({
"tap #profileLink": function(event, instance){
Router.go("profile");
},
"tap #feedback": function(event, instance) {
GlobalUI.showDialog({
heading: "Feedback",
template: "feedback",
fullOnMobile: true,
});
},
"tap #changeLog": function(event, instance) {
Router.go("changeLog");
},
});

View File

@@ -0,0 +1,20 @@
<template name="changeLog">
<core-toolbar class="blue-grey white-text">
<core-icon-button icon="menu" core-drawer-toggle></core-icon-button>
<div flex>
Change Log
</div>
</core-toolbar>
<div class="changeLog scroll-y" fit>
{{#each changeLogs}}
<paper-shadow class="white padded" style="margin: 8px;">
<h2>{{version}}</h2>
<ul>
{{#each changes}}
<li>{{this}}</li>
{{/each}}
</ul>
</paper-shadow>
{{/each}}
</div>
</template>

View File

@@ -0,0 +1,11 @@
<template name="notFound">
<div layout vertical center center-justified fit>
<h2>The data for the page you requested could not be found.</h2>
{{#if currentUser}}
<h2>It might not exist, or you might not have permission to view it.</h2>
{{else}}
<h2>Perhaps you need to sign in first:</h2>
{{atForm}}
{{/if}}
</div>
</template>

View File

@@ -20,12 +20,12 @@
aria-label="Delete Feature"
noink></paper-icon-button>
{{else}}
{{#unless hideEdit}}
{{#if showEdit}}
<paper-icon-button id="editButton"
icon="create"
aria-label="Delete Feature"
noink></paper-icon-button>
{{/unless}}
{{/if}}
{{/if}}
</core-toolbar>
<div class="detailContent">

View File

@@ -11,6 +11,18 @@ Template.baseDialog.helpers({
editing: function(){
return Template.instance().editing.get();
},
showEdit: function() {
if (this.hideEdit) return false;
var charId = Template.parentData().charId;
if (charId){
var char = Characters.findOne(charId);
var userId = Meteor.userId();
if (char && userId)
return char.owner === userId ||
_.contains(char.writers, userId);
}
return true;
},
});
Template.baseDialog.events({

View File

@@ -3,11 +3,7 @@
<core-toolbar class="blue-grey white-text">
<core-icon-button icon="menu" core-drawer-toggle></core-icon-button>
<div id="username" class="clickable" flex>
{{#if username}}
{{username}}
{{else}}
Tap to set username
{{/if}}
{{profileName}}
</div>
</core-toolbar>
<div id="userProfile" class="padded">
@@ -20,5 +16,7 @@
{{/each}}
</div>
</div>
{{> atForm}}
{{> atNavButton }}
{{/with}}
</template>

View File

@@ -1,3 +1,12 @@
Template.profile.helpers({
profileName: function() {
var user = Meteor.user();
return user.profile && user.profile.username ||
user.username ||
"Tap to set username";
}
});
Template.profile.events({
"tap #username": function(){
if (this._id === Meteor.userId()){

View File

@@ -1,9 +1,8 @@
<template name="usernameDialog">
{{#with currentUser}}
<div>
<paper-input id="usernameInput" label="Username" value={{username}}></paper-input>
</div>
{{/with}}
<div>
<paper-input id="usernameInput" label="Username" value={{profileName}}></paper-input>
</div>
<div style="color: red;" class="vertMargin">{{errorMessage}}</div>
<paper-button id="cancelButton" affirmative> Cancel </paper-button>
<paper-button id="changeButton" affirmative> Change Username </paper-button>
<paper-button id="changeButton" disabled={{invalid}} affirmative> Change Username </paper-button>
</template>

View File

@@ -1,8 +1,49 @@
var getUsername = function() {
var user = Meteor.user();
return user.profile && user.profile.username || user.username;
};
Template.usernameDialog.onCreated(function() {
this.errorMessage = new ReactiveVar("");
this.username = new ReactiveVar(getUsername());
});
Template.usernameDialog.helpers({
profileName: function() {
return getUsername();
},
invalid: function() {
return !!Template.instance().errorMessage.get();
},
errorMessage: function() {
return Template.instance().errorMessage.get();
},
});
Template.usernameDialog.events({
"change #usernameInput, input #usernameInput": function(event, instance) {
var username = instance.find("#usernameInput").value;
username = username.trim().toLowerCase().replace(/\s+/gm, "");
if (username.length < 3){
instance.errorMessage.set("Username too short");
} else {
instance.errorMessage.set("Validating...");
Meteor.call("getUserId", username, function(err, userId){
if (userId && userId !== Meteor.userId())
instance.errorMessage.set("This username is taken");
else
instance.errorMessage.set("");
});
}
},
"tap #changeButton": function(event, instance){
var username = instance.find("#usernameInput").value;
username = username.trim().replace(/\s+/gm, " ");
var profileName = username;
username = username.toLowerCase().replace(/\s+/gm, "");
Meteor.users.update(
Meteor.userId(),
{$set: {username: instance.find("#usernameInput").value}}
{$set: {username: username, "profile.username": profileName}}
);
}
},
});

View File

@@ -0,0 +1,13 @@
<template name="titledAtForm">
<core-toolbar class="blue-grey white-text">
<core-icon-button icon="menu" core-drawer-toggle></core-icon-button>
<div flex>
</div>
</core-toolbar>
<div class="scroll-y padded" fit layout vertical center center-justified>
<paper-shadow class="white" style="max-width: 400px;">
{{> atForm}}
</paper-shadow>
</div>
</template>

View File

@@ -1,3 +1,14 @@
Meteor.methods({
canWriteCharacter: function(charId) {
var userId = this.userId;
var char = Characters.findOne(
charId,
{fields: {owner: 1, writers: 1}}
);
return (userId && char.owner === userId || _.contains(char.writers, userId));
},
});
CHARACTER_SUBSCHEMA_ALLOW = {
// the user must be logged in, and the user must be a writer of the character
insert: function(userId, doc) {

View File

@@ -0,0 +1,71 @@
AccountsTemplates.configure({
//behaviour
confirmPassword: true,
enablePasswordChange: true,
enforceEmailVerification: true,
overrideLoginErrors: false,
sendVerificationEmail: true,
lowercaseUsername: true,
//appearance
continuousValidation: true,
negativeValidation: true,
negativeFeedback: true,
showValidating: true,
showAddRemoveServices: true,
showForgotPasswordLink: true,
showResendVerificationEmailLink: true,
});
AccountsTemplates.configureRoute("changePwd", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("enrollAccount", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("forgotPwd", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("resetPwd", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("signIn", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("signUp", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("verifyEmail", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("resendVerificationEmail", {
template: "titledAtForm",
});
if (Meteor.isServer){
Meteor.methods({
"userExists": function(username){
return !!Meteor.users.findOne({username: username});
},
});
}
AccountsTemplates.addField({
_id: "username",
type: "text",
required: true,
func: function(value){
if (Meteor.isClient) {
var self = this;
Meteor.call("userExists", value, function(err, userExists){
if (!userExists)
self.setSuccess();
else
self.setError("This username is taken");
self.setValidating(false);
});
return;
}
// Server
return Meteor.call("userExists", value);
},
});

View File

@@ -9,23 +9,27 @@ evaluate = function(charId, string){
}
//ability modifiers
var abilityMods = [
"STRENGTHMOD",
"DEXTERITYMOD",
"CONSTITUTIONMOD",
"INTELLIGENCEMOD",
"WISDOMMOD",
"CHARISMAMOD",
"strengthMod",
"dexterityMod",
"constitutionMod",
"intelligenceMod",
"wisdomMod",
"charismaMod",
];
if (_.contains(abilityMods, sub.toUpperCase())){
if (_.contains(abilityMods, sub)){
var slice = sub.slice(0, -3);
return char.abilityMod(slice);
try {
return char.abilityMod(slice);
} catch (e){
return sub;
}
}
//class levels
if (/\w+levels?\b/gi.test(sub)){
//strip out "level"
var className = sub.replace(/levels?\b/gi, "");
var cls = Classes.findOne({charId: charId, name: className});
return cls && cls.level;
return cls && cls.level || sub;
}
//character level
if (sub.toUpperCase() === "LEVEL"){

View File

@@ -42,6 +42,12 @@ var inheritParentProperties = function(doc, collection){
"Document's parent does not exist"
);
var handMeDowns = _.pick(parent, collection.inheritedKeys);
if (
_.contains(collection.inheritedKeys, "charId") &&
doc.parent.collection === "Characters"
){
handMeDowns.charId = doc.parent.id;
}
if (_.isEmpty(handMeDowns)) return;
collection.update(doc._id, {$set: handMeDowns});
};
@@ -76,14 +82,13 @@ makeChild = function(collection, inheritedKeys){
}
});
if (Meteor.isClient) {
collection.after.update(function(userId, doc, fieldNames, modifier, options) {
if (modifier && modifier.$set && modifier.$set.parent){
//when we change parents, inherit its properties
inheritParentProperties(doc, collection);
}
});
}
collection.after.update(function(userId, doc, fieldNames, modifier, options) {
if (modifier && modifier.$set && modifier.$set["parent.id"]){
//when we change parents, inherit its properties
console.log("re-inheriting")
inheritParentProperties(doc, collection);
}
});
collection.softRemoveNode = collection.softRemoveNode || function(id){
collection.softRemove(id);
@@ -102,14 +107,12 @@ makeParent = function(collection, donatedKeys){
donatedKeys = joinWithDefaultKeys(donatedKeys);
var collectionName = collection._collection.name;
//after changing, push the changes to all children
if (Meteor.isClient) {
collection.after.update(function(userId, doc, fieldNames, modifier, options) {
modifier = limitModifierToKeys(modifier, donatedKeys);
doc = _.pick(doc, ["_id", "charId"]);
if (!modifier) return;
Meteor.call("updateChildren", doc, modifier, true);
});
}
collection.after.update(function(userId, doc, fieldNames, modifier, options) {
modifier = limitModifierToKeys(modifier, donatedKeys);
doc = _.pick(doc, ["_id", "charId"]);
if (!modifier) return;
Meteor.call("updateChildren", doc, modifier, true);
});
collection.softRemoveNode = function(id){
Meteor.call("softRemoveNode", collectionName, id);
};

View File

@@ -0,0 +1,27 @@
AccountsMeld.configure({
meldDBCallback: function(sourceUserId, destinationUserId){
// Here you can modify every collection you need for the document referencing
// to sourceUserId to be modified in order to point to destinationUserId
Characters.update(
{owner: sourceUserId},
{$set: {owner: destinationUserId}},
{multi: true}
);
Characters.update(
{writers: sourceUserId},
{
$pull: {writers: sourceUserId},
$addToSet: {writers: destinationUserId},
},
{multi: true}
);
Characters.update(
{readers: sourceUserId},
{
$pull: {readers: sourceUserId},
$addToSet: {readers: destinationUserId},
},
{multi: true}
);
},
});

View File

@@ -0,0 +1,3 @@
Meteor.publish("changeLog", function(){
return ChangeLogs.find();
});

View File

@@ -1,5 +1,6 @@
Meteor.publish("singleCharacter", function(characterId){
userId = this.userId;
if (!userId) return [];
var char = Characters.findOne({
_id: characterId,
$or: [
@@ -30,5 +31,7 @@ Meteor.publish("singleCharacter", function(characterId){
{fields: {username: 1}}
),
];
} else {
return [];
}
});

View File

@@ -0,0 +1,3 @@
Meteor.publish("user", function(){
return Meteor.users.find(this.userId, {fields: {roles: 1}});
});