1
0
forked from noxious/client

Create characters WIP

This commit is contained in:
Dennis Postma 2025-02-21 03:14:46 +01:00
parent ed0a02a795
commit dc2afba82b

View File

@ -10,8 +10,9 @@
<div class="filler"></div> <div class="filler"></div>
<div class="w-2/3 max-w-[860px]" v-if="!isLoading"> <div class="w-2/3 max-w-[860px]" v-if="!isLoading">
<div class="mb-5 flex flex-col gap-1"> <div class="mb-5 flex flex-col gap-1">
<h1 class="text-white font-bold">SELECT CHARACTER TO PLAY</h1> <h1 class="text-white font-bold">{{ isCreatingCharacter ? 'CREATE CHARACTER' : 'SELECT CHARACTER TO PLAY' }}</h1>
<p class="m-0">Maximum of 4 characters can be created per player</p> <p class="m-0" v-if="!isCreatingCharacter">Maximum of 4 characters can be created per player</p>
<p class="m-0" v-if="isCreatingCharacter">Some text lol</p>
</div> </div>
<div class="flex w-full max-lg:flex-col lg:h-[400px] default-border rounded-md bg-gray"> <div class="flex w-full max-lg:flex-col lg:h-[400px] default-border rounded-md bg-gray">
<div class="lg:min-w-[285px] max-lg:min-h-[383px] lg:w-1/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center border-0 max-lg:border-b lg:border-r border-solid border-gray-500 max-lg:rounded-t-md lg:rounded-l-md relative"> <div class="lg:min-w-[285px] max-lg:min-h-[383px] lg:w-1/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center border-0 max-lg:border-b lg:border-r border-solid border-gray-500 max-lg:rounded-t-md lg:rounded-l-md relative">
@ -20,13 +21,14 @@
<img src="/assets/placeholders/head.png" class="w-9 h-9 object-contain center-element" alt="Player head" /> <img src="/assets/placeholders/head.png" class="w-9 h-9 object-contain center-element" alt="Player head" />
<input class="h-full w-full absolute m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 btn-sound" type="radio" name="character" :value="character.id" v-model="selectedCharacterId" /> <input class="h-full w-full absolute m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 btn-sound" type="radio" name="character" :value="character.id" v-model="selectedCharacterId" />
</div> </div>
<div class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')]" :class="{ active: characters.length == 0 }" v-if="characters.length < 4"> <div class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')]" :class="{ active: isCreatingCharacter }" v-if="characters.length < characterCreationSettings.maxCharacters">
<button class="p-0 h-full w-full flex flex-col justify-between focus-visible:outline-offset-0 btn-sound" @click="isCreateNewCharacterModalOpen = true"> <button class="p-0 h-full w-full flex flex-col justify-between focus-visible:outline-offset-0 btn-sound" @click="startCharacterCreation">
<img class="w-6 h-6 object-contain center-element btn-sound" draggable="false" src="/assets/icons/plus-icon.svg" alt="Add character" /> <img class="w-6 h-6 object-contain center-element btn-sound" draggable="false" src="/assets/icons/plus-icon.svg" alt="Add character" />
</button> </button>
</div> </div>
</div> </div>
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6 justify-center" v-if="selectedCharacterId"> <div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6 justify-center">
<template v-if="selectedCharacterId && !isCreatingCharacter">
<input class="input-field w-[158px]" type="text" name="name" :placeholder="characters.find((c) => c.id == selectedCharacterId)?.name" v-model="newNickname" /> <input class="input-field w-[158px]" type="text" name="name" :placeholder="characters.find((c) => c.id == selectedCharacterId)?.name" v-model="newNickname" />
<div class="flex flex-col gap-4 items-center"> <div class="flex flex-col gap-4 items-center">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
@ -40,29 +42,43 @@
</button> </button>
</div> </div>
</div> </div>
<!-- <div class="flex justify-between w-[190px]">-->
<!-- <button class="btn-empty flex gap-2 selected">-->
<!-- <img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />-->
<!-- <span class="text-white">Male</span>-->
<!-- </button>-->
<!-- <button class="btn-empty flex gap-2">-->
<!-- <img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />-->
<!-- <span class="text-white">Female</span>-->
<!-- </button>-->
<!-- </div>-->
</div> </div>
</template>
<template v-if="isCreatingCharacter">
<div class="form-field-full mb-6">
<input class="input-field" v-model="newCharacterName" name="name" id="name" placeholder="Enter a nickname..." />
</div>
<div class="bg-[url('/assets/ui-elements/character-select-ui-shape.svg')] w-[190px] h-52 bg-no-repeat bg-center flex items-center justify-center">
<img class="w-24 object-contain mb-3.5 max-h-[70%]" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + defaultCharacterTypeId + '/' + (selectedHairId ?? 'default')" />
</div>
<div class="flex justify-between w-[190px]">
<button class="btn-empty flex gap-2" :class="{ selected: selectedGender === 'MALE' }" @click="selectedGender = 'MALE'">
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
<span class="text-white">Male</span>
</button>
<button class="btn-empty flex gap-2" :class="{ selected: selectedGender === 'FEMALE' }" @click="selectedGender = 'FEMALE'">
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Female symbol" />
<span class="text-white">Female</span>
</button>
</div>
</template>
</div> </div>
</div> </div>
<div class="flex-1 lg:w-2/3 max-lg:min-h-[212px] h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center max-lg:rounded-bl-md rounded-r-md"> <div class="flex-1 lg:w-2/3 max-lg:min-h-[212px] h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center max-lg:rounded-bl-md rounded-r-md">
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-10" v-if="selectedCharacterId"> <div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-10" v-if="selectedCharacterId || isCreatingCharacter">
<div class="flex flex-col gap-3 w-full"> <div class="flex flex-col gap-3 w-full">
<span class="text-sm">Hair color</span> <span class="text-sm">Hair color</span>
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
<div class="hair-deselect relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-white focus-visible:bg-cyan has-[:checked]:bg-cyan has-[:checked]:border-transparent"> <div
class="hair-deselect relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-white focus-visible:bg-cyan has-[:checked]:bg-cyan has-[:checked]:border-transparent"
>
<img src="/assets/icons/x-button-gray.svg" class="w-4 h-4" alt="Empty button" /> <img src="/assets/icons/x-button-gray.svg" class="w-4 h-4" alt="Empty button" />
<input type="radio" name="hair-color" :value="null" v-model="selectedHairColor" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" /> <input type="radio" name="hair-color" :value="null" v-model="selectedHairColor" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
</div> </div>
<div v-for="color in uniqueHairColors" class="relative flex justify-center items-center default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent"> <div
v-for="color in uniqueHairColors"
class="relative flex justify-center items-center default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent"
>
<div class="w-full h-full rounded-sm" :style="getHairColorStyle(color)"></div> <div class="w-full h-full rounded-sm" :style="getHairColorStyle(color)"></div>
<input type="radio" name="hair-color" :value="color" v-model="selectedHairColor" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" /> <input type="radio" name="hair-color" :value="color" v-model="selectedHairColor" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
</div> </div>
@ -94,14 +110,20 @@
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" /> <img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
</div> </div>
<div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading"> <div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading">
<template v-if="!isCreatingCharacter">
<button class="btn-empty min-w-48" @click.stop="gameStore.disconnectSocket()">Back</button> <button class="btn-empty min-w-48" @click.stop="gameStore.disconnectSocket()">Back</button>
<button class="btn-cyan min-w-48 disabled:bg-cyan-800 disabled:cursor-not-allowed" :disabled="!selectedCharacterId" @click="loginWithCharacter()">Play now</button> <button class="btn-cyan min-w-48 disabled:bg-cyan-800 disabled:cursor-not-allowed" :disabled="!selectedCharacterId" @click="loginWithCharacter()">Play now</button>
</template>
<template v-else>
<button class="btn-empty min-w-48" @click="cancelCharacterCreation">Cancel</button>
<button class="btn-cyan min-w-48" @click="createCharacter">Create</button>
</template>
</div> </div>
</div> </div>
</div> </div>
<!-- CREATE CHARACTER MODAL --> <!-- CREATE CHARACTER MODAL -->
<Modal :isModalOpen="isCreateNewCharacterModalOpen" @modal:close="isCreateNewCharacterModalOpen = false" :modal-width="430" :modal-height="275"> <Modal :isModalOpen="isCreateNewCharacterModalOpen" @modal:close="isCreateNewCharacterModalOpen = false" :modal-width="800" :modal-height="500">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium text-white">Create your character</h3> <h3 class="m-0 font-medium text-white">Create your character</h3>
</template> </template>
@ -109,10 +131,65 @@
<template #modalBody> <template #modalBody>
<div class="p-4 h-[calc(100%_-_32px)]"> <div class="p-4 h-[calc(100%_-_32px)]">
<form method="post" @submit.prevent="createCharacter" class="h-full flex flex-col justify-between"> <form method="post" @submit.prevent="createCharacter" class="h-full flex flex-col justify-between">
<div class="form-field-full"> <div class="flex gap-8">
<div class="w-1/2">
<div class="form-field-full mb-6">
<label for="name" class="text-white">Nickname</label> <label for="name" class="text-white">Nickname</label>
<input class="input-field" v-model="newCharacterName" name="name" id="name" placeholder="Enter a nickname..." /> <input class="input-field" v-model="newCharacterName" name="name" id="name" placeholder="Enter a nickname..." />
</div> </div>
<div class="flex flex-col gap-3 w-full mb-6">
<span class="text-sm">Hair color</span>
<div class="flex gap-2 flex-wrap">
<div
class="hair-deselect relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-white focus-visible:bg-cyan has-[:checked]:bg-cyan has-[:checked]:border-transparent"
>
<img src="/assets/icons/x-button-gray.svg" class="w-4 h-4" alt="Empty button" />
<input type="radio" name="new-hair-color" :value="null" v-model="selectedHairColor" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
</div>
<div
v-for="color in uniqueHairColors"
class="relative flex justify-center items-center default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent"
>
<div class="w-full h-full rounded-sm" :style="{ backgroundColor: color }"></div>
<input type="radio" name="new-hair-color" :value="color" v-model="selectedHairColor" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
</div>
</div>
</div>
<div class="flex flex-col gap-3 w-full">
<span class="text-sm">Hairstyle</span>
<div class="flex gap-2 flex-wrap max-h-20 overflow-y-auto scrollbar">
<div
class="hair-deselect relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-white focus-visible:bg-cyan has-[:checked]:bg-cyan has-[:checked]:border-transparent"
>
<img src="/assets/icons/x-button-gray.svg" class="w-4 h-4" alt="Empty button" />
<input type="radio" name="new-hair" :value="null" v-model="selectedHairId" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
</div>
<div
v-for="hair in filteredHairs"
class="relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent"
>
<img class="h-16 -m-5 mt-4 object-contain" :src="config.server_endpoint + '/textures/sprites/' + hair.sprite + '/front.png'" alt="Hair sprite" />
<input type="radio" name="new-hair" :value="hair.id" v-model="selectedHairId" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
</div>
</div>
</div>
</div>
<div class="w-1/2 flex-col items-center justify-center">
<div class="bg-[url('/assets/ui-elements/character-select-ui-shape.svg')] w-[190px] h-52 bg-no-repeat bg-center flex items-center justify-center">
<img class="w-24 object-contain mb-3.5 max-h-[70%]" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + defaultCharacterTypeId + '/' + (selectedHairId ?? 'default')" />
</div>
<div class="flex justify-between w-[190px]">
<button class="btn-empty flex gap-2 selected">
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
<span class="text-white">Male</span>
</button>
<button class="btn-empty flex gap-2">
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
<span class="text-white">Female</span>
</button>
</div>
</div>
</div>
<div class="grid grid-flow-col justify-stretch gap-4"> <div class="grid grid-flow-col justify-stretch gap-4">
<button type="button" class="btn-empty py-1.5 px-4 inline-block" @click.prevent="isCreateNewCharacterModalOpen = false">Cancel</button> <button type="button" class="btn-empty py-1.5 px-4 inline-block" @click.prevent="isCreateNewCharacterModalOpen = false">Cancel</button>
<button class="btn-cyan py-1.5 px-4 inline-block" type="submit">Create</button> <button class="btn-cyan py-1.5 px-4 inline-block" type="submit">Create</button>
@ -130,10 +207,17 @@ import { type CharacterHair, type Character as CharacterT, type Map } from '@/ap
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useSoundComposable } from '@/composables/useSoundComposable' import { useSoundComposable } from '@/composables/useSoundComposable'
import { socketManager } from '@/managers/SocketManager' import { socketManager } from '@/managers/SocketManager'
import { CharacterHairStorage } from '@/storage/storages' import { CharacterHairStorage, CharacterTypeStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const characterCreationSettings = {
maxCharacters: 4,
minNameLength: 3,
maxNameLength: 20,
defaultGender: 'MALE' as const
}
const { playSound } = useSoundComposable() const { playSound } = useSoundComposable()
const gameStore = useGameStore() const gameStore = useGameStore()
const isLoading = ref<boolean>(true) const isLoading = ref<boolean>(true)
@ -144,6 +228,9 @@ const newNickname = ref<string>('')
const newCharacterName = ref<string>('') const newCharacterName = ref<string>('')
const characterHairs = ref<CharacterHair[]>([]) const characterHairs = ref<CharacterHair[]>([])
const selectedHairId = ref<string | null>(null) const selectedHairId = ref<string | null>(null)
const defaultCharacterTypeId = ref<string>('')
const isCreatingCharacter = ref<boolean>(false)
const selectedGender = ref()
const selectedHairColor = ref<string | null>(null) const selectedHairColor = ref<string | null>(null)
const uniqueHairColors = computed(() => { const uniqueHairColors = computed(() => {
@ -170,6 +257,22 @@ function getHairColorStyle(color: string | null) {
} }
} }
function startCharacterCreation() {
isCreatingCharacter.value = true
selectedCharacterId.value = null
newCharacterName.value = ''
selectedHairId.value = null
selectedHairColor.value = null
selectedGender.value = characterCreationSettings.defaultGender
}
function cancelCharacterCreation() {
isCreatingCharacter.value = false
newCharacterName.value = ''
selectedHairId.value = null
selectedHairColor.value = null
}
// Fetch characters // Fetch characters
setTimeout(() => { setTimeout(() => {
socketManager.emit(SocketEvent.CHARACTER_LIST) socketManager.emit(SocketEvent.CHARACTER_LIST)
@ -198,23 +301,46 @@ function loginWithCharacter() {
// Create character logics // Create character logics
function createCharacter() { function createCharacter() {
socketManager.emit(SocketEvent.CHARACTER_CREATE, { name: newCharacterName.value }, (success: boolean) => { if (newCharacterName.value.length < characterCreationSettings.minNameLength || newCharacterName.value.length > characterCreationSettings.maxNameLength) {
if (success) return return
isCreateNewCharacterModalOpen.value = false }
})
socketManager.emit(
SocketEvent.CHARACTER_CREATE,
{
name: newCharacterName.value,
gender: selectedGender.value,
hairId: selectedHairId.value
},
(success: boolean) => {
if (success) {
cancelCharacterCreation()
socketManager.emit(SocketEvent.CHARACTER_LIST)
}
}
)
} }
// Watch changes for selected character and update hairs // Watch changes for selected character and update hairs
watch(selectedCharacterId, (characterId) => { watch(selectedCharacterId, (characterId) => {
if (!characterId) return if (!characterId) return
newNickname.value = '' newNickname.value = ''
isCreatingCharacter.value = false
selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHair ?? null selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHair ?? null
}) })
onMounted(async () => { onMounted(async () => {
playSound('/assets/music/intro.mp3') playSound('/assets/music/intro.mp3')
const characterHairStorage = new CharacterHairStorage() const characterHairStorage = new CharacterHairStorage()
const characterTypeStorage = new CharacterTypeStorage()
characterHairs.value = await characterHairStorage.getAll() characterHairs.value = await characterHairStorage.getAll()
// Get the first available character type for preview
const types = await characterTypeStorage.getAll()
const defaultType = types.find((type) => type.isSelectable)
if (defaultType) {
defaultCharacterTypeId.value = defaultType.id
}
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {