Compare commits

..

41 Commits

Author SHA1 Message Date
8bf67ab168 Minor fix and format 2025-02-14 02:23:13 +01:00
f83e2bf8c8 Merge remote-tracking branch 'origin/main' into feature/map-refactor 2025-02-14 02:08:23 +01:00
8b0bf6534e Most comprehensive undo/redo so far 2025-02-13 13:27:15 -06:00
5e243e5201 Fixed object moving when its not supposed to 2025-02-13 12:19:09 -06:00
c82db9813e Remove redundant import 2025-02-13 11:34:32 -06:00
579749f4e0 fuck depth sorting man 2025-02-13 17:08:47 +01:00
ddc26a021b Depth sort fix attempt 2025-02-13 14:50:46 +01:00
2d6b1ff1e0 Merge branch 'feature/map-refactor' of ssh://gitea.directonline.io:29417/noxious/client into feature/map-refactor
# Conflicts:
#	src/components/gameMaster/mapEditor/Map.vue
#	src/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue
#	src/components/gameMaster/mapEditor/partials/Toolbar.vue
#	src/components/screens/MapEditor.vue
2025-02-12 18:38:14 -06:00
16720777c9 Rm unused func 2025-02-12 21:32:34 +01:00
41e7832cbe Minor improvements 2025-02-12 21:11:17 +01:00
e6412d8a65 Temp. fix for finding children in scene, character create bug fix, chat logic improvements, added image compression upon build 2025-02-12 13:44:37 +01:00
faa8e5def9 Remove history commit that was used to pad the start 2025-02-11 21:14:03 -06:00
beed1d6903 Chat fix 2025-02-12 03:19:56 +01:00
2ebcc24390 npm update 2025-02-12 03:17:01 +01:00
2e3ff803f6 Improvement 2025-02-11 23:17:06 +01:00
dd1cc795de Replaced all event names with numbers for less bandwidth usage 2025-02-11 23:13:15 +01:00
59243e0e17 Merge cleanup 2025-02-10 16:21:23 -06:00
87ffc98cce Best undo/redo function across all map editor elements 2025-02-10 16:08:56 -06:00
0c450b24ed Teleport modal restored, and expanded undo/redo to include all placed and erase edits across each map element type (map object advanced actions WIP) 2025-02-10 16:08:15 -06:00
9459639497 Best undo/redo function across all map editor elements 2025-02-10 16:02:38 -06:00
5f2c7a09b1 Slightly improved logic 2025-02-10 18:32:35 +01:00
13e8c1b4dd Show sprite duration 2025-02-10 16:10:36 +01:00
b27a2e8779 Updated move interval 2025-02-10 15:33:09 +01:00
b3c9e3ca3d 75 2025-02-10 15:30:08 +01:00
31a91c3f9f Prod. env improvements 2025-02-10 15:26:47 +01:00
5d4de60f90 Broken import fix 2025-02-10 15:12:34 +01:00
4070bcf048 Removed BackgroundImageLoader component 2025-02-10 15:11:36 +01:00
04203cb9c1 Add opacity to placed map object preview, don't show seconds in clock , updated date format 2025-02-10 14:37:34 +01:00
592d1df9bf Removed conflicting and redundant logic 2025-02-10 13:39:17 +01:00
9413fdbb2f Improved check 2025-02-10 02:12:24 +01:00
34caac562c Minor fixes 2025-02-10 02:05:16 +01:00
52dafb8643 Added character profile images to preload 2025-02-10 01:53:21 +01:00
390187f353 #202 - Enable / disable placed map object preview 2025-02-10 01:51:45 +01:00
cbd111a05b Map eidtor work , new walk sound 2025-02-10 01:46:31 +01:00
5ef11f3157 Add ID instead of object when adding map object 2025-02-09 22:40:06 +01:00
c56c2796c4 format 2025-02-09 22:36:55 +01:00
c228af7bb6 Moved if logic into component for improved performance, inline value updating for select fields 2025-02-09 22:29:24 +01:00
f45a51c230 Clean 2025-02-09 22:09:42 +01:00
790a62c600 Merge remote-tracking branch 'origin/feature/#321' 2025-02-09 21:54:36 +01:00
6de0bb200d Removed nginx config 2025-02-09 19:58:54 +01:00
ca307d4de3 Teleport modal restored, and expanded undo/redo to include all placed and erase edits across each map element type (map object advanced actions WIP) 2025-02-08 15:07:21 -06:00
55 changed files with 1178 additions and 672 deletions

View File

@ -1,16 +0,0 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Redirect example
location /discord {
return 301 https://discord.gg/JTev3nzeDa;
}
# Serve static files
location / {
try_files $uri $uri/ /index.html;
}
}

753
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,10 +18,12 @@
"@vueuse/integrations": "^10.5.0", "@vueuse/integrations": "^10.5.0",
"axios": "^1.7.7", "axios": "^1.7.7",
"dexie": "^4.0.8", "dexie": "^4.0.8",
"phaser": "^3.86.0", "phaser": "3.87.0",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"sharp": "^0.33.5",
"socket.io-client": "^4.8.0", "socket.io-client": "^4.8.0",
"universal-cookie": "^6.1.3", "universal-cookie": "^6.1.3",
"vite-plugin-image-optimizer": "^1.1.8",
"vue": "^3.5.12", "vue": "^3.5.12",
"zod": "^3.22.2" "zod": "^3.22.2"
}, },

Binary file not shown.

View File

@ -1,7 +1,6 @@
<template> <template>
<Debug /> <Debug />
<Notifications /> <Notifications />
<BackgroundImageLoader />
<GmPanel v-if="gameStore.character?.role === 'gm'" /> <GmPanel v-if="gameStore.character?.role === 'gm'" />
<component :is="currentScreen" /> <component :is="currentScreen" />
</template> </template>
@ -13,7 +12,6 @@ import Game from '@/components/screens/Game.vue'
import Loading from '@/components/screens/Loading.vue' import Loading from '@/components/screens/Loading.vue'
import Login from '@/components/screens/Login.vue' import Login from '@/components/screens/Login.vue'
import MapEditor from '@/components/screens/MapEditor.vue' import MapEditor from '@/components/screens/MapEditor.vue'
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
import Debug from '@/components/utilities/Debug.vue' import Debug from '@/components/utilities/Debug.vue'
import Notifications from '@/components/utilities/Notifications.vue' import Notifications from '@/components/utilities/Notifications.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'

View File

@ -3,3 +3,59 @@ export enum Direction {
NEGATIVE, NEGATIVE,
UNCHANGED UNCHANGED
} }
export enum SocketEvent {
CLOSE = '52',
DATA = '51',
CHARACTER_CONNECT = '50',
CHARACTER_CREATE = '49',
CHARACTER_DELETE = '48',
CHARACTER_LIST = '47',
GM_CHARACTERHAIR_CREATE = '46',
GM_CHARACTERHAIR_REMOVE = '45',
GM_CHARACTERHAIR_LIST = '44',
GM_CHARACTERHAIR_UPDATE = '43',
GM_CHARACTERTYPE_CREATE = '42',
GM_CHARACTERTYPE_REMOVE = '41',
GM_CHARACTERTYPE_LIST = '40',
GM_CHARACTERTYPE_UPDATE = '39',
GM_ITEM_CREATE = '38',
GM_ITEM_REMOVE = '37',
GM_ITEM_LIST = '36',
GM_ITEM_UPDATE = '35',
GM_MAPOBJECT_LIST = '34',
GM_MAPOBJECT_REMOVE = '33',
GM_MAPOBJECT_UPDATE = '32',
GM_MAPOBJECT_UPLOAD = '31',
GM_SPRITE_COPY = '30',
GM_SPRITE_CREATE = '29',
GM_SPRITE_DELETE = '28',
GM_SPRITE_LIST = '27',
GM_SPRITE_UPDATE = '26',
GM_TILE_DELETE = '25',
GM_TILE_LIST = '24',
GM_TILE_UPDATE = '23',
GM_TILE_UPLOAD = '22',
GM_MAP_CREATE = '21',
GM_MAP_DELETE = '20',
GM_MAP_REQUEST = '19',
GM_MAP_UPDATE = '18',
MAP_CHARACTER_MOVEERROR = '17',
DISCONNECT = '16',
USER_DISCONNECT = '15',
LOGIN = '14',
LOGGED_IN = '13',
NOTIFICATION = '12',
DATE = '11',
FAILED = '10',
COMPLETED = '9',
CONNECTION = '8',
WEATHER = '7',
CHARACTER_DISCONNECT = '6',
MAP_CHARACTER_ATTACK = '5',
MAP_CHARACTER_TELEPORT = '4',
MAP_CHARACTER_JOIN = '3',
MAP_CHARACTER_LEAVE = '2',
MAP_CHARACTER_MOVE = '1',
CHAT_MESSAGE = '0'
}

View File

