From 84512beb724404b1348c22343bdd4c103b619dbb Mon Sep 17 00:00:00 2001 From: Thaum Date: Wed, 21 Jan 2015 11:16:00 +0000 Subject: [PATCH] Implemented Feature editing UI --- .codio | 2 +- rpg-docs/.meteor/packages | 3 +- rpg-docs/.meteor/versions | 3 +- rpg-docs/Model/Character/Characters.js | 2 - rpg-docs/Model/Character/Features.js | 43 +-- .../Model/Character/SubSchemas/Attribute.js | 30 +- .../SubSchemas/{ => Effect}/Effect.js | 4 +- rpg-docs/Model/Inventory/Containers.js | 3 +- rpg-docs/Model/Inventory/Items.js | 28 +- rpg-docs/Routes/Routes.js | 9 +- rpg-docs/client/globalHelpers/GlobalUI.js | 55 +++ .../client/globalHelpers/setupAutoform.js | 1 + .../client/views/character/Stats/stats.html | 2 +- .../views/character/characterSheet.html | 16 +- .../character/features/featureDialog.html | 24 ++ .../views/character/features/featureDialog.js | 36 ++ .../character/features/featureEffect.html | 69 ++++ .../views/character/features/featureEffect.js | 329 ++++++++++++++++++ .../views/character/features/features.css | 5 + .../views/character/features/features.html | 46 +-- .../views/character/features/features.js | 28 +- .../character/features/featuresDialog.css | 23 ++ .../views/character/inventory/inventory.html | 33 +- .../views/character/inventory/inventory.js | 27 ++ .../views/character/inventory/itemDialog.html | 5 + rpg-docs/client/views/layout/head.html | 2 +- rpg-docs/client/views/layout/imports.html | 4 + rpg-docs/client/views/layout/layout.html | 9 +- rpg-docs/lib/constants/standardItems.js | 316 +++++++++++++++++ rpg-docs/lib/methods/effectUtils.js | 28 +- rpg-docs/lib/methods/featureUtils.js | 36 +- .../server/publications/singleCharacter.js | 12 + 32 files changed, 1072 insertions(+), 161 deletions(-) rename rpg-docs/Model/Character/SubSchemas/{ => Effect}/Effect.js (83%) create mode 100644 rpg-docs/client/globalHelpers/GlobalUI.js create mode 100644 rpg-docs/client/globalHelpers/setupAutoform.js create mode 100644 rpg-docs/client/views/character/features/featureDialog.html create mode 100644 rpg-docs/client/views/character/features/featureDialog.js create mode 100644 rpg-docs/client/views/character/features/featureEffect.html create mode 100644 rpg-docs/client/views/character/features/featureEffect.js create mode 100644 rpg-docs/client/views/character/features/features.css create mode 100644 rpg-docs/client/views/character/features/featuresDialog.css create mode 100644 rpg-docs/client/views/character/inventory/inventory.js create mode 100644 rpg-docs/client/views/character/inventory/itemDialog.html create mode 100644 rpg-docs/lib/constants/standardItems.js diff --git a/.codio b/.codio index 54d2b857..b9be9fad 100644 --- a/.codio +++ b/.codio @@ -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 diff --git a/rpg-docs/.meteor/packages b/rpg-docs/.meteor/packages index a9323ad0..9ace0240 100644 --- a/rpg-docs/.meteor/packages +++ b/rpg-docs/.meteor/packages @@ -14,5 +14,6 @@ reactive-var cw4gn3r:jquery-event-drag underscore aldeed:collection2 -aldeed:autoform differential:vulcanize +aldeed:autoform +conielo:autoform-polymer-paper diff --git a/rpg-docs/.meteor/versions b/rpg-docs/.meteor/versions index dbe05c70..27dd0d74 100644 --- a/rpg-docs/.meteor/versions +++ b/rpg-docs/.meteor/versions @@ -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 diff --git a/rpg-docs/Model/Character/Characters.js b/rpg-docs/Model/Character/Characters.js index e0cd5b2c..5a51a189 100644 --- a/rpg-docs/Model/Character/Characters.js +++ b/rpg-docs/Model/Character/Characters.js @@ -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}, diff --git a/rpg-docs/Model/Character/Features.js b/rpg-docs/Model/Character/Features.js index b7f545a3..b65cdfba 100644 --- a/rpg-docs/Model/Character/Features.js +++ b/rpg-docs/Model/Character/Features.js @@ -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); } -}); \ No newline at end of file +}); diff --git a/rpg-docs/Model/Character/SubSchemas/Attribute.js b/rpg-docs/Model/Character/SubSchemas/Attribute.js index 031a061f..b9975eda 100644 --- a/rpg-docs/Model/Character/SubSchemas/Attribute.js +++ b/rpg-docs/Model/Character/SubSchemas/Attribute.js @@ -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"] + } }); diff --git a/rpg-docs/Model/Character/SubSchemas/Effect.js b/rpg-docs/Model/Character/SubSchemas/Effect/Effect.js similarity index 83% rename from rpg-docs/Model/Character/SubSchemas/Effect.js rename to rpg-docs/Model/Character/SubSchemas/Effect/Effect.js index 82b4474f..0034ce8a 100644 --- a/rpg-docs/Model/Character/SubSchemas/Effect.js +++ b/rpg-docs/Model/Character/SubSchemas/Effect/Effect.js @@ -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: { diff --git a/rpg-docs/Model/Inventory/Containers.js b/rpg-docs/Model/Inventory/Containers.js index c04a98e2..7a8a7d78 100644 --- a/rpg-docs/Model/Inventory/Containers.js +++ b/rpg-docs/Model/Inventory/Containers.js @@ -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 } }); diff --git a/rpg-docs/Model/Inventory/Items.js b/rpg-docs/Model/Inventory/Items.js index 3b231f23..1dc9281e 100644 --- a/rpg-docs/Model/Inventory/Items.js +++ b/rpg-docs/Model/Inventory/Items.js @@ -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); } }); diff --git a/rpg-docs/Routes/Routes.js b/rpg-docs/Routes/Routes.js index dba32a79..4713a04e 100644 --- a/rpg-docs/Routes/Routes.js +++ b/rpg-docs/Routes/Routes.js @@ -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' }); -}); +}); \ No newline at end of file diff --git a/rpg-docs/client/globalHelpers/GlobalUI.js b/rpg-docs/client/globalHelpers/GlobalUI.js new file mode 100644 index 00000000..1b36e788 --- /dev/null +++ b/rpg-docs/client/globalHelpers/GlobalUI.js @@ -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); + }, +}); diff --git a/rpg-docs/client/globalHelpers/setupAutoform.js b/rpg-docs/client/globalHelpers/setupAutoform.js new file mode 100644 index 00000000..2a956027 --- /dev/null +++ b/rpg-docs/client/globalHelpers/setupAutoform.js @@ -0,0 +1 @@ +AutoForm.setDefaultTemplate('paper'); \ No newline at end of file diff --git a/rpg-docs/client/views/character/Stats/stats.html b/rpg-docs/client/views/character/Stats/stats.html index f42854c3..0d00fc2f 100644 --- a/rpg-docs/client/views/character/Stats/stats.html +++ b/rpg-docs/client/views/character/Stats/stats.html @@ -1,5 +1,5 @@ diff --git a/rpg-docs/client/views/character/features/featureDialog.html b/rpg-docs/client/views/character/features/featureDialog.html new file mode 100644 index 00000000..0b8e9bc6 --- /dev/null +++ b/rpg-docs/client/views/character/features/featureDialog.html @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/rpg-docs/client/views/character/features/featureDialog.js b/rpg-docs/client/views/character/features/featureDialog.js new file mode 100644 index 00000000..1003539c --- /dev/null +++ b/rpg-docs/client/views/character/features/featureDialog.js @@ -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); + } +}); \ No newline at end of file diff --git a/rpg-docs/client/views/character/features/featureEffect.html b/rpg-docs/client/views/character/features/featureEffect.html new file mode 100644 index 00000000..12395e77 --- /dev/null +++ b/rpg-docs/client/views/character/features/featureEffect.html @@ -0,0 +1,69 @@ + + + + + + + \ No newline at end of file diff --git a/rpg-docs/client/views/character/features/featureEffect.js b/rpg-docs/client/views/character/features/featureEffect.js new file mode 100644 index 00000000..ebc11cdb --- /dev/null +++ b/rpg-docs/client/views/character/features/featureEffect.js @@ -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); + } +}); diff --git a/rpg-docs/client/views/character/features/features.css b/rpg-docs/client/views/character/features/features.css new file mode 100644 index 00000000..17e50a09 --- /dev/null +++ b/rpg-docs/client/views/character/features/features.css @@ -0,0 +1,5 @@ +paper-shadow.featureCard { + padding: 16px; + margin: 8px; + background: white; +} \ No newline at end of file diff --git a/rpg-docs/client/views/character/features/features.html b/rpg-docs/client/views/character/features/features.html index a0817e26..95880ec0 100644 --- a/rpg-docs/client/views/character/features/features.html +++ b/rpg-docs/client/views/character/features/features.html @@ -1,32 +1,22 @@ \ No newline at end of file diff --git a/rpg-docs/client/views/character/features/features.js b/rpg-docs/client/views/character/features/features.js index 2a1e77c1..0f61c4cb 100644 --- a/rpg-docs/client/views/character/features/features.js +++ b/rpg-docs/client/views/character/features/features.js @@ -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 + }) } }); \ No newline at end of file diff --git a/rpg-docs/client/views/character/features/featuresDialog.css b/rpg-docs/client/views/character/features/featuresDialog.css new file mode 100644 index 00000000..d8a4fda9 --- /dev/null +++ b/rpg-docs/client/views/character/features/featuresDialog.css @@ -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; +} \ No newline at end of file diff --git a/rpg-docs/client/views/character/inventory/inventory.html b/rpg-docs/client/views/character/inventory/inventory.html index 6b38212a..b7daf0dd 100644 --- a/rpg-docs/client/views/character/inventory/inventory.html +++ b/rpg-docs/client/views/character/inventory/inventory.html @@ -1,20 +1,27 @@ \ No newline at end of file diff --git a/rpg-docs/client/views/character/inventory/inventory.js b/rpg-docs/client/views/character/inventory/inventory.js new file mode 100644 index 00000000..c3fdb3b0 --- /dev/null +++ b/rpg-docs/client/views/character/inventory/inventory.js @@ -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}); + } +}) \ No newline at end of file diff --git a/rpg-docs/client/views/character/inventory/itemDialog.html b/rpg-docs/client/views/character/inventory/itemDialog.html new file mode 100644 index 00000000..0f7f6b20 --- /dev/null +++ b/rpg-docs/client/views/character/inventory/itemDialog.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/rpg-docs/client/views/layout/head.html b/rpg-docs/client/views/layout/head.html index 843887a0..ef76c01d 100644 --- a/rpg-docs/client/views/layout/head.html +++ b/rpg-docs/client/views/layout/head.html @@ -1,6 +1,6 @@ RPG Docs - + diff --git a/rpg-docs/client/views/layout/imports.html b/rpg-docs/client/views/layout/imports.html index 053dce62..4ff1ca7b 100644 --- a/rpg-docs/client/views/layout/imports.html +++ b/rpg-docs/client/views/layout/imports.html @@ -8,11 +8,15 @@ + + + + diff --git a/rpg-docs/client/views/layout/layout.html b/rpg-docs/client/views/layout/layout.html index 67881b74..540cb8cc 100644 --- a/rpg-docs/client/views/layout/layout.html +++ b/rpg-docs/client/views/layout/layout.html @@ -13,11 +13,14 @@ - + 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}} diff --git a/rpg-docs/lib/constants/standardItems.js b/rpg-docs/lib/constants/standardItems.js new file mode 100644 index 00000000..3d7a1d25 --- /dev/null +++ b/rpg-docs/lib/constants/standardItems.js @@ -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 wearer’s 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 wearer’s 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 wearer’s 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 it’s usually worn only by those who can’t 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, + } + ] + } + }, +] \ No newline at end of file diff --git a/rpg-docs/lib/methods/effectUtils.js b/rpg-docs/lib/methods/effectUtils.js index 408df881..cdabd81c 100644 --- a/rpg-docs/lib/methods/effectUtils.js +++ b/rpg-docs/lib/methods/effectUtils.js @@ -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}); + } } diff --git a/rpg-docs/lib/methods/featureUtils.js b/rpg-docs/lib/methods/featureUtils.js index 7dd69c1a..d6a4a98a 100644 --- a/rpg-docs/lib/methods/featureUtils.js +++ b/rpg-docs/lib/methods/featureUtils.js @@ -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); }); }; \ No newline at end of file diff --git a/rpg-docs/server/publications/singleCharacter.js b/rpg-docs/server/publications/singleCharacter.js index 01b90f21..92548212 100644 --- a/rpg-docs/server/publications/singleCharacter.js +++ b/rpg-docs/server/publications/singleCharacter.js @@ -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}); }); \ No newline at end of file