diff options
| author | Leonardo Bishop <me@leonardobishop.com> | 2024-02-15 23:04:33 +0000 |
|---|---|---|
| committer | Leonardo Bishop <me@leonardobishop.com> | 2024-02-15 23:04:33 +0000 |
| commit | 1195b085e31c44bc8fec6817d64063de9022eb66 (patch) | |
| tree | 6f30cbfbf30acc19347890080cbc907ad094106b | |
| parent | 1869b5c5f9565b5e9e20697c4401a2f9ba9f2c3a (diff) | |
Partially add itemstack support
| -rw-r--r-- | src/assets/base.css | 4 | ||||
| -rw-r--r-- | src/components/Control/ItemStackForm.vue | 50 | ||||
| -rw-r--r-- | src/components/Control/ItemStackModal.vue | 172 | ||||
| -rw-r--r-- | src/components/Control/ItemStackPicker.vue | 100 | ||||
| -rw-r--r-- | src/components/Control/Modal.vue | 3 | ||||
| -rw-r--r-- | src/components/Editor/EditorPane.vue | 8 | ||||
| -rw-r--r-- | src/components/Editor/EditorSidebarCategory.vue | 4 | ||||
| -rw-r--r-- | src/components/Editor/EditorSidebarQuest.vue | 4 | ||||
| -rw-r--r-- | src/components/Editor/Quest/QuestTasksOptionsPanel.vue | 25 | ||||
| -rw-r--r-- | src/components/Editor/Quest/Task/Modal/AddTaskModal.vue | 90 | ||||
| -rw-r--r-- | src/components/Editor/Quest/Task/Modal/ChangeTaskModal.vue | 24 | ||||
| -rw-r--r-- | src/components/Editor/Quest/Task/TaskConfiguration.vue | 16 | ||||
| -rw-r--r-- | src/components/Editor/Quest/Task/TaskConfigurationRow.vue | 10 | ||||
| -rw-r--r-- | src/data/taskDefinitions.json | 43 | ||||
| -rw-r--r-- | src/lib/materials.ts | 3 | ||||
| -rw-r--r-- | src/lib/util.ts | 19 | ||||
| -rw-r--r-- | src/main.ts | 5 |
17 files changed, 561 insertions, 19 deletions
diff --git a/src/assets/base.css b/src/assets/base.css index 8331bf0..5e116e8 100644 --- a/src/assets/base.css +++ b/src/assets/base.css @@ -143,6 +143,10 @@ input[type="checkbox"] { accent-color: var(--color-primary); } +input[type="radio"] { + accent-color: var(--color-primary); +} + .multiselect__input { background: var(--color-background-soft) !important ; color: inherit !important; diff --git a/src/components/Control/ItemStackForm.vue b/src/components/Control/ItemStackForm.vue new file mode 100644 index 0000000..250e8c9 --- /dev/null +++ b/src/components/Control/ItemStackForm.vue @@ -0,0 +1,50 @@ +<script setup lang="ts"> +import { computed } from 'vue'; +import materials from '@/lib/materials'; + +const model = defineModel<any>(); + +if (typeof model.value !== 'object' || model.value === null) { + model.value = {}; +} + +const itemName = computed({ + get() { + return model.value.name; + }, + set(newValue: string) { + model.value.name = newValue; + }, +}); + +const itemType = computed({ + get() { + return model.value.type || model.value.material || model.value.item; + }, + set(newValue: string) { + if (model.value.material) { + model.value.material = newValue; + } else if (model.value.item) { + model.value.item = newValue; + } else { + model.value.type = newValue; + } + }, +}); +</script> + +<template> + <div class="option-group"> + <label for="itemstack-name">Name</label> + <input id="itemstack-name" name="itemstack-name" v-model="itemName" placeholder="Enter a display name" /> + </div> + + <div class="option-group"> + <label for="itemstack-name">Type</label> + <multiselect v-model="itemType" + :options="materials" :searchable="true" placeholder="Choose a material" /> + </div> +</template> + +<style scoped> +</style>
\ No newline at end of file diff --git a/src/components/Control/ItemStackModal.vue b/src/components/Control/ItemStackModal.vue new file mode 100644 index 0000000..642c5f9 --- /dev/null +++ b/src/components/Control/ItemStackModal.vue @@ -0,0 +1,172 @@ +<script setup lang="ts"> +import Modal from '@/components/Control/Modal.vue'; +import Button from '@/components/Control/Button.vue'; +import { computed, ref } from 'vue'; +import materials from '@/lib/materials'; +import ItemStackForm from './ItemStackForm.vue'; + +const model = defineModel(); + +const emit = defineEmits(['confirm']); + +const props = defineProps({ + value: String, +}); + +//TODO unshitify +const value = ref<any>(props.value); + +const isQuestItem = computed(() => { + return value.value?.['quest-item'] !== undefined; +}); +const isItemStack = computed(() => { + return ( + typeof value.value === 'object' + && ( + value.value?.item !== undefined + || value.value?.type !== undefined + || value.value?.material !== undefined + )) +}); +const isMaterial = computed(() => { + return typeof value.value === 'string' && materials.includes(value.value) +}); + +const selectedType = ref( + isQuestItem.value + ? 'questitem' + : isItemStack.value + ? 'itemstack' + : isMaterial.value + ? 'material' + : '' +); + +const noTypeSelected = computed(() => selectedType.value === ''); +const noValue = computed(() => !isQuestItem.value && !isItemStack.value && !isMaterial.value); + +const setSelectedType = (type: string) => { + if (type === 'questitem') { + value.value = {}; + } else if (type === 'itemstack') { + value.value = {}; + } else if (type === 'material') { + value.value = ''; + } + selectedType.value = type; +}; + +const confirm = () => { + emit('confirm', value.value); +}; +</script> + +<template> + <Modal v-model="model"> + <template v-slot:header> + <h2>Edit ItemStack</h2> + </template> + + <template v-slot:body> + <div id="type"> + <span class="option" @click="setSelectedType('questitem')" :class="{selected: selectedType === 'questitem'}"> + <span> + <font-awesome-icon :icon="['fas', 'fa-tag']" /> + Quest Item + </span> + <p v-if="noTypeSelected">Re-use a quest item.</p> + </span> + <span class="option" @click="setSelectedType('itemstack')" :class="{selected: selectedType === 'itemstack'}"> + <span> + <font-awesome-icon :icon="['fas', 'fa-cube']" /> + ItemStack + </span> + <p v-if="noTypeSelected">Define a new item stack.</p> + </span> + <span class="option" @click="setSelectedType('material')" :class="{selected: selectedType === 'material'}"> + <span> + <font-awesome-icon :icon="['fas', 'fa-apple-whole']" /> + Material + </span> + <p v-if="noTypeSelected">Define a specific item type.</p> + </span> + </div> + + <div id="material" class="option-group" v-if="selectedType === 'material'"> + <label for="material">Material</label> + <multiselect v-model="value" + :options="materials" :searchable="true" placeholder="Enter material name" /> + </div> + + <div id="itemstack" class="option-group" v-if="selectedType === 'itemstack'"> + <ItemStackForm v-model="value" /> + </div> + + + <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'" + @click="confirm" + ></Button> + <!-- :disabled="noTypeSelected || noValue" --> + </div> + </template> + </Modal> +</template> + +<style scoped> +#confirm { + display: flex; + justify-content: flex-end; + margin-top: 1rem; +} + +#type { + display: flex; + justify-content: space-around; + gap: 0.25rem; + user-select: none; + margin-bottom: 1rem; + + .option { + border: 1px solid var(--color-border); + cursor: pointer; + display: flex; + flex-direction: column; + flex-basis: 0; + flex-grow: 1; + align-items: center; + gap: 0.5rem; + padding: 0.5rem; + background-color: var(--color-background-soft); + transition: background-color 0.3s; + + span { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 700; + } + + p { + text-align: center; + font-size: 0.8rem; + } + + &:hover { + background-color: var(--color-hover); + } + + &.selected { + background-color: var(--color-primary-mute); + } + } +} +</style>
\ No newline at end of file diff --git a/src/components/Control/ItemStackPicker.vue b/src/components/Control/ItemStackPicker.vue new file mode 100644 index 0000000..613adfe --- /dev/null +++ b/src/components/Control/ItemStackPicker.vue @@ -0,0 +1,100 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import ItemStackModal from './ItemStackModal.vue'; +import materials from '@/lib/materials'; + +const props = defineProps<{ + value: any; +}>(); +const emit = defineEmits(['update']); + +const value = ref(props.value); + +const showItemStackModal = ref(false); + +//TODO unshitify +const empty = computed(() => { + return value.value === undefined + || value.value === null + || value.value === '' + || (Array.isArray(value.value) && value.value.length === 0) + || (typeof value.value === 'object' && Object.keys(value.value).length === 0); +}); +const isQuestItem = computed(() => { + return value.value?.['quest-item'] !== undefined; +}); +const isItemStack = computed(() => { + if (typeof value.value !== 'object') { + return false; + } + + const key = 'item' in value.value + ? 'item' + : 'type' in value.value + ? 'type' + : 'material' + + return (!!value.value[key]); +}); +const isMaterial = computed(() => { + return typeof value.value === 'string' && materials.includes(value.value) +}); + +const update = (newValue: any) => { + value.value = newValue; + showItemStackModal.value = false; + emit('update', value.value); +}; +</script> + +<template> + <div class="itemstack" @click="showItemStackModal = true"> + <span v-if="empty" class="empty">ItemStack...</span> + <span v-if="isQuestItem" class="item"><font-awesome-icon :icon="['fas', 'fa-tag']" /> Quest Item</span> + <span v-if="isItemStack" class="item"><font-awesome-icon :icon="['fas', 'fa-cube']" /> ItemStack of '{{ value.type || value.item || value.material }}'</span> + <span v-if="isMaterial" class="item"><font-awesome-icon :icon="['fas', 'fa-apple-whole']" /> {{ value }}</span> + <span v-if="!empty && !isQuestItem && !isItemStack && !isMaterial" class="invalid"><font-awesome-icon :icon="['fas', 'fa-triangle-exclamation']" /> Invalid ItemStack</span> + </div> + + <ItemStackModal + v-model="showItemStackModal" + :value="value" + @confirm="update" + /> +</template> + +<style scoped> +.itemstack { + display: flex; + width: 100%; + height: 100%; + cursor: pointer; + align-items: center; + padding: 0.5rem; + user-select: none; + transition: background-color 0.3s; + background-color: var(--color-background-soft); + + span { + font-family: monospace; + font-size: 0.8rem; + } + + .empty { + color: var(--color-text-mute); + } + + .item { + color: var(--color-primary); + } + + .invalid { + color: var(--color-false); + } + + &:hover { + background-color: var(--color-hover); + } +} + +</style>
\ No newline at end of file diff --git a/src/components/Control/Modal.vue b/src/components/Control/Modal.vue index d47d281..46d5da5 100644 --- a/src/components/Control/Modal.vue +++ b/src/components/Control/Modal.vue @@ -29,6 +29,7 @@ const model = defineModel(); background-color: rgba(0, 0, 0, 0.5); transition: opacity 0.3s; display: none; + overflow: visible; } .modal-content { @@ -38,7 +39,7 @@ const model = defineModel(); width: 100%; max-width: 600px; max-height: 80%; - overflow-y: auto; + overflow-y: visible; border-radius: 4px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); } diff --git a/src/components/Editor/EditorPane.vue b/src/components/Editor/EditorPane.vue index 21d1f80..bf9532a 100644 --- a/src/components/Editor/EditorPane.vue +++ b/src/components/Editor/EditorPane.vue @@ -1,7 +1,7 @@ <script setup lang="ts"> import { useSessionStore } from '@/stores/session'; import { computed, ref } from 'vue'; -import { stripColourCodes } from '@/lib/util'; +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'; @@ -70,16 +70,16 @@ const duplicateQuest = (oldId: string, newId: string) => { <template v-if="selectedType === 'Quest'"> <template v-if="categoryFromSelectedQuest"> <font-awesome-icon class="icon" :icon="['fas', 'fa-folder']"/> - {{ stripColourCodes(categoryFromSelectedQuest?.display.name) }} + {{ 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">{{ stripColourCodes(selectedName!) }} </span> + <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">{{ stripColourCodes(selectedName!) }} </span> + <span class="title">{{ stripColorCodes(selectedName!) }} </span> <code>({{ selectedId }})</code> </template> </span> diff --git a/src/components/Editor/EditorSidebarCategory.vue b/src/components/Editor/EditorSidebarCategory.vue index 932b36b..7153b92 100644 --- a/src/components/Editor/EditorSidebarCategory.vue +++ b/src/components/Editor/EditorSidebarCategory.vue @@ -1,7 +1,7 @@ <script setup lang="ts"> import { useSessionStore, type EditorCategory } from '@/stores/session'; import { computed, ref, toRefs } from 'vue'; -import { stripColourCodes } from '@/lib/util'; +import { stripColorCodes } from '@/lib/util'; import EditorSidebarQuest from '@/components/Editor/EditorSidebarQuest.vue'; const props = defineProps<{ @@ -36,7 +36,7 @@ const selected = computed(() => { <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> + <span id="category-display-name">{{ stripColorCodes(category.display.name) }}</span> <code id="category-display-id">{{ category.id }}</code> </span> </span> diff --git a/src/components/Editor/EditorSidebarQuest.vue b/src/components/Editor/EditorSidebarQuest.vue index 08f1625..baf06f1 100644 --- a/src/components/Editor/EditorSidebarQuest.vue +++ b/src/components/Editor/EditorSidebarQuest.vue @@ -1,7 +1,7 @@ <script setup lang="ts"> import { useSessionStore, type EditorQuest } from '@/stores/session'; import { computed, toRefs } from 'vue'; -import { stripColourCodes } from '@/lib/util'; +import { stripColorCodes } from '@/lib/util'; const props = defineProps<{ quest: EditorQuest; @@ -25,7 +25,7 @@ const selected = computed(() => { <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> + <span id="quest-display-name">{{ stripColorCodes(quest.display.name) }}</span> <code id="quest-display-id">{{ quest.id }}</code> </span> </span> diff --git a/src/components/Editor/Quest/QuestTasksOptionsPanel.vue b/src/components/Editor/Quest/QuestTasksOptionsPanel.vue index 12b1263..a79e636 100644 --- a/src/components/Editor/Quest/QuestTasksOptionsPanel.vue +++ b/src/components/Editor/Quest/QuestTasksOptionsPanel.vue @@ -1,9 +1,10 @@ <script setup lang="ts"> import { useSessionStore, type EditorQuest } from '@/stores/session'; -import { computed } from 'vue'; +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; @@ -14,6 +15,19 @@ 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> @@ -21,6 +35,7 @@ const quest = computed(() => { <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"> @@ -29,10 +44,18 @@ const quest = computed(() => { :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> diff --git a/src/components/Editor/Quest/Task/Modal/AddTaskModal.vue b/src/components/Editor/Quest/Task/Modal/AddTaskModal.vue new file mode 100644 index 0000000..57139bb --- /dev/null +++ b/src/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/src/components/Editor/Quest/Task/Modal/ChangeTaskModal.vue b/src/components/Editor/Quest/Task/Modal/ChangeTaskModal.vue index f8ffef7..c6b5921 100644 --- a/src/components/Editor/Quest/Task/Modal/ChangeTaskModal.vue +++ b/src/components/Editor/Quest/Task/Modal/ChangeTaskModal.vue @@ -1,17 +1,26 @@ <script setup lang="ts"> import Modal from '@/components/Control/Modal.vue'; import Button from '@/components/Control/Button.vue'; -import { ref } from 'vue'; +import { computed, ref } from 'vue'; +import { useSessionStore } from '@/stores/session'; const model = defineModel(); const emit = defineEmits(['update']); -defineProps({ +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> @@ -24,8 +33,16 @@ const newType = ref(''); <div id="body"> <div class="option-group"> <label for="new-type">New type</label> - <input id="new-type" name="new-type" type="text" v-model="newType" /> + <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 @@ -37,6 +54,7 @@ const newType = ref(''); type="solid" :icon="['fas', 'fa-check']" :label="'Change'" + :disabled="unknownTaskType || noChange" @click="emit('update', newType)" ></Button> </div> diff --git a/src/components/Editor/Quest/Task/TaskConfiguration.vue b/src/components/Editor/Quest/Task/TaskConfiguration.vue index 7006f18..0646ad4 100644 --- a/src/components/Editor/Quest/Task/TaskConfiguration.vue +++ b/src/components/Editor/Quest/Task/TaskConfiguration.vue @@ -65,6 +65,16 @@ const deleteValue = (fieldName: string) => { 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> @@ -87,6 +97,7 @@ const showChangeModal = ref(false); <Button :icon="['fas', 'fa-trash']" :label="'Delete'" + @click="deleteTaskType(props.taskId)" ></Button> </div> </div> @@ -105,7 +116,7 @@ const showChangeModal = ref(false); <div v-if="taskDefintion"> <TaskConfigurationRow v-for="fieldName in [...givenRequiredFields, ...missingFields, ...remainingGivenFields]" - :key="`${quest.id}-${props.taskId}-${fieldName}`" + :key="`${quest.id}-${props.taskId}-${taskType}-${fieldName}`" :required="requiredFields.includes(fieldName)" :configKey="fieldName" :initialValue="taskConfig[fieldName]" @@ -130,6 +141,9 @@ const showChangeModal = ref(false); <ChangeTaskModal v-model="showChangeModal" :taskId="props.taskId" + :currentTaskType="taskType" + :key="`change-task-${props.taskId}`" + @update="updateTaskType" /> </template> diff --git a/src/components/Editor/Quest/Task/TaskConfigurationRow.vue b/src/components/Editor/Quest/Task/TaskConfigurationRow.vue index d77e450..f68ce97 100644 --- a/src/components/Editor/Quest/Task/TaskConfigurationRow.vue +++ b/src/components/Editor/Quest/Task/TaskConfigurationRow.vue @@ -2,7 +2,8 @@ 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'; +import ItemStackPicker from '@/components/Control/ItemStackPicker.vue'; +import materials from '@/lib/materials'; const props = defineProps({ taskType: { @@ -33,7 +34,9 @@ const currentValue = ref(props.initialValue || ? false : (props.type === 'material-list' || props.type === 'string-list' ? [] - : '' + : props.type === 'itemstack' + ? null + : '' ))); if (props.initialValue !== currentValue.value) { @@ -81,6 +84,9 @@ const addValue = (searchQuery: any) => { <!-- 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> diff --git a/src/data/taskDefinitions.json b/src/data/taskDefinitions.json index 8aba912..443493c 100644 --- a/src/data/taskDefinitions.json +++ b/src/data/taskDefinitions.json @@ -89,6 +89,49 @@ "description": "The worlds in which the blocks should be broken." } } + }, + "inventory": { + "description": "Obtain a set amount of items.", + "configuration": { + "amount": { + "type": "number", + "description": "The amount of items to obtain.", + "default": 1, + "required": true + }, + "item": { + "type": "itemstack", + "description": "The specific item to obtain.", + "required": true + }, + "data": { + "type": "number", + "description": "The data value of the item to obtain.", + "default": 0, + "note": "Not required for Minecraft versions 1.13 and above." + }, + "remove-items-when-complete": { + "type": "boolean", + "description": "Whether the items should be removed from the player's inventory when the quest is complete.", + "default": false, + "note": "If allow-partial-completion is true, this will be set to true as well." + }, + "allow-partial-completion": { + "type": "boolean", + "description": "Whether the quest can be completed with less than the required amount of items.", + "default": false, + "note": "Setting to true will imply remove-items-when-complete is true as well. If a player obtains any matching item, it will be immediately taken away from them and added towards the quest progress." + }, + "exact-match": { + "type": "boolean", + "description": "Whether the item must match this item exactly, including lore and enchantments.", + "default": false + }, + "worlds": { + "type": "string-list", + "description": "The worlds in which the items should be obtained." + } + } } } }
\ No newline at end of file diff --git a/src/lib/materials.ts b/src/lib/materials.ts new file mode 100644 index 0000000..36c5aee --- /dev/null +++ b/src/lib/materials.ts @@ -0,0 +1,3 @@ +import materials from '@/data/materials.json'; + +export default materials;
\ No newline at end of file diff --git a/src/lib/util.ts b/src/lib/util.ts index 9903e2c..b8be8cb 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -1,3 +1,18 @@ -export function stripColourCodes(str: string): string { - return str.replace(/&[0-9a-fk-or]/i, ''); +const COLOR_CODE_REGEX = /&[0-9a-fk-or]/i; +const VALID_ID_REGEX = /^[a-z0-9_]+$/i; + +export function stripColorCodes(str: string): string { + return str.replace(COLOR_CODE_REGEX, ''); +} + +export function validateQuestId(id: string): boolean { + return VALID_ID_REGEX.test(id); +} + +export function validateCategoryId(id: string): boolean { + return VALID_ID_REGEX.test(id); +} + +export function validateTaskId(id: string): boolean { + return VALID_ID_REGEX.test(id); }
\ No newline at end of file diff --git a/src/main.ts b/src/main.ts index f2d6127..74e9999 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { library } from "@fortawesome/fontawesome-svg-core"; -import { faFolder, faCaretDown, faCaretUp, faChevronRight, faTriangleExclamation, faPen, faTrash, faCode, faCheck, faXmark, faPlus, faCopy, faSave } from "@fortawesome/free-solid-svg-icons"; +import { faFolder, faCaretDown, faCaretUp, faChevronRight, faTriangleExclamation, faPen, faTrash, faCode, faCheck, faXmark, faPlus, faCopy, faSave, faCube, faTag, faAppleWhole } from "@fortawesome/free-solid-svg-icons"; import { faCompass } from '@fortawesome/free-regular-svg-icons'; import App from './App.vue' @@ -27,6 +27,9 @@ library.add(faXmark); library.add(faPlus); library.add(faCopy); library.add(faSave); +library.add(faCube); +library.add(faTag); +library.add(faAppleWhole); app.component('font-awesome-icon', FontAwesomeIcon) app.component('multiselect', Multiselect) |
