diff --git a/rpg-docs/Model/Library/LibrarySpells.js b/rpg-docs/Model/Library/LibrarySpells.js
new file mode 100644
index 00000000..bdbb67de
--- /dev/null
+++ b/rpg-docs/Model/Library/LibrarySpells.js
@@ -0,0 +1,66 @@
+LibrarySpells = new Mongo.Collection("librarySpells");
+
+Schemas.LibrarySpells = new SimpleSchema({
+ name: {
+ type: String,
+ trim: false,
+ defaultValue: "New Spell",
+ },
+ description: {
+ type: String,
+ optional: true,
+ trim: false,
+ },
+ castingTime: {
+ type: String,
+ optional: true,
+ defaultValue: "action",
+ trim: false,
+ },
+ range: {
+ type: String,
+ optional: true,
+ trim: false,
+ },
+ duration: {
+ type: String,
+ optional: true,
+ trim: false,
+ defaultValue: "Instantaneous",
+ },
+ "components.verbal": {type: Boolean, defaultValue: false},
+ "components.somatic": {type: Boolean, defaultValue: false},
+ "components.concentration": {type: Boolean, defaultValue: false},
+ "components.material": {type: String, optional: true},
+ ritual: {
+ type: Boolean,
+ defaultValue: false,
+ },
+ level: {
+ type: Number,
+ defaultValue: 1,
+ },
+ school: {
+ type: String,
+ defaultValue: "Abjuration",
+ allowedValues: magicSchools,
+ },
+ library: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
+ effects: {type: [Schemas.LibraryEffects], defaultValue: []},
+ attacks: {type: [Schemas.LibraryAttacks], defaultValue: []},
+});
+
+LibrarySpells.attachSchema(Schemas.LibrarySpells);
+
+LibrarySpells.allow({
+ insert(userId, doc) {
+ return Libraries.canEdit(userId, doc.library);
+ },
+ update(userId, doc, fields, modifier) {
+ return Libraries.canEdit(userId, doc.library);
+ },
+ remove(userId, doc) {
+ return Libraries.canEdit(userId, doc.library);
+ },
+ fetch: ["library"],
+});
diff --git a/rpg-docs/client/views/character/spells/spells.html b/rpg-docs/client/views/character/spells/spells.html
index 35c0736e..7158c8c3 100644
--- a/rpg-docs/client/views/character/spells/spells.html
+++ b/rpg-docs/client/views/character/spells/spells.html
@@ -131,6 +131,15 @@
+
+
+ Spell library
+
+
+
+
New spell
diff --git a/rpg-docs/client/views/character/spells/spells.js b/rpg-docs/client/views/character/spells/spells.js
index e7cf0ad7..85b010ba 100644
--- a/rpg-docs/client/views/character/spells/spells.js
+++ b/rpg-docs/client/views/character/spells/spells.js
@@ -211,8 +211,17 @@ Template.spells.events({
});
},
"click .addSpell": function(event, instance){
- var charId = this.charId;
- var listId = SpellLists.findOne({charId: this._id})._id;
+ var charId = this._id;
+ var list = SpellLists.findOne({charId});
+ var listId = list && list._id
+ if (!listId){
+ listId = SpellLists.insert({
+ name: "New SpellList",
+ charId: charId,
+ saveDC: "8 + intelligenceMod + proficiencyBonus",
+ attackBonus: "intelligenceMod + proficiencyBonus",
+ });
+ }
var id = Spells.insert({
name: "New Spell",
charId: this._id,
@@ -229,6 +238,48 @@ Template.spells.events({
returnElement: () => instance.find(`.spell[data-id='${id}']`),
});
},
+ "click .librarySpell": function(event, instance){
+ var charId = this._id;
+ var spellId = Random.id();
+ var list = SpellLists.findOne({charId});
+ var listId = list && list._id
+ pushDialogStack({
+ template: "spellLibraryDialog",
+ element: event.currentTarget,
+ callback: (result) => {
+ if (!result) return;
+ if (!listId){
+ listId = SpellLists.insert({
+ name: "New SpellList",
+ charId: charId,
+ saveDC: "8 + intelligenceMod + proficiencyBonus",
+ attackBonus: "intelligenceMod + proficiencyBonus",
+ });
+ }
+ // Make the library spell into a regular spell
+ let spell = _.omit(result, "library", "attacks", "effects");
+ spell.charId = charId;
+ spell.parent = {
+ id: listId,
+ collection: "SpellLists",
+ };
+ spell.prepared = "prepared";
+ Spells.insert(spell);
+ // Copy over attacks and effects
+ _.each(result.attacks, (attack) => {
+ attack.charId = charId;
+ attack.parent = {id: spellId, collection: "Spells"};
+ Attacks.insert(attack);
+ });
+ _.each(result.effects, (effect) => {
+ effect.charId = charId;
+ effect.parent = {id: spellId, collection: "Spells"};
+ Effects.insert(effect);
+ });
+ },
+ returnElement: () => $(`[data-id='${spellId}']`).get(0),
+ })
+ },
"click .preparedCheckbox": function(event){
event.stopPropagation();
},
diff --git a/rpg-docs/client/views/character/spells/spellsLibraryDialog/spellLibraryDialog.css b/rpg-docs/client/views/character/spells/spellsLibraryDialog/spellLibraryDialog.css
new file mode 100644
index 00000000..e69de29b
diff --git a/rpg-docs/client/views/character/spells/spellsLibraryDialog/spellLibraryDialog.html b/rpg-docs/client/views/character/spells/spellsLibraryDialog/spellLibraryDialog.html
new file mode 100644
index 00000000..86972f17
--- /dev/null
+++ b/rpg-docs/client/views/character/spells/spellsLibraryDialog/spellLibraryDialog.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+ Spells
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{spell.name}}
+
+ |
+
+
diff --git a/rpg-docs/client/views/character/spells/spellsLibraryDialog/spellLibraryDialog.js b/rpg-docs/client/views/character/spells/spellsLibraryDialog/spellLibraryDialog.js
new file mode 100644
index 00000000..7e37d184
--- /dev/null
+++ b/rpg-docs/client/views/character/spells/spellsLibraryDialog/spellLibraryDialog.js
@@ -0,0 +1,115 @@
+const librarySubs = new SubsManager();
+
+const categories = [
+ {name: "Cantrips", key: 0},
+ {name: "Level 1", key: 1},
+ {name: "Level 2", key: 2},
+ {name: "Level 3", key: 3},
+ {name: "Level 4", key: 4},
+ {name: "Level 5", key: 5},
+ {name: "Level 6", key: 6},
+ {name: "Level 7", key: 7},
+ {name: "Level 8", key: 8},
+ {name: "Level 9", key: 9},
+];
+
+Template.spellLibraryDialog.onCreated(function(){
+ this.selectedSpell = new ReactiveVar();
+ this.searchTerm = new ReactiveVar();
+ this.categoriesOpen = new ReactiveVar([]);
+ this.readyDict = new ReactiveDict();
+ this.searchReady = new ReactiveVar();
+ librarySubs.subscribe("standardLibraries");
+ this.autorun(() => {
+ // Subscribe to all open categories
+ _.each(this.categoriesOpen.get(), (key) => {
+ var handle = librarySubs.subscribe("standardLibrarySpells", key);
+ this.autorun(() => {
+ this.readyDict.set(key, handle.ready());
+ });
+ });
+ });
+ this.autorun(() => {
+ // If we are searching, subscibe to all categories
+ if (this.searchTerm.get()){
+ let handles = _.map(categories, category =>
+ librarySubs.subscribe("standardLibrarySpells", category.key)
+ );
+ // Ready when all handles are ready
+ this.autorun(() => {
+ this.searchReady.set(_.every(handles, h => h.ready()));
+ });
+ }
+ });
+});
+
+Template.spellLibraryDialog.helpers({
+ ready(key){
+ return Template.instance().readyDict.get(key);
+ },
+ categories(){
+ return categories;
+ },
+ spellsInCategory(categoryKey){
+ return LibrarySpells.find({
+ library: "SRDLibraryGA3XWsd",
+ "settings.category": categoryKey,
+ }, {
+ sort: {name: 1},
+ });
+ },
+ isSelected(spell){
+ const selected = Template.instance().selectedSpell.get();
+ return selected && selected._id === spell._id;
+ },
+ isOpen(key){
+ const cats = Template.instance().categoriesOpen.get();
+ return _.contains(cats, key);
+ },
+ searchTerm(){
+ return Template.instance().searchTerm.get();
+ },
+ searchReady(){
+ return Template.instance().searchReady.get();
+ },
+ searchSpells(){
+ const searchTerm = Template.instance().searchTerm.get();
+ if (!searchTerm) return;
+ return LibrarySpells.find({
+ library: "SRDLibraryGA3XWsd",
+ name: {
+ $regex: new RegExp(".*" + searchTerm + ".*", "gi")
+ },
+ });
+ },
+});
+
+Template.spellLibraryDialog.events({
+ "click .cancelButton": function(event, template){
+ popDialogStack();
+ },
+ "click .okButton": function(event, template){
+ popDialogStack(template.selectedSpell.get());
+ },
+ "click .library-spell": function(event, template){
+ template.selectedSpell.set(this.spell);
+ },
+ "click #backButton": function(event, template){
+ popDialogStack();
+ },
+ "click .category-header": function(event, template){
+ let cats = template.categoriesOpen.get();
+ const key = this.key;
+ // Toggle whether this key is in the array or not
+ if (_.contains(cats, key)){
+ cats = _.without(cats, key);
+ } else {
+ cats.push(key);
+ }
+ template.categoriesOpen.set(cats);
+ },
+ "input .search-input, change .search-input": function(event, template){
+ const value = event.currentTarget.value;
+ template.searchTerm.set(value);
+ },
+});
diff --git a/rpg-docs/server/publications/library.js b/rpg-docs/server/publications/library.js
index e113a531..e5d0c276 100644
--- a/rpg-docs/server/publications/library.js
+++ b/rpg-docs/server/publications/library.js
@@ -14,3 +14,12 @@ Meteor.publish("standardLibraryItems", function(categoryKey){
sort: {name: 1},
});
});
+
+Meteor.publish("standardLibrarySpells", function(level){
+ return LibraryItems.find({
+ library: {$in: standardLibraryIds},
+ level,
+ }, {
+ sort: {name: 1},
+ });
+});