-
+
Nothing found.
-
- Most players want to roll their own attacks, you probably shouldn't take that away from them. ;)
+ Most players want to roll their own attacks, you probably shouldn't take that away from them.
+ ;)
diff --git a/src/components/compendium/Monster.vue b/src/components/compendium/Monster.vue
index fcd578070..38a90b387 100644
--- a/src/components/compendium/Monster.vue
+++ b/src/components/compendium/Monster.vue
@@ -322,7 +322,7 @@
{{ monster.name.capitalizeEach() }} can take {{ monster.legendary_count }} legendary
actions, choosing from the options below. Only one legendary action option can be used
- at a time and only at the end of another creature’s turn. {{ monster.name }} regains
+ at a time and only at the end of another creature's turn. {{ monster.name }} regains
spent legendary actions at the start of their turn.
@@ -336,36 +336,9 @@
ability.action_list[0].rolls.length
"
>
-
-
-
-
-
- {{ ability.name }}
-
-
-
-
-
- {{ i + 1 }}
-
- {{ getVersatile(ability, i) }}
-
-
-
-
-
-
-
-
+
-
+
@@ -495,20 +468,9 @@ export default {
},
methods: {
...mapActions("api_monsters", ["fetch_monster"]),
- ...mapActions(["setActionRoll"]),
setSize(size) {
this.width = size.width;
},
- getVersatile(ability, i) {
- return ability[`versatile_${i ? "two" : "one"}`] || `Option ${i + 1}`;
- },
- roll(e, action, versatile) {
- const config = {
- type: "monster_action",
- versatile,
- };
- this.setActionRoll(this.rollAction(e, action, config));
- },
passivePerception() {
return 10 + parseInt(this.skillModifier("wisdom", "perception"));
},
diff --git a/src/components/compendium/Spell.vue b/src/components/compendium/Spell.vue
index 43febf1ef..4e5176504 100644
--- a/src/components/compendium/Spell.vue
+++ b/src/components/compendium/Spell.vue
@@ -1,116 +1,253 @@
-
-
{{ spell.name }}
-
-
- {{ spell.level | numeral("oO") }} level
-
- Cantrip
- {{ spell.school.name }}
-
-
- Casting time: {{ spell.casting_time }}
- Range: {{ spell.range }}
- Components:
-
- {{ component }},
+
+
+ Roll
+
+
+
+
+
+
+
+
+
+ Select Casting level
+
+
+
+
+
+ Roll
+
+
+
+
+
+
+ {{ spell.name.capitalizeEach() }} {{ spell.source }}
+
+
+ Cantrip
+ {{ spell.level | numeral("0o") }}-level
+ {{ spell.school.capitalize() }}
+
+
+
+
+ Casting time: {{ spell.cast_time }} {{ spell.cast_time_type }}
+ Range: {{ range }}
+ Ritual: Yes
+
+ Components:
+ {{ spell.components.map((comp) => comp.charAt(0).toUpperCase()).join(", ") }}
+ {{ spell.material_description ? `(${spell.material_description})` : "" }}
- ({{ parse_spell_str(spell.material) }})
-
- Duration:
- Concentration,
- {{ spell.duration }}
+
+ Duration: {{ duration }}
- Classes:
+ Classes:
- {{ _class.name }},
+ {{ _class.capitalize()
+ }},
-
-
-
+
-
- At higher levels.
-
- {{ parse_spell_str(higher) }}
-
-
+
+
+
+ At Higher Levels. {{ spell.higher_level }}
+
+
+
+
diff --git a/src/components/contribute/spell/ViewSpell.vue b/src/components/contribute/spell/ViewSpell.vue
index f9a1c02f4..dbffd090e 100644
--- a/src/components/contribute/spell/ViewSpell.vue
+++ b/src/components/contribute/spell/ViewSpell.vue
@@ -1,96 +1,81 @@
-
{{ spell.name }} {{ spell.source }}
+
+ {{ spell.name }} {{ spell.source }}
+
Cantrip
- {{ spell.level | numeral('0o') }}-level
+ {{ spell.level | numeral("0o") }}-level
{{ spell.school }}
- Casting time: {{ spell.cast_time_nr }} {{spell.cast_time_type }}
- Range: {{ range }}
- Components:
- V
- S
- M
- {{ spell.material_description ? `(${spell.material_description})` : "" }}
-
- Duration: {{ duration }}
-
- Classes:
-
- {{ _class }},
-
+ Casting time: {{ spell.cast_time }} {{ spell.cast_time_type }}
+ Range: {{ range }}
+ Ritual: Yes
+
+ Components:
+ {{ spell.components.map((comp) => comp.charAt(0).toUpperCase()).join(", ") }}
+ {{ spell.material_description ? `(${spell.material_description})` : "" }}
+
+
+ Duration: {{ duration }}
+
+ Classes:
+
+ {{ _class }},
+
-
+
- At Higher Levels. {{ spell.higher_level }}
+ At Higher Levels. {{ spell.higher_level }}
-
+
Roll spell
-
+
-
-
-
-
-
-
- Advantage
-
-
-
- Disadvantage
-
-
+
Select Casting level
-
{{ i }}
@@ -98,174 +83,82 @@
-
- Roll
-
+
Roll
+
diff --git a/src/components/contribute/spell/edit.vue b/src/components/contribute/spell/edit.vue
index c10525970..e3fd698c4 100644
--- a/src/components/contribute/spell/edit.vue
+++ b/src/components/contribute/spell/edit.vue
@@ -1,242 +1,203 @@
-
- {{ (spell.changed) ? spell.name : old_spell.name }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Parse to new spell
-
-
-
-
- {{ spell_levels[old_spell.level] }}
- {{ old_spell.school.name }}
-
-
-
- Casting time: {{ old_spell.casting_time }}
- Range: {{ old_spell.range }}
- Components:
-
- {{ component }},
-
- ({{ old_spell.material }})
-
- Duration:
- Concentration,
- {{ old_spell.duration }}
- Classes:
-
- {{ _class.name }},
-
-
-
-
- {{ desc }}
-
-
-
- At higher levels.
-
- {{ higher }}
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
- There are unsaved changes in the spell
-
-
Revert
-
-
- Cancel
-
-
+
-
+
-
\ No newline at end of file
+
diff --git a/src/components/contribute/spell/forms/BasicInfo.vue b/src/components/contribute/spell/forms/BasicInfo.vue
deleted file mode 100644
index cdd8026f0..000000000
--- a/src/components/contribute/spell/forms/BasicInfo.vue
+++ /dev/null
@@ -1,436 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- At higer levels
-
-
- Set in what way the spell changes at higher levels.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Markdown
-
-
-
- The description field accepts markdown.
- *Italic*
- **Bold**
- __Underline__
- - list item
- | table column |
-
-
-
-
-
-
-
-
-
-
-
Preview
-
-
- At Higher Levels. {{ spell.higher_level }}
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/contribute/spell/forms/actions/Conditions.vue b/src/components/contribute/spell/forms/actions/Conditions.vue
deleted file mode 100644
index af7fcab7c..000000000
--- a/src/components/contribute/spell/forms/actions/Conditions.vue
+++ /dev/null
@@ -1,412 +0,0 @@
-
-
-
-
-
-
-
-
- {{ parseInt(con_index) + 1 }}. {{ condition.condition }}
-
-
-
-
-
- Remove
-
-
-
-
-
-
-
-
-
-
-
{{ errors.first(`condition-${con_index}`) }}
-
-
-
-
-
-
-
-
-
-
- Apply condition
-
-
-
- Select when this condition should be applied.
-
-
-
-
-
-
-
{{ errors.first(`application-${con_index}`) }}
-
-
-
-
-
-
-
{{ errors.first(`dice_count-${con_index}`) }}
-
-
-
-
-
{{ errors.first(`dice_type-${con_index}`) }}
-
-
-
-
-
-
-
-
-
- Fixed value
-
-
- Set the fixed value that is added on top of the rolled value.
-
-
-
-
-
-
-
-
-
-
-
{{ errors.first(`order-${con_index}`) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ errors.first(`level-${con_index}`) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/contribute/spell/forms/actions/Effects.vue b/src/components/contribute/spell/forms/actions/Effects.vue
deleted file mode 100644
index 54cf8f795..000000000
--- a/src/components/contribute/spell/forms/actions/Effects.vue
+++ /dev/null
@@ -1,410 +0,0 @@
-
-
-
-
-
-
-
-
- {{parseInt(eff_index) + 1}}.
- {{ effect.effect.type }}
- {{ effect.effect.subtype }}
-
-
-
-
-
- Remove effect
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Apply effect
-
-
-
- Select when this effect should be applied.
-
-
-
-
-
-
-
{{ errors.first(`application-${eff_index}`) }}
-
-
-
-
-
-
-
-
-
- Target
-
-
- Select to whom the effect should be applied.
-
-
-
-
-
-
-
{{ errors.first(`target-${eff_index}`) }}
-
-
-
-
-
-
-
-
-
-
-
-
{{ errors.first(`level-${eff_index}`) }}
-
-
-
-
-
-
-
- Dice Type
-
-
-
-
-
-
-
-
- {{line}}
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/contribute/spell/forms/actions/Notifications.vue b/src/components/contribute/spell/forms/actions/Notifications.vue
deleted file mode 100644
index 206b4f753..000000000
--- a/src/components/contribute/spell/forms/actions/Notifications.vue
+++ /dev/null
@@ -1,391 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/contribute/spell/forms/actions/Rolls.vue b/src/components/contribute/spell/forms/actions/Rolls.vue
deleted file mode 100644
index 5a335a54d..000000000
--- a/src/components/contribute/spell/forms/actions/Rolls.vue
+++ /dev/null
@@ -1,197 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- {{data.row.projectile_count ? `${data.row.projectile_count}x`: ""}}
- {{data.row.dice_count}}{{data.row.dice_type ? "d" : ""}}{{data.row.dice_type}}{{data.row.fixed_val ? "+" : ""}}{{data.row.fixed_val}}
-
-
-
-
- Healing
-
-
-
-
- {{ data.row.damage_type.capitalize() }}
- damage
-
-
-
-
- {{
- action_type === "save"
- ? `Save: ${application[data.row.save_fail_mod]}`
- : `Miss: ${application[data.row.miss_mod]}`
- }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/contribute/spell/forms/actions/index.vue b/src/components/contribute/spell/forms/actions/index.vue
deleted file mode 100644
index a1dbb34ba..000000000
--- a/src/components/contribute/spell/forms/actions/index.vue
+++ /dev/null
@@ -1,189 +0,0 @@
-
-
-
-
-
- Spell actions are the parts of a spell that can be rolled.
- By adding spell actions to your spell, it can be used during
- encounters to quickly apply damage or healing.
-
-
-
-
-
-
- {{ action.name }}
-
-
-
-
-
- Remove
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Doesn't cost a spell slot
-
-
-
-
-
-
-
- Can be cast seperately
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/contribute/spell/forms/index.vue b/src/components/contribute/spell/forms/index.vue
deleted file mode 100644
index 69e42191a..000000000
--- a/src/components/contribute/spell/forms/index.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/components/contribute/spell/index.vue b/src/components/contribute/spell/index.vue
index c10fbf0ee..eb3bbd39a 100644
--- a/src/components/contribute/spell/index.vue
+++ b/src/components/contribute/spell/index.vue
@@ -1,166 +1,166 @@
- {{ (spell.changed) ? spell.name : oldSpell.name }}
+ {{ spell.changed ? spell.name : oldSpell.name }}
-
- Edit
-
+ Edit
-
-
-
- {{ levels[oldSpell.level] }}
- {{ oldSpell.school.name }}
-
+
+
+
+ {{ levels[oldSpell.level] }}
+ {{ oldSpell.school.name }}
+
-
- Casting time: {{ oldSpell.casting_time }}
- Range: {{ oldSpell.range }}
- Components:
-
- {{ component }},
-
- ({{ oldSpell.material }})
-
- Duration:
- Concentration,
- {{ oldSpell.duration }}
- Classes:
-
- {{ _class.name }},
-
-
-
-
- {{ desc }}
-
+
+ Casting time: {{ oldSpell.casting_time }}
+ Range: {{ oldSpell.range }}
+ Components:
+
+ {{ component
+ }},
+
+ ({{ oldSpell.material }})
+
+ Duration:
+ Concentration,
+ {{ oldSpell.duration }}
+ Classes:
+
+ {{ _class.name
+ }},
+
+
+
+
+ {{ desc }}
+
-
- At higher levels.
-
- {{ higher }}
-
-
-
-
-
-
- {{ levels[spell.level] }}
- {{ spell.school }}
-
+
+ At higher levels.
+
+ {{ higher }}
+
+
+
+
+
+
+ {{ levels[spell.level] }}
+ {{ spell.school }}
+
-
- Casting time: {{ spell.cast_time_nr }} {{spell.cast_time_type}}
- Range: {{ (spell.range_type == "Ranged") ? spell.range + " feet" : spell.range_type}}
- Components:
-
- {{ val ? component.charAt(0).toUpperCase() : ""}}
-
- ({{ spell.material_desciption }})
-
- Duration: {{ spell.duration_type }}
- Classes:
-
- {{ _class }},
-
-
-
-
-
-
-
+
+ Casting time: {{ spell.cast_time_nr }} {{ spell.cast_time_type }}
+ Range:
+ {{ spell.range_type == "Ranged" ? spell.range + " feet" : spell.range_type }}
+ Components:
+
+ {{ val ? component.charAt(0).toUpperCase() : "" }}
+
+ ({{ spell.material_desciption }})
+
+ Duration: {{ spell.duration_type }}
+ Classes:
+
+ {{ _class }},
+
+
+
+
+
+
+
\ No newline at end of file
+ i {
+ font-size: 15px;
+ }
+}
+
diff --git a/src/components/hk-components/hk-action-rolls/hk-action-roll-form.vue b/src/components/hk-components/hk-action-rolls/hk-action-roll-form.vue
new file mode 100644
index 000000000..78a2d4094
--- /dev/null
+++ b/src/components/hk-components/hk-action-rolls/hk-action-roll-form.vue
@@ -0,0 +1,452 @@
+
+
+
+
+ Description
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ignore this roll for the {{ key }} option.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The damage counts as magical to overcome resistances to non
+ magical bludgeoning, piercing or slashing damage.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add primary stat modifier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/hk-components/hk-action-rolls/hk-action-roll-scaling.vue b/src/components/hk-components/hk-action-rolls/hk-action-roll-scaling.vue
new file mode 100644
index 000000000..b795a0ab1
--- /dev/null
+++ b/src/components/hk-components/hk-action-rolls/hk-action-roll-scaling.vue
@@ -0,0 +1,223 @@
+
+
+
+
+ At higher levels description
+ {{ spell.higher_level }}
+
+
+
+
+
+
+ $set(level_tier, 'level', value != undefined ? parseInt(value) : value)
+ "
+ />
+
+
+
+
+
+
+ $set(level_tier, 'dice_count', value != undefined ? parseInt(value) : value)
+ "
+ >
+ d{{ roll.dice_type }}
+
+
+
+
+
+
+ $set(level_tier, 'fixed_val', value != undefined ? parseInt(value) : value)
+ "
+ />
+
+
+
+
+
+
+ $set(level_tier, 'projectile_count', value != undefined ? parseInt(value) : value)
+ "
+ />
+
+
+
+
+
+
+
+ Generated description
+ {{ scalingDesc(scaling, spell.scaling, spell.level, roll.dice_type) }}
+
+
+
+
+
+
+
diff --git a/src/components/hk-components/hk-action-rolls/hk-action-rolls-table.vue b/src/components/hk-components/hk-action-rolls/hk-action-rolls-table.vue
new file mode 100644
index 000000000..b54e2a103
--- /dev/null
+++ b/src/components/hk-components/hk-action-rolls/hk-action-rolls-table.vue
@@ -0,0 +1,209 @@
+
+
+
+
+ {{ calcAverage(data.row.dice_type, data.row.dice_count, data.row.fixed_val) }}
+ ({{ data.row.dice_count || "" }}{{ data.row.dice_type ? `d${data.row.dice_type}` : `` }}
+
+ {{
+ (data.row.fixed_val < 0) ? `- ${Math.abs(data.row.fixed_val)}` : `+ ${data.row.fixed_val}`
+
+
+
+
+
+ }})
+
+ {{ data.row.fixed_val }})
+
+ {{ versatileOptions[0] || "Enter versatile option" }}
+
+
+
+ |
+ {{
+ calcAverage(
+ data.row.versatile_dice_type || data.row.dice_type,
+ data.row.versatile_dice_count || data.row.dice_count,
+ data.row.versatile_fixed_val || data.row.fixed_val
+ )
+ }}
+ ({{ versatileRoll(data.row) }})
+
+ {{ versatileOptions[1] || "Enter versatile option" }}
+
+
+
+
+
+
+
+
+
+
+
+
+ Healing
+
+
+
+
+ {{ data.row.damage_type.capitalize() }}
+
+ {{ versatileOptions[1] || "Enter versatile option" }}
+
+
+
+ |
+
+
+ {{ data.row.versatile_damage_type.capitalize() }}
+
+ {{ versatileOptions[1] || "Enter versatile option" }}
+
+
+
+ damage
+
+
+
+
+
+ Magical
+
+
+
+
+ {{
+ type === "save"
+ ? `Save: ${application[data.row.save_fail_mod]}`
+ : `Miss: ${application[data.row.miss_mod]}`
+ }}
+
+
+
+
+
+
+
+
diff --git a/src/components/hk-components/hk-action-rolls/hk-roll-action.vue b/src/components/hk-components/hk-action-rolls/hk-roll-action.vue
new file mode 100644
index 000000000..92f853207
--- /dev/null
+++ b/src/components/hk-components/hk-action-rolls/hk-roll-action.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+ {{ action.name }}
+
+
+
+
+
+ {{ i + 1 }}
+
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ action.name }}
+
+
+
+
+
+ {{ i + 1 }}
+
+ {{ getVersatile(action, i) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/hk-components/hk-markdown-editor.vue b/src/components/hk-components/hk-markdown-editor.vue
index 746cc5e04..4482fd846 100644
--- a/src/components/hk-components/hk-markdown-editor.vue
+++ b/src/components/hk-components/hk-markdown-editor.vue
@@ -1,36 +1,35 @@
-
+
+
\ No newline at end of file
+}
+
diff --git a/src/components/hk-components/hk-roll.vue b/src/components/hk-components/hk-roll.vue
index f97a34a37..1b29b8ce6 100644
--- a/src/components/hk-components/hk-roll.vue
+++ b/src/components/hk-components/hk-roll.vue
@@ -1,18 +1,26 @@
-
-
+
- {{ tooltip }} {{ Object.keys(advantage).length === 1 ? `with ${Object.keys(advantage)[0]}` : `` }}
+ {{ tooltip }}
+ {{ Object.keys(advantage).length === 1 ? `with ${Object.keys(advantage)[0]}` : `` }}
-
+
@@ -20,16 +28,10 @@
-
+
Advantage
-
+
Disadvantage
@@ -39,130 +41,136 @@
\ No newline at end of file
+ },
+ emit(e, advantage_disadvantage) {
+ this.$emit("roll", { e, advantage_disadvantage });
+ },
+ },
+};
+
diff --git a/src/components/hk-components/hk-rolls/hk-single-roll.vue b/src/components/hk-components/hk-rolls/hk-single-roll.vue
index ab57574e4..ec595c470 100644
--- a/src/components/hk-components/hk-rolls/hk-single-roll.vue
+++ b/src/components/hk-components/hk-rolls/hk-single-roll.vue
@@ -181,7 +181,7 @@
-
+
Magical
@@ -196,7 +196,7 @@
/>
- {{ rolled.modifierRoll.roll }}
+ {{ rolled.rollResult.roll }}
@@ -207,25 +207,25 @@
Crit!
{{ crit_description[critSettings] }}
- {{ rolled.modifierRoll.roll }}
+ {{ rolled.rollResult.roll }}
-
+
- {{ rolled.modifierRoll.mod }}
+ {{ rolled.rollResult.mod }}
-
+
-
- Scale ({{ selectedLevel }}): {{ rolled.scaledRoll.roll }} =
- {{ rolled.scaledRoll.total }}
- {{ rolled.scaledRoll.throws }}
-
-
- Scale ({{ selectedLevel }}): {{ rolled.scaledRoll.roll }} =
- {{ rolled.scaledRoll.total }}
- {{ rolled.scaledRoll.throws }}
-
Successful saving throw: {{ missSaveEffect(rolled.missSave, "text") }}
@@ -280,11 +268,7 @@
Final result
- (
- + {{ rolled.scaledRoll.total }} )
+ (
)
Add
-
- Add
-
+ Add
-
+
{{ name }}
@@ -39,9 +38,16 @@
Add {{ name_single }}
-
+
-
+
-
-
-
-
-
-
- {{ ability.name }}
-
-
-
-
- {{ i + 1 }}
-
- {{ getVersatile(ability, i) }}
-
-
-
-
-
-
-
+
+
-
+
{{ ability.name }}
- {{ ability.recharge ? `(Recharge ${ability.recharge === 'rest' ? "after a Short or Long Rest" : ability.recharge})` : `` }}
- {{ ability.limit ? `(${ability.limit}/${ability.limit_type ? ability.limit_type.capitalize() : `Day`})` : `` }}
- {{ ability.legendary_cost > 1 ? `(Costs ${ability.legendary_cost} Actions)` : `` }}
+ {{
+ ability.recharge
+ ? `(Recharge ${
+ ability.recharge === "rest"
+ ? "after a Short or Long Rest"
+ : ability.recharge
+ })`
+ : ``
+ }}
+ {{
+ ability.limit
+ ? `(${ability.limit}/${
+ ability.limit_type ? ability.limit_type.capitalize() : `Day`
+ })`
+ : ``
+ }}
+ {{
+ ability.legendary_cost > 1
+ ? `(Costs ${ability.legendary_cost} Actions)`
+ : ``
+ }}
-
- Remove
-
+ Remove
-
+
-
+
-
+
-
+
Range & area of effect
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ Options allow you to create slightly different rolls for the actions and
+ choose to use this action with one of the options. Think of versatile
+ weapon attacks where you roll a different damage die for 1- or 2-handed
+ attacks.
-
+
+
-
+
Type of action
-
+
-
+
-
+
-
+
-
-
-
- {{ calcAverage(data.row.dice_type, data.row.dice_count, data.row.fixed_val) }}
- ({{ data.row.dice_count || '' }}{{ data.row.dice_type ? `d${data.row.dice_type}` : `` }}
-
- {{ (data.row.fixed_val < 0) ? `- ${Math.abs(data.row.fixed_val)}` : `+ ${data.row.fixed_val}` }})
-
- {{ data.row.fixed_val }})
-
- {{ ability.versatile_one || "Enter versatile option" }}
-
-
-
- | {{
- calcAverage(
- data.row.versatile_dice_type || data.row.dice_type,
- data.row.versatile_dice_count || data.row.dice_count,
- data.row.versatile_fixed_val || data.row.fixed_val)
- }}
- ({{ versatileRoll(data.row) }})
-
- {{ ability.versatile_two || "Enter versatile option" }}
-
-
-
-
-
-
- Healing
-
-
-
-
- {{ data.row.damage_type.capitalize() }}
-
- {{ ability.versatile_two || "Enter versatile option" }}
-
-
-
- |
-
- {{ data.row.versatile_damage_type.capitalize() }}
-
- {{ ability.versatile_two || "Enter versatile option" }}
-
-
-
- damage
-
-
-
-
-
-
- Magical
-
-
-
-
-
- {{
- action.type === "save"
- ? `Save: ${application[data.row.save_fail_mod]}`
- : `Miss: ${application[data.row.miss_mod]}`
- }}
-
-
-
-
-
+ :rolls="action.rolls"
+ :type="action.type"
+ :versatile="ability.versatile"
+ :versatile-options="[ability.versatile_one, ability.versatile_two]"
+ @edit="
+ editRoll(
+ $event,
+ ability_index,
+ ability,
+ category,
+ action_index,
+ action
+ )
+ "
+ @delete="deleteRoll($event, ability_index, category, action_index)"
+ />
@@ -513,24 +507,29 @@
-
+
-
- {{ npc[edit_action.category][edit_action.ability_index].desc }}
-
-
-
- Select an action type first
-
+
Select an action type first
@@ -541,347 +540,276 @@
diff --git a/src/components/slides/MarkdownInfo.vue b/src/components/slides/MarkdownInfo.vue
index de6c5663b..2b57a36ce 100644
--- a/src/components/slides/MarkdownInfo.vue
+++ b/src/components/slides/MarkdownInfo.vue
@@ -1,7 +1,5 @@
-
Description fields
-
Markdown guide
@@ -12,92 +10,68 @@
Character stats
- You can add stats of your character into the description.
- Simply enter the reference of the desired stat (as shown in the tables below) into the description field.
- The actual value will be shown when the description is displayed. The brackets are required.
+ You can add stats of your character into the description. Simply enter the reference of the
+ desired stat (as shown in the tables below) into the description field. The actual value
+ will be shown when the description is displayed. The brackets are required.
-
+
-
+
-
- Ability
-
-
- Reference
-
+ Ability
+ Reference
-
+
{{ stat }}
{{ ref }}
-
+
-
+
-
- Ability
-
-
- Reference
-
+ Ability
+ Reference
-
+
{{ stat }}
{{ ref }}
-
+
-
+
-
- Value
-
-
- Reference
-
+ Value
+ Reference
-
+
{{ stat }}
{{ ref }}
-
+
-
+
-
- Value
-
-
- Reference
-
+ Value
+ Reference
-
+
{{ stat }}
{{ ref }}
@@ -109,124 +83,125 @@
\ No newline at end of file
+export default {
+ name: "Descriptions",
+ data() {
+ return {
+ markdownInfo: {
+ emphasis: {
+ header: "Emphasis",
+ content:
+ "**Bold **\n" +
+ "*Italic *\n" +
+ "__Underline __\n" +
+ "~~Strikethrough ~~",
+ },
+ headers: {
+ header: "Headers",
+ content:
+ "# Big header\n" + "## Medium header\n" + "### Small header\n" + "#### Tiny header\n",
+ },
+ lists: {
+ header: "Lists",
+ content:
+ "* Unordered list item\n" +
+ "* Unordered list item\n" +
+ "* Unordered list item\n\n" +
+ "1. Ordered list item\n" +
+ "2. Ordered list item\n" +
+ "3. Ordered list item",
+ },
+ tables: {
+ header: "Tables",
+ content:
+ "| Column 1 | Column 2 | Column 3 |\n" +
+ "| -------- | -------- | -------- |\n" +
+ "| 5th | Human | Rogue |\n" +
+ "| 8th | Dwarf | Cleric |\n\n" +
+ "Column alignment is not required\n\n" +
+ "| Column 1 | Column 2 | Column 3 |\n" +
+ "| - | - | - |\n" +
+ "| 5th | Human | Rogue |\n" +
+ "| 8th | Dwarf | Cleric |",
+ },
+ },
+ ability_scores: [
+ {
+ stat: "Strength",
+ ref: "[str]",
+ },
+ {
+ stat: "Dexterity",
+ ref: "[dex]",
+ },
+ {
+ stat: "Constitution",
+ ref: "[con]",
+ },
+ {
+ stat: "Intelligence",
+ ref: "[int]",
+ },
+ {
+ stat: "Wisdom",
+ ref: "[wis]",
+ },
+ {
+ stat: "Charisma",
+ ref: "[cha]",
+ },
+ ],
+ ability_mods: [
+ {
+ stat: "Strength",
+ ref: "[str_mod]",
+ },
+ {
+ stat: "Dexterity",
+ ref: "[dex_mod]",
+ },
+ {
+ stat: "Constitution",
+ ref: "[con_mod]",
+ },
+ {
+ stat: "Intelligence",
+ ref: "[int_mod]",
+ },
+ {
+ stat: "Wisdom",
+ ref: "[wis_mod]",
+ },
+ {
+ stat: "Charisma",
+ ref: "[cha_mod]",
+ },
+ ],
+ character: [
+ {
+ stat: "Proficiency bonus",
+ ref: "[proficiency]",
+ },
+ {
+ stat: "Character level",
+ ref: "[character_level]",
+ },
+ ],
+ Class: [
+ {
+ stat: "Class level",
+ ref: "[class_level]",
+ },
+ {
+ stat: "Spell attack mod",
+ ref: "[spell_attack]",
+ },
+ {
+ stat: "Spell save DC",
+ ref: "[spell_save_dc]",
+ },
+ ],
+ };
+ },
+};
+
diff --git a/src/components/spells/Actions.vue b/src/components/spells/Actions.vue
new file mode 100644
index 000000000..f49c95be3
--- /dev/null
+++ b/src/components/spells/Actions.vue
@@ -0,0 +1,375 @@
+
+
+
+
+
+
+ Spell actions are the parts of a spell that can be rolled. By adding spell actions to your
+ spell, it can be used during encounters to quickly apply damage or healing.
+
+
+ parseToInt(value, spell, 'projectiles')"
+ >
+
+
+
+ Some spells, like Magic Missiles (phb 257) have multiple projectiles that can be
+ fired to different targets. For each projectile, every spell action is rolled.
+
+
+
+
+
+
+
+
+
+
+
+ Options allow you to create slightly different rolls for the actions and choose to use
+ this spell with one of the options. Think of versatile weapon attacks where you roll a
+ different damage die for 1- or 2-handed attacks.
+
+
+
+
+
+
+
+
+
+
+
+
+ Validation errors
+
+
+ {{ action.name }}
+
+
+
+ Remove
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/spells/BasicInfo.vue b/src/components/spells/BasicInfo.vue
new file mode 100644
index 000000000..4cf5bb541
--- /dev/null
+++ b/src/components/spells/BasicInfo.vue
@@ -0,0 +1,599 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ parseToInt(value, spell, 'cast_time')"
+ />
+
+
+
+
+
+
+ {
+ if (value !== 'reaction') $delete(spell, 'cast_time_react_desc');
+ }
+ "
+ :error="invalid && validated"
+ :error-message="errors[0]"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ if (value !== 'ranged') $delete(spell, 'range');
+ }
+ "
+ :error="invalid && validated"
+ :error-message="errors[0]"
+ />
+
+
+
+
+
+
+ parseToInt(value, spell, 'range')"
+ >
+ ft.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ if (!spell_duration_types_time.includes(value)) {
+ $delete(spell, 'duration');
+ $delete(spell, 'duration_scale');
+ }
+ }
+ "
+ :error="invalid && validated"
+ :error-message="errors[0]"
+ />
+
+
+
+
+
+
+ parseToInt(value, spell, 'duration')"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ if (value === 'none') $delete(spell, 'aoe_size');
+ }
+ "
+ :error="invalid && validated"
+ :error-message="errors[0]"
+ />
+
+
+
+
+ parseToInt(value, spell, 'aoe_size')"
+ :error="invalid && validated"
+ :error-message="errors[0]"
+ >
+ ft.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/userContent/Content.vue b/src/components/userContent/Content.vue
index fff40a599..92050c7b5 100644
--- a/src/components/userContent/Content.vue
+++ b/src/components/userContent/Content.vue
@@ -1,12 +1,12 @@
-
-
-
+
{{ content_count[type] }}
-
+
/
{{ tier.benefits[type] }}
@@ -33,65 +33,66 @@
\ No newline at end of file
+ .q-linear-progress {
+ position: absolute;
+ left: 0;
+ top: 0;
+ z-index: -1;
+ }
+}
+
diff --git a/src/components/userContent/SignedIn.vue b/src/components/userContent/SignedIn.vue
index a9913f467..8477bb6ad 100644
--- a/src/components/userContent/SignedIn.vue
+++ b/src/components/userContent/SignedIn.vue
@@ -1,12 +1,10 @@
-
+
- Thanks for creating an account!
-
- Now that you're signed in you can use our tools from your custom content page.
-
+ Thanks for creating an account!
+ Now that you're signed in you can use our tools from your custom content page.
-
\ No newline at end of file
+
diff --git a/src/components/userContent/Tier.vue b/src/components/userContent/Tier.vue
index 4ed97ade3..0a630cb5f 100644
--- a/src/components/userContent/Tier.vue
+++ b/src/components/userContent/Tier.vue
@@ -1,17 +1,28 @@
-
-
+
-
- {{ tier.benefits[key] }}
+
+ {{
+ tier.benefits[key]
+ }}
{{ benefit.title }}
@@ -22,23 +33,34 @@
-
-
+
+ content_count[storage_type] > tier.benefits[storage_type]
+ ? 'red'
+ : content_count[storage_type] === tier.benefits[storage_type]
+ ? 'neutral-2'
+ : 'green'
+ "
+ >
{{ tier.benefits[storage_type] }}
- {{ storage_type.slice(0, -1).capitalize() }}{{ (tier.benefits[storage_type] > 1 || tier.benefits[storage_type] === "infinite") ? "s" : "" }}
-
-
- (per campaign)
+ {{ storage_type.slice(0, -1).capitalize()
+ }}{{
+ tier.benefits[storage_type] > 1 || tier.benefits[storage_type] === "infinite"
+ ? "s"
+ : ""
+ }}
+ (per campaign)
@@ -47,75 +69,65 @@
\ No newline at end of file
+ li.storage {
+ padding-left: 35px;
+ background-color: $neutral-8;
+ }
+}
+
diff --git a/src/css/styles.scss b/src/css/styles.scss
index 16ce96f96..2768ee0a5 100644
--- a/src/css/styles.scss
+++ b/src/css/styles.scss
@@ -745,6 +745,48 @@ html body {
}
}
}
+ .gap {
+ &-1 {
+ gap: .5rem;
+ }
+ &-2 {
+ gap: 1rem;
+ }
+ &-3 {
+ gap: 1.5rem;
+ }
+ &-4 {
+ gap: 2rem;
+ }
+ &-x {
+ &-1 {
+ column-gap: .5rem;
+ }
+ &-2 {
+ column-gap: 1rem;
+ }
+ &-3 {
+ column-gap: 1.5rem;
+ }
+ &-4 {
+ column-gap: 2rem;
+ }
+ }
+ &-y {
+ &-1 {
+ row-gap: .5rem;
+ }
+ &-2 {
+ row-gap: 1rem;
+ }
+ &-3 {
+ row-gap: 1.5rem;
+ }
+ &-4 {
+ row-gap: 2rem;
+ }
+ }
+ }
.justify-content {
&-between {
justify-content: space-between;
diff --git a/src/functions.js b/src/functions.js
index 701b6ae48..7c04b4380 100644
--- a/src/functions.js
+++ b/src/functions.js
@@ -32,6 +32,12 @@ Number.prototype.between = function (min = 0, max = Infinity) {
return Math.max(Math.min(this, max), min);
};
+Number.prototype.toOrdinal = function () {
+ const s = ["th", "st", "nd", "rd"];
+ const v = this % 100;
+ return this + (s[(v - 20) % 10] || s[v] || s[0]);
+};
+
Array.min = function (array) {
return Math.min.apply(Math, array);
};
diff --git a/src/layouts/authenticated.vue b/src/layouts/authenticated.vue
index e09956167..b96093705 100644
--- a/src/layouts/authenticated.vue
+++ b/src/layouts/authenticated.vue
@@ -4,9 +4,9 @@
-
-
-
+
+
+
@@ -19,52 +19,49 @@
\ No newline at end of file
+ },
+};
+
diff --git a/src/layouts/character-builder.vue b/src/layouts/character-builder.vue
index 1b4009e90..6fd65cc87 100644
--- a/src/layouts/character-builder.vue
+++ b/src/layouts/character-builder.vue
@@ -1,27 +1,20 @@
-
-
-
+
+
-
+
\ No newline at end of file
+ next();
+ },
+};
+
diff --git a/src/layouts/default.vue b/src/layouts/default.vue
index a70b9fd63..1d5458511 100644
--- a/src/layouts/default.vue
+++ b/src/layouts/default.vue
@@ -1,74 +1,72 @@
-
-
-
-
-
-
-
-
-
-
-
Combat Tracker
+
+
+
+
+
+
+
+
+
+
Combat Tracker
+
Harmless Key
-
Harmless Key
+
Try demo
-
Try demo
+
+
+
-
-
-
+
+
+
-
-
\ No newline at end of file
+ },
+};
+
diff --git a/src/mixins/dice.js b/src/mixins/dice.js
index 2f5b78814..05395cdf6 100644
--- a/src/mixins/dice.js
+++ b/src/mixins/dice.js
@@ -187,23 +187,27 @@ export const dice = {
/**
* Roll any spell or monster action
*
- * @param {object} e Event, holds info for advantage/disadvantege
+ * @param {object} e Event, holds info for advantage/disadvantage
* @param {object} ability Full ability object
- * @param {object} config Holds configuration options {type, castLevel, casterLevel, toHitModifier, versatile}
+ * @param {object} config Holds configuration options {type, cast_level, caster_level, toHitModifier, versatile}
*
* @returns {object}
*/
rollAction(e, ability, config = {}) {
let returnRoll = {
- name: ability.name,
+ name: this.getAbilityName(ability, config),
actions: [],
};
- if (config.versatile !== undefined) {
- returnRoll.name =
- config.versatile === 0
- ? `${ability.name} (${ability.versatile_one || "Option 1"})`
- : `${ability.name} (${ability.versatile_two || "Option 2"})`;
+ if (config.option !== undefined) {
+ if (ability.options && ability.options.length) {
+ returnRoll.name = `${ability.name} (${config.option})`;
+ } else if (ability.versatile) {
+ returnRoll.name =
+ config.option === 0
+ ? `${ability.name} (${ability.versatile_one || "Option 1"})`
+ : `${ability.name} (${ability.versatile_two || "Option 2"})`;
+ }
}
// Check for advantage/disadvantage in the $event
@@ -216,14 +220,12 @@ export const dice = {
}
const actions = ability.action_list; // All actions in the ability
- const scaleType = ability.level_scaling; // Only for spells
- const spellLevel = ability.level; // Only for spells
let i = 0;
// LOOP OVER ALL ACTIONS
for (let action of actions) {
let type = action.type;
- let attack_bonus = action.attack_bonus || config.toHitModifier;
+ let attack_bonus = action.attack_bonus || config.attack_bonus;
let toHit = false;
let crit = false;
returnRoll.actions[i] = { type, rolls: [] };
@@ -240,7 +242,7 @@ export const dice = {
false,
advantage_object
);
- if (returnRoll.actions[i].toHit.throwsTotal === 20) {
+ if (returnRoll.actions[i].toHit.throwsTotal >= 20) {
returnRoll.actions[i].crit = true;
crit = true;
}
@@ -249,115 +251,101 @@ export const dice = {
// For a saving throw set the ability and DC for display
if (type === "save") {
returnRoll.actions[i].save_ability = action.save_ability;
- returnRoll.actions[i].save_dc = action.save_dc;
+ returnRoll.actions[i].save_dc = action.save_dc || 10;
}
- for (let modifier of action.rolls) {
- let damage_type = modifier.damage_type;
- let dice_type = modifier.dice_type;
- let dice_count = modifier.dice_count;
- let fixed_val = modifier.fixed_val ? modifier.fixed_val : 0;
- let modifierRoll = undefined;
- let scaledRoll = undefined;
- let scaledModifier = undefined;
- let missSave = toHit ? modifier.miss_mod : modifier.save_fail_mod; //what happens on miss/failed save
+ for (const roll of action.rolls) {
+ // Create editable Roll object with important props from roll object via anonymous function
+ let editableRoll = (({ damage_type, dice_count, dice_type, fixed_val }) => ({
+ damage_type,
+ dice_count,
+ dice_type,
+ fixed_val,
+ }))(roll);
+
+ const missSave = toHit ? roll.miss_mod : roll.save_fail_mod; //what happens on miss/failed save
const special =
- !modifier.special || modifier.length === 0
+ !roll.special || roll.length === 0
? undefined
- : modifier.special && Array.isArray(modifier.special)
- ? modifier.special
- : [modifier.special];
- let magical = !!modifier.magical;
-
+ : roll.special && Array.isArray(roll.special)
+ ? roll.special
+ : [roll.special];
+ let magical = !!roll.magical;
+
+ // Check for options
+ if (ability.options && config.option && roll.options && roll.options[config.option]) {
+ const option = roll.options[config.option];
+ if (option.ignore) {
+ editableRoll = {};
+ } else {
+ editableRoll.damage_type = option.damage_type || editableRoll.damage_type;
+ editableRoll.dice_type = option.dice_type || editableRoll.dice_type;
+ editableRoll.dice_count = option.dice_count || editableRoll.dice_count;
+ editableRoll.fixed_val = option.fixed_val || editableRoll.fixed_val;
+ magical = option.magical;
+ }
+ }
// Check for versatile. 1 is the alternative option
// Changes only have to be made if the versatile roll is the alternative (1)
- if (config.versatile === 1) {
- damage_type = modifier.versatile_damage_type
- ? modifier.versatile_damage_type
- : damage_type;
- dice_type = modifier.versatile_dice_type ? modifier.versatile_dice_type : dice_type;
- dice_count = modifier.versatile_dice_count ? modifier.versatile_dice_count : dice_count;
- fixed_val = modifier.versatile_fixed_val ? modifier.versatile_fixed_val : fixed_val;
- magical = modifier.versatile_magical;
+ else if (ability.versatile && config.option === 1) {
+ editableRoll.damage_type = roll.versatile_damage_type || editableRoll.damage_type;
+ editableRoll.dice_type = roll.versatile_dice_type || editableRoll.dice_type;
+ editableRoll.dice_count = roll.versatile_dice_count || editableRoll.dice_count;
+ editableRoll.fixed_val = roll.versatile_fixed_val || editableRoll.fixed_val;
+ magical = roll.versatile_magical;
}
- dice_count = this.getDiceCount(crit, dice_count);
// Check if the action scales with the current roll
- let tiers = modifier.level_tiers;
- if (tiers) {
- scaledModifier = this.__levelScaling__(
- tiers,
- config.castLevel,
- spellLevel,
- config.casterLevel,
- scaleType
- );
-
- // Roll the scaledModifier
- if (scaledModifier) {
- scaledModifier.dice_count = this.getDiceCount(crit, scaledModifier.dice_count);
- if (scaledModifier.dice_type && scaledModifier.dice_count)
- scaledRoll = this.rollD(
- e.e,
- scaledModifier.dice_type,
- scaledModifier.dice_count,
- scaledModifier.fixed_val
- );
- // When there is nothing to roll, but only a fixed value
- // still a roll must be created
- else
- scaledRoll = {
- title: ability.name,
- roll: fixed_val,
- mod: fixed_val,
- throws: [],
- throwsTotal: 0,
- total: parseInt(fixed_val),
- };
- }
+ const tiers = roll.scaling;
+ if (roll.scaling) {
+ editableRoll = this.__levelScaling__(tiers, editableRoll, ability, config);
}
- // Roll the modifier
- // If the modifier scales with character level, overwrite the modifierRoll with the scaledRoll
- if (scaleType === "character_level" && scaledModifier) {
- modifierRoll = scaledRoll;
- scaledRoll = undefined; //Only return the scaled modifierRoll
- } else {
- if (dice_type && dice_count)
- modifierRoll = this.rollD(e.e, dice_type, dice_count, fixed_val, `${ability.name}`);
+ // check crits
+ editableRoll.dice_count = this.getDiceCount(crit, editableRoll.dice_count);
+
+ // Roll the roll
+ let rollResult = undefined;
+ if (editableRoll.dice_type && editableRoll.dice_count) {
+ rollResult = this.rollD(
+ e.e,
+ editableRoll.dice_type,
+ editableRoll.dice_count,
+ editableRoll.fixed_val,
+ `${editableRoll.name}`
+ );
// When there is nothing to roll, but only a fixed value
// still a roll must be created
- else
- modifierRoll = {
- title: ability.name,
- roll: fixed_val,
- mod: fixed_val,
- throws: [],
- throwsTotal: 0,
- total: parseInt(fixed_val),
- };
+ } else {
+ rollResult = {
+ title: editableRoll.name,
+ roll: editableRoll.fixed_val,
+ mod: editableRoll.fixed_val,
+ throws: [],
+ throwsTotal: 0,
+ total: parseInt(editableRoll.fixed_val),
+ };
}
// Double the rolled damage (without the modifier [throwsTotal])
- // simply add [trhowsTotal] once more to the [total]
+ // simply add [throwsTotal] once more to the [total]
// Only when it's a crit and crit settings are set to double
if (crit && this.critSettings === "double") {
- modifierRoll.total = modifierRoll.total + modifierRoll.throwsTotal;
- const { n, d, m, s } = modifierRoll;
- modifierRoll.roll = m !== 0 ? `2x(${n}d${d})${s}${m}` : `2x(${n}d${d})`;
+ rollResult.total = rollResult.total + rollResult.throwsTotal;
+ const { n, d, m, s } = rollResult;
+ rollResult.roll = m !== 0 ? `2x(${n}d${d})${s}${m}` : `2x(${n}d${d})`;
}
// Add the max damage output of the roll to the total when crit and setting is max
if (crit && this.critSettings === "max") {
- const { n, d, m, s } = modifierRoll;
- modifierRoll.total = modifierRoll.total + n * d;
- modifierRoll.roll = m !== 0 ? `(${n}d${d}+${n * d})${s}${m}` : `(${n}d${d}+${n * d}) `;
+ const { n, d, m, s } = rollResult;
+ rollResult.total = rollResult.total + n * d;
+ rollResult.roll = m !== 0 ? `(${n}d${d}+${n * d})${s}${m}` : `(${n}d${d}+${n * d}) `;
}
// Push the rolled modifier to the array with all rolled modifiers
returnRoll.actions[i].rolls.push({
- modifierRoll,
- damage_type,
- scaledRoll,
+ rollResult,
+ damage_type: editableRoll.damage_type,
missSave,
magical,
special,
@@ -367,48 +355,40 @@ export const dice = {
}
return returnRoll;
},
- __levelScaling__(tiers, castLevel, spellLevel, casterLevel, scaleType) {
- let scaledModifier = undefined;
-
+ __levelScaling__(tiers, roll, ability, config) {
// SPELL SCALE
- if (scaleType === "spell_scale") {
- let scale = tiers[0].level;
- let dice_type = tiers[0].dice_type;
- let dice_count = tiers[0].dice_count;
- let fixed_val = tiers[0].fixed_val;
+ if (ability.scaling === "spell_scale") {
+ const scale = tiers[0].level;
+ const dice_count = tiers[0].dice_count;
+ const fixed_val = tiers[0].fixed_val;
// Calculate the increase based on spell level, on what level the spell is cast and the scale
- let increase = parseInt(Math.floor((castLevel - spellLevel) / scale));
+ const increase = parseInt(Math.floor((config.cast_level - ability.level) / scale));
// If there is an increase,
if (increase) {
- scaledModifier = {};
- scaledModifier.dice_count = increase * dice_count;
- scaledModifier.dice_type = dice_type;
-
- // Check if there is a fixed value
- // If there is one, add it for every scale level
- // If the spell scales with 1 and is cast 3 levels higer, multiply the fixed value by 3
- scaledModifier.fixed_val = fixed_val ? increase * fixed_val : 0;
+ roll.dice_count += increase * dice_count;
+ if (roll.fixed_val) roll.fixed_val += fixed_val ? increase * fixed_val : 0;
}
}
// CHARACTER LEVEL
- if (scaleType === "character_level") {
+ else if (ability.scaling === "character_level") {
tiers.sort((a, b) => (parseInt(a.level) > parseInt(b.level) ? 1 : -1));
for (let tier of tiers) {
- if (parseInt(casterLevel) >= parseInt(tier.level)) {
- scaledModifier = {
- dice_count: tier.dice_count,
- dice_type: tier.dice_type,
- };
- if (tier.fixed_val) {
- scaledModifier.fixed_val = tier.fixed_val;
- }
+ if (parseInt(config.caster_level) >= parseInt(tier.level)) {
+ roll.dice_count = tier.dice_count;
+ roll.fixed_val = tier.fixed_val || 0;
}
}
}
- return scaledModifier;
+ return roll;
+ },
+ getAbilityName(ability, config) {
+ if (config.cast_level) {
+ return `${ability.name} (${config.cast_level.toOrdinal()})`;
+ }
+ return ability.name;
},
animateValue(id, start, end, duration) {
if (start === end) return;
diff --git a/src/mixins/runEncounter.js b/src/mixins/runEncounter.js
index 4f02b534a..0812a8249 100644
--- a/src/mixins/runEncounter.js
+++ b/src/mixins/runEncounter.js
@@ -12,11 +12,20 @@ export const runEncounter = {
methods: {
...mapActions(["setActionRoll", "set_limitedUses"]),
...mapActions("campaigns", ["set_share"]),
- roll_action({ e, action_index, action, category, entity, targets, versatile = undefined }) {
+ roll_action({
+ e,
+ action_index,
+ action,
+ category,
+ entity,
+ targets,
+ projectiles = 1,
+ option = undefined,
+ }) {
let roll;
const config = {
type: "monster_action",
- versatile,
+ option,
};
// Roll once for AOE
diff --git a/src/mixins/spells.js b/src/mixins/spells.js
deleted file mode 100644
index 7bf5ce84e..000000000
--- a/src/mixins/spells.js
+++ /dev/null
@@ -1,112 +0,0 @@
-export const spells = {
- data() {
- return {
- spell_levels: [
- {
- value: 0,
- label: "Cantrip"
- },
- {
- value: 1,
- label: "1st"
- },
- {
- value: 2,
- label: "2nd"
- },
- {
- value: 3,
- label: "3rd"
- },
- {
- value: 4,
- label: "4th"
- },
- {
- value: 5,
- label: "5th"
- },
- {
- value: 5,
- label: "6th"
- },
- {
- value: 7,
- label: "7th"
- },
- {
- value: 8,
- label: "8th"
- },
- {
- value: 9,
- label: "9th"
- },
- ],
- spell_schools: [
- { label: "Abjuration", value: "abjuration" },
- { label: "Conjuration", value: "conjuration" },
- { label: "Divination", value: "divination" },
- { label: "Enchantment", value: "enchantment" },
- { label: "Evocation", value: "evocation" },
- { label: "Illusion", value: "illusion" },
- { label: "Necromancy", value: "necromancy" },
- { label: "Transmutation", value: "transmutation" },
- ],
- spell_components: [
- { label: "Verbal", value: "verbal" },
- { label: "Somatic", value: "somatic" },
- { label: "Material", value: "material" }
- ],
- spell_cast_time_types: [
- { label: "Action", value: "action" },
- { label: "Bonus Action", value: "bonus_action" },
- { label: "Reaction", value: "reaction" },
- { label: "Minute", value: "minute" },
- { label: "Hour", value: "hour" },
- { label: "No Action", value: "no_action" },
- { label: "Special", value: "special" },
- ],
- spell_range_types: [
- { label: "Self", value: "self" },
- { label: "Touch", value: "touch" },
- { label: "Ranged", value: "ranged" },
- { label: "Sight", value: "sight" },
- { label: "Unlimited", value: "unlimited" },
- ],
- spell_duration_types: [
- { label: "Concentration", value: "concentration" },
- { label: "Instantaneous", value: "instantaneous" },
- { label: "Special", value: "special" },
- { label: "Time", value: "time" },
- { label: "Until Dispelled", value: "until_dispelled" },
- { label: "Until Dispelled or Triggered", value: "until_dispelled_or_triggered" },
- ],
- spell_duration_types_time: [ "concentration", "time" ],
- spell_duration_times: [
- { label: "Round", value: "round" },
- { label: "Minute", value: "minute" },
- { label: "Hour", value: "hour" },
- { label: "Day", value: "day" },
- ],
- aoe_types: [
- { label: "None", value: "none" },
- { label: "Cone", value: "cone" },
- { label: "Cube", value: "cube" },
- { label: "Cylinder", value: "cylinder" },
- { label: "Line", value: "line" },
- { label: "Sphere", value: "sphere" },
- { label: "Square", value: "square" },
- { label: "Square Feet", value: "square feet" },
- ],
- level_scaling: [
- { label: "None", value: "none" },
- { label: "Character Level", value: "character_level" },
- { label: "Spell Scale", value: "spell_scale" },
- { label: "Spell Level", value: "spell_level" },
- ],
- }
- },
- methods: {
- }
-}
diff --git a/src/router/routes.js b/src/router/routes.js
index 6e82f6fc0..ffc6d4c9b 100644
--- a/src/router/routes.js
+++ b/src/router/routes.js
@@ -354,6 +354,48 @@ const routes = [
],
},
+ // Spells
+ {
+ path: "spells",
+ component: {
+ render(c) {
+ return c("router-view");
+ },
+ },
+ meta: {
+ title: "Spells",
+ },
+ children: [
+ {
+ path: "",
+ name: "Spells",
+ component: () => import("src/views/UserContent/Spells/Spells.vue"),
+ meta: {
+ title: "Spells",
+ description: "Your custom spells on Harmless Key.",
+ },
+ },
+ {
+ path: "add-spell",
+ name: "Add spell",
+ component: () => import("src/views/UserContent/Spells/EditSpell.vue"),
+ meta: {
+ title: "Add spell",
+ description: "Create a new spell on Harmless Key.",
+ },
+ },
+ {
+ path: ":id",
+ name: "Edit spell",
+ component: () => import("src/views/UserContent/Spells/EditSpell.vue"),
+ meta: {
+ title: "Edit spell",
+ description: "Edit an existing spell on Harmless Key.",
+ },
+ },
+ ],
+ },
+
// Reminders
{
path: "reminders",
@@ -536,7 +578,7 @@ const routes = [
meta: {
title: "D&D 5e Tools",
description:
- "Online tools for D&D 5e. \nCombat Tracker \nEncounter Builder \nMonster builder \nCharacter Builder \nCompendium",
+ "Online tools for D&D 5e. \nCombat Tracker \nEncounter Builder \nMonster builder \nSpell creator \nCharacter Builder \nCompendium",
},
},
{
@@ -592,7 +634,7 @@ const routes = [
},
},
meta: {
- title: "D&D 5eMonster creator",
+ title: "D&D 5e Monster creator",
description:
"An advanced monster creator for D&D 5e. Create a stat block with easy to roll actions.",
},
@@ -618,6 +660,41 @@ const routes = [
},
],
},
+ {
+ path: "spell-creator",
+ component: {
+ render(c) {
+ return c("router-view");
+ },
+ },
+ meta: {
+ title: "D&D 5e Spell creator",
+ description:
+ "Create spells for D&D 5e to roll directly or use in your custom spellcaster monsters.",
+ },
+ children: [
+ {
+ path: "",
+ name: "ToolsSpellCreator",
+ component: () => import("src/views/Tools/SpellCreator"),
+ meta: {
+ title: "Dungeons & Dragons Spell Creator",
+ description:
+ "Create spells for D&D 5e to roll directly or use in your custom spellcaster monsters.",
+ },
+ },
+ {
+ path: "create-spell",
+ name: "ToolsCreateSpell",
+ component: () => import("src/views/UserContent/Spells/EditSpell"),
+ meta: {
+ title: "Create spell",
+ description:
+ "Create your custom D&D 5e spell to roll directly or use in your custom spellcaster monsters.",
+ },
+ },
+ ],
+ },
{
path: "character-builder",
name: "ToolsCharacterBuilder",
@@ -1048,7 +1125,7 @@ const routes = [
children: [
{
path: "",
- name: "Spells",
+ name: "ContributeSpells",
component: () => import("src/views/Contribute/Spells.vue"),
},
{
@@ -1069,7 +1146,7 @@ const routes = [
},
{
path: "edit",
- name: "Edit spell",
+ name: "Edit contribute spell",
component: () => import("src/components/contribute/spell/edit.vue"),
},
],
diff --git a/src/schemas/hk-npc-schema.json b/src/schemas/hk-npc-schema.json
index 99feab8e3..228a7303a 100644
--- a/src/schemas/hk-npc-schema.json
+++ b/src/schemas/hk-npc-schema.json
@@ -9,6 +9,14 @@
"type": "string",
"maxLength": 100
},
+ "created": {
+ "title": "Created",
+ "type": "integer"
+ },
+ "updated": {
+ "title": "Updated",
+ "type": "integer"
+ },
"name": {
"title": "Name",
"type": "string",
@@ -477,9 +485,7 @@
"range": { "$ref": "#/$defs/action-range" },
"aoe_type": { "$ref": "#/$defs/action-aoe-type" },
"aoe_size": { "$ref": "#/$defs/action-aoe-size" },
- "versatile": { "$ref": "#/$defs/action-versatile" },
- "versatile_one": { "$ref": "#/$defs/action-versatile-one" },
- "versatile_two": { "$ref": "#/$defs/action-versatile-two" },
+ "options": { "$ref": "#/$defs/action-options" },
"action_list": { "$ref": "#/$defs/action-list" }
},
"required": ["name"]
@@ -501,9 +507,7 @@
"range": { "$ref": "#/$defs/action-range" },
"aoe_type": { "$ref": "#/$defs/action-aoe-type" },
"aoe_size": { "$ref": "#/$defs/action-aoe-size" },
- "versatile": { "$ref": "#/$defs/action-versatile" },
- "versatile_one": { "$ref": "#/$defs/action-versatile-one" },
- "versatile_two": { "$ref": "#/$defs/action-versatile-two" },
+ "options": { "$ref": "#/$defs/action-options" },
"action_list": { "$ref": "#/$defs/action-list" }
},
"required": ["name"]
@@ -538,9 +542,7 @@
"range": { "$ref": "#/$defs/action-range" },
"aoe_type": { "$ref": "#/$defs/action-aoe-type" },
"aoe_size": { "$ref": "#/$defs/action-aoe-size" },
- "versatile": { "$ref": "#/$defs/action-versatile" },
- "versatile_one": { "$ref": "#/$defs/action-versatile-one" },
- "versatile_two": { "$ref": "#/$defs/action-versatile-two" },
+ "options": { "$ref": "#/$defs/action-options" },
"action_list": { "$ref": "#/$defs/action-list" }
},
"required": ["name", "legendary_cost"]
@@ -562,9 +564,7 @@
"range": { "$ref": "#/$defs/action-range" },
"aoe_type": { "$ref": "#/$defs/action-aoe-type" },
"aoe_size": { "$ref": "#/$defs/action-aoe-size" },
- "versatile": { "$ref": "#/$defs/action-versatile" },
- "versatile_one": { "$ref": "#/$defs/action-versatile-one" },
- "versatile_two": { "$ref": "#/$defs/action-versatile-two" },
+ "options": { "$ref": "#/$defs/action-options" },
"action_list": { "$ref": "#/$defs/action-list" }
},
"required": ["name"]
@@ -677,7 +677,7 @@
"action-aoe-type": {
"title": "AOE type",
"type": "string",
- "enum": ["cone", "cube", "cylinder", "line", "sphere", "square", "square feet"]
+ "enum": ["cone", "cube", "cylinder", "line", "radius", "sphere", "square", "square feet"]
},
"action-aoe-size": {
"title": "AOE size",
@@ -685,19 +685,12 @@
"minimum": 0,
"maximum": 999
},
- "action-versatile": {
- "title": "versatile",
- "type": "boolean"
- },
- "action-versatile-one": {
- "title": "Versatile one",
- "type": "string",
- "maxLength": 25
- },
- "action-versatile-two": {
- "title": "Versatile two",
- "type": "string",
- "maxLength": 25
+ "action-options": {
+ "title": "Options",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
},
"action-list": {
"title": "Action list",
@@ -762,26 +755,43 @@
"minimum": -99,
"maximum": 99
},
- "versatile_damage_type": {
- "$ref": "#/$defs/damage-type-select"
- },
- "versatile_dice_count": {
- "title": "versatile dice count",
- "type": "integer",
- "minimum": 1,
- "maximum": 99
- },
- "versatile_dice_type": {
- "title": "Versatile dice type",
- "type": "integer",
- "minimum": 1,
- "maximum": 20
- },
- "versatile_fixed_val": {
- "title": "Versatile fixed val",
- "type": "integer",
- "minimum": -99,
- "maximum": 99
+ "options": {
+ "title": "Options",
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "^[A-Za-z0-9\\._%\\+-]+@[A-Za-z0-9\\.-]+\\.[A-Za-z]{2,6}$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "ignore": {
+ "title": "Ignore",
+ "type": "boolean"
+ },
+ "damage_type": {
+ "$ref": "#/$defs/damage-type-select"
+ },
+ "dice_count": {
+ "title": "Dice count",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 99
+ },
+ "dice_type": {
+ "title": "Dice type",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 20
+ },
+ "fixed_val": {
+ "title": "Fixed val",
+ "type": "integer",
+ "minimum": -99,
+ "maximum": 99
+ }
+ }
+ }
+ }
},
"projectile_count": {
"title": "Projectile count",
diff --git a/src/schemas/hk-spell-schema.json b/src/schemas/hk-spell-schema.json
new file mode 100644
index 000000000..1c94ecc86
--- /dev/null
+++ b/src/schemas/hk-spell-schema.json
@@ -0,0 +1,383 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Harmless Key Spell",
+ "description": "JSON Schema for importing Harmless Key spells",
+ "additionalProperties": false,
+ "type": "object",
+ "properties": {
+ "harmless_key": {
+ "type": "string",
+ "maxLength": 100
+ },
+ "created": {
+ "title": "Created",
+ "type": "integer"
+ },
+ "updated": {
+ "title": "Updated",
+ "type": "integer"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "source": {
+ "title": "Source",
+ "type": "string",
+ "maxLength": 30
+ },
+ "level": {
+ "title": "Source",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 9
+ },
+ "school": {
+ "type": "string",
+ "enum": [
+ "abjuration",
+ "conjuration",
+ "divination",
+ "enchantment",
+ "evocation",
+ "illusion",
+ "necromancy",
+ "transmutation"
+ ]
+ },
+ "cast_time_type": {
+ "type": "string",
+ "enum": ["action", "bonus_action", "reaction", "minute", "hour", "no_action", "special"]
+ },
+ "cast_time": {
+ "title": "Cast time",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 999
+ },
+ "cast_time_react_desc": {
+ "title": "Cast time reaction description",
+ "type": "string",
+ "maxLength": 200
+ },
+ "components": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["verbal", "somatic", "material"]
+ }
+ },
+ "material_description": {
+ "title": "Material description",
+ "type": "string",
+ "maxLength": 500
+ },
+ "range_type": {
+ "type": "string",
+ "enum": ["self", "touch", "ranged", "sight", "unlimited", "special"]
+ },
+ "range": {
+ "title": "Range",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 9999999
+ },
+ "classes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "bard",
+ "barbarian",
+ "cleric",
+ "druid",
+ "fighter",
+ "monk",
+ "paladin",
+ "ranger",
+ "rogue",
+ "sorcerer",
+ "warlock",
+ "wizard"
+ ]
+ }
+ },
+ "duration_type": {
+ "type": "string",
+ "enum": [
+ "concentration",
+ "instantaneous",
+ "special",
+ "time",
+ "until_dispelled",
+ "until_dispelled_or_triggered"
+ ]
+ },
+ "duration": {
+ "title": "Range",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 999
+ },
+ "duration_scale": {
+ "type": "string",
+ "enum": ["round", "minute", "hour", "day"]
+ },
+ "aoe_type": {
+ "title": "AOE type",
+ "type": "string",
+ "enum": [
+ "none",
+ "cone",
+ "cube",
+ "cylinder",
+ "line",
+ "radius",
+ "sphere",
+ "square",
+ "square feet"
+ ]
+ },
+ "aoe_size": {
+ "title": "AOE Size",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 99999
+ },
+ "ritual": {
+ "type": "boolean"
+ },
+ "scaling": {
+ "title": "Scaling",
+ "type": "string",
+ "enum": ["none", "character_level", "spell_scale", "spell_level"]
+ },
+ "description": {
+ "title": "Description",
+ "type": "string",
+ "maxLength": 5000
+ },
+ "higher_level": {
+ "title": "At higher levels",
+ "type": "string",
+ "maxLength": 1000
+ },
+ "projectiles": {
+ "title": "Projectiles",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 99
+ },
+ "projectile_scaling": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "level": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 20
+ },
+ "projectile_count": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 25
+ }
+ }
+ }
+ },
+ "options": {
+ "title": "Options",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "maxLength": 30
+ }
+ },
+ "actions": {
+ "title": "Action list",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string",
+ "maxLength": 100
+ },
+ "type": {
+ "title": "type",
+ "type": "string",
+ "enum": [
+ "melee_weapon",
+ "ranged_weapon",
+ "spell_attack",
+ "save",
+ "damage",
+ "healing",
+ "other"
+ ]
+ },
+ "save_ability": {
+ "$ref": "#/$defs/ability-select"
+ },
+ "rolls": {
+ "title": "Rolls",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "damage_type": {
+ "$ref": "#/$defs/damage-type-select"
+ },
+ "dice_count": {
+ "title": "Dice count",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 99
+ },
+ "dice_type": {
+ "title": "Dice type",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 20
+ },
+ "fixed_val": {
+ "title": "Fixed val",
+ "type": "integer",
+ "minimum": -99,
+ "maximum": 99
+ },
+ "primary": {
+ "title": "Primary stat modifier",
+ "type": "boolean"
+ },
+ "options": {
+ "title": "Options",
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "^[A-Za-z0-9\\._%\\+-]+@[A-Za-z0-9\\.-]+\\.[A-Za-z]{2,6}$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "ignore": {
+ "title": "Ignore",
+ "type": "boolean"
+ },
+ "damage_type": {
+ "$ref": "#/$defs/damage-type-select"
+ },
+ "dice_count": {
+ "title": "Dice count",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 99
+ },
+ "dice_type": {
+ "title": "Dice type",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 20
+ },
+ "fixed_val": {
+ "title": "Fixed val",
+ "type": "integer",
+ "minimum": -99,
+ "maximum": 99
+ }
+ }
+ }
+ }
+ },
+ "miss_mod": {
+ "title": "Miss modifier",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 2
+ },
+ "save_fail_mod": {
+ "title": "Save fail modifier",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 2
+ },
+ "special": {
+ "title": "special",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["siphon_full", "siphon_half", "drain"]
+ }
+ },
+ "scaling": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "level": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 20
+ },
+ "dice_count": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 99
+ },
+ "fixed_val": {
+ "type": "integer",
+ "minimum": -99,
+ "maximum": 99
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "required": [
+ "name",
+ "level",
+ "school",
+ "cast_time",
+ "cast_time_type",
+ "range_type",
+ "duration_type",
+ "aoe_type",
+ "scaling"
+ ],
+ "$defs": {
+ "ability-select": {
+ "type": "string",
+ "enum": ["strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma"]
+ },
+ "damage-type-select": {
+ "type": "string",
+ "enum": [
+ "acid",
+ "bludgeoning",
+ "cold",
+ "fire",
+ "force",
+ "lightning",
+ "necrotic",
+ "piercing",
+ "poison",
+ "psychic",
+ "radiant",
+ "slashing",
+ "thunder"
+ ]
+ }
+ }
+}
diff --git a/src/services/api/items.js b/src/services/api/items.js
index 78c5ed50d..1ec645315 100644
--- a/src/services/api/items.js
+++ b/src/services/api/items.js
@@ -3,43 +3,54 @@ import axios from "axios";
const ITEMS_REF = "/items";
export class itemServices {
- constructor() {
- this.HK = axios.create({
- baseURL: process.env.VUE_APP_HK_API_ROOT
- });
- }
-
- async getItems(query, pageNumber = 1, pageSize = 15, fields=["ALL"], sortBy = "name", descending=false) {
- const skip = (pageNumber - 1)*pageSize;
- const fieldsString = fields.join(" ");
- let params = `?skip=${skip}&limit=${pageSize}&fields=${fieldsString}`;
-
- if(sortBy) {
- params += `&sort=${sortBy}${descending ? ":desc" : ""}`;
- }
-
- if(query) {
- const queryParams = [];
-
- if(query.search) {
- queryParams.push(`name=${query.search}`);
- }
-
- params += `&${queryParams.join("&")}`;
- }
-
- return this.HK.get(ITEMS_REF + params).then((response) => {
- return response.data;
- }).catch((error) => {
- throw error;
- });
- }
-
- async getItem(id) {
- return this.HK.get(`${ITEMS_REF}/${id}`).then((response) => {
- return response.data;
- }).catch((error) => {
- throw error;
- });
- }
-}
\ No newline at end of file
+ constructor() {
+ this.HK = axios.create({
+ baseURL: process.env.VUE_APP_HK_API_ROOT,
+ });
+ }
+
+ async getItems(
+ query,
+ pageNumber = 1,
+ pageSize = 15,
+ fields = ["ALL"],
+ sortBy = "name",
+ descending = false
+ ) {
+ const skip = (pageNumber - 1) * pageSize;
+ const fieldsString = fields.join(" ");
+ let params = `?skip=${skip}&limit=${pageSize}&fields=${fieldsString}`;
+
+ if (sortBy) {
+ params += `&sort=${sortBy}${descending ? ":desc" : ""}`;
+ }
+
+ if (query) {
+ const queryParams = [];
+
+ if (query.search) {
+ queryParams.push(`name=${query.search}`);
+ }
+
+ params += `&${queryParams.join("&")}`;
+ }
+
+ return this.HK.get(ITEMS_REF + params)
+ .then((response) => {
+ return response.data;
+ })
+ .catch((error) => {
+ throw error;
+ });
+ }
+
+ async getItem(id) {
+ return this.HK.get(`${ITEMS_REF}/${id}`)
+ .then((response) => {
+ return response.data;
+ })
+ .catch((error) => {
+ throw error;
+ });
+ }
+}
diff --git a/src/services/api/spells.js b/src/services/api/spells.js
index 37befc64b..ea8cf8fad 100644
--- a/src/services/api/spells.js
+++ b/src/services/api/spells.js
@@ -4,41 +4,69 @@ const SPELLS_REF = "/spells";
export class spellServices {
constructor() {
- this.HK = axios.create({
- baseURL: process.env.VUE_APP_HK_API_ROOT
- });
- }
-
- async getSpells(query, pageNumber = 1, pageSize = 15, fields=["ALL"], sortBy = "name", descending=false) {
- const skip = (pageNumber - 1)*pageSize;
- const fieldsString = fields.join(" ");
- let params = `?skip=${skip}&limit=${pageSize}&fields=${fieldsString}`;
-
- if(sortBy) {
- params += `&sort=${sortBy}${descending ? ":desc" : ""}`;
- }
-
- if(query) {
- const queryParams = [];
-
- if(query.search) {
- queryParams.push(`name=${query.search}`);
- }
- params += `&${queryParams.join("&")}`;
- }
-
- return this.HK.get(SPELLS_REF + params).then((response) => {
- return response.data;
- }).catch((error) => {
- throw error;
- });
- }
-
- async getSpell(id) {
- return this.HK.get(`${SPELLS_REF}/${id}`).then((response) => {
- return response.data;
- }).catch((error) => {
- throw error;
- });
- }
-}
\ No newline at end of file
+ this.HK = axios.create({
+ baseURL: process.env.VUE_APP_HK_API_ROOT,
+ });
+ }
+
+ async getSpells(
+ query,
+ pageNumber = 1,
+ pageSize = 15,
+ fields = ["ALL"],
+ sortBy = "name",
+ descending = false
+ ) {
+ const skip = (pageNumber - 1) * pageSize;
+ const fieldsString = fields.join(" ");
+ let params = `?skip=${skip}&limit=${pageSize}&fields=${fieldsString}`;
+
+ if (sortBy) {
+ params += `&sort=${sortBy}${descending ? ":desc" : ""}`;
+ }
+
+ if (query) {
+ const queryParams = [];
+ console.log(query);
+
+ if (query.search) {
+ queryParams.push(`name=${query.search}`);
+ }
+ if (query.schools && query.schools.length) {
+ for (const school of query.schools) {
+ console.log(school);
+ queryParams.push(`school[]=${school}`);
+ }
+ }
+ if (query.classes && query.classes.length) {
+ for (const cls of query.classes) {
+ queryParams.push(`classes[]=${cls}`);
+ }
+ }
+ if (query.levels && query.levels.length) {
+ for (const lvl of query.levels) {
+ queryParams.push(`level[]=${lvl}`);
+ }
+ }
+ params += `&${queryParams.join("&")}`;
+ }
+
+ return this.HK.get(SPELLS_REF + params)
+ .then((response) => {
+ return response.data;
+ })
+ .catch((error) => {
+ throw error;
+ });
+ }
+
+ async getSpell(id) {
+ return this.HK.get(`${SPELLS_REF}/${id}`)
+ .then((response) => {
+ return response.data;
+ })
+ .catch((error) => {
+ throw error;
+ });
+ }
+}
diff --git a/src/services/spells.js b/src/services/spells.js
new file mode 100644
index 000000000..f51119323
--- /dev/null
+++ b/src/services/spells.js
@@ -0,0 +1,145 @@
+import { firebase, db } from "src/firebase";
+
+const SPELLS_REF = db.ref("spells");
+const SEARCH_SPELLS_REF = db.ref("search_spells");
+
+/**
+ * Spell Firebase Service
+ * CRUD interface implementation for Firebase
+ * Updates both 'custom_spells' and 'search_custom_spells' ref on CRUD
+ */
+export class SpellServices {
+ /**
+ * Get all the spells from the search_custom_spells reference
+ *
+ * @param {String} uid ID of active user
+ * @returns All the content of search_custom_spells reference
+ */
+ async getSpells(uid) {
+ try {
+ const spells = await SEARCH_SPELLS_REF.child(`${uid}/results`).once("value", (snapshot) => {
+ return snapshot;
+ });
+ return spells.val();
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ /**
+ * Get the number of spells that a user has from the search_custom_spells reference
+ *
+ * @param {String} uid ID of active user
+ * @returns Number of spells of a user
+ */
+ async getSpellCount(uid) {
+ try {
+ const path = `${uid}/metadata/count`;
+ let count = await SEARCH_SPELLS_REF.child(path).once("value");
+ return count.val();
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ /**
+ * Get an entire spell from 'custom_spells' reference
+ *
+ * @param {String} uid ID of active user
+ * @param {String} id ID of the requested spell
+ * @returns An entire spell from the 'custom_spells' reference
+ */
+ async getSpell(uid, id) {
+ try {
+ const spell = await SPELLS_REF.child(uid)
+ .child(id)
+ .once("value", (snapshot) => {
+ return snapshot;
+ });
+ return spell.val();
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ /**
+ * Adds an spell to the 'custom_spells' ref and the 'search_custom_spells' ref.
+ * Also updates the count metadata in 'search_custom_spells'
+ *
+ * @param {String} uid ID of active user
+ * @param {Object} spell Spell to add
+ * @param {Int} new_count Updated number of spells
+ * @param {Object} search_spell Compressed spell
+ * @returns Key of the newly added spell
+ */
+ async addSpell(uid, spell, search_spell) {
+ try {
+ spell.name = spell.name.toLowerCase();
+ const newSpell = await SPELLS_REF.child(uid).push(spell);
+
+ spell.created = firebase.database.ServerValue.TIMESTAMP;
+ spell.updated = firebase.database.ServerValue.TIMESTAMP;
+
+ // Update search_spells
+ SEARCH_SPELLS_REF.child(`${uid}/results/${newSpell.key}`).set(search_spell);
+
+ return newSpell.key;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ /**
+ * Updates an existing spell in both 'custom_spells' and 'search_custom_spells' ref
+ *
+ * @param {String} uid ID of active user
+ * @param {String} id ID of spell to edit
+ * @param {Object} spell Edited spell
+ * @param {Object} search_spell Compressed spell
+ */
+ async editSpell(uid, id, spell, search_spell) {
+ spell.name = spell.name.toLowerCase();
+ spell.updated = firebase.database.ServerValue.TIMESTAMP;
+
+ SPELLS_REF.child(uid)
+ .child(id)
+ .set(spell)
+ .then(() => {
+ SEARCH_SPELLS_REF.child(`${uid}/results/${id}`).set(search_spell);
+ })
+ .catch((error) => {
+ throw error;
+ });
+ }
+
+ /**
+ * Deletes an existing spell in both 'custom_spells' and 'search_custom_spells' ref
+ *
+ * @param {String} uid ID of active user
+ * @param {String} id ID of spell to edit
+ */
+ async deleteSpell(uid, id) {
+ try {
+ SPELLS_REF.child(uid).child(id).remove();
+
+ //Update search_custom_spells
+ SEARCH_SPELLS_REF.child(`${uid}/results`).child(id).remove();
+ return;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ /**
+ * Update spell_count in the search table of search_spells
+ *
+ * @param {String} uid User ID
+ * @param {Int} diff Difference to add or subtract from spell count
+ */
+ async updateSpellCount(uid, diff) {
+ const spell_count_path = `${uid}/metadata/count`;
+ let spell_count = await SEARCH_SPELLS_REF.child(spell_count_path).once("value");
+ await SEARCH_SPELLS_REF.child(spell_count_path).set(spell_count.val() + diff);
+ return spell_count.val() + diff;
+ }
+}
diff --git a/src/services/user.js b/src/services/user.js
index 028e6ee65..94bbb303e 100644
--- a/src/services/user.js
+++ b/src/services/user.js
@@ -8,126 +8,138 @@ const SEARCH_USERS_REF = db.ref("search_users");
const VOUCHER_HISTORY_REF = db.ref("voucher_history");
/**
-* User Firebase Service
-* CRUD interface implementation for Firebase
-* Updates 'users'
-*/
+ * User Firebase Service
+ * CRUD interface implementation for Firebase
+ * Updates 'users'
+ */
export class userServices {
+ /**
+ * Get the user object from firebase
+ *
+ * @param {String} uid ID of active user
+ * @returns Full user object from /users/uid
+ */
+ async getFullUser(uid) {
+ try {
+ const user = await USERS_REF.child(uid).once("value");
+ return user.val();
+ } catch (error) {
+ throw error;
+ }
+ }
- /**
- * Get the user object from firebase
- *
- * @param {String} uid ID of active user
- * @returns Full user object from /users/uid
- */
- async getFullUser(uid) {
- try {
- const user = await USERS_REF.child(uid).once('value');
- return user.val();
- } catch(error) {
- throw error;
- }
- }
+ /**
+ * Get the seasch_user object from firebase
+ *
+ * @param {String} uid ID of active user
+ * @returns Full user object from /search_user/uid
+ */
+ async getSearchUser(uid) {
+ try {
+ const user = await SEARCH_USERS_REF.child(uid).once("value");
+ return user.val();
+ } catch (error) {
+ throw error;
+ }
+ }
- /**
- * Get the seasch_user object from firebase
- *
- * @param {String} uid ID of active user
- * @returns Full user object from /search_user/uid
- */
- async getSearchUser(uid) {
- try {
- const user = await SEARCH_USERS_REF.child(uid).once('value');
- return user.val();
- } catch(error) {
- throw error;
- }
- }
+ /**
+ * Get the search_user object from firebase
+ *
+ * @param {String} uid ID of active user
+ * @returns All user settings from /settings/uid
+ */
+ async getSettings(uid) {
+ try {
+ const settings = await SETTINGS_REF.child(uid).once("value");
+ return settings.val();
+ } catch (error) {
+ throw error;
+ }
+ }
- /**
- * Get the seasch_user object from firebase
- *
- * @param {String} uid ID of active user
- * @returns All user settings from /settings/uid
- */
- async getSettings(uid) {
- try {
- const settings = await SETTINGS_REF.child(uid).once('value');
- return settings.val();
- } catch(error) {
- throw error;
- }
- }
+ /**
+ * Update a setting for a user
+ *
+ * @param {String} uid ID of active user
+ * @param {String} category general|encounter|track
+ * @param {string} sub_category undefined|npcs|players
+ * @param {string} type
+ * @param {any} value
+ */
+ async updateSettings(uid, category, sub_category, type, value) {
+ value = value === undefined ? null : value;
+ let path = `${uid}/${category}`;
+ path = sub_category ? `${path}/${sub_category}` : `${path}`;
+ SETTINGS_REF.child(path)
+ .update({ [type]: value })
+ .then(() => {
+ return;
+ })
+ .catch((error) => {
+ throw error;
+ });
+ }
- /**
- * Update a setting for a user
- *
- * @param {String} uid ID of active user
- * @param {String} category general|encounter|track
- * @param {string} sub_category undefined|npcs|players
- * @param {string} type
- * @param {any} value
- */
- async updateSettings(uid, category, sub_category, type, value) {
- value = (value === undefined) ? null : value;
- let path = `${uid}/${category}`;
- path = (sub_category) ? `${path}/${sub_category}` : `${path}`;
- SETTINGS_REF.child(path).update({ [type]: value }).then(() => {
- return;
- }).catch((error) => {
- throw error;
- });
- }
+ /**
+ * Update a setting for a user
+ *
+ * @param {String} uid ID of active user
+ * @param {String} category general|encounter|track
+ */
+ async setDefaultSettings(uid, category) {
+ SETTINGS_REF.child(uid)
+ .child(category)
+ .remove()
+ .then(() => {
+ return;
+ })
+ .catch((error) => {
+ throw error;
+ });
+ }
- /**
- * Update a setting for a user
- *
- * @param {String} uid ID of active user
- * @param {String} category general|encounter|track
- */
- async setDefaultSettings(uid, category) {
- SETTINGS_REF.child(uid).child(category).remove().then(() => {
- return;
- }).catch((error) => {
- throw error;
- });
- }
+ /*
+ *
+ */
- /*
- *
- */
+ static async setActiveVoucher(uid, voucher_object) {
+ let date = await serverUtils.getServerTime();
+ date.setMonth(date.getMonth() + voucher_object.duration);
- static async setActiveVoucher(uid, voucher_object) {
- let date = await serverUtils.getServerTime();
- date.setMonth(date.getMonth() + voucher_object.duration);
+ const fbVoucher = {
+ id: voucher_object.tier,
+ date: date.toLocaleDateString("en-US"),
+ };
+ const voucherHistItem = {
+ ...voucher_object,
+ applied_on: (await serverUtils.getServerTime()).toLocaleDateString("en-US"),
+ };
- const fbVoucher = {
- id: voucher_object.tier,
- date: date.toLocaleDateString('en-US')
- }
- const voucherHistItem = {
- ...voucher_object,
- applied_on: (await serverUtils.getServerTime()).toLocaleDateString('en-US')
- }
+ const usedBefore = await VOUCHER_HISTORY_REF.child(uid)
+ .child(voucherHistItem.voucher)
+ .once("value");
+ if (usedBefore.val()) {
+ throw "Voucher has already been redeemed.";
+ }
- const usedBefore = await VOUCHER_HISTORY_REF.child(uid).child(voucherHistItem.voucher).once('value');
- if (usedBefore.val()) {
- throw "Voucher has already been redeemed.";
- }
+ await Promise.all([
+ USERS_REF.child(uid).child("voucher").set(fbVoucher),
+ VOUCHER_HISTORY_REF.child(uid).child(voucherHistItem.voucher).set(voucherHistItem),
+ voucherService.incrementVoucherUsage(voucherHistItem.voucher),
+ ]);
+ return fbVoucher;
+ }
- await Promise.all([
- USERS_REF.child(uid).child('voucher').set(fbVoucher),
- VOUCHER_HISTORY_REF.child(uid).child(voucherHistItem.voucher).set(voucherHistItem),
- voucherService.incrementVoucherUsage(voucherHistItem.voucher)
- ])
- return fbVoucher;
- }
-
- static async removeVoucher(uid) {
- USERS_REF.child(uid).child('voucher').remove().then(() => {
- return;
- }).catch((error) => {
- throw error;
- });
- }
+ static async removeVoucher(uid) {
+ USERS_REF.child(uid)
+ .child("voucher")
+ .remove()
+ .then(() => {
+ return;
+ })
+ .catch((error) => {
+ throw error;
+ });
+ }
}
diff --git a/src/store/index.js b/src/store/index.js
index a1a9d64e0..150caf5d9 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -1,21 +1,22 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import general from './modules/general';
-import tips from './modules/tips';
-import { run_encounter } from './modules/runEncounter';
-import user from './modules/user';
-import api_spells from './modules/content/spells.js';
-import api_monsters from './modules/content/monsters.js';
-import api_items from './modules/content/items.js';
-import api_conditions from './modules/content/conditions.js';
-import campaigns from './modules/userContent/campaigns.js';
-import npcs from './modules/userContent/npcs.js';
-import items from './modules/userContent/items.js';
-import reminders from './modules/userContent/reminders.js';
-import players from './modules/userContent/players.js';
-import encounters from './modules/userContent/encounters.js';
-import characters from './modules/userContent/characters.js';
-import trackCampaign from './modules/trackCampaign.js';
+import Vue from "vue";
+import Vuex from "vuex";
+import general from "./modules/general";
+import tips from "./modules/tips";
+import { run_encounter } from "./modules/runEncounter";
+import user from "./modules/user";
+import api_spells from "./modules/content/spells.js";
+import api_monsters from "./modules/content/monsters.js";
+import api_items from "./modules/content/items.js";
+import api_conditions from "./modules/content/conditions.js";
+import campaigns from "./modules/userContent/campaigns.js";
+import npcs from "./modules/userContent/npcs.js";
+import items from "./modules/userContent/items.js";
+import spells from "./modules/userContent/spells.js";
+import reminders from "./modules/userContent/reminders.js";
+import players from "./modules/userContent/players.js";
+import encounters from "./modules/userContent/encounters.js";
+import characters from "./modules/userContent/characters.js";
+import trackCampaign from "./modules/trackCampaign.js";
Vue.use(Vuex);
@@ -29,9 +30,9 @@ Vue.use(Vuex);
*/
export default function () {
- return new Vuex.Store({
- modules: {
- general: general,
+ return new Vuex.Store({
+ modules: {
+ general: general,
tips: tips,
user: user,
encounter: run_encounter,
@@ -42,15 +43,16 @@ export default function () {
api_conditions: api_conditions,
npcs: npcs,
items: items,
+ spells: spells,
reminders: reminders,
players: players,
encounters: encounters,
characters: characters,
- trackCampaign: trackCampaign
- },
- // enable strict mode (adds overhead!)
- // for dev mode only
- // strict: process.env.DEBUGGING
- strict: false
- });
+ trackCampaign: trackCampaign,
+ },
+ // enable strict mode (adds overhead!)
+ // for dev mode only
+ // strict: process.env.DEBUGGING
+ strict: false,
+ });
}
diff --git a/src/store/modules/general.js b/src/store/modules/general.js
index ca81b18ca..e9bd04d63 100644
--- a/src/store/modules/general.js
+++ b/src/store/modules/general.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
-import { browserDetect } from '../../functions';
+import Vue from "vue";
+import { browserDetect } from "../../functions";
export default {
state: () => ({
@@ -10,31 +10,47 @@ export default {
action_rolls: [],
side_collapsed: true,
side_small_screen: false,
- browser: browserDetect()
+ browser: browserDetect(),
}),
getters: {
- initialized: (state) => { return state.initialized },
- theme: (state) => { return state.theme },
- getSlide(state) { return state.slide; },
- rolls(state) { return state.rolls; },
- action_rolls(state) { return state.action_rolls; },
- side_collapsed(state) { return state.side_collapsed; },
- side_small_screen(state) { return state.side_small_screen; },
- browser(state) { return state.browser; },
+ initialized: (state) => {
+ return state.initialized;
+ },
+ theme: (state) => {
+ return state.theme;
+ },
+ getSlide(state) {
+ return state.slide;
+ },
+ rolls(state) {
+ return state.rolls;
+ },
+ action_rolls(state) {
+ return state.action_rolls;
+ },
+ side_collapsed(state) {
+ return state.side_collapsed;
+ },
+ side_small_screen(state) {
+ return state.side_small_screen;
+ },
+ browser(state) {
+ return state.browser;
+ },
},
actions: {
// Initialize basic settings depending on a user being logged in or not.
async initialize({ state, dispatch, commit, rootGetters }) {
- if(state.initialized) return;
-
+ if (state.initialized) return;
+
dispatch("setTips");
// In main.js before the Vue instance is rendered
// it's checked if there is a firebase authorization present.
// Therefore we can check here with 'auth' if there is a user.
- if(rootGetters.user) {
+ if (rootGetters.user) {
// await dispatch("setUser");
// first set the user settings in order to set theme correctly
await dispatch("set_user_settings")
@@ -49,7 +65,8 @@ export default {
dispatch("items/fetch_item_count"),
dispatch("campaigns/fetch_campaign_count"),
dispatch("encounters/fetch_encounter_count"),
- dispatch("reminders/fetch_reminder_count")
+ dispatch("reminders/fetch_reminder_count"),
+ dispatch("spells/fetch_spell_count"),
]);
})
.then(async () => {
@@ -63,7 +80,7 @@ export default {
commit("SET_INITIALIZED", true);
})
- .catch(error => {
+ .catch((error) => {
const roll = Math.floor(Math.random() * 15);
console.log(
`%cRolled ${roll} for a DC 15 initialize check.\nInitialization of Harmless Key failed.`,
@@ -74,13 +91,13 @@ export default {
} else {
dispatch("setTheme");
commit("SET_INITIALIZED", true);
- }
+ }
},
/**
- * Forces reinitialization
+ * Forces re-initialization
* Needed when signing in
* First store was initialize without a user, but now it has a user.
- * By simply setting initialized to false
+ * By simply setting initialized to false
* the beforeEach() in main.js will take care of the rest
*/
async reinitialize({ commit, dispatch }) {
@@ -89,28 +106,31 @@ export default {
},
setTheme({ commit, state, rootGetters, dispatch }, theme) {
const uid = rootGetters.user ? rootGetters.user.uid : undefined;
-
+
// If no theme is specified, it's called from initialize() so set it to the previously choosen theme if it exists, or dark otherwise.
- if(!theme) {
+ if (!theme) {
theme = "dark";
- if(process.browser) {
- if(uid && rootGetters.userSettings) {
- theme = (rootGetters.userSettings.general && rootGetters.userSettings.general.theme) ? rootGetters.userSettings.general.theme : theme;
+ if (process.browser) {
+ if (uid && rootGetters.userSettings) {
+ theme =
+ rootGetters.userSettings.general && rootGetters.userSettings.general.theme
+ ? rootGetters.userSettings.general.theme
+ : theme;
} else {
- theme = (localStorage.getItem("theme")) ? localStorage.getItem("theme") : theme;
+ theme = localStorage.getItem("theme") ? localStorage.getItem("theme") : theme;
}
document.documentElement.setAttribute("data-theme", theme);
}
commit("SET_THEME", theme);
- }
+ }
// Set the new choosen theme
else {
- if(theme !== state.theme) {
- if(uid) {
- dispatch("update_settings", {
+ if (theme !== state.theme) {
+ if (uid) {
+ dispatch("update_settings", {
category: "general",
type: "theme",
- value: theme
+ value: theme,
});
} else {
localStorage.setItem("theme", theme);
@@ -129,15 +149,15 @@ export default {
setActionRoll({ commit, state }, newRoll) {
let current = state.action_rolls;
let key = Date.now() + Math.random().toString(36).substring(7);
-
+
// Shuffle the key
- key = key.toString().split('');
+ key = key.toString().split("");
key.sort(() => {
return 0.5 - Math.random();
- });
- key = key.join('');
+ });
+ key = key.join("");
Vue.set(newRoll, "key", key);
-
+
current.unshift(newRoll);
commit("SET_ACTION_ROLLS", current);
},
@@ -147,48 +167,73 @@ export default {
setSlide({ commit, state }, payload) {
let slide = state.slide;
- if(slide.type !== payload.type || (JSON.stringify(slide.data) !== JSON.stringify(payload.data) && payload.data != undefined)) {
- commit('SET_SLIDE', false);
- setTimeout(() => commit('SET_SLIDE', payload), 100);
+ if (
+ slide.type !== payload.type ||
+ (JSON.stringify(slide.data) !== JSON.stringify(payload.data) && payload.data != undefined)
+ ) {
+ commit("SET_SLIDE", false);
+ setTimeout(() => commit("SET_SLIDE", payload), 100);
} else {
- commit('SET_SLIDE', false);
- }
+ commit("SET_SLIDE", false);
+ }
},
toggleSideCollapsed({ commit, state, dispatch }) {
commit("TOGGLE_SIDE_COLLAPSE"); // First toggle
const collapsed = state.side_collapsed; // Then get from state
-
+
// Then update in user settings
- dispatch("update_settings", {
+ dispatch("update_settings", {
category: "general",
type: "side_collapsed",
- value: collapsed || undefined
+ value: collapsed || undefined,
});
},
setSideCollapsed({ commit, rootGetters }) {
const uid = rootGetters.user ? rootGetters.user.uid : undefined;
- if(uid && rootGetters.userSettings) {
- const collapsed = (rootGetters.userSettings.general && rootGetters.userSettings.general.side_collapsed)
- ? rootGetters.userSettings.general.side_collapsed : false;
+ if (uid && rootGetters.userSettings) {
+ const collapsed =
+ rootGetters.userSettings.general && rootGetters.userSettings.general.side_collapsed
+ ? rootGetters.userSettings.general.side_collapsed
+ : false;
commit("SET_SIDE_COLLAPSE", collapsed);
}
},
setSideSmallScreen({ commit }, payload) {
- commit("SET_SIDE_SMALL_SCREEN", payload)
+ commit("SET_SIDE_SMALL_SCREEN", payload);
},
},
mutations: {
- SET_INITIALIZED(state, payload) { Vue.set(state, "initialized", payload) },
- SET_THEME(state, payload) { Vue.set(state, 'theme', payload); },
- SET_SLIDE(state, payload) { Vue.set(state, 'slide', payload); },
- SET_ROLLS(state, payload) { Vue.set(state, 'rolls', payload); },
- SET_ACTION_ROLLS(state, payload) { Vue.set(state, 'action_rolls', payload); },
- CLEAR_ACTION_ROLLS(state) { Vue.set(state, 'action_rolls', []); },
- REMOVE_ACTION_ROLL(state, payload) { Vue.delete(state.action_rolls, payload); },
- TOGGLE_SIDE_COLLAPSE(state) { Vue.set(state, 'side_collapsed', !state.side_collapsed); },
- SET_SIDE_COLLAPSE(state, payload) { Vue.set(state, 'side_collapsed', payload) },
- SET_SIDE_SMALL_SCREEN(state, payload) { Vue.set(state, 'side_small_screen', payload); },
- }
-};
\ No newline at end of file
+ SET_INITIALIZED(state, payload) {
+ Vue.set(state, "initialized", payload);
+ },
+ SET_THEME(state, payload) {
+ Vue.set(state, "theme", payload);
+ },
+ SET_SLIDE(state, payload) {
+ Vue.set(state, "slide", payload);
+ },
+ SET_ROLLS(state, payload) {
+ Vue.set(state, "rolls", payload);
+ },
+ SET_ACTION_ROLLS(state, payload) {
+ Vue.set(state, "action_rolls", payload);
+ },
+ CLEAR_ACTION_ROLLS(state) {
+ Vue.set(state, "action_rolls", []);
+ },
+ REMOVE_ACTION_ROLL(state, payload) {
+ Vue.delete(state.action_rolls, payload);
+ },
+ TOGGLE_SIDE_COLLAPSE(state) {
+ Vue.set(state, "side_collapsed", !state.side_collapsed);
+ },
+ SET_SIDE_COLLAPSE(state, payload) {
+ Vue.set(state, "side_collapsed", payload);
+ },
+ SET_SIDE_SMALL_SCREEN(state, payload) {
+ Vue.set(state, "side_small_screen", payload);
+ },
+ },
+};
diff --git a/src/store/modules/user.js b/src/store/modules/user.js
index 84387152b..4e69c8530 100644
--- a/src/store/modules/user.js
+++ b/src/store/modules/user.js
@@ -2,7 +2,6 @@ import { Cookies } from "quasar";
import { db, auth } from "src/firebase";
import { userServices } from "src/services/user";
import { voucherService } from "src/services/vouchers";
-import { serverUtils } from "src/services/serverUtils";
import Vue from "vue";
@@ -242,6 +241,7 @@ const user_actions = {
count.npcs = rootGetters["npcs/npc_count"];
count.items = rootGetters["items/item_count"];
count.reminders = rootGetters["reminders/reminder_count"];
+ count.spells = rootGetters["spells/spell_count"];
count.encounters = 0;
let used_slots = Object.values(count).reduce((sum, count) => sum + count, 0);
@@ -276,6 +276,7 @@ const user_actions = {
count.items > benefits.items ||
count.reminders > benefits.reminders ||
count.players > benefits.players ||
+ count.spells > benefits.spells ||
count.characters > benefits.characters;
commit("SET_SLOTS_USED", { available_slots, used_slots });
}
@@ -389,6 +390,7 @@ const user_actions = {
await dispatch("npcs/clear_npc_store", {}, { root: true });
await dispatch("reminders/clear_reminder_store", {}, { root: true });
await dispatch("items/clear_item_store", {}, { root: true });
+ await dispatch("items/clear_spell_store", {}, { root: true });
await commit("CLEAR_USER", undefined);
// Sign out from firebase
diff --git a/src/store/modules/userContent/npcs.js b/src/store/modules/userContent/npcs.js
index f76f37511..d87ee9b25 100644
--- a/src/store/modules/userContent/npcs.js
+++ b/src/store/modules/userContent/npcs.js
@@ -1,310 +1,328 @@
-import Vue from 'vue';
-import { npcServices } from "src/services/npcs";
+import Vue from "vue";
+import { npcServices } from "src/services/npcs";
import _ from "lodash";
// Converts a full npc to a search_npc
const convert_npc = (npc) => {
- const properties = [
- "name",
- "challenge_rating",
- "avatar",
- "storage_avatar",
- "type"
- ];
- const returnNpc = {};
-
- for(const prop of properties) {
- if(npc.hasOwnProperty(prop)) {
- returnNpc[prop] = (prop === "name") ? npc[prop].toLowerCase() : npc[prop];
- }
+ const properties = ["name", "challenge_rating", "avatar", "storage_avatar", "type"];
+ const returnNpc = {};
+
+ for (const prop of properties) {
+ if (npc.hasOwnProperty(prop)) {
+ returnNpc[prop] = prop === "name" ? npc[prop].toLowerCase() : npc[prop];
+ }
}
return returnNpc;
-}
+};
const npc_state = () => ({
- npc_services: null,
- cached_npcs: {},
- npc_count: 0,
- npcs: undefined
+ npc_services: null,
+ cached_npcs: {},
+ npc_count: 0,
+ npcs: undefined,
});
const npc_getters = {
- npcs: (state) => {
- // Convert object to sorted array
- return _.chain(state.npcs)
- .filter((npc, key) => {
- npc.key = key;
- return npc;
- }).orderBy("name", "asc").value();
- },
- npc_count: (state) => { return state.npc_count; },
- npc_services: (state) => { return state.npc_services; }
+ npcs: (state) => {
+ // Convert object to sorted array
+ return _.chain(state.npcs)
+ .filter((npc, key) => {
+ npc.key = key;
+ return npc;
+ })
+ .orderBy("name", "asc")
+ .value();
+ },
+ npc_count: (state) => {
+ return state.npc_count;
+ },
+ npc_services: (state) => {
+ return state.npc_services;
+ },
};
const npc_actions = {
- async get_npc_services({ getters, commit }) {
- if(getters.npc_services === null || !Object.keys(getters.npc_services).length) {
- commit("SET_NPC_SERVICES", new npcServices);
- }
- return getters.npc_services;
- },
+ async get_npc_services({ getters, commit }) {
+ if (getters.npc_services === null || !Object.keys(getters.npc_services).length) {
+ commit("SET_NPC_SERVICES", new npcServices());
+ }
+ return getters.npc_services;
+ },
+
+ /**
+ * Fetches all the search_npcs for a user
+ * and stores them in npcs
+ */
+ async get_npcs({ state, rootGetters, dispatch, commit }) {
+ const uid = rootGetters.user ? rootGetters.user.uid : undefined;
+ let npcs = state.npcs ? state.npcs : undefined;
+
+ if (!npcs && uid) {
+ const services = await dispatch("get_npc_services");
+ try {
+ npcs = await services.getNpcs(uid);
+ commit("SET_NPCS", npcs || {});
+ } catch (error) {
+ throw error;
+ }
+ }
+ return npcs;
+ },
- /**
- * Fetches all the search_npcs for a user
- * and stores them in npcs
- */
- async get_npcs({ state, rootGetters, dispatch, commit }) {
- const uid = (rootGetters.user) ? rootGetters.user.uid : undefined;
- let npcs = (state.npcs) ? state.npcs : undefined;
+ /**
+ * Fetches the total count of npcs for a user
+ * and stores it in npc_count
+ */
+ async fetch_npc_count({ rootGetters, commit, dispatch }) {
+ const uid = rootGetters.user ? rootGetters.user.uid : undefined;
+ if (uid) {
+ const services = await dispatch("get_npc_services");
+ try {
+ const count = (await services.getNpcCount(uid)) || 0;
+ commit("SET_NPC_COUNT", count);
+ return;
+ } catch (error) {
+ throw error;
+ }
+ }
+ },
- if(!npcs && uid) {
- const services = await dispatch("get_npc_services");
- try {
- npcs = await services.getNpcs(uid);
- commit("SET_NPCS", npcs || {});
- } catch(error) {
- throw error;
- }
- }
- return npcs;
- },
-
- /**
- * Fetches the total count of npcs for a user
- * and stores it in npc_count
- */
- async fetch_npc_count({ rootGetters, commit, dispatch }) {
- const uid = (rootGetters.user) ? rootGetters.user.uid : undefined;
- if(uid) {
- const services = await dispatch("get_npc_services");
- try {
- const count = await services.getNpcCount(uid) || 0;
- commit("SET_NPC_COUNT", count);
- return;
- } catch(error) {
- throw error;
- }
- }
- },
-
- async get_npc({ state, commit, dispatch }, { uid, id }) {
- let npc = (state.cached_npcs[uid]) ? state.cached_npcs[uid][id] : undefined;
+ async get_npc({ state, commit, dispatch }, { uid, id }) {
+ let npc = state.cached_npcs[uid] ? state.cached_npcs[uid][id] : undefined;
- // The npc is not in the store and needs to be fetched from the database
- // If the NPC is not found in firebase, it returns null
- // We don't have to check for null NPCs again, we know they don't exist
- // Therefore we only do a call to firebase if npc === undefined
- if(npc === undefined) {
- const services = await dispatch("get_npc_services");
- try {
- npc = await services.getNpc(uid, id);
- commit("SET_CACHED_NPC", { uid, id, npc });
- } catch(error) {
- throw error;
- }
- }
- return npc;
- },
+ // The npc is not in the store and needs to be fetched from the database
+ // If the NPC is not found in firebase, it returns null
+ // We don't have to check for null NPCs again, we know they don't exist
+ // Therefore we only do a call to firebase if npc === undefined
+ if (npc === undefined) {
+ const services = await dispatch("get_npc_services");
+ try {
+ npc = await services.getNpc(uid, id);
+ commit("SET_CACHED_NPC", { uid, id, npc });
+ } catch (error) {
+ throw error;
+ }
+ }
+ return npc;
+ },
- /**
- * Adds a newly created NPC for a user
- * A user can only add NPC's for themselves so we use the uid from the store
- *
- * @param {object} npc
- * @returns {string} the id of the newly added npc
- */
- async add_npc({ rootGetters, commit, dispatch }, npc) {
- const uid = (rootGetters.user) ? rootGetters.user.uid : undefined;
- const available_slots = rootGetters.tier.benefits.npcs;
+ /**
+ * Adds a newly created NPC for a user
+ * A user can only add NPC's for themselves so we use the uid from the store
+ *
+ * @param {object} npc
+ * @returns {string} the id of the newly added npc
+ */
+ async add_npc({ rootGetters, commit, dispatch }, npc) {
+ const uid = rootGetters.user ? rootGetters.user.uid : undefined;
+ const available_slots = rootGetters.tier.benefits.npcs;
- if(uid) {
- const services = await dispatch("get_npc_services");
- const used_slots = await services.getNpcCount(uid);
-
- if(used_slots >= available_slots) {
- throw "Not enough slots";
- }
- try {
- const search_npc = convert_npc(npc);
- const id = await services.addNpc(uid, npc, search_npc);
- commit("SET_NPC", { id, search_npc });
- commit("SET_CACHED_NPC", { uid, id, npc });
+ if (uid) {
+ const services = await dispatch("get_npc_services");
+ const used_slots = await services.getNpcCount(uid);
- const new_count = await services.updateNpcCount(uid, 1);
- commit("SET_NPC_COUNT", new_count);
- dispatch("checkEncumbrance", "", { root: true });
- return id;
- } catch(error) {
- throw error;
- }
- }
- },
+ if (used_slots >= available_slots) {
+ return "Not enough slots";
+ }
+ try {
+ const search_npc = convert_npc(npc);
+ const id = await services.addNpc(uid, npc, search_npc);
+ commit("SET_NPC", { id, search_npc });
+ commit("SET_CACHED_NPC", { uid, id, npc });
- /**
- * Updates and existing NPC
- * It is possible to edit the NPC of another user (for companions)
- * therefore we send the uid from where the function is called
- *
- * @param {string} uid
- * @param {string} id
- * @param {object} npc
- */
- async edit_npc({ commit, dispatch }, { uid, id, npc }) {
- if(uid) {
- const services = await dispatch("get_npc_services");
- try {
- const search_npc = convert_npc(npc);
- await services.editNpc(uid, id, npc, search_npc);
- commit("SET_NPC", { id, search_npc });
- commit("SET_CACHED_NPC", { uid, id, npc });
- return;
- } catch(error) {
- throw error;
- }
- }
- },
+ const new_count = await services.updateNpcCount(uid, 1);
+ commit("SET_NPC_COUNT", new_count);
+ dispatch("checkEncumbrance", "", { root: true });
+ return id;
+ } catch (error) {
+ throw error;
+ }
+ }
+ },
- /**
- * Updates a single (non nested) property of an NPC
- *
- * @param {string} uid
- * @param {string} id
- * @param {string} property
- * @param {string|number} value
- */
- async update_npc_prop({ commit, dispatch }, { uid, id, property, value }) {
- if(uid) {
- const services = await dispatch("get_npc_services");
- const update_search = [
- "name",
- "challenge_rating",
- "avatar",
- "storage_avatar",
- "type"
- ].includes(property);
- try {
- await services.updateNpc(uid, id, "", { [property]: value}, update_search);
- commit("SET_NPC_PROP", { uid, id, property, value, update_search });
- return;
- } catch(error) {
- throw error;
- }
- }
- },
+ /**
+ * Updates and existing NPC
+ * It is possible to edit the NPC of another user (for companions)
+ * therefore we send the uid from where the function is called
+ *
+ * @param {string} uid
+ * @param {string} id
+ * @param {object} npc
+ */
+ async edit_npc({ commit, dispatch }, { uid, id, npc }) {
+ if (uid) {
+ const services = await dispatch("get_npc_services");
+ try {
+ const search_npc = convert_npc(npc);
+ await services.editNpc(uid, id, npc, search_npc);
+ commit("SET_NPC", { id, search_npc });
+ commit("SET_CACHED_NPC", { uid, id, npc });
+ return;
+ } catch (error) {
+ throw error;
+ }
+ }
+ },
- /**
- * Deletes an existing NPC
- * A user can only delete their own NPC's so use uid from the store
- *
- * @param {string} id
- */
- async delete_npc({ rootGetters, commit, dispatch }, id) {
- const uid = (rootGetters.user) ? rootGetters.user.uid : undefined;
- if(uid) {
- const services = await dispatch("get_npc_services");
- try {
- const npc = await dispatch("get_npc", { uid, id });
-
- // DELETE COMPANION FROM PLAYER
- if(npc.player_id) {
- const player = await dispatch("players/get_player", { uid, id: npc.player_id }, { root: true });
-
- // Remove the companion from the player
- await dispatch("players/delete_companion", { uid, playerId: npc.player_id, id }, { root: true });
+ /**
+ * Updates a single (non nested) property of an NPC
+ *
+ * @param {string} uid
+ * @param {string} id
+ * @param {string} property
+ * @param {string|number} value
+ */
+ async update_npc_prop({ commit, dispatch }, { uid, id, property, value }) {
+ if (uid) {
+ const services = await dispatch("get_npc_services");
+ const update_search = [
+ "name",
+ "challenge_rating",
+ "avatar",
+ "storage_avatar",
+ "type",
+ ].includes(property);
+ try {
+ await services.updateNpc(uid, id, "", { [property]: value }, update_search);
+ commit("SET_NPC_PROP", { uid, id, property, value, update_search });
+ return;
+ } catch (error) {
+ throw error;
+ }
+ }
+ },
- // Remove the companion from the campaign
- if(player.campaign_id) {
- await dispatch("campaigns/delete_companion", { id: player.campaign_id, companionId: id }, { root: true });
- }
- }
+ /**
+ * Deletes an existing NPC
+ * A user can only delete their own NPC's so use uid from the store
+ *
+ * @param {string} id
+ */
+ async delete_npc({ rootGetters, commit, dispatch }, id) {
+ const uid = rootGetters.user ? rootGetters.user.uid : undefined;
+ if (uid) {
+ const services = await dispatch("get_npc_services");
+ try {
+ const npc = await dispatch("get_npc", { uid, id });
- // Delete the NPC
- await services.deleteNpc(uid, id);
- commit("REMOVE_NPC", id);
- commit("REMOVE_CACHED_NPC", { uid, id });
+ // DELETE COMPANION FROM PLAYER
+ if (npc.player_id) {
+ const player = await dispatch(
+ "players/get_player",
+ { uid, id: npc.player_id },
+ { root: true }
+ );
- const new_count = await services.updateNpcCount(uid, -1);
- commit("SET_NPC_COUNT", new_count);
- dispatch("checkEncumbrance", "", { root: true });
- return;
- } catch(error) {
- throw error;
- }
- }
- },
+ // Remove the companion from the player
+ await dispatch(
+ "players/delete_companion",
+ { uid, playerId: npc.player_id, id },
+ { root: true }
+ );
- /**
- * Gets all REAL NPCs from a USER
- */
- async get_full_npcs({ rootGetters, commit, dispatch }) {
- const uid = (rootGetters.user) ? rootGetters.user.uid : undefined;
- if (uid) {
- const services = await dispatch("get_npc_services");
+ // Remove the companion from the campaign
+ if (player.campaign_id) {
+ await dispatch(
+ "campaigns/delete_companion",
+ { id: player.campaign_id, companionId: id },
+ { root: true }
+ );
+ }
+ }
- try {
- const all_npcs = await services.getFullNpcs(uid);
- commit("SET_CACHED_NPCS", { uid, npcs: all_npcs })
- return all_npcs;
- } catch(error) {
- throw error;
- }
- }
- },
+ // Delete the NPC
+ await services.deleteNpc(uid, id);
+ commit("REMOVE_NPC", id);
+ commit("REMOVE_CACHED_NPC", { uid, id });
- clear_npc_store({ commit, rootGetters }) {
- const uid = (rootGetters.user) ? rootGetters.user.uid : undefined;
- if(uid) {
- commit("CLEAR_STORE");
- }
- }
+ const new_count = await services.updateNpcCount(uid, -1);
+ commit("SET_NPC_COUNT", new_count);
+ dispatch("checkEncumbrance", "", { root: true });
+ return;
+ } catch (error) {
+ throw error;
+ }
+ }
+ },
+
+ /**
+ * Gets all REAL NPCs from a USER
+ */
+ async get_full_npcs({ rootGetters, commit, dispatch }) {
+ const uid = rootGetters.user ? rootGetters.user.uid : undefined;
+ if (uid) {
+ const services = await dispatch("get_npc_services");
+
+ try {
+ const all_npcs = await services.getFullNpcs(uid);
+ commit("SET_CACHED_NPCS", { uid, npcs: all_npcs });
+ return all_npcs;
+ } catch (error) {
+ throw error;
+ }
+ }
+ },
+
+ clear_npc_store({ commit, rootGetters }) {
+ const uid = rootGetters.user ? rootGetters.user.uid : undefined;
+ if (uid) {
+ commit("CLEAR_STORE");
+ }
+ },
};
const npc_mutations = {
- SET_NPC_SERVICES(state, payload) { Vue.set(state, "npc_services", payload); },
- SET_NPC_COUNT(state, value) { Vue.set(state, "npc_count", value); },
- SET_NPCS(state, value) { Vue.set(state, "npcs", value); },
- SET_CACHED_NPCS(state, { uid, npcs }) {
- Vue.set(state.cached_npcs, uid, npcs);
- },
- SET_CACHED_NPC(state, { uid, id, npc }) {
- if(state.cached_npcs[uid]) {
- Vue.set(state.cached_npcs[uid], id, npc);
- } else {
- Vue.set(state.cached_npcs, uid, { [id]: npc });
- }
- },
- SET_NPC_PROP(state, { uid, id, property, value, update_search }) {
- if(state.cached_npcs[uid] && state.cached_npcs[uid][id]) {
- Vue.set(state.cached_npcs[uid][id], property, value);
- }
- if(update_search && state.npcs && state.npcs[id]) {
- Vue.set(state.npcs[id], property, value);
- }
- },
- SET_NPC(state, { id, search_npc }) {
- if(state.npcs) {
- Vue.set(state.npcs, id, search_npc);
- } else {
- Vue.set(state, "npcs", { [id]: search_npc });
- }
- },
- REMOVE_NPC(state, id) {
- Vue.delete(state.npcs, id);
- },
- REMOVE_CACHED_NPC(state, { uid, id }) {
- if(state.cached_npcs[uid]) {
- Vue.delete(state.cached_npcs[uid], id);
- }
- },
- CLEAR_STORE(state) {
- Vue.set(state, "npcs", undefined);
- Vue.set(state, "npc_count", 0);
- }
+ SET_NPC_SERVICES(state, payload) {
+ Vue.set(state, "npc_services", payload);
+ },
+ SET_NPC_COUNT(state, value) {
+ Vue.set(state, "npc_count", value);
+ },
+ SET_NPCS(state, value) {
+ Vue.set(state, "npcs", value);
+ },
+ SET_CACHED_NPCS(state, { uid, npcs }) {
+ Vue.set(state.cached_npcs, uid, npcs);
+ },
+ SET_CACHED_NPC(state, { uid, id, npc }) {
+ if (state.cached_npcs[uid]) {
+ Vue.set(state.cached_npcs[uid], id, npc);
+ } else {
+ Vue.set(state.cached_npcs, uid, { [id]: npc });
+ }
+ },
+ SET_NPC_PROP(state, { uid, id, property, value, update_search }) {
+ if (state.cached_npcs[uid] && state.cached_npcs[uid][id]) {
+ Vue.set(state.cached_npcs[uid][id], property, value);
+ }
+ if (update_search && state.npcs && state.npcs[id]) {
+ Vue.set(state.npcs[id], property, value);
+ }
+ },
+ SET_NPC(state, { id, search_npc }) {
+ if (state.npcs) {
+ Vue.set(state.npcs, id, search_npc);
+ } else {
+ Vue.set(state, "npcs", { [id]: search_npc });
+ }
+ },
+ REMOVE_NPC(state, id) {
+ Vue.delete(state.npcs, id);
+ },
+ REMOVE_CACHED_NPC(state, { uid, id }) {
+ if (state.cached_npcs[uid]) {
+ Vue.delete(state.cached_npcs[uid], id);
+ }
+ },
+ CLEAR_STORE(state) {
+ Vue.set(state, "npcs", undefined);
+ Vue.set(state, "npc_count", 0);
+ },
};
export default {
- namespaced: true,
- state: npc_state,
- getters: npc_getters,
- actions: npc_actions,
- mutations: npc_mutations
-}
+ namespaced: true,
+ state: npc_state,
+ getters: npc_getters,
+ actions: npc_actions,
+ mutations: npc_mutations,
+};
diff --git a/src/store/modules/userContent/spells.js b/src/store/modules/userContent/spells.js
new file mode 100644
index 000000000..137a31ea2
--- /dev/null
+++ b/src/store/modules/userContent/spells.js
@@ -0,0 +1,242 @@
+import Vue from "vue";
+import { SpellServices } from "src/services/spells";
+import _ from "lodash";
+
+// Converts a full spell to a search_spell
+const convert_spell = (spell) => {
+ const properties = ["name", "school", "level"];
+ const returnSpell = {};
+
+ for (const prop of properties) {
+ if (spell.hasOwnProperty(prop)) {
+ returnSpell[prop] = prop === "name" ? spell[prop].toLowerCase() : spell[prop];
+ }
+ }
+ return returnSpell;
+};
+
+const spell_state = () => ({
+ spell_services: null,
+ cached_spells: {},
+ spell_count: 0,
+ spells: undefined,
+});
+
+const spell_getters = {
+ spells: (state) => {
+ // Convert object to sorted array
+ return _.chain(state.spells)
+ .filter((spell, key) => {
+ spell.key = key;
+ return spell;
+ })
+ .orderBy("name", "asc")
+ .value();
+ },
+ spell_services: (state) => {
+ return state.spell_services;
+ },
+ spell_count: (state) => {
+ return state.spell_count;
+ },
+};
+
+const spell_actions = {
+ async get_spell_services({ getters, commit }) {
+ if (getters.spell_services === null || !Object.keys(getters.spell_services).length) {
+ commit("SET_SPELL_SERVICES", new SpellServices());
+ }
+ return getters.spell_services;
+ },
+
+ /**
+ * Get all the spells for a user from 'search_spells'
+ * Stores those spells in spells store
+ * Returns an Array of spells, ordered by name.
+ */
+ async get_spells({ state, rootGetters, dispatch, commit }) {
+ const uid = rootGetters.user ? rootGetters.user.uid : undefined;
+ let spells = state.spells ? state.spells : undefined;
+
+ if (!spells && uid) {
+ const services = await dispatch("get_spell_services");
+ try {
+ spells = await services.getSpells(uid);
+ commit("SET_SPELLS", spells || {});
+ } catch (error) {
+ throw error;
+ }
+ }
+ return spells;
+ },
+
+ /**
+ * Fetches the total count of spells for a user
+ * Stores the count it in spell_count
+ */
+ async fetch_spell_count({ rootGetters, commit, dispatch }) {
+ const uid = rootGetters.user ? rootGetters.user.uid : undefined;
+ if (uid) {
+ const services = await dispatch("get_spell_services");
+ try {
+ let count = (await services.getSpellCount(uid)) || 0;
+ commit("SET_SPELL_COUNT", count);
+ return;
+ } catch (error) {
+ throw error;
+ }
+ }
+ },
+
+ async get_spell({ state, commit, dispatch }, { uid, id }) {
+ let spell = state.cached_spells[uid] ? state.cached_spells[uid][id] : undefined;
+
+ // The spell is not in the store and needs to be fetched from the database
+ if (!spell) {
+ const services = await dispatch("get_spell_services");
+ try {
+ spell = await services.getSpell(uid, id);
+ commit("SET_CACHED_SPELL", { uid, id, spell });
+ } catch (error) {
+ throw error;
+ }
+ }
+ return spell;
+ },
+
+ /**
+ * Adds a newly created SPELL for a user
+ * A user can only add SPELLS for themselves so we use the uid from the store
+ *
+ * @param {object} spell
+ * @returns {string} the id of the newly added spell
+ */
+ async add_spell({ rootGetters, commit, dispatch }, spell) {
+ const uid = rootGetters.user ? rootGetters.user.uid : undefined;
+ const available_slots = rootGetters.tier.benefits.spells;
+
+ if (uid) {
+ const services = await dispatch("get_spell_services");
+ const used_slots = await services.getSpellCount(uid);
+
+ if (used_slots >= available_slots) {
+ return "Not enough slots";
+ }
+ try {
+ const search_spell = convert_spell(spell);
+ const id = await services.addSpell(uid, spell, search_spell);
+ commit("SET_SPELL", { id, search_spell });
+ commit("SET_CACHED_SPELL", { uid, id, spell });
+
+ const new_count = await services.updateSpellCount(uid, 1);
+ commit("SET_SPELL_COUNT", new_count);
+ dispatch("checkEncumbrance", "", { root: true });
+ return id;
+ } catch (error) {
+ throw error;
+ }
+ }
+ },
+
+ /**
+ * Updates and existing spell
+ * A user can only edit their own spells so use uid from the store
+ *
+ * @param {string} uid
+ * @param {string} id
+ * @param {object} spell
+ */
+ async edit_spell({ rootGetters, commit, dispatch }, { id, spell }) {
+ const uid = rootGetters.user ? rootGetters.user.uid : undefined;
+ if (uid) {
+ const services = await dispatch("get_spell_services");
+ try {
+ const search_spell = convert_spell(spell);
+ await services.editSpell(uid, id, spell, search_spell);
+ commit("SET_SPELL", { id, search_spell });
+ commit("SET_CACHED_SPELL", { uid, id, spell });
+ return;
+ } catch (error) {
+ throw error;
+ }
+ }
+ },
+
+ /**
+ * Deletes an existing spell
+ * A user can only delete their own spells so use uid from the store
+ *
+ * @param {string} id
+ */
+ async delete_spell({ rootGetters, commit, dispatch }, id) {
+ const uid = rootGetters.user ? rootGetters.user.uid : undefined;
+ if (uid) {
+ const services = await dispatch("get_spell_services");
+ try {
+ await services.deleteSpell(uid, id);
+ commit("REMOVE_SPELL", id);
+ commit("REMOVE_CACHED_SPELL", { uid, id });
+
+ const new_count = await services.updateSpellCount(uid, -1);
+ commit("SET_SPELL_COUNT", new_count);
+ dispatch("checkEncumbrance", "", { root: true });
+ return;
+ } catch (error) {
+ throw error;
+ }
+ }
+ },
+
+ clear_spell_store({ commit, rootGetters }) {
+ const uid = rootGetters.user ? rootGetters.user.uid : undefined;
+ if (uid) {
+ commit("CLEAR_STORE");
+ }
+ },
+};
+
+const spell_mutations = {
+ SET_SPELL_SERVICES(state, payload) {
+ Vue.set(state, "spell_services", payload);
+ },
+ SET_SPELL_COUNT(state, value) {
+ Vue.set(state, "spell_count", value);
+ },
+ SET_SPELLS(state, value) {
+ Vue.set(state, "spells", value);
+ },
+ SET_CACHED_SPELL(state, { uid, id, spell }) {
+ if (state.cached_spells[uid]) {
+ Vue.set(state.cached_spells[uid], id, spell);
+ } else {
+ Vue.set(state.cached_spells, uid, { [id]: spell });
+ }
+ },
+ SET_SPELL(state, { id, search_spell }) {
+ if (state.spells) {
+ Vue.set(state.spells, id, search_spell);
+ } else {
+ Vue.set(state, "spells", { [id]: search_spell });
+ }
+ },
+ REMOVE_SPELL(state, id) {
+ Vue.delete(state.spells, id);
+ },
+ REMOVE_CACHED_SPELL(state, { uid, id }) {
+ if (state.cached_spells[uid]) {
+ Vue.delete(state.cached_spells[uid], id);
+ }
+ },
+ CLEAR_STORE(state) {
+ Vue.set(state, "spells", undefined);
+ Vue.set(state, "spell_count", 0);
+ },
+};
+
+export default {
+ namespaced: true,
+ state: spell_state,
+ getters: spell_getters,
+ actions: spell_actions,
+ mutations: spell_mutations,
+};
diff --git a/src/utils/actionConstants.js b/src/utils/actionConstants.js
new file mode 100644
index 000000000..c0996956c
--- /dev/null
+++ b/src/utils/actionConstants.js
@@ -0,0 +1,49 @@
+export const aoe_types = Object.freeze([
+ { label: "None", value: "none" },
+ { label: "Cone", value: "cone" },
+ { label: "Cube", value: "cube" },
+ { label: "Cylinder", value: "cylinder" },
+ { label: "Line", value: "line" },
+ { label: "Radius", value: "radius" },
+ { label: "Sphere", value: "sphere" },
+ { label: "Square", value: "square" },
+ { label: "Square Feet", value: "square feet" },
+]);
+
+export const attack_types = Object.freeze({
+ melee_weapon: {
+ label: "Melee weapon",
+ value: "melee_weapon",
+ hint: "A melee weapon attack",
+ },
+ ranged_weapon: {
+ label: "Ranged weapon",
+ value: "ranged_weapon",
+ hint: "A ranged weapon attack",
+ },
+ spell_attack: {
+ label: "Spell attack",
+ value: "spell_attack",
+ hint: "A spell attack that has to hit",
+ },
+ save: {
+ label: "Save",
+ value: "save",
+ hint: "An attack that requires a saving throw",
+ },
+ damage: {
+ label: "Damage",
+ value: "damage",
+ hint: "Damage without a to hit or saving throw",
+ },
+ healing: {
+ label: "Healing",
+ value: "healing",
+ hint: "Restores hit points to a target",
+ },
+ other: {
+ label: "Other",
+ value: "other",
+ hint: "An action without damage or healing",
+ },
+});
diff --git a/src/utils/spellConstants.js b/src/utils/spellConstants.js
new file mode 100644
index 000000000..c64c23f07
--- /dev/null
+++ b/src/utils/spellConstants.js
@@ -0,0 +1,85 @@
+export const spell_levels = Object.freeze([
+ { value: 0, label: "Cantrip" },
+ { value: 1, label: "1st" },
+ { value: 2, label: "2nd" },
+ { value: 3, label: "3rd" },
+ { value: 4, label: "4th" },
+ { value: 5, label: "5th" },
+ { value: 5, label: "6th" },
+ { value: 7, label: "7th" },
+ { value: 8, label: "8th" },
+ { value: 9, label: "9th" },
+]);
+
+export const spell_schools = Object.freeze([
+ { label: "Abjuration", value: "abjuration" },
+ { label: "Conjuration", value: "conjuration" },
+ { label: "Divination", value: "divination" },
+ { label: "Enchantment", value: "enchantment" },
+ { label: "Evocation", value: "evocation" },
+ { label: "Illusion", value: "illusion" },
+ { label: "Necromancy", value: "necromancy" },
+ { label: "Transmutation", value: "transmutation" },
+]);
+
+export const spell_components = Object.freeze([
+ { label: "Verbal", value: "verbal" },
+ { label: "Somatic", value: "somatic" },
+ { label: "Material", value: "material" },
+]);
+
+export const spell_cast_time_types = Object.freeze([
+ { label: "Action", value: "action" },
+ { label: "Bonus Action", value: "bonus_action" },
+ { label: "Reaction", value: "reaction" },
+ { label: "Minute", value: "minute" },
+ { label: "Hour", value: "hour" },
+ { label: "No Action", value: "no_action" },
+ { label: "Special", value: "special" },
+]);
+
+export const spell_range_types = Object.freeze([
+ { label: "Self", value: "self" },
+ { label: "Touch", value: "touch" },
+ { label: "Ranged", value: "ranged" },
+ { label: "Sight", value: "sight" },
+ { label: "Unlimited", value: "unlimited" },
+ { label: "Special", value: "special" },
+]);
+
+export const spell_duration_types = Object.freeze([
+ { label: "Concentration", value: "concentration" },
+ { label: "Instantaneous", value: "instantaneous" },
+ { label: "Special", value: "special" },
+ { label: "Time", value: "time" },
+ { label: "Until Dispelled", value: "until_dispelled" },
+ { label: "Until Dispelled or Triggered", value: "until_dispelled_or_triggered" },
+]);
+
+export const spell_duration_types_time = Object.freeze(["concentration", "time"]);
+
+export const spell_duration_times = Object.freeze([
+ { label: "Round", value: "round" },
+ { label: "Minute", value: "minute" },
+ { label: "Hour", value: "hour" },
+ { label: "Day", value: "day" },
+]);
+
+export const level_scaling = Object.freeze([
+ { label: "None", value: "none" },
+ { label: "Character Level", value: "character_level" },
+ { label: "Spell Scale", value: "spell_scale" },
+ { label: "Spell Level", value: "spell_level" },
+]);
+
+export default {
+ spell_levels,
+ spell_schools,
+ spell_components,
+ spell_cast_time_types,
+ spell_range_types,
+ spell_duration_types,
+ spell_duration_types_time,
+ spell_duration_times,
+ level_scaling,
+};
diff --git a/src/utils/spellFunctions.js b/src/utils/spellFunctions.js
new file mode 100644
index 000000000..3dbe12d13
--- /dev/null
+++ b/src/utils/spellFunctions.js
@@ -0,0 +1,102 @@
+import numeral from "numeral";
+
+/**
+ * Generates description for each level tier for spell level scaling
+ *
+ * @param {object[]} tiers
+ * @param {string} scaling character_level | spell_scale | spell_level
+ * @param {number} level base spell level
+ *
+ * @returns {string}
+ * */
+export function spellScalingDescription(tiers, scaling, level, dice_type) {
+ let description = [];
+
+ // CHARACTER LEVEL
+ if (scaling === "character_level") {
+ description = getDescriptionForSpellScalingCharacterLevel(tiers, dice_type);
+ }
+
+ // SPELL SCALE
+ else if (scaling === "spell_scale") {
+ description = getDescriptionForSpellScaling(tiers, level, dice_type);
+ }
+
+ // SPELL LEVEL
+ else if (scaling === "spell_level") {
+ description = getDescriptionForSpellLevel(tiers, dice_type);
+ }
+ return description.join("\n");
+}
+
+function getDescriptionForSpellScalingCharacterLevel(tiers, dice_type) {
+ const description = [
+ "This spell's damage/projectiles increases when your character reaches a higher level.",
+ ];
+ for (let tier of tiers) {
+ let count_txt = `${tier.projectile_count} projectile${tier.projectile_count > 1 ? "s" : ""}`;
+ let level_txt = `at ${numeral(tier.level).format("0o")} level`;
+ let damage_txt = "this spell roll does ";
+ damage_txt +=
+ tier.dice_count || dice_type ? `${tier.dice_count || "..."}d${dice_type || "..."}` : "";
+
+ if (tier.fixed_val) {
+ damage_txt += `${tier.dice_count || dice_type ? "+" : ""}${tier.fixed_val || ""}`;
+ }
+
+ let new_line = `${tier.projectile_count ? count_txt : ""} `;
+ new_line += `${
+ !tier.projectile_count && tier.dice_count ? level_txt.capitalize() + "s," : level_txt
+ }`;
+ new_line += `${tier.projectile_count && tier.dice_count ? ", and " : "."}`;
+ new_line += `${tier.dice_count ? damage_txt : ""}`;
+ description.push(new_line);
+ }
+
+ return description;
+}
+
+function getDescriptionForSpellScaling(tiers, level, dice_type) {
+ let tier = tiers[0];
+ // Opening line
+ let level_txt = "When you cast this spell using a spell slot of ";
+ level_txt += `${numeral(parseInt(level) + 1).format("0o")} level or higher,`;
+
+ // Damage modifier text
+ let damage_txt = "the damage of this roll increases by ";
+ damage_txt +=
+ tier.dice_count || dice_type ? `${tier.dice_count || "..."}d${dice_type || "..."}` : "";
+
+ if (tier.fixed_val) {
+ damage_txt += `${tier.dice_count || dice_type ? "+" : ""}${tier.fixed_val || ""}`;
+ }
+
+ // Projectile count text
+ let count_txt = `the spell creates ${tier.projectile_count} more projectile${
+ tier.projectile_count > 1 ? "s" : ""
+ }`;
+ // Spell slot text
+ let slot_txt = `for ${
+ tier.level < 2 ? "each slot level" : "every " + tier.level + " slot levels"
+ } above ${numeral(level).format("0o")}.`;
+
+ let text = `${level_txt} ${tier.projectile_count ? count_txt : ""} ${
+ tier.projectile_count && tier.dice_count ? "and " : ""
+ }${tier.dice_count ? damage_txt : ""} ${slot_txt}`;
+ return [text];
+}
+
+function getDescriptionForSpellLevel(tiers, dice_type) {
+ const description = [];
+ for (let tier of tiers) {
+ let new_line = "When you cast this spell using a ";
+ new_line += `${numeral(tier.level).format("0o")}-level spell slot, this spell roll does `;
+ new_line += `${tier.dice_count || "..."}d${dice_type || "..."}${tier.fixed_val ? "+" : ""}${
+ tier.fixed_val || ""
+ } damage.`;
+
+ description.push(new_line);
+ }
+
+ return description;
+}
diff --git a/src/views/Admin/ExportDatabase.vue b/src/views/Admin/ExportDatabase.vue
index 854ddfb7d..861522f75 100644
--- a/src/views/Admin/ExportDatabase.vue
+++ b/src/views/Admin/ExportDatabase.vue
@@ -1,22 +1,14 @@
-
- Creates a JSON file with an array of all entries in a firebase reference.
-
+
Creates a JSON file with an array of all entries in a firebase reference.
- ".key" is saved under "_id" for mongodb import.
- "metadata" is deleted.
- "changed" is deleted.
- "url" is generated, kebap-lowercase-name.
-
+
{{ ref ? `Download ${ref}` : "Select a reference" }}
@@ -29,67 +21,84 @@
\ No newline at end of file
+.select {
+ max-width: 400px;
+ margin-bottom: 20px;
+}
+
diff --git a/src/views/Admin/GenerateXML.vue b/src/views/Admin/GenerateXML.vue
index 4bc8cb52b..2c5d2d186 100644
--- a/src/views/Admin/GenerateXML.vue
+++ b/src/views/Admin/GenerateXML.vue
@@ -39,6 +39,8 @@ export default {
"https://harmlesskey.com/tools/encounter-builder/build-encounter",
"https://harmlesskey.com/tools/monster-creator",
"https://harmlesskey.com/tools/monster-creator/create-monster",
+ "https://harmlesskey.com/tools/spell-creator",
+ "https://harmlesskey.com/tools/spell-creator/create-spell",
"https://harmlesskey.com/tools/character-builder",
],
};
diff --git a/src/views/Compendium/Monsters.vue b/src/views/Compendium/Monsters.vue
index a3af41ec2..dac91371f 100644
--- a/src/views/Compendium/Monsters.vue
+++ b/src/views/Compendium/Monsters.vue
@@ -7,72 +7,26 @@
-
-
-
-
-
-
-
-
-
-
- {{ types[0] }}
-
-
- (+{{ types.length-1 }})
-
-
-
-
-
-
-
-
- {{ challenge_rating[0] }}
-
-
- (+{{ challenge_rating.length-1 }})
-
-
-
-
-
- {{
- (scope.opt == 0.125) ? "1/8" :
- (scope.opt == 0.25) ? "1/4" :
- (scope.opt == 0.5) ? "1/2" :
- scope.opt
- }}
-
-
-
-
-
-
- Filter
-
-
+
+
+
+
+
+ Filter
+
+
+
+
Nothing found
@@ -81,8 +35,8 @@
with a type of {{ types.join(" or ")}}
-
- {{ types ? "and a" : "with a" }} CR of {{ challenge_rating.join(" or ")}}
+
+ {{ types ? "and a" : "with a" }} CR between {{ cr.min }} and {{ cr.max }}
@@ -146,6 +100,41 @@
+
+
+
+
+
+
+ Challenge rating
+
+
+
+
+
@@ -153,6 +142,7 @@
import ViewMonster from "src/components/compendium/Monster.vue";
import { monsterMixin } from "src/mixins/monster.js";
import { mapActions } from "vuex";
+ import _ from "lodash";
export default {
name: "Monsters",
@@ -163,10 +153,12 @@
data() {
return {
monsters: [],
+ filter_dialog: false,
+ filter: {},
search: "",
query: null,
- challenge_rating: [],
- types: null,
+ cr: { min: 0, max: 30 },
+ types: [],
pagination: {
sortBy: "name",
descending: false,
@@ -196,7 +188,7 @@
field: "challenge_rating",
align: "left",
sortable: true,
- format: val => this.cr(val)
+ format: val => this.cr_label(val)
}
],
loading: true,
@@ -209,31 +201,53 @@
crs.push(Number(cr));
}
return crs.sort(function(a, b){return a-b});
+ },
+ type_options() {
+ return this.monster_types.map(type => { return { label: type, value: type } });
}
},
methods: {
...mapActions("api_monsters", ["fetch_monsters"]),
- cr(val) {
+ cr_label(val) {
return (val == 0.125) ? "1/8" :
(val == 0.25) ? "1/4" :
(val == 0.5) ? "1/2" :
val;
},
- selectCR(cr) {
- if(!this.challenge_rating || !this.challenge_rating.includes(cr)) {
- this.challenge_rating.push(cr);
+ setFilter() {
+ this.filter_dialog = false;
+ // Set CR filter
+ if(this.cr.min > 0 || this.cr.max < 30) {
+ const cr = _.range(this.cr.min, this.cr.max+1);
+ if(this.cr.min === 0) {
+ cr.unshift([0.125, 0.25, 0.5]);
+ }
+ this.$set(this.filter, "cr", cr);
} else {
- this.challenge_rating = this.challenge_rating.filter(item => item !== cr);
+ this.$delete(this.filter, "cr");
}
+
+ // Set type filter
+ if(!this.types || !this.types.length || this.types.length === this.monster_types.length) {
+ this.$delete(this.filter, "types");
+ } else {
+ this.$set(this.filter, "types", this.types);
+ }
+ this.filterMonsters();
+ },
+ clearFilter() {
+ this.filter_dialog = false;
+ this.$set(this, "filter", {});
+ this.filterMonsters();
},
- filter() {
+ filterMonsters() {
this.loading = true;
this.monsters = [];
this.pagination.page = 1;
this.query = {
search: this.search,
- types: this.types,
- challenge_ratings: this.challenge_rating
+ types: this.filter.types,
+ challenge_ratings: this.filter.cr
}
this.fetchMonsters();
},
diff --git a/src/views/Compendium/Spells.vue b/src/views/Compendium/Spells.vue
index 2800e7e31..2219a163c 100644
--- a/src/views/Compendium/Spells.vue
+++ b/src/views/Compendium/Spells.vue
@@ -3,22 +3,47 @@
-
-
- Filter
+ placeholder="Search"
+ >
+
+
+
+
+ Filter
+
+
+
-
+
+ Nothing found
+ for "{{ query.search }}"
+ with a type of {{ schools.join(" or ") }}
+
-
+
-
+
{{ col.label }}
@@ -52,15 +73,15 @@
-
-
+
+
-
+
@@ -79,83 +100,188 @@
+
+
+
+
+
+
+
+ Level
+
+
+
+
+
\ No newline at end of file
+ },
+ async mounted() {
+ await this.fetchSpells();
+ },
+};
+
diff --git a/src/views/Contribute/Spells.vue b/src/views/Contribute/Spells.vue
index 043c9b7b1..a213c4387 100644
--- a/src/views/Contribute/Spells.vue
+++ b/src/views/Contribute/Spells.vue
@@ -1,10 +1,29 @@
- Contribute to Spells
+ Contribute to Spells
-
-
+
+
+ {{ Object.keys(allFinishedSpells).length }} / {{ Object.keys(allSpells).length }} ({{
+ Math.floor(
+ (Object.keys(allFinishedSpells).length / Object.keys(allSpells).length) * 100
+ )
+ }}%)
+
+
+
+
+
+
-
-
+
+
+
-
- {{ data.item }}
+
+
+ {{ data.item ? data.item.capitalizeEach() : data.item }}
+
-
-
-
-
-
-
-
-
- {{ data.item }}
-
- {{ getPlayerName(data.row.metadata.tagged) }}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Approved spells
+
+
+ {{ data.item.capitalizeEach() }}
+
+
@@ -204,153 +260,201 @@
\ No newline at end of file
+}
+
diff --git a/src/views/Contribute/index.vue b/src/views/Contribute/index.vue
index f1ff135e4..050b277e2 100644
--- a/src/views/Contribute/index.vue
+++ b/src/views/Contribute/index.vue
@@ -1,44 +1,34 @@
-
-
-
-
-
-
- {{ item.name }}
-
-
-
-
-
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
\ No newline at end of file
+
diff --git a/src/views/Pages/Sitemap.vue b/src/views/Pages/Sitemap.vue
index 99b846fe5..ecb0ae14e 100644
--- a/src/views/Pages/Sitemap.vue
+++ b/src/views/Pages/Sitemap.vue
@@ -43,6 +43,7 @@
+ Spell Creator
Character Builder
diff --git a/src/views/Tools/CombatTracker.vue b/src/views/Tools/CombatTracker.vue
index 3ba20b771..6f012d667 100644
--- a/src/views/Tools/CombatTracker.vue
+++ b/src/views/Tools/CombatTracker.vue
@@ -5,17 +5,16 @@
- Harmless Key is probably the most advanced and effective combat trackers for Dungeons &
- Dragons. Designed specifically for in-person play, our tools can be of some use to almost any
- dungeon master.
+ Harmless Key is probably the most advanced combat tracker for Dungeons & Dragons. Designed
+ specifically for in-person play, our tools can be of some use to almost any dungeon master.
- One of the key features that sets Harmless Key apart, aside from its ability to easily track
- basic stats, like initiative and hit points, is the tracking of important details like bonuses
+ Of course basic stats basic stats, like initiative and hit points are tracked, but one of the
+ key features that sets Harmless Key apart, is the tracking of important details like bonuses
and setting reminders. With just a few clicks, you can add or remove combatants, track damage,
and manage other aspects of the battle. This saves time and ensures that combat encounters run
- smoothly and efficiently and you have time to focus more on the what's really important in
- your games.
+ smoothly and efficiently and you have time to focus more on what's really important in your
+ games.
Overall, Harmless Key is a versatile tool that can enhance your Dungeons & Dragons gameplay
diff --git a/src/views/Tools/MonsterCreator.vue b/src/views/Tools/MonsterCreator.vue
index e4d87f3a9..07dd3f106 100644
--- a/src/views/Tools/MonsterCreator.vue
+++ b/src/views/Tools/MonsterCreator.vue
@@ -1,168 +1,182 @@
-
+
+
+ Create custom monster
+
-
- Create custom monster
-
+
+ With our monster creator you can create stat blocks for custom monsters that you can use in
+ our combat tracker .
+
-
- With our monster creator you can create stat blocks for custom monsters that you can use in our combat tracker .
-
+
+ Actions
+
+ Advanced actions that can be rolled with one click can easily be created for a monster.
+ Different types of damage can be added as separate rolls and this allows us to apply
+ different damage types separately.
+
+
+ If you click the button below, you roll the action from image. The example roll is a
+ versatile action, which means it can be rolled in 2 ways. In this case you can either
+ perform the attack 1-handed or 2-handed. The damage rolls will be slightly different for
+ both options.
+ Since this example is a weapon attack, it can be rolled with
+ Advantage or Disadvantage . When
+ clicking on the option you want to roll, hold [shift] to roll with advantage or hold [ctrl]
+ to roll with disadvantage.
+
-
- Actions
-
- Advanced actions that can be rolled with one click can easily be created for a monster.
- Different types of damage can be added as separate rolls and this allows us to apply different damage types separately.
-
-
- If you click the button below, you roll the action from image.
- The example roll is a versatile action, which means it can be rolled in 2 ways. In this case you can either perform the attack 1 handed or 2 handed.
- The damage rolls will be slightly different for both options.
- Since this example is a weapon attack, it can be rolled with Advantage or Disadvantage .
- When clicking on the option you want to roll, hold [shift] to roll with advantage or hold [ctrl] to roll with disadvantage.
-
+
+ It is very simple to create actions like these yourself, just fill in a few form fields in
+ the monster creator.
+
- It is very simple to create actions like these yourself, just fill in a few form fields in the monster creator.
+
+
+
+
+
+ Roll example action
+
+
+
-
-
-
-
- Roll example action
-
-
-
-
- Burning Spear
-
-
-
-
-
- {{ i + 1 }}
-
- {{ action.action_list[0][`versatile_${i ? 'two' : 'one'}`]}}
-
-
-
-
-
-
-
-
+
+
+
Resistances and Vulnerabilities
+
+ You can define what types of damage the monster is vulnerable ,
+ resistant or immune to.
+ When you apply damage to the monster in our combat tracker, the amount is automatically
+ adjusted based on these resistances. When you apply 5 cold damage to a monster that is
+ vulnerable to this damage type, the damage is doubled, resulting in 10 frost damage.
+
+
+
+
-
-
-
Resistances and Vulnerabilities
-
- You can define what types of damage the monster is vulnerable , resistant or immune to.
- When you apply damage to the monster in our combat tracker, the amount is automatically adjusted based on these resistances.
- When you apply 5 cold damage to a monster that is vulnerable to this damage type, the damage is doubled, resulting in 10 frost damage.
-
-
-
-
+
+ Spellcasters
+
+ In combination with our
+ spell creator you can create powerful
+ spellcasters that are easy to use in our combat tracker. Unleashing magic
+ users on your party can be a pain for you as a DM, but with our combined tools that is no
+ longer the case. Spell slots are tracked for you and casting a spell is as simple as using
+ any other monster action in Harmless Key.
+
+
- To save a monster, you need an account, but you can always download your creations.
-
+
+ Share your creations
+
+ Our monster creator also makes it easy to share your custom monsters with other dungeon
+ masters. You can save your monsters to your library if you have an account, export them to
+ share with others, or import monsters created by other Harmless Key users.
+ If you're a content creator you can provide your followers with monsters that they can
+ directly use in Harmless Key.
+
+
+ To save a monster, you need an account, but you can always download your creations.
+
diff --git a/src/views/Tools/SpellCreator.vue b/src/views/Tools/SpellCreator.vue
new file mode 100644
index 000000000..b71de9397
--- /dev/null
+++ b/src/views/Tools/SpellCreator.vue
@@ -0,0 +1,145 @@
+
+
+
+
+ Create custom spell
+
+
+
+
+
+ Our spell creator lets you create custom spells with actions that can be rolled with one
+ click.
+
+
+
+
+ Scaling
+
+ The actions of your spell change depending on caster level, or at what level the spell is
+ casted. Rolls will automatically scale when you cast the spell with a higher lever character, or using a higher spell slot.
+
+
+
+ Cast Blight
+
+
+
+ Cast at level {{ cast_level }}
+
+
+
+
+
+
+ Spellcasting monsters
+
+ You can combine your custom spells with the monsters you've made in Harmless Key to create
+ powerful spellcasters that are easy to use in our
+ combat tracker .
+
+
+
+
+ Share your creations
+
+ Our spell creator also makes it easy to share your custom spells with other dungeon masters
+ and players. You can save your spells to your library if you have an account, export them to
+ share with others, or import spells created by other Harmless Key users.
+ If you're a content creator you can provide your followers with spells that they can
+ directly use in Harmless Key.
+
+
+
+
+
+
+
+
+.caster__levels {
+ display: flex;
+ justify-content: center;
+ column-gap: 3px;
+ margin-bottom: 10px;
+
+ .level {
+ width: 30px;
+ height: 30px;
+ line-height: 30px;
+ text-align: center;
+ cursor: pointer;
+ background-color: $neutral-8;
+ user-select: none;
+
+ &.selected {
+ background-color: $blue;
+ color: $neutral-1;
+ }
+ &.disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+ }
+}
+
\ No newline at end of file
diff --git a/src/views/Tools/index.vue b/src/views/Tools/index.vue
index 4ef66851d..89a473b6e 100644
--- a/src/views/Tools/index.vue
+++ b/src/views/Tools/index.vue
@@ -4,123 +4,144 @@
D&D 5e Tools
-
-
-
-
-
-
-
-
-
-
- {{ tool.description }}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ {{ tool.description }}
+
+
+
+
+
+
diff --git a/src/views/UserContent/ManageContent.vue b/src/views/UserContent/ManageContent.vue
index 72f3813e1..c26d5f427 100644
--- a/src/views/UserContent/ManageContent.vue
+++ b/src/views/UserContent/ManageContent.vue
@@ -3,10 +3,12 @@
You're over encumbered
- You can either delete some of your content, or get a subscription that allows for more.
+
+ You can either delete some of your content, or get a subscription that allows for more.
+
-
- {{ tier.name === "Free" ? "Get" : "Upgrade" }} supscription
+
+ {{ tier.name === "Free" ? "Get" : "Upgrade" }} subscription
@@ -17,47 +19,46 @@
\ No newline at end of file
+export default {
+ name: "manage-content",
+ components: {
+ Content,
+ },
+ data() {
+ return {
+ content_types: [
+ {
+ type: "campaigns",
+ icon: "fa-dungeon",
+ },
+ {
+ type: "players",
+ icon: "fa-user",
+ },
+ {
+ type: "npcs",
+ icon: "fa-dragon",
+ },
+ {
+ type: "spells",
+ icon: "fa-wand-magic",
+ },
+ {
+ type: "reminders",
+ icon: "fa-stopwatch",
+ },
+ {
+ type: "items",
+ icon: "fa-staff",
+ },
+ ],
+ };
+ },
+ computed: {
+ ...mapGetters(["user", "tier", "content_count", "overencumbered"]),
+ },
+};
+
diff --git a/src/views/UserContent/Npcs/EditNpc.vue b/src/views/UserContent/Npcs/EditNpc.vue
index 61283163d..631ba1205 100644
--- a/src/views/UserContent/Npcs/EditNpc.vue
+++ b/src/views/UserContent/Npcs/EditNpc.vue
@@ -1,6 +1,6 @@
-
+
@@ -39,7 +39,11 @@
There are validation errors
-
Cancel
+
{{ unsaved_changes ? "Cancel" : "Back" }}
Download
@@ -49,8 +53,8 @@
Unsaved changes
-
-
+
+
{{ userId ? "Revert" : "Reset" }}
@@ -80,168 +84,186 @@
\ No newline at end of file
+
diff --git a/src/views/UserContent/Npcs/Npcs.vue b/src/views/UserContent/Npcs/Npcs.vue
index 6cdd8c60b..3fa840d82 100644
--- a/src/views/UserContent/Npcs/Npcs.vue
+++ b/src/views/UserContent/Npcs/Npcs.vue
@@ -2,25 +2,28 @@
-
+
Export NPCs
-
-
+
+
Import NPCs
-
+
-
+
-
- These are your custom Non-Player Characters and monsters.
-
+
These are your custom Non-Player Characters and monsters.
-
@@ -40,19 +43,22 @@
:pagination="{ rowsPerPage: 15 }"
:filter="search"
wrap-cells
- >
+ >
-
-
+
-
+
{{ props.value }}
@@ -64,21 +70,18 @@
-
- Edit
-
+ Edit
-
- Download
-
+ Download
-
+
-
- Delete
-
+ Delete
@@ -87,10 +90,18 @@
-
+
Create your first NPC
-
+
Get more NPC slots
@@ -103,10 +114,10 @@
-
+
@@ -114,144 +125,139 @@
\ No newline at end of file
+
+ const json_export = Object.values(all_npcs);
+ downloadJSON(json_export);
+ },
+ async exportNPC(id) {
+ const npc = await this.get_npc({ uid: this.userId, id });
+ npc.harmless_key = id;
+ downloadJSON(npc);
+ },
+ },
+};
+
diff --git a/src/views/UserContent/Spells/EditSpell.vue b/src/views/UserContent/Spells/EditSpell.vue
new file mode 100644
index 000000000..d42d8e43d
--- /dev/null
+++ b/src/views/UserContent/Spells/EditSpell.vue
@@ -0,0 +1,267 @@
+
+
+
+
+
+
+
+
+ There are validation errors
+
+
+
+
+ Copy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ There are validation errors
+
+
+ {{ unsaved_changes ? "Cancel" : "Back" }}
+
+
+ Download
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/UserContent/Spells/Spells.vue b/src/views/UserContent/Spells/Spells.vue
new file mode 100644
index 000000000..b1625e583
--- /dev/null
+++ b/src/views/UserContent/Spells/Spells.vue
@@ -0,0 +1,218 @@
+
+
+
+
+
+ Import spells
+
+
+
+
+
These are your custom spells.
+
+
+
+
+
+
+
+
+
+
+
+ {{ props.value }}
+
+
+ {{ props.value }}
+
+
+
+
+
+
+
+ Edit
+
+
+
+ Download
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+ Create your first Spell
+
+
+ Get more spell slots
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/UserContent/index.vue b/src/views/UserContent/index.vue
index fdc4d37cb..d12115e8b 100644
--- a/src/views/UserContent/index.vue
+++ b/src/views/UserContent/index.vue
@@ -4,19 +4,19 @@
-
-
+ >
Continue
- {{ active_campaign.name }}
+ {{ active_campaign.name }}
Dive right back into your adventure.
@@ -29,15 +29,11 @@
Campaigns
-
- No active campaign
-
+
No active campaign
Start your first adventure.
-
- Campaign overview
-
+
Campaign overview
@@ -45,9 +41,10 @@
-
@@ -65,13 +62,14 @@
-
+
-
@@ -82,7 +80,9 @@
{{ label }}
{{ caption }}
- {{ badge }}
+ {{ badge }}
@@ -95,188 +95,191 @@
\ No newline at end of file
+}
+
diff --git a/src/views/profile/Profile.vue b/src/views/profile/Profile.vue
index 5dc0f3d29..46010340c 100644
--- a/src/views/profile/Profile.vue
+++ b/src/views/profile/Profile.vue
@@ -82,7 +82,7 @@
-