Compare commits
45 Commits
feature/ch
...
feature/#2
Author | SHA1 | Date | |
---|---|---|---|
bdc566e30f | |||
a653b61b51 | |||
7b61f71fa9 | |||
42539cc73d | |||
864369860c | |||
89938b7f93 | |||
f076661d4a | |||
0b018f53a7 | |||
32a13f0f3c | |||
f203bf9534 | |||
67b6339ffc | |||
6233da0044 | |||
56f455a08e | |||
9d541cc46a | |||
adf86d369b | |||
bbcb84ed03 | |||
6f40c774ea | |||
e3b70df46f | |||
584262a59b | |||
7e4e613405 | |||
e2c60bfd5a | |||
e38c402266 | |||
a299d5dc55 | |||
43c0f0ab1e | |||
ed17e7f16e | |||
7d723530e6 | |||
a3532a5940 | |||
74cbf3f2c8 | |||
d402744955 | |||
39b65b3884 | |||
c62ff2efc1 | |||
08f55c9680 | |||
1afc50ea6a | |||
7c259f455c | |||
be854a79b8 | |||
a71890ab68 | |||
dc2b6b9851 | |||
d091aabeb3 | |||
c261937cf5 | |||
4aa1309797 | |||
4f8517a50c | |||
446e049e6e | |||
7db2ba322c | |||
70fb732051 | |||
5128aa83f9 |
@ -1,4 +1,4 @@
|
|||||||
VITE_NAME=New Quest
|
VITE_NAME=Sylvan Quest
|
||||||
VITE_DEVELOPMENT=true
|
VITE_DEVELOPMENT=true
|
||||||
VITE_SERVER_ENDPOINT=http://localhost:4000
|
VITE_SERVER_ENDPOINT=http://localhost:4000
|
||||||
VITE_TILE_SIZE_X=64
|
VITE_TILE_SIZE_X=64
|
||||||
|
684
package-lock.json
generated
684
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -50,6 +50,7 @@
|
|||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^5.4.9",
|
"vite": "^5.4.9",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-vue-devtools": "^7.5.2",
|
"vite-plugin-vue-devtools": "^7.5.2",
|
||||||
"vitest": "^2.0.3",
|
"vitest": "^2.0.3",
|
||||||
"vue-tsc": "^1.6.5"
|
"vue-tsc": "^1.6.5"
|
||||||
|
30
src/App.vue
30
src/App.vue
@ -2,7 +2,6 @@
|
|||||||
<Notifications />
|
<Notifications />
|
||||||
<GmTools v-if="gameStore.character?.role === 'gm'" />
|
<GmTools v-if="gameStore.character?.role === 'gm'" />
|
||||||
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||||
|
|
||||||
<component :is="currentScreen" />
|
<component :is="currentScreen" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -12,22 +11,37 @@ import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|||||||
import Notifications from '@/components/utilities/Notifications.vue'
|
import Notifications from '@/components/utilities/Notifications.vue'
|
||||||
import GmTools from '@/components/gameMaster/GmTools.vue'
|
import GmTools from '@/components/gameMaster/GmTools.vue'
|
||||||
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
||||||
import Login from '@/screens/Login.vue'
|
import Login from '@/components/screens/Login.vue'
|
||||||
import Characters from '@/screens/Characters.vue'
|
import Characters from '@/components/screens/Characters.vue'
|
||||||
import Game from '@/screens/Game.vue'
|
import Game from '@/components/screens/Game.vue'
|
||||||
// import Loading from '@/screens/Loading.vue'
|
import ZoneEditor from '@/components/screens/ZoneEditor.vue'
|
||||||
import ZoneEditor from '@/screens/ZoneEditor.vue'
|
import { computed, watch } from 'vue'
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
const currentScreen = computed(() => {
|
const currentScreen = computed(() => {
|
||||||
// if (!gameStore.isAssetsLoaded) return Loading
|
|
||||||
if (!gameStore.connection) return Login
|
if (!gameStore.connection) return Login
|
||||||
if (!gameStore.token) return Login
|
if (!gameStore.token) return Login
|
||||||
if (!gameStore.character) return Characters
|
if (!gameStore.character) return Characters
|
||||||
if (zoneEditorStore.active) return ZoneEditor
|
if (zoneEditorStore.active) return ZoneEditor
|
||||||
return Game
|
return Game
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Watch zoneEditorStore.active and empty gameStore.game.loadedAssets
|
||||||
|
watch(
|
||||||
|
() => zoneEditorStore.active,
|
||||||
|
() => {
|
||||||
|
gameStore.game.loadedAssets = []
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// #209: Play sound when a button is pressed
|
||||||
|
addEventListener('click', (event) => {
|
||||||
|
if (!(event.target instanceof HTMLButtonElement)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const audio = new Audio('/assets/music/click-btn.mp3')
|
||||||
|
audio.play()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,146 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Controls :layer="tiles" :depth="0" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import config from '@/config'
|
|
||||||
import { useScene } from 'phavuer'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import { onBeforeMount, onBeforeUnmount } from 'vue'
|
|
||||||
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
|
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
|
||||||
|
|
||||||
const emit = defineEmits(['tilemap:create'])
|
|
||||||
|
|
||||||
const scene = useScene()
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
|
||||||
|
|
||||||
const zoneTilemap = createTilemap()
|
|
||||||
const tiles = createTileLayer()
|
|
||||||
|
|
||||||
function createTilemap() {
|
|
||||||
const zoneData = new Phaser.Tilemaps.MapData({
|
|
||||||
width: zoneEditorStore.zone?.width,
|
|
||||||
height: zoneEditorStore.zone?.height,
|
|
||||||
tileWidth: config.tile_size.x,
|
|
||||||
tileHeight: config.tile_size.y,
|
|
||||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
|
||||||
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
|
||||||
})
|
|
||||||
const tilemap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
|
||||||
emit('tilemap:create', tilemap)
|
|
||||||
return tilemap
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTileLayer() {
|
|
||||||
const tilesetImages = gameStore.assets.filter((asset) => asset.group === 'tiles').map((asset, index) => zoneTilemap.addTilesetImage(asset.key, asset.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y }))
|
|
||||||
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
|
||||||
|
|
||||||
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages as any, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
|
||||||
|
|
||||||
layer.setDepth(0)
|
|
||||||
layer.setCullPadding(2, 2)
|
|
||||||
|
|
||||||
return layer
|
|
||||||
}
|
|
||||||
|
|
||||||
function pencil(pointer: Phaser.Input.Pointer) {
|
|
||||||
// Check if zone is set
|
|
||||||
if (!zoneEditorStore.zone) return
|
|
||||||
|
|
||||||
// Check if tool is pencil
|
|
||||||
if (zoneEditorStore.tool !== 'pencil') return
|
|
||||||
|
|
||||||
// Check if draw mode is tile
|
|
||||||
if (zoneEditorStore.drawMode !== 'tile') return
|
|
||||||
|
|
||||||
// Check if there is a selected tile
|
|
||||||
if (!zoneEditorStore.selectedTile) return
|
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
|
||||||
if (!pointer.isDown) return
|
|
||||||
|
|
||||||
// Check if shift is not pressed, this means we are moving the camera
|
|
||||||
if (pointer.event.shiftKey) return
|
|
||||||
|
|
||||||
// Check if there is a tile
|
|
||||||
const tile = getTile(tiles, pointer.worldX, pointer.worldY)
|
|
||||||
if (!tile) return
|
|
||||||
|
|
||||||
// Place tile
|
|
||||||
placeTile(zoneTilemap, tiles, tile.x, tile.y, zoneEditorStore.selectedTile.id)
|
|
||||||
|
|
||||||
// Adjust zoneEditorStore.zone.tiles
|
|
||||||
zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile.id
|
|
||||||
}
|
|
||||||
|
|
||||||
function eraser(pointer: Phaser.Input.Pointer) {
|
|
||||||
// Check if zone is set
|
|
||||||
if (!zoneEditorStore.zone) return
|
|
||||||
|
|
||||||
// Check if tool is pencil
|
|
||||||
if (zoneEditorStore.tool !== 'eraser') return
|
|
||||||
|
|
||||||
// Check if draw mode is tile
|
|
||||||
if (zoneEditorStore.eraserMode !== 'tile') return
|
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
|
||||||
if (!pointer.isDown) return
|
|
||||||
|
|
||||||
// Check if shift is not pressed, this means we are moving the camera
|
|
||||||
if (pointer.event.shiftKey) return
|
|
||||||
|
|
||||||
// Check if there is a tile
|
|
||||||
const tile = getTile(tiles, pointer.worldX, pointer.worldY)
|
|
||||||
if (!tile) return
|
|
||||||
|
|
||||||
// Place tile
|
|
||||||
placeTile(zoneTilemap, tiles, tile.x, tile.y, 'blank_tile')
|
|
||||||
|
|
||||||
// Adjust zoneEditorStore.zone.tiles
|
|
||||||
zoneEditorStore.zone.tiles[tile.y][tile.x] = 'blank_tile'
|
|
||||||
}
|
|
||||||
|
|
||||||
function paint(pointer: Phaser.Input.Pointer) {
|
|
||||||
// Check if zone is set
|
|
||||||
if (!zoneEditorStore.zone) return
|
|
||||||
|
|
||||||
// Check if tool is pencil
|
|
||||||
if (zoneEditorStore.tool !== 'paint') return
|
|
||||||
|
|
||||||
// Check if there is a selected tile
|
|
||||||
if (!zoneEditorStore.selectedTile) return
|
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
|
||||||
if (!pointer.isDown) return
|
|
||||||
|
|
||||||
// Set new tileArray with selected tile
|
|
||||||
setLayerTiles(zoneTilemap, tiles, createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id))
|
|
||||||
|
|
||||||
// Adjust zoneEditorStore.zone.tiles
|
|
||||||
zoneEditorStore.zone.tiles = createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
if (!zoneEditorStore.zone?.tiles) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setLayerTiles(zoneTilemap, tiles, zoneEditorStore.zone.tiles)
|
|
||||||
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
|
|
||||||
|
|
||||||
zoneTilemap.destroyLayer('tiles')
|
|
||||||
zoneTilemap.removeAllLayers()
|
|
||||||
zoneTilemap.destroy()
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Tiles @tilemap:create="tileMap = $event" />
|
<ZoneTiles @tileMap:create="tileMap = $event" />
|
||||||
<Objects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
<EventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<ZoneEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
|
|
||||||
<Toolbar @save="save" />
|
<Toolbar @save="save" @clear="clear" />
|
||||||
|
|
||||||
<ZoneList />
|
<ZoneList />
|
||||||
<TileList />
|
<TileList />
|
||||||
@ -26,15 +26,24 @@ import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.v
|
|||||||
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
|
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
|
||||||
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
|
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
|
||||||
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
|
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
|
||||||
import Tiles from '@/components/gameMaster/zoneEditor/Tiles.vue'
|
import ZoneTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue'
|
||||||
import Objects from '@/components/gameMaster/zoneEditor/Objects.vue'
|
import ZoneObjects from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObjects.vue'
|
||||||
import EventTiles from '@/components/gameMaster/zoneEditor/EventTiles.vue'
|
import ZoneEventTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneEventTiles.vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
// Clear objects, event tiles and tiles
|
||||||
|
zoneEditorStore.zone.zoneObjects = []
|
||||||
|
zoneEditorStore.zone.zoneEventTiles = []
|
||||||
|
zoneEditorStore.triggerClearTiles()
|
||||||
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
if (!zoneEditorStore.zone) return
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
@ -42,23 +42,18 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { ref, onMounted, computed, watch } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import type { Object } from '@/types'
|
import type { Object, ZoneObject } from '@/types'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const isModalOpen = ref(false)
|
const isModalOpen = ref(false)
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
// const objectDepth = ref(0)
|
|
||||||
const selectedTags = ref<string[]>([])
|
const selectedTags = ref<string[]>([])
|
||||||
|
|
||||||
// watch(objectDepth, (depth) => {
|
|
||||||
// zoneEditorStore.setObjectDepth(depth)
|
|
||||||
// })
|
|
||||||
|
|
||||||
const uniqueTags = computed(() => {
|
const uniqueTags = computed(() => {
|
||||||
const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || [])
|
const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || [])
|
||||||
return Array.from(new Set(allTags))
|
return Array.from(new Set(allTags))
|
||||||
@ -81,8 +76,6 @@ const toggleTag = (tag: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
zoneEditorStore.setObjectDepth(0)
|
|
||||||
|
|
||||||
isModalOpen.value = true
|
isModalOpen.value = true
|
||||||
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
||||||
zoneEditorStore.setObjectList(response)
|
zoneEditorStore.setObjectList(response)
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
||||||
:src="`${config.server_endpoint}/assets/tiles/${selectedGroup.parent.id}.png`"
|
:src="`${config.server_endpoint}/assets/tiles/${selectedGroup.parent.id}.png`"
|
||||||
:alt="selectedGroup.parent.name"
|
:alt="selectedGroup.parent.name"
|
||||||
@click="selectTile(selectedGroup.parent)"
|
@click="selectTile(selectedGroup.parent.id)"
|
||||||
:class="{
|
:class="{
|
||||||
'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent),
|
'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent),
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
||||||
@ -65,7 +65,7 @@
|
|||||||
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
||||||
:src="`${config.server_endpoint}/assets/tiles/${childTile.id}.png`"
|
:src="`${config.server_endpoint}/assets/tiles/${childTile.id}.png`"
|
||||||
:alt="childTile.name"
|
:alt="childTile.name"
|
||||||
@click="selectTile(childTile)"
|
@click="selectTile(childTile.id)"
|
||||||
:class="{
|
:class="{
|
||||||
'border-cyan shadow-lg scale-105': isActiveTile(childTile),
|
'border-cyan shadow-lg scale-105': isActiveTile(childTile),
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
||||||
@ -218,7 +218,7 @@ function closeGroup() {
|
|||||||
selectedGroup.value = null
|
selectedGroup.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTile(tile: Tile) {
|
function selectTile(tile: string) {
|
||||||
zoneEditorStore.setSelectedTile(tile)
|
zoneEditorStore.setSelectedTile(tile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|||||||
import { Image, useScene } from 'phavuer'
|
import { Image, useScene } from 'phavuer'
|
||||||
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
import { uuidv4 } from '@/utilities'
|
import { uuidv4 } from '@/utilities'
|
||||||
import { onBeforeMount, onBeforeUnmount } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
@ -102,14 +102,14 @@ function eraser(pointer: Phaser.Input.Pointer) {
|
|||||||
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
|
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onMounted(() => {
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onUnmounted(() => {
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Image, useScene } from 'phavuer'
|
||||||
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
|
import type { AssetDataT, ZoneObject } from '@/types'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
|
zoneObject: ZoneObject
|
||||||
|
selectedZoneObject: ZoneObject | null
|
||||||
|
movingZoneObject: ZoneObject | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const imageProps = computed(() => ({
|
||||||
|
alpha: props.movingZoneObject?.id === props.zoneObject.id ? 0.5 : 1,
|
||||||
|
tint: props.selectedZoneObject?.id === props.zoneObject.id ? 0x00ff00 : 0xffffff,
|
||||||
|
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
|
||||||
|
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
||||||
|
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
||||||
|
flipX: props.zoneObject.isRotated,
|
||||||
|
texture: props.zoneObject.object.id,
|
||||||
|
originY: Number(props.zoneObject.object.originX),
|
||||||
|
originX: Number(props.zoneObject.object.originY)
|
||||||
|
}))
|
||||||
|
|
||||||
|
loadTexture(scene, {
|
||||||
|
key: props.zoneObject.object.id,
|
||||||
|
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
|
||||||
|
group: 'objects',
|
||||||
|
updatedAt: props.zoneObject.object.updatedAt,
|
||||||
|
frameWidth: props.zoneObject.object.frameWidth,
|
||||||
|
frameHeight: props.zoneObject.object.frameHeight
|
||||||
|
} as AssetDataT).catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,40 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" />
|
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" :movingZoneObject="movingZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" />
|
||||||
<Image v-for="object in zoneEditorStore.zone?.zoneObjects" v-bind="getImageProps(object)" @pointerup="() => (selectedZoneObject = object)" />
|
<ZoneObject v-for="zoneObject in zoneEditorStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject :selectedZoneObject :movingZoneObject @pointerup="clickZoneObject(zoneObject)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { uuidv4 } from '@/utilities'
|
import { uuidv4 } from '@/utilities'
|
||||||
import { calculateIsometricDepth, getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
import { getTile } from '@/composables/zoneComposable'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import type { ZoneObject } from '@/types'
|
|
||||||
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
|
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
|
||||||
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
import ZoneObject from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObject.vue'
|
||||||
|
import type { ZoneObject as ZoneObjectT } from '@/types'
|
||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const selectedZoneObject = ref<ZoneObject | null>(null)
|
const selectedZoneObject = ref<ZoneObjectT | null>(null)
|
||||||
const movingZoneObject = ref<ZoneObject | null>(null)
|
const movingZoneObject = ref<ZoneObjectT | null>(null)
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function getImageProps(zoneObject: ZoneObject) {
|
|
||||||
return {
|
|
||||||
alpha: zoneObject.id === movingZoneObject.value?.id ? 0.5 : 1,
|
|
||||||
depth: calculateIsometricDepth(zoneObject.positionX, zoneObject.positionY, zoneObject.object.frameWidth, zoneObject.object.frameHeight),
|
|
||||||
tint: selectedZoneObject.value?.id === zoneObject.id ? 0x00ff00 : 0xffffff,
|
|
||||||
x: tileToWorldX(props.tilemap, zoneObject.positionX, zoneObject.positionY),
|
|
||||||
y: tileToWorldY(props.tilemap, zoneObject.positionX, zoneObject.positionY),
|
|
||||||
flipX: zoneObject.isRotated,
|
|
||||||
texture: zoneObject.object.id,
|
|
||||||
originY: Number(zoneObject.object.originX),
|
|
||||||
originX: Number(zoneObject.object.originY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pencil(pointer: Phaser.Input.Pointer) {
|
function pencil(pointer: Phaser.Input.Pointer) {
|
||||||
// Check if zone is set
|
// Check if zone is set
|
||||||
if (!zoneEditorStore.zone) return
|
if (!zoneEditorStore.zone) return
|
||||||
@ -54,6 +41,9 @@ function pencil(pointer: Phaser.Input.Pointer) {
|
|||||||
// Check if shift is not pressed, this means we are moving the camera
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
if (pointer.event.shiftKey) return
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed, this means we are selecting the object
|
||||||
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
@ -66,7 +56,7 @@ function pencil(pointer: Phaser.Input.Pointer) {
|
|||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
zoneId: zoneEditorStore.zone.id,
|
zoneId: zoneEditorStore.zone.id,
|
||||||
zone: zoneEditorStore.zone,
|
zone: zoneEditorStore.zone,
|
||||||
objectId: zoneEditorStore.selectedObject.id,
|
objectId: zoneEditorStore.selectedObject,
|
||||||
object: zoneEditorStore.selectedObject,
|
object: zoneEditorStore.selectedObject,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
isRotated: false,
|
isRotated: false,
|
||||||
@ -94,6 +84,9 @@ function eraser(pointer: Phaser.Input.Pointer) {
|
|||||||
// Check if shift is not pressed, this means we are moving the camera
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
if (pointer.event.shiftKey) return
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed, this means we are selecting the object
|
||||||
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
@ -106,6 +99,37 @@ function eraser(pointer: Phaser.Input.Pointer) {
|
|||||||
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== existingObject.id)
|
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== existingObject.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function objectPicker(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if zone is set
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (zoneEditorStore.tool !== 'pencil') return
|
||||||
|
|
||||||
|
// Check if draw mode is object
|
||||||
|
if (zoneEditorStore.drawMode !== 'object') return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// If alt is not pressed, return
|
||||||
|
if (!pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Check if there is a tile
|
||||||
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
// Check if object already exists on position
|
||||||
|
const existingObject = zoneEditorStore.zone.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
|
||||||
|
if (!existingObject) return
|
||||||
|
|
||||||
|
// Select the object
|
||||||
|
zoneEditorStore.setSelectedObject(existingObject)
|
||||||
|
}
|
||||||
|
|
||||||
function moveZoneObject(id: string) {
|
function moveZoneObject(id: string) {
|
||||||
// Check if zone is set
|
// Check if zone is set
|
||||||
if (!zoneEditorStore.zone) return
|
if (!zoneEditorStore.zone) return
|
||||||
@ -155,18 +179,29 @@ function deleteZoneObject(id: string) {
|
|||||||
selectedZoneObject.value = null
|
selectedZoneObject.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
function clickZoneObject(zoneObject: ZoneObjectT) {
|
||||||
|
selectedZoneObject.value = zoneObject
|
||||||
|
|
||||||
|
// If alt is pressed, select the object
|
||||||
|
if (scene.input.activePointer.event.altKey) {
|
||||||
|
zoneEditorStore.setSelectedObject(zoneObject.object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, objectPicker)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onUnmounted(() => {
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, objectPicker)
|
||||||
})
|
})
|
||||||
|
|
||||||
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
|
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
|
212
src/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue
Normal file
212
src/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
<template>
|
||||||
|
<Controls :layer="tileLayer" :depth="0" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/config'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
|
||||||
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import type { AssetDataT } from '@/types'
|
||||||
|
|
||||||
|
const emit = defineEmits(['tileMap:create'])
|
||||||
|
|
||||||
|
const scene = useScene()
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
const tileMap = createTileMap()
|
||||||
|
const tileLayer = createTileLayer()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Tilemap is a container for Tilemap data.
|
||||||
|
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
|
||||||
|
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
|
||||||
|
*/
|
||||||
|
function createTileMap() {
|
||||||
|
const zoneData = new Phaser.Tilemaps.MapData({
|
||||||
|
width: zoneEditorStore.zone?.width,
|
||||||
|
height: zoneEditorStore.zone?.height,
|
||||||
|
tileWidth: config.tile_size.x,
|
||||||
|
tileHeight: config.tile_size.y,
|
||||||
|
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
||||||
|
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
||||||
|
})
|
||||||
|
|
||||||
|
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
||||||
|
emit('tileMap:create', newTileMap)
|
||||||
|
|
||||||
|
return newTileMap
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
|
||||||
|
*/
|
||||||
|
function createTileLayer() {
|
||||||
|
const tilesArray = gameStore.getLoadedAssetsByGroup('tiles')
|
||||||
|
|
||||||
|
const tilesetImages = Array.from(tilesArray).map((tile: AssetDataT, index: number) => {
|
||||||
|
return tileMap.addTilesetImage(tile.key, tile.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
|
||||||
|
}) as any
|
||||||
|
|
||||||
|
// Add blank tile
|
||||||
|
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
||||||
|
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
|
layer.setDepth(0)
|
||||||
|
layer.setCullPadding(2, 2)
|
||||||
|
|
||||||
|
return layer
|
||||||
|
}
|
||||||
|
|
||||||
|
function pencil(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if zone is set
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (zoneEditorStore.tool !== 'pencil') return
|
||||||
|
|
||||||
|
// Check if draw mode is tile
|
||||||
|
if (zoneEditorStore.drawMode !== 'tile') return
|
||||||
|
|
||||||
|
// Check if there is a selected tile
|
||||||
|
if (!zoneEditorStore.selectedTile) return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if there is a tile
|
||||||
|
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
// Place tile
|
||||||
|
placeTile(tileMap, tileLayer, tile.x, tile.y, zoneEditorStore.selectedTile)
|
||||||
|
|
||||||
|
// Adjust zoneEditorStore.zone.tiles
|
||||||
|
zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile
|
||||||
|
}
|
||||||
|
|
||||||
|
function eraser(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if zone is set
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (zoneEditorStore.tool !== 'eraser') return
|
||||||
|
|
||||||
|
// Check if draw mode is tile
|
||||||
|
if (zoneEditorStore.eraserMode !== 'tile') return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed
|
||||||
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Check if there is a tile
|
||||||
|
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
// Place tile
|
||||||
|
placeTile(tileMap, tileLayer, tile.x, tile.y, 'blank_tile')
|
||||||
|
|
||||||
|
// Adjust zoneEditorStore.zone.tiles
|
||||||
|
zoneEditorStore.zone.tiles[tile.y][tile.x] = 'blank_tile'
|
||||||
|
}
|
||||||
|
|
||||||
|
function paint(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if zone is set
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (zoneEditorStore.tool !== 'paint') return
|
||||||
|
|
||||||
|
// Check if there is a selected tile
|
||||||
|
if (!zoneEditorStore.selectedTile) return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed
|
||||||
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Set new tileArray with selected tile
|
||||||
|
setLayerTiles(tileMap, tileLayer, createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile))
|
||||||
|
|
||||||
|
// Adjust zoneEditorStore.zone.tiles
|
||||||
|
zoneEditorStore.zone.tiles = createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
|
||||||
|
function tilePicker(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if zone is set
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (zoneEditorStore.tool !== 'pencil') return
|
||||||
|
|
||||||
|
// Check if draw mode is tile
|
||||||
|
if (zoneEditorStore.drawMode !== 'tile') return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed
|
||||||
|
if (!pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Check if there is a tile
|
||||||
|
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
// Select the tile
|
||||||
|
zoneEditorStore.setSelectedTile(zoneEditorStore.zone.tiles[tile.y][tile.x])
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => zoneEditorStore.shouldClearTiles,
|
||||||
|
(shouldClear) => {
|
||||||
|
if (shouldClear && zoneEditorStore.zone) {
|
||||||
|
const blankTiles = createTileArray(tileMap.width, tileMap.height, 'blank_tile')
|
||||||
|
setLayerTiles(tileMap, tileLayer, blankTiles)
|
||||||
|
zoneEditorStore.zone.tiles = blankTiles
|
||||||
|
zoneEditorStore.resetClearTilesFlag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!zoneEditorStore.zone?.tiles) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLayerTiles(tileMap, tileLayer, zoneEditorStore.zone.tiles)
|
||||||
|
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, tilePicker)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
|
||||||
|
|
||||||
|
tileMap.destroyLayer('tiles')
|
||||||
|
tileMap.removeAllLayers()
|
||||||
|
tileMap.destroy()
|
||||||
|
})
|
||||||
|
</script>
|
@ -168,8 +168,8 @@ function stopDrag() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function adjustPosition() {
|
function adjustPosition() {
|
||||||
x.value = Math.max(0, Math.min(x.value, window.innerWidth - width.value))
|
x.value = Math.min(x.value, window.innerWidth - width.value)
|
||||||
y.value = Math.max(0, Math.min(y.value, window.innerHeight - height.value))
|
y.value = Math.min(y.value, window.innerHeight - height.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializePosition() {
|
function initializePosition() {
|
||||||
@ -236,6 +236,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
removeEventListener('keydown', keyPress)
|
||||||
removeEventListener('mousemove', drag)
|
removeEventListener('mousemove', drag)
|
||||||
removeEventListener('mouseup', stopDrag)
|
removeEventListener('mouseup', stopDrag)
|
||||||
})
|
})
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
<div class="flex gap-14 w-full max-h-[650px] overflow-x-auto" v-if="!isLoading">
|
<div class="flex gap-14 w-full max-h-[650px] overflow-x-auto" v-if="!isLoading">
|
||||||
<!-- CHARACTER LIST -->
|
<!-- CHARACTER LIST -->
|
||||||
<div v-for="character in characters" :key="character.id" class="group first:ml-auto last:mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 relative shadow-character" :class="{ active: selected_character == character.id }">
|
<div v-for="character in characters" :key="character.id" class="group first:ml-auto last:mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 relative shadow-character" :class="{ active: selected_character == character.id }">
|
||||||
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" />
|
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" />
|
||||||
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" />
|
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" alt="UI box inner" />
|
||||||
<input class="opacity-0 h-full w-full absolute m-0 z-10" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" />
|
<input class="opacity-0 h-full w-full absolute m-0 z-10" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" />
|
||||||
<label class="font-bold absolute left-1/2 top-4 max-w-32 -translate-x-1/2 -translate-y-1/2 text-center text-ellipsis overflow-hidden whitespace-nowrap drop-shadow-text" :for="character.id">{{ character.name }}</label>
|
<label class="font-bold absolute left-1/2 top-4 max-w-32 -translate-x-1/2 -translate-y-1/2 text-center text-ellipsis overflow-hidden whitespace-nowrap drop-shadow-text" :for="character.id">{{ character.name }}</label>
|
||||||
|
|
||||||
@ -103,10 +103,10 @@ import Modal from '@/components/utilities/Modal.vue'
|
|||||||
import { type Character as CharacterT } from '@/types'
|
import { type Character as CharacterT } from '@/types'
|
||||||
import ConfirmationModal from '@/components/utilities/ConfirmationModal.vue'
|
import ConfirmationModal from '@/components/utilities/ConfirmationModal.vue'
|
||||||
|
|
||||||
const isLoading = ref(true)
|
|
||||||
const characters = ref([])
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const deletingCharacter = ref(null)
|
const isLoading = ref(true)
|
||||||
|
const characters = ref([] as CharacterT[])
|
||||||
|
const deletingCharacter = ref(null as CharacterT | null)
|
||||||
|
|
||||||
// Fetch characters
|
// Fetch characters
|
||||||
gameStore.connection?.on('character:list', (data: any) => {
|
gameStore.connection?.on('character:list', (data: any) => {
|
@ -32,10 +32,8 @@ import CharacterProfile from '@/components/gui/CharacterProfile.vue'
|
|||||||
import Effects from '@/components/Effects.vue'
|
import Effects from '@/components/Effects.vue'
|
||||||
import Minimap from '@/components/gui/Minimap.vue'
|
import Minimap from '@/components/gui/Minimap.vue'
|
||||||
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
|
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
|
||||||
import { useAssetManager } from '@/utilities/assetManager'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const assetManager = useAssetManager
|
|
||||||
|
|
||||||
const gameConfig = {
|
const gameConfig = {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
@ -78,41 +76,7 @@ function preloadScene(scene: Phaser.Scene) {
|
|||||||
*/
|
*/
|
||||||
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
||||||
scene.load.image('waypoint', '/assets/waypoint.png')
|
scene.load.image('waypoint', '/assets/waypoint.png')
|
||||||
|
|
||||||
scene.load.rexAwait(async function (successCallback) {
|
|
||||||
await assetManager.getAssetsByGroup('tiles').then((assets) => {
|
|
||||||
assets.forEach((asset) => {
|
|
||||||
if (scene.load.textureManager.exists(asset.key)) return
|
|
||||||
scene.textures.addBase64(asset.key, asset.data)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Load objects
|
|
||||||
await assetManager.getAssetsByGroup('objects').then((assets) => {
|
|
||||||
assets.forEach((asset) => {
|
|
||||||
if (scene.load.textureManager.exists(asset.key)) return
|
|
||||||
scene.textures.addBase64(asset.key, asset.data)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
successCallback()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createScene(scene: Phaser.Scene) {
|
function createScene(scene: Phaser.Scene) {}
|
||||||
/**
|
|
||||||
* Create sprite animations
|
|
||||||
* This is done here because phaser forces us to
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
25
src/components/screens/Loading.vue
Normal file
25
src/components/screens/Loading.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col justify-center items-center h-dvh relative">
|
||||||
|
<button @click="continueBtnClick" class="w-32 h-12 rounded-full bg-gray-500 flex items-center justify-between px-4 hover:bg-gray-600 transition-colors">
|
||||||
|
<span class="text-white text-lg flex-1 text-center">Play</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" async>
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
|
function continueBtnClick() {
|
||||||
|
// Play music
|
||||||
|
const audio = new Audio('/assets/music/login.mp3')
|
||||||
|
audio.play()
|
||||||
|
|
||||||
|
// Set isLoaded to true
|
||||||
|
gameStore.game.isLoaded = true
|
||||||
|
}
|
||||||
|
</script>
|
51
src/components/screens/Login.vue
Normal file
51
src/components/screens/Login.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<!-- @TODO this must be shown over the login screen -->
|
||||||
|
<div class="relative max-lg:h-dvh flex flex-row-reverse">
|
||||||
|
<ResetPassword :isModalOpen="isPasswordResetFormShown" @close="() => (isPasswordResetFormShown = false)" />
|
||||||
|
<div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
|
||||||
|
<div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div>
|
||||||
|
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative">
|
||||||
|
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
|
||||||
|
<img src="/assets/login/sq-logo-v1.svg" class="mb-10" alt="Sylvan Quest logo" />
|
||||||
|
<div class="relative">
|
||||||
|
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" />
|
||||||
|
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" alt="UI box inner" />
|
||||||
|
|
||||||
|
<!-- Login Form -->
|
||||||
|
<LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" />
|
||||||
|
|
||||||
|
<!-- Register Form -->
|
||||||
|
<RegisterForm v-if="currentForm === 'register' && !doesUrlHaveToken" @switchToLogin="currentForm = 'login'" />
|
||||||
|
|
||||||
|
<!-- New Password Form -->
|
||||||
|
<NewPasswordForm v-if="doesUrlHaveToken" @switchToLogin="currentForm = 'login'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
|
import LoginForm from '@/components/screens/partials/login/LoginForm.vue'
|
||||||
|
import RegisterForm from '@/components/screens/partials/login/RegisterForm.vue'
|
||||||
|
import NewPasswordForm from '@/components/screens/partials/login/NewPasswordForm.vue'
|
||||||
|
import ResetPassword from '@/components/utilities/ResetPassword.vue'
|
||||||
|
|
||||||
|
const isPasswordResetFormShown = ref(false)
|
||||||
|
const doesUrlHaveToken = window.location.hash.includes('#')
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const currentForm = ref('login')
|
||||||
|
|
||||||
|
// automatic login because of development
|
||||||
|
onMounted(async () => {
|
||||||
|
const token = useCookies().get('token')
|
||||||
|
if (token) {
|
||||||
|
gameStore.setToken(token)
|
||||||
|
gameStore.initConnection()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
@ -2,7 +2,7 @@
|
|||||||
<div class="flex justify-center items-center h-dvh relative">
|
<div class="flex justify-center items-center h-dvh relative">
|
||||||
<Game :config="gameConfig" @create="createGame">
|
<Game :config="gameConfig" @create="createGame">
|
||||||
<Scene name="main" @preload="preloadScene" @create="createScene">
|
<Scene name="main" @preload="preloadScene" @create="createScene">
|
||||||
<ZoneEditor v-if="isLoaded" :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt}`)" />
|
<ZoneEditor :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt}`)" />
|
||||||
</Scene>
|
</Scene>
|
||||||
</Game>
|
</Game>
|
||||||
</div>
|
</div>
|
||||||
@ -11,22 +11,32 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import 'phaser'
|
import 'phaser'
|
||||||
import { ref, onBeforeUnmount } from 'vue'
|
|
||||||
import { Game, Scene } from 'phavuer'
|
import { Game, Scene } from 'phavuer'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
|
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
|
||||||
|
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
|
||||||
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
|
import type { AssetDataT } from '@/types'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const isLoaded = ref(false)
|
|
||||||
|
|
||||||
const gameConfig = {
|
const gameConfig = {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
||||||
resolution: 5
|
resolution: 5,
|
||||||
|
plugins: {
|
||||||
|
global: [
|
||||||
|
{
|
||||||
|
key: 'rexAwaitLoader',
|
||||||
|
plugin: AwaitLoaderPlugin,
|
||||||
|
start: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createGame = (game: Phaser.Game) => {
|
const createGame = (game: Phaser.Game) => {
|
||||||
@ -48,43 +58,6 @@ const createGame = (game: Phaser.Game) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const preloadScene = async (scene: Phaser.Scene) => {
|
const preloadScene = async (scene: Phaser.Scene) => {
|
||||||
isLoaded.value = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create loading bar
|
|
||||||
*/
|
|
||||||
const width = scene.cameras.main.width
|
|
||||||
const height = scene.cameras.main.height
|
|
||||||
|
|
||||||
const progressBox = scene.add.graphics()
|
|
||||||
const progressBar = scene.add.graphics()
|
|
||||||
progressBox.fillStyle(0x222222, 0.8)
|
|
||||||
progressBox.fillRect(width / 2 - 180, height / 2, 320, 50)
|
|
||||||
|
|
||||||
const loadingText = scene.make.text({
|
|
||||||
x: width / 2,
|
|
||||||
y: height / 2 - 50,
|
|
||||||
text: 'Loading...',
|
|
||||||
style: {
|
|
||||||
font: '20px monospace',
|
|
||||||
fill: '#ffffff'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
loadingText.setOrigin(0.5, 0.5)
|
|
||||||
|
|
||||||
scene.load.on(Phaser.Loader.Events.PROGRESS, function (value: any) {
|
|
||||||
progressBar.clear()
|
|
||||||
progressBar.fillStyle(0x368f8b, 1)
|
|
||||||
progressBar.fillRect(width / 2 - 180 + 10, height / 2 + 10, 300 * value, 30)
|
|
||||||
})
|
|
||||||
|
|
||||||
scene.load.on(Phaser.Loader.Events.COMPLETE, function () {
|
|
||||||
progressBar.destroy()
|
|
||||||
progressBox.destroy()
|
|
||||||
loadingText.destroy()
|
|
||||||
isLoaded.value = true
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the base assets into the Phaser scene
|
* Load the base assets into the Phaser scene
|
||||||
*/
|
*/
|
||||||
@ -92,26 +65,21 @@ const preloadScene = async (scene: Phaser.Scene) => {
|
|||||||
scene.load.image('TELEPORT', '/assets/zone/tp_tile.png')
|
scene.load.image('TELEPORT', '/assets/zone/tp_tile.png')
|
||||||
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
||||||
scene.load.image('waypoint', '/assets/waypoint.png')
|
scene.load.image('waypoint', '/assets/waypoint.png')
|
||||||
}
|
|
||||||
|
|
||||||
const createScene = async (scene: Phaser.Scene) => {
|
|
||||||
/**
|
/**
|
||||||
* Create sprite animations
|
* Because Phaser can't load tiles after the scene with map in it is created,
|
||||||
* This is done here because phaser forces us to
|
* we need to load and cache all the tiles first.
|
||||||
|
* Then load them into the scene.
|
||||||
*/
|
*/
|
||||||
gameStore.assets.forEach((asset) => {
|
scene.load.rexAwait(async function (successCallback: any) {
|
||||||
if (asset.group !== 'sprite_animations') return
|
const tiles: AssetDataT[] = await fetch(config.server_endpoint + '/assets/list_tiles').then((response) => response.json())
|
||||||
|
for await (const tile of tiles) {
|
||||||
|
await loadTexture(scene, tile)
|
||||||
|
}
|
||||||
|
|
||||||
scene.anims.create({
|
successCallback()
|
||||||
key: asset.key,
|
|
||||||
frameRate: 7,
|
|
||||||
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }),
|
|
||||||
repeat: -1
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
const createScene = async (scene: Phaser.Scene) => {}
|
||||||
isLoaded.value = false
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
69
src/components/screens/partials/login/LoginForm.vue
Normal file
69
src/components/screens/partials/login/LoginForm.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="loginFunc" class="relative px-6 py-11">
|
||||||
|
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
||||||
|
<div class="w-full grid gap-3 relative">
|
||||||
|
<input class="input-field xs:min-w-[350px] min-w-64" id="username-login" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
||||||
|
<div class="relative">
|
||||||
|
<input class="input-field xs:min-w-[350px] min-w-64" id="password-login" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
|
||||||
|
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
|
||||||
|
</div>
|
||||||
|
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
|
||||||
|
</div>
|
||||||
|
<button @click.stop="() => emit('openResetPasswordModal')" type="button" class="inline-flex self-end p-0 text-cyan-300 text-base">Forgot password?</button>
|
||||||
|
<button class="btn-cyan px-0 xs:w-full" type="submit">Play now</button>
|
||||||
|
|
||||||
|
<!-- Divider shape -->
|
||||||
|
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
|
||||||
|
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||||
|
<div class="w-36 h-full bg-gray-300"></div>
|
||||||
|
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-8">
|
||||||
|
<p class="m-0 text-center">Don't have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="() => emit('switchToRegister')">Sign up</button></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { login } from '@/services/authentication'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
|
|
||||||
|
const emit = defineEmits(['openResetPasswordModal', 'switchToRegister'])
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const username = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const loginError = ref('')
|
||||||
|
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 loginFunc() {
|
||||||
|
// check if username and password are valid
|
||||||
|
if (username.value === '' || password.value === '') {
|
||||||
|
loginError.value = 'Please enter a valid username and password'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// send login event to server
|
||||||
|
const response = await login(username.value, password.value)
|
||||||
|
|
||||||
|
if (response.success === undefined) {
|
||||||
|
loginError.value = response.error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gameStore.setToken(response.token)
|
||||||
|
gameStore.initConnection()
|
||||||
|
return true // Indicate success
|
||||||
|
}
|
||||||
|
</script>
|
68
src/components/screens/partials/login/NewPasswordForm.vue
Normal file
68
src/components/screens/partials/login/NewPasswordForm.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="newPasswordFunc" class="relative px-6 py-11">
|
||||||
|
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
||||||
|
<div class="w-full grid gap-3 relative">
|
||||||
|
<input class="input-field xs:min-w-[350px] min-w-64" id="password-register" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
|
||||||
|
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
|
||||||
|
<span v-if="newPasswordError" class="text-red-200 text-xs absolute top-full mt-1">{{ newPasswordError }}</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn-cyan xs:w-full" type="submit">Change password</button>
|
||||||
|
|
||||||
|
<!-- Divider shape -->
|
||||||
|
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
|
||||||
|
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||||
|
<div class="w-36 h-full bg-gray-300"></div>
|
||||||
|
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-8">
|
||||||
|
<p class="m-0 text-center"><button class="text-cyan-300 text-base p-0" @click.prevent="cancelNewPassword">Back to login</button></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { newPassword } from '@/services/authentication'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
|
|
||||||
|
const emit = defineEmits(['switchToLogin'])
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const password = ref('')
|
||||||
|
const newPasswordError = ref('')
|
||||||
|
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() {
|
||||||
|
// check if username and password are valid
|
||||||
|
if (password.value === '') {
|
||||||
|
newPasswordError.value = 'Please enter a password'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlToken = window.location.hash.split('#')[1]
|
||||||
|
|
||||||
|
// send new password event to server along with the token
|
||||||
|
const response = await newPassword(urlToken, password.value)
|
||||||
|
|
||||||
|
if (response.success === undefined) {
|
||||||
|
newPasswordError.value = response.error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelNewPassword() {
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
</script>
|
97
src/components/screens/partials/login/RegisterForm.vue
Normal file
97
src/components/screens/partials/login/RegisterForm.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="registerFunc" class="relative px-6 py-11">
|
||||||
|
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
||||||
|
<div class="w-full grid gap-3 relative">
|
||||||
|
<input class="input-field xs:min-w-[350px] min-w-64" id="username-register" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
||||||
|
<input class="input-field xs:min-w-[350px] min-w-64" id="email-register" v-model="email" type="email" name="email" placeholder="Email" required />
|
||||||
|
<div class="relative">
|
||||||
|
<input class="input-field xs:min-w-[350px] min-w-64" id="password-register" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
|
||||||
|
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
|
||||||
|
</div>
|
||||||
|
<span v-if="loginError" class="text-red-200 text-xs -mt-2">{{ loginError }}</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn-cyan xs:w-full" type="submit">Register now</button>
|
||||||
|
|
||||||
|
<!-- Divider shape -->
|
||||||
|
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
|
||||||
|
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||||
|
<div class="w-36 h-full bg-gray-300"></div>
|
||||||
|
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-8">
|
||||||
|
<p class="m-0 text-center">Already have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="() => emit('switchToLogin')">Log in</button></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { login, register } from '@/services/authentication'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
|
|
||||||
|
const emit = defineEmits(['switchToLogin'])
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const username = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const email = ref('')
|
||||||
|
const loginError = ref('')
|
||||||
|
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 loginFunc() {
|
||||||
|
// check if username and password are valid
|
||||||
|
if (username.value === '' || password.value === '') {
|
||||||
|
loginError.value = 'Please enter a valid username and password'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// send login event to server
|
||||||
|
const response = await login(username.value, password.value)
|
||||||
|
|
||||||
|
if (response.success === undefined) {
|
||||||
|
loginError.value = response.error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gameStore.setToken(response.token)
|
||||||
|
gameStore.initConnection()
|
||||||
|
return true // Indicate success
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerFunc() {
|
||||||
|
// check if username and password are valid
|
||||||
|
if (username.value === '' || email.value === '' || password.value === '') {
|
||||||
|
loginError.value = 'Please enter a valid username, email, and password'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email.value === '') {
|
||||||
|
loginError.value = 'Please enter an email'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// send register event to server
|
||||||
|
const response = await register(username.value, email.value, password.value)
|
||||||
|
|
||||||
|
if (response.success === undefined) {
|
||||||
|
loginError.value = response.error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginSuccess = await loginFunc()
|
||||||
|
if (!loginSuccess) {
|
||||||
|
loginError.value = 'Login after registration failed. Please try logging in manually.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -18,12 +18,13 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { type ExtendedCharacter } from '@/types'
|
import { type ExtendedCharacter, type Sprite as SpriteT } from '@/types'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
|
import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { Container, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
|
import { Container, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
|
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||||
|
|
||||||
enum Direction {
|
enum Direction {
|
||||||
POSITIVE,
|
POSITIVE,
|
||||||
@ -181,6 +182,15 @@ watch(
|
|||||||
watch(() => props.character.isMoving, updateSprite)
|
watch(() => props.character.isMoving, updateSprite)
|
||||||
watch(() => props.character.rotation, updateSprite)
|
watch(() => props.character.rotation, updateSprite)
|
||||||
|
|
||||||
|
loadSpriteTextures(scene, props.character.characterType?.sprite as SpriteT)
|
||||||
|
.then(() => {
|
||||||
|
charSprite.value!.setTexture(charTexture.value)
|
||||||
|
charSprite.value!.setFlipX(isFlippedX.value)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
charChatContainer.value!.setName(`${props.character!.name}_chatContainer`)
|
charChatContainer.value!.setName(`${props.character!.name}_chatContainer`)
|
||||||
charChatContainer.value!.setVisible(false)
|
charChatContainer.value!.setVisible(false)
|
||||||
@ -194,10 +204,6 @@ onMounted(() => {
|
|||||||
scene.cameras.main.stopFollow()
|
scene.cameras.main.stopFollow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set sprite
|
|
||||||
charSprite.value!.setTexture(charTexture.value)
|
|
||||||
charSprite.value!.setFlipX(isFlippedX.value)
|
|
||||||
|
|
||||||
updatePosition(props.character.positionX, props.character.positionY, props.character.rotation)
|
updatePosition(props.character.positionX, props.character.positionY, props.character.rotation)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,22 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div v-if="isModalOpenRef" class="fixed border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
|
<div v-if="isModalOpenRef" class="fixed border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
|
||||||
|
<!-- Header -->
|
||||||
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500 relative">
|
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500 relative">
|
||||||
<div class="rounded-t-md absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-center bg-cover opacity-90"></div>
|
<div class="rounded-t absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-center bg-cover opacity-90" />
|
||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<slot name="modalHeader" />
|
<slot name="modalHeader" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2.5">
|
<div class="flex gap-2.5">
|
||||||
<button @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out" v-if="canFullScreen">
|
<button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out">
|
||||||
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" draggable="false" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/increase-size-option.svg'" class="w-3.5 h-3.5 invert" />
|
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" />
|
||||||
</button>
|
</button>
|
||||||
<button @click="close" v-if="closable" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
<button v-if="closable" @click="emit('modal:close')" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
||||||
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
|
<img alt="close" src="/assets/icons/close-button-white.svg" class="w-full h-full" draggable="false" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
<div class="overflow-hidden grow relative">
|
<div class="overflow-hidden grow relative">
|
||||||
<div class="rounded-b-md absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center opacity-90"></div>
|
<div class="rounded-b absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center opacity-90" />
|
||||||
<div class="relative z-10 h-full">
|
<div class="relative z-10 h-full">
|
||||||
<slot name="modalBody" />
|
<slot name="modalBody" />
|
||||||
</div>
|
</div>
|
||||||
@ -27,219 +30,187 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineEmits, onMounted, onUnmounted, ref, watch, computed } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
interface ModalProps {
|
||||||
isModalOpen: {
|
isModalOpen: boolean
|
||||||
type: Boolean,
|
closable?: boolean
|
||||||
default: false
|
isResizable?: boolean
|
||||||
},
|
canFullScreen?: boolean
|
||||||
closable: {
|
modalPositionX?: number
|
||||||
type: Boolean,
|
modalPositionY?: number
|
||||||
default: true
|
modalWidth?: number
|
||||||
},
|
modalHeight?: number
|
||||||
isResizable: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
canFullScreen: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
modalPositionX: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
modalPositionY: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
modalWidth: {
|
|
||||||
type: Number,
|
|
||||||
default: 500
|
|
||||||
},
|
|
||||||
modalHeight: {
|
|
||||||
type: Number,
|
|
||||||
default: 280
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ModalProps>(), {
|
||||||
|
isModalOpen: false,
|
||||||
|
closable: true,
|
||||||
|
isResizable: true,
|
||||||
|
canFullScreen: false,
|
||||||
|
modalPositionX: 0,
|
||||||
|
modalPositionY: 0,
|
||||||
|
modalWidth: 500,
|
||||||
|
modalHeight: 280
|
||||||
})
|
})
|
||||||
|
|
||||||
const isModalOpenRef = ref(props.isModalOpen)
|
const emit = defineEmits<{
|
||||||
const emit = defineEmits(['modal:close', 'character:create'])
|
'modal:close': []
|
||||||
|
'character:create': []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isModalOpenRef = ref(props.isModalOpen)
|
||||||
const width = ref(props.modalWidth)
|
const width = ref(props.modalWidth)
|
||||||
const height = ref(props.modalHeight)
|
const height = ref(props.modalHeight)
|
||||||
const x = ref(0)
|
const x = ref(0)
|
||||||
const y = ref(0)
|
const y = ref(0)
|
||||||
|
|
||||||
const minWidth = ref(200)
|
|
||||||
const minHeight = ref(100)
|
|
||||||
const isResizing = ref(false)
|
const isResizing = ref(false)
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const isFullScreen = ref(false)
|
const isFullScreen = ref(false)
|
||||||
|
|
||||||
let startX = 0
|
const minDimensions = {
|
||||||
let startY = 0
|
width: 200,
|
||||||
let initialX = 0
|
height: 100
|
||||||
let initialY = 0
|
}
|
||||||
let startWidth = 0
|
|
||||||
let startHeight = 0
|
let dragState = {
|
||||||
let preFullScreenState = { x: 0, y: 0, width: 0, height: 0 }
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
initialX: 0,
|
||||||
|
initialY: 0,
|
||||||
|
startWidth: 0,
|
||||||
|
startHeight: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let preFullScreenState: Position = { x: 0, y: 0, width: 0, height: 0 }
|
||||||
|
|
||||||
const modalStyle = computed(() => ({
|
const modalStyle = computed(() => ({
|
||||||
borderRadius: isFullScreen.value ? '0' : '6px',
|
borderRadius: isFullScreen.value ? '0' : '6px',
|
||||||
top: isFullScreen.value ? '0' : `${y.value}px`,
|
top: isFullScreen.value ? '0' : `${y.value}px`,
|
||||||
left: isFullScreen.value ? '0' : `${x.value}px`,
|
left: isFullScreen.value ? '0' : `${x.value}px`,
|
||||||
width: isFullScreen.value ? '100vw' : `${width.value}px`,
|
width: isFullScreen.value ? '100vw' : `${width.value}px`,
|
||||||
height: isFullScreen.value ? '100vh' : `${height.value}px`,
|
height: isFullScreen.value ? '100vh' : `${height.value}px`
|
||||||
maxWidth: '100vw',
|
|
||||||
maxHeight: '100vh'
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function close() {
|
|
||||||
emit('modal:close')
|
|
||||||
}
|
|
||||||
|
|
||||||
function startResize(event: MouseEvent) {
|
function startResize(event: MouseEvent) {
|
||||||
if (isFullScreen.value) return
|
if (isFullScreen.value) return
|
||||||
isResizing.value = true
|
isResizing.value = true
|
||||||
startWidth = width.value - event.clientX
|
dragState.startWidth = width.value - event.clientX
|
||||||
startHeight = height.value - event.clientY
|
dragState.startHeight = height.value - event.clientY
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeModal(event: MouseEvent) {
|
function resizeModal(event: MouseEvent) {
|
||||||
if (!isResizing.value || isFullScreen.value) return
|
if (!isResizing.value || isFullScreen.value) return
|
||||||
const newWidth = Math.min(startWidth + event.clientX, window.innerWidth)
|
width.value = Math.max(dragState.startWidth + event.clientX, minDimensions.width)
|
||||||
const newHeight = Math.min(startHeight + event.clientY, window.innerHeight)
|
height.value = Math.max(dragState.startHeight + event.clientY, minDimensions.height)
|
||||||
width.value = Math.max(newWidth, minWidth.value)
|
|
||||||
height.value = Math.max(newHeight, minHeight.value)
|
|
||||||
adjustPosition()
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopResize() {
|
|
||||||
isResizing.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startDrag(event: MouseEvent) {
|
function startDrag(event: MouseEvent) {
|
||||||
if (isFullScreen.value) return
|
if (isFullScreen.value) return
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
startX = event.clientX
|
dragState = {
|
||||||
startY = event.clientY
|
startX: event.clientX,
|
||||||
initialX = x.value
|
startY: event.clientY,
|
||||||
initialY = y.value
|
initialX: x.value,
|
||||||
|
initialY: y.value,
|
||||||
|
startWidth: width.value,
|
||||||
|
startHeight: height.value
|
||||||
|
}
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
function drag(event: MouseEvent) {
|
function drag(event: MouseEvent) {
|
||||||
if (!isDragging.value || isFullScreen.value) return
|
if (!isDragging.value || isFullScreen.value) return
|
||||||
const dx = event.clientX - startX
|
x.value = dragState.initialX + (event.clientX - dragState.startX)
|
||||||
const dy = event.clientY - startY
|
y.value = dragState.initialY + (event.clientY - dragState.startY)
|
||||||
x.value = initialX + dx
|
|
||||||
y.value = initialY + dy
|
|
||||||
adjustPosition()
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopDrag() {
|
|
||||||
isDragging.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function adjustPosition() {
|
|
||||||
if (isFullScreen.value) return
|
|
||||||
x.value = Math.max(0, Math.min(x.value, window.innerWidth - width.value))
|
|
||||||
y.value = Math.max(0, Math.min(y.value, window.innerHeight - height.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleResize() {
|
|
||||||
if (isFullScreen.value) return
|
|
||||||
width.value = Math.min(width.value, window.innerWidth)
|
|
||||||
height.value = Math.min(height.value, window.innerHeight)
|
|
||||||
adjustPosition()
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializePosition() {
|
|
||||||
width.value = Math.min(props.modalWidth, window.innerWidth)
|
|
||||||
height.value = Math.min(props.modalHeight, window.innerHeight)
|
|
||||||
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
|
|
||||||
x.value = props.modalPositionX
|
|
||||||
y.value = props.modalPositionY
|
|
||||||
} else {
|
|
||||||
x.value = (window.innerWidth - width.value) / 2
|
|
||||||
y.value = (window.innerHeight - height.value) / 2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFullScreen() {
|
function toggleFullScreen() {
|
||||||
if (isFullScreen.value) {
|
if (isFullScreen.value) {
|
||||||
// Exit full-screen
|
Object.assign({ x, y, width, height }, preFullScreenState)
|
||||||
x.value = preFullScreenState.x
|
|
||||||
y.value = preFullScreenState.y
|
|
||||||
width.value = preFullScreenState.width
|
|
||||||
height.value = preFullScreenState.height
|
|
||||||
isFullScreen.value = false
|
|
||||||
} else {
|
} else {
|
||||||
// Enter full-screen
|
|
||||||
preFullScreenState = { x: x.value, y: y.value, width: width.value, height: height.value }
|
preFullScreenState = { x: x.value, y: y.value, width: width.value, height: height.value }
|
||||||
isFullScreen.value = true
|
|
||||||
}
|
}
|
||||||
|
isFullScreen.value = !isFullScreen.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initializePosition() {
|
||||||
|
width.value = props.modalWidth
|
||||||
|
height.value = props.modalHeight
|
||||||
|
x.value = props.modalPositionX || (window.innerWidth - width.value) / 2
|
||||||
|
y.value = props.modalPositionY || (window.innerHeight - height.value) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watchers
|
||||||
watch(
|
watch(
|
||||||
() => props.isModalOpen,
|
() => props.isModalOpen,
|
||||||
(value) => {
|
(value) => {
|
||||||
isModalOpenRef.value = value
|
isModalOpenRef.value = value
|
||||||
if (value) {
|
if (value) initializePosition()
|
||||||
initializePosition()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modalWidth,
|
() => props.modalWidth,
|
||||||
(value) => {
|
(value) => (width.value = value)
|
||||||
width.value = Math.min(value, window.innerWidth)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modalHeight,
|
() => props.modalHeight,
|
||||||
(value) => {
|
(value) => (height.value = value)
|
||||||
height.value = Math.min(value, window.innerHeight)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modalPositionX,
|
() => props.modalPositionX,
|
||||||
(value) => {
|
(value) => (x.value = value)
|
||||||
x.value = value
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modalPositionY,
|
() => props.modalPositionY,
|
||||||
(value) => {
|
(value) => (y.value = value)
|
||||||
y.value = value
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Lifecycle hooks
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('mousemove', drag)
|
const handlers: Record<string, EventListener[]> = {
|
||||||
window.addEventListener('mouseup', stopDrag)
|
mousemove: [(e: Event) => drag(e as MouseEvent), (e: Event) => resizeModal(e as MouseEvent)],
|
||||||
window.addEventListener('mousemove', resizeModal)
|
mouseup: [
|
||||||
window.addEventListener('mouseup', stopResize)
|
() => {
|
||||||
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
|
isDragging.value = false
|
||||||
window.addEventListener('resize', handleResize)
|
},
|
||||||
|
() => {
|
||||||
|
isResizing.value = false
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(handlers).forEach(([event, fns]) => {
|
||||||
|
fns.forEach((fn) => window.addEventListener(event, fn))
|
||||||
|
})
|
||||||
|
|
||||||
initializePosition()
|
initializePosition()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('mousemove', drag)
|
const handlers: Record<string, EventListener[]> = {
|
||||||
window.removeEventListener('mouseup', stopDrag)
|
mousemove: [(e: Event) => drag(e as MouseEvent), (e: Event) => resizeModal(e as MouseEvent)],
|
||||||
window.removeEventListener('mousemove', resizeModal)
|
mouseup: [
|
||||||
window.removeEventListener('mouseup', stopResize)
|
() => {
|
||||||
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
|
isDragging.value = false
|
||||||
window.removeEventListener('resize', handleResize)
|
},
|
||||||
|
() => {
|
||||||
|
isResizing.value = false
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(handlers).forEach(([event, fns]) => {
|
||||||
|
fns.forEach((fn) => window.removeEventListener(event, fn))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { onBeforeMount, onBeforeUnmount, watch } from 'vue'
|
import { onBeforeMount, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ function setupNotificationListener(connection: any) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onMounted(() => {
|
||||||
const connection = gameStore.connection
|
const connection = gameStore.connection
|
||||||
if (connection) {
|
if (connection) {
|
||||||
setupNotificationListener(connection)
|
setupNotificationListener(connection)
|
||||||
@ -49,7 +49,7 @@ onBeforeMount(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onUnmounted(() => {
|
||||||
const connection = gameStore.connection
|
const connection = gameStore.connection
|
||||||
if (connection) {
|
if (connection) {
|
||||||
connection.off('notification')
|
connection.off('notification')
|
||||||
|
55
src/components/utilities/ResetPassword.vue
Normal file
55
src/components/utilities/ResetPassword.vue
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<Modal @modal:close="() => emit('close')" :modal-width="400" :modal-height="300" :is-resizable="false">
|
||||||
|
<template #modalHeader>
|
||||||
|
<h3 class="m-0 font-medium shrink-0 text-white">Reset Password</h3>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #modalBody>
|
||||||
|
<div class="h-[calc(100%_-_32px)] p-4">
|
||||||
|
<form class="h-full flex flex-col justify-between" @submit.prevent="resetPasswordFunc">
|
||||||
|
<div class="flex flex-col relative">
|
||||||
|
<p>Fill in your email to receive a password reset request.</p>
|
||||||
|
<input type="email" name="email" class="input-field" v-model="email" placeholder="E-mail" />
|
||||||
|
<span v-if="resetPasswordError" class="text-red-200 text-xs absolute top-full mt-1">{{ resetPasswordError }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-flow-col justify-stretch gap-4">
|
||||||
|
<button class="btn-empty py-1.5 px-4 min-w-24 inline-block" @click.stop="() => emit('close')">Cancel</button>
|
||||||
|
<button class="btn-cyan py-1.5 px-4 min-w-24 inline-block" type="submit">Send mail</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { resetPassword } from '@/services/authentication'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const email = ref('')
|
||||||
|
const resetPasswordError = ref('')
|
||||||
|
const isPasswordResetOpen = ref(false)
|
||||||
|
|
||||||
|
async function resetPasswordFunc() {
|
||||||
|
// check if email is valid
|
||||||
|
if (email.value === '') {
|
||||||
|
resetPasswordError.value = 'Please enter an email'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// send reset password event to server
|
||||||
|
const response = await resetPassword(email.value)
|
||||||
|
|
||||||
|
if (response.success === undefined) {
|
||||||
|
resetPasswordError.value = response.error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,18 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tilemap:create="tileMap = $event" />
|
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tileMap:create="tileMap = $event" />
|
||||||
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onUnmounted } from 'vue'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
import { onBeforeUnmount, ref, onBeforeMount } from 'vue'
|
import { loadZoneTilesIntoScene } from '@/composables/zoneComposable'
|
||||||
import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types'
|
import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types'
|
||||||
import ZoneTiles from '@/components/zone/ZoneTiles.vue'
|
import ZoneTiles from '@/components/zone/ZoneTiles.vue'
|
||||||
import ZoneObjects from '@/components/zone/ZoneObjects.vue'
|
import ZoneObjects from '@/components/zone/ZoneObjects.vue'
|
||||||
import Characters from '@/components/zone/Characters.vue'
|
import Characters from '@/components/zone/Characters.vue'
|
||||||
|
|
||||||
|
const scene = useScene()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneStore = useZoneStore()
|
const zoneStore = useZoneStore()
|
||||||
|
|
||||||
@ -33,6 +36,7 @@ gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) =
|
|||||||
zoneId: data.zone.id
|
zoneId: data.zone.id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await loadZoneTilesIntoScene(data.zone, scene)
|
||||||
zoneStore.setZone(data.zone)
|
zoneStore.setZone(data.zone)
|
||||||
zoneStore.setCharacters(data.characters)
|
zoneStore.setCharacters(data.characters)
|
||||||
})
|
})
|
||||||
@ -51,15 +55,14 @@ gameStore.connection!.on('character:move', (data: ExtendedCharacterT) => {
|
|||||||
zoneStore.updateCharacter(data)
|
zoneStore.updateCharacter(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
// Emit zone:character:join event to server and wait for response, then set zone and characters
|
||||||
gameStore!.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
|
gameStore!.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
|
||||||
// Set zone and characters
|
await loadZoneTilesIntoScene(response.zone, scene)
|
||||||
zoneStore.setZone(response.zone)
|
zoneStore.setZone(response.zone)
|
||||||
zoneStore.setCharacters(response.characters)
|
zoneStore.setCharacters(response.characters)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onUnmounted(() => {
|
||||||
zoneStore.reset()
|
zoneStore.reset()
|
||||||
gameStore.connection!.off('zone:character:teleport')
|
gameStore.connection!.off('zone:character:teleport')
|
||||||
gameStore.connection!.off('zone:character:join')
|
gameStore.connection!.off('zone:character:join')
|
||||||
|
@ -1,23 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<Controls :layer="tiles" :depth="0" />
|
<Controls :layer="tileLayer" :depth="0" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
import { onBeforeMount, onBeforeUnmount } from 'vue'
|
import { onBeforeUnmount } from 'vue'
|
||||||
import { setLayerTiles } from '@/composables/zoneComposable'
|
import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
|
import { unduplicateArray } from '@/utilities'
|
||||||
|
|
||||||
const emit = defineEmits(['tilemap:create'])
|
const emit = defineEmits(['tileMap:create'])
|
||||||
|
|
||||||
const zoneStore = useZoneStore()
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const zoneTilemap = createTilemap()
|
const zoneStore = useZoneStore()
|
||||||
const tiles = createTileLayer()
|
const tileMap = createTileMap()
|
||||||
|
const tileLayer = createTileLayer()
|
||||||
|
|
||||||
function createTilemap() {
|
/**
|
||||||
|
* A Tilemap is a container for Tilemap data.
|
||||||
|
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
|
||||||
|
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
|
||||||
|
*/
|
||||||
|
function createTileMap() {
|
||||||
const zoneData = new Phaser.Tilemaps.MapData({
|
const zoneData = new Phaser.Tilemaps.MapData({
|
||||||
width: zoneStore.zone?.width,
|
width: zoneStore.zone?.width,
|
||||||
height: zoneStore.zone?.height,
|
height: zoneStore.zone?.height,
|
||||||
@ -26,22 +32,26 @@ function createTilemap() {
|
|||||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
||||||
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
||||||
})
|
})
|
||||||
const tilemap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
|
||||||
emit('tilemap:create', tilemap)
|
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
||||||
return tilemap
|
emit('tileMap:create', newTileMap)
|
||||||
|
|
||||||
|
return newTileMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
|
||||||
|
*/
|
||||||
function createTileLayer() {
|
function createTileLayer() {
|
||||||
const tilesFromZone = zoneStore.zone?.tiles || []
|
const tilesArray = unduplicateArray(FlattenZoneArray(zoneStore.zone?.tiles ?? []))
|
||||||
const uniqueTiles = new Set(tilesFromZone.flat().filter(Boolean))
|
|
||||||
|
|
||||||
const tilesetImages = Array.from(uniqueTiles).map((tile, index) => {
|
const tilesetImages = Array.from(tilesArray).map((tile: any, index: number) => {
|
||||||
return zoneTilemap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
|
return tileMap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
|
||||||
}) as any
|
}) as any
|
||||||
|
|
||||||
// Add blank tile
|
// Add blank tile
|
||||||
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
||||||
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
layer.setDepth(0)
|
layer.setDepth(0)
|
||||||
layer.setCullPadding(2, 2)
|
layer.setCullPadding(2, 2)
|
||||||
@ -49,16 +59,11 @@ function createTileLayer() {
|
|||||||
return layer
|
return layer
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
setLayerTiles(tileMap, tileLayer, zoneStore.zone?.tiles)
|
||||||
if (!zoneStore.zone?.tiles) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setLayerTiles(zoneTilemap, tiles, zoneStore.zone.tiles)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
zoneTilemap.destroyLayer('tiles')
|
tileMap.destroyLayer('tiles')
|
||||||
zoneTilemap.removeAllLayers()
|
tileMap.removeAllLayers()
|
||||||
zoneTilemap.destroy()
|
tileMap.destroy()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<Image v-if="isTextureLoaded" v-bind="imageProps" />
|
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { Image, useScene } from 'phavuer'
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
import { useAssetManager } from '@/utilities/assetManager'
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
import type { ZoneObject } from '@/types'
|
import type { AssetDataT, ZoneObject } from '@/types'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
zoneObject: ZoneObject
|
zoneObject: ZoneObject
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const assetManager = useAssetManager
|
|
||||||
const isTextureLoaded = ref(false)
|
|
||||||
|
|
||||||
const imageProps = computed(() => ({
|
const imageProps = computed(() => ({
|
||||||
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
|
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
|
||||||
@ -28,36 +28,14 @@ const imageProps = computed(() => ({
|
|||||||
originX: Number(props.zoneObject.object.originY)
|
originX: Number(props.zoneObject.object.originY)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const loadTexture = async () => {
|
loadTexture(scene, {
|
||||||
const textureId = props.zoneObject.object.id
|
key: props.zoneObject.object.id,
|
||||||
|
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
|
||||||
// Check if the texture is already loaded in Phaser
|
group: 'objects',
|
||||||
if (scene.textures.exists(textureId)) {
|
updatedAt: props.zoneObject.object.updatedAt,
|
||||||
isTextureLoaded.value = true
|
frameWidth: props.zoneObject.object.frameWidth,
|
||||||
return
|
frameHeight: props.zoneObject.object.frameHeight
|
||||||
}
|
} as AssetDataT).catch((error) => {
|
||||||
|
|
||||||
let assetData = await assetManager.getAsset(textureId)
|
|
||||||
|
|
||||||
if (!assetData) {
|
|
||||||
await assetManager.downloadAsset(textureId, `/assets/objects/${textureId}.png`, 'objects', props.zoneObject.object.updatedAt)
|
|
||||||
assetData = await assetManager.getAsset(textureId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assetData) {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
scene.textures.addBase64(textureId, assetData.data)
|
|
||||||
scene.textures.once(`addtexture-${textureId}`, () => {
|
|
||||||
isTextureLoaded.value = true
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadTexture().catch((error) => {
|
|
||||||
console.error('Error loading texture:', error)
|
console.error('Error loading texture:', error)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
34
src/composables/game/scene/loaderComposable.ts
Normal file
34
src/composables/game/scene/loaderComposable.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export function createSceneLoader(scene: Phaser.Scene) {
|
||||||
|
const width = scene.cameras.main.width
|
||||||
|
const height = scene.cameras.main.height
|
||||||
|
|
||||||
|
const progressBox = scene.add.graphics()
|
||||||
|
const progressBar = scene.add.graphics()
|
||||||
|
progressBox.fillStyle(0x222222, 0.8)
|
||||||
|
progressBox.fillRect(width / 2 - 180, height / 2, 320, 50)
|
||||||
|
|
||||||
|
const loadingText = scene.make.text({
|
||||||
|
x: width / 2,
|
||||||
|
y: height / 2 - 50,
|
||||||
|
text: 'Loading...',
|
||||||
|
style: {
|
||||||
|
font: '20px monospace',
|
||||||
|
// @ts-ignore
|
||||||
|
fill: '#ffffff'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
loadingText.setOrigin(0.5, 0.5)
|
||||||
|
|
||||||
|
scene.load.on(Phaser.Loader.Events.PROGRESS, function (value: any) {
|
||||||
|
progressBar.clear()
|
||||||
|
progressBar.fillStyle(0x368f8b, 1)
|
||||||
|
progressBar.fillRect(width / 2 - 180 + 10, height / 2 + 10, 300 * value, 30)
|
||||||
|
})
|
||||||
|
|
||||||
|
scene.load.on(Phaser.Loader.Events.COMPLETE, function () {
|
||||||
|
progressBar.destroy()
|
||||||
|
progressBox.destroy()
|
||||||
|
loadingText.destroy()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
import type { AssetDataT, Sprite } from '@/types'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { AssetStorage } from '@/storage/assetStorage'
|
||||||
|
import config from '@/config'
|
||||||
|
|
||||||
|
const textureLoadingPromises = new Map<string, Promise<boolean>>()
|
||||||
|
|
||||||
|
export async function loadTexture(scene: Phaser.Scene, assetData: AssetDataT): Promise<boolean> {
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const assetStorage = new AssetStorage()
|
||||||
|
|
||||||
|
// Check if the texture is already loaded in Phaser
|
||||||
|
if (gameStore.game.loadedAssets.find((asset) => asset.key === assetData.key)) {
|
||||||
|
return Promise.resolve(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's already a loading promise for this texture, return it
|
||||||
|
if (textureLoadingPromises.has(assetData.key)) {
|
||||||
|
return await textureLoadingPromises.get(assetData.key)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new loading promise
|
||||||
|
const loadingPromise = (async () => {
|
||||||
|
// Check if the asset is already cached
|
||||||
|
let asset = await assetStorage.get(assetData.key)
|
||||||
|
|
||||||
|
// If asset is not found, download it
|
||||||
|
if (!asset) {
|
||||||
|
await assetStorage.download(assetData)
|
||||||
|
asset = await assetStorage.get(assetData.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If asset is found, add it to the scene
|
||||||
|
if (asset) {
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
// Remove existing texture if it exists
|
||||||
|
if (scene.textures.exists(asset.key)) {
|
||||||
|
scene.textures.remove(asset.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.textures.addBase64(asset.key, asset.data)
|
||||||
|
scene.textures.once(`addtexture-${asset.key}`, () => {
|
||||||
|
gameStore.game.loadedAssets.push(assetData)
|
||||||
|
textureLoadingPromises.delete(assetData.key) // Clean up the promise
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
textureLoadingPromises.delete(assetData.key) // Clean up the promise
|
||||||
|
return Promise.resolve(false)
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Store the loading promise
|
||||||
|
textureLoadingPromises.set(assetData.key, loadingPromise)
|
||||||
|
return loadingPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadSpriteTextures(scene: Phaser.Scene, sprite: Sprite) {
|
||||||
|
const sprite_actions = await fetch(config.server_endpoint + '/assets/list_sprite_actions/' + sprite?.id).then((response) => response.json())
|
||||||
|
for await (const sprite_action of sprite_actions) {
|
||||||
|
await loadTexture(scene, {
|
||||||
|
key: sprite_action.key,
|
||||||
|
data: sprite_action.data,
|
||||||
|
group: sprite_action.isAnimated ? 'sprite_animations' : 'sprites',
|
||||||
|
updatedAt: sprite_action.updatedAt,
|
||||||
|
frameCount: sprite_action.frameCount,
|
||||||
|
frameWidth: sprite_action.frameWidth,
|
||||||
|
frameHeight: sprite_action.frameHeight
|
||||||
|
} as AssetDataT)
|
||||||
|
|
||||||
|
// If the sprite is not animated, skip
|
||||||
|
if (!sprite_action.isAnimated) continue
|
||||||
|
|
||||||
|
// Add the animation to the scene
|
||||||
|
const anim = scene.textures.get(sprite_action.key)
|
||||||
|
scene.textures.addSpriteSheet(sprite_action.key, anim, { frameWidth: sprite_action.frameWidth ?? 0, frameHeight: sprite_action.frameHeight ?? 0 })
|
||||||
|
scene.anims.create({
|
||||||
|
key: sprite_action.key,
|
||||||
|
frameRate: 7,
|
||||||
|
frames: scene.anims.generateFrameNumbers(sprite_action.key, { start: 0, end: sprite_action.frameCount! - 1 }),
|
||||||
|
repeat: -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return Promise.resolve(true)
|
||||||
|
}
|
||||||
|
@ -31,7 +31,7 @@ export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilema
|
|||||||
const { worldX, worldY } = pointer
|
const { worldX, worldY } = pointer
|
||||||
updateWaypoint(worldX, worldY)
|
updateWaypoint(worldX, worldY)
|
||||||
|
|
||||||
if (gameStore.isPlayerDraggingCamera) {
|
if (gameStore.game.isPlayerDraggingCamera) {
|
||||||
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
||||||
|
|
||||||
if (distance > dragThreshold) {
|
if (distance > dragThreshold) {
|
||||||
|
@ -26,7 +26,7 @@ export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dragZone(pointer: Phaser.Input.Pointer) {
|
function dragZone(pointer: Phaser.Input.Pointer) {
|
||||||
if (gameStore.isPlayerDraggingCamera) {
|
if (gameStore.game.isPlayerDraggingCamera) {
|
||||||
const { x, y, prevPosition } = pointer
|
const { x, y, prevPosition } = pointer
|
||||||
const { scrollX, scrollY, zoom } = camera
|
const { scrollX, scrollY, zoom } = camera
|
||||||
camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom)
|
camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom)
|
||||||
|
@ -3,6 +3,8 @@ import Tilemap = Phaser.Tilemaps.Tilemap
|
|||||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||||
import Tileset = Phaser.Tilemaps.Tileset
|
import Tileset = Phaser.Tilemaps.Tileset
|
||||||
import Tile = Phaser.Tilemaps.Tile
|
import Tile = Phaser.Tilemaps.Tile
|
||||||
|
import type { AssetDataT, Zone as ZoneT } from '@/types'
|
||||||
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
|
|
||||||
export function getTile(layer: TilemapLayer | Tilemap, x: number, y: number): Tile | undefined {
|
export function getTile(layer: TilemapLayer | Tilemap, x: number, y: number): Tile | undefined {
|
||||||
const tile = layer.getTileAtWorldXY(x, y)
|
const tile = layer.getTileAtWorldXY(x, y)
|
||||||
@ -45,12 +47,15 @@ export function tileToWorldY(layer: TilemapLayer | Tilemap, pos_x: number, pos_y
|
|||||||
export function placeTile(zone: Tilemap, layer: TilemapLayer, x: number, y: number, tileName: string) {
|
export function placeTile(zone: Tilemap, layer: TilemapLayer, x: number, y: number, tileName: string) {
|
||||||
let tileImg = zone.getTileset(tileName) as Tileset
|
let tileImg = zone.getTileset(tileName) as Tileset
|
||||||
if (!tileImg) {
|
if (!tileImg) {
|
||||||
|
console.log('tile not found:', tileName)
|
||||||
tileImg = zone.getTileset('blank_tile') as Tileset
|
tileImg = zone.getTileset('blank_tile') as Tileset
|
||||||
}
|
}
|
||||||
layer.putTileAt(tileImg.firstgid, x, y)
|
layer.putTileAt(tileImg.firstgid, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setLayerTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][]) {
|
export function setLayerTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][]) {
|
||||||
|
if (!tiles) return
|
||||||
|
|
||||||
tiles.forEach((row: string[], y: number) => {
|
tiles.forEach((row: string[], y: number) => {
|
||||||
row.forEach((tile: string, x: number) => {
|
row.forEach((tile: string, x: number) => {
|
||||||
placeTile(zone, layer, x, y, tile)
|
placeTile(zone, layer, x, y, tile)
|
||||||
@ -69,3 +74,23 @@ export const calculateIsometricDepth = (x: number, y: number, width: number = 0,
|
|||||||
}
|
}
|
||||||
return baseDepth + (width + height) / (2 * config.tile_size.x)
|
return baseDepth + (width + height) / (2 * config.tile_size.x)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function FlattenZoneArray(tiles: string[][]) {
|
||||||
|
const normalArray = []
|
||||||
|
|
||||||
|
for (const row of tiles) {
|
||||||
|
normalArray.push(...row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalArray
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadZoneTilesIntoScene(zone: ZoneT, scene: Phaser.Scene) {
|
||||||
|
// Fetch the list of tiles from the server
|
||||||
|
const tileArray: AssetDataT[] = await fetch(config.server_endpoint + '/assets/list_tiles/' + zone.id).then((response) => response.json())
|
||||||
|
|
||||||
|
// Load each tile into the scene
|
||||||
|
for (const tile of tileArray) {
|
||||||
|
await loadTexture(scene, tile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
<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/assetManager'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This component downloads all assets from the server and
|
|
||||||
* stores them in the asset manager.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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.downloadAsset(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>
|
|
@ -1,130 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="relative max-lg:h-dvh flex flex-row-reverse">
|
|
||||||
<div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
|
|
||||||
<div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div>
|
|
||||||
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative">
|
|
||||||
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
|
|
||||||
<img src="/assets/login/sq-logo-v1.svg" class="mb-10" />
|
|
||||||
<div class="relative">
|
|
||||||
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" />
|
|
||||||
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" />
|
|
||||||
|
|
||||||
<!-- Login Form -->
|
|
||||||
<form v-show="switchForm === 'login'" @submit.prevent="loginFunc" class="relative px-6 py-11">
|
|
||||||
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
|
||||||
<div class="w-full grid gap-3 relative">
|
|
||||||
<input class="input-field xs:min-w-[350px] min-w-64" id="username-login" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
|
||||||
<div class="relative">
|
|
||||||
<input class="input-field xs:min-w-[350px] min-w-64" id="password-login" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
|
|
||||||
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
|
|
||||||
</div>
|
|
||||||
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
|
|
||||||
</div>
|
|
||||||
<button class="inline-flex self-end p-0 text-cyan-300 text-base">Forgot password?</button>
|
|
||||||
<button class="btn-cyan px-0 xs:w-full" type="submit">Play now</button>
|
|
||||||
|
|
||||||
<!-- Divider shape -->
|
|
||||||
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
|
|
||||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
|
||||||
<div class="w-36 h-full bg-gray-300"></div>
|
|
||||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pt-8">
|
|
||||||
<p class="m-0 text-center">Don't have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="switchForm = 'register'">Sign up</button></p>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Register Form -->
|
|
||||||
<form v-show="switchForm === 'register'" @submit.prevent="registerFunc" class="relative px-6 py-11">
|
|
||||||
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
|
||||||
<div class="w-full grid gap-3 relative">
|
|
||||||
<input class="input-field xs:min-w-[350px] min-w-64" id="username-register" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
|
||||||
<div class="relative">
|
|
||||||
<input class="input-field xs:min-w-[350px] min-w-64" id="password-register" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
|
|
||||||
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
|
|
||||||
</div>
|
|
||||||
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
|
|
||||||
</div>
|
|
||||||
<button class="btn-cyan xs:w-full" type="submit">Register now</button>
|
|
||||||
|
|
||||||
<!-- Divider shape -->
|
|
||||||
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
|
|
||||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
|
||||||
<div class="w-36 h-full bg-gray-300"></div>
|
|
||||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pt-8">
|
|
||||||
<p class="m-0 text-center">Already have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="switchForm = 'login'">Log in</button></p>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { login, register } from '@/services/authentication'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const username = ref('')
|
|
||||||
const password = ref('')
|
|
||||||
const switchForm = ref('login')
|
|
||||||
const loginError = ref('')
|
|
||||||
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 loginFunc() {
|
|
||||||
// check if username and password are valid
|
|
||||||
if (username.value === '' || password.value === '') {
|
|
||||||
loginError.value = 'Please enter a valid username and password'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// send login event to server
|
|
||||||
const response = await login(username.value, password.value)
|
|
||||||
|
|
||||||
if (response.success === undefined) {
|
|
||||||
loginError.value = response.error
|
|
||||||
return
|
|
||||||
}
|
|
||||||
gameStore.setToken(response.token)
|
|
||||||
gameStore.initConnection()
|
|
||||||
return true // Indicate success
|
|
||||||
}
|
|
||||||
|
|
||||||
async function registerFunc() {
|
|
||||||
// check if username and password are valid
|
|
||||||
if (username.value === '' || password.value === '') {
|
|
||||||
loginError.value = 'Please enter a valid username and password'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// send register event to server
|
|
||||||
const response = await register(username.value, password.value)
|
|
||||||
|
|
||||||
if (response.success === undefined) {
|
|
||||||
loginError.value = response.error
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginSuccess = await loginFunc()
|
|
||||||
if (!loginSuccess) {
|
|
||||||
loginError.value = 'Login after registration failed. Please try logging in manually.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,13 +1,17 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
|
import { getDomain } from '@/utilities'
|
||||||
|
|
||||||
export async function register(username: string, password: string) {
|
export async function register(username: string, email: string, password: string) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`${config.server_endpoint}/register`, { username, password })
|
const response = await axios.post(`${config.server_endpoint}/register`, { username, email, password })
|
||||||
useCookies().set('token', response.data.token as string)
|
useCookies().set('token', response.data.token as string)
|
||||||
return { success: true, token: response.data.token }
|
return { success: true, token: response.data.token }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
if (typeof error.response.data === 'undefined') {
|
||||||
|
return { error: 'Could not connect to server' }
|
||||||
|
}
|
||||||
return { error: error.response.data.message }
|
return { error: error.response.data.message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -16,12 +20,34 @@ export async function login(username: string, password: string) {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.post(`${config.server_endpoint}/login`, { username, password })
|
const response = await axios.post(`${config.server_endpoint}/login`, { username, password })
|
||||||
useCookies().set('token', response.data.token as string, {
|
useCookies().set('token', response.data.token as string, {
|
||||||
// for whole domain
|
domain: getDomain()
|
||||||
// @TODO : #190
|
|
||||||
// domain: window.location.hostname.split('.').slice(-2).join('.')
|
|
||||||
})
|
})
|
||||||
return { success: true, token: response.data.token }
|
return { success: true, token: response.data.token }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return { error: error.response.data.message }
|
return { error: error.response.data.message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resetPassword(email: string) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${config.server_endpoint}/reset-password`, { email })
|
||||||
|
return { success: true, token: response.data.token }
|
||||||
|
} catch (error: any) {
|
||||||
|
if (typeof error.response.data === 'undefined') {
|
||||||
|
return { error: 'Could not connect to server' }
|
||||||
|
}
|
||||||
|
return { error: error.response.data.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function newPassword(urlToken: string, password: string) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${config.server_endpoint}/new-password`, { urlToken, password })
|
||||||
|
return { success: true, token: response.data.token }
|
||||||
|
} catch (error: any) {
|
||||||
|
if (typeof error.response.data === 'undefined') {
|
||||||
|
return { error: 'Could not connect to server' }
|
||||||
|
}
|
||||||
|
return { error: error.response.data.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
69
src/storage/assetStorage.ts
Normal file
69
src/storage/assetStorage.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import config from '@/config'
|
||||||
|
import Dexie from 'dexie'
|
||||||
|
import type { AssetDataT } from '@/types'
|
||||||
|
|
||||||
|
export class AssetStorage {
|
||||||
|
private db: Dexie
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.db = new Dexie('assets')
|
||||||
|
this.db.version(1).stores({
|
||||||
|
assets: 'key, group'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async download(asset: AssetDataT) {
|
||||||
|
try {
|
||||||
|
// Check if the asset already exists, then check if updatedAt is newer
|
||||||
|
const _asset = await this.db.table('assets').get(asset.key)
|
||||||
|
if (_asset && _asset.updatedAt > asset.updatedAt) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the asset
|
||||||
|
const response = await fetch(config.server_endpoint + asset.data)
|
||||||
|
const blob = await response.blob()
|
||||||
|
|
||||||
|
// Store the asset in the database
|
||||||
|
await this.db.table('assets').put({ key: asset.key, data: blob, group: asset.group, updatedAt: asset.updatedAt, frameCount: asset.frameCount, frameWidth: asset.frameWidth, frameHeight: asset.frameHeight })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to add asset ${asset.key}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(key: string) {
|
||||||
|
try {
|
||||||
|
const asset = await this.db.table('assets').get(key)
|
||||||
|
if (asset) {
|
||||||
|
return {
|
||||||
|
...asset,
|
||||||
|
data: URL.createObjectURL(asset.data) // Convert blob to data URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to retrieve asset ${key}:`, error)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByGroup(group: string) {
|
||||||
|
try {
|
||||||
|
const assets = await this.db.table('assets').where('group').equals(group).toArray()
|
||||||
|
return assets.map((asset) => ({
|
||||||
|
...asset,
|
||||||
|
data: URL.createObjectURL(asset.data) // Convert blob to data URL
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to retrieve assets for group ${group}:`, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(key: string) {
|
||||||
|
try {
|
||||||
|
await this.db.table('assets').delete(key)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to delete asset ${key}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,29 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { io, Socket } from 'socket.io-client'
|
import { io, Socket } from 'socket.io-client'
|
||||||
import type { Asset, Character, Notification, User, WorldSettings } from '@/types'
|
import type { AssetDataT, Character, Notification, User, WorldSettings } from '@/types'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
|
import { getDomain } from '@/utilities'
|
||||||
|
|
||||||
export const useGameStore = defineStore('game', {
|
export const useGameStore = defineStore('game', {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
notifications: [] as Notification[],
|
notifications: [] as Notification[],
|
||||||
isAssetsLoaded: false,
|
|
||||||
loadedAssets: [] as string[],
|
|
||||||
token: '' as string | null,
|
token: '' as string | null,
|
||||||
connection: null as Socket | null,
|
connection: null as Socket | null,
|
||||||
user: null as User | null,
|
user: null as User | null,
|
||||||
character: null as Character | null,
|
character: null as Character | null,
|
||||||
isPlayerDraggingCamera: false,
|
|
||||||
world: {
|
world: {
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
isRainEnabled: false,
|
isRainEnabled: false,
|
||||||
isFogEnabled: false,
|
isFogEnabled: false,
|
||||||
fogDensity: 0.5
|
fogDensity: 0.5
|
||||||
} as WorldSettings,
|
} as WorldSettings,
|
||||||
gameSettings: {
|
game: {
|
||||||
|
isLoading: false,
|
||||||
|
isLoaded: false, // isLoaded is currently being used to determine if the player has interacted with the game
|
||||||
|
loadedAssets: [] as AssetDataT[],
|
||||||
|
isPlayerDraggingCamera: false,
|
||||||
isCameraFollowingCharacter: false
|
isCameraFollowingCharacter: false
|
||||||
},
|
},
|
||||||
uiSettings: {
|
uiSettings: {
|
||||||
@ -31,6 +33,17 @@ export const useGameStore = defineStore('game', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getters: {
|
||||||
|
getLoadedAssets: (state) => {
|
||||||
|
return state.game.loadedAssets
|
||||||
|
},
|
||||||
|
getLoadedAsset: (state) => {
|
||||||
|
return (key: string) => state.game.loadedAssets.find((asset) => asset.key === key)
|
||||||
|
},
|
||||||
|
getLoadedAssetsByGroup: (state) => {
|
||||||
|
return (group: string) => state.game.loadedAssets.filter((asset) => asset.group === group)
|
||||||
|
}
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
addNotification(notification: Notification) {
|
addNotification(notification: Notification) {
|
||||||
if (!notification.id) {
|
if (!notification.id) {
|
||||||
@ -53,17 +66,8 @@ export const useGameStore = defineStore('game', {
|
|||||||
toggleGmPanel() {
|
toggleGmPanel() {
|
||||||
this.uiSettings.isGmPanelOpen = !this.uiSettings.isGmPanelOpen
|
this.uiSettings.isGmPanelOpen = !this.uiSettings.isGmPanelOpen
|
||||||
},
|
},
|
||||||
togglePlayerDraggingCamera() {
|
|
||||||
this.isPlayerDraggingCamera = !this.isPlayerDraggingCamera
|
|
||||||
},
|
|
||||||
setPlayerDraggingCamera(moving: boolean) {
|
setPlayerDraggingCamera(moving: boolean) {
|
||||||
this.isPlayerDraggingCamera = moving
|
this.game.isPlayerDraggingCamera = moving
|
||||||
},
|
|
||||||
toggleCameraFollowingCharacter() {
|
|
||||||
this.gameSettings.isCameraFollowingCharacter = !this.gameSettings.isCameraFollowingCharacter
|
|
||||||
},
|
|
||||||
setCameraFollowingCharacter(following: boolean) {
|
|
||||||
this.gameSettings.isCameraFollowingCharacter = following
|
|
||||||
},
|
},
|
||||||
toggleChat() {
|
toggleChat() {
|
||||||
this.uiSettings.isChatOpen = !this.uiSettings.isChatOpen
|
this.uiSettings.isChatOpen = !this.uiSettings.isChatOpen
|
||||||
@ -101,21 +105,22 @@ export const useGameStore = defineStore('game', {
|
|||||||
this.connection?.disconnect()
|
this.connection?.disconnect()
|
||||||
|
|
||||||
useCookies().remove('token', {
|
useCookies().remove('token', {
|
||||||
// for whole domain
|
domain: getDomain()
|
||||||
// @TODO : #190
|
|
||||||
// domain: window.location.hostname.split('.').slice(-2).join('.')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.isAssetsLoaded = false
|
|
||||||
this.connection = null
|
this.connection = null
|
||||||
this.token = null
|
this.token = null
|
||||||
this.user = null
|
this.user = null
|
||||||
this.character = null
|
this.character = null
|
||||||
this.uiSettings.isGmPanelOpen = false
|
|
||||||
this.isPlayerDraggingCamera = false
|
this.game.isLoaded = false
|
||||||
this.gameSettings.isCameraFollowingCharacter = false
|
this.game.loadedAssets = []
|
||||||
|
this.game.isPlayerDraggingCamera = false
|
||||||
|
this.game.isCameraFollowingCharacter = false
|
||||||
|
|
||||||
this.uiSettings.isChatOpen = false
|
this.uiSettings.isChatOpen = false
|
||||||
this.uiSettings.isCharacterProfileOpen = false
|
this.uiSettings.isCharacterProfileOpen = false
|
||||||
|
this.uiSettings.isGmPanelOpen = false
|
||||||
|
|
||||||
this.world.date = new Date()
|
this.world.date = new Date()
|
||||||
this.world.isRainEnabled = false
|
this.world.isRainEnabled = false
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import type { Zone, Object, Tile, ZoneEffect } from '@/types'
|
import type { Zone, Object, Tile, ZoneEffect, ZoneObject } from '@/types'
|
||||||
|
|
||||||
export type TeleportSettings = {
|
export type TeleportSettings = {
|
||||||
toZoneId: number
|
toZoneId: number
|
||||||
@ -12,7 +12,7 @@ export type TeleportSettings = {
|
|||||||
export const useZoneEditorStore = defineStore('zoneEditor', {
|
export const useZoneEditorStore = defineStore('zoneEditor', {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
active: false,
|
active: true,
|
||||||
zone: null as Zone | null,
|
zone: null as Zone | null,
|
||||||
tool: 'move',
|
tool: 'move',
|
||||||
drawMode: 'tile',
|
drawMode: 'tile',
|
||||||
@ -20,14 +20,14 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
|||||||
zoneList: [] as Zone[],
|
zoneList: [] as Zone[],
|
||||||
tileList: [] as Tile[],
|
tileList: [] as Tile[],
|
||||||
objectList: [] as Object[],
|
objectList: [] as Object[],
|
||||||
selectedTile: null as Tile | null,
|
selectedTile: '',
|
||||||
selectedObject: null as Object | null,
|
selectedObject: null as Object | null,
|
||||||
objectDepth: 0,
|
|
||||||
isTileListModalShown: false,
|
isTileListModalShown: false,
|
||||||
isObjectListModalShown: false,
|
isObjectListModalShown: false,
|
||||||
isZoneListModalShown: false,
|
isZoneListModalShown: false,
|
||||||
isCreateZoneModalShown: false,
|
isCreateZoneModalShown: false,
|
||||||
isSettingsModalShown: false,
|
isSettingsModalShown: false,
|
||||||
|
shouldClearTiles: false,
|
||||||
zoneSettings: {
|
zoneSettings: {
|
||||||
name: '',
|
name: '',
|
||||||
width: 0,
|
width: 0,
|
||||||
@ -88,15 +88,12 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
|||||||
setObjectList(objects: Object[]) {
|
setObjectList(objects: Object[]) {
|
||||||
this.objectList = objects
|
this.objectList = objects
|
||||||
},
|
},
|
||||||
setSelectedTile(tile: Tile) {
|
setSelectedTile(tile: string) {
|
||||||
this.selectedTile = tile
|
this.selectedTile = tile
|
||||||
},
|
},
|
||||||
setSelectedObject(object: any) {
|
setSelectedObject(object: Object) {
|
||||||
this.selectedObject = object
|
this.selectedObject = object
|
||||||
},
|
},
|
||||||
setObjectDepth(depth: number) {
|
|
||||||
this.objectDepth = depth
|
|
||||||
},
|
|
||||||
toggleSettingsModal() {
|
toggleSettingsModal() {
|
||||||
this.isSettingsModalShown = !this.isSettingsModalShown
|
this.isSettingsModalShown = !this.isSettingsModalShown
|
||||||
},
|
},
|
||||||
@ -110,6 +107,13 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
|||||||
setTeleportSettings(teleportSettings: TeleportSettings) {
|
setTeleportSettings(teleportSettings: TeleportSettings) {
|
||||||
this.teleportSettings = teleportSettings
|
this.teleportSettings = teleportSettings
|
||||||
},
|
},
|
||||||
|
triggerClearTiles() {
|
||||||
|
this.shouldClearTiles = true
|
||||||
|
},
|
||||||
|
|
||||||
|
resetClearTilesFlag() {
|
||||||
|
this.shouldClearTiles = false
|
||||||
|
},
|
||||||
reset(resetZone = false) {
|
reset(resetZone = false) {
|
||||||
if (resetZone) this.zone = null
|
if (resetZone) this.zone = null
|
||||||
this.zoneList = []
|
this.zoneList = []
|
||||||
@ -117,12 +121,12 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
|||||||
this.objectList = []
|
this.objectList = []
|
||||||
this.tool = 'move'
|
this.tool = 'move'
|
||||||
this.drawMode = 'tile'
|
this.drawMode = 'tile'
|
||||||
this.selectedTile = null
|
this.selectedTile = ''
|
||||||
this.selectedObject = null
|
this.selectedObject = null
|
||||||
this.objectDepth = 0
|
|
||||||
this.isSettingsModalShown = false
|
this.isSettingsModalShown = false
|
||||||
this.isZoneListModalShown = false
|
this.isZoneListModalShown = false
|
||||||
this.isCreateZoneModalShown = false
|
this.isCreateZoneModalShown = false
|
||||||
|
this.shouldClearTiles = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -4,11 +4,12 @@ export type Notification = {
|
|||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AssetT = {
|
export type AssetDataT = {
|
||||||
key: string
|
key: string
|
||||||
url: string
|
data: string
|
||||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
|
isAnimated?: boolean
|
||||||
frameCount?: number
|
frameCount?: number
|
||||||
frameWidth?: number
|
frameWidth?: number
|
||||||
frameHeight?: number
|
frameHeight?: number
|
||||||
|
@ -1,3 +1,25 @@
|
|||||||
export function uuidv4() {
|
export function uuidv4() {
|
||||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
|
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function unduplicateArray(array: any[]) {
|
||||||
|
return [...new Set(array.flat())]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDomain() {
|
||||||
|
// Check if not localhost
|
||||||
|
if (window.location.hostname !== 'localhost') {
|
||||||
|
return window.location.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if not IP address
|
||||||
|
if (window.location.hostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
|
||||||
|
return window.location.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.location.hostname.split('.').length < 3) {
|
||||||
|
return window.location.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.location.hostname.split('.').slice(-2).join('.')
|
||||||
|
}
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
import config from '@/config'
|
|
||||||
import Dexie from 'dexie'
|
|
||||||
|
|
||||||
class AssetManager extends Dexie {
|
|
||||||
assets!: Dexie.Table<
|
|
||||||
{
|
|
||||||
key: string
|
|
||||||
data: Blob
|
|
||||||
group: string
|
|
||||||
updatedAt: Date
|
|
||||||
frameCount?: number
|
|
||||||
frameWidth?: number
|
|
||||||
frameHeight?: number
|
|
||||||
},
|
|
||||||
string
|
|
||||||
>
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super('Assets')
|
|
||||||
this.version(1).stores({
|
|
||||||
assets: 'key, group'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadAsset(key: string, url: string, group: string, updatedAt: Date, frameCount?: number, frameWidth?: number, frameHeight?: number) {
|
|
||||||
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) {
|
|
||||||
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) {
|
|
||||||
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 deleteAsset(key: string) {
|
|
||||||
try {
|
|
||||||
await this.assets.delete(key)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to delete asset ${key}:`, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAssetManager = new AssetManager()
|
|
@ -2,12 +2,14 @@ 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 VueDevTools from 'vite-plugin-vue-devtools'
|
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
import viteCompression from 'vite-plugin-compression';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
VueDevTools(),
|
VueDevTools(),
|
||||||
|
viteCompression()
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
Reference in New Issue
Block a user