diff --git a/package-lock.json b/package-lock.json index fc3e187..c91e10e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@vueuse/core": "^10.5.0", "@vueuse/integrations": "^10.5.0", "axios": "^1.7.7", + "dexie": "^4.0.8", "phaser": "^3.86.0", "pinia": "^2.1.6", "socket.io-client": "^4.8.0", @@ -3549,6 +3550,12 @@ "node": ">=0.10" } }, + "node_modules/dexie": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.8.tgz", + "integrity": "sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ==", + "license": "Apache-2.0" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -3884,9 +3891,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.29.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.29.0.tgz", - "integrity": "sha512-hamyjrBhNH6Li6R1h1VF9KHfshJlKgKEg3ARbGTn72CMNDSMhWbgC7NdkRDEh25AFW+4SDATzyNM+3gWuZii8g==", + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.29.1.tgz", + "integrity": "sha512-MH/MbVae4HV/tM8gKAVWMPJbYgW04CK7SuzYRrlNERpxbO0P3+Zdsa2oAcFBW6xNu7W6lIkGOsFAMCRTYmrlWQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 63b3c51..b54fe60 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@vueuse/core": "^10.5.0", "@vueuse/integrations": "^10.5.0", "axios": "^1.7.7", + "dexie": "^4.0.8", "phaser": "^3.86.0", "pinia": "^2.1.6", "socket.io-client": "^4.8.0", diff --git a/public/assets/music/click-btn.mp3 b/public/assets/music/click-btn.mp3 new file mode 100644 index 0000000..6ef1729 Binary files /dev/null and b/public/assets/music/click-btn.mp3 differ diff --git a/public/assets/music/login.mp3 b/public/assets/music/login.mp3 new file mode 100644 index 0000000..1943d41 Binary files /dev/null and b/public/assets/music/login.mp3 differ diff --git a/src/App.vue b/src/App.vue index d188312..ba67548 100644 --- a/src/App.vue +++ b/src/App.vue @@ -15,6 +15,7 @@ import GmPanel from '@/components/gameMaster/GmPanel.vue' import Login from '@/screens/Login.vue' import Characters from '@/screens/Characters.vue' import Game from '@/screens/Game.vue' +import Loading from '@/screens/Loading.vue' import ZoneEditor from '@/screens/ZoneEditor.vue' import { computed } from 'vue' @@ -22,16 +23,11 @@ const gameStore = useGameStore() const zoneEditorStore = useZoneEditorStore() const currentScreen = computed(() => { + if (!gameStore.isAssetsLoaded) return Loading if (!gameStore.connection) return Login if (!gameStore.token) return Login if (!gameStore.character) return Characters if (zoneEditorStore.active) return ZoneEditor return Game }) - -// Disable right click -addEventListener('contextmenu', (event) => event.preventDefault()) - -// Disable pinch zoom -addEventListener('gesturestart', (event) => event.preventDefault()) </script> diff --git a/src/components/gameMaster/zoneEditor/ZoneEditor.vue b/src/components/gameMaster/zoneEditor/ZoneEditor.vue index dc9ee28..6dad5d4 100644 --- a/src/components/gameMaster/zoneEditor/ZoneEditor.vue +++ b/src/components/gameMaster/zoneEditor/ZoneEditor.vue @@ -14,11 +14,9 @@ </template> <script setup lang="ts"> -import { onBeforeMount, onUnmounted, ref } from 'vue' -import { useScene } from 'phavuer' +import { onUnmounted, ref } from 'vue' import { useGameStore } from '@/stores/gameStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore' -import { loadAssets } from '@/composables/zoneComposable' import { type Zone } from '@/types' // Components @@ -32,7 +30,6 @@ import Tiles from '@/components/gameMaster/zoneEditor/Tiles.vue' import Objects from '@/components/gameMaster/zoneEditor/Objects.vue' import EventTiles from '@/components/gameMaster/zoneEditor/EventTiles.vue' -const scene = useScene() const gameStore = useGameStore() const zoneEditorStore = useZoneEditorStore() @@ -62,11 +59,6 @@ function save() { }) } -onBeforeMount(async () => { - await gameStore.fetchAllZoneAssets() - await loadAssets(scene) -}) - onUnmounted(() => { zoneEditorStore.reset() }) diff --git a/src/components/zone/Zone.vue b/src/components/zone/Zone.vue index f94c321..05ac949 100644 --- a/src/components/zone/Zone.vue +++ b/src/components/zone/Zone.vue @@ -5,19 +5,16 @@ </template> <script setup lang="ts"> -import { useScene } from 'phavuer' import { useGameStore } from '@/stores/gameStore' import { useZoneStore } from '@/stores/zoneStore' -import { onBeforeMount, onBeforeUnmount, ref } from 'vue' +import { onBeforeUnmount, ref } from 'vue' import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types' import Tiles from '@/components/zone/Tiles.vue' import Objects from '@/components/zone/Objects.vue' import Characters from '@/components/zone/Characters.vue' -import { loadAssets } from '@/composables/zoneComposable' const gameStore = useGameStore() const zoneStore = useZoneStore() -const scene = useScene() const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null) @@ -28,13 +25,6 @@ type zoneLoadData = { // Event listeners gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => { - /** - * This is the cause of the bug - */ - // Fetch assets for new zone - await gameStore.fetchZoneAssets(data.zone.id) - await loadAssets(scene) - /** * @TODO : Update character via global event server-side, remove this and listen for it somewhere not here */ @@ -61,16 +51,9 @@ gameStore.connection!.on('character:move', (data: ExtendedCharacterT) => { zoneStore.updateCharacter(data) }) -onBeforeMount(() => { - gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => { - // Fetch assets for new zone - await gameStore.fetchZoneAssets(response.zone.id) - await loadAssets(scene) - - // Set zone and characters - zoneStore.setZone(response.zone) - zoneStore.setCharacters(response.characters) - }) +gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => { + zoneStore.setZone(response.zone) + zoneStore.setCharacters(response.characters) }) onBeforeUnmount(() => { diff --git a/src/composables/zoneComposable.ts b/src/composables/zoneComposable.ts index 0879d64..af3ce03 100644 --- a/src/composables/zoneComposable.ts +++ b/src/composables/zoneComposable.ts @@ -4,6 +4,7 @@ import TilemapLayer = Phaser.Tilemaps.TilemapLayer import Tileset = Phaser.Tilemaps.Tileset import Tile = Phaser.Tilemaps.Tile import { useGameStore } from '@/stores/gameStore' +import { useAssetManager } from '@/utilities/assets' export function getTile(layer: TilemapLayer | Tilemap, x: number, y: number): Tile | undefined { const tile = layer.getTileAtWorldXY(x, y) @@ -89,17 +90,35 @@ export const clearAssets = (scene: Phaser.Scene) => { export const loadAssets = (scene: Phaser.Scene): Promise<void> => { return new Promise((resolve) => { - const gameStore = useGameStore() + let assetManager = useAssetManager let addedLoad = false - gameStore.assets.forEach((asset) => { - if (scene.load.textureManager.exists(asset.key)) return - addedLoad = true - if (asset.group === 'sprite_animations') { - scene.load.spritesheet(asset.key, config.server_endpoint + asset.url, { frameWidth: asset.frameWidth ?? 0, frameHeight: asset.frameHeight ?? 0 }) - } else { - scene.load.image(asset.key, config.server_endpoint + asset.url) - } + assetManager.getAssetsByGroup('tiles').then((assets) => { + assets.forEach((asset) => { + if (scene.load.textureManager.exists(asset.key)) return + addedLoad = true + scene.textures.addBase64(asset.key, asset.data) + }) + }) + + assetManager.getAssetsByGroup('objects').then((assets) => { + assets.forEach((asset) => { + if (scene.load.textureManager.exists(asset.key)) return + addedLoad = true + scene.textures.addBase64(asset.key, asset.data) + }) + }) + + assetManager.getAssetsByGroup('sprites').then((assets) => { + assets.forEach((asset) => { + if (scene.load.textureManager.exists(asset.key)) return + let img = new Image(); + img.onload = () => { + scene.textures.addSpriteSheet(asset.key, img, { frameWidth: asset.frameWidth ?? 0, frameHeight: asset.frameHeight ?? 0 }) + }; + img.src = asset.data; + addedLoad = true + }) }) if (addedLoad) { diff --git a/src/screens/Characters.vue b/src/screens/Characters.vue index f048449..ab99162 100644 --- a/src/screens/Characters.vue +++ b/src/screens/Characters.vue @@ -114,13 +114,7 @@ gameStore.connection?.on('character:list', (data: any) => { }) onMounted(async () => { - /** - * Fetch sprite assets from the server - * This is done here because phaser needs it in createScene in Game.vue. - */ - await gameStore.fetchSpriteAssets() - - // wait 0.5 sec + // wait 0.75 sec setTimeout(() => { gameStore.connection?.emit('character:list') isLoading.value = false diff --git a/src/screens/Game.vue b/src/screens/Game.vue index 6432a9e..714bbd7 100644 --- a/src/screens/Game.vue +++ b/src/screens/Game.vue @@ -19,11 +19,12 @@ </div> </template> -<script setup lang="ts"> +<script setup lang="ts" async> import config from '@/config' import 'phaser' import { ref, onBeforeUnmount } from 'vue' import { Game, Scene } from 'phavuer' +import { useAssetManager } from '@/utilities/assets' import { useGameStore } from '@/stores/gameStore' import Menu from '@/components/gui/Menu.vue' import ExpBar from '@/components/gui/ExpBar.vue' @@ -36,6 +37,7 @@ import Effects from '@/components/Effects.vue' import { loadAssets } from '@/composables/zoneComposable' import Minimap from '@/components/gui/Minimap.vue' +const assetManager = useAssetManager const gameStore = useGameStore() const isLoaded = ref(false) @@ -120,16 +122,16 @@ const createScene = async (scene: Phaser.Scene) => { * Create sprite animations * This is done here because phaser forces us to */ - gameStore.assets.forEach((asset) => { - if (asset.group !== 'sprite_animations') return - - scene.anims.create({ - key: asset.key, - frameRate: 7, - frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }), - repeat: -1 - }) - }) + // assetManager.getAssetsByGroup('sprite_animations').then((assets) => { + // assets.forEach((asset) => { + // scene.anims.create({ + // key: asset.key, + // frameRate: 7, + // frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }), + // repeat: -1 + // }) + // }) + // }) } onBeforeUnmount(() => { diff --git a/src/screens/Loading.vue b/src/screens/Loading.vue new file mode 100644 index 0000000..c3abe7f --- /dev/null +++ b/src/screens/Loading.vue @@ -0,0 +1,78 @@ +<template> + <div class="flex flex-col justify-center items-center h-dvh relative"> + <div v-if="!isLoaded" class="w-20 h-20 rounded-full border-4 border-solid border-gray-300 border-t-transparent animate-spin"></div> + <button v-else @click="continueBtnClick" class="w-20 h-20 rounded-full bg-gray-500 flex items-center justify-center hover:bg-gray-600 transition-colors"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> + </svg> + </button> + <div v-if="!isLoaded" class="text-center mt-6"> + <h1 class="text-2xl font-bold">Loading...</h1> + <p class="text-gray-400">Please wait while we load the assets.</p> + </div> + </div> +</template> + +<script setup lang="ts" async> +import { onMounted, ref } from 'vue' +import config from '@/config' +import type { AssetT as ServerAsset } from '@/types' +import { useAssetManager } from '@/utilities/assets' +import { useGameStore } from '@/stores/gameStore' + +const gameStore = useGameStore(); +const assetManager = useAssetManager; +const isLoaded = ref(false) + +async function getAssets() { + return fetch(config.server_endpoint + '/assets/') + .then((response) => { + if (!response.ok) throw new Error('Failed to fetch assets') + console.log(response) + return response.json() + }) + .catch((error) => { + console.error('Error fetching assets:', error) + return false + }) +} + +async function loadAssetsIntoAssetManager(assets: ServerAsset[]): Promise<void> { + for (const asset of assets) { + // Check if the asset is already loaded + const existingAsset = await assetManager.getAsset(asset.key); + + // Check if the asset needs to be updated + if (!existingAsset || new Date(asset.updatedAt) > new Date(existingAsset.updatedAt)) { + + // Check if the asset is already loaded, if so, delete it + if (existingAsset) { + await assetManager.deleteAsset(asset.key); + } + + // Add the asset to the asset manager + await assetManager.addAsset( + asset.key, + asset.url, + asset.group, + asset.updatedAt, + asset.frameCount, + asset.frameWidth, + asset.frameHeight + ); + } + } +} + +function continueBtnClick() { + gameStore.isAssetsLoaded = true +} + +onMounted(async () => { + const assets = await getAssets() + if (assets) { + await loadAssetsIntoAssetManager(assets) + isLoaded.value = true + } +}) +</script> \ No newline at end of file diff --git a/src/screens/ZoneEditor.vue b/src/screens/ZoneEditor.vue index 239d036..b4b463e 100644 --- a/src/screens/ZoneEditor.vue +++ b/src/screens/ZoneEditor.vue @@ -16,7 +16,6 @@ import { Game, Scene } from 'phavuer' import { useGameStore } from '@/stores/gameStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore' import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue' -import { loadAssets } from '@/composables/zoneComposable' const gameStore = useGameStore() const zoneEditorStore = useZoneEditorStore() @@ -93,11 +92,6 @@ const preloadScene = async (scene: Phaser.Scene) => { scene.load.image('TELEPORT', '/assets/zone/tp_tile.png') scene.load.image('blank_tile', '/assets/zone/blank_tile.png') scene.load.image('waypoint', '/assets/waypoint.png') - - /** - * Load the assets into the Phaser scene - */ - await loadAssets(scene) } const createScene = async (scene: Phaser.Scene) => { diff --git a/src/services/authentication.ts b/src/services/authentication.ts index 1fed2d3..4ccb044 100644 --- a/src/services/authentication.ts +++ b/src/services/authentication.ts @@ -1,9 +1,8 @@ import axios from 'axios' import config from '@/config' -import { useGameStore } from '@/stores/gameStore' import { useCookies } from '@vueuse/integrations/useCookies' -export async function register(username: string, password: string, gameStore = useGameStore()) { +export async function register(username: string, password: string) { try { const response = await axios.post(`${config.server_endpoint}/register`, { username, password }) useCookies().set('token', response.data.token as string) @@ -13,13 +12,13 @@ export async function register(username: string, password: string, gameStore = u } } -export async function login(username: string, password: string, gameStore = useGameStore()) { +export async function login(username: string, password: string) { try { const response = await axios.post(`${config.server_endpoint}/login`, { username, password }) useCookies().set('token', response.data.token as string, { // for whole domain // @TODO : #190 - domain: window.location.hostname.split('.').slice(-2).join('.') + // domain: window.location.hostname.split('.').slice(-2).join('.') }) return { success: true, token: response.data.token } } catch (error: any) { diff --git a/src/stores/gameStore.ts b/src/stores/gameStore.ts index df3ca75..c6737fd 100644 --- a/src/stores/gameStore.ts +++ b/src/stores/gameStore.ts @@ -7,9 +7,8 @@ import { useCookies } from '@vueuse/integrations/useCookies' export const useGameStore = defineStore('game', { state: () => { return { - loginMessage: null as string | null, notifications: [] as Notification[], - assets: [] as Asset[], + isAssetsLoaded: false, token: '' as string | null, connection: null as Socket | null, user: null as User | null, @@ -47,54 +46,6 @@ export const useGameStore = defineStore('game', { removeNotification(id: string) { this.notifications = this.notifications.filter((notification: Notification) => notification.id !== id) }, - setAssets(assets: Asset[]) { - this.assets = assets - }, - addAsset(asset: Asset) { - this.assets.push(asset) - }, - addAssets(assets: Asset[]) { - this.assets = this.assets.concat(assets) - }, - async fetchSpriteAssets() { - return fetch(config.server_endpoint + '/assets/sprites') - .then((response) => response.json()) - .then((assets) => { - // Only add the sprites that are not already in the store - this.addAssets(assets.filter((asset: Asset) => !this.getAssetByKey(asset.key))) - return true - }) - .catch((error) => { - console.error('Error fetching assets:', error) - return false - }) - }, - async fetchZoneAssets(zoneId: number) { - return fetch(config.server_endpoint + '/assets/zone/' + zoneId) - .then((response) => response.json()) - .then((assets) => { - // Only add the zones that are not already in the store - this.addAssets(assets.filter((asset: Asset) => !this.getAssetByKey(asset.key))) - return true - }) - .catch((error) => { - console.error('Error fetching assets:', error) - return false - }) - }, - async fetchAllZoneAssets() { - return fetch(config.server_endpoint + '/assets/zone') - .then((response) => response.json()) - .then((assets) => { - // Only add the zones that are not already in the store - this.addAssets(assets.filter((asset: Asset) => !this.getAssetByKey(asset.key))) - return true - }) - .catch((error) => { - console.error('Error fetching assets:', error) - return false - }) - }, setToken(token: string) { this.token = token }, @@ -152,15 +103,15 @@ export const useGameStore = defineStore('game', { }) }, disconnectSocket() { - if (this.connection) this.connection.disconnect() + this.connection?.disconnect() useCookies().remove('token', { // for whole domain // @TODO : #190 - domain: window.location.hostname.split('.').slice(-2).join('.') + // domain: window.location.hostname.split('.').slice(-2).join('.') }) - this.assets = [] + // this.isAssetsLoaded = false this.connection = null this.token = null this.user = null diff --git a/src/types.ts b/src/types.ts index a784736..0ffa1d8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,10 +4,11 @@ export type Notification = { message?: string } -export type Asset = { +export type AssetT = { key: string url: string group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other' + updatedAt: Date frameCount?: number frameWidth?: number frameHeight?: number diff --git a/src/utilities/assets.ts b/src/utilities/assets.ts new file mode 100644 index 0000000..e33db9d --- /dev/null +++ b/src/utilities/assets.ts @@ -0,0 +1,101 @@ +import config from '@/config'; +import Dexie from 'dexie'; + +interface Asset { + key: string; + data: Blob; + group: string; + updatedAt: Date; + frameCount?: number; + frameWidth?: number; + frameHeight?: number; +} + +interface AssetWithUrl extends Omit<Asset, 'data'> { + data: string; +} + +class AssetManager extends Dexie { + assets!: Dexie.Table<Asset, string>; + + constructor() { + super('AssetManager'); + this.version(1).stores({ + assets: 'key, group' + }); + } + + async getAssetCount(): Promise<number> { + try { + const count = await this.assets.count(); + return count; + } catch (error) { + console.error('Failed to get asset count:', error); + return 0; + } + } + + async addAsset( + key: string, + url: string, + group: string, + updatedAt: Date, + frameCount?: number, + frameWidth?: number, + frameHeight?: number + ): Promise<void> { + try { + const response = await fetch(config.server_endpoint + url); + const blob = await response.blob(); + await this.assets.put({ key, data: blob, group, updatedAt, frameCount, frameWidth, frameHeight }); + } catch (error) { + console.error(`Failed to add asset ${key}:`, error); + } + } + + async getAsset(key: string): Promise<AssetWithUrl | null> { + try { + const asset = await this.assets.get(key); + if (asset) { + return { + ...asset, + data: URL.createObjectURL(asset.data) + }; + } + } catch (error) { + console.error(`Failed to retrieve asset ${key}:`, error); + } + return null; + } + + async getAssetsByGroup(group: string): Promise<AssetWithUrl[]> { + try { + const assets = await this.assets.where('group').equals(group).toArray(); + return assets.map(asset => ({ + ...asset, + data: URL.createObjectURL(asset.data) + })); + } catch (error) { + console.error(`Failed to retrieve assets for group ${group}:`, error); + return []; + } + } + + async clearAssets(): Promise<void> { + try { + await this.assets.clear(); + } catch (error) { + console.error('Failed to clear assets:', error); + } + } + + async deleteAsset(key: string): Promise<void> { + try { + await this.assets.delete(key); + } catch (error) { + console.error(`Failed to delete asset ${key}:`, error); + } + } +} + +export const useAssetManager = new AssetManager(); \ No newline at end of file