diff options
Diffstat (limited to 'components/Editor/Quest/Task')
| -rw-r--r-- | components/Editor/Quest/Task/Modal/AddTaskModal.vue | 90 | ||||
| -rw-r--r-- | components/Editor/Quest/Task/Modal/ChangeTaskModal.vue | 77 | ||||
| -rw-r--r-- | components/Editor/Quest/Task/TaskConfiguration.vue | 209 | ||||
| -rw-r--r-- | components/Editor/Quest/Task/TaskConfigurationRow.vue | 195 |
4 files changed, 571 insertions, 0 deletions
diff --git a/components/Editor/Quest/Task/Modal/AddTaskModal.vue b/components/Editor/Quest/Task/Modal/AddTaskModal.vue new file mode 100644 index 0000000..57139bb --- /dev/null +++ b/components/Editor/Quest/Task/Modal/AddTaskModal.vue @@ -0,0 +1,90 @@ +<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 new file mode 100644 index 0000000..c6b5921 --- /dev/null +++ b/components/Editor/Quest/Task/Modal/ChangeTaskModal.vue @@ -0,0 +1,77 @@ +<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 new file mode 100644 index 0000000..0646ad4 --- /dev/null +++ b/components/Editor/Quest/Task/TaskConfiguration.vue @@ -0,0 +1,209 @@ +<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 new file mode 100644 index 0000000..f68ce97 --- /dev/null +++ b/components/Editor/Quest/Task/TaskConfigurationRow.vue @@ -0,0 +1,195 @@ +<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 |