@ -28,7 +28,7 @@ const gameStore = useGameStore()
const mapStore = useMapStore() const mapStore = useMapStore()
const scene = useScene() const scene = useScene()
const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, playAnimation, calcDirection, updateSprite, initializeSprite, cleanup } = useCharacterSpriteComposable(scene, props.tileMap, props.mapCharacter) const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, playAnimation, updateSprite, initializeSprite, cleanup } = useCharacterSpriteComposable(scene, props.tileMap, props.mapCharacter)
const { playSound, stopSound } = useSoundComposable() const { playSound, stopSound } = useSoundComposable()
const handlePositionUpdate = (newValues: any, oldValues: any) => { const handlePositionUpdate = (newValues: any, oldValues: any) => {
@ -92,7 +92,6 @@ watch(
onMounted(async () => { onMounted(async () => {
await initializeSprite() await initializeSprite()
if (props.mapCharacter.character.id === gameStore.character!.id) { if (props.mapCharacter.character.id === gameStore.character!.id) {
mapStore.setCharacterLoaded(true)
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container) scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
} }
}) })

View File

@ -1,51 +0,0 @@
<template>
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" />
</template>
<script lang="ts" setup>
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
import { loadSpriteTextures } from '@/services/textureService'
import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer'
import { computed } from 'vue'
const props = defineProps<{
mapCharacter: MapCharacter
currentX: number
currentY: number
}>()
const gameStore = useGameStore()
const scene = useScene()
const texture = computed(() => {
const { rotation, characterHair } = props.mapCharacter.character
const spriteId = characterHair?.sprite?.id
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
return `${spriteId}-${direction}`
})
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
const imageProps = computed(() => {
// Get the current sprite action based on direction
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
const spriteAction = props.mapCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
return {
depth: 1,
originX: Number(spriteAction?.originX) ?? 0,
originY: Number(spriteAction?.originY) ?? 0,
flipX: isFlippedX.value,
texture: texture.value
// y: props.mapCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
}
})
loadSpriteTextures(scene, props.mapCharacter.character.characterHair?.sprite as SpriteT)
.then(() => {})
.catch((error) => {
console.error('Error loading texture:', error)
})
</script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Container ref="characterChatContainer" :depth="999"> <Container ref="characterChatContainer">
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" /> <RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" /> <Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
</Container> </Container>

View File

@ -2,7 +2,7 @@
<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 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 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"> <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> <span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{ 'text-cyan-300': gameStore.character?.role == 'gm' }">{{ message.character }}</span>
<p class="text-gray-50 m-0">{{ message.message }}</p> <p class="text-gray-50 m-0">{{ message.message }}</p>
</div> </div>
</div> </div>
@ -21,7 +21,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Chat } from '@/application/types' import { SocketEvent } from '@/application/enums'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore' import { useMapStore } from '@/stores/mapStore'
import { onClickOutside, useFocus } from '@vueuse/core' import { onClickOutside, useFocus } from '@vueuse/core'
@ -33,7 +33,7 @@ const gameStore = useGameStore()
const mapStore = useMapStore() const mapStore = useMapStore()
const message = ref('') const message = ref('')
const chats = ref([] as Chat[]) const chats = ref<{ character: string; message: string }[]>([])
const chatWindow = ref<HTMLElement | null>(null) const chatWindow = ref<HTMLElement | null>(null)
const chatInput = ref<HTMLElement | null>(null) const chatInput = ref<HTMLElement | null>(null)
@ -55,7 +55,7 @@ function unfocusChat(event: Event, targetElement: HTMLElement) {
const sendMessage = () => { const sendMessage = () => {
if (!message.value.trim()) return if (!message.value.trim()) return
gameStore.connection?.emit('chat:message', { message: message.value }, (response: boolean) => {}) gameStore.connection?.emit(SocketEvent.CHAT_MESSAGE, { message: message.value }, (response: boolean) => {})
message.value = '' message.value = ''
} }
@ -79,21 +79,30 @@ const scrollToBottom = () => {
}) })
} }
gameStore.connection?.on('chat:message', (data: Chat) => { gameStore.connection?.on(SocketEvent.CHAT_MESSAGE, (data: { character: string; message: string }) => {
chats.value.push(data) if (!data.character || !data.message) return
chats.value.push({ character: data.character, message: data.message })
scrollToBottom() scrollToBottom()
if (!mapStore.characterLoaded) return const characterContainer = scene.children.getByName(data.character) as Phaser.GameObjects.Container
if (!characterContainer) {
console.log('No character container found')
return
}
const characterContainer = scene.children.getByName(data.character.name) as Phaser.GameObjects.Container const characterChatContainer = characterContainer.getByName(data.character + '_chatContainer') as Phaser.GameObjects.Container
if (!characterContainer) return if (!characterChatContainer) {
console.log('No character chat container found')
return
}
const characterChatContainer = characterContainer.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container const chatBubble = characterChatContainer.getByName(data.character + '_chatBubble') as Phaser.GameObjects.Container
if (!characterChatContainer) return const chatText = characterChatContainer.getByName(data.character + '_chatText') as Phaser.GameObjects.Text
if (!chatText || !chatBubble) {
const chatBubble = characterChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container console.log('No chat text or bubble found')
const chatText = characterChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text return
if (!chatText || !chatBubble) return }
function calculateTextWidth(text: string, font: string, fontSize: number): number { function calculateTextWidth(text: string, font: string, fontSize: number): number {
// Create a canvas element // Create a canvas element
@ -144,7 +153,7 @@ onMounted(() => {
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
gameStore.connection?.off('chat:message') gameStore.connection?.off(SocketEvent.CHAT_MESSAGE)
removeEventListener('keydown', focusChat) removeEventListener('keydown', focusChat)
}) })
</script> </script>

View File

@ -1,21 +1,20 @@
<template> <template>
<div class="absolute top-0 right-4 hidden lg:block"> <div class="absolute top-0 right-4 hidden lg:block" v-if="gameStore.world.date && typeof gameStore.world.date === 'object'">
<p class="text-white text-lg">{{ gameStore.world.date.toLocaleString() }}</p> <p class="text-white text-lg">
{{ useDateFormat(gameStore.world.date, 'YYYY/MM/DD HH:mm') }}
</p>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useDateFormat } from '@vueuse/core'
import { onUnmounted } from 'vue' import { onUnmounted } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
// Listen for new date from socket
gameStore.connection?.on('date', (data: Date) => {
gameStore.world.date = new Date(data)
})
onUnmounted(() => { onUnmounted(() => {
gameStore.connection?.off('date') gameStore.connection?.off(SocketEvent.DATE)
}) })
</script> </script>

View File

@ -3,6 +3,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { MapCharacter, UUID } from '@/application/types' import type { MapCharacter, UUID } from '@/application/types'
import Character from '@/components/game/character/Character.vue' import Character from '@/components/game/character/Character.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -16,15 +17,15 @@ const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap tileMap: Phaser.Tilemaps.Tilemap
}>() }>()
gameStore.connection?.on('map:character:join', async (data: MapCharacter) => { gameStore.connection?.on(SocketEvent.MAP_CHARACTER_JOIN, (data: MapCharacter) => {
mapStore.addCharacter(data) mapStore.addCharacter(data)
}) })
gameStore.connection?.on('map:character:leave', (characterId: UUID) => { gameStore.connection?.on(SocketEvent.MAP_CHARACTER_LEAVE, (characterId: UUID) => {
mapStore.removeCharacter(characterId) mapStore.removeCharacter(characterId)
}) })
gameStore.connection?.on('map:character:move', (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => { gameStore.connection?.on(SocketEvent.MAP_CHARACTER_MOVE, (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
mapStore.updateCharacterPosition(data) mapStore.updateCharacterPosition(data)
// @TODO: Replace with universal class, composable or store // @TODO: Replace with universal class, composable or store
if (data.characterId === gameStore.character?.id) { if (data.characterId === gameStore.character?.id) {
@ -34,13 +35,14 @@ gameStore.connection?.on('map:character:move', (data: { characterId: UUID; posit
} }
}) })
gameStore.connection?.on('map:character:attack', (characterId: UUID) => { gameStore.connection?.on(SocketEvent.MAP_CHARACTER_ATTACK, (characterId: UUID) => {
mapStore.updateCharacterProperty(characterId, 'isAttacking', true) mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
}) })
onUnmounted(() => { onUnmounted(() => {
gameStore.connection?.off('map:character:join') gameStore.connection?.off(SocketEvent.MAP_CHARACTER_ATTACK)
gameStore.connection?.off('map:character:leave') gameStore.connection?.off(SocketEvent.MAP_CHARACTER_MOVE)
gameStore.connection?.off('map:character:move') gameStore.connection?.off(SocketEvent.MAP_CHARACTER_JOIN)
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_LEAVE)
}) })
</script> </script>

View File

@ -5,6 +5,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { mapLoadData } from '@/application/types' import type { mapLoadData } from '@/application/types'
import { unduplicateArray } from '@/application/utilities' import { unduplicateArray } from '@/application/utilities'
import Characters from '@/components/game/map/Characters.vue' import Characters from '@/components/game/map/Characters.vue'
@ -28,7 +29,7 @@ const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>() const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
// Event listeners // Event listeners
gameStore.connection?.on('map:character:teleport', async (data: mapLoadData) => { gameStore.connection?.on(SocketEvent.MAP_CHARACTER_TELEPORT, (data: mapLoadData) => {
mapStore.setMapId(data.mapId) mapStore.setMapId(data.mapId)
mapStore.setCharacters(data.characters) mapStore.setCharacters(data.characters)
}) })
@ -64,6 +65,6 @@ onUnmounted(() => {
tileMap.value.destroy() tileMap.value.destroy()
} }
gameStore.connection?.off('map:character:teleport') gameStore.connection?.off(SocketEvent.MAP_CHARACTER_TELEPORT)
}) })
</script> </script>

View File

@ -59,9 +59,9 @@ function calculateObjectPlacement(mapObj: PlacedMapObject): { x: number; y: numb
} }
const imageProps = computed(() => ({ const imageProps = computed(() => ({
alpha: mapEditor.movingPlacedObject.value?.id == props.placedMapObject.id ? 0.5 : 1, alpha: mapEditor.movingPlacedObject.value?.id == props.placedMapObject.id || mapEditor.selectedMapObject.value?.id == props.placedMapObject.id ? 0.5 : 1,
tint: mapEditor.selectedPlacedObject.value?.id == props.placedMapObject.id ? 0x00ff00 : 0xffffff, tint: mapEditor.selectedPlacedObject.value?.id == props.placedMapObject.id ? 0x00ff00 : 0xffffff,
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, mapObject.value!.frameWidth, mapObject.value!.frameHeight), depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, mapObject.value!.frameWidth, mapObject.value!.frameHeight, mapObject.value!.originX, mapObject.value!.originY),
...calculateObjectPlacement(props.placedMapObject), ...calculateObjectPlacement(props.placedMapObject),
flipX: props.placedMapObject.isRotated, flipX: props.placedMapObject.isRotated,
texture: mapObject.value!.id, texture: mapObject.value!.id,

View File

@ -20,9 +20,9 @@
<script setup lang="ts"> <script setup lang="ts">
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue' import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue' import { ref } from 'vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const gameStore = useGameStore() const gameStore = useGameStore()

View File

@ -34,6 +34,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types' import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -65,7 +66,7 @@ if (selectedCharacterHair.value) {
function removeCharacterHair() { function removeCharacterHair() {
if (!selectedCharacterHair.value) return if (!selectedCharacterHair.value) return
gameStore.connection?.emit('gm:characterHair:remove', { id: selectedCharacterHair.value.id }, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_REMOVE, { id: selectedCharacterHair.value.id }, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to remove character hair') console.error('Failed to remove character hair')
return return
@ -75,7 +76,7 @@ function removeCharacterHair() {
} }
function refreshCharacterHairList(unsetSelectedCharacterHair = true) { function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response) assetManagerStore.setCharacterHairList(response)
if (unsetSelectedCharacterHair) { if (unsetSelectedCharacterHair) {
@ -93,7 +94,7 @@ function saveCharacterHair() {
spriteId: characterSpriteId.value spriteId: characterSpriteId.value
} }
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_UPDATE, characterHairData, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to save character type') console.error('Failed to save character type')
return return
@ -113,7 +114,7 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
onMounted(() => { onMounted(() => {
if (!selectedCharacterHair.value) return if (!selectedCharacterHair.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })

View File

@ -32,6 +32,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { CharacterHair } from '@/application/types' import type { CharacterHair } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -52,13 +53,13 @@ const handleSearch = () => {
} }
const createNewCharacterHair = () => { const createNewCharacterHair = () => {
gameStore.connection?.emit('gm:characterHair:create', {}, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_CREATE, {}, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to create new character type') console.error('Failed to create new character type')
return return
} }
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response) assetManagerStore.setCharacterHairList(response)
}) })
}) })
@ -92,7 +93,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response) assetManagerStore.setCharacterHairList(response)
}) })
}) })

