Implemented Feature editing UI

This commit is contained in:
Thaum
2015-01-21 11:16:00 +00:00
parent 078f873219
commit 84512beb72
32 changed files with 1072 additions and 161 deletions

2
.codio
View File

@@ -3,7 +3,7 @@
// Run button configuration
"commands": {
"Run Meteor": "cd rpg-docs \n METEOR_OFFLINE_CATALOG=1 meteor run"
"Run Meteor": "cd rpg-docs \n meteor run"
},
// Preview button configuration

View File

@@ -14,5 +14,6 @@ reactive-var
cw4gn3r:jquery-event-drag
underscore
aldeed:collection2
aldeed:autoform
differential:vulcanize
aldeed:autoform
conielo:autoform-polymer-paper

View File

@@ -3,7 +3,7 @@ accounts-password@1.0.5
accounts-ui@1.1.4
accounts-ui-unstyled@1.1.5
aldeed:autoform@4.2.2
aldeed:collection2@2.3.0
aldeed:collection2@2.3.1
aldeed:simple-schema@1.1.0
application-configuration@1.0.4
autoupdate@1.1.4
@@ -14,6 +14,7 @@ blaze-tools@1.0.2
boilerplate-generator@1.0.2
callback-hook@1.0.2
check@1.0.3
conielo:autoform-polymer-paper@0.1.1
cw4gn3r:jquery-event-drag@2.2.0
dburles:collection-helpers@1.0.2
ddp@1.0.13

View File

@@ -213,8 +213,6 @@ Schemas.Character = new SimpleSchema({
},
//mechanics
features: { type: [String], defaultValue: [], regEx: SimpleSchema.RegEx.Id,},
customFeatures: { type: [Schemas.Feature], defaultValue: []},
actions: { type: [Schemas.Action], defaultValue: []},
deathSave: { type: Schemas.DeathSave },
time: { type: Number, min: 0, decimal: true, defaultValue: 0},

View File

@@ -1,28 +1,9 @@
//Features are features that can be selected but not edited
//they are the things that come in the player's handbook and
//facilitate the quick building of characters
//They are the primary means of collecting cease and desist letters :(
//
//Should only be edited by admins
//
//TODO add a Meteor Method that lets users add a feature to their character
//and pushes the effects and actions accordingly
//
//TODO add a Method that updates every character with a given feature if that feature should change
Features = new Meteor.Collection("features");
Schemas.Feature = new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue: function(){
if(!this.isSet) return Random.id();
}
},
charId: {type: String, regEx: SimpleSchema.RegEx.Id, optional: true},
name: {type: String},
description:{type: String, optional: true},
source: {type: String, optional: true},
effects: {type: [Schemas.Effect], defaultValue: []},
actions: {type: [Schemas.Action], defaultValue: []},
attacks: {type: [Schemas.Attack], defaultValue: []},
@@ -31,12 +12,24 @@ Schemas.Feature = new SimpleSchema({
Features.attachSchema(Schemas.Feature);
//observe standard features for changes and update characters using them
Features.find().observe({
//update the features of the items as needed
Features.find({}, {fields: {name: 0, description: 0}}).observe({
added: function(newFeature){
if(newFeature.charId){
//make sure existing versions of this feature's effects aren't duplicated
removeFeatureEffects(newFeature.charId, newFeature);
//add the new feature's effects
addFeatureEffects(newFeature.charId, newFeature);
}
},
changed: function(newFeature, oldFeature){
//TODO
if(oldFeature.charId)
removeFeatureEffects(oldFeature.charId, oldFeature);
if(newFeature.charId)
addFeatureEffects(newFeature.charId, newFeature);
},
removed: function(oldFeature){
//TODO
if(oldFeature.charId)
removeFeatureEffects(oldFeature.charId, oldFeature);
}
});
});

View File

@@ -7,11 +7,11 @@ Schemas.Attribute = new SimpleSchema({
},
//effect arrays
effects: { type: [Schemas.Effect], defaultValue: [] },
reset: {
type: String,
defaultValue: "longRest",
allowedValues: ["longRest", "shortRest"]
}
reset: {
type: String,
defaultValue: "longRest",
allowedValues: ["longRest", "shortRest"]
}
});
//note that to make an invulnerability add a new max of zero value
@@ -22,12 +22,16 @@ Schemas.Vulnerability = new SimpleSchema({
defaultValue: 0
},
//effect arrays
mul: { type: [Schemas.Effect], defaultValue: [] },
min: { type: [Schemas.Effect], defaultValue: [{name: "Resistance doesn't stack", value: 0.5}] },
max: { type: [Schemas.Effect], defaultValue: [{name: "Vulnerability doesn't stack", value: 2}] },
reset: {
type: String,
defaultValue: "longRest",
allowedValues: ["longRest", "shortRest"]
}
effects: {
type: [Schemas.Effect],
defaultValue: [
{type: "inate", name: "Resistance doesn't stack", operation: "min", value: 0.5},
{type: "inate", name: "Vulnerability doesn't stack", operation: "max", value: 2}
]
},
reset: {
type: String,
defaultValue: "longRest",
allowedValues: ["longRest", "shortRest"]
}
});

View File

