Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 32 additions & 15 deletions src/components/encounters/Entities.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,30 @@

<!-- CUSTOM NPCs -->
<template v-if="monster_resource === 'custom'">
<q-input
:dark="$store.getters.theme !== 'light'"
v-model="searchNpc"
borderless
filled
square
debounce="300"
clearable
placeholder="Search custom NPCs"
>
<q-icon slot="prepend" name="search" />
</q-input>
<div class="d-flex items-center mb-1">
<q-input
:dark="$store.getters.theme !== 'light'"
v-model="searchNpc"
borderless
filled
square
debounce="300"
clearable
placeholder="Search custom NPCs"
class="col"
>
<q-icon slot="prepend" name="search" />
</q-input>
<q-toggle
v-if="campaignId"
:dark="$store.getters.theme !== 'light'"
v-model="campaignOnly"
label="Campaign only"
class="ml-2"
/>
</div>
<q-table
:data="npcs"
:data="filteredCustomNpcs"
:visible-columns="visibleColumns"
:columns="columns"
row-key="key"
Expand All @@ -107,7 +117,7 @@
:props="props"
:auto-width="col.name !== 'name'"
>
<router-link v-if="col.name === 'name'" :to="`/content/npcs/${col.key}`">
<router-link v-if="col.name === 'name'" :to="`/content/npcs/${props.key}`">
{{ col.value }}
</router-link>
<span v-else-if="col.name === 'environment'">
Expand Down Expand Up @@ -457,7 +467,7 @@ import { mapActions, mapGetters } from "vuex";

import { dice } from "src/mixins/dice.js";
import { general } from "src/mixins/general.js";
import { uuid } from "src/utils/generalFunctions";
import { uuid, campaignGroupKey } from "src/utils/generalFunctions";
import ViewMonster from "src/components/compendium/Monster.vue";
import TutorialPopover from "src/components/demo/TutorialPopover.vue";