View File

@ -40,6 +40,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types' import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -74,7 +75,7 @@ if (selectedCharacterType.value) {
function removeCharacterType() { function removeCharacterType() {
if (!selectedCharacterType.value) return if (!selectedCharacterType.value) return
gameStore.connection?.emit('gm:characterType:remove', { id: selectedCharacterType.value.id }, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_REMOVE, { id: selectedCharacterType.value.id }, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to remove character type') console.error('Failed to remove character type')
return return
@ -84,7 +85,7 @@ function removeCharacterType() {
} }
function refreshCharacterTypeList(unsetSelectedCharacterType = true) { function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response) assetManagerStore.setCharacterTypeList(response)
if (unsetSelectedCharacterType) { if (unsetSelectedCharacterType) {
@ -103,7 +104,7 @@ function saveCharacterType() {
spriteId: characterSpriteId.value spriteId: characterSpriteId.value
} }
gameStore.connection?.emit('gm:characterType:update', characterTypeData, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_UPDATE, characterTypeData, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to save character type') console.error('Failed to save character type')
return return
@ -124,7 +125,7 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
onMounted(() => { onMounted(() => {
if (!selectedCharacterType.value) return if (!selectedCharacterType.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })

View File

@ -32,6 +32,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { CharacterType } from '@/application/types' import type { CharacterType } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -52,13 +53,13 @@ const handleSearch = () => {
} }
const createNewCharacterType = () => { const createNewCharacterType = () => {
gameStore.connection?.emit('gm:characterType:create', {}, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_CREATE, {}, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to create new character type') console.error('Failed to create new character type')
return return
} }
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response) assetManagerStore.setCharacterTypeList(response)
}) })
}) })
@ -92,7 +93,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response) assetManagerStore.setCharacterTypeList(response)
}) })
}) })

View File

@ -44,6 +44,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types' import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -80,7 +81,7 @@ if (selectedItem.value) {
function removeItem() { function removeItem() {
if (!selectedItem.value) return if (!selectedItem.value) return
gameStore.connection?.emit('gm:item:remove', { id: selectedItem.value.id }, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_ITEM_REMOVE, { id: selectedItem.value.id }, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to remove item') console.error('Failed to remove item')
return return
@ -90,7 +91,7 @@ function removeItem() {
} }
function refreshItemList(unsetSelectedItem = true) { function refreshItemList(unsetSelectedItem = true) {
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => { gameStore.connection?.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
assetManagerStore.setItemList(response) assetManagerStore.setItemList(response)
if (unsetSelectedItem) { if (unsetSelectedItem) {
@ -110,7 +111,7 @@ function saveItem() {
spriteId: itemSpriteId.value spriteId: itemSpriteId.value
} }
gameStore.connection?.emit('gm:item:update', itemData, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_ITEM_UPDATE, itemData, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to save item') console.error('Failed to save item')
return return
@ -132,7 +133,7 @@ watch(selectedItem, (item: Item | null) => {
onMounted(() => { onMounted(() => {
if (!selectedItem.value) return if (!selectedItem.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })

View File

@ -29,6 +29,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { Item } from '@/application/types' import type { Item } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -48,13 +49,13 @@ const handleSearch = () => {
} }
const createNewItem = () => { const createNewItem = () => {
gameStore.connection?.emit('gm:item:create', {}, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_ITEM_CREATE, {}, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to create new item') console.error('Failed to create new item')
return return
} }
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => { gameStore.connection?.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
assetManagerStore.setItemList(response) assetManagerStore.setItemList(response)
}) })
}) })
@ -88,7 +89,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => { gameStore.connection?.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
assetManagerStore.setItemList(response) assetManagerStore.setItemList(response)
}) })
}) })

View File

@ -44,6 +44,7 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { MapObject } from '@/application/types' import type { MapObject } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue' import ChipsInput from '@/components/forms/ChipsInput.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
@ -78,7 +79,7 @@ if (selectedMapObject.value) {
} }
function removeObject() { function removeObject() {
gameStore.connection?.emit('gm:mapObject:remove', { mapObject: selectedMapObject.value?.id }, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_REMOVE, { mapObject: selectedMapObject.value?.id }, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to remove mapObject') console.error('Failed to remove mapObject')
return return
@ -88,7 +89,7 @@ function removeObject() {
} }
function refreshObjectList(unsetSelectedMapObject = true) { function refreshObjectList(unsetSelectedMapObject = true) {
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => { gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
assetManagerStore.setMapObjectList(response) assetManagerStore.setMapObjectList(response)
if (unsetSelectedMapObject) { if (unsetSelectedMapObject) {

View File

@ -29,6 +29,7 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { MapObject } from '@/application/types' import type { MapObject } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -47,13 +48,13 @@ const elementToScroll = ref()
const handleFileUpload = (e: Event) => { const handleFileUpload = (e: Event) => {
const files = (e.target as HTMLInputElement).files const files = (e.target as HTMLInputElement).files
if (!files) return if (!files) return
gameStore.connection?.emit('gm:mapObject:upload', files, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_UPLOAD, files, (response: boolean) => {
if (!response) { if (!response) {
if (config.environment === 'development') console.error('Failed to upload map object') if (config.environment === 'development') console.error('Failed to upload map object')
return return
} }
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => { gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
assetManagerStore.setMapObjectList(response) assetManagerStore.setMapObjectList(response)
}) })
}) })
@ -92,7 +93,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => { gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
assetManagerStore.setMapObjectList(response) assetManagerStore.setMapObjectList(response)
}) })
}) })

View File

