Skip to content

Commit

Permalink
Merge pull request #162 from HarmlessKey/feature/import_npcs_with_spells
Browse files Browse the repository at this point in the history
Import NPCs with spells
  • Loading branch information
HarmlessHarm authored Jun 2, 2023
2 parents 28c2c79 + 26164c9 commit 77e07e4
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 35 deletions.
164 changes: 143 additions & 21 deletions src/components/ImportContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,30 +53,30 @@
</template>
<hk-loader v-else-if="!parsed" prefix="Validating" :title="type_label" />
<div v-else-if="!importing">
<template v-for="type in Object.keys(imports)">
<div v-if="imports[type].length" class="mb-4" :key="type">
<template v-for="import_type in Object.keys(imports)">
<div v-if="imports[import_type].length" class="mb-4" :key="import_type">
<p>
Found
<span :class="type === 'unique' ? 'green' : 'orange'">
{{ imports[type].length }} {{ type === "unique" ? "new" : "duplicate" }}
<span :class="import_type === 'unique' ? 'green' : 'orange'">
{{ imports[import_type].length }} {{ import_type === "unique" ? "new" : "duplicate" }}
</span>
{{ type_label }}
</p>
<q-table
class="sticky-header-table mb-2"
class="sticky-header-table mb-2 no-table-margin"
:virtual-scroll-sticky-size-start="48"
:dark="$store.getters.theme !== 'light'"
flat
dense
square
:data="imports[type]"
:data="imports[import_type]"
:columns="columns"
row-key="index"
virtual-scroll
:pagination.sync="pagination"
:rows-per-page-options="[0]"
selection="multiple"
:selected.sync="selected[type]"
:selected.sync="selected[import_type]"
hide-bottom
>
<template v-slot:body-cell-invalid="props">
Expand All @@ -102,13 +102,76 @@
</template>
</q-table>
<q-toggle
v-if="type === 'duplicate'"
v-if="import_type === 'duplicate'"
v-model="overwrite"
label="Overwrite duplicates"
unchecked-icon="none"
/>
</div>
</template>
<template v-if="Object.values(custom_spells).length">
<p>
Found
<span class="yellow"> {{ Object.values(custom_spells).length }}</span>
Custom Spells
<q-toggle
v-model="import_custom_spells"
label="Import custom spells"
unchecked-icon="none"
/>
</p>
<template v-if="import_custom_spells">
<q-table
class="sticky-header-table mb-2 no-table-margin"
:virtual-scroll-sticky-size-start="48"
:dark="$store.getters.theme !== 'light'"
flat
dense
square
virtual-scroll
:pagination.sync="pagination"
:rows-per-page-options="[0]"
:data="Object.values(custom_spells)"
:columns="columns"
row-key="index"
hide-bottom
>
<template v-slot:body-cell-invalid="props">
<td class="text-right">
<hk-popover v-if="props.row.errors" header="Validation errors">
<q-icon name="error" class="red" />
<div slot="content">
<ol class="px-3">
<li
v-for="(error, i) in props.row.errors"
:key="`${props.row.index}-error-${i}`"
class="red"
>
<strong v-if="error.instancePath" class="neutral-1">
{{ error.instancePath }}
</strong>
{{ error.message.capitalize() }}
</li>
</ol>
</div>
</hk-popover>
</td>
</template>
</q-table>
<template v-if="Object.keys(this.custom_spells).length > this.availableSpellSlots">
<div>
Insufficient spell slots. You're trying to import
<strong class="red">{{ Object.keys(this.custom_spells).length }}</strong>
spells,<br />
but have only <strong class="red">{{ availableSpellSlots }}</strong> slots available.
</div>
<router-link class="btn btn-sm bg-patreon-red my-2" to="/patreon">
<i aria-hidden="true" class="fab fa-patreon white mr-1" />
Get more slots
</router-link>
</template>
</template>
</template>
<div v-if="importTotal > availableSlots">
Insufficient slots. You're trying to import
<strong class="red">{{ importTotal }}</strong> {{ type_label }},<br />
Expand Down Expand Up @@ -256,6 +319,9 @@ export default {
overwrite: true,
parsing: false,
parsed: false,
map_old_to_custom: {},
custom_spells: {},
import_custom_spells: true,
failed_imports: [],
importing: undefined,
imported: 0,
Expand Down Expand Up @@ -310,6 +376,11 @@ export default {
? Infinity
: this.tier.benefits[this.type] - this.content_count;
},
availableSpellSlots() {
return this.tier.benefits["spells"] === "infinite"
? Infinity
: this.tier.benefits["spells"] - this.spell_count;
},
importTotal() {
return !this.overwrite
? this.selected.unique.length + this.selected.duplicate.length
Expand All @@ -321,14 +392,21 @@ export default {
},
methods: {
...mapActions("npcs", ["add_npc", "edit_npc", "get_npcs", "get_npc"]),
...mapActions("spells", ["add_spell", "edit_spell", "get_spells", "get_spell"]),
...mapActions("spells", [
"add_spell",
"edit_spell",
"get_spells",
"get_spell",
"get_spell_id_by_name",
"reserve_spell_id",
]),
async getItem(id) {
return this.type === "npcs"
? this.get_npc({ uid: this.uid, id })
: this.get_spell({ uid: this.uid, id });
},
async addItem(item) {
this.type === "npcs" ? await this.add_npc(item) : await this.add_spell(item);
this.type === "npcs" ? await this.add_npc(item) : await this.add_spell({ spell: item });
},
async editItem(id, item) {
this.type === "npcs"
Expand Down Expand Up @@ -370,8 +448,8 @@ export default {
let checkable_item = item;
// Parse versatile to options for NPCs
if (this.type === "npcs") {
checkable_item = this.removeCustomSpells(item);
checkable_item = this.versatileToOptions(item);
checkable_item = await this.parseCustomSpells(item);
checkable_item = this.versatileToOptions(checkable_item);
}
const valid = ajv.validate(this.schema, checkable_item);
Expand Down Expand Up @@ -410,6 +488,22 @@ export default {
this.parsed = true;
},
async importData() {
// First check if there are custom spells from imported NPCs that need to be added.
if (
this.import_custom_spells &&
this.custom_spells &&
Object.keys(this.custom_spells).length <= this.availableSpellSlots
) {
for (const [key, spell] of Object.entries(this.custom_spells)) {
try {
// TODO: use parse function to filter out spells
await this.add_spell({ spell: spell, predefined_key: key });
} catch (error) {
this.failed_imports.push(spell);
}
}
}
if (this.importTotal <= this.availableSlots) {
this.importing = this.selected.unique.length + this.selected.duplicate.length;
for (const item of this.selected.unique) {
Expand Down Expand Up @@ -532,19 +626,44 @@ export default {
}
return npc;
},
removeCustomSpells(npc) {
delete npc.custom_spells;
for (const spell_list_type of ["caster_spells", "innate_spells"]) {
if (npc[spell_list_type]) {
const spell_list = Object.assign({}, npc[spell_list_type]);
for (const [spell_key, spell] of Object.entries(spell_list)) {
if (spell.custom) {
delete npc[spell_list_type][spell_key];
async parseCustomSpells(npc) {
if (npc.custom_spells) {
for (const [old_key, spell] of Object.entries(npc.custom_spells)) {
// Check if there already is a spell with name
delete spell.key;
delete spell.updated;
delete spell.created;
const valid = ajv.validate(spellSchema, spell);
if (!valid) {
spell.errors = ajv.errors;
}
// Check if spell is already known to importer
if (!Object.keys(this.map_old_to_custom).includes(old_key)) {
let spell_id = await this.get_spell_id_by_name({ name: spell.name });
if (!spell_id) {
// Generate a id for the spell so when we can link the spell in NPC to a future spell
const new_spell_id = await this.reserve_spell_id();
spell_id = new_spell_id;
this.$set(this.custom_spells, spell_id, spell);
}
this.map_old_to_custom[old_key] = spell_id;
}
}
for (const spell_list_type of ["caster_spells", "innate_spells"]) {
if (npc[spell_list_type]) {
const spell_list = Object.assign({}, npc[spell_list_type]);
for (const [spell_key, spell] of Object.entries(spell_list)) {
if (spell.custom && spell_key in this.map_old_to_custom) {
npc[spell_list_type][this.map_old_to_custom[spell_key]] = { ...spell };
delete npc[spell_list_type][spell_key];
}
}
}
}
delete npc.custom_spells;
}
return npc;
},
},
Expand All @@ -555,4 +674,7 @@ export default {
.q-expansion-item {
background-color: $neutral-9;
}
.no-table-margin::v-deep table {
margin-bottom: 0;
}
</style>
42 changes: 38 additions & 4 deletions src/services/spells.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ export class SpellServices {
}
}

async getSearchSpellByName(uid, name) {
try {
const spell = await SEARCH_SPELLS_REF.child(uid)
.child("results")
.orderByChild("name")
.equalTo(name)
.get();

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'
Expand All @@ -70,18 +84,25 @@ export class SpellServices {
* @param {Object} search_spell Compressed spell
* @returns Key of the newly added spell
*/
async addSpell(uid, spell, search_spell) {
async addSpell(uid, spell, search_spell, predefined_key = undefined) {
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;

let spell_key = predefined_key;
if (predefined_key) {
await SPELLS_REF.child(uid).child(predefined_key).set(spell);
} else {
const newSpell = await SPELLS_REF.child(uid).push(spell);
spell_key = newSpell.key;
}

// Update search_spells
SEARCH_SPELLS_REF.child(`${uid}/results/${newSpell.key}`).set(search_spell);
SEARCH_SPELLS_REF.child(`${uid}/results/${spell_key}`).set(search_spell);

return newSpell.key;
return spell_key;
} catch (error) {
throw error;
}
Expand Down Expand Up @@ -128,6 +149,19 @@ export class SpellServices {
}
}

/**
* Reserve an ID for a spell that might be stored in the future
* Useful when you don't know yet if you want to store a spell, but want to be able to link to it
* from different db entries
*
* E.g. New NPC has Custom Spells that need to be stored, but you store both NPC and Spell at same time.
*
* @param {String} uid ID of active user
*/
async reserveSpellId(uid) {
return (await SPELLS_REF.child(uid).push()).key;
}

/**
* Update spell_count in the search table of search_spells
*
Expand Down
2 changes: 1 addition & 1 deletion src/store/modules/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +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 dispatch("spells/clear_spell_store", {}, { root: true });
await commit("CLEAR_USER", undefined);

// Sign out from firebase
Expand Down
Loading

0 comments on commit 77e07e4

Please sign in to comment.