aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/Editor/Quest/Task
diff options
context:
space:
mode:
authorLeonardo Bishop <me@leonardobishop.com>2024-02-15 14:01:30 +0000
committerLeonardo Bishop <me@leonardobishop.com>2024-02-15 14:01:30 +0000
commit2aca4247c5d0c7061a300517178dd31316b65fab (patch)
tree10e145c2b57d76c778bdf1a11191495dcfe191f3 /src/components/Editor/Quest/Task
Initial commit
Diffstat (limited to 'src/components/Editor/Quest/Task')
-rw-r--r--src/components/Editor/Quest/Task/TaskConfiguration.vue191
-rw-r--r--src/components/Editor/Quest/Task/TaskConfigurationRow.vue156
2 files changed, 347 insertions, 0 deletions
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