Expand Down Expand Up @@ -499,6 +509,7 @@ export default {
player: {},
drawer: this.$store.getters.getDrawer,
to_add: {},
campaignOnly: false,
filter_dialog: false,
filter: {},
typeFilter: [],
Expand Down Expand Up @@ -599,6 +610,12 @@ export default {
computed: {
...mapGetters(["content_count"]),
...mapGetters("npcs", ["npcs", "npc_count"]),
filteredCustomNpcs() {
const npcs = this.npcs;
if (!this.campaignOnly || !this.campaignId) return npcs;
const groupKey = campaignGroupKey(this.campaignId);
return npcs.filter((npc) => npc.groups && npc.groups[groupKey]);
},
...mapGetters("tutorial", ["follow_tutorial", "get_step"]),
monster_resource: {
get() {
Expand Down
295 changes: 295 additions & 0 deletions src/components/npcs/NpcGroupManager.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
<template>
<hk-card class="npc-group-manager">
<div slot="header" class="card-header">
<template v-if="selectedGroup">
<a @click="selectedGroup = null" class="btn btn-sm bg-neutral-5 mr-2" aria-label="Back to groups">
<i aria-hidden="true" class="fas fa-arrow-left" />
</a>
<span>{{ selectedGroup.name ? selectedGroup.name.capitalizeEach() : "" }}</span>
</template>
<span v-else>Manage NPC Groups</span>
<q-btn padding="sm" size="sm" no-caps icon="fas fa-times" flat v-close-popup />
</div>
<div class="card-body">
<!-- GROUP DETAIL VIEW -->
<template v-if="selectedGroup">
<!-- Rename (only for custom groups, not campaigns) -->
<div v-if="!selectedGroup.isCampaign" class="row q-col-gutter-sm mb-3">
<div class="col">
<q-input
:dark="$store.getters.theme === 'dark'"
filled
square
dense
v-model="editName"
label="Group name"
@keyup.enter="renameGroup"
/>
</div>
<div class="col-auto">
<q-btn
no-caps
color="primary"
:disable="!editName || editName.toLowerCase() === selectedGroup.name"
@click="renameGroup"
>
Rename
</q-btn>
</div>
</div>

<!-- Add NPCs -->
<q-select
:dark="$store.getters.theme === 'dark'"
filled
square
dense
multiple
use-chips
label="Add NPCs to group"
v-model="groupNpcIds"
:options="npcOptions"
option-value="value"
option-label="label"
emit-value
map-options
class="mb-3"
/>

<!-- NPC Members -->
<div v-if="groupMembers.length" class="member-list">
<div
v-for="npc in groupMembers"
:key="npc.key"
class="member d-flex justify-content-between items-center"
>
<span>{{ npc.name ? npc.name.capitalizeEach() : npc.key }}</span>
<a class="btn btn-sm bg-neutral-5" @click="removeNpcFromGroup(npc.key)" aria-label="Remove NPC from group">
<i aria-hidden="true" class="fas fa-times red" />
</a>
</div>
</div>
<p v-else class="neutral-3">No NPCs in this group yet.</p>
</template>

<!-- GROUP LIST VIEW -->
<template v-else>
<!-- Add new group -->
<div class="row q-col-gutter-sm mb-3">
<div class="col">
<q-input
:dark="$store.getters.theme === 'dark'"
filled
square
dense
v-model="newGroupName"
label="New group name"
@keyup.enter="addGroup"
/>
</div>
<div class="col-auto">
<q-btn no-caps color="primary" :disable="!newGroupName" @click="addGroup">
Add
</q-btn>
</div>
</div>

<!-- Groups list -->
<div v-if="npc_groups && npc_groups.length" class="group-list">
<div
v-for="group in npc_groups"
:key="group.key"
class="group-item d-flex justify-content-between items-center"
>
<a @click="selectGroup(group)" class="group-name truncate">
{{ group.name ? group.name.capitalizeEach() : group.key }}
</a>
<div class="d-flex">
<a class="btn btn-sm bg-neutral-5 mr-1" @click="selectGroup(group)" aria-label="Edit group">
<i aria-hidden="true" class="fas fa-pencil" />
</a>
<a class="btn btn-sm bg-neutral-5" @click="confirmDeleteGroup(group)" aria-label="Delete group">
<i aria-hidden="true" class="fas fa-trash-alt" />
</a>
</div>
</div>
</div>
<p v-else class="neutral-3">No groups created yet.</p>

<!-- Campaign groups -->
<template v-if="campaignGroups.length">
<h3 class="mt-3 mb-2">Campaigns</h3>
<div class="group-list">
<div
v-for="group in campaignGroups"
:key="group.key"
class="group-item d-flex justify-content-between items-center"
>
<a @click="selectGroup(group)" class="group-name truncate">
{{ group.name ? group.name.capitalizeEach() : group.key }}
</a>
<a class="btn btn-sm bg-neutral-5" @click="selectGroup(group)" aria-label="Edit campaign group">
<i aria-hidden="true" class="fas fa-pencil" />
</a>
</div>
</div>
</template>
</template>
</div>
</hk-card>
</template>

<script>
import { mapActions, mapGetters } from "vuex";
import { campaignGroupKey } from "src/utils/generalFunctions";

export default {
name: "NpcGroupManager",
data() {
return {
newGroupName: "",
selectedGroup: null,
editName: "",
};
},
computed: {
...mapGetters("npcGroups", ["npc_groups"]),
...mapGetters("npcs", ["npcs"]),
...mapGetters("campaigns", ["campaigns"]),
campaignGroups() {
if (!this.campaigns) return [];
return this.campaigns.map((campaign) => ({
key: campaignGroupKey(campaign.key),
name: campaign.name || campaign.key,
isCampaign: true,
}));
},
npcOptions() {
return this.npcs.map((npc) => ({
label: npc.name ? npc.name.capitalizeEach() : npc.key,
value: npc.key,
}));
},
groupMembers() {
if (!this.selectedGroup) return [];
return this.npcs.filter(
(npc) => npc.groups && npc.groups[this.selectedGroup.key]
);
},
groupNpcIds: {
get() {
return this.groupMembers.map((npc) => npc.key);
},
set(val) {
this.syncGroupNpcs(val);
},
},
},
methods: {
...mapActions("npcGroups", ["add_npc_group", "edit_npc_group", "delete_npc_group"]),
...mapActions("npcs", ["update_npc_groups"]),
async addGroup() {
if (!this.newGroupName) return;
await this.add_npc_group({ name: this.newGroupName });
this.newGroupName = "";
},
selectGroup(group) {
this.selectedGroup = group;
this.editName = group.name ? group.name.capitalizeEach() : "";
},
async renameGroup() {
if (!this.editName || this.editName.toLowerCase() === this.selectedGroup.name) return;
await this.edit_npc_group({
id: this.selectedGroup.key,
group: { name: this.editName },
});
this.selectedGroup.name = this.editName.toLowerCase();
},
confirmDeleteGroup(group) {
this.$snotify.error(
`Are you sure you want to delete "${group.name ? group.name.capitalizeEach() : group.key}"?`,
"Delete Group",
{
timeout: false,
buttons: [
{
text: "Yes",
action: (toast) => {
this.delete_npc_group(group.key);
if (this.selectedGroup && this.selectedGroup.key === group.key) {
this.selectedGroup = null;
}
this.$snotify.remove(toast.id);
},
bold: false,
},
{
text: "No",
action: (toast) => {
this.$snotify.remove(toast.id);
},
bold: true,
},
],
}
);
},
async syncGroupNpcs(newNpcIds) {
const groupId = this.selectedGroup.key;
const currentNpcIds = this.groupMembers.map((npc) => npc.key);

// NPCs to add to group
const toAdd = newNpcIds.filter((id) => !currentNpcIds.includes(id));
// NPCs to remove from group
const toRemove = currentNpcIds.filter((id) => !newNpcIds.includes(id));

for (const npcId of toAdd) {
const npc = this.npcs.find((n) => n.key === npcId);
const groups = { ...(npc.groups || {}), [groupId]: true };
await this.update_npc_groups({ id: npcId, groups });
}
for (const npcId of toRemove) {
await this.removeNpcFromGroup(npcId);
}
},
async removeNpcFromGroup(npcId) {
const groupId = this.selectedGroup.key;
const npc = this.npcs.find((n) => n.key === npcId);
if (npc && npc.groups) {
const groups = { ...npc.groups };
delete groups[groupId];
await this.update_npc_groups({
id: npcId,
groups: Object.keys(groups).length ? groups : undefined,
});
}
},
},
};
</script>

<style lang="scss" scoped>
.npc-group-manager {
max-width: 95vw;
width: 576px;
margin-top: 100px;
}
.group-list,
.member-list {
.group-item,
.member {
padding: 5px 0;
border-bottom: solid 1px $neutral-6;

&:last-child {
border-bottom: none;
}
}
.group-name {
cursor: pointer;
&:hover {
color: $blue;
}
}
}
</style>
1 change: 1 addition & 0 deletions src/components/userContent/ExportUserContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export default {

addNpcToExport(npc_id, npc) {
delete npc.player_id;
delete npc.groups;
npc.harmless_key = npc_id;

this.exportData.npcs[npc_id] = npc;
Expand Down
1 change: 1 addition & 0 deletions src/components/userContent/ImportUserContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ export default {
*/

this.removeTimestamps(npc);
delete npc.groups;

this.versatileToOptions(npc);
this.renameNpcProps(npc);
Expand Down
Loading