diff options
| author | Leonardo Bishop <me@leonardobishop.com> | 2024-02-15 14:01:30 +0000 |
|---|---|---|
| committer | Leonardo Bishop <me@leonardobishop.com> | 2024-02-15 14:01:30 +0000 |
| commit | 2aca4247c5d0c7061a300517178dd31316b65fab (patch) | |
| tree | 10e145c2b57d76c778bdf1a11191495dcfe191f3 /src/components/Editor | |
Initial commit
Diffstat (limited to 'src/components/Editor')
| -rw-r--r-- | src/components/Editor/Category/CategoryChildrenOptionsPanel.vue | 53 | ||||
| -rw-r--r-- | src/components/Editor/Category/CategoryOptionsPanel.vue | 54 | ||||
| -rw-r--r-- | src/components/Editor/EditorOptionsPanel.vue | 18 | ||||
| -rw-r--r-- | src/components/Editor/EditorPane.vue | 157 | ||||
| -rw-r--r-- | src/components/Editor/EditorSidebar.vue | 28 | ||||
| -rw-r--r-- | src/components/Editor/EditorSidebarCategory.vue | 94 | ||||
| -rw-r--r-- | src/components/Editor/EditorSidebarQuest.vue | 68 | ||||
| -rw-r--r-- | src/components/Editor/Quest/QuestOptionsPanel.vue | 154 | ||||
| -rw-r--r-- | src/components/Editor/Quest/QuestTasksOptionsPanel.vue | 75 | ||||
| -rw-r--r-- | src/components/Editor/Quest/Task/TaskConfiguration.vue | 191 | ||||
| -rw-r--r-- | src/components/Editor/Quest/Task/TaskConfigurationRow.vue | 156 |
11 files changed, 1048 insertions, 0 deletions
diff --git a/src/components/Editor/Category/CategoryChildrenOptionsPanel.vue b/src/components/Editor/Category/CategoryChildrenOptionsPanel.vue new file mode 100644 index 0000000..6e96f64 --- /dev/null +++ b/src/components/Editor/Category/CategoryChildrenOptionsPanel.vue @@ -0,0 +1,53 @@ +<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/src/components/Editor/Category/CategoryOptionsPanel.vue b/src/components/Editor/Category/CategoryOptionsPanel.vue new file mode 100644 index 0000000..f7d548c --- /dev/null +++ b/src/components/Editor/Category/CategoryOptionsPanel.vue @@ -0,0 +1,54 @@ +<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/src/components/Editor/EditorOptionsPanel.vue b/src/components/Editor/EditorOptionsPanel.vue new file mode 100644 index 0000000..1415d84 --- /dev/null +++ b/src/components/Editor/EditorOptionsPanel.vue @@ -0,0 +1,18 @@ +<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/src/components/Editor/EditorPane.vue b/src/components/Editor/EditorPane.vue new file mode 100644 index 0000000..6245e56 --- /dev/null +++ b/src/components/Editor/EditorPane.vue @@ -0,0 +1,157 @@ +<script setup lang="ts"> +import { useSessionStore } from '@/stores/session'; +import { computed } from 'vue'; +import { stripColourCodes } 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'; + +const sessionStore = useSessionStore(); + +const selectedType = computed(() => sessionStore.editor.selected.type); +const selectedId = computed(() => sessionStore.editor.selected.id); +const selectedName = computed(() => { + if (selectedType.value === 'Quest') { + return sessionStore.getQuestById(selectedId.value)?.display.name; + } else if (selectedType.value === 'Category') { + return sessionStore.getCategoryById(selectedId.value)?.display.name; + } else { + return ''; + } +}); + +const categoryFromSelectedQuest = computed(() => { + const quest = sessionStore.getQuestById(selectedId.value); + if (quest) { + return sessionStore.getCategoryById(quest.options.category) || null; + } else { + return null; + } +}); +</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']"/> + {{ stripColourCodes(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">{{ stripColourCodes(selectedName!) }} </span> + <code>({{ selectedId }})</code> + </template> + <template v-if="selectedType === 'Category'"> + <font-awesome-icon class="icon" :icon="['fas', 'fa-folder']"/> + <span class="title">{{ stripColourCodes(selectedName!) }} </span> + <code>({{ selectedId }})</code> + </template> + </span> + <span id="controls"> + <Button + v-if="selectedType === 'Quest'" + :icon="['fas', 'fa-code']" + :label="'YAML'" + ></Button> + <Button + :icon="['fas', 'fa-pen']" + :label="'Rename'" + ></Button> + <Button + :icon="['fas', 'fa-trash']" + :label="'Delete'" + ></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> +</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); + } + } + + #controls { + display: flex; + gap: 1rem; + } +} + +.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/src/components/Editor/EditorSidebar.vue b/src/components/Editor/EditorSidebar.vue new file mode 100644 index 0000000..c9539fa --- /dev/null +++ b/src/components/Editor/EditorSidebar.vue @@ -0,0 +1,28 @@ +<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/src/components/Editor/EditorSidebarCategory.vue b/src/components/Editor/EditorSidebarCategory.vue new file mode 100644 index 0000000..932b36b --- /dev/null +++ b/src/components/Editor/EditorSidebarCategory.vue @@ -0,0 +1,94 @@ +<script setup lang="ts"> +import { useSessionStore, type EditorCategory } from '@/stores/session'; +import { computed, ref, toRefs } from 'vue'; +import { stripColourCodes } 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 questsInCategory = computed(() => { + return sessionStore.getQuestsInCategory(category.value.id); +}); + +const expandCategory = () => { + expanded.value = !expanded.value; +}; + +const setSelectedCategory = () => { + sessionStore.setEditorSelected('Category', category.value.id); +}; + +const selected = computed(() => { + return sessionStore.editor.selected.type === 'Category' && sessionStore.editor.selected.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">{{ stripColourCodes(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/src/components/Editor/EditorSidebarQuest.vue b/src/components/Editor/EditorSidebarQuest.vue new file mode 100644 index 0000000..08f1625 --- /dev/null +++ b/src/components/Editor/EditorSidebarQuest.vue @@ -0,0 +1,68 @@ +<script setup lang="ts"> +import { useSessionStore, type EditorQuest } from '@/stores/session'; +import { computed, toRefs } from 'vue'; +import { stripColourCodes } from '@/lib/util'; + +const props = defineProps<{ + quest: EditorQuest; +}>(); + +const { quest } = toRefs(props); + +const sessionStore = useSessionStore(); + +const setSelectedQuest = () => { + sessionStore.setEditorSelected('Quest', quest.value.id); +}; + +const selected = computed(() => { + return sessionStore.editor.selected.type === 'Quest' && sessionStore.editor.selected.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">{{ stripColourCodes(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/src/components/Editor/Quest/QuestOptionsPanel.vue b/src/components/Editor/Quest/QuestOptionsPanel.vue new file mode 100644 index 0000000..3495d60 --- /dev/null +++ b/src/components/Editor/Quest/QuestOptionsPanel.vue @@ -0,0 +1,154 @@ +<script setup lang="ts"> +import { useSessionStore, type EditorQuest } from '@/stores/session'; +import { computed } 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; +} + +.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/src/components/Editor/Quest/QuestTasksOptionsPanel.vue b/src/components/Editor/Quest/QuestTasksOptionsPanel.vue new file mode 100644 index 0000000..12b1263 --- /dev/null +++ b/src/components/Editor/Quest/QuestTasksOptionsPanel.vue @@ -0,0 +1,75 @@ +<script setup lang="ts"> +import { useSessionStore, type EditorQuest } from '@/stores/session'; +import { computed } 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'; + +const props = defineProps<{ + questId: string; +}>(); + +const sessionStore = useSessionStore(); + +const quest = computed(() => { + return sessionStore.getQuestById(props.questId) as EditorQuest; +}); +</script> + +<template> + <EditorOptionsPanel v-if="quest"> + <div id="options"> + <h2>Tasks <code>({{ Object.keys(quest.tasks).length }})</code></h2> + + <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" + /> + </div> + </div> + </EditorOptionsPanel> +</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/src/components/Editor/Quest/Task/TaskConfiguration.vue b/src/components/Editor/Quest/Task/TaskConfiguration.vue new file mode 100644 index 0000000..5c6613a --- /dev/null +++ b/src/components/Editor/Quest/Task/TaskConfiguration.vue @@ -0,0 +1,191 @@ +<script setup lang="ts"> +import { useSessionStore, type EditorQuest } from '@/stores/session'; +import { computed } from 'vue'; +import Button from '@/components/Control/Button.vue'; +import TaskConfigurationRow from '@/components/Editor/Quest/Task/TaskConfigurationRow.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]; +}; + +</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"> + <Button + :icon="['fas', 'fa-pen']" + :label="'Change'" + ></Button> + <Button + :icon="['fas', 'fa-trash']" + :label="'Delete'" + ></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="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> +</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); + } + } + + #task-controls { + display: flex; + gap: 1rem; + } + } +} + +#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/src/components/Editor/Quest/Task/TaskConfigurationRow.vue b/src/components/Editor/Quest/Task/TaskConfigurationRow.vue new file mode 100644 index 0000000..fb872a8 --- /dev/null +++ b/src/components/Editor/Quest/Task/TaskConfigurationRow.vue @@ -0,0 +1,156 @@ +<script setup lang="ts"> +import { useSessionStore } from '@/stores/session'; +import { computed, ref, toRefs, watch } from 'vue'; +import TrueFalseSwitch from '@/components/Control/TrueFalseSwitch.vue'; +import materials from '@/data/materials.json'; + +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); + +const error = computed(() => currentValue.value === undefined || currentValue.value === null || currentValue.value === ''); +const updateValue = (value: any) => { + currentValue.value = value; +}; + +watch(currentValue, () => { + emit('update', currentValue.value); +}); + +</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"> + <input v-if="props.type === 'string'" v-model="currentValue" /> + <input v-else-if="props.type === 'number'" type="number" v-model="currentValue" /> + <TrueFalseSwitch v-else-if="props.type === 'boolean'" :value="!!currentValue" @update="updateValue" /> + <multiselect v-else-if="props.type === 'material-list'" :value="currentValue" :options="materials" + :multiple="true" :taggable="true" :searchable="true" placeholder="Enter material name"></multiselect> + </div> + <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> +</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); + } +} + +#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 |
