<template> <div class="relative max-lg:h-dvh flex flex-row-reverse"> <div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div> <div class="bg-[url('/assets/login/login-bg.png')] opacity-20 w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center grayscale"></div> <div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative"></div> <div class="absolute top-8 right-0 py-[18px] pr-[15px] pl-32 bg-gradient-to-r from-transparent to-cyan-900 z-20"> <h2 class="text-white">CHARACTER SELECTION</h2> </div> <div class="ui-wrapper h-dvh w-[calc(100%_-_80px)] sm:w-[calc(100%_-_160px)] absolute flex flex-col justify-center items-center gap-14 px-10 sm:px-20 z-30"> <div class="filler"></div> <div class="w-2/3 max-w-[860px]" v-if="!isLoading"> <div class="mb-5 flex flex-col gap-1"> <h1 class="text-white font-bold">SELECT CHARACTER TO PLAY</h1> <p class="m-0">Maximum of 4 characters can be created per player</p> </div> <div class="flex w-full h-[400px] border border-solid border-gray-500 rounded-md rounded-tl-none bg-gray"> <div class="w-1/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center border-0 border-r border-solid border-gray-500 rounded-bl-md relative"> <div class="absolute right-full -top-px flex gap-1 flex-col"> <div v-for="character in characters" :key="character.id" class="character relative rounded-l border border-solid border-gray-500 w-9 h-[50px] bg-[url('/assets/ui-texture.png')] after:absolute after:w-full after:h-px after:bg-gray-500" :class="{ active: selectedCharacterId == character.id }" > <img src="/assets/avatar/default/head.png" class="w-9 h-9 object-contain absolute top-1/2 -translate-y-1/2" alt="Player head" /> <input class="h-full w-full absolute m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0" type="radio" name="character" :value="character.id" v-model="selectedCharacterId" /> </div> <div class="character relative rounded-l border border-solid border-gray-500 w-9 h-[50px] bg-[url('/assets/ui-texture.png')]" :class="{ active: characters.length == 0 }" v-if="characters.length < 4"> <button class="p-0 h-full w-full flex flex-col justify-between focus-visible:outline-offset-0" @click="isCreateNewCharacterModalOpen = true"> <img class="w-6 h-6 object-contain absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2" draggable="false" src="/assets/icons/plus-icon.svg" /> </button> </div> </div> <div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6 justify-center" v-if="selectedCharacterId"> <input class="input-field w-[158px]" type="text" name="name" :placeholder="characters.find((c) => c.id == selectedCharacterId)?.name" /> <div class="flex flex-col gap-4 items-center"> <div class="flex flex-col gap-3"> <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-between"> <button class="ml-6 w-4 h-8 p-0"> <img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" /> </button> <img class="w-12 object-contain mb-3.5" src="/assets/avatar/default/0.png" alt="Player avatar" /> <button class="mr-6 w-4 h-8 p-0"> <img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" /> </button> </div> <!-- <div class="flex justify-between w-[190px]">--> <!-- <!– TODO: replace with color swatches –>--> <!-- <button v-for="n in 9" class="w-4 h-4 rounded-sm bg-white"></button>--> <!-- </div>--> </div> <!-- TODO: update gender on (selected) character --> <div class="flex justify-between w-[190px]"> <button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === '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: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === 'FEMALE' }"> <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> <div class="w-2/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center 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="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 border border-solid border-gray-500 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="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> <!-- TODO #255: make radio button so we can set a value, do the same with swatches --> <div v-for="hair in characterHairs" class="relative flex justify-center items-center bg-gray border border-solid border-gray-500 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="w-4 h-4" :src="config.server_endpoint + '/assets/sprites/' + hair.spriteId + '/front.png'" alt="Hair sprite" /> <input type="radio" name="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 class="flex flex-col gap-3 w-full"> <span class="text-sm">Hair color</span> <div class="flex gap-2 flex-wrap"> <!-- TODO: replace with hair colors --> <input type="radio" name="hair-color" v-for="n in 10" class="bg-white w-6 h-6 m-0 rounded-sm hover:cursor-pointer" /> </div> </div> </div> </div> </div> </div> <div v-else> <img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" /> </div> <div class="button-wrapper flex self-center justify-end gap-4 max-w-[860px] w-full" v-if="!isLoading"> <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> </div> </div> </div> <!-- CREATE CHARACTER MODAL --> <Modal :isModalOpen="isCreateNewCharacterModalOpen" @modal:close="isCreateNewCharacterModalOpen = false" :modal-width="430" :modal-height="275"> <template #modalHeader> <h3 class="m-0 font-medium text-white">Create your character</h3> </template> <template #modalBody> <div class="p-4 h-[calc(100%_-_32px)]"> <form method="post" @submit.prevent="createCharacter" class="h-full flex flex-col justify-between"> <div class="form-field-full"> <label for="name" class="text-white">Nickname</label> <input class="input-field" v-model="newCharacterName" name="name" id="name" placeholder="Enter a nickname..." /> </div> <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 class="btn-cyan py-1.5 px-4 inline-block" type="submit">Create</button> </div> </form> </div> </template> </Modal> <!-- DELETE CHARACTER MODAL --> <ConfirmationModal v-if="deletingCharacter != null" :confirm-function="deleteCharacter.bind(this, deletingCharacter.id)" :cancel-function="(() => (deletingCharacter = null)).bind(this)" confirm-button-text="Delete"> <template #modalHeader> <h3 class="m-0 font-medium text-white">Delete character?</h3> </template> <template #modalBody> <p class="mt-0 mb-5 text-white text-lg"> Do you want to permanently delete <span class="font-extrabold text-white">{{ deletingCharacter.name }}</span >? </p> </template> </ConfirmationModal> </template> <script setup lang="ts"> import config from '@/config' import { useGameStore } from '@/stores/gameStore' import { onBeforeUnmount, ref, watch } from 'vue' import Modal from '@/components/utilities/Modal.vue' import { type Character as CharacterT, type CharacterHair } from '@/types' import ConfirmationModal from '@/components/utilities/ConfirmationModal.vue' const gameStore = useGameStore() const isLoading = ref<boolean>(true) const characters = ref<CharacterT[]>([]) const deletingCharacter = ref(null as CharacterT | null) const selectedCharacterId = ref<number | null>(null) const isCreateNewCharacterModalOpen = ref<boolean>(false) const newCharacterName = ref<string>('') const characterHairs = ref<CharacterHair[]>([]) const selectedHairId = ref<number | null>(null) // Fetch characters setTimeout(() => { gameStore.connection?.emit('character:list') }, 750) gameStore.connection?.on('character:list', (data: any) => { characters.value = data isLoading.value = false // Fetch hairs // @TODO: This is hacky, we should have a better way to do this gameStore.connection?.emit('character:hair:list', {}, (data: CharacterHair[]) => { characterHairs.value = data }) }) // Select character logics function loginWithCharacter() { if (!selectedCharacterId.value) return gameStore.connection?.emit('character:connect', { characterId: selectedCharacterId.value, characterHairId: selectedHairId.value }) gameStore.connection?.on('character:connect', (data: CharacterT) => gameStore.setCharacter(data)) } // Delete character logics function deleteCharacter(characterId: number) { if (!characterId) return deletingCharacter.value = null gameStore.connection?.emit('character:delete', { characterId: characterId }) } // Create character logics function createCharacter() { gameStore.connection?.on('character:create:success', (data: CharacterT) => { gameStore.setCharacter(data) isCreateNewCharacterModalOpen.value = false }) gameStore.connection?.emit('character:create', { name: newCharacterName.value }) } // Watch changes for selected character and update hairs watch(selectedCharacterId, (characterId) => { if (!characterId) return selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHairId ?? null }) onBeforeUnmount(() => { gameStore.connection?.off('character:list') gameStore.connection?.off('character:connect') gameStore.connection?.off('character:create:success') }) </script>