<template> <div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col absolute left-1/2 -translate-x-1/2 bottom-5"> <div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray rounded-md border-2 border-solid border-gray-500 text-gray-300" v-show="gameStore.uiSettings.isChatOpen"> <div v-for="message in chats" class="flex-col py-2 items-center p-3"> <span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{ 'text-cyan-300': gameStore.character?.role == 'gm' }">{{ message.character.name }}</span> <p class="text-gray-50 m-0">{{ message.message }}</p> </div> </div> <div class="w-96 mx-auto relative"> <img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" /> <input class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800" placeholder="Type something..." v-model="message" @keypress="handleKeyPress" @submit="handleSubmit" ref="chatInput" /> </div> </div> </template> <script setup lang="ts"> import { onBeforeUnmount, ref, nextTick, onMounted } from 'vue' import { onClickOutside, useFocus } from '@vueuse/core' import { useGameStore } from '@/stores/gameStore' import type { Chat } from '@/application/types' import { useZoneStore } from '@/stores/zoneStore' import { useScene } from 'phavuer' const scene = useScene() const gameStore = useGameStore() const zoneStore = useZoneStore() const message = ref('') const chats = ref([] as Chat[]) const chatWindow = ref<HTMLElement | null>(null) const chatInput = ref<HTMLElement | null>(null) const { focused } = useFocus(chatInput) function focusChat(event: KeyboardEvent) { if (event.key === 'Enter' && !focused.value) { focused.value = true } } onClickOutside(chatInput, (event) => unfocusChat(event, chatInput.value as HTMLElement)) function unfocusChat(event: Event, targetElement: HTMLElement) { if (!(event.target instanceof Node) || !targetElement.contains(event.target)) { targetElement.blur() } } const sendMessage = () => { if (!message.value.trim()) return gameStore.connection?.emit('chat:message', { message: message.value }, (response: boolean) => {}) message.value = '' } const handleSubmit = (event: Event) => { event.preventDefault() sendMessage() } const handleKeyPress = (event: KeyboardEvent) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault() sendMessage() } } const scrollToBottom = () => { nextTick(() => { if (chatWindow.value) { chatWindow.value.scrollTop = chatWindow.value.scrollHeight } }) } gameStore.connection?.on('chat:message', (data: Chat) => { chats.value.push(data) scrollToBottom() if (!zoneStore.characterLoaded) return const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container if (!charChatContainer) return const chatBubble = charChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container const chatText = charChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text if (!chatText || !chatBubble) return function calculateTextWidth(text: string, font: string, fontSize: number): number { // Create a canvas element const canvas = document.createElement('canvas') const context = canvas.getContext('2d') if (!context) { throw new Error('Unable to create canvas context') } // Set the font context.font = `${fontSize}px ${font}` // Measure the text width const metrics = context.measureText(text) return metrics.width } chatBubble.width = calculateTextWidth(data.message.substring(0, 90), 'Arial', 13) + 30 // setText but with max. char limit of 90 chatText.setText(data.message.substring(0, 90)) charChatContainer.setVisible(true) /** * Hide chat bubble after a few seconds */ // Clear any existing hide timer if (charChatContainer.getData('hideTimer')) { clearTimeout(charChatContainer.getData('hideTimer')) } // Set a new hide timer const hideTimer = setTimeout(() => { charChatContainer.setVisible(false) }, 3000) // Store the timer on the container itself charChatContainer.setData('hideTimer', hideTimer) }) scrollToBottom() onMounted(() => { addEventListener('keydown', focusChat) }) onBeforeUnmount(() => { gameStore.connection?.off('chat:message') removeEventListener('keydown', focusChat) }) </script>