@@ -16,7 +16,7 @@ Schemas.Effect = new SimpleSchema({
operation: {
type: String,
defaultValue: "add",
allowedValues: ["base", "proficiency","add","mul","min","max","advantage","disadvantage","passiveAdd","fail","conditional","passiveAdd"]
allowedValues: ["base", "proficiency","add","mul","min","max","advantage","disadvantage","passiveAdd","fail","conditional"]
},
value: {
type: Number,
@@ -31,7 +31,7 @@ Schemas.Effect = new SimpleSchema({
type: {
type: String,
defaultValue: "editable",
allowedValues: ["editable", "feat", "buff", "equipment", "inate"]
allowedValues: ["editable", "feature", "buff", "equipment", "inate"]
},
//which stat the effect is applied to
stat: {

View File

@@ -1,9 +1,10 @@
//set up the collection for containers
Containers = new Meteor.Collection("containers");
Schemas.Container = new SimpleSchema({
name: { type: String },
owner: { type: String, regEx: SimpleSchema.RegEx.Id},
charId: { type: String, regEx: SimpleSchema.RegEx.Id},
isCarried: { type: Boolean }
});

View File

@@ -4,14 +4,20 @@ Schemas.Item = new SimpleSchema({
name: {type: String, defaultValue: "New Item"},
plural: {type: String, optional: true},
description:{type: String, defaultValue: ""},
container: {type: String}, //id of container it normally is stowed in
character: {type: String, regEx: SimpleSchema.RegEx.Id}, //id of owner
container: {type: String, regEx: SimpleSchema.RegEx.Id, optional: true}, //id of container it normally is stowed in
charId: {type: String, regEx: SimpleSchema.RegEx.Id, optional: true}, //id of owner
quantity: {type: Number, min: 0, defaultValue: 1},
weight: {type: Number, min: 0, defaultValue: 0, decimal: true},
value: {type: Number, min: 0, defaultValue: 0, decimal: true},
tradeGood: {type: Boolean, defaultValue: false},
stackable: {type: Boolean, defaultValue: false},
feature: {type: Schemas.Feature},
"feature.name": {type: String, autoValue: function(){return this.field("name").value}},
"feature.description": {type: String, autoValue: function(){return this.field("description").value}},
"feature.source": {type: String, autoValue: function(){return this.field("name").value}},
"feature.effects.$.name": {type: String, autoValue: function(){return this.field("name").value}},
"feature.effects.$.type": {type: String, autoValue: function(){return "equipment"}},
"feature.attacks.$.name": {type: String, autoValue: function(){return this.field("name").value}},
equipmentSlot: {
type: String,
defaultValue: "none",
@@ -23,20 +29,20 @@ Schemas.Item = new SimpleSchema({
Items.attachSchema(Schemas.Item);
//update the features of the items as needed
Items.find({}, {fields: {feature: 1, character: 1, equipped: 1}}).observe({
Items.find({}, {fields: {feature: 1, charId: 1, equipped: 1}}).observe({
added: function(newItem){
if(newItem.feature && newItem.character)
addFeatureEffects(newItem.character, newItem.feature);
if(newItem.feature && newItem.charId)
addFeatureEffects(newItem.charId, newItem.feature);
},
changed: function(newItem, oldItem){
if(oldItem.feature && oldItem.character)
removeFeatureEffects(oldItem.character, oldItem.feature);
if(newItem.feature && newItem.character)
addFeatureEffects(newItem.character, newItem.feature);
if(oldItem.feature && oldItem.charId)
removeFeatureEffects(oldItem.charId, oldItem.feature);
if(newItem.feature && newItem.charId)
addFeatureEffects(newItem.charId, newItem.feature);
},
removed: function(oldItem){
if(oldItem.feature && oldItem.character)
removeFeatureEffects(oldItem.character, oldItem.feature);
if(oldItem.feature && oldItem.charId)
removeFeatureEffects(oldItem.charId, oldItem.feature);
}
});

View File

@@ -20,7 +20,12 @@ Router.map( function () {
this.route('characterSheet', {
path: '/character/:_id',
waitOn: function(){
return Meteor.subscribe("singleCharacter", this.params._id, Meteor.userId());
return [
Meteor.subscribe("singleCharacter", this.params._id, Meteor.userId()),
Meteor.subscribe("characterContainers", this.params._id, Meteor.userId()),
Meteor.subscribe("characterItems", this.params._id, Meteor.userId()),
Meteor.subscribe("characterFeatures", this.params._id, Meteor.userId()),
];
},
data: function() {
var data = Characters.findOne({_id: this.params._id}, {fields: {_id: 1}});
@@ -50,4 +55,4 @@ Router.map( function () {
this.route('loading', {
path: '/loading'
});
});
});

View File

@@ -0,0 +1,55 @@
this.GlobalUI = (function() {
function GlobalUI() {}
GlobalUI.dialog = {};
GlobalUI.toast = function(text, className) {
var toast;
toast = $("[global-toast]")[0];
toast.text = text;
return toast.show();
};
GlobalUI.showDialog = function(opts) {
this.dialog = $("[global-dialog]")[0];
Session.set("global.ui.dialogHeader", opts.heading);
Session.set("global.ui.dialogData", opts.data);
Session.set("global.ui.dialogTemplate", opts.template);
Session.set("global.ui.dialogFullOnMobile", opts.fullOnMobile != null);
return Tracker.afterFlush((function(_this) {
return function() {
return _this.dialog.open();
};
})(this));
};
GlobalUI.closeDialog = function() {
return this.dialog.close();
};
return GlobalUI;
})();
Template.layout.helpers({
globalDialogTemplate: function() {
return Session.get("global.ui.dialogTemplate");
},
globalDialogData: function() {
return Session.get("global.ui.dialogData");
},
globalDialogFullOnMobile: function() {
return Session.get("global.ui.dialogFullOnMobile");
},
globalDialogHeader: function(){
return Session.get("global.ui.dialogHeader");
}
});
Template.layout.events({
"core-overlay-close-completed [global-dialog]": function(e) {
Session.set("global.ui.dialogTemplate", null);
Session.set("global.ui.dialogData", null);
return Session.set("global.ui.dialogFullOnMobile", null);
},
});

View File

@@ -0,0 +1 @@
AutoForm.setDefaultTemplate('paper');

View File

@@ -1,5 +1,5 @@
<template name="stats">
<core-animated-pages selected={{selectedSection}} transitions="hero-transition cross-fade">
<core-animated-pages selected={{selectedSection}} transitions="hero-transition cross-fade" fit style="overflow-y: scroll">
<section id="stats">
{{> abilityCards}}
</section>

View File

@@ -17,14 +17,14 @@
</paper-tabs>
</div>
</core-toolbar>
<div>
<core-animated-pages id="tabPages" selected={{selectedTab}} transitions="slide-from-right">
<swipe-detect touch-action="pan-y">{{> stats}}</swipe-detect>
<swipe-detect touch-action="pan-y">{{> features}}</swipe-detect>
<swipe-detect touch-action="pan-y">inventory</swipe-detect>
<swipe-detect touch-action="pan-y">spellBook</swipe-detect>
<swipe-detect touch-action="pan-y">persona</swipe-detect>
<swipe-detect touch-action="pan-y">journal</swipe-detect>
<div fit>
<core-animated-pages id="tabPages" selected={{selectedTab}} transitions="slide-from-right" fit>
<swipe-detect touch-action="pan-y" flex>{{> stats}}</swipe-detect>
<swipe-detect touch-action="pan-y" flex>{{> features}}</swipe-detect>
<swipe-detect touch-action="pan-y" flex>{{> inventory}}</swipe-detect>
<swipe-detect touch-action="pan-y" flex>spellBook</swipe-detect>
<swipe-detect touch-action="pan-y" flex>persona</swipe-detect>
<swipe-detect touch-action="pan-y" flex>journal</swipe-detect>
</core-animated-pages>
</div>
</template>

View File

@@ -0,0 +1,24 @@
<template name="autoFeatureDialog">
{{> quickForm schema="Schemas.Feature" id="insertFeatureForm" type="insert"}}
<paper-button affirmative>Cancel</paper-button>
<paper-button affirmative>Save Item</paper-button>
</template>
<template name="featureDialog">
{{#with feature}}
<div class="featureDialogWidth"></div>
<paper-input id="featureNameInput" label="Name" floatinglabel value={{name}}></paper-input>
<paper-input-decorator label="Description" floatinglabel layout vertical>
<paper-autogrow-textarea>
<textarea id="featureDescriptionInput" placeholder aria-label="Description" value={{description}}></textarea>
</paper-autogrow-textarea>
</paper-input-decorator>
<h3>Effects</h3>
{{#each effects}}
{{>featureEffect}}
{{/each}}
<br>
<paper-icon-button id="addEffectButton" role="button" tabindex="0" icon="add" aria-label="addEffect"></paper-icon-button>
{{/with}}
<paper-button affirmative>Done</paper-button>
</template>

View File

@@ -0,0 +1,36 @@
Template.featureDialog.rendered = function(){
var self = this;
this.autorun(function(){
var feature = Features.findOne(Template.currentData().featureId, {fields: {name: 1}});
if(feature && feature.name) Session.set("global.ui.dialogHeader", feature.name);
})
}
Template.featureDialog.events({
"tap #addEffectButton": function(){
var numUpdated = Features.update(this._id, {
$push: {
"effects": {
name: "fe",
operation: "add",
type: "feature"
}
}
});
console.log("pushed add button ", numUpdated, " updated");
},
"change #featureNameInput": function(event){
var name = Template.instance().find("#featureNameInput").value;
Features.update(this._id, {$set: {name: name}});
},
"change #featureDescriptionInput": function(event){
var description = Template.instance().find("#featureDescriptionInput").value;
Features.update(this._id, {$set: {description: description}});
}
});
Template.featureDialog.helpers({
feature: function(){
return Features.findOne(this.featureId);
}
});

View File

@@ -0,0 +1,69 @@
<template name="featureEffect">
<paper-dropdown-menu id="statGroupDropDown" label="Stat Group">
<paper-dropdown class="dropdown">
<core-menu id="statGroupMenu" class="menu" selected={{selectedStatGroup}}>
{{#each statGroups}}
<paper-item>{{this}}</paper-item>
{{/each}}
</core-menu>
</paper-dropdown>
</paper-dropdown-menu>
{{#if stats}}
<paper-dropdown-menu id="statDropDown" label="Stat">
<paper-dropdown class="dropdown">
<core-menu id="statMenu" class="menu" selected={{selectedStat}}>
{{#each stats}}
<paper-item>{{name}}</paper-item>
{{/each}}
</core-menu>
</paper-dropdown>
</paper-dropdown-menu>
{{/if}}
{{#if operations}}
<paper-dropdown-menu id="operationDropDown" label="Operation">
<paper-dropdown class="dropdown">
<core-menu id="operationMenu" class="menu" selected={{selectedOperation}}>
{{#each operations}}
<paper-item>{{name}}</paper-item>
{{/each}}
</core-menu>
</paper-dropdown>
</paper-dropdown-menu>
{{/if}}
{{> Template.dynamic template=effectValueTemplate data=valueTemplateData}}
{{#if needsCommit}}
<paper-icon-button id="commitChanges" role="button" tabindex="0" icon="check" aria-label="Commit Changes"></paper-icon-button>
<paper-icon-button id="clearChanges" role="button" tabindex="0" icon="clear" aria-label="Clear Changes"></paper-icon-button>
{{else}}
<paper-icon-button id="deleteEffect" role="button" tabindex="0" icon="delete" aria-label="Delete"></paper-icon-button>
{{/if}}
<br>
</template>
<template name="regularEffectValue">
<paper-input id="effectValueInput" label="Value" floatinglabel value={{effectValue}}></paper-input>
</template>
<template name="multiplierEffectValue">
<paper-dropdown-menu id="damageMultiplierDropDown" label="Damage Multiplier">
<paper-dropdown class="dropdown">
<core-menu id="multiplierMenu" class="menu" selected={{selectedDamageMultiplier}}>
<paper-item>Resistance</paper-item>
<paper-item>Vulnerability</paper-item>
<paper-item>Immunity</paper-item>
</core-menu>
</paper-dropdown>
</paper-dropdown-menu>
</template>
<template name="proficiencyEffectValue">
<paper-dropdown-menu id="proficiencyDropDown" label="Proficiency">
<paper-dropdown class="dropdown">
<core-menu id="proficiencyMenu" class="menu" selected={{selectedProfiencyMultiplier}}>
<paper-item>Proficient</paper-item>
<paper-item>Half Prof. Bonus</paper-item>
<paper-item>Double Prof. Bonus</paper-item>
</core-menu>
</paper-dropdown>
</paper-dropdown-menu>
</template>

View File

@@ -0,0 +1,329 @@
var stats = [
{stat: "strength", name: "Strength", group: "Ability Scores"},
{stat: "dexterity", name: "Dexterity", group: "Ability Scores"},
{stat: "constitution", name: "Constitution", group: "Ability Scores"},
{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"},
{stat: "hitPoints", name: "Hit Points", group: "Stats"},
{stat: "armor", name: "Armor", group: "Stats"},
{stat: "speed", name: "Speed", group: "Stats"},
{stat: "ki", name: "Ki Points", group: "Stats"},
{stat: "sorceryPoints", name: "Sorcery Points", group: "Stats"},
{stat: "rages", name: "Rages", group: "Stats"},
{stat: "rageDamage", name: "Rage Damage", group: "Stats"},
{stat: "expertiseDice", name: "Expertise Dice", group: "Stats"},
{stat: "superiorityDice", name: "Superiority Dice", 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"},
{stat: "level4SpellSlots", name: "level 4", group: "Spell Slots"},
{stat: "level5SpellSlots", name: "level 5", group: "Spell Slots"},
{stat: "level6SpellSlots", name: "level 6", group: "Spell Slots"},
{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: "acidMultiplier", name: "Acid", group: "Weakness/Resistance"},
{stat: "bludgeoningMultiplier", name: "Bludgeoning", group: "Weakness/Resistance"},
{stat: "coldMultiplier", name: "Cold", group: "Weakness/Resistance"},
{stat: "fireMultiplier", name: "Fire", group: "Weakness/Resistance"},
{stat: "forceMultiplier", name: "Force", group: "Weakness/Resistance"},
{stat: "lightningMultiplier", name: "Lightning", group: "Weakness/Resistance"},
{stat: "necroticMultiplier", name: "Necrotic", group: "Weakness/Resistance"},
{stat: "piercingMultiplier", name: "Piercing", group: "Weakness/Resistance"},
{stat: "poisonMultiplier", name: "Poison", group: "Weakness/Resistance"},
{stat: "psychicMultiplier", name: "Psychic", group: "Weakness/Resistance"},
{stat: "radiantMultiplier", name: "Radiant", group: "Weakness/Resistance"},
{stat: "slashingMultiplier", name: "Slashing", group: "Weakness/Resistance"},
{stat: "thunderMultiplier", name: "Thunder", group: "Weakness/Resistance"}
];
var statsDict = _.indexBy(stats, "stat")
var statGroups = _.groupBy(stats, "group");
var statGroupNames = _.keys(statGroups);
var statGroupIndex = function(statName){
if(!_.isString(statName)) return;
var stat = statsDict[statName];
if(stat){
return _.indexOf(statGroupNames, stat.group)
}
}
var statIndex = function(statName){
if(!_.isString(statName)) return;
var stat = statsDict[statName];
if(!stat) return;
var group = statGroups[stat.group];
if(!group) return;
return _.indexOf(_.pluck(group, "stat"), statName);
}
var attributeOperations = [
{name: "Base Value", operation: "base"},
{name: "Add", operation: "add"},
{name: "Multiply", operation: "mul"},
{name: "Min", operation: "min"},
{name: "Max", operation: "max"}
];
var skillOperations = [
{name: "Proficiency", operation: "proficiency"},
{name: "Add", operation: "add"},
{name: "Multiply", operation: "mul"},
{name: "Min", operation: "min"},
{name: "Max", operation: "max"},
{name: "Advantage", operation: "advantage"},
{name: "Disadvantage", operation: "disadvantage"},
{name: "Passive Bonus", operation: "passiveAdd"},
{name: "Automatically Fail", operation: "fail"},
{name: "Conditional Benefit", operation: "conditional"}
];
var operationIndex = function(statName, operation){
if(!_.isString(statName)) return;
if(!_.isString(operation)) return;
var group = statsDict[statName].group;
var opGroup;
if(group === "Saving Throws" || group === "Skills"){
opGroup = skillOperations;
} else {
opGroup = attributeOperations;
}
return _.indexOf(_.pluck(opGroup, "operation"), operation);
}
Template.featureEffect.created = function(){
this.selectedStatGroup = new ReactiveVar();
this.selectedStat = new ReactiveVar();
this.selectedOperation = new ReactiveVar();
this.value = new ReactiveVar();
};
Template.featureEffect.rendered = function(){
var self = this;
self.autorun(function(){
var data = Template.currentData();
if(!data) return;
if(data.stat){
if(statsDict[data.stat]){
self.selectedStatGroup.set(statsDict[data.stat].group);
}
self.selectedStat.set(data.stat);
}
if(data.operation){
self.selectedOperation.set(data.operation);
}
var value = undefined;
if(_.isNumber(data.value)){
value = data.value;
} else if (_.isString(data.calculation)){
value = data.calculation;
}
if(value){
self.value.set(value);
}
})
};
Template.featureEffect.helpers({
selectedStatGroup: function(){
var groupName = Template.instance().selectedStatGroup.get();
return _.indexOf(statGroupNames, groupName);
},
selectedStat: function(){
var statName = Template.instance().selectedStat.get();
return statIndex(statName);
},
selectedOperation: function(){
var opName = Template.instance().selectedOperation.get();
var statName = Template.instance().selectedStat.get();
return operationIndex(statName, opName);
},
statGroups: function(){
return statGroupNames;
},
stats: function(){
var group = Template.instance().selectedStatGroup.get();
return statGroups[group];
},
operations: function(){
var group = Template.instance().selectedStatGroup.get();
if(group === "Weakness/Resistance") return null;
if(group === "Saving Throws" || group === "Skills"){
return skillOperations;
} else {
return attributeOperations;
}
},
effectValueTemplate: function(){
//resistance/vulnerability template
var group = Template.instance().selectedStatGroup.get();
if(group === "Weakness/Resistance") return "multiplierEffectValue";
var op = Template.instance().selectedOperation.get();
if(!op) return null;
//operations that don't need templates
if(op === "advantage" || op === "disadvantage" || op === "fail") return null;
//proficiency template
if(op === "proficiency") return "proficiencyEffectValue";
//default template
return "regularEffectValue";
},
needsCommit: function(){
var inst = Template.instance();
if(
inst.selectedStat.get() !== this.stat ||
inst.selectedOperation.get() !== this.operation ||
(inst.value.get() !== this.value && inst.value.get() !== this.calculation)
){
return true;
} else {
return false;
}
},
valueTemplateData: function(){
var value = Template.instance().value.get()
var effectValue = value;
var selectedDamageMultiplier = null;
if(value === 0.5) selectedDamageMultiplier = 0;
if(value === 2) selectedDamageMultiplier = 1;
if(value === 0) selectedDamageMultiplier = 2;
var selectedProfiencyMultiplier = null;
if(value === 1) selectedProfiencyMultiplier = 0;
if(value === 0.5) selectedProfiencyMultiplier = 1;
if(value === 2) selectedProfiencyMultiplier = 2;
var data = {
effectValue: effectValue,
selectedDamageMultiplier: selectedDamageMultiplier,
selectedProfiencyMultiplier: selectedProfiencyMultiplier
};
return data;
}
});
Template.featureEffect.events({
"tap #commitChanges": function(event){
var newEffect = this;
var inst = Template.instance();
newEffect.operation = inst.selectedOperation.get();
newEffect.stat = inst.selectedStat.get();
var val = inst.value.get();
if(_.isNumber(val)){
newEffect.value = val;
newEffect.calculation = null;
} else if(_.isString(val)) {
newEffect.calculation = val;
newEffect.value = null;
}
Meteor.call("updateFeatureEffect", Template.parentData()._id, newEffect);
},
"tap #clearChanges": function(event){
//essentially re-render
var inst = Template.instance();
if(this.operation) inst.selectedOperation.set(this.operation);
if(this.stat) inst.selectedStat.set(this.stat);
if(this.stat) inst.selectedStatGroup.set(statsDict[this.stat].group)
var value = undefined;
if(_.isNumber(this.value)){
value = this.value;
} else if (_.isString(this.calculation)){
value = this.calculation;
}
inst.value.set(value);
},
"tap #deleteEffect": function(event){
Features.update(Template.parentData()._id, { $pull: { "effects": {_id: this._id} } });
},
"core-select #statGroupMenu": function(event){
var groupIndex = Template.instance().find("#statGroupMenu").selected;
var groupName = statGroupNames[groupIndex]
var oldName = Template.instance().selectedStatGroup.get();
if(oldName != groupName){
Template.instance().selectedStatGroup.set(groupName);
var oldIndex = statGroupIndex(Template.instance().selectedStat.get())
if(oldIndex != groupIndex){
Template.instance().selectedStat.set(null);
}
}
},
"core-select #statMenu": function(event){
var statIndex = Template.instance().find("#statMenu").selected;
var groupIndex = Template.instance().find("#statGroupMenu").selected;
var groupName = statGroupNames[groupIndex]
var group = statGroups[groupName];
var statObj = group[statIndex];
if(!statObj) return;
var statName = statObj.stat;
Template.instance().selectedStat.set(statName);
},
"core-select #operationMenu": function(event){
var groupName = Template.instance().selectedStatGroup.get();
var opGroup = (groupName === "Saving Throws" || groupName === "Skills")? skillOperations : attributeOperations;
var opIndex = Template.instance().find("#operationMenu").selected;
var op = opGroup[opIndex];
if(!op) return;
var opName = op.operation;
Template.instance().selectedOperation.set(opName);
},
"core-select #multiplierMenu": function(event){
var inst = Template.instance();
var selected = Template.instance().find("#multiplierMenu").selected;
if(selected === 0){
inst.value.set(0.5);
inst.selectedOperation.set("mul");
} else if (selected === 1){
inst.value.set(2);
inst.selectedOperation.set("mul");
} else if (selected === 2){
inst.value.set(0);
inst.selectedOperation.set("max");
}
},
"core-select #proficiencyMenu": function(event){
var inst = Template.instance();
var selected = inst.find("#proficiencyMenu").selected;
var value;
if(selected === 0){
inst.value.set(1);
} else if (selected === 1){
inst.value.set(0.5);
} else if (selected === 2){
inst.value.set(2);
}
},
"change #effectValueInput": function(event){
var inst = Template.instance();
var value = inst.find("#effectValueInput").value;
inst.value.set(value);
}
});

View File

@@ -0,0 +1,5 @@
paper-shadow.featureCard {
padding: 16px;
margin: 8px;
background: white;
}

View File

@@ -1,32 +1,22 @@
<template name="features">
<div class="statsFlex"><!--resources-->
{{#if attributeBase "rages"}}
{{#statCard id="rages" type="attribute" title="Rages"}}
{{attributeValue "rages"}}
{{/statCard}}
{{/if}}
{{#if canCast}}
{{#statCard id="spellSlots" type="attribute" title="Spell Slots"}}
<h1>{{> spellSlots}}</h1>
{{/statCard}}
{{/if}}
{{#if attributeBase "sorceryPoints"}}
{{#statCard id="sorceryPoints" type="attribute" title="Sorcery Points"}}
{{attributeValue "sorceryPoints"}}
{{/statCard}}
{{/if}}
{{#if attributeBase "expertiseDice"}}
{{#statCard id="expertiseDice" type="attribute" title="Expertise Dice"}}
{{attributeValue "expertiseDice"}}
{{/statCard}}
{{/if}}
{{#if attributeBase "superiorityDice"}}
{{#statCard id="superiorityDice" type="attribute" title="Superiority Dice"}}
{{attributeValue "superiorityDice"}}
{{/statCard}}
{{/if}}
</div>
<div class="actionsFlex">
<div class="resources"><!--resources-->
</div>
<div class="actions">
</div>
<div class="features">
{{#each features}}
<paper-shadow class="featureCard">
<div class="featureCardTop">
<h1>{{name}}</h1>
</div>
<div class="featureCardBottom">
<p>{{description}}</p>
</div>
<paper-ripple fit></paper-ripple>
</paper-shadow>
{{/each}}
</div>
<paper-fab id="addFeature" icon="add" title="Add" role="button" tabindex="0" aria-label="Add"></paper-fab>
</template>

View File

@@ -1,19 +1,27 @@
Template.features.helpers({
features: function(){
var features = Features.find({character: this._id});
var features = Features.find({charId: this._id});
return features;
}
});
Template.features.events({
// Fires when any element is clicked
'change .enabled': function (event) {
var enable = event.target.checked
Features.update(this._id, {$set: {enabled: enable}});
if(enable){
Template.parentData(1).pushEffects(this.name, this.effects);
} else {
Template.parentData(1).pullEffects(this.effects);
}
"tap #addFeature": function(event){
var featureId = Features.insert({name: "New Feature", charId: this._id});
GlobalUI.showDialog({
heading: "New Feature",
template: "featureDialog",
data: {featureId: featureId},
fullOnMobile: true
})
},
"tap .featureCard": function(event){
var featureId = this._id;
GlobalUI.showDialog({
heading: this.name,
template: "featureDialog",
data: {featureId: featureId},
fullOnMobile: true
})
}
});

View File

@@ -0,0 +1,23 @@
body /deep/ .featureDialogWidth {
width: 600px;
}
body /deep/ #statGroupDropDown {
width: 120px;
}
body /deep/ #statDropDown {
width: 120px;
}
body /deep/ #operationDropDown {
width: 100px;
}
body /deep/ #damageMultiplierDropDown {
width: 120px;
}
body /deep/ #proficiencyDropDown {
width: 120px;
}

View File

@@ -1,20 +1,27 @@
<template name="inventory">
<paper-shadow class="equipment">
Armor: {{#if equippedArmor}}{{equippedArmor}}{{else}}None{{/if}}
<paper-shadow class="equipment card double">
Armor:<br>
<paper-item>{{#if armor}}{{armor.name}}{{else}}none{{/if}}</paper-item>
Equipment:<br>
{{#each equipment}}
<paper-item>
{{name}}
</paper-item>
{{/each}}
</paper-shadow>
<div class="containers">
{{#each containers}}
<paper-shadow>
<h3>{{name}}</h3>
<table>
{{#each items}}
<tr class={{#if isSelected}}selected{{/if}}>
<td>{{#if stackable}}{{quantity}}{{/if}}</td>
<td>{{pluralName}}</td>
</tr>
{{/each}}
</table>
</paper-shadow>
<paper-shadow class="card">
<h3>{{name}}</h3>
{{#each items ../_id _id}}
<paper-item>
{{#if stackable}}{{quantity}}{{/if}} {{pluralName}}
</paper-item>
{{/each}}
</paper-shadow>
{{/each}}
</div>
<paper-fab id="add" icon="add" title="Add" role="button" tabindex="0" aria-label="Add"></paper-fab>
<paper-fab id="addItem" icon="note-add" title="Add Item" role="button" tabindex="0" aria-label="Add Item"></paper-fab>
<paper-fab id="addContainer" icon="work" title="Add Container" role="button" tabindex="0" aria-label="Add Container"></paper-fab>
</template>

View File

@@ -0,0 +1,27 @@
Template.inventory.helpers({
containers: function(){
return Containers.find({charId: this._id})
},
items: function(charId, containerId){
return Items.find({charId: charId, equipped: false, container: containerId })
},
armor: function(){
return Items.findOne({ charId: this._id, equipped: true, equipmentSlot: "armor" })
},
equipment: function(){
return Items.find({ charId: this._id, equipped: true, equipmentSlot: {$ne: "armor"} })
}
});
Template.inventory.events({
"tap #addItem": function(){
GlobalUI.showDialog({
template: "itemDialog",
data: null,
fullOnMobile: true
})
},
"tap #addContainer": function(){
Containers.insert({name: "New Container", isCarried: true, charId: this._id});
}
})

View File

@@ -0,0 +1,5 @@
<template name="itemDialog">
{{> quickForm collection="Items" id="insertItemForm" type="insert"}}
<paper-button affirmative>Cancel</paper-button>
<paper-button affirmative>Save Item</paper-button>
</template>

View File

@@ -1,6 +1,6 @@
<head>
<title>RPG Docs</title>
<meta name="viewport" content="width=device-width initial-scale=1.0, user-scalable=no">
<link href='http://fonts.googleapis.com/css?family=Roboto:400,300,300italic,400italic,500,500italic,700,700italic,900,900italic,100italic,100&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Roboto:400,300,300italic,400italic,500,500italic,700,700italic,900,900italic,100italic,100&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<script src="/components/webcomponentsjs/webcomponents.js"></script>
</head>

View File

@@ -8,11 +8,15 @@
<link rel="import" href="/components/core-scaffold/core-scaffold.html">
<!--paper components-->
<link rel="import" href="/components/paper-dialog/paper-action-dialog.html">
<link rel="import" href="/components/paper-dialog/paper-dialog.html">
<link rel="import" href="/components/paper-dropdown/paper-dropdown.html">
<link rel="import" href="/components/paper-dropdown-menu/paper-dropdown-menu.html">
<link rel="import" href="/components/paper-fab/paper-fab.html">
<link rel="import" href="/components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="/components/paper-input/paper-autogrow-textarea.html">
<link rel="import" href="/components/paper-input/paper-input.html">
<link rel="import" href="/components/paper-input/paper-input-decorator.html">
<link rel="import" href="/components/paper-item/paper-item.html">
<link rel="import" href="/components/paper-shadow/paper-shadow.html">
<link rel="import" href="/components/paper-spinner/paper-spinner.html">

View File

@@ -13,11 +13,14 @@
</core-header-panel>
</core-drawer-panel>
<paper-action-dialog global-dialog layered backdrop
<paper-action-dialog global-dialog backdrop
transition="paper-dialog-transition-bottom"
full-on-mobile>
class={{#if globalDialogFullOnMobile}}full-on-mobile{{/if}}
autoclosedisabled
heading={{globalDialogHeader}}
layered>
{{#if globalDialogTemplate}}
{{> UI.dynamic template=globalDialogTemplate data=globalDialogData}}
{{> UI.dynamic template=globalDialogTemplate data=globalDialogData}}
{{/if}}
</paper-action-dialog>

View File

@@ -0,0 +1,316 @@
standardItems = [
//armor
{
name: "Padded Armor",
plural: "Padded Armor",
description: "Padded armor consists of quilted layers of cloth and batting.",
equipmentSlot: "armor",
weight: 8,
value: 5,
feature: {
effects: [
{
stat: "armor",
operation: "base",
value: 11,
},
{
stat: "stealth",
operation: "disadvantage",
value: 1,
}
]
}
},
{
name: "Leather Armor",
plural: "Leather Armor",
description:
"The breastplate and shoulder protectors of this armor are made of leather that has been stiffened by being boiled in oil. The rest of the armor is made of softer and more flexible materials.",
equipmentSlot: "armor",
weight: 10,
value: 10,
feature: {
effects: [
{
stat: "armor",
operation: "base",
value: 11,
}
]
}
},
{
name: "Studded leather Armor",
plural: "Studded leather Armor",
description:
"Made from tough but flexible leather, studded leather is reinforced with close-set rivets or spikes.",
equipmentSlot: "armor",
weight: 13,
value: 45,
feature: {
effects: [
{
stat: "armor",
operation: "base",
value: 12,
}
]
}
},
{
name: "Hide Armor",
plural: "Hide Armor",
description:
"This crude armor consists of thick furs and pelts. It is commonly worn by barbarian tribes, evil humanoids, and other folk who lack access to the tools and materials needed to create better armor.",
equipmentSlot: "armor",
weight: 12,
value: 10,
feature: {
effects: [
{
stat: "armor",
operation: "base",
value: 12,
},
{
stat: "dexterityArmor",
operation: "max",
value: 2,
}
]
}
},
{
name: "Chain Shirt",
plural: "Chain Shirts",
description:
"Made of interlocking metal rings, a chain shirt is worn between layers of clothing or leather. This armor offers modest protection to the wearers upper body and allows the sound of the rings rubbing against one another to be muffled by outer layers.",
equipmentSlot: "armor",
weight: 20,
value: 50,
feature: {
effects: [
{
stat: "armor",
operation: "base",
value: 13,
},
{
stat: "dexterityArmor",
operation: "max",
value: 2,
}
]
}
},
{
name: "Scale Mail",
plural: "Scale Mail",
description:
"This armor consists of a coat and leggings (and perhaps a separate skirt) of leather covered with overlapping pieces of metal, much like the scales of a fish. The suit includes gauntlets.",
equipmentSlot: "armor",
weight: 45,
value: 50,
feature: {
effects: [
{
stat: "armor",
operation: "base",
value: 14,
},
{
stat: "dexterityArmor",
operation: "max",
value: 2,
},
{
stat: "stealth",
operation: "disadvantage",
value: 1,
}
]
}
},
{
name: "Breastplate",
plural: "Breastplates",
description:
"This armor consists of a fitted metal chest piece worn with supple leather. Although it leaves the legs and arms relatively unprotected, this armor provides good protection for the wearers vital organs while leaving the wearer relatively unencumbered.",
equipmentSlot: "armor",
weight: 20,
value: 400,
feature: {
effects: [
{
stat: "armor",
operation: "base",
value: 14,
},
{
stat: "dexterityArmor",
operation: "max",
value: 2,
}
]
}
},
{
name: "Half Plate",
plural: "Half Plate",
description:
"Half plate consists of shaped metal plates that cover most of the wearers body. It does not include leg protection beyond simple greaves that are attached with leather straps.",
equipmentSlot: "armor",
weight: 40,
value: 750,
feature: {
effects: [
{
stat: "armor",
operation: "base",
value: 15,
},
{
stat: "dexterityArmor",
operation: "max",
value: 2,
},
{
stat: "stealth",
operation: "disadvantage",
value: 1,
}
]
}
},
{
name: "Ring Mail",
plural: "Ring Mail",
description:
"This armor is leather armor with heavy rings sewn into it. The rings help reinforce the armor against blows from swords and axes. Ring mail is inferior to chain mail, and its usually worn only by those who cant afford better armor.",
equipmentSlot: "armor",
weight: 40,
value: 30,
feature: {
effects: [
{
stat: "armor",
operation: "base",
value: 14,
},
{
stat: "dexterityArmor",
operation: "max",
value: 0,
},
{
stat: "stealth",
operation: "disadvantage",
value: 1,
}
]
}
},
{
name: "Chain Mail",
plural: "Chain Mail",
description:
"Made of interlocking metal rings, chain mail includes a layer of quilted fabric worn underneath the mail to prevent chafing and to cushion the impact of blows. The suit includes gauntlets.",
equipmentSlot: "armor",
weight: 55,
value: 75,
feature: {
effects: [
{
stat: "armor",
operation: "base",
value: 16,
},
{
stat: "dexterityArmor",
operation: "max",
value: 0,
},
{
stat: "stealth",
operation: "disadvantage",
value: 1,
}
]
}
},
{
name: "Splint Armor",
plural: "Splint Armor",
description:
"This armor is made of narrow vertical strips of metal riveted to a backing of leather that is worn over cloth padding. Flexible chain mail protects the joints.",
equipmentSlot: "armor",
weight: 60,
value: 200,
feature: {
effects: [
{
stat: "armor",
operation: "base",
value: 17,
},
{
stat: "dexterityArmor",
operation: "max",
value: 0,
},
{
stat: "stealth",
operation: "disadvantage",
value: 1,
}
]
}
},
{
name: "Plate Armor",
plural: "Plate Armor",
description:
"Plate consists of shaped, interlocking metal plates to cover the entire body. A suit of plate includes gauntlets, heavy leather boots, a visored helmet, and thick layers of padding underneath the armor. Buckles and straps distribute the weight over the body.",
equipmentSlot: "armor",
weight: 65,
value: 1500,
feature: {
effects: [
{
stat: "armor",
operation: "base",
value: 18,
},
{
stat: "dexterityArmor",
operation: "max",
value: 0,
},
{
stat: "stealth",
operation: "disadvantage",
value: 1,
}
]
}
},
{
name: "Shield",
plural: "Shields",
description:
"A shield is made from wood or metal and is carried in one hand. Wielding a shield increases your Armor Class by 2. You can benefit from only one shield at a time.",
equipmentSlot: "held",
weight: 6,
value: 10,
feature: {
effects: [
{
stat: "armor",
operation: "add",
value: 2,
}
]
}
},
]

View File

@@ -8,20 +8,34 @@ Meteor.methods({
selector,
{ $set: setter }
)
},
updateFeatureEffect: function (featureId, newEffect) {
var selector = {_id: featureId};
selector["effects._id"] = newEffect._id;
var setter = {};
setter["effects.$"] = newEffect
Features.update(
selector,
{ $set: setter }
)
}
});
//pull a single effect by stat and id
pullEffect = function(id, effect){
var pullObject = {};
pullObject[effect.stat + ".effects"] = {_id: effect._id};
Characters.update(id, {$pull: pullObject });
}
if(effect.stat){
var pullObject = {};
pullObject[effect.stat + ".effects"] = {_id: effect._id};
Characters.update(id, {$pull: pullObject });
}
},
pushEffect = function(id, effect){
var pushObject = {};
pushObject[effect.stat + ".effects"] = effect;
Characters.update(id, {$push: pushObject});
if(effect.stat){
var pushObject = {};
pushObject[effect.stat + ".effects"] = effect;
Characters.update(id, {$push: pushObject});
}
}

View File

@@ -1,32 +1,6 @@
Meteor.methods({
addFeature: function(charId, newFeature){
Characters.update(
charId,
{ $push: {"customFeatures": newFeature} }
);
addFeatureEffects(charId, newFeature);
},
removeFeature: function(charId, oldFeature){
Characters.update(
charId,
{ $pull: { "customFeatures": {"_id": oldFeature._id} } }
);
removeFeatureEffects(charId, oldFeature);
},
updateFeature: function (charId, oldFeature, newFeature) {
var selector = {_id: charId, "customFeatures._id": oldFeature._id};
var setter = {"customFeatures.$": newFeature};
Characters.update(
selector,
{ $set: setter }
);
removeFeatureEffects(charId, oldFeature);
addFeatureEffects(charId, newFeature);
}
});
addFeatureEffects = function(charId, newFeature){
_.each(newFeature.effects, function(effect){
if(newFeature.name) effect.name = newFeature.name;
pushEffect(charId, effect);
});
_.each(newFeature.actions, function(action){
@@ -47,10 +21,10 @@ removeFeatureEffects = function(charId, oldFeature){
_.each(oldFeature.actions, function(action){
pullAction(charId, action);
});
_.each(newFeature.attacks, function(attack){
pushAttack(charId, attack);
_.each(oldFeature.attacks, function(attack){
pullAttack(charId, attack);
});
_.each(newFeature.spells, function(spell){
pushSpell(charId, spell);
_.each(oldFeature.spells, function(spell){
pullSpell(charId, spell);
});
};

View File

@@ -1,4 +1,16 @@
Meteor.publish("singleCharacter", function(characterId, userId){
//TODO check if this characer can be viewed by this user
return Characters.find({_id: characterId});
});
Meteor.publish("characterContainers", function(characterId, userId){
return Containers.find({charId: characterId});
});
Meteor.publish("characterItems", function(characterId, userId){
return Items.find({charId: characterId});
});
Meteor.publish("characterFeatures", function(characterId, userId){
return Features.find({charId: characterId});
});