@ -68,6 +68,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { Sprite, SpriteAction, UUID } from '@/application/types' import type { Sprite, SpriteAction, UUID } from '@/application/types'
import { uuidv4 } from '@/application/utilities' import { uuidv4 } from '@/application/utilities'
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue' import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
@ -97,7 +98,7 @@ if (selectedSprite.value) {
} }
function deleteSprite() { function deleteSprite() {
gameStore.connection?.emit('gm:sprite:delete', { id: selectedSprite.value?.id }, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_DELETE, { id: selectedSprite.value?.id }, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to delete sprite') console.error('Failed to delete sprite')
return return
@ -107,7 +108,7 @@ function deleteSprite() {
} }
function copySprite() { function copySprite() {
gameStore.connection?.emit('gm:sprite:copy', { id: selectedSprite.value?.id }, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_COPY, { id: selectedSprite.value?.id }, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to copy sprite') console.error('Failed to copy sprite')
return return
@ -117,7 +118,7 @@ function copySprite() {
} }
function refreshSpriteList(unsetSelectedSprite = true) { function refreshSpriteList(unsetSelectedSprite = true) {
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
if (unsetSelectedSprite) { if (unsetSelectedSprite) {
@ -149,7 +150,7 @@ function saveSprite() {
}) ?? [] }) ?? []
} }
gameStore.connection?.emit('gm:sprite:update', updatedSprite, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_UPDATE, updatedSprite, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to save sprite') console.error('Failed to save sprite')
return return

View File

@ -25,6 +25,7 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { Sprite } from '@/application/types' import type { Sprite } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -40,13 +41,13 @@ const hasScrolled = ref(false)
const elementToScroll = ref() const elementToScroll = ref()
function newButtonClickHandler() { function newButtonClickHandler() {
gameStore.connection?.emit('gm:sprite:create', {}, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_CREATE, {}, (response: boolean) => {
if (!response) { if (!response) {
if (config.environment === 'development') console.error('Failed to create new sprite') if (config.environment === 'development') console.error('Failed to create new sprite')
return return
} }
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })
@ -85,7 +86,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })

View File

@ -34,7 +34,7 @@
</div> </div>
<div class="flex flex-col justify-center gap-8 flex-1"> <div class="flex flex-col justify-center gap-8 flex-1">
<div class="flex flex-col"> <div class="flex flex-col">
<label class="block mb-2 text-white">Frame Rate: {{ frameRate }} FPS</label> <label class="block mb-2 text-white">Frame Rate: {{ frameRate }} FPS (Duration: {{ totalDuration }}s)</label>
<input type="range" v-model.number="localFrameRate" min="0" max="60" step="1" class="w-full accent-cyan-500" @input="updateFrameRate" /> <input type="range" v-model.number="localFrameRate" min="0" max="60" step="1" class="w-full accent-cyan-500" @input="updateFrameRate" />
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
@ -76,6 +76,11 @@ const localFrameRate = ref(props.frameRate)
const zoomLevel = ref(100) const zoomLevel = ref(100)
let animationInterval: number | null = null let animationInterval: number | null = null
const totalDuration = computed(() => {
if (props.frameRate <= 0) return 0
return (props.sprites.length / props.frameRate).toFixed(2)
})
const spritesWithTempOffset = computed(() => { const spritesWithTempOffset = computed(() => {
return props.sprites.map((sprite, index) => { return props.sprites.map((sprite, index) => {
if (index === props.tempOffsetIndex && props.tempOffset) { if (index === props.tempOffsetIndex && props.tempOffset) {

View File

@ -24,6 +24,7 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { Tile } from '@/application/types' import type { Tile } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue' import ChipsInput from '@/components/forms/ChipsInput.vue'
import { TileStorage } from '@/storage/storages' import { TileStorage } from '@/storage/storages'
@ -56,7 +57,7 @@ watch(selectedTile, (tile: Tile | null) => {
}) })
async function deleteTile() { async function deleteTile() {
gameStore.connection?.emit('gm:tile:delete', { id: selectedTile.value?.id }, async (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_TILE_DELETE, { id: selectedTile.value?.id }, async (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to delete tile') console.error('Failed to delete tile')
return return
@ -67,7 +68,7 @@ async function deleteTile() {
} }
function refreshTileList(unsetSelectedTile = true) { function refreshTileList(unsetSelectedTile = true) {
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => { gameStore.connection?.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
assetManagerStore.setTileList(response) assetManagerStore.setTileList(response)
if (unsetSelectedTile) { if (unsetSelectedTile) {

View File

@ -29,6 +29,7 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { Tile } from '@/application/types' import type { Tile } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -47,13 +48,13 @@ const elementToScroll = ref()
const handleFileUpload = (e: Event) => { const handleFileUpload = (e: Event) => {
const files = (e.target as HTMLInputElement).files const files = (e.target as HTMLInputElement).files
if (!files) return if (!files) return
gameStore.connection?.emit('gm:tile:upload', files, (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_TILE_UPLOAD, files, (response: boolean) => {
if (!response) { if (!response) {
if (config.environment === 'development') console.error('Failed to upload tile') if (config.environment === 'development') console.error('Failed to upload tile')
return return
} }
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => { gameStore.connection?.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
assetManagerStore.setTileList(response) assetManagerStore.setTileList(response)
}) })
}) })
@ -92,7 +93,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => { gameStore.connection?.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
assetManagerStore.setTileList(response) assetManagerStore.setTileList(response)
}) })
}) })

View File

@ -1,30 +1,141 @@
<template> <template>
<MapTiles ref="mapTiles" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer /> <MapTiles ref="mapTiles" @createCommand="addCommand" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
<PlacedMapObjects ref="mapObjects" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer /> <PlacedMapObjects ref="mapObjects" @update="updateMapObjects" @updateAndCommit="updateAndCommit" @pauseObjectTracking="pause" @resumeObjectTracking="resume" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
<MapEventTiles ref="eventTiles" v-if="tileMap" :tileMap /> <MapEventTiles ref="eventTiles" @createCommand="addCommand" v-if="tileMap" :tileMap />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { MapEventTile, Map as MapT, PlacedMapObject as PlacedMapObjectT } from '@/application/types'
import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue' import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue'
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue' import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue' import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { createTileLayer, createTileMap } from '@/services/mapService' import { cloneArray, createTileArray, createTileLayer, createTileMap, placeTiles } from '@/services/mapService'
import { TileStorage } from '@/storage/storages' import { TileStorage } from '@/storage/storages'
import { useManualRefHistory, useRefHistory } from '@vueuse/core'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { onBeforeUnmount, onMounted, onUnmounted, shallowRef, useTemplateRef } from 'vue' import { onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>() const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>() const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const scene = useScene() const scene = useScene()
const mapTiles = useTemplateRef('mapTiles') const mapTiles = useTemplateRef('mapTiles')
const mapObjects = useTemplateRef('mapObjects') const mapObjects = useTemplateRef('mapObjects')
const eventTiles = useTemplateRef('eventTiles') const eventTiles = useTemplateRef('eventTiles')
//Record of commands
let commandStack: (EditorCommand | number)[] = []
let commandIndex = ref(0)
let originTiles: string[][] = []
let originEventTiles: MapEventTile[] = []
let originObjects = ref<PlacedMapObjectT[]>(mapEditor.currentMap.value.placedMapObjects)
const { undo, redo, commit, pause, resume, canUndo, canRedo } = useRefHistory(originObjects, { deep: true, capacity: 9 })
//Command Pattern basic interface, extended to store what elements have been changed by each edit
export interface EditorCommand {
apply: (elements: any[]) => any[]
type: 'tile' | 'map_object' | 'event_tile'
operation: 'draw' | 'erase' | 'clear'
}
function applyCommands(tiles: any[], ...commands: EditorCommand[]): any[] {
let tileVersion = cloneArray(tiles)
for (let command of commands) {
tileVersion = command.apply(tileVersion)
}
return tileVersion
}
watch(
() => mapEditor.shouldClearTiles.value,
(shouldClear) => {
if (shouldClear && mapEditor.currentMap.value) {
mapTiles.value!.clearTiles()
eventTiles.value!.clearTiles()
mapEditor.currentMap.value.placedMapObjects = []
updateAndCommit(mapEditor.currentMap.value)
mapEditor.resetClearTilesFlag()
}
}
)
function update(commands: (EditorCommand | number)[]) {
if (!mapEditor.currentMap.value) return
if (commandStack.length >= 9) {
if (typeof commandStack[0] !== 'number') {
const base = commandStack.shift() as EditorCommand
if (base.operation !== 'clear') {
switch (base.type) {
case 'tile':
originTiles = base.apply(originTiles) as string[][]
break
case 'event_tile':
originEventTiles = base.apply(originEventTiles) as MapEventTile[]
break
}
} else {
commandStack.shift()
}
} else if (typeof commandStack[0] === 'number') {
commandStack.shift()
}
}
let tileCommands = commands.filter((item) => typeof item !== 'number' && item.type === 'tile') as EditorCommand[]
let eventTileCommands = commands.filter((item) => typeof item !== 'number' && item.type === 'event_tile') as EditorCommand[]
let modifiedTiles = applyCommands(originTiles, ...tileCommands)
placeTiles(tileMap.value!, tileMapLayer.value!, modifiedTiles)
let eventTiles = applyCommands(originEventTiles, ...eventTileCommands)
mapEditor.currentMap.value.tiles = modifiedTiles
mapEditor.currentMap.value.mapEventTiles = eventTiles
mapEditor.currentMap.value.placedMapObjects = originObjects.value
}
function updateMapObjects(map: MapT) {
originObjects.value = map.placedMapObjects
}
function updateAndCommit(map?: MapT) {
commandStack = commandStack.slice(0, commandIndex.value)
if (map) updateMapObjects(map)
commit()
commandStack.push(0)
commandIndex.value = commandStack.length
}
function addCommand(command: EditorCommand) {
commandStack = commandStack.slice(0, commandIndex.value)
commandStack.push(command)
commandIndex.value = commandStack.length
}
function undoEdit() {
if (commandIndex.value > 0) {
if (typeof commandStack[--commandIndex.value] === 'number' && canUndo) {
undo()
}
update(commandStack.slice(0, commandIndex.value))
}
}
function redoEdit() {
if (commandIndex.value <= 9 && commandIndex.value < commandStack.length) {
if (typeof commandStack[commandIndex.value++] === 'number' && canRedo) {
redo()
}
update(commandStack.slice(0, commandIndex.value))
}
}
function handlePointerDown(pointer: Phaser.Input.Pointer) { function handlePointerDown(pointer: Phaser.Input.Pointer) {
if (!mapTiles.value || !mapObjects.value || !eventTiles.value) return if (!mapTiles.value || !mapObjects.value || !eventTiles.value) return
@ -54,12 +165,12 @@ function handlePointerDown(pointer: Phaser.Input.Pointer) {
function handleKeyDown(event: KeyboardEvent) { function handleKeyDown(event: KeyboardEvent) {
//CTRL+Y //CTRL+Y
if (event.key === 'y' && event.ctrlKey) { if (event.key === 'y' && event.ctrlKey) {
mapTiles.value!.redo() redoEdit()
} }
//CTRL+Z //CTRL+Z
if (event.key === 'z' && event.ctrlKey) { if (event.key === 'z' && event.ctrlKey) {
mapTiles.value!.undo() undoEdit()
} }
} }
@ -70,8 +181,22 @@ function handlePointerMove(pointer: Phaser.Input.Pointer) {
} }
function handlePointerUp(pointer: Phaser.Input.Pointer) { function handlePointerUp(pointer: Phaser.Input.Pointer) {
if (mapEditor.drawMode.value === 'tile') { switch (mapEditor.drawMode.value) {
mapTiles.value?.finalizeCommand() case 'tile':
mapTiles.value!.finalizeCommand()
break
case 'map_object':
if (mapEditor.tool.value === 'pencil' || mapEditor.tool.value === 'eraser') {
resume()
updateAndCommit()
}
break
case 'teleport':
eventTiles.value!.finalizeCommand()
break
case 'blocking tile':
eventTiles.value!.finalizeCommand()
break
} }
} }
@ -79,6 +204,10 @@ onMounted(async () => {
let mapValue = mapEditor.currentMap.value let mapValue = mapEditor.currentMap.value
if (!mapValue) return if (!mapValue) return
//Clone
originTiles = cloneArray(mapValue.tiles)
originEventTiles = cloneArray(mapValue.mapEventTiles)
const tileStorage = new TileStorage() const tileStorage = new TileStorage()
const allTiles = await tileStorage.getAll() const allTiles = await tileStorage.getAll()
const allTileIds = allTiles.map((tile) => tile.id) const allTileIds = allTiles.map((tile) => tile.id)

View File

@ -5,20 +5,67 @@
<script setup lang="ts"> <script setup lang="ts">
import { MapEventTileType, type MapEventTile, type Map as MapT, type UUID } from '@/application/types' import { MapEventTileType, type MapEventTile, type Map as MapT, type UUID } from '@/application/types'
import { uuidv4 } from '@/application/utilities' import { uuidv4 } from '@/application/utilities'
import { type EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { getTile, tileToWorldX, tileToWorldY } from '@/services/mapService' import { cloneArray, getTile, tileToWorldX, tileToWorldY } from '@/services/mapService'
import { Image } from 'phavuer' import { Image } from 'phavuer'
import { shallowRef } from 'vue'
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
defineExpose({ handlePointer }) defineExpose({ handlePointer, finalizeCommand, clearTiles })
const emit = defineEmits(['createCommand'])
const props = defineProps<{ const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap tileMap: Phaser.Tilemaps.Tilemap
}>() }>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>() // *** COMMAND STATE ***
let currentCommand: EventTileCommand | null = null
class EventTileCommand implements EditorCommand {
public operation: 'draw' | 'erase' | 'clear' = 'draw'
public type: 'event_tile' = 'event_tile'
public affectedTiles: MapEventTile[] = []
apply(elements: MapEventTile[]) {
let tileVersion = cloneArray(elements) as MapEventTile[]
if (this.operation === 'draw') {
tileVersion = tileVersion.concat(this.affectedTiles)
} else if (this.operation === 'erase') {
tileVersion = tileVersion.filter((v) => !this.affectedTiles.includes(v))
} else if (this.operation === 'clear') {
tileVersion = []
}
return tileVersion
}
constructor(operation: 'draw' | 'erase' | 'clear') {
this.operation = operation
}
}
function createCommandUpdate(tile?: MapEventTile, operation: 'draw' | 'erase' | 'clear') {
if (!currentCommand) {
currentCommand = new EventTileCommand(operation)
}
//If position is already in, do not proceed
for (const priorTile of currentCommand.affectedTiles) {
if (priorTile.positionX === tile.positionX && priorTile.positionY == tile.positionY) return
}
currentCommand.affectedTiles.push(tile)
}
function finalizeCommand() {
if (!currentCommand) return
emit('createCommand', currentCommand)
currentCommand = null
}
// *** HANDLERS ***
function getImageProps(tile: MapEventTile) { function getImageProps(tile: MapEventTile) {
return { return {
@ -44,7 +91,7 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
const newEventTile = { const newEventTile = {
id: uuidv4() as UUID, id: uuidv4() as UUID,
mapId: map.id, mapId: map.id,
map: map.id, map: map,
type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT, type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
positionX: tile.x, positionX: tile.x,
positionY: tile.y, positionY: tile.y,
@ -59,6 +106,8 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
: undefined : undefined
} }
createCommandUpdate(newEventTile, 'draw')
map.mapEventTiles.push(newEventTile) map.mapEventTiles.push(newEventTile)
} }
@ -77,6 +126,8 @@ function erase(pointer: Phaser.Input.Pointer, map: MapT) {
else return else return
} }
createCommandUpdate(existingEventTile, 'erase')
// Remove existing event tile // Remove existing event tile
map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id) map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
} }
@ -96,4 +147,10 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
break break
} }
} }
function clearTiles() {
if (mapEditor.currentMap.value.mapEventTiles.length === 0) return
createCommandUpdate(null, 'clear')
finalizeCommand()
}
</script> </script>

View File

@ -3,59 +3,74 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { type EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue'
import Controls from '@/components/utilities/Controls.vue' import Controls from '@/components/utilities/Controls.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { createTileArray, getTile, placeTile, placeTiles } from '@/services/mapService' import { cloneArray, createTileArray, getTile, placeTile, placeTiles } from '@/services/mapService'
import { onMounted, ref, watch } from 'vue' import { onMounted, ref, watch } from 'vue'
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
defineExpose({ handlePointer, finalizeCommand, undo, redo }) defineExpose({ handlePointer, finalizeCommand, clearTiles })
const emit = defineEmits(['createCommand'])
const props = defineProps<{ const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap tileMap: Phaser.Tilemaps.Tilemap
tileMapLayer: Phaser.Tilemaps.TilemapLayer tileMapLayer: Phaser.Tilemaps.TilemapLayer
}>() }>()
class EditorCommand { // *** COMMAND STATE ***
public operation: 'draw' | 'erase' = 'draw'
public tileName: string = 'blank_tile'
public affectedTiles: number[][]
constructor(operation: 'draw' | 'erase', tileName: string) { let currentCommand: TileCommand | null = null
class TileCommand implements EditorCommand {
public operation: 'draw' | 'erase' | 'clear' = 'draw'
public type: 'tile' = 'tile'
public tileName: string = 'blank_tile'
public affectedTiles: number[][] = []
apply(elements: string[][]) {
let tileVersion
if (this.operation === 'clear') {
tileVersion = createTileArray(props.tileMapLayer.width, props.tileMapLayer.height, 'blank_tile')
} else {
tileVersion = cloneArray(elements) as string[][]
for (const position of this.affectedTiles) {
tileVersion[position[1]][position[0]] = this.tileName
}
}
return tileVersion
}
constructor(operation: 'draw' | 'erase' | 'clear', tileName: string) {
this.operation = operation this.operation = operation
this.tileName = tileName this.tileName = tileName
this.affectedTiles = []
} }
} }
//Record of commands function createCommandUpdate(x: number, y: number, tileName: string, operation: 'draw' | 'erase' | 'clear') {
let commandStack: EditorCommand[] = [] if (!currentCommand) {
let currentCommand: EditorCommand | null = null currentCommand = new TileCommand(operation, tileName)
let commandIndex = ref(0) }
let originTiles: string[][] = []
function pencil(pointer: Phaser.Input.Pointer) { //If position is already in, do not proceed
let map = mapEditor.currentMap.value for (const vec of currentCommand.affectedTiles) {
if (!map) return if (vec[0] === x && vec[1] === y) return
}
// Check if there is a selected tile currentCommand.affectedTiles.push([x, y])
if (!mapEditor.selectedTile.value) return
// Check if there is a tile
const tile = getTile(props.tileMapLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(props.tileMap, props.tileMapLayer, tile.x, tile.y, mapEditor.selectedTile.value)
createCommandUpdate(tile.x, tile.y, mapEditor.selectedTile.value, 'draw')
// Adjust mapEditorStore.map.tiles
map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value
} }
function eraser(pointer: Phaser.Input.Pointer) { function finalizeCommand() {
if (!currentCommand) return
emit('createCommand', currentCommand)
currentCommand = null
}
// *** HANDLERS ***
function draw(pointer: Phaser.Input.Pointer, tileName: string) {
let map = mapEditor.currentMap.value let map = mapEditor.currentMap.value
if (!map) return if (!map) return
@ -64,12 +79,12 @@ function eraser(pointer: Phaser.Input.Pointer) {
if (!tile) return if (!tile) return
// Place tile // Place tile
placeTile(props.tileMap, props.tileMapLayer, tile.x, tile.y, 'blank_tile') placeTile(props.tileMap, props.tileMapLayer, tile.x, tile.y, tileName)
createCommandUpdate(tile.x, tile.y, 'blank_tile', 'erase') createCommandUpdate(tile.x, tile.y, tileName, tileName === 'blank_tile' ? 'erase' : 'draw')
// Adjust mapEditorStore.map.tiles // Adjust mapEditorStore.map.tiles
map.tiles[tile.y][tile.x] = 'blank_tile' map.tiles[tile.y][tile.x] = tileName
} }
function paint(pointer: Phaser.Input.Pointer) { function paint(pointer: Phaser.Input.Pointer) {
@ -113,10 +128,10 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
// Check if draw mode is tile // Check if draw mode is tile
switch (mapEditor.tool.value) { switch (mapEditor.tool.value) {
case 'pencil': case 'pencil':
pencil(pointer) draw(pointer, mapEditor.selectedTile.value!)
break break
case 'eraser': case 'eraser':
eraser(pointer) draw(pointer, 'blank_tile')
break break
case 'paint': case 'paint':
paint(pointer) paint(pointer)
@ -124,90 +139,19 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
} }
} }
function createCommandUpdate(x: number, y: number, tileName: string, operation: 'draw' | 'erase') { // *** LIFECYCLE ***
if (!currentCommand) {
currentCommand = new EditorCommand(operation, tileName)
}
//If position is already in, do not proceed function clearTiles() {
for (const vec of currentCommand.affectedTiles) { const tileArray = createTileArray(props.tileMap.width, props.tileMap.height, 'blank_tile')
if (vec[0] === x && vec[1] === y) return placeTiles(props.tileMap, props.tileMapLayer, tileArray)
} createCommandUpdate(0, 0, 'blank_tile', 'clear')
finalizeCommand()
currentCommand.affectedTiles.push([x, y])
} }
function finalizeCommand() {
if (!currentCommand) return
//Cut the stack so the current edit is the last
commandStack = commandStack.slice(0, commandIndex.value)
commandStack.push(currentCommand)
if (commandStack.length >= 9) {
originTiles = applyCommands(originTiles, commandStack.shift()!)
}
commandIndex.value = commandStack.length
currentCommand = null
}
function undo() {
if (commandIndex.value > 0) {
commandIndex.value--
updateMapTiles()
}
}
function redo() {
if (commandIndex.value <= 9 && commandIndex.value <= commandStack.length) {
commandIndex.value++
updateMapTiles()
}
}
function applyCommands(tiles: string[][], ...commands: EditorCommand[]): string[][] {
let tileVersion = cloneArray(tiles)
for (let command of commands) {
for (const position of command.affectedTiles) {
tileVersion[position[1]][position[0]] = command.tileName
}
}
return tileVersion
}
function updateMapTiles() {
if (!mapEditor.currentMap.value) return
let indexedCommands = commandStack.slice(0, commandIndex.value)
let modifiedTiles = applyCommands(originTiles, ...indexedCommands)
placeTiles(props.tileMap, props.tileMapLayer, modifiedTiles)
mapEditor.currentMap.value.tiles = modifiedTiles
}
//Recursive Array Clone
function cloneArray(arr: any[]): any[] {
return arr.map((item) => (item instanceof Array ? cloneArray(item) : item))
}
watch(
() => mapEditor.shouldClearTiles.value,
(shouldClear) => {
if (shouldClear && mapEditor.currentMap.value) {
const blankTiles = createTileArray(props.tileMapLayer.width, props.tileMapLayer.height, 'blank_tile')
placeTiles(props.tileMap, props.tileMapLayer, blankTiles)
mapEditor.currentMap.value.tiles = blankTiles
mapEditor.resetClearTilesFlag()
}
}
)
onMounted(async () => { onMounted(async () => {
if (!mapEditor.currentMap.value) return if (!mapEditor.currentMap.value) return
const mapState = mapEditor.currentMap.value const mapState = mapEditor.currentMap.value
//Clone
originTiles = cloneArray(mapState.tiles)
placeTiles(props.tileMap, props.tileMapLayer, mapState.tiles) placeTiles(props.tileMap, props.tileMapLayer, mapState.tiles)
}) })
</script> </script>

View File

@ -1,4 +1,11 @@
<template> <template>
<PlacedMapObject
v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object' && mapEditor.isPlacedMapObjectPreviewEnabled.value && mapEditor.selectedMapObject.value && previewPlacedMapObject"
:tileMap
:tileMapLayer
:key="previewPlacedMapObject?.id"
:placedMapObject="previewPlacedMapObject as PlacedMapObjectT"
/>
<SelectedPlacedMapObjectComponent v-if="mapEditor.selectedPlacedObject.value" :key="mapEditor.selectedPlacedObject.value.id" :map :placedMapObject="mapEditor.selectedPlacedObject.value" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" /> <SelectedPlacedMapObjectComponent v-if="mapEditor.selectedPlacedObject.value" :key="mapEditor.selectedPlacedObject.value.id" :map :placedMapObject="mapEditor.selectedPlacedObject.value" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
<PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap :tileMapLayer :placedMapObject @pointerdown="clickPlacedMapObject(placedMapObject)" /> <PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap :tileMapLayer :placedMapObject @pointerdown="clickPlacedMapObject(placedMapObject)" />
</template> </template>
@ -11,7 +18,7 @@ import SelectedPlacedMapObjectComponent from '@/components/gameMaster/mapEditor/
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { getTile } from '@/services/mapService' import { getTile } from '@/services/mapService'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { computed } from 'vue' import {computed, onMounted, onUnmounted, ref, watch} from 'vue'
import Tilemap = Phaser.Tilemaps.Tilemap import Tilemap = Phaser.Tilemaps.Tilemap
import TilemapLayer = Phaser.Tilemaps.TilemapLayer import TilemapLayer = Phaser.Tilemaps.TilemapLayer
@ -20,6 +27,8 @@ const scene = useScene()
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const map = computed(() => mapEditor.currentMap.value!) const map = computed(() => mapEditor.currentMap.value!)
const emit = defineEmits<{ (e: 'update', map: MapT): void; (e: 'updateAndCommit', map: MapT): void; (e: 'pauseObjectTracking'): void; (e: 'resumeObjectTracking'): void }>()
defineExpose({ handlePointer }) defineExpose({ handlePointer })
const props = defineProps<{ const props = defineProps<{
@ -27,36 +36,59 @@ const props = defineProps<{
tileMapLayer: TilemapLayer tileMapLayer: TilemapLayer
}>() }>()
const previewPosition = ref({ x: 0, y: 0 })
const previewPlacedMapObject = computed(() => ({
id: mapEditor.selectedMapObject.value!.id,
mapObject: mapEditor.selectedMapObject.value!.id,
isRotated: false,
positionX: previewPosition.value.x,
positionY: previewPosition.value.y
}))
function updatePreviewPosition(pointer: Phaser.Input.Pointer) {
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
if (!tile || (previewPosition.value.x === tile.x && previewPosition.value.y === tile.y)) return
previewPosition.value = { x: tile.x, y: tile.y }
}
function pencil(pointer: Phaser.Input.Pointer, map: MapT) { function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
emit('pauseObjectTracking')
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY) const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
// Check if object already exists on position // Check if object already exists on position
const existingPlacedMapObject = findObjectByPointer(pointer, map) const existingPlacedMapObject = findObjectByPointer(pointer, mapEditor.currentMap.value!)
if (existingPlacedMapObject) return if (existingPlacedMapObject) return
if (!mapEditor.selectedMapObject.value) return if (!mapEditor.selectedMapObject.value) return
const newPlacedMapObject: PlacedMapObjectT = { const newPlacedMapObject: PlacedMapObjectT = {
id: uuidv4(), id: uuidv4(),
mapObject: mapEditor.selectedMapObject.value, mapObject: mapEditor.selectedMapObject.value.id,
isRotated: false, isRotated: false,
positionX: tile.x, positionX: tile.x,
positionY: tile.y positionY: tile.y
} }
// Add new object to mapObjects // Add new object to mapObjects
map.placedMapObjects.push(newPlacedMapObject)
mapEditor.selectedPlacedObject.value = newPlacedMapObject mapEditor.selectedPlacedObject.value = newPlacedMapObject
map.placedMapObjects.push(newPlacedMapObject)
emit('update', map)
} }
function eraser(pointer: Phaser.Input.Pointer, map: MapT) { function eraser(pointer: Phaser.Input.Pointer, map: MapT) {
emit('pauseObjectTracking')
// Check if object already exists on position // Check if object already exists on position
const existingPlacedMapObject = findObjectByPointer(pointer, map) const existingPlacedMapObject = findObjectByPointer(pointer, map)
if (!existingPlacedMapObject) return if (!existingPlacedMapObject) return
// Remove existing object // Remove existing object
map.placedMapObjects = map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id) map.placedMapObjects = map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id)
emit('update', map)
} }
function findObjectByPointer(pointer: Phaser.Input.Pointer, map: MapT): PlacedMapObjectT | undefined { function findObjectByPointer(pointer: Phaser.Input.Pointer, map: MapT): PlacedMapObjectT | undefined {
@ -78,6 +110,8 @@ function objectPicker(pointer: Phaser.Input.Pointer, map: MapT) {
function moveMapObject(id: string, map: MapT) { function moveMapObject(id: string, map: MapT) {
mapEditor.movingPlacedObject.value = map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT mapEditor.movingPlacedObject.value = map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT
emit('pauseObjectTracking')
function handlePointerMove(pointer: Phaser.Input.Pointer) { function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!mapEditor.movingPlacedObject.value) return if (!mapEditor.movingPlacedObject.value) return
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY) const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
@ -88,24 +122,43 @@ function moveMapObject(id: string, map: MapT) {
} }
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove) scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
function handlePointerUp() {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
mapEditor.movingPlacedObject.value = null
}
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp) scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
function handlePointerUp(pointer: Phaser.Input.Pointer) {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
if (!tile) return
console.log(id)
map.placedMapObjects.map((placed) => {
if (placed.id === id) {
placed.positionX = tile.x
placed.positionY = tile.y
}
})
mapEditor.movingPlacedObject.value = null
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
emit('resumeObjectTracking')
emit('updateAndCommit', map)
}
} }
function rotatePlacedMapObject(id: string, map: MapT) { function rotatePlacedMapObject(id: string, map: MapT) {
const matchingObject = map.placedMapObjects.find((placedMapObject) => placedMapObject.id === id) map.placedMapObjects.map((placed) => {
matchingObject!.isRotated = !matchingObject!.isRotated if (placed.id === id) {
placed.isRotated = !placed.isRotated
}
})
emit('updateAndCommit', map)
} }
function deletePlacedMapObject(id: string, map: MapT) { function deletePlacedMapObject(id: string, map: MapT) {
let mapE = mapEditor.currentMap.value! map.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id)
mapE.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id)
mapEditor.selectedPlacedObject.value = null mapEditor.selectedPlacedObject.value = null
emit('updateAndCommit', map)
} }
function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) { function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) {
@ -137,4 +190,12 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
break break
} }
} }
onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_MOVE, updatePreviewPosition)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, updatePreviewPosition)
})
</script> </script>

View File

@ -35,6 +35,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { Map } from '@/application/types' import type { Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
@ -56,7 +57,7 @@ const pvp = ref(false)
defineExpose({ open: () => modalRef.value?.open() }) defineExpose({ open: () => modalRef.value?.open() })
async function submit() { async function submit() {
gameStore.connection?.emit('gm:map:create', { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => { gameStore.connection?.emit(SocketEvent.GM_MAP_CREATE, { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => {
if (!response) { if (!response) {
return return
} }

View File

@ -29,13 +29,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Map, UUID } from '@/application/types' import { SocketEvent } from '@/application/enums'
import type { Map } from '@/application/types'
import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue' import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { MapStorage } from '@/storage/storages' import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onMounted, ref, useTemplateRef } from 'vue' import { onMounted, ref, useTemplateRef } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
@ -60,15 +60,15 @@ async function fetchMaps() {
mapList.value = await mapStorage.getAll() mapList.value = await mapStorage.getAll()
} }
function loadMap(id: UUID) { function loadMap(id: string) {
gameStore.connection?.emit('gm:map:request', { mapId: id }, (response: Map) => { gameStore.connection?.emit(SocketEvent.GM_MAP_REQUEST, { mapId: id }, (response: Map) => {
mapEditor.loadMap(response) mapEditor.loadMap(response)
}) })
modalRef.value?.close() modalRef.value?.close()
} }
async function deleteMap(id: UUID) { async function deleteMap(id: string) {
gameStore.connection?.emit('gm:map:delete', { mapId: id }, async (response: boolean) => { gameStore.connection?.emit(SocketEvent.GM_MAP_DELETE, { mapId: id }, async (response: boolean) => {
if (!response) { if (!response) {
gameStore.addNotification({ gameStore.addNotification({
title: 'Error', title: 'Error',

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800"> <div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object'">
<div class="flex flex-col gap-2.5 p-2.5"> <div class="flex flex-col gap-2.5 p-2.5">
<div class="relative flex"> <div class="relative flex">
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" /> <img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
@ -7,7 +7,7 @@
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" /> <input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
</div> </div>
<div class="flex"> <div class="flex">
<select class="input-field w-full" name="lists"> <select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
<option value="tile">Tiles</option> <option value="tile">Tiles</option>
<option value="map_object">Objects</option> <option value="map_object">Objects</option>
</select> </select>

View File

@ -41,6 +41,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { MapObject, Map as MapT, PlacedMapObject } from '@/application/types' import type { MapObject, Map as MapT, PlacedMapObject } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
@ -81,7 +82,7 @@ async function handleUpdate() {
if (!mapObject.value) return if (!mapObject.value) return
gameStore.connection?.emit( gameStore.connection?.emit(
'gm:mapObject:update', SocketEvent.GM_MAPOBJECT_UPDATE,
{ {
id: props.placedMapObject.mapObject as string, id: props.placedMapObject.mapObject as string,
name: mapObjectName.value, name: mapObjectName.value,

View File

@ -39,6 +39,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { Map } from '@/application/types' import type { Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -58,9 +59,9 @@ defineExpose({
onMounted(fetchMaps) onMounted(fetchMaps)
function fetchMaps() { function fetchMaps() {
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => { // gameStore.connection?.emit(SocketEvent.GM_MAP_LIST, {}, (response: Map[]) => {
mapList.value = response // mapList.value = response
}) // })
} }
const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings() const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings()

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800"> <div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="(mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'tile') || mapEditor.tool.value === 'paint'">
<div class="flex flex-col gap-2.5 p-2.5"> <div class="flex flex-col gap-2.5 p-2.5">
<div class="relative flex"> <div class="relative flex">
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" /> <img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
@ -7,7 +7,7 @@
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" /> <input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
</div> </div>
<div class="flex"> <div class="flex">
<select class="input-field w-full" name="lists"> <select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
<option value="tile">Tiles</option> <option value="tile">Tiles</option>
<option value="map_object">Objects</option> <option value="map_object">Objects</option>
</select> </select>
@ -37,8 +37,9 @@
</div> </div>
<div v-else class="h-full overflow-auto"> <div v-else class="h-full overflow-auto">
<div class="p-4"> <div class="p-4">
<button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button> <div class="text-center mb-8">
<h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4> <button @click="closeGroup" class="hover:text-white">Back to all tiles</button>
</div>
<div class="grid grid-cols-4 gap-2 justify-items-center"> <div class="grid grid-cols-4 gap-2 justify-items-center">
<div class="flex flex-col items-center justify-center"> <div class="flex flex-col items-center justify-center">
<img <img

View File

@ -68,7 +68,7 @@
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button> <button class="flex justify-center items-center min-w-10 p-0 relative" @click="isMapEditorSettingsModalOpen = !isMapEditorSettingsModalOpen"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
@ -79,7 +79,7 @@
<button class="btn-cyan px-3.5" @click="() => emit('open-maps')">Load</button> <button class="btn-cyan px-3.5" @click="() => emit('open-maps')">Load</button>
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditor.currentMap.value">Save</button> <button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditor.currentMap.value">Save</button>
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditor.currentMap.value">Clear</button> <button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditor.currentMap.value">Clear</button>
<button class="btn-cyan px-3.5" @click="() => emit('close-editor')">Exit</button> <button class="btn-cyan px-3.5" @click="() => mapEditor.toggleActive()">Exit</button>
</div> </div>
</div> </div>
@ -89,9 +89,13 @@
</template> </template>
<template #modalBody> <template #modalBody>
<div class="m-4 flex items-center space-x-2"> <div class="m-4 flex items-center space-x-2">
<input id="continuous-drawing" @change="handleCheck" v-model="checkboxValue" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" /> <input id="continuous-drawing" @change="toggleContinuousDrawing" v-model="isContinuousDrawingEnabled" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
<label for="continuous-drawing" class="text-sm font-medium text-gray-200 cursor-pointer"> Continuous Drawing </label> <label for="continuous-drawing" class="text-sm font-medium text-gray-200 cursor-pointer"> Continuous Drawing </label>
</div> </div>
<div class="m-4 flex items-center space-x-2">
<input id="show-placed-map-object-preview" @change="mapEditor.togglePlacedMapObjectPreview()" v-model="isShowPlacedMapObjectPreviewEnabled" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
<label for="show-placed-map-object-preview" class="text-sm font-medium text-gray-200 cursor-pointer"> Show placed map object preview </label>
</div>
</template> </template>
</Modal> </Modal>
</template> </template>
@ -100,19 +104,20 @@
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import { onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor']) const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'open-teleport'])
// States // States
const toolbar = ref(null) const toolbar = ref(null)
const isMapEditorSettingsModalOpen = ref(false) const isMapEditorSettingsModalOpen = ref(false)
const selectPencilOpen = ref(false) const selectPencilOpen = ref(false)
const selectEraserOpen = ref(false) const selectEraserOpen = ref(false)
const checkboxValue = ref<Boolean>(false) const isContinuousDrawingEnabled = ref<Boolean>(false)
const listOpen = ref(false) const isShowPlacedMapObjectPreviewEnabled = ref<Boolean>(mapEditor.isPlacedMapObjectPreviewEnabled.value)
const listOpen = computed(() => mapEditor.tool.value === 'pencil' && (mapEditor.drawMode.value === 'tile' || mapEditor.drawMode.value === 'map_object'))
// drawMode // drawMode
function setDrawMode(value: string) { function setDrawMode(value: string) {
@ -132,8 +137,8 @@ function setEraserMode() {
selectEraserOpen.value = false selectEraserOpen.value = false
} }
function handleCheck() { function toggleContinuousDrawing() {
mapEditor.setInputMode(checkboxValue.value ? 'hold' : 'tap') mapEditor.setInputMode(isContinuousDrawingEnabled.value ? 'hold' : 'tap')
} }
function handleModeClick(mode: string, type: 'pencil' | 'eraser') { function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
@ -142,20 +147,10 @@ function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
} }
function handleClick(tool: string) { function handleClick(tool: string) {
if (tool === 'mapEditorSettings') {
isMapEditorSettingsModalOpen.value = true
listOpen.value = false
} else if (tool === 'settings') {
listOpen.value = false
} else if (tool === 'move') {
listOpen.value = false
mapEditor.setTool(tool) mapEditor.setTool(tool)
} else {
mapEditor.setTool(tool)
}
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
if (mapEditor.drawMode.value === 'teleport') emit('open-teleport')
} }
function cycleToolMode(tool: 'pencil' | 'eraser') { function cycleToolMode(tool: 'pencil' | 'eraser') {
@ -171,7 +166,7 @@ function initKeyShortcuts(event: KeyboardEvent) {
// Check if map is set // Check if map is set
if (!mapEditor.currentMap.value) return if (!mapEditor.currentMap.value) return
// prevent if focused on composables // prevent if focused on inputs
if (document.activeElement?.tagName === 'INPUT') return if (document.activeElement?.tagName === 'INPUT') return
if (event.ctrlKey) return if (event.ctrlKey) return

View File

@ -39,15 +39,6 @@ const password = ref('')
const formError = ref('') const formError = ref('')
const showPassword = ref(false) const showPassword = ref(false)
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
if (token) {
gameStore.setToken(token)
gameStore.initConnection()
}
})
async function submit() { async function submit() {
// check if username and password are valid // check if username and password are valid
if (username.value === '' || password.value === '') { if (username.value === '' || password.value === '') {

View File

@ -34,15 +34,6 @@ const password = ref('')
const newPasswordError = ref('') const newPasswordError = ref('')
const showPassword = ref(false) const showPassword = ref(false)
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
if (token) {
gameStore.setToken(token)
gameStore.initConnection()
}
})
async function newPasswordFunc() { async function newPasswordFunc() {
// check if username and password are valid // check if username and password are valid
if (password.value === '') { if (password.value === '') {

View File

@ -40,15 +40,6 @@ const email = ref('')
const formError = ref('') const formError = ref('')
const showPassword = ref(false) const showPassword = ref(false)
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
if (token) {
gameStore.setToken(token)
gameStore.initConnection()
}
})
async function submit() { async function submit() {
// check if username and password are valid // check if username and password are valid
if (username.value === '' || email.value === '' || password.value === '') { if (username.value === '' || email.value === '' || password.value === '') {

View File

@ -122,6 +122,7 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types' import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useSoundComposable } from '@/composables/useSoundComposable' import { useSoundComposable } from '@/composables/useSoundComposable'
@ -141,10 +142,11 @@ const selectedHairId = ref<string | null>(null)
// Fetch characters // Fetch characters
setTimeout(() => { setTimeout(() => {
gameStore.connection?.emit('character:list') console.log(SocketEvent.CHARACTER_LIST)
gameStore.connection?.emit(SocketEvent.CHARACTER_LIST)
}, 750) }, 750)
gameStore.connection?.on('character:list', (data: any) => { gameStore.connection?.on(SocketEvent.CHARACTER_LIST, (data: any) => {
characters.value = data characters.value = data
isLoading.value = false isLoading.value = false
}) })
@ -154,7 +156,7 @@ function loginWithCharacter() {
if (!selectedCharacterId.value) return if (!selectedCharacterId.value) return
gameStore.connection?.emit( gameStore.connection?.emit(
'character:connect', SocketEvent.CHARACTER_CONNECT,
{ {
characterId: selectedCharacterId.value, characterId: selectedCharacterId.value,
characterHairId: selectedHairId.value characterHairId: selectedHairId.value
@ -167,7 +169,7 @@ function loginWithCharacter() {
// Create character logics // Create character logics
function createCharacter() { function createCharacter() {
gameStore.connection?.emit('character:create', { name: newCharacterName.value }, (success: boolean) => { gameStore.connection?.emit(SocketEvent.CHARACTER_CREATE, { name: newCharacterName.value }, (success: boolean) => {
if (success) return if (success) return
isCreateNewCharacterModalOpen.value = false isCreateNewCharacterModalOpen.value = false
}) })
@ -176,7 +178,7 @@ function createCharacter() {
// 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
// selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHairId ?? null selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHair ?? null
}) })
onMounted(async () => { onMounted(async () => {
@ -186,8 +188,8 @@ onMounted(async () => {
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
gameStore.connection?.off('character:list') gameStore.connection?.off(SocketEvent.CHARACTER_LIST)
gameStore.connection?.off('character:connect') gameStore.connection?.off(SocketEvent.CHARACTER_CONNECT)
gameStore.connection?.off('character:create:success') gameStore.connection?.off(SocketEvent.CHARACTER_CREATE)
}) })
</script> </script>

View File

@ -44,13 +44,4 @@ function switchToLogin() {
currentForm.value = 'login' currentForm.value = 'login'
doesUrlHaveToken.value = false doesUrlHaveToken.value = false
} }
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
if (token) {
gameStore.setToken(token)
gameStore.initConnection()
}
})
</script> </script>

View File

@ -5,10 +5,10 @@
<div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div> <div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div>
<div v-else> <div v-else>
<Map v-if="mapEditor.currentMap.value" :key="mapEditor.currentMap.value?.id" /> <Map v-if="mapEditor.currentMap.value" :key="mapEditor.currentMap.value?.id" />
<Toolbar ref="toolbar" @save="save" @clear="clear" @open-maps="mapModal?.open" @open-settings="mapSettingsModal?.open" @close-editor="mapEditor.toggleActive" /> <Toolbar ref="toolbar" @save="save" @clear="clear" @open-maps="mapModal?.open" @open-settings="mapSettingsModal?.open" @open-teleport="teleportModal?.open" />
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" /> <MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
<TileList v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'tile'" /> <TileList />
<MapObjectList v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object'" /> <MapObjectList />
<MapSettings ref="mapSettingsModal" /> <MapSettings ref="mapSettingsModal" />
<TeleportModal ref="teleportModal" /> <TeleportModal ref="teleportModal" />
</div> </div>
@ -19,6 +19,7 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import 'phaser' import 'phaser'
import type { Map as MapT } from '@/application/types' import type { Map as MapT } from '@/application/types'
import Map from '@/components/gameMaster/mapEditor/Map.vue' import Map from '@/components/gameMaster/mapEditor/Map.vue'
@ -41,6 +42,7 @@ const gameStore = useGameStore()
const mapModal = useTemplateRef('mapModal') const mapModal = useTemplateRef('mapModal')
const mapSettingsModal = useTemplateRef('mapSettingsModal') const mapSettingsModal = useTemplateRef('mapSettingsModal')
const teleportModal = useTemplateRef('teleportModal')
const isLoaded = ref(false) const isLoaded = ref(false)
@ -86,18 +88,11 @@ function save() {
if (!currentMap) return if (!currentMap) return
const data = { const data = {
mapId: currentMap.id, ...currentMap,
name: currentMap.name, mapId: currentMap.id
width: currentMap.width,
height: currentMap.height,
tiles: currentMap.tiles,
pvp: currentMap.pvp,
mapEffects: currentMap.mapEffects,
mapEventTiles: currentMap.mapEventTiles,
placedMapObjects: currentMap.placedMapObjects.map(({ id, mapObject, isRotated, positionX, positionY }) => ({ id, mapObject, isRotated, positionX, positionY })) ?? []
} }
gameStore.connection?.emit('gm:map:update', data, (response: MapT) => { gameStore.connection?.emit(SocketEvent.GM_MAP_UPDATE, data, (response: MapT) => {
mapStorage.update(response.id, response) mapStorage.update(response.id, response)
}) })
} }
@ -106,7 +101,6 @@ function clear() {
if (!mapEditor.currentMap.value) return if (!mapEditor.currentMap.value) return
// Clear placed objects, event tiles and tiles // Clear placed objects, event tiles and tiles
mapEditor.clearMap()
mapEditor.triggerClearTiles() mapEditor.triggerClearTiles()
} }
</script> </script>

View File

@ -1,23 +0,0 @@
<template>
<div style="display: none">
<img v-for="(url, index) in imageUrls" :key="index" :src="url" alt="" @load="handleImageLoad(index)" @error="handleImageError(index)" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// Internal array of images to preload
const imageUrls = ref<string[]>(['/assets/ui-elements/button-ui-box-textured.svg', '/assets/ui-elements/button-ui-frame-empty.svg', '/assets/ui-elements/button-ui-box-textured-small.svg'])
const loadedImages = ref<Set<number>>(new Set())
const handleImageLoad = (index: number) => {
loadedImages.value.add(index)
console.log(`Image ${index} loaded:`, imageUrls.value[index])
}
const handleImageError = (index: number) => {
console.log(`Image ${index} failed to load:`, imageUrls.value[index])
}
</script>

View File

@ -1,15 +1,7 @@
<template></template> <template></template>
<script setup lang="ts"> <script setup lang="ts">
import { import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SoundStorage, SpriteStorage, TileStorage } from '@/storage/storages'
CharacterHairStorage,
CharacterTypeStorage,
MapObjectStorage,
MapStorage,
SoundStorage,
SpriteStorage,
TileStorage
} from '@/storage/storages'
import { TextureStorage } from '@/storage/textureStorage' import { TextureStorage } from '@/storage/textureStorage'
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted } from 'vue'

View File

@ -10,6 +10,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { onBeforeMount, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue' import { onBeforeMount, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue'
@ -26,7 +27,7 @@ type Notification = {
} }
function setupNotificationListener(connection: any) { function setupNotificationListener(connection: any) {
connection.on('notification', (data: Notification) => { connection.on(SocketEvent.NOTIFICATION, (data: Notification) => {
gameStore.addNotification({ gameStore.addNotification({
title: data.title, title: data.title,
message: data.message message: data.message
@ -52,7 +53,7 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
const connection = gameStore.connection const connection = gameStore.connection
if (connection) { if (connection) {
connection.off('notification') connection.off(SocketEvent.NOTIFICATION)
} }
}) })
</script> </script>

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '@/application/enums'
import { getTile } from '@/services/mapService' import { getTile } from '@/services/mapService'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import type { Ref } from 'vue' import type { Ref } from 'vue'
@ -22,7 +23,7 @@ export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Til
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY) const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
if (!pointerTile) return if (!pointerTile) return
gameStore.connection?.emit('map:character:move', { gameStore.connection?.emit(SocketEvent.MAP_CHARACTER_MOVE, {
positionX: pointerTile.x, positionX: pointerTile.x,
positionY: pointerTile.y positionY: pointerTile.y
}) })
@ -34,21 +35,22 @@ export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Til
function handleKeyDown(event: KeyboardEvent) { function handleKeyDown(event: KeyboardEvent) {
if (!gameStore.character) return if (!gameStore.character) return
// console.log(event.key)
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) { if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
// Prevent key repeat events
if (event.repeat) return
pressedKeys.add(event.key) pressedKeys.add(event.key)
// Start movement loop if not already running // Start movement loop if not already running
if (!moveInterval) { if (!moveInterval) {
moveInterval = window.setInterval(moveCharacter, 100) // Adjust timing as needed moveInterval = window.setInterval(moveCharacter, 80) // Increased interval to match server throttle `MOVEMENT_THROTTLE`
moveCharacter() // Move immediately on first press moveCharacter() // Move immediately on first press
} }
} }
// Attack on CTRL // Attack on CTRL
if (event.key === 'Control') { if (event.key === 'Control') {
gameStore.connection?.emit('map:character:attack') gameStore.connection?.emit(SocketEvent.MAP_CHARACTER_ATTACK)
} }
} }
@ -64,30 +66,22 @@ export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Til
function moveCharacter() { function moveCharacter() {
if (!gameStore.character) return if (!gameStore.character) return
const { positionX, positionY } = gameStore.character
if (pressedKeys.has('ArrowLeft')) { const { positionX, positionY } = gameStore.character
gameStore.connection?.emit('map:character:move', { let newX = positionX
positionX: positionX - 1, let newY = positionY
positionY: positionY
}) // Calculate new position based on pressed keys
} if (pressedKeys.has('ArrowLeft')) newX--
if (pressedKeys.has('ArrowRight')) { if (pressedKeys.has('ArrowRight')) newX++
gameStore.connection?.emit('map:character:move', { if (pressedKeys.has('ArrowUp')) newY--
positionX: positionX + 1, if (pressedKeys.has('ArrowDown')) newY++
positionY: positionY
}) // Only emit if position changed
} if (newX !== positionX || newY !== positionY) {
if (pressedKeys.has('ArrowUp')) { gameStore.connection?.emit(SocketEvent.MAP_CHARACTER_MOVE, {
gameStore.connection?.emit('map:character:move', { positionX: newX,
positionX: positionX, positionY: newY
positionY: positionY - 1
})
}
if (pressedKeys.has('ArrowDown')) {
gameStore.connection?.emit('map:character:move', {
positionX: positionX,
positionY: positionY + 1
}) })
} }
} }

View File

@ -18,7 +18,7 @@ export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phase
const tween = ref<Phaser.Tweens.Tween | null>(null) const tween = ref<Phaser.Tweens.Tween | null>(null)
const updateIsometricDepth = (positionX: number, positionY: number) => { const updateIsometricDepth = (positionX: number, positionY: number) => {
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 30, 95, true) isometricDepth.value = calculateIsometricDepth(positionX, positionY, 30, 95)
} }
const updatePosition = (positionX: number, positionY: number) => { const updatePosition = (positionX: number, positionY: number) => {

View File

@ -14,6 +14,7 @@ const tool = ref('move')
const drawMode = ref('tile') const drawMode = ref('tile')
const inputMode = ref('tap') const inputMode = ref('tap')
const selectedTile = ref('') const selectedTile = ref('')
const isPlacedMapObjectPreviewEnabled = ref(true)
const selectedMapObject = ref<MapObject | null>(null) const selectedMapObject = ref<MapObject | null>(null)
const movingPlacedObject = ref<PlacedMapObject | null>(null) const movingPlacedObject = ref<PlacedMapObject | null>(null)
const selectedPlacedObject = ref<PlacedMapObject | null>(null) const selectedPlacedObject = ref<PlacedMapObject | null>(null)
@ -42,17 +43,15 @@ export function useMapEditorComposable() {
} }
} }
const clearMap = () => {
if (!currentMap.value) return
currentMap.value.placedMapObjects = []
currentMap.value.mapEventTiles = []
}
const toggleActive = () => { const toggleActive = () => {
if (active.value) reset() if (active.value) reset()
active.value = !active.value active.value = !active.value
} }
const togglePlacedMapObjectPreview = () => {
isPlacedMapObjectPreviewEnabled.value = !isPlacedMapObjectPreviewEnabled.value
}
const setTool = (newTool: string) => { const setTool = (newTool: string) => {
tool.value = newTool tool.value = newTool
} }
@ -94,6 +93,7 @@ export function useMapEditorComposable() {
drawMode.value = 'tile' drawMode.value = 'tile'
inputMode.value = 'tap' inputMode.value = 'tap'
selectedTile.value = '' selectedTile.value = ''
isPlacedMapObjectPreviewEnabled.value = false
selectedMapObject.value = null selectedMapObject.value = null
shouldClearTiles.value = false shouldClearTiles.value = false
refreshMapObject.value = 0 refreshMapObject.value = 0
@ -107,6 +107,7 @@ export function useMapEditorComposable() {
drawMode, drawMode,
inputMode, inputMode,
selectedTile, selectedTile,
isPlacedMapObjectPreviewEnabled,
selectedMapObject, selectedMapObject,
movingPlacedObject, movingPlacedObject,
selectedPlacedObject, selectedPlacedObject,
@ -117,12 +118,12 @@ export function useMapEditorComposable() {
// Methods // Methods
loadMap, loadMap,
updateProperty, updateProperty,
clearMap,
toggleActive, toggleActive,
setTool, setTool,
setDrawMode, setDrawMode,
setInputMode, setInputMode,
setSelectedTile, setSelectedTile,
togglePlacedMapObjectPreview,
setSelectedMapObject, setSelectedMapObject,
setTeleportSettings, setTeleportSettings,
triggerClearTiles, triggerClearTiles,

View File

@ -151,3 +151,8 @@ export function createTileLayer(tileMap: Phaser.Tilemaps.Tilemap, tilesArray: st
return layer return layer
} }
//Recursive Array Clone
export function cloneArray(arr: any[]): any[] {
return arr.map((item) => (item instanceof Array ? cloneArray(item) : item))
}

View File

@ -1,4 +1,5 @@
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { Character, Notification, User, WorldSettings } from '@/application/types' import type { Character, Notification, User, WorldSettings } from '@/application/types'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
@ -85,10 +86,10 @@ export const useGameStore = defineStore('game', {
}) })
// Let the server know the user is logged in // Let the server know the user is logged in
this.connection.emit('login') this.connection.emit(SocketEvent.LOGIN)
// set user // set user
this.connection.on('logged_in', (user: User) => { this.connection.on(SocketEvent.LOGGED_IN, (user: User) => {
this.setUser(user) this.setUser(user)
}) })
@ -96,8 +97,17 @@ export const useGameStore = defineStore('game', {
this.connection.on('reconnect_failed', () => { this.connection.on('reconnect_failed', () => {
this.disconnectSocket() this.disconnectSocket()
}) })
// Listen for new date from socket
this.connection.on(SocketEvent.DATE, (data: Date) => {
this.world.date = new Date(data)
})
}, },
disconnectSocket() { disconnectSocket() {
// Remove event listeners
this.connection?.off('connect_error')
this.connection?.off('reconnect_failed')
this.connection?.off(SocketEvent.DATE)
this.connection?.disconnect() this.connection?.disconnect()
useCookies().remove('token', { useCookies().remove('token', {

View File

@ -5,8 +5,7 @@ export const useMapStore = defineStore('map', {
state: () => { state: () => {
return { return {
mapId: '', mapId: '',
characters: [] as MapCharacter[], characters: [] as MapCharacter[]
characterLoaded: false
} }
}, },
getters: { getters: {
@ -36,9 +35,6 @@ export const useMapStore = defineStore('map', {
removeCharacter(characterId: UUID) { removeCharacter(characterId: UUID) {
this.characters = this.characters.filter((char) => char.character.id !== characterId) this.characters = this.characters.filter((char) => char.character.id !== characterId)
}, },
setCharacterLoaded(loaded: boolean) {
this.characterLoaded = loaded
},
updateCharacterPosition(data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) { updateCharacterPosition(data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) {
const character = this.characters.find((char) => char.character.id === data.characterId) const character = this.characters.find((char) => char.character.id === data.characterId)
if (character) { if (character) {
@ -51,7 +47,6 @@ export const useMapStore = defineStore('map', {
reset() { reset() {
this.mapId = '' this.mapId = ''
this.characters = [] this.characters = []
this.characterLoaded = false
} }
} }
}) })

View File

@ -2,13 +2,37 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import viteCompression from 'vite-plugin-compression'; import viteCompression from 'vite-plugin-compression';
import {ViteImageOptimizer} from "vite-plugin-image-optimizer";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
viteCompression() viteCompression({
algorithm: 'gzip',
ext: '.gz',
threshold: 10240 // Only compress files larger than 10KB
}),
ViteImageOptimizer()
], ],
build: {
minify: 'terser', // Better minification
terserOptions: {
compress: {
drop_console: true, // Remove console.log in production
drop_debugger: true
}
},
rollupOptions: {
output: {
manualChunks: {
'vendor': ['vue'], // Split vendor chunks
// Add other large dependencies here
}
}
},
chunkSizeWarningLimit: 1000, // Increase chunk size warning limit if needed
},
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url))