aboutsummaryrefslogtreecommitdiffstats
path: root/components/Editor
diff options
context:
space:
mode:
Diffstat (limited to 'components/Editor')
-rw-r--r--components/Editor/Category/CategoryChildrenOptionsPanel.vue53
-rw-r--r--components/Editor/Category/CategoryOptionsPanel.vue54
-rw-r--r--components/Editor/EditorOptionsPanel.vue18
-rw-r--r--components/Editor/EditorPane.vue216
-rw-r--r--components/Editor/EditorSidebar.vue28
-rw-r--r--components/Editor/EditorSidebarCategory.vue96
-rw-r--r--components/Editor/EditorSidebarQuest.vue68
-rw-r--r--components/Editor/Quest/Modal/DeleteQuestModal.vue42
-rw-r--r--components/Editor/Quest/Modal/DuplicateQuestModal.vue69
-rw-r--r--components/Editor/Quest/Modal/RenameQuestModal.vue69
-rw-r--r--components/Editor/Quest/QuestOptionsPanel.vue145
-rw-r--r--components/Editor/Quest/QuestTasksOptionsPanel.vue98
-rw-r--r--components/Editor/Quest/Task/Modal/AddTaskModal.vue90
-rw-r--r--components/Editor/Quest/Task/Modal/ChangeTaskModal.vue77
-rw-r--r--components/Editor/Quest/Task/TaskConfiguration.vue209
-rw-r--r--components/Editor/Quest/Task/TaskConfigurationRow.vue195
16 files changed, 0 insertions, 1527 deletions
diff --git a/components/Editor/Category/CategoryChildrenOptionsPanel.vue b/components/Editor/Category/CategoryChildrenOptionsPanel.vue
deleted file mode 100644
index 6e96f64..0000000
--- a/components/Editor/Category/CategoryChildrenOptionsPanel.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<script setup lang="ts">
-import { useSessionStore, type EditorCategory } from '@/stores/session';
-import { computed } from 'vue';
-import EditorOptionsPanel from '../EditorOptionsPanel.vue';
-
-const props = defineProps<{
- categoryId: string;
-}>();
-
-const sessionStore = useSessionStore();
-
-const category = computed(() => {
- return sessionStore.getCategoryById(props.categoryId) as EditorCategory;
-});
-</script>
-
-<template>
- <EditorOptionsPanel v-if="category">
- <div id="options">
- <h2>Quests in this category</h2>
- <p>Drag to reorder.</p>
- <div class="option-group">
- </div>
- </div>
- </EditorOptionsPanel>
-</template>
-
-<style>
-#options {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.option-group {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.description {
- font-size: 0.8em;
-}
-
-label {
- font-weight: 700;
-}
-
-h2 {
- border-bottom: 1px solid var(--color-border);
-}
-</style>
-
diff --git a/components/Editor/Category/CategoryOptionsPanel.vue b/components/Editor/Category/CategoryOptionsPanel.vue
deleted file mode 100644
index f7d548c..0000000
--- a/components/Editor/Category/CategoryOptionsPanel.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<script setup lang="ts">
-import { useSessionStore, type EditorCategory } from '@/stores/session';
-import { computed } from 'vue';
-import EditorOptionsPanel from '../EditorOptionsPanel.vue';
-import Checkbox from '@/components/Control/Checkbox.vue';
-
-const props = defineProps<{
- categoryId: string;
-}>();
-
-const sessionStore = useSessionStore();
-
-const category = computed(() => {
- return sessionStore.getCategoryById(props.categoryId) as EditorCategory;
-});
-</script>
-
-<template>
- <EditorOptionsPanel v-if="category">
- <div id="options">
- <div class="option-group">
- <Checkbox id="category-permissionrequired" label="Require permission for category"
- description="Players must have permission to open and start quests in this category." v-model="category.permissionRequired" />
- </div>
- </div>
- </EditorOptionsPanel>
-</template>
-
-<style>
-#options {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.option-group {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.description {
- font-size: 0.8em;
-}
-
-label {
- font-weight: 700;
-}
-
-h2 {
- border-bottom: 1px solid var(--color-border);
-}
-</style>
-
diff --git a/components/Editor/EditorOptionsPanel.vue b/components/Editor/EditorOptionsPanel.vue
deleted file mode 100644
index 1415d84..0000000
--- a/components/Editor/EditorOptionsPanel.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-<script setup lang="ts">
-</script>
-
-<template>
- <div id="options-panel">
- <slot />
- </div>
-</template>
-
-<style scoped>
-#options-panel {
- width: 100%;
- background-color: var(--color-background);
- border: 1px solid var(--color-border);
- padding: 1rem;
- height: 100%;
-}
-</style> \ No newline at end of file
diff --git a/components/Editor/EditorPane.vue b/components/Editor/EditorPane.vue
deleted file mode 100644
index bf9532a..0000000
--- a/components/Editor/EditorPane.vue
+++ /dev/null
@@ -1,216 +0,0 @@
-<script setup lang="ts">
-import { useSessionStore } from '@/stores/session';
-import { computed, ref } from 'vue';
-import { stripColorCodes } from '@/lib/util';
-import QuestOptionsPanel from '@/components/Editor/Quest/QuestOptionsPanel.vue';
-import QuestTasksOptionsPanel from '@/components/Editor/Quest/QuestTasksOptionsPanel.vue';
-import CategoryOptionsPanel from '@/components/Editor/Category/CategoryOptionsPanel.vue';
-import CategoryChildrenOptionsPanel from '@/components/Editor/Category/CategoryChildrenOptionsPanel.vue';
-import Button from '@/components/Control/Button.vue';
-import DeleteQuestModal from '@/components/Editor/Quest/Modal/DeleteQuestModal.vue';
-import RenameQuestModal from '@/components/Editor/Quest/Modal/RenameQuestModal.vue';
-import DuplicateQuestModal from '@/components/Editor/Quest/Modal/DuplicateQuestModal.vue';
-
-const sessionStore = useSessionStore();
-
-const selectedType = computed(() => sessionStore.editor.selected.type);
-const selectedId = computed(() => sessionStore.editor.selected.id);
-
-const selectedName = computed(() => {
- if (selectedType.value === 'Quest' && selectedId.value) {
- return sessionStore.getQuestById(selectedId.value)?.display.name;
- } else if (selectedType.value === 'Category' && selectedId.value) {
- return sessionStore.getCategoryById(selectedId.value)?.display.name;
- } else {
- return '';
- }
-});
-
-const categoryFromSelectedQuest = computed(() => {
- if (!selectedId.value || selectedType.value !== 'Quest') return null;
-
- const quest = sessionStore.getQuestById(selectedId.value);
- if (quest) {
- return sessionStore.getCategoryById(quest.options.category) || null;
- } else {
- return null;
- }
-});
-
-const showDeleteModal = ref(false);
-const showRenameModal = ref(false);
-const showDuplicateModal = ref(false);
-
-const renameQuest = (oldId: string, newId: string) => {
- sessionStore.changeQuestId(oldId, newId);
- sessionStore.editor.selected.id = newId;
- showRenameModal.value = false;
-};
-
-const deleteQuest = (questId: string) => {
- sessionStore.deleteQuest(questId);
- sessionStore.setEditorSelected(null, null);
- showDeleteModal.value = false;
-};
-
-const duplicateQuest = (oldId: string, newId: string) => {
- sessionStore.duplicateQuest(oldId, newId);
- sessionStore.editor.selected.id = newId;
- showDuplicateModal.value = false;
-};
-</script>
-
-<template>
- <div id="pane-container" v-if="!selectedId || !selectedType">
- <h1 class="none-selected">Select a quest or category from the sidebar to continue</h1>
- </div>
- <div id="pane-container" v-if="selectedId && selectedType">
- <div id="header">
- <span id="path">
- <template v-if="selectedType === 'Quest'">
- <template v-if="categoryFromSelectedQuest">
- <font-awesome-icon class="icon" :icon="['fas', 'fa-folder']"/>
- {{ stripColorCodes(categoryFromSelectedQuest?.display.name) }}
- <font-awesome-icon class="chevron" :icon="['fas', 'fa-chevron-right']"/>
- </template>
- <font-awesome-icon class="icon" :icon="['far', 'fa-compass']"/>
- <span class="title">{{ stripColorCodes(selectedName!) }} </span>
- <code>({{ selectedId }})</code>
- </template>
- <template v-if="selectedType === 'Category'">
- <font-awesome-icon class="icon" :icon="['fas', 'fa-folder']"/>
- <span class="title">{{ stripColorCodes(selectedName!) }} </span>
- <code>({{ selectedId }})</code>
- </template>
- </span>
- <span id="controls" class="control-group">
- <Button
- v-if="selectedType === 'Quest'"
- :icon="['fas', 'fa-code']"
- :label="'YAML'"
- ></Button>
- <Button
- v-if="selectedType === 'Quest'"
- :icon="['fas', 'fa-copy']"
- :label="'Duplicate'"
- @click="showDuplicateModal = true"
- ></Button>
- <Button
- :icon="['fas', 'fa-pen']"
- :label="'Rename'"
- @click="showRenameModal = true"
- ></Button>
- <Button
- :icon="['fas', 'fa-trash']"
- :label="'Delete'"
- @click="showDeleteModal = true"
- ></Button>
- <Button
- type="solid"
- :disabled="true"
- :icon="['fas', 'fa-save']"
- :label="'Save'"
- ></Button>
- </span>
- </div>
-
- <div id="options-container">
- <QuestOptionsPanel v-if="selectedType === 'Quest'" :questId="selectedId" />
- <QuestTasksOptionsPanel v-if="selectedType === 'Quest'" :questId="selectedId" />
-
- <CategoryOptionsPanel v-if="selectedType === 'Category'" :categoryId="selectedId" />
- <CategoryChildrenOptionsPanel v-if="selectedType === 'Category'" :categoryId="selectedId" />
- </div>
- </div>
-
- <DeleteQuestModal
- v-if="selectedType === 'Quest' && selectedId"
- v-model="showDeleteModal"
- :key="`delete-quest-${selectedId}`"
- :questId="selectedId"
- @delete="() => selectedId && deleteQuest(selectedId)"
- />
- <RenameQuestModal
- v-if="selectedType === 'Quest' && selectedId"
- v-model="showRenameModal"
- :key="`rename-quest-${selectedId}`"
- :questId="selectedId"
- @update="newId => selectedId && renameQuest(selectedId, newId)"
- />
- <DuplicateQuestModal
- v-if="selectedType === 'Quest' && selectedId"
- v-model="showDuplicateModal"
- :key="`duplicate-quest-${selectedId}`"
- :questId="selectedId"
- @duplicate="newId => selectedId && duplicateQuest(selectedId, newId)"
- />
-</template>
-
-<style scoped>
-#header {
- padding: 1rem 1rem 0.5rem 1rem;
- background-color: var(--color-background);
- border-bottom: 1px solid var(--color-border);
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- width: 100%;
- height: 55px;
- display: flex;
- align-items: left;
- justify-content: space-between;
- gap: 1rem;
-
- #path {
- font-size: 1.2rem;
- display: flex;
- gap: 0.5rem;
- align-items: center;
-
- .icon {
- font-size: 0.8rem;
- }
-
- .chevron {
- font-size: 0.8rem;
- color: var(--color-text-mute);
- }
-
- .title {
- font-weight: 700;
- }
-
- code {
- font-size: 0.8rem;
- color: var(--color-text-mute);
- }
- }
-}
-
-.none-selected {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 1rem;
- font-size: 1.2rem;
- color: var(--color-text-mute);
-}
-
-#pane-container {
- width: 100%;
- flex-grow: 1;
- height: calc(100vh - 73px);
- max-height: calc(100vh - 73px);
-}
-
-#options-container {
- width: 100%;
- display: flex;
- gap: 1rem;
- padding: 1rem;
- overflow: scroll;
- max-height: calc(100% - 55px);
-}
-
-header {
- border-bottom: 1px solid var(--color-border);
-}
-</style> \ No newline at end of file
diff --git a/components/Editor/EditorSidebar.vue b/components/Editor/EditorSidebar.vue
deleted file mode 100644
index c9539fa..0000000
--- a/components/Editor/EditorSidebar.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<script setup lang="ts">
-import { useSessionStore } from '@/stores/session';
-import { storeToRefs } from 'pinia';
-import EditorSidebarCategory from '@/components/Editor/EditorSidebarCategory.vue';
-import EditorSidebarQuest from '@/components/Editor/EditorSidebarQuest.vue';
-
-const sessionStore = useSessionStore();
-
-const { session } = storeToRefs(sessionStore);
-</script>
-
-<template>
- <div id="sidebar-container">
- <EditorSidebarCategory v-for="category in session.categories" :key="category.id" :category="category" />
- <EditorSidebarQuest v-for="quest in session.quests.filter((q) => (!session.categories.some((c) => c.id === q.options.category)))" :key="quest.id" :quest="quest" />
- </div>
-</template>
-
-<style scoped>
-#sidebar-container {
- width: 20rem;
- border-right: 1px solid var(--color-border);
- height: calc(100vh - 73px);
- max-height: calc(100vh - 73px);
- background-color: var(--color-background);
- user-select: none;
-}
-</style> \ No newline at end of file
diff --git a/components/Editor/EditorSidebarCategory.vue b/components/Editor/EditorSidebarCategory.vue
deleted file mode 100644
index 071ebc8..0000000
--- a/components/Editor/EditorSidebarCategory.vue
+++ /dev/null
@@ -1,96 +0,0 @@
-<script setup lang="ts">
-import { useSessionStore, type EditorCategory } from '@/stores/session';
-import { computed, ref, toRefs } from 'vue';
-import { stripColorCodes } from '@/lib/util';
-import EditorSidebarQuest from '@/components/Editor/EditorSidebarQuest.vue';
-
-const props = defineProps<{
- category: EditorCategory;
-}>();
-
-const { category } = toRefs(props);
-
-const expanded = ref(true);
-
-const sessionStore = useSessionStore();
-const route = useRoute();
-
-const questsInCategory = computed(() => {
- return sessionStore.getQuestsInCategory(category.value.id);
-});
-
-const expandCategory = () => {
- expanded.value = !expanded.value;
-};
-
-const setSelectedCategory = () => {
- navigateTo({ path: `/category/${category.value.id}` })
-};
-
-const selected = computed(() => {
- return route.path.startsWith('/category') && route.params.id === category.value.id;
-});
-</script>
-
-<template>
- <div id="category-container" :class="{ selected: selected }">
- <span id="category-title" @click="setSelectedCategory">
- <font-awesome-icon @click.stop="expandCategory" class="category-icon"
- :icon="expanded ? ['fas', 'fa-caret-down'] : ['fas', 'fa-caret-up']" />
- <span id="category-name">
- <span id="category-display-name">{{ stripColorCodes(category.display.name) }}</span>
- <code id="category-display-id">{{ category.id }}</code>
- </span>
- </span>
- </div>
- <div v-if="expanded" id="quests">
- <EditorSidebarQuest class="quest" v-for="quest in questsInCategory" :key="quest.id" :quest="quest" />
- </div>
-</template>
-
-<style scoped>
-#category-container {
- cursor: pointer;
- padding: 0.5rem 1rem;
- transition: background-color 0.3s;
- border-bottom: 1px solid var(--color-border-soft);
-
- #category-title {
- display: flex;
- align-items: center;
- margin: 0;
- gap: 1rem;
- font-size: 1.1rem;
-
- #category-name {
- display: flex;
- flex-direction: column;
- align-items: left;
-
- #category-display-id {
- font-size: 0.8rem;
- color: var(--color-text-mute);
- }
- }
- }
-}
-
-.selected {
- background-color: var(--color-primary-mute);
-}
-
-#quests {
- background-color: var(--color-background-mute);
- border-bottom: 1px solid var(--color-border-soft);
-}
-
-.quest {
- margin: 0 0 0 1.4rem;
- background-color: var(--color-background);
- border-left: 1px solid var(--color-border);
-}
-
-#category-container:hover {
- background-color: var(--color-hover);
-}
-</style> \ No newline at end of file
diff --git a/components/Editor/EditorSidebarQuest.vue b/components/Editor/EditorSidebarQuest.vue
deleted file mode 100644
index a7b3e3f..0000000
--- a/components/Editor/EditorSidebarQuest.vue
+++ /dev/null
@@ -1,68 +0,0 @@
-<script setup lang="ts">
-import { useSessionStore, type EditorQuest } from '@/stores/session';
-import { computed, toRefs } from 'vue';
-import { stripColorCodes } from '@/lib/util';
-
-const props = defineProps<{
- quest: EditorQuest;
-}>();
-
-const { quest } = toRefs(props);
-
-const route = useRoute();
-
-const setSelectedQuest = () => {
- navigateTo({ path: `/quest/${quest.value.id}` })
-};
-
-const selected = computed(() => {
- return route.path.startsWith('/quest') && route.params.id === quest.value.id;
-});
-</script>
-
-<template>
- <div id="quest-container" @click.stop="setSelectedQuest" :class="{ selected: selected }">
- <span id="quest-title">
- <font-awesome-icon class="quest-icon" :icon="['far', 'fa-compass']" />
- <span id="quest-name">
- <span id="quest-display-name">{{ stripColorCodes(quest.display.name) }}</span>
- <code id="quest-display-id">{{ quest.id }}</code>
- </span>
- </span>
- </div>
-</template>
-
-<style scoped>
-#quest-container {
- cursor: pointer;
- padding: 0.3rem 1rem;
- transition: background-color 0.3s;
-
- #quest-title {
- display: flex;
- align-items: center;
- margin: 0;
- gap: 0.5rem;
- font-size: 0.8rem;
-
- #quest-name {
- display: flex;
- flex-direction: column;
- align-items: left;
-
- #quest-display-id {
- font-size: 0.6rem;
- color: var(--color-text-mute);
- }
- }
- }
-}
-
-.selected {
- background-color: var(--color-primary-mute) !important;
-}
-
-#quest-container:hover {
- background-color: var(--color-hover);
-}
-</style> \ No newline at end of file
diff --git a/components/Editor/Quest/Modal/DeleteQuestModal.vue b/components/Editor/Quest/Modal/DeleteQuestModal.vue
deleted file mode 100644
index d0b0c5a..0000000
--- a/components/Editor/Quest/Modal/DeleteQuestModal.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<script setup lang="ts">
-import Modal from '@/components/Control/Modal.vue';
-import Button from '@/components/Control/Button.vue';
-
-const model = defineModel();
-
-const emit = defineEmits(['delete']);
-
-defineProps({
- questId: String,
-});
-</script>
-
-<template>
- <Modal v-model="model">
- <template v-slot:header>
- <h2>Really delete quest '{{ questId }}'?</h2>
- </template>
- <p>Are you sure you want to delete this quest? The quests editor does not have undo functionality (yet)! </p>
- <div id="confirm" class="control-group">
- <Button
- :icon="['fas', 'fa-times']"
- :label="'Cancel'"
- @click="model = false"
- ></Button>
- <Button
- type="solid"
- :icon="['fas', 'fa-trash']"
- :label="'Delete'"
- @click="emit('delete')"
- ></Button>
- </div>
- </Modal>
-</template>
-
-<style scoped>
-#confirm {
- display: flex;
- justify-content: flex-end;
- margin-top: 1rem;
-}
-</style> \ No newline at end of file
diff --git a/components/Editor/Quest/Modal/DuplicateQuestModal.vue b/components/Editor/Quest/Modal/DuplicateQuestModal.vue
deleted file mode 100644
index bcd3782..0000000
--- a/components/Editor/Quest/Modal/DuplicateQuestModal.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<script setup lang="ts">
-import Modal from '@/components/Control/Modal.vue';
-import Button from '@/components/Control/Button.vue';
-import { computed, ref } from 'vue';
-import { useSessionStore } from '@/stores/session';
-
-const model = defineModel();
-
-const emit = defineEmits(['duplicate']);
-
-const props = defineProps({
- questId: String,
-});
-
-const session = useSessionStore();
-
-const newQuestId = ref(props.questId);
-
-const isDuplicate = computed(() => {
- return session.getQuestById(newQuestId.value!) !== undefined;
-});
-
-</script>
-
-<template>
- <Modal v-model="model">
- <template v-slot:header>
- <h2>Duplicate '{{ questId }}'</h2>
- </template>
-
- <template v-slot:body>
- <div id="body">
- <div class="option-group">
- <label for="new-type">ID of new quest</label>
- <input id="new-type" name="new-type" type="text" v-model="newQuestId" />
- </div>
- <p v-if="isDuplicate" class="error-text">Name is not unique.</p>
- <p>A Quest ID must be unique, alphanumeric, and not contain any spaces.</p>
- <div id="confirm" class="control-group">
- <Button
- :icon="['fas', 'fa-times']"
- :label="'Cancel'"
- @click="model = false"
- ></Button>
- <Button
- type="solid"
- :icon="['fas', 'fa-check']"
- :label="'Duplicate'"
- :disabled="isDuplicate"
- @click="emit('duplicate', newQuestId)"
- ></Button>
- </div>
- </div>
- </template>
- </Modal>
-</template>
-
-<style scoped>
-#confirm {
- display: flex;
- justify-content: flex-end;
-}
-
-#body {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-</style> \ No newline at end of file
diff --git a/components/Editor/Quest/Modal/RenameQuestModal.vue b/components/Editor/Quest/Modal/RenameQuestModal.vue
deleted file mode 100644
index 2ad1481..0000000
--- a/components/Editor/Quest/Modal/RenameQuestModal.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<script setup lang="ts">
-import Modal from '@/components/Control/Modal.vue';
-import Button from '@/components/Control/Button.vue';
-import { computed, ref } from 'vue';
-import { useSessionStore } from '@/stores/session';
-
-const model = defineModel();
-
-const emit = defineEmits(['update']);
-
-const props = defineProps({
- questId: String,
-});
-
-const session = useSessionStore();
-
-const newQuestId = ref(props.questId);
-
-const isDuplicate = computed(() => {
- return session.getQuestById(newQuestId.value!) !== undefined;
-});
-
-</script>
-
-<template>
- <Modal v-model="model">
- <template v-slot:header>
- <h2>Rename quest '{{ questId }}'</h2>
- </template>
-
- <template v-slot:body>
- <div id="body">
- <div class="option-group">
- <label for="new-type">New quest ID</label>
- <input id="new-type" name="new-type" type="text" v-model="newQuestId" />
- </div>
- <p v-if="isDuplicate" class="error-text">Name is not unique.</p>
- <p>A Quest ID must be unique, alphanumeric, and not contain any spaces.</p>
- <div id="confirm" class="control-group">
- <Button
- :icon="['fas', 'fa-times']"
- :label="'Cancel'"
- @click="model = false"
- ></Button>
- <Button
- type="solid"
- :icon="['fas', 'fa-check']"
- :label="'Rename'"
- :disabled="isDuplicate"
- @click="emit('update', newQuestId)"
- ></Button>
- </div>
- </div>
- </template>
- </Modal>
-</template>
-
-<style scoped>
-#confirm {
- display: flex;
- justify-content: flex-end;
-}
-
-#body {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-</style> \ No newline at end of file
diff --git a/components/Editor/Quest/QuestOptionsPanel.vue b/components/Editor/Quest/QuestOptionsPanel.vue
deleted file mode 100644
index a462126..0000000
--- a/components/Editor/Quest/QuestOptionsPanel.vue
+++ /dev/null
@@ -1,145 +0,0 @@
-<script setup lang="ts">
-import { useSessionStore, type EditorQuest } from '@/stores/session';
-import { computed, ref } from 'vue';
-import EditorOptionsPanel from '@/components/Editor/EditorOptionsPanel.vue';
-import Checkbox from '@/components/Control/Checkbox.vue';
-
-const props = defineProps<{
- questId: string;
-}>();
-
-const sessionStore = useSessionStore();
-
-const quest = computed(() => {
- return sessionStore.getQuestById(props.questId) as EditorQuest;
-});
-const knownCategories = computed(() => {
- return sessionStore.session.categories.map((category) => category.id);
-});
-const knownQuests = computed(() => {
- return sessionStore.session.quests.map((quest) => quest.id);
-});
-
-</script>
-
-<template>
- <EditorOptionsPanel v-if="quest">
- <div id="options">
- <div class="option-group">
- <label for="quest-category">Category</label>
- <multiselect
- id="quest-category"
- v-model="quest.options.category"
- :options="knownCategories"
- :searchable="true"
- placeholder="No category"></multiselect>
- </div>
-
- <div class="option-group">
- <label for="quest-requirements">Requirements</label>
- <multiselect
- id="quest-requirements"
- v-model="quest.options.requirements"
- :options="knownQuests"
- :searchable="true"
- :taggable="true"
- :multiple="true"
- placeholder="Add requirement"></multiselect>
- <p class="description">
- This quest will only be available if the player has completed all of the quests listed above.
- </p>
- </div>
-
- <h2>Quest options</h2>
-
- <div class="option-group">
- <Checkbox id="quest-permissionrequired" label="Require permission to start quest"
- description="Players must have permission to start the quest." v-model="quest.options.permissionRequired" />
- </div>
-
- <div class="option-group">
- <Checkbox id="quest-cancellable" label="Allow players to cancel quest"
- description="Players can cancel the quest after they have started it." v-model="quest.options.cancellable" />
- </div>
-
- <div class="option-group">
- <Checkbox id="quest-countstowardslimit" label="Count towards quest limit"
- description="Quest will count towards the player's quest started limit."
- v-model="quest.options.countsTowardsLimit" />
- </div>
-
- <div class="option-group">
- <Checkbox id="quest-repeatable" label="Allow players to repeat quest"
- description="Quest can be completed again after it has been completed once."
- v-model="quest.options.repeatable" />
- </div>
-
- <div class="option-group">
- <Checkbox id="quest-autostart" label="Automatically start quest"
- description="Quest will start automatically when the player has unlocked it."
- v-model="quest.options.autostart" />
- </div>
-
-
- <h2>Cooldown</h2>
-
- <div class="option-group">
- <Checkbox id="quest-cooldown" label="Enable cooldown"
- description="Players will have to wait a certain amount of time before they can start the quest again."
- v-model="quest.options.cooldown.enabled" />
- </div>
-
- <div class="option-group">
- <label for="quest-cooldown-time">
- Cooldown (in seconds)
- </label>
- <input id="quest-cooldown-time" type="number" v-model="quest.options.cooldown.time"
- :disabled="!quest.options.cooldown.enabled" />
- <p class="description">
- Common values are: <code>3600</code> (1 hour), <code>86400</code> (24 hours), <code>604800</code> (7 days),
- <code>2592000</code> (30 days)
- </p>
- </div>
-
- <h2>Time limit</h2>
-
- <div class="option-group">
- <Checkbox id="quest-timelimit" label="Enable time limit"
- description="Players will be required to complete the quest within a certain amount of time, otherwise it will be automatically cancelled."
- v-model="quest.options.timeLimit.enabled" />
- </div>
-
- <div class="option-group">
- <label for="quest-timelimit-time">
- Time limit (in seconds)
- </label>
- <input id="quest-timelimit-time" type="number" v-model="quest.options.timeLimit.time"
- :disabled="!quest.options.timeLimit.enabled" />
- <p class="description">
- Common values are: <code>3600</code> (1 hour), <code>86400</code> (24 hours), <code>604800</code> (7 days),
- <code>2592000</code> (30 days)
- </p>
- </div>
- </div>
- </EditorOptionsPanel>
-</template>
-
-<style src="vue-multiselect/dist/vue-multiselect.css" />
-
-<style>
-#options {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.description {
- font-size: 0.8em;
-}
-
-h2 {
- border-bottom: 1px solid var(--color-border);
-}
-
-</style>
-
diff --git a/components/Editor/Quest/QuestTasksOptionsPanel.vue b/components/Editor/Quest/QuestTasksOptionsPanel.vue
deleted file mode 100644
index a79e636..0000000
--- a/components/Editor/Quest/QuestTasksOptionsPanel.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-<script setup lang="ts">
-import { useSessionStore, type EditorQuest } from '@/stores/session';
-import { computed, ref } from 'vue';
-import EditorOptionsPanel from '@/components/Editor/EditorOptionsPanel.vue';
-import TaskConfiguration from '@/components/Editor/Quest/Task/TaskConfiguration.vue';
-import Button from '@/components/Control/Button.vue';
-import AddTaskModal from './Task/Modal/AddTaskModal.vue';
-
-const props = defineProps<{
- questId: string;
-}>();
-
-const sessionStore = useSessionStore();
-
-const quest = computed(() => {
- return sessionStore.getQuestById(props.questId) as EditorQuest;
-});
-
-const showAddTaskModal = ref(false);
-
-const addTask = (newId: string, newType: string) => {
- sessionStore.getQuestById(props.questId)!.tasks[newId] = {
- id: newId,
- config: {
- type: newType,
- },
- };
-
- showAddTaskModal.value = false;
-};
-</script>
-
-<template>
- <EditorOptionsPanel v-if="quest">
- <div id="options">
- <h2>Tasks <code>({{ Object.keys(quest.tasks).length }})</code></h2>
-
- <p v-if="Object.keys(quest.tasks).length === 0" class="error-text">This quest does not have any tasks.</p>
- <TaskConfiguration v-for="(task, taskId) in quest.tasks" :key="taskId" :taskId="String(taskId)" :quest="quest" />
-
- <div id="controls">
- <Button
- id="add-task"
- :icon="['fas', 'fa-plus']"
- type="solid"
- label="Add task"
- @click="showAddTaskModal = true"
- />
- </div>
- </div>
- </EditorOptionsPanel>
-
- <AddTaskModal
- v-if="quest"
- v-model="showAddTaskModal"
- :questId="questId"
- @add="addTask"
- />
-</template>
-
-
-<style scoped>
-#options {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-
- #controls {
- display: flex;
- justify-content: flex-end;
- gap: 1rem;
- }
-}
-
-.option-group {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.description {
- font-size: 0.8em;
-}
-
-label {
- font-weight: 700;
-}
-
-h2 {
- border-bottom: 1px solid var(--color-border);
-
- code {
- font-size: 0.8em;
- color: var(--color-text-mute);
- }
-}
-</style>
-
diff --git a/components/Editor/Quest/Task/Modal/AddTaskModal.vue b/components/Editor/Quest/Task/Modal/AddTaskModal.vue
deleted file mode 100644
index 57139bb..0000000
--- a/components/Editor/Quest/Task/Modal/AddTaskModal.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-<script setup lang="ts">
-import Modal from '@/components/Control/Modal.vue';
-import Button from '@/components/Control/Button.vue';
-import { computed, ref } from 'vue';
-import { useSessionStore } from '@/stores/session';
-import { validateTaskId } from '@/lib/util';
-
-const model = defineModel();
-
-const emit = defineEmits(['add']);
-
-const session = useSessionStore();
-
-const props = defineProps({
- questId: {
- type: String,
- required: true,
- },
-});
-
-const knownTasks = computed(() => session.getQuestById(props.questId)!.tasks);
-const knownTaskTypes = computed(() => session.getKnownTaskTypes());
-
-const newId = ref('');
-const newType = ref('');
-const unknownTaskType = computed(() => !knownTaskTypes.value.includes(newType.value));
-const invalidTaskId = computed(() => !validateTaskId(newId.value));
-const duplicateTaskId = computed(() => knownTasks.value[newId.value] !== undefined);
-
-const newTypeDescription = computed(() => session.getTaskDefinitionByTaskType(newType.value)?.description);
-</script>
-
-<template>
- <Modal v-model="model">
- <template v-slot:header>
- <h2>Add new task</h2>
- </template>
-
- <template v-slot:body>
- <div id="body">
- <div class="option-group">
- <label for="new-type">Task ID</label>
- <input id="new-id" name="new-id" type="text" v-model="newId" />
- <p v-if="invalidTaskId" class="error-text">Invalid task ID.</p>
- <p v-if="duplicateTaskId" class="error-text">Task ID already exists.</p>
- </div>
- <div class="option-group">
- <label for="new-type">Task type</label>
- <multiselect
- id="new-type"
- v-model="newType"
- :options="knownTaskTypes"
- :searchable="true"
- placeholder="Select a new type"
- ></multiselect>
- <p v-if="unknownTaskType" class="error-text">Invalid task type.</p>
- </div>
- <p v-if="newTypeDescription">{{ newTypeDescription }}</p>
- <p>A task ID must be unique, alphanumeric, and not contain any spaces.</p>
- <div id="confirm" class="control-group">
- <Button
- :icon="['fas', 'fa-times']"
- :label="'Cancel'"
- @click="model = false"
- ></Button>
- <Button
- type="solid"
- :icon="['fas', 'fa-check']"
- :label="'Confirm'"
- :disabled="unknownTaskType || invalidTaskId || duplicateTaskId"
- @click="emit('add', newId, newType)"
- ></Button>
- </div>
- </div>
- </template>
- </Modal>
-</template>
-
-<style scoped>
-#confirm {
- display: flex;
- justify-content: flex-end;
-}
-
-#body {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-</style> \ No newline at end of file
diff --git a/components/Editor/Quest/Task/Modal/ChangeTaskModal.vue b/components/Editor/Quest/Task/Modal/ChangeTaskModal.vue
deleted file mode 100644
index c6b5921..0000000
--- a/components/Editor/Quest/Task/Modal/ChangeTaskModal.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-<script setup lang="ts">
-import Modal from '@/components/Control/Modal.vue';
-import Button from '@/components/Control/Button.vue';
-import { computed, ref } from 'vue';
-import { useSessionStore } from '@/stores/session';
-
-const model = defineModel();
-
-const emit = defineEmits(['update']);
-
-const session = useSessionStore();
-
-const props = defineProps({
- taskId: String,
- currentTaskType: String,
-});
-
-const knownTaskTypes = computed(() => session.getKnownTaskTypes());
-
-const newType = ref('');
-const unknownTaskType = computed(() => !knownTaskTypes.value.includes(newType.value));
-const noChange = computed(() => newType.value === props.currentTaskType);
-const newTypeDescription = computed(() => session.getTaskDefinitionByTaskType(newType.value)?.description);
-</script>
-
-<template>
- <Modal v-model="model">
- <template v-slot:header>
- <h2>Change the task type of '{{ taskId }}'</h2>
- </template>
-
- <template v-slot:body>
- <div id="body">
- <div class="option-group">
- <label for="new-type">New type</label>
- <multiselect
- id="new-type"
- v-model="newType"
- :options="knownTaskTypes"
- :searchable="true"
- placeholder="Select a new type"
- ></multiselect>
- </div>
- <p v-if="unknownTaskType" class="error-text">Invalid task type.</p>
- <p v-if="newTypeDescription">{{ newTypeDescription }}</p>
- <p>Any configured options for this task will be overwritten.</p>
- <div id="confirm" class="control-group">
- <Button
- :icon="['fas', 'fa-times']"
- :label="'Cancel'"
- @click="model = false"
- ></Button>
- <Button
- type="solid"
- :icon="['fas', 'fa-check']"
- :label="'Change'"
- :disabled="unknownTaskType || noChange"
- @click="emit('update', newType)"
- ></Button>
- </div>
- </div>
- </template>
- </Modal>
-</template>
-
-<style scoped>
-#confirm {
- display: flex;
- justify-content: flex-end;
-}
-
-#body {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-</style> \ No newline at end of file
diff --git a/components/Editor/Quest/Task/TaskConfiguration.vue b/components/Editor/Quest/Task/TaskConfiguration.vue
deleted file mode 100644
index 0646ad4..0000000
--- a/components/Editor/Quest/Task/TaskConfiguration.vue
+++ /dev/null
@@ -1,209 +0,0 @@
-<script setup lang="ts">
-import { useSessionStore, type EditorQuest } from '@/stores/session';
-import { computed, ref } from 'vue';
-import Button from '@/components/Control/Button.vue';
-import TaskConfigurationRow from '@/components/Editor/Quest/Task/TaskConfigurationRow.vue';
-import ChangeTaskModal from './Modal/ChangeTaskModal.vue';
-
-const props = defineProps<{
- taskId: string;
- quest: EditorQuest;
-}>();
-
-const sessionStore = useSessionStore();
-
-const taskType = computed(() => props.quest.tasks[props.taskId].config.type);
-const taskDefintion = computed(() => sessionStore.getTaskDefinitionByTaskType(taskType.value));
-
-const taskConfig = computed(() => {
- return Object.keys(props.quest.tasks[props.taskId].config).filter((fieldName) => fieldName !== 'type').reduce((acc, fieldName) => {
- acc[fieldName] = props.quest.tasks[props.taskId].config[fieldName];
- return acc;
- }, {} as { [key: string]: any });
-});
-
-const requiredFields = computed(() => {
- return Object.keys(taskDefintion.value.configuration).filter((fieldName) => taskDefintion.value.configuration[fieldName].required);
-});
-
-const givenRequiredFields = computed(() => {
- return requiredFields.value.filter((fieldName) => taskConfig.value[fieldName]);
-});
-
-const missingFields = computed(() => {
- return requiredFields.value.filter((fieldName) => !props.quest.tasks[props.taskId].config[fieldName]);
-});
-
-const remainingGivenFields = computed(() => {
- return Object.keys(taskConfig.value).filter((fieldName) => !requiredFields.value.includes(fieldName));
-});
-
-const configKeysOptions = computed(() => Object.keys(taskDefintion.value.configuration).filter((key) => !Object.keys(taskConfig.value).some((fieldName) => fieldName === key)));
-// const configKeysOptions = computed(() => {
-// const keys = Object.keys(taskDefintion.value.configuration).filter((key) => !Object.keys(taskConfig.value).some((fieldName) => fieldName === key));
-//
-// return keys.map((key) => {
-// return {
-// value: key,
-// description: taskDefintion.value.configuration[key].description,
-// note: taskDefintion.value.configuration[key].note,
-// };
-// });
-// });
-
-const onAddOption = (option: any) => {
- sessionStore.getQuestById(props.quest.id)!.tasks[props.taskId].config[option] = taskDefintion.value.configuration[option].default || null;
-};
-
-const updateValue = (fieldName: string, value: any) => {
- sessionStore.getQuestById(props.quest.id)!.tasks[props.taskId].config[fieldName] = value;
-};
-
-const deleteValue = (fieldName: string) => {
- delete sessionStore.getQuestById(props.quest.id)!.tasks[props.taskId].config[fieldName];
-};
-
-const showChangeModal = ref(false);
-
-const updateTaskType = (newType: string) => {
- sessionStore.getQuestById(props.quest.id)!.tasks[props.taskId].config = {
- type: newType
- };
- showChangeModal.value = false;
-}
-
-const deleteTaskType = (taskId: string) => {
- delete sessionStore.getQuestById(props.quest.id)!.tasks[taskId];
-}
-</script>
-
-<template>
- <div id="task-configuration-table">
- <div id="task-header">
- <p id="task-id">
- <span id="task-name">
- {{ props.taskId }}
- </span>
- <code>
- ({{ taskType }})
- </code>
- </p>
- <div id="task-controls" class="control-group">
- <Button
- :icon="['fas', 'fa-pen']"
- :label="'Change'"
- @click="showChangeModal = true"
- ></Button>
- <Button
- :icon="['fas', 'fa-trash']"
- :label="'Delete'"
- @click="deleteTaskType(props.taskId)"
- ></Button>
- </div>
- </div>
- <div id="task-configuration">
- <div v-if="!taskDefintion" class="error">
- <font-awesome-icon id="error-icon" :icon="['fas', 'fa-triangle-exclamation']"/>
- <p id="error-message">
- Unable to edit task <code>{{ props.taskId }}</code>.
- </p>
- <p id="error-description">
- The quests web editor does not know how to configure task
- type <code>{{ taskType }}</code> as it has no task definition.
- </p>
- </div>
-
- <div v-if="taskDefintion">
- <TaskConfigurationRow
- v-for="fieldName in [...givenRequiredFields, ...missingFields, ...remainingGivenFields]"
- :key="`${quest.id}-${props.taskId}-${taskType}-${fieldName}`"
- :required="requiredFields.includes(fieldName)"
- :configKey="fieldName"
- :initialValue="taskConfig[fieldName]"
- :taskType="taskType"
- :type="(taskDefintion.configuration[fieldName].type as string)"
- @update="newValue => updateValue(fieldName, newValue)"
- @delete="() => deleteValue(fieldName)"
- />
- <div id="add-option">
- <multiselect
- class="multiselect"
- :options="configKeysOptions"
- :searchable="true"
- @select="onAddOption"
- placeholder="Add option...">
- </multiselect>
- </div>
- </div>
- </div>
- </div>
-
- <ChangeTaskModal
- v-model="showChangeModal"
- :taskId="props.taskId"
- :currentTaskType="taskType"
- :key="`change-task-${props.taskId}`"
- @update="updateTaskType"
- />
-</template>
-
-<style scoped>
-.error {
- padding: 0.5rem 0.5rem 0.5rem calc(0.5rem + 20px);
-
- #error-icon {
- float: left;
- margin: 5px 0 0 -20px;
- }
-
- #error-message {
- font-weight: 700;
- }
-
-}
-
-#task-configuration-table {
- display: flex;
- flex-direction: column;
- border: 1px solid var(--color-border);
-
- #task-header {
- display: flex;
- justify-content: space-between;
- border-bottom: 1px solid var(--color-border);
- background-color: var(--color-background-soft);
- padding: 0.5rem;
-
- #task-id {
- font-size: 1.2em;
-
- #task-name {
- font-weight: 700;
- }
-
- code {
- font-size: 0.8em;
- color: var(--color-text-mute);
- }
- }
- }
-}
-
-#add-option {
- width: 100%;
- border-right: 1px solid var(--color-border);
- border-top: 1px solid var(--color-border);
-}
-
-.multiselect::v-deep .multiselect__tags {
- border: none !important;
- border-radius: 0px !important;
- background: transparent !important;
-}
-
-.multiselect::v-deep .multiselect__select {
- background: transparent !important;
-}
-
-</style>
-
diff --git a/components/Editor/Quest/Task/TaskConfigurationRow.vue b/components/Editor/Quest/Task/TaskConfigurationRow.vue
deleted file mode 100644
index f68ce97..0000000
--- a/components/Editor/Quest/Task/TaskConfigurationRow.vue
+++ /dev/null
@@ -1,195 +0,0 @@
-<script setup lang="ts">
-import { useSessionStore } from '@/stores/session';
-import { computed, ref, toRefs, watch } from 'vue';
-import TrueFalseSwitch from '@/components/Control/TrueFalseSwitch.vue';
-import ItemStackPicker from '@/components/Control/ItemStackPicker.vue';
-import materials from '@/lib/materials';
-
-const props = defineProps({
- taskType: {
- type: String,
- required: true,
- },
- configKey: {
- type: String,
- required: true,
- },
- initialValue: null,
- type: String,
- required: Boolean,
-});
-const emit = defineEmits(['update', 'delete']);
-
-const sessionStore = useSessionStore();
-
-const definition = computed(() => {
- const def = sessionStore.getTaskDefinitionByTaskType(props.taskType).configuration[props.configKey];
- return { description: def.description, note: def.note };
-});
-
-const { description, note } = toRefs(definition.value);
-const showDescription = ref(false);
-const currentValue = ref(props.initialValue ||
- (props.type === 'boolean'
- ? false
- : (props.type === 'material-list' || props.type === 'string-list'
- ? []
- : props.type === 'itemstack'
- ? null
- : ''
- )));
-
-if (props.initialValue !== currentValue.value) {
- emit('update', currentValue.value);
-}
-
-const error = computed(() => currentValue.value === undefined || currentValue.value === null || currentValue.value === '' || (Array.isArray(currentValue.value) && currentValue.value.length === 0));
-const updateValue = (value: any) => {
- currentValue.value = value;
-};
-
-watch(currentValue, () => {
- emit('update', currentValue.value);
-});
-
-const addValue = (searchQuery: any) => {
- currentValue.value.push(searchQuery);
-};
-
-</script>
-
-<template>
- <div id="task-configuration-row">
- <div id="key">
- <div id="delete" @click="emit('delete')" v-if="!props.required" class="delete">
- <font-awesome-icon :icon="['fas', 'fa-xmark']" />
- </div>
- <p id="name" @click="showDescription = !showDescription">{{ props.configKey }}</p>
- </div>
- <div id="value">
- <div id="value-container">
- <!-- Data type 'string' -->
- <input v-if="props.type === 'string'" v-model="currentValue" />
-
- <!-- Data type 'number' -->
- <input v-else-if="props.type === 'number'" type="number" v-model="currentValue" />
-
- <!-- Data type 'boolean' -->
- <TrueFalseSwitch v-else-if="props.type === 'boolean'" :value="!!currentValue" @update="updateValue" />
-
- <!-- Data type 'material-list' -->
- <multiselect v-else-if="props.type === 'material-list'" v-model="currentValue"
- :options="materials" :multiple="true" :taggable="true" :searchable="true" placeholder="Enter material name" />
-
- <!-- Data type 'string-list' -->
- <multiselect v-else-if="props.type === 'string-list'" v-model="currentValue" :options="[]" @tag="addValue"
- :multiple="true" :taggable="true" :searchable="true" placeholder="Enter string" />
-
- <!-- Data type 'itemstack' -->
- <ItemStackPicker v-else-if="props.type === 'itemstack'" :value="currentValue" @update="updateValue" />
-
- <div v-if="showDescription || error" id="task-configuration-row-info">
- <p v-if="error" class="error">A value is required.</p>
- <p>{{ description }} <i>{{ note }}</i></p>
- </div>
- </div>
- </div>
- </div>
-</template>
-
-<style scoped>
-#task-configuration-row {
- display: flex;
- flex-direction: row;
- transition: background-color 0.3s;
- border-bottom: 1px solid var(--color-border);
-
- #key {
- width: 25%;
- background-color: var(--color-background);
- display: flex;
- flex-direction: row;
- align-items: center;
- user-select: none;
-
- #delete {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 100%;
- cursor: pointer;
- color: var(--color-text-mute);
- border-right: 1px solid var(--color-border);
- background-color: var(--color-background-soft);
- transition: color 0.3s;
-
- &:hover {
- color: var(--color-false);
- }
- }
-
- #name {
- display: flex;
- align-items: center;
- font-size: 0.8rem;
- padding: 0.5rem;
- width: 100%;
- height: 100%;
- font-family: monospace;
- cursor: pointer;
- transition: background-color 0.3s;
-
- &:hover {
- background-color: var(--color-hover);
- }
- }
- }
-
- #value {
- width: 75%;
- background-color: var(--color-background);
- border-left: 1px solid var(--color-border);
-
- #value-container {
- display: flex;
- flex-direction: column;
- height: 100%;
- }
- }
-}
-
-#task-configuration-row:hover {
- background-color: var(--color-hover);
-}
-
-#task-configuration-row-info {
- padding: 0.25rem 0.5rem;
- font-size: 0.8em;
- background-color: var(--color-background);
- border-top: 1px solid var(--color-border);
-}
-
-input {
- width: 100%;
- padding: 0.5rem;
- border-radius: 0;
- border: none;
- font-family: monospace;
- font-size: 0.8rem;
- height: 40px;
-}
-
-.error {
- color: var(--color-false);
-}
-
-.multiselect::v-deep .multiselect__tags {
- border: unset !important;
- border-radius: 0px !important;
-}
-
-.multiselect::v-deep .multiselect__select {
- background: unset !important;
-}
-</style> \ No newline at end of file