Compare commits
No commits in common. "main" and "feature/map-refactor" have entirely different histories.
main
...
feature/ma
902
package-lock.json
generated
19
package.json
@ -16,22 +16,19 @@
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.5.0",
|
||||
"@vueuse/integrations": "^10.5.0",
|
||||
"axios": "^1.7.9",
|
||||
"dexie": "^4.0.11",
|
||||
"phaser": "^3.88.2",
|
||||
"phaser3-rex-plugins": "^1.80.13",
|
||||
"phavuer": "^0.16.5",
|
||||
"pinia": "^2.3.1",
|
||||
"axios": "^1.7.7",
|
||||
"dexie": "^4.0.8",
|
||||
"phaser": "3.87.0",
|
||||
"pinia": "^2.1.6",
|
||||
"sharp": "^0.33.5",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"socket.io-client": "^4.8.0",
|
||||
"universal-cookie": "^6.1.3",
|
||||
"vite-plugin-image-optimizer": "^1.1.8",
|
||||
"vue": "^3.5.13",
|
||||
"zod": "^3.24.2"
|
||||
"vue": "^3.5.12",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
||||
"@tauri-apps/cli": "^2.2.7",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^20.14.11",
|
||||
@ -41,6 +38,8 @@
|
||||
"autoprefixer": "^10.4.19",
|
||||
"jsdom": "^24.1.1",
|
||||
"npm-run-all2": "^6.2.3",
|
||||
"phaser3-rex-plugins": "^1.80.8",
|
||||
"phavuer": "^0.16.1",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.3.3",
|
||||
"sass": "^1.79.4",
|
||||
|
4
src-tauri/.gitignore
vendored
@ -1,4 +0,0 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
5149
src-tauri/Cargo.lock
generated
@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.4", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
tauri = { version = "2.2.4", features = [] }
|
||||
tauri-plugin-log = "2.0.0-rc"
|
@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default"
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 49 KiB |
@ -1,16 +0,0 @@
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
.build(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
app_lib::run();
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "noxious",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.noxious.app",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build-only"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Noxious",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
@ -16,7 +16,6 @@ import Debug from '@/components/utilities/Debug.vue'
|
||||
import Notifications from '@/components/utilities/Notifications.vue'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
@ -27,8 +26,8 @@ const { playSound } = useSoundComposable()
|
||||
|
||||
const currentScreen = computed(() => {
|
||||
if (!gameStore.game.isLoaded) return Loading
|
||||
if (!socketManager.connection) return Login
|
||||
if (!socketManager.token) return Login
|
||||
if (!gameStore.connection) return Login
|
||||
if (!gameStore.token) return Login
|
||||
if (!gameStore.character) return Characters
|
||||
if (mapEditor.active.value) return MapEditor
|
||||
return Game
|
||||
|
@ -5,8 +5,6 @@ export enum Direction {
|
||||
}
|
||||
|
||||
export enum SocketEvent {
|
||||
CONNECT_ERROR = 'connect_error',
|
||||
RECONNECT_FAILED = 'reconnect_failed',
|
||||
CLOSE = '52',
|
||||
DATA = '51',
|
||||
CHARACTER_CONNECT = '50',
|
||||
@ -43,7 +41,7 @@ export enum SocketEvent {
|
||||
GM_MAP_REQUEST = '19',
|
||||
GM_MAP_UPDATE = '18',
|
||||
MAP_CHARACTER_MOVEERROR = '17',
|
||||
DISCONNECT = 'disconnect',
|
||||
DISCONNECT = '16',
|
||||
USER_DISCONNECT = '15',
|
||||
LOGIN = '14',
|
||||
LOGGED_IN = '13',
|
||||
@ -51,7 +49,7 @@ export enum SocketEvent {
|
||||
DATE = '11',
|
||||
FAILED = '10',
|
||||
COMPLETED = '9',
|
||||
CONNECTION = 'connection',
|
||||
CONNECTION = '8',
|
||||
WEATHER = '7',
|
||||
CHARACTER_DISCONNECT = '6',
|
||||
MAP_CHARACTER_ATTACK = '5',
|
||||
|
@ -36,8 +36,7 @@ export type Tile = {
|
||||
export type MapObject = {
|
||||
id: string
|
||||
name: string
|
||||
tags: string[]
|
||||
depthOffsets: number[]
|
||||
tags: any | null
|
||||
originX: number
|
||||
originY: number
|
||||
frameRate: number
|
||||
@ -101,7 +100,7 @@ export enum MapEventTileType {
|
||||
|
||||
export type MapEventTile = {
|
||||
id: string
|
||||
map: string
|
||||
mapid: string
|
||||
type: MapEventTileType
|
||||
positionX: number
|
||||
positionY: number
|
||||
@ -151,9 +150,8 @@ export type CharacterType = {
|
||||
export type CharacterHair = {
|
||||
id: string
|
||||
name: string
|
||||
sprite: string | Sprite
|
||||
sprite?: Sprite
|
||||
gender: CharacterGender
|
||||
color: string
|
||||
isSelectable: boolean
|
||||
}
|
||||
|
||||
@ -210,8 +208,6 @@ export enum CharacterEquipmentSlotType {
|
||||
export type Sprite = {
|
||||
id: string
|
||||
name: string
|
||||
width: number | null
|
||||
height: number | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
spriteActions: SpriteAction[]
|
||||
|
@ -21,17 +21,7 @@ export async function downloadCache<T extends { id: string; updatedAt: Date }>(e
|
||||
}
|
||||
|
||||
const items = response.data ?? []
|
||||
const serverItemIds = new Set(items.map((item) => item.id))
|
||||
|
||||
// Remove items that don't exist on server
|
||||
const existingItems = await storage.getAll()
|
||||
for (const existingItem of existingItems) {
|
||||
if (!serverItemIds.has(existingItem.id)) {
|
||||
await storage.delete(existingItem.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Update or add new items
|
||||
for (const item of items) {
|
||||
let overwrite = false
|
||||
const existingItem = await storage.getById(item.id)
|
||||
|
@ -124,7 +124,7 @@ button {
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
@apply bg-red-500;
|
||||
@apply bg-red-400;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,8 @@
|
||||
<Container ref="characterContainer" :x="currentPositionX" :y="currentPositionY" :depth="isometricDepth">
|
||||
<ChatBubble :mapCharacter="props.mapCharacter" />
|
||||
<HealthBar :mapCharacter="props.mapCharacter" />
|
||||
<CharacterHair :mapCharacter="props.mapCharacter" :flipX="isFlippedX" />
|
||||
<Sprite ref="characterSprite" :flipX="isFlippedX" />
|
||||
<CharacterHair :mapCharacter="props.mapCharacter" />
|
||||
<Sprite ref="characterSprite" :origin-y="1" :flipX="isFlippedX" />
|
||||
</Container>
|
||||
</template>
|
||||
|
||||
@ -84,14 +84,13 @@ watch(
|
||||
rotation: props.mapCharacter.character.rotation,
|
||||
isAttacking: props.mapCharacter.isAttacking
|
||||
}),
|
||||
async (oldValues, newValues) => {
|
||||
(oldValues, newValues) => {
|
||||
handlePositionUpdate(oldValues, newValues)
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await initializeSprite()
|
||||
|
||||
if (props.mapCharacter.character.id === gameStore.character!.id) {
|
||||
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
|
||||
}
|
||||
|
@ -1,63 +1,54 @@
|
||||
<template>
|
||||
<Image ref="image" v-if="hairSpriteId" />
|
||||
<Image v-bind="imageProps" v-if="gameStore.isTextureLoaded(texture)" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
|
||||
import { loadSpriteTextures } from '@/services/textureService'
|
||||
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
|
||||
import { Image, refObj, useScene } from 'phavuer'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
mapCharacter: MapCharacter
|
||||
}>()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const scene = useScene()
|
||||
const hairSpriteId = ref('')
|
||||
const hairSprite = ref<SpriteT | null>(null)
|
||||
const characterSpriteHeight = ref(0)
|
||||
const image = refObj<Phaser.GameObjects.Image>()
|
||||
const sprite = ref<SpriteT | null>(null)
|
||||
|
||||
const flipX = computed(() => [6, 0].includes(props.mapCharacter.character.rotation ?? 0))
|
||||
const texture = computed(() => {
|
||||
const direction = flipX.value ? 'back' : 'front'
|
||||
const { rotation } = props.mapCharacter.character
|
||||
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
||||
|
||||
return `${hairSpriteId.value}-${direction}`
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.mapCharacter.character,
|
||||
(newValue) => {
|
||||
if (!image.value) return
|
||||
image.value.setTexture(texture.value)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
|
||||
|
||||
const imageProps = computed(() => {
|
||||
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
||||
const spriteAction = sprite.value?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
||||
|
||||
return {
|
||||
depth: 9999,
|
||||
originX: Number(spriteAction?.originX) ?? 0,
|
||||
originY: Number(spriteAction?.originY) ?? 0,
|
||||
flipX: isFlippedX.value,
|
||||
texture: texture.value
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!props.mapCharacter.character.characterType || !props.mapCharacter.character.characterHair) return
|
||||
|
||||
const characterTypeStorage = new CharacterTypeStorage()
|
||||
const characterHairStorage = new CharacterHairStorage()
|
||||
const spriteId = await characterHairStorage.getSpriteId(props.mapCharacter.character.characterHair!)
|
||||
if (!spriteId) return
|
||||
|
||||
hairSpriteId.value = spriteId
|
||||
const spriteStorage = new SpriteStorage()
|
||||
|
||||
const characterType = await characterTypeStorage.getById(props.mapCharacter.character.characterType!)
|
||||
if (!characterType) return
|
||||
characterSpriteHeight.value = 100
|
||||
|
||||
hairSpriteId.value = await characterHairStorage.getSpriteId(props.mapCharacter.character.characterHair)
|
||||
if (!hairSpriteId.value) return
|
||||
|
||||
hairSprite.value = await spriteStorage.getById(hairSpriteId.value)
|
||||
if (!hairSprite.value) return
|
||||
|
||||
await loadSpriteTextures(scene, hairSpriteId.value)
|
||||
|
||||
if (!image.value) return
|
||||
|
||||
image.value.setOrigin(0.5, 2.15)
|
||||
image.value.setTexture(texture.value)
|
||||
image.value.setSize(30, 40)
|
||||
sprite.value = await spriteStorage.getById(spriteId)
|
||||
await loadSpriteTextures(scene, spriteId)
|
||||
})
|
||||
</script>
|
||||
|
@ -22,7 +22,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useMapStore } from '@/stores/mapStore'
|
||||
import { onClickOutside, useFocus } from '@vueuse/core'
|
||||
@ -31,6 +30,7 @@ import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
const scene = useScene()
|
||||
const gameStore = useGameStore()
|
||||
const mapStore = useMapStore()
|
||||
|
||||
const message = ref('')
|
||||
const chats = ref<{ character: string; message: string }[]>([])
|
||||
@ -55,7 +55,7 @@ function unfocusChat(event: Event, targetElement: HTMLElement) {
|
||||
|
||||
const sendMessage = () => {
|
||||
if (!message.value.trim()) return
|
||||
socketManager.emit(SocketEvent.CHAT_MESSAGE, { message: message.value }, (response: boolean) => {})
|
||||
gameStore.connection?.emit(SocketEvent.CHAT_MESSAGE, { message: message.value }, (response: boolean) => {})
|
||||
message.value = ''
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ const scrollToBottom = () => {
|
||||
})
|
||||
}
|
||||
|
||||
socketManager.on(SocketEvent.CHAT_MESSAGE, (data: { character: string; message: string }) => {
|
||||
gameStore.connection?.on(SocketEvent.CHAT_MESSAGE, (data: { character: string; message: string }) => {
|
||||
if (!data.character || !data.message) return
|
||||
|
||||
chats.value.push({ character: data.character, message: data.message })
|
||||
@ -153,7 +153,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
socketManager.off(SocketEvent.CHAT_MESSAGE)
|
||||
gameStore.connection?.off(SocketEvent.CHAT_MESSAGE)
|
||||
removeEventListener('keydown', focusChat)
|
||||
})
|
||||
</script>
|
||||
|
@ -8,7 +8,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useDateFormat } from '@vueuse/core'
|
||||
import { onUnmounted } from 'vue'
|
||||
@ -16,6 +15,6 @@ import { onUnmounted } from 'vue'
|
||||
const gameStore = useGameStore()
|
||||
|
||||
onUnmounted(() => {
|
||||
socketManager.off(SocketEvent.DATE)
|
||||
gameStore.connection?.off(SocketEvent.DATE)
|
||||
})
|
||||
</script>
|
||||
|
@ -6,7 +6,6 @@
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { MapCharacter, UUID } from '@/application/types'
|
||||
import Character from '@/components/game/character/Character.vue'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useMapStore } from '@/stores/mapStore'
|
||||
import { onUnmounted } from 'vue'
|
||||
@ -18,32 +17,32 @@ const props = defineProps<{
|
||||
tileMap: Phaser.Tilemaps.Tilemap
|
||||
}>()
|
||||
|
||||
socketManager.on(SocketEvent.MAP_CHARACTER_JOIN, (data: MapCharacter) => {
|
||||
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_JOIN, (data: MapCharacter) => {
|
||||
mapStore.addCharacter(data)
|
||||
})
|
||||
|
||||
socketManager.on(SocketEvent.MAP_CHARACTER_LEAVE, (characterId: UUID) => {
|
||||
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_LEAVE, (characterId: UUID) => {
|
||||
mapStore.removeCharacter(characterId)
|
||||
})
|
||||
|
||||
socketManager.on(SocketEvent.MAP_CHARACTER_MOVE, ([characterId, posX, posY, rot, isMoving]: [UUID, number, number, number, boolean]) => {
|
||||
mapStore.updateCharacterPosition([characterId, posX, posY, rot, isMoving])
|
||||
|
||||
if (characterId === gameStore.character?.id) {
|
||||
gameStore.character!.positionX = posX
|
||||
gameStore.character!.positionY = posY
|
||||
gameStore.character!.rotation = rot
|
||||
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_MOVE, (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
|
||||
mapStore.updateCharacterPosition(data)
|
||||
// @TODO: Replace with universal class, composable or store
|
||||
if (data.characterId === gameStore.character?.id) {
|
||||
gameStore.character!.positionX = data.positionX
|
||||
gameStore.character!.positionY = data.positionY
|
||||
gameStore.character!.rotation = data.rotation
|
||||
}
|
||||
})
|
||||
|
||||
socketManager.on(SocketEvent.MAP_CHARACTER_ATTACK, (characterId: UUID) => {
|
||||
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_ATTACK, (characterId: UUID) => {
|
||||
mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
socketManager.off(SocketEvent.MAP_CHARACTER_ATTACK)
|
||||
socketManager.off(SocketEvent.MAP_CHARACTER_MOVE)
|
||||
socketManager.off(SocketEvent.MAP_CHARACTER_JOIN)
|
||||
socketManager.off(SocketEvent.MAP_CHARACTER_LEAVE)
|
||||
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_ATTACK)
|
||||
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_MOVE)
|
||||
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_JOIN)
|
||||
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_LEAVE)
|
||||
})
|
||||
</script>
|
||||
|
@ -11,7 +11,6 @@ import { unduplicateArray } from '@/application/utilities'
|
||||
import Characters from '@/components/game/map/Characters.vue'
|
||||
import MapTiles from '@/components/game/map/MapTiles.vue'
|
||||
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { createTileLayer, createTileMap, loadTileTexturesFromMapTileArray } from '@/services/mapService'
|
||||
import { MapStorage } from '@/storage/storages'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
@ -30,7 +29,7 @@ const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
||||
|
||||
// Event listeners
|
||||
socketManager.on(SocketEvent.MAP_CHARACTER_TELEPORT, (data: mapLoadData) => {
|
||||
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_TELEPORT, (data: mapLoadData) => {
|
||||
mapStore.setMapId(data.mapId)
|
||||
mapStore.setCharacters(data.characters)
|
||||
})
|
||||
@ -66,6 +65,6 @@ onUnmounted(() => {
|
||||
tileMap.value.destroy()
|
||||
}
|
||||
|
||||
socketManager.off(SocketEvent.MAP_CHARACTER_TELEPORT)
|
||||
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_TELEPORT)
|
||||
})
|
||||
</script>
|
||||
|
@ -1,102 +0,0 @@
|
||||
<template>
|
||||
<Zone :depth="baseDepth" :origin-x="mapObj?.originX" :origin-y="mapObj?.originY" :width="mapObj?.frameWidth" :height="mapObj?.frameHeight" :x="x" :y="y" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MapObject, PlacedMapObject } from '@/application/types'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { calculateIsometricDepth } from '@/services/mapService'
|
||||
import { onPreUpdate, useScene, Zone } from 'phavuer'
|
||||
import { computed, onUnmounted } from 'vue'
|
||||
|
||||
interface Props {
|
||||
obj?: PlacedMapObject
|
||||
mapObj?: MapObject
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const mapEditor = useMapEditorComposable()
|
||||
const scene = useScene()
|
||||
|
||||
const group = scene.add.group()
|
||||
const partitionPoints = computed(() => {
|
||||
if (!props.mapObj?.frameWidth || !props.mapObj?.depthOffsets.length) return []
|
||||
|
||||
const sliceCount = props.mapObj.depthOffsets.length
|
||||
return Array.from({ length: sliceCount + 1 }, (_, i) => i * (props.mapObj!.frameWidth / sliceCount))
|
||||
})
|
||||
|
||||
let baseDepth = 0
|
||||
|
||||
const createImagePartition = (startX: number, endX: number, depthOffset: number): void => {
|
||||
if (!props.mapObj?.id) return
|
||||
|
||||
const img = scene.add.image(0, 0, props.mapObj.id)
|
||||
img.setOrigin(props.mapObj.originX, props.mapObj.originY)
|
||||
img.setCrop(startX, 0, endX, props.mapObj.frameHeight)
|
||||
img.setDepth(baseDepth + depthOffset)
|
||||
group.add(img)
|
||||
}
|
||||
|
||||
const updateGroupProperties = (): void => {
|
||||
if (!props.obj || !props.x || !props.y) return
|
||||
|
||||
const isMoving = mapEditor.movingPlacedObject.value?.id === props.obj.id
|
||||
const isSelected = mapEditor.selectedMapObject.value?.id === props.obj.id
|
||||
const isPlacedSelected = mapEditor.selectedPlacedObject.value?.id === props.obj.id
|
||||
|
||||
baseDepth = calculateIsometricDepth(props.obj.positionX, props.obj.positionY)
|
||||
|
||||
group.setXY(props.x, props.y)
|
||||
group.setAlpha(isMoving || isSelected ? 0.5 : 1)
|
||||
group.setTint(isPlacedSelected ? 0x00ff00 : 0xffffff)
|
||||
group.setDepth(baseDepth)
|
||||
}
|
||||
|
||||
const updateImageProperties = (): void => {
|
||||
const orderedImages = group.getChildren() as Phaser.GameObjects.Image[]
|
||||
|
||||
orderedImages.forEach((image, index) => {
|
||||
if (!props.obj || !props.mapObj || !props.x) return
|
||||
|
||||
image.flipX = props.obj.isRotated
|
||||
|
||||
if (props.obj.isRotated) {
|
||||
const offsetNum = props.mapObj.depthOffsets.length
|
||||
const xOffset = props.mapObj.frameWidth / offsetNum
|
||||
image.x = props.x + (index < offsetNum / 2 ? -xOffset : xOffset)
|
||||
image.setDepth(baseDepth - props.mapObj.depthOffsets[index])
|
||||
} else {
|
||||
image.x = props.x
|
||||
image.setDepth(baseDepth + props.mapObj.depthOffsets[index])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onPreUpdate(() => {
|
||||
updateGroupProperties()
|
||||
updateImageProperties()
|
||||
})
|
||||
|
||||
// Initial setup
|
||||
const initializeGroup = (): void => {
|
||||
if (!props.mapObj || !props.x || !props.y || !props.obj) return
|
||||
|
||||
baseDepth = calculateIsometricDepth(props.obj.positionX, props.obj.positionY)
|
||||
group.setXY(props.x, props.y)
|
||||
group.setOrigin(props.mapObj.originX, props.mapObj.originY)
|
||||
|
||||
const points = partitionPoints.value
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
createImagePartition(points[i], points[i + 1], props.mapObj.depthOffsets[i])
|
||||
}
|
||||
}
|
||||
|
||||
initializeGroup()
|
||||
|
||||
onUnmounted(() => {
|
||||
group.destroy(true, true)
|
||||
})
|
||||
</script>
|
@ -1,15 +1,16 @@
|
||||
<template>
|
||||
<ImageGroup v-bind="groupProps" v-if="mapObject && gameStore.isTextureLoaded(props.placedMapObject.mapObject as string)" />
|
||||
<Image v-if="mapObject && gameStore.isTextureLoaded(props.placedMapObject.mapObject as string)" v-bind="imageProps" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import type { MapObject, PlacedMapObject } from '@/application/types'
|
||||
import ImageGroup from '@/components/game/map/partials/ImageGroup.vue'
|
||||
import { loadMapObjectTextures, tileToWorldXY } from '@/services/mapService'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { calculateIsometricDepth, loadMapObjectTextures, tileToWorldXY } from '@/services/mapService'
|
||||
import { MapObjectStorage } from '@/storage/storages'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useScene } from 'phavuer'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import Tilemap = Phaser.Tilemaps.Tilemap
|
||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||
@ -23,15 +24,10 @@ const props = defineProps<{
|
||||
const scene = useScene()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const mapEditor = useMapEditorComposable()
|
||||
|
||||
const mapObject = ref<MapObject>()
|
||||
|
||||
const groupProps = computed(() => ({
|
||||
...calculateObjectPlacement(props.placedMapObject),
|
||||
mapObj: mapObject.value,
|
||||
obj: props.placedMapObject
|
||||
}))
|
||||
|
||||
async function initialize() {
|
||||
if (!props.placedMapObject.mapObject) return
|
||||
|
||||
@ -48,8 +44,6 @@ async function initialize() {
|
||||
const _mapObject = await mapObjectStorage.getById(props.placedMapObject.mapObject as string)
|
||||
if (!_mapObject) return
|
||||
|
||||
console.log(_mapObject)
|
||||
|
||||
mapObject.value = _mapObject
|
||||
|
||||
await loadMapObjectTextures([_mapObject], scene)
|
||||
@ -59,11 +53,29 @@ function calculateObjectPlacement(mapObj: PlacedMapObject): { x: number; y: numb
|
||||
let position = tileToWorldXY(props.tileMapLayer, mapObj.positionX, mapObj.positionY)
|
||||
|
||||
return {
|
||||
x: position.worldPositionX,
|
||||
y: position.worldPositionY
|
||||
x: position.worldPositionX - mapObject.value!.frameWidth / 2,
|
||||
y: position.worldPositionY - mapObject.value!.frameHeight / 2 + config.tile_size.height
|
||||
}
|
||||
}
|
||||
|
||||
const imageProps = computed(() => ({
|
||||
alpha: mapEditor.movingPlacedObject.value?.id == props.placedMapObject.id || mapEditor.selectedMapObject.value?.id == props.placedMapObject.id ? 0.5 : 1,
|
||||
tint: mapEditor.selectedPlacedObject.value?.id == props.placedMapObject.id ? 0x00ff00 : 0xffffff,
|
||||
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, mapObject.value!.frameWidth, mapObject.value!.frameHeight, mapObject.value!.originX, mapObject.value!.originY),
|
||||
...calculateObjectPlacement(props.placedMapObject),
|
||||
flipX: props.placedMapObject.isRotated,
|
||||
texture: mapObject.value!.id,
|
||||
originX: mapObject.value!.originX,
|
||||
originY: mapObject.value!.originY
|
||||
}))
|
||||
|
||||
watch(
|
||||
() => mapEditor.refreshMapObject.value,
|
||||
async () => {
|
||||
await initialize()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await initialize()
|
||||
})
|
||||
|
@ -20,29 +20,12 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<div class="space-x-6 flex items-center">
|
||||
<label for="color">Color</label>
|
||||
<input v-model="characterColor" class="input-field" type="text" name="color" placeholder="Character Hair Color" />
|
||||
<div class="h-[38px] w-[38px] rounded" :style="{ backgroundColor: characterColor }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="spriteId">Sprite</label>
|
||||
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
||||
<option disabled selected value="">Select sprite</option>
|
||||
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label>Preview</label>
|
||||
<div v-if="characterSpriteId" class="flex flex-col">
|
||||
<div class="p-3 pb-5 min-h-32 block rounded-md default-border bg-gray-800">
|
||||
<div class="flex items-center justify-center p-1 h-full bg-gray-700 rounded">
|
||||
<img :src="config.server_endpoint + '/textures/sprites/' + characterSpriteId + '/front.png'" class="max-w-[200px] max-h-[200px] object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterHair">Remove</button>
|
||||
</form>
|
||||
@ -51,50 +34,49 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
|
||||
import { downloadCache } from '@/application/utilities'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { CharacterHairStorage } from '@/storage/storages'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
|
||||
|
||||
const characterName = ref('')
|
||||
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
||||
const characterColor = ref<string>('#000000')
|
||||
const characterIsSelectable = ref<boolean>(false)
|
||||
const characterSpriteId = ref<string | null | undefined>(null)
|
||||
|
||||
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
||||
|
||||
if (!selectedCharacterHair.value) {
|
||||
console.error('No character hair selected')
|
||||
}
|
||||
|
||||
if (selectedCharacterHair.value) {
|
||||
characterName.value = selectedCharacterHair.value.name
|
||||
characterGender.value = selectedCharacterHair.value.gender
|
||||
characterColor.value = selectedCharacterHair.value.color
|
||||
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
|
||||
characterSpriteId.value = selectedCharacterHair.value.sprite?.id
|
||||
}
|
||||
|
||||
async function removeCharacterHair() {
|
||||
function removeCharacterHair() {
|
||||
if (!selectedCharacterHair.value) return
|
||||
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_REMOVE, { id: selectedCharacterHair.value.id }, async (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_REMOVE, { id: selectedCharacterHair.value.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to remove character hair')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('character_hair', new CharacterHairStorage())
|
||||
await refreshCharacterHairList()
|
||||
refreshCharacterHairList()
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||
function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||
assetManagerStore.setCharacterHairList(response)
|
||||
|
||||
if (unsetSelectedCharacterHair) {
|
||||
@ -103,24 +85,21 @@ async function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
||||
})
|
||||
}
|
||||
|
||||
async function saveCharacterHair() {
|
||||
function saveCharacterHair() {
|
||||
const characterHairData = {
|
||||
id: selectedCharacterHair.value!.id,
|
||||
name: characterName.value,
|
||||
gender: characterGender.value,
|
||||
color: characterColor.value,
|
||||
isSelectable: characterIsSelectable.value,
|
||||
spriteId: characterSpriteId.value
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_UPDATE, characterHairData, async (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_UPDATE, characterHairData, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save character type')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('character_hair', new CharacterHairStorage())
|
||||
await refreshCharacterHairList(false)
|
||||
refreshCharacterHairList(false)
|
||||
})
|
||||
}
|
||||
|
||||
@ -128,7 +107,6 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
|
||||
if (!characterHair) return
|
||||
characterName.value = characterHair.name
|
||||
characterGender.value = characterHair.gender
|
||||
characterColor.value = characterHair.color
|
||||
characterIsSelectable.value = characterHair.isSelectable
|
||||
characterSpriteId.value = characterHair.sprite?.id
|
||||
})
|
||||
@ -136,7 +114,7 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
|
||||
onMounted(() => {
|
||||
if (!selectedCharacterHair.value) return
|
||||
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
|
@ -34,7 +34,6 @@
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { CharacterHair } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
@ -54,13 +53,13 @@ const handleSearch = () => {
|
||||
}
|
||||
|
||||
const createNewCharacterHair = () => {
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_CREATE, {}, (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_CREATE, {}, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to create new character type')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||
assetManagerStore.setCharacterHairList(response)
|
||||
})
|
||||
})
|
||||
@ -94,7 +93,7 @@ function toTop() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||
assetManagerStore.setCharacterHairList(response)
|
||||
})
|
||||
})
|
||||
|
@ -42,12 +42,11 @@
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
|
||||
import { downloadCache } from '@/application/utilities'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { CharacterTypeStorage } from '@/storage/storages'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const selectedCharacterType = computed(() => assetManagerStore.selectedCharacterType)
|
||||
@ -73,22 +72,20 @@ if (selectedCharacterType.value) {
|
||||
characterSpriteId.value = selectedCharacterType.value.sprite?.id
|
||||
}
|
||||
|
||||
async function removeCharacterType() {
|
||||
function removeCharacterType() {
|
||||
if (!selectedCharacterType.value) return
|
||||
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_REMOVE, { id: selectedCharacterType.value.id }, async (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_REMOVE, { id: selectedCharacterType.value.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to remove character type')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('character_types', new CharacterTypeStorage())
|
||||
await refreshCharacterTypeList()
|
||||
refreshCharacterTypeList()
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||
function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||
assetManagerStore.setCharacterTypeList(response)
|
||||
|
||||
if (unsetSelectedCharacterType) {
|
||||
@ -97,7 +94,7 @@ async function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
||||
})
|
||||
}
|
||||
|
||||
async function saveCharacterType() {
|
||||
function saveCharacterType() {
|
||||
const characterTypeData = {
|
||||
id: selectedCharacterType.value!.id,
|
||||
name: characterName.value,
|
||||
@ -107,14 +104,12 @@ async function saveCharacterType() {
|
||||
spriteId: characterSpriteId.value
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_UPDATE, characterTypeData, async (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_UPDATE, characterTypeData, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save character type')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('character_types', new CharacterTypeStorage())
|
||||
await refreshCharacterTypeList(false)
|
||||
refreshCharacterTypeList(false)
|
||||
})
|
||||
}
|
||||
|
||||
@ -130,7 +125,7 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
|
||||
onMounted(() => {
|
||||
if (!selectedCharacterType.value) return
|
||||
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
|
@ -34,7 +34,6 @@
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { CharacterType } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
@ -54,13 +53,13 @@ const handleSearch = () => {
|
||||
}
|
||||
|
||||
const createNewCharacterType = () => {
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_CREATE, {}, (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_CREATE, {}, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to create new character type')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||
assetManagerStore.setCharacterTypeList(response)
|
||||
})
|
||||
})
|
||||
@ -94,7 +93,7 @@ function toTop() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||
assetManagerStore.setCharacterTypeList(response)
|
||||
})
|
||||
})
|
||||
|
@ -46,7 +46,6 @@
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
@ -82,7 +81,7 @@ if (selectedItem.value) {
|
||||
function removeItem() {
|
||||
if (!selectedItem.value) return
|
||||
|
||||
socketManager.emit(SocketEvent.GM_ITEM_REMOVE, { id: selectedItem.value.id }, (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_ITEM_REMOVE, { id: selectedItem.value.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to remove item')
|
||||
return
|
||||
@ -92,7 +91,7 @@ function removeItem() {
|
||||
}
|
||||
|
||||
function refreshItemList(unsetSelectedItem = true) {
|
||||
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||
assetManagerStore.setItemList(response)
|
||||
|
||||
if (unsetSelectedItem) {
|
||||
@ -112,7 +111,7 @@ function saveItem() {
|
||||
spriteId: itemSpriteId.value
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_ITEM_UPDATE, itemData, (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_ITEM_UPDATE, itemData, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save item')
|
||||
return
|
||||
@ -134,7 +133,7 @@ watch(selectedItem, (item: Item | null) => {
|
||||
onMounted(() => {
|
||||
if (!selectedItem.value) return
|
||||
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
|
@ -31,7 +31,6 @@
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Item } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
@ -50,13 +49,13 @@ const handleSearch = () => {
|
||||
}
|
||||
|
||||
const createNewItem = () => {
|
||||
socketManager.emit(SocketEvent.GM_ITEM_CREATE, {}, (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_ITEM_CREATE, {}, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to create new item')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||
assetManagerStore.setItemList(response)
|
||||
})
|
||||
})
|
||||
@ -90,7 +89,7 @@ function toTop() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||
assetManagerStore.setItemList(response)
|
||||
})
|
||||
})
|
||||
|
@ -1,30 +1,8 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||
<div class="grid grid-cols-[160px_auto_max-content] gap-12">
|
||||
<div>
|
||||
<input type="checkbox" checked v-model="showOrigin" /><label>Show Origin</label>
|
||||
<br />
|
||||
<input type="checkbox" checked v-model="showPartitionOverlay" /><label>Show Partitions</label>
|
||||
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" />
|
||||
</div>
|
||||
<div class="relative w-fit h-fit">
|
||||
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" ref="imageRef" />
|
||||
<svg ref="svg" class="absolute top-0 left-0 w-full h-full inline-block pointer-events-none">
|
||||
<circle v-if="showOrigin && svg" r="4" :cx="mapObjectOriginX * width" :cy="mapObjectOriginY * height" stroke="white" stroke-width="2" />
|
||||
<rect v-if="showPartitionOverlay && svg" v-for="(offset, index) in mapObjectDepthOffsets" style="opacity: 0.5" stroke="red" :x="index * (width / mapObjectDepthOffsets.length)" :width="width / mapObjectDepthOffsets.length" :y="0" :height="height" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" @click="mapObjectDepthOffsets.push(0)">Add Partition</button>
|
||||
<p>Depth Offset</p>
|
||||
<div class="text-white grid grid-cols-[120px_80px_auto] items-baseline gap-2" v-for="(offset, index) in mapObjectDepthOffsets">
|
||||
<input class="input-field max-h-4 mt-2" type="number" :value="offset" @change="setPartitionDepth($event, index)" />
|
||||
<button @click="mapObjectDepthOffsets.splice(index, 1)">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 block">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||
<div class="form-field-full">
|
||||
@ -68,32 +46,23 @@
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { MapObject } from '@/application/types'
|
||||
import { downloadCache } from '@/application/utilities'
|
||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { MapObjectStorage } from '@/storage/storages'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { Rectangle } from 'phavuer'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
|
||||
const svg = useTemplateRef('svg')
|
||||
const { width, height } = useElementSize(svg)
|
||||
|
||||
const mapObjectName = ref('')
|
||||
const mapObjectTags = ref<string[]>([])
|
||||
const mapObjectDepthOffsets = ref<number[]>([])
|
||||
const mapObjectOriginX = ref(0)
|
||||
const mapObjectOriginY = ref(0)
|
||||
const mapObjectFrameRate = ref(0)
|
||||
const mapObjectFrameWidth = ref(0)
|
||||
const mapObjectFrameHeight = ref(0)
|
||||
const imageRef = ref<HTMLImageElement | null>(null)
|
||||
const showOrigin = ref(true)
|
||||
const showPartitionOverlay = ref(true)
|
||||
|
||||
if (!selectedMapObject.value) {
|
||||
console.error('No map mapObject selected')
|
||||
@ -102,7 +71,6 @@ if (!selectedMapObject.value) {
|
||||
if (selectedMapObject.value) {
|
||||
mapObjectName.value = selectedMapObject.value.name
|
||||
mapObjectTags.value = selectedMapObject.value.tags
|
||||
mapObjectDepthOffsets.value = selectedMapObject.value.depthOffsets
|
||||
mapObjectOriginX.value = selectedMapObject.value.originX
|
||||
mapObjectOriginY.value = selectedMapObject.value.originY
|
||||
mapObjectFrameRate.value = selectedMapObject.value.frameRate
|
||||
@ -110,23 +78,18 @@ if (selectedMapObject.value) {
|
||||
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
|
||||
}
|
||||
|
||||
const setPartitionDepth = (event: any, idx: number) => (mapObjectDepthOffsets.value[idx] = Number.parseInt(event.target.value))
|
||||
|
||||
async function removeObject() {
|
||||
if (!selectedMapObject.value) return
|
||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_REMOVE, { mapObjectId: selectedMapObject.value.id }, async (response: boolean) => {
|
||||
function removeObject() {
|
||||
gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_REMOVE, { mapObject: selectedMapObject.value?.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to remove mapObject')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('map_objects', new MapObjectStorage())
|
||||
await refreshObjectList()
|
||||
refreshObjectList()
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshObjectList(unsetSelectedMapObject = true) {
|
||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||
function refreshObjectList(unsetSelectedMapObject = true) {
|
||||
gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||
assetManagerStore.setMapObjectList(response)
|
||||
|
||||
if (unsetSelectedMapObject) {
|
||||
@ -135,32 +98,30 @@ async function refreshObjectList(unsetSelectedMapObject = true) {
|
||||
})
|
||||
}
|
||||
|
||||
async function saveObject() {
|
||||
function saveObject() {
|
||||
if (!selectedMapObject.value) {
|
||||
console.error('No mapObject selected')
|
||||
return
|
||||
}
|
||||
socketManager.emit(
|
||||
SocketEvent.GM_MAPOBJECT_UPDATE,
|
||||
|
||||
gameStore.connection?.emit(
|
||||
'gm:mapObject:update',
|
||||
{
|
||||
id: selectedMapObject.value.id,
|
||||
name: mapObjectName.value,
|
||||
tags: mapObjectTags.value,
|
||||
depthOffsets: mapObjectDepthOffsets.value,
|
||||
originX: mapObjectOriginX.value,
|
||||
originY: mapObjectOriginY.value,
|
||||
frameRate: mapObjectFrameRate.value,
|
||||
frameWidth: mapObjectFrameWidth.value,
|
||||
frameHeight: mapObjectFrameHeight.value
|
||||
},
|
||||
async (response: boolean) => {
|
||||
(response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save mapObject')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('map_objects', new MapObjectStorage())
|
||||
await refreshObjectList(false)
|
||||
refreshObjectList(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -169,7 +130,6 @@ watch(selectedMapObject, (mapObject: MapObject | null) => {
|
||||
if (!mapObject) return
|
||||
mapObjectName.value = mapObject.name
|
||||
mapObjectTags.value = mapObject.tags
|
||||
mapObjectDepthOffsets.value = mapObject.depthOffsets
|
||||
mapObjectOriginX.value = mapObject.originX
|
||||
mapObjectOriginY.value = mapObject.originY
|
||||
mapObjectFrameRate.value = mapObject.frameRate
|
||||
@ -181,37 +141,7 @@ onMounted(() => {
|
||||
if (!selectedMapObject.value) return
|
||||
})
|
||||
|
||||
// function startDragging(index: number, event: MouseEvent) {
|
||||
// isDragging.value = true
|
||||
// draggedPointIndex.value = index
|
||||
//
|
||||
// const moveHandler = (e: MouseEvent) => {
|
||||
// if (!isDragging.value || !imageRef.value) return
|
||||
// const rect = imageRef.value.getBoundingClientRect()
|
||||
// mapObjectPivotPoints.value[draggedPointIndex.value] = {
|
||||
// x: e.clientX - rect.left,
|
||||
// y: e.clientY - rect.top
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// const upHandler = () => {
|
||||
// isDragging.value = false
|
||||
// draggedPointIndex.value = -1
|
||||
// window.removeEventListener('mousemove', moveHandler)
|
||||
// window.removeEventListener('mouseup', upHandler)
|
||||
// }
|
||||
//
|
||||
// window.addEventListener('mousemove', moveHandler)
|
||||
// window.addEventListener('mouseup', upHandler)
|
||||
// }
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
assetManagerStore.setSelectedMapObject(null)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pointer-events-none {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
@ -31,7 +31,6 @@
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { MapObject } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
@ -49,13 +48,13 @@ const elementToScroll = ref()
|
||||
const handleFileUpload = (e: Event) => {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files) return
|
||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_UPLOAD, files, (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_UPLOAD, files, (response: boolean) => {
|
||||
if (!response) {
|
||||
if (config.environment === 'development') console.error('Failed to upload map object')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||
assetManagerStore.setMapObjectList(response)
|
||||
})
|
||||
})
|
||||
@ -94,7 +93,7 @@ function toTop() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||
assetManagerStore.setMapObjectList(response)
|
||||
})
|
||||
})
|
||||
|
@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="relative flex flex-col">
|
||||
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray mb-4">
|
||||
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray">
|
||||
<div class="w-full flex flex-col">
|
||||
<label class="mb-1.5 font-titles" for="name">Name</label>
|
||||
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
|
||||
</div>
|
||||
|
||||
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
||||
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
||||
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
||||
@ -14,39 +15,53 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-cyan px-4" type="button" @click.prevent="addNewImage">New action</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="action in spriteActions" :key="action.id">
|
||||
<div class="flex flex-wrap gap-3 mb-3">
|
||||
<div v-for="(image, index) in action.sprites" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group">
|
||||
<img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" />
|
||||
<div v-if="imageDimensions[index]" class="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-1 py-0.5 rounded transition-opacity font-default">{{ imageDimensions[index].width }}x{{ imageDimensions[index].height }}</div>
|
||||
|
||||
<button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button>
|
||||
<Accordion v-for="action in spriteActions" :key="action.id">
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
{{ action.action }}
|
||||
<div class="ml-auto space-x-2">
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="openPreviewModal(action)">View</button>
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="mr-3 space-x-2">
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24 text-left" type="button" @click.stop.prevent="openEditorModal(action)">
|
||||
Editor
|
||||
<div class="flex">
|
||||
<small class="text-xs font-default">{{ action.action }}</small>
|
||||
</template>
|
||||
<template #content>
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveSprite">
|
||||
<div class="form-field-full">
|
||||
<label for="action">Action</label>
|
||||
<input v-model="action.action" class="input-field" type="text" name="action" placeholder="Action" />
|
||||
</div>
|
||||
</button>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-x">Origin X</label>
|
||||
<input v-model.number="action.originX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-y">Origin Y</label>
|
||||
<input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="frame-speed">Frame rate</label>
|
||||
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||
</div>
|
||||
<SpriteEditor
|
||||
v-for="[actionId, editorData] in Array.from(openEditors.entries())"
|
||||
:key="actionId"
|
||||
:sprite="selectedSprite!"
|
||||
:sprites="editorData.action.sprites"
|
||||
:frame-rate="editorData.action.frameRate"
|
||||
:is-modal-open="editorData.isOpen"
|
||||
:temp-offset-index="getTempOffsetIndex(editorData.action)"
|
||||
:temp-offset="getTempOffset(editorData.action)"
|
||||
@update:frame-rate="(value) => updateFrameRate(editorData.action, value)"
|
||||
@update:is-modal-open="(value) => handleEditorModalClose(editorData.action, value)"
|
||||
@update:temp-offset="(index, offset) => handleTempOffsetChange(editorData.action, index, offset)"
|
||||
<div class="form-field-full">
|
||||
<SpriteActionsInput v-model="action.sprites" @tempOffsetChange="(index, offset) => handleTempOffsetChange(action, index, offset)" />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</Accordion>
|
||||
<SpritePreview
|
||||
v-if="selectedAction"
|
||||
:sprites="selectedAction.sprites"
|
||||
:frame-rate="selectedAction.frameRate"
|
||||
:is-modal-open="isModalOpen"
|
||||
:temp-offset-index="tempOffsetData.index"
|
||||
:temp-offset="tempOffsetData.offset"
|
||||
@update:frame-rate="updateFrameRate"
|
||||
@update:is-modal-open="isModalOpen = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -54,22 +69,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Sprite, SpriteAction } from '@/application/types'
|
||||
import { downloadCache, uuidv4 } from '@/application/utilities'
|
||||
import SpriteEditor from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteEditor.vue'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { SpriteStorage } from '@/storage/storages'
|
||||
import type { Sprite, SpriteAction, UUID } from '@/application/types'
|
||||
import { uuidv4 } from '@/application/utilities'
|
||||
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
||||
import SpritePreview from '@/components/gameMaster/assetManager/partials/sprite/partials/SpritePreview.vue'
|
||||
import Accordion from '@/components/utilities/Accordion.vue'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const selectedSprite = computed(() => assetManagerStore.selectedSprite)
|
||||
const tempOffsetData = ref<Map<string, { index: number | undefined; offset: { x: number; y: number } | undefined }>>(new Map())
|
||||
|
||||
const spriteName = ref('')
|
||||
const spriteActions = ref<SpriteAction[]>([])
|
||||
|
||||
const openEditors = ref(new Map<string, { action: SpriteAction; isOpen: boolean }>())
|
||||
const isModalOpen = ref(false)
|
||||
const selectedAction = ref<SpriteAction | null>(null)
|
||||
|
||||
if (!selectedSprite.value) {
|
||||
console.error('No sprite selected')
|
||||
@ -80,32 +97,28 @@ if (selectedSprite.value) {
|
||||
spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
|
||||
}
|
||||
|
||||
async function deleteSprite() {
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_DELETE, { id: selectedSprite.value?.id }, async (response: boolean) => {
|
||||
function deleteSprite() {
|
||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_DELETE, { id: selectedSprite.value?.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to delete sprite')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('sprites', new SpriteStorage())
|
||||
await refreshSpriteList()
|
||||
refreshSpriteList()
|
||||
})
|
||||
}
|
||||
|
||||
async function copySprite() {
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_COPY, { id: selectedSprite.value?.id }, async (response: boolean) => {
|
||||
function copySprite() {
|
||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_COPY, { id: selectedSprite.value?.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to copy sprite')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('sprites', new SpriteStorage())
|
||||
await refreshSpriteList(false)
|
||||
refreshSpriteList(false)
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshSpriteList(unsetSelectedSprite = true) {
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
function refreshSpriteList(unsetSelectedSprite = true) {
|
||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
|
||||
if (unsetSelectedSprite) {
|
||||
@ -114,7 +127,7 @@ async function refreshSpriteList(unsetSelectedSprite = true) {
|
||||
})
|
||||
}
|
||||
|
||||
async function saveSprite() {
|
||||
function saveSprite() {
|
||||
if (!selectedSprite.value) {
|
||||
console.error('No sprite selected')
|
||||
return
|
||||
@ -137,14 +150,12 @@ async function saveSprite() {
|
||||
}) ?? []
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_UPDATE, updatedSprite, async (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_UPDATE, updatedSprite, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save sprite')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('sprites', new SpriteStorage())
|
||||
await refreshSpriteList(false)
|
||||
refreshSpriteList(false)
|
||||
})
|
||||
}
|
||||
|
||||
@ -175,69 +186,39 @@ function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
|
||||
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
||||
}
|
||||
|
||||
function openEditorModal(action: SpriteAction) {
|
||||
const newOpenEditors = new Map(openEditors.value)
|
||||
newOpenEditors.set(action.id, { action, isOpen: true })
|
||||
openEditors.value = newOpenEditors
|
||||
function openPreviewModal(action: SpriteAction) {
|
||||
selectedAction.value = action
|
||||
isModalOpen.value = true
|
||||
}
|
||||
|
||||
function updateFrameRate(action: SpriteAction, value: number) {
|
||||
console.log('update frame rate', action)
|
||||
action.frameRate = value
|
||||
function updateFrameRate(value: number) {
|
||||
if (selectedAction.value) {
|
||||
selectedAction.value.frameRate = value
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditorModalClose(action: SpriteAction, isOpen: boolean) {
|
||||
if (isOpen) return
|
||||
const newOpenEditors = new Map(openEditors.value)
|
||||
newOpenEditors.delete(action.id)
|
||||
openEditors.value = newOpenEditors
|
||||
}
|
||||
const tempOffsetData = ref<{ index: number | undefined; offset: { x: number; y: number } | undefined }>({
|
||||
index: undefined,
|
||||
offset: undefined
|
||||
})
|
||||
|
||||
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
|
||||
// Update the temporary offset data for this action
|
||||
const newTempOffsetData = new Map(tempOffsetData.value)
|
||||
newTempOffsetData.set(action.id, { index, offset })
|
||||
tempOffsetData.value = newTempOffsetData
|
||||
|
||||
// Also update the actual sprite data so changes persist
|
||||
if (action.sprites && action.sprites[index]) {
|
||||
action.sprites[index].offset = { ...offset };
|
||||
if (selectedAction.value === action) {
|
||||
tempOffsetData.value = { index, offset }
|
||||
}
|
||||
}
|
||||
|
||||
function getTempOffsetIndex(action: SpriteAction): number | undefined {
|
||||
return tempOffsetData.value.get(action.id)?.index
|
||||
}
|
||||
|
||||
function getTempOffset(action: SpriteAction): { x: number; y: number } | undefined {
|
||||
return tempOffsetData.value.get(action.id)?.offset
|
||||
}
|
||||
|
||||
watch(selectedSprite, (sprite: Sprite | null) => {
|
||||
if (!sprite) return
|
||||
spriteName.value = sprite.name
|
||||
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
||||
openEditors.value = new Map()
|
||||
tempOffsetData.value = new Map() // Reset temp offset data when sprite changes
|
||||
})
|
||||
|
||||
interface SpriteImage {
|
||||
url: string
|
||||
offset: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
}
|
||||
|
||||
const imageDimensions = ref<{ [key: number]: { width: number; height: number } }>({})
|
||||
|
||||
const updateImageDimensions = (event: Event, index: number) => {
|
||||
const img = event.target as HTMLImageElement
|
||||
imageDimensions.value[index] = {
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight
|
||||
}
|
||||
watch(isModalOpen, (newValue) => {
|
||||
if (!newValue) {
|
||||
selectedAction.value = null
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!selectedSprite.value) return
|
||||
|
@ -27,7 +27,6 @@
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Sprite } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
@ -42,13 +41,13 @@ const hasScrolled = ref(false)
|
||||
const elementToScroll = ref()
|
||||
|
||||
function newButtonClickHandler() {
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_CREATE, {}, (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_CREATE, {}, (response: boolean) => {
|
||||
if (!response) {
|
||||
if (config.environment === 'development') console.error('Failed to create new sprite')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
@ -87,7 +86,7 @@ function toTop() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
|
@ -1,366 +0,0 @@
|
||||
<template>
|
||||
<Modal :is-modal-open="isModalOpen" :modal-width="700" :modal-height="330" :can-full-screen="true" bg-style="none" @modal:close="closeModal">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">Sprite editor</h3>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<div class="m-4 flex gap-4 h-full">
|
||||
<!-- Settings -->
|
||||
<div class="w-80 h-full flex flex-col overflow-y-auto">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col">
|
||||
<label class="block mb-1 text-white text-sm">Frame Rate: {{ frameRate }} FPS</label>
|
||||
<div class="text-xs font-default text-gray-400 mb-1">Duration: {{ totalDuration }}s</div>
|
||||
<input type="range" v-model.number="localFrameRate" min="0" max="60" step="1" class="w-full accent-cyan-500" @input="updateFrameRate" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label class="block mb-1 text-white text-sm">Frame: {{ currentFrame + 1 }} of {{ sprites.length }}</label>
|
||||
<input type="range" v-model.number="currentFrame" :min="0" :max="sprites.length - 1" step="1" class="w-full accent-cyan-500" @input="stopAnimation" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label class="block mb-1 text-white text-sm">Zoom: {{ zoomLevel }}%</label>
|
||||
<input type="range" v-model.number="zoomLevel" min="10" max="600" step="10" class="w-full accent-cyan-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 space-y-2">
|
||||
<button @click="toggleAnimation" class="px-3 py-1 bg-cyan-600 hover:bg-cyan-700 text-white rounded transition-colors w-full">
|
||||
{{ isAnimating ? 'Pause' : 'Play' }}
|
||||
</button>
|
||||
<button @click="toggleReferenceSprites" class="px-3 py-1 bg-cyan-600 hover:bg-cyan-700 text-white rounded transition-colors w-full">
|
||||
{{ showReferenceSprites ? 'Hide References' : 'Show References' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="">
|
||||
<div class="relative flex py-5 items-center">
|
||||
<div class="flex-grow border-t border-gray-400"></div>
|
||||
<span class="flex-shrink mx-4 text-gray-400">Sprite action</span>
|
||||
<div class="flex-grow border-solid border-gray-200"></div>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="action">Name</label>
|
||||
<input class="input-field" type="text" name="action" placeholder="Action" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-x">Origin X</label>
|
||||
<input class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-y">Origin Y</label>
|
||||
<input class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
</div>
|
||||
<div class="relative flex py-5 items-center">
|
||||
<div class="flex-grow border-t border-gray-400"></div>
|
||||
<span class="flex-shrink mx-4 text-gray-400">Sprite action image</span>
|
||||
<div class="flex-grow border-t border-gray-400"></div>
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="offset-x">Offset X</label>
|
||||
<input class="input-field" type="number" step="1" v-model.number="offsetXModel" :disabled="isAnimating" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="offset-y">Offset Y</label>
|
||||
<input class="input-field" type="number" step="1" v-model.number="offsetYModel" :disabled="isAnimating" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="frame-speed">Frame rate</label>
|
||||
<input class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sprite thumbnails -->
|
||||
<div class="flex-1 flex flex-col h-full">
|
||||
<div class="bg-gray-800 border-solid border-white/10 rounded flex-grow mb-2 relative overflow-hidden" @mousedown="startDrag" @mousemove="onDrag" @mouseup="stopDrag" @mouseleave="stopDrag">
|
||||
<!-- Background reference sprites (semi-transparent) -->
|
||||
<img
|
||||
v-for="(sprite, index) in spritesWithTempOffset"
|
||||
:key="`bg-${index}`"
|
||||
:src="sprite.url"
|
||||
alt="Reference sprite"
|
||||
v-show="index !== currentFrame && showReferenceSprites"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: `${(sprite.offset?.x || 0) * (zoomLevel / 100)}px`,
|
||||
bottom: `${(sprite.offset?.y || 0) * (zoomLevel / 100)}px`,
|
||||
opacity: 0.3,
|
||||
transform: `scale(${zoomLevel / 100})`,
|
||||
transformOrigin: 'bottom left',
|
||||
pointerEvents: 'none'
|
||||
}"
|
||||
/>
|
||||
<!-- Current sprite (draggable) -->
|
||||
<img
|
||||
v-for="(sprite, index) in spritesWithTempOffset"
|
||||
:key="index"
|
||||
:src="sprite.url"
|
||||
alt="Sprite"
|
||||
:class="{ 'cursor-move': currentFrame === index }"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: `${(sprite.offset?.x || 0) * (zoomLevel / 100)}px`,
|
||||
bottom: `${(sprite.offset?.y || 0) * (zoomLevel / 100)}px`,
|
||||
display: currentFrame === index ? 'block' : 'none',
|
||||
transform: `scale(${zoomLevel / 100})`,
|
||||
transformOrigin: 'bottom left',
|
||||
userSelect: 'none',
|
||||
pointerEvents: currentFrame === index ? 'auto' : 'none'
|
||||
}"
|
||||
@dragstart.prevent
|
||||
/>
|
||||
</div>
|
||||
<div class="bg-gray-800 p-2 overflow-x-auto border-solid border-white/10 rounded mb-8 h-24 min-h-16">
|
||||
<div class="flex gap-2">
|
||||
<div
|
||||
v-for="(sprite, index) in sprites"
|
||||
:key="`thumb-${index}`"
|
||||
class="relative cursor-pointer border-solid transition-all duration-200 rounded flex-shrink-0 p-3 px-12"
|
||||
:class="currentFrame === index ? 'border-cyan-600 bg-cyan-500/10' : 'border-transparent hover:border-white/30'"
|
||||
@click="selectFrame(index)"
|
||||
>
|
||||
<img :src="sprite.url" alt="Sprite thumbnail" class="h-16 w-auto object-contain rounded" />
|
||||
<div class="absolute top-0 right-0 bg-gray-400 text-white text-xs font-default px-1 rounded-bl">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Sprite, SpriteImage } from '@/application/types'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
sprite: Sprite
|
||||
sprites: SpriteImage[]
|
||||
frameRate: number
|
||||
isModalOpen?: boolean
|
||||
tempOffsetIndex?: number
|
||||
tempOffset?: { x: number; y: number }
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:frameRate', value: number): void
|
||||
(e: 'update:isModalOpen', value: boolean): void
|
||||
(e: 'update:tempOffset', index: number, offset: { x: number; y: number }): void
|
||||
}>()
|
||||
|
||||
const currentFrame = ref(0)
|
||||
const localFrameRate = ref(props.frameRate)
|
||||
const zoomLevel = ref(100)
|
||||
const isAnimating = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const startDragPos = ref({ x: 0, y: 0 })
|
||||
const currentOffset = ref({ x: 0, y: 0 })
|
||||
let animationInterval: number | null = null
|
||||
|
||||
const totalDuration = computed(() => {
|
||||
if (props.frameRate <= 0) return 0
|
||||
return (props.sprites.length / props.frameRate).toFixed(2)
|
||||
})
|
||||
|
||||
const spritesWithTempOffset = computed(() => {
|
||||
return props.sprites.map((sprite, index) => {
|
||||
if (index === props.tempOffsetIndex && props.tempOffset) {
|
||||
return { ...sprite, offset: props.tempOffset }
|
||||
}
|
||||
return sprite
|
||||
})
|
||||
})
|
||||
|
||||
const currentSprite = computed(() => {
|
||||
if (currentFrame.value >= 0 && currentFrame.value < spritesWithTempOffset.value.length) {
|
||||
return spritesWithTempOffset.value[currentFrame.value]
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// Create computed properties with getters and setters for two-way binding
|
||||
const offsetXModel = computed({
|
||||
get: () => currentSprite.value?.offset?.x || 0,
|
||||
set: (value) => {
|
||||
if (isAnimating.value) return
|
||||
const newOffset = {
|
||||
x: value,
|
||||
y: currentSprite.value?.offset?.y || 0
|
||||
}
|
||||
emit('update:tempOffset', currentFrame.value, newOffset)
|
||||
}
|
||||
})
|
||||
|
||||
const offsetYModel = computed({
|
||||
get: () => currentSprite.value?.offset?.y || 0,
|
||||
set: (value) => {
|
||||
if (isAnimating.value) return
|
||||
const newOffset = {
|
||||
x: currentSprite.value?.offset?.x || 0,
|
||||
y: value
|
||||
}
|
||||
emit('update:tempOffset', currentFrame.value, newOffset)
|
||||
}
|
||||
})
|
||||
|
||||
// Toggle for showing reference sprites
|
||||
const showReferenceSprites = ref(true)
|
||||
|
||||
function updateAnimation() {
|
||||
stopAnimation()
|
||||
if (props.frameRate <= 0 || props.sprites.length === 0) {
|
||||
currentFrame.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (isAnimating.value) {
|
||||
animationInterval = window.setInterval(() => {
|
||||
currentFrame.value = (currentFrame.value + 1) % props.sprites.length
|
||||
}, 1000 / props.frameRate)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAnimation() {
|
||||
isAnimating.value = !isAnimating.value
|
||||
if (isAnimating.value) {
|
||||
updateAnimation()
|
||||
} else {
|
||||
stopAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
function stopAnimation() {
|
||||
if (animationInterval) {
|
||||
clearInterval(animationInterval)
|
||||
animationInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
function selectFrame(index: number) {
|
||||
currentFrame.value = index
|
||||
stopAnimation()
|
||||
isAnimating.value = false
|
||||
}
|
||||
|
||||
function updateFrameRate() {
|
||||
emit('update:frameRate', localFrameRate.value)
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
emit('update:isModalOpen', false)
|
||||
}
|
||||
|
||||
function startDrag(event: MouseEvent) {
|
||||
if (isAnimating.value) return
|
||||
|
||||
const previewContainer = event.currentTarget as HTMLElement
|
||||
const rect = previewContainer.getBoundingClientRect()
|
||||
|
||||
// Store initial mouse position
|
||||
startDragPos.value = {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
}
|
||||
|
||||
// Store current offset
|
||||
if (currentSprite.value && currentSprite.value.offset) {
|
||||
currentOffset.value = {
|
||||
x: currentSprite.value.offset.x,
|
||||
y: currentSprite.value.offset.y
|
||||
}
|
||||
} else {
|
||||
currentOffset.value = { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
function onDrag(event: MouseEvent) {
|
||||
if (!isDragging.value) return
|
||||
|
||||
// Calculate the difference from the start position
|
||||
const deltaX = event.clientX - startDragPos.value.x
|
||||
const deltaY = startDragPos.value.y - event.clientY // Inverted for bottom positioning
|
||||
|
||||
// Apply the zoom factor to the delta
|
||||
// This ensures that the movement in screen pixels is converted to the correct
|
||||
// number of pixels at the sprite's natural size, regardless of zoom level
|
||||
const zoomFactor = 100 / zoomLevel.value
|
||||
const scaledDeltaX = deltaX * zoomFactor
|
||||
const scaledDeltaY = deltaY * zoomFactor
|
||||
|
||||
// Calculate new offset
|
||||
// These offsets are in the sprite's natural coordinate space (as if zoom was 100%)
|
||||
const newOffset = {
|
||||
x: currentOffset.value.x + scaledDeltaX,
|
||||
y: currentOffset.value.y + scaledDeltaY
|
||||
}
|
||||
|
||||
// Emit the new offset
|
||||
emit('update:tempOffset', currentFrame.value, newOffset)
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
if (isDragging.value && currentSprite.value?.offset) {
|
||||
// Ensure the final offset is applied when dragging stops
|
||||
emit('update:tempOffset', currentFrame.value, {
|
||||
x: currentSprite.value.offset.x,
|
||||
y: currentSprite.value.offset.y
|
||||
})
|
||||
}
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
function toggleReferenceSprites() {
|
||||
showReferenceSprites.value = !showReferenceSprites.value
|
||||
}
|
||||
|
||||
function updateOffset(event: Event, axis: 'x' | 'y') {
|
||||
if (isAnimating.value) return
|
||||
|
||||
const input = event.target as HTMLInputElement
|
||||
const value = parseInt(input.value) || 0
|
||||
|
||||
if (currentSprite.value && currentSprite.value.offset) {
|
||||
const newOffset = { ...currentSprite.value.offset }
|
||||
newOffset[axis] = value
|
||||
emit('update:tempOffset', currentFrame.value, newOffset)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.frameRate,
|
||||
(newValue) => {
|
||||
localFrameRate.value = newValue
|
||||
updateAnimation()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(() => props.sprites, updateAnimation, { immediate: true })
|
||||
|
||||
watch(
|
||||
() => isAnimating.value,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
updateAnimation()
|
||||
} else {
|
||||
stopAnimation()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
isAnimating.value = props.frameRate > 0
|
||||
if (isAnimating.value) {
|
||||
updateAnimation()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAnimation()
|
||||
})
|
||||
</script>
|
@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
|
||||
<img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" />
|
||||
<div v-if="imageDimensions[index]" class="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-1 py-0.5 rounded transition-opacity font-default">{{ imageDimensions[index].width }}x{{ imageDimensions[index].height }}</div>
|
||||
<div class="absolute top-1 left-1 flex-row space-y-1">
|
||||
<button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<button @click.stop="openOffsetModal(index)" class="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Scope image">
|
||||
<svg width="50px" height="50px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.29289 3.70711L1 11V15H5L12.2929 7.70711L8.29289 3.70711Z" fill="white" />
|
||||
<path d="M9.70711 2.29289L13.7071 6.29289L15.1716 4.82843C15.702 4.29799 16 3.57857 16 2.82843C16 1.26633 14.7337 0 13.1716 0C12.4214 0 11.702 0.297995 11.1716 0.828428L9.70711 2.29289Z" fill="white" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Modal :is-modal-open="selectedImageIndex === index" :modal-width="300" :modal-height="210" :is-resizable="false" bg-style="none" @modal:close="closeOffsetModal">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">Action offset ({{ selectedImageIndex }})</h3>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<div class="m-4">
|
||||
<form method="post" @submit.prevent="saveOffset(index)" class="inline">
|
||||
<div class="gap-2.5 flex flex-wrap">
|
||||
<div class="form-field-half">
|
||||
<label for="offsetX">Offset X</label>
|
||||
<input class="input-field max-w-64" v-model="tempOffset.x" name="offsetX" id="offsetX" type="number" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="offsetY">Offset Y</label>
|
||||
<input class="input-field max-w-64" v-model="tempOffset.y" name="offsetY" id="offsetY" type="number" />
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
<div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" ref="fileInput" @change="onFileChange" multiple accept="image/png" class="hidden" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
interface SpriteImage {
|
||||
url: string
|
||||
offset: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: SpriteImage[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: SpriteImage[]): void
|
||||
(e: 'close'): void
|
||||
(e: 'tempOffsetChange', index: number, offset: { x: number; y: number }): void
|
||||
}>()
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const draggedIndex = ref<number | null>(null)
|
||||
const selectedImageIndex = ref<number | null>(null)
|
||||
const tempOffset = ref({ x: 0, y: 0 })
|
||||
|
||||
const triggerFileInput = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const onFileChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (target.files) {
|
||||
handleFiles(target.files)
|
||||
}
|
||||
}
|
||||
|
||||
const onDrop = (event: DragEvent) => {
|
||||
if (event.dataTransfer?.files) {
|
||||
handleFiles(event.dataTransfer.files)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFiles = (files: FileList) => {
|
||||
Array.from(files).forEach((file) => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (typeof e.target?.result === 'string') {
|
||||
const newImage: SpriteImage = {
|
||||
url: e.target.result,
|
||||
offset: { x: 0, y: 0 }
|
||||
}
|
||||
updateImages([...props.modelValue, newImage])
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
const updateImages = (newImages: SpriteImage[]) => {
|
||||
emit('update:modelValue', newImages)
|
||||
}
|
||||
|
||||
const deleteImage = (index: number) => {
|
||||
const newImages = [...props.modelValue]
|
||||
newImages.splice(index, 1)
|
||||
updateImages(newImages)
|
||||
}
|
||||
|
||||
const dragStart = (event: DragEvent, index: number) => {
|
||||
draggedIndex.value = index
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
}
|
||||
|
||||
const drop = (event: DragEvent, dropIndex: number) => {
|
||||
event.preventDefault()
|
||||
if (draggedIndex.value !== null && draggedIndex.value !== dropIndex) {
|
||||
const newImages = [...props.modelValue]
|
||||
const [reorderedItem] = newImages.splice(draggedIndex.value, 1)
|
||||
newImages.splice(dropIndex, 0, reorderedItem)
|
||||
updateImages(newImages)
|
||||
}
|
||||
draggedIndex.value = null
|
||||
}
|
||||
|
||||
const openOffsetModal = (index: number) => {
|
||||
selectedImageIndex.value = index
|
||||
tempOffset.value = { ...props.modelValue[index].offset }
|
||||
}
|
||||
|
||||
const closeOffsetModal = () => {
|
||||
selectedImageIndex.value = null
|
||||
}
|
||||
|
||||
const saveOffset = (index: number) => {
|
||||
const newImages = [...props.modelValue]
|
||||
newImages[index] = {
|
||||
...newImages[index],
|
||||
offset: { ...tempOffset.value }
|
||||
}
|
||||
updateImages(newImages)
|
||||
closeOffsetModal()
|
||||
}
|
||||
|
||||
const onOffsetChange = () => {
|
||||
if (selectedImageIndex.value !== null) {
|
||||
emit('tempOffsetChange', selectedImageIndex.value, tempOffset.value)
|
||||
}
|
||||
}
|
||||
|
||||
watch(tempOffset, onOffsetChange, { deep: true })
|
||||
|
||||
const imageDimensions = ref<{ [key: number]: { width: number; height: number } }>({})
|
||||
|
||||
const updateImageDimensions = (event: Event, index: number) => {
|
||||
const img = event.target as HTMLImageElement
|
||||
imageDimensions.value[index] = {
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<Modal :is-modal-open="isModalOpen" :modal-width="700" :modal-height="330" bg-style="none" @modal:close="closeModal">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">View sprite</h3>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<div class="m-4 flex gap-8">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="sprite-container bg-gray-800"
|
||||
:style="{
|
||||
width: `${maxWidth}px`,
|
||||
height: `${maxHeight}px`,
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-for="(sprite, index) in spritesWithTempOffset"
|
||||
:key="index"
|
||||
:src="sprite.url"
|
||||
alt="Sprite"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: `${sprite.offset?.x || 0}px`,
|
||||
bottom: `${sprite.offset?.y || 0}px`,
|
||||
display: currentFrame === index ? 'block' : 'none',
|
||||
transform: `scale(${zoomLevel / 100})`,
|
||||
transformOrigin: 'bottom left'
|
||||
}"
|
||||
@load="updateContainerSize"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center gap-8 flex-1">
|
||||
<div class="flex flex-col">
|
||||
<label class="block mb-2 text-white">Frame Rate: {{ frameRate }} FPS (Duration: {{ totalDuration }}s)</label>
|
||||
<input type="range" v-model.number="localFrameRate" min="0" max="60" step="1" class="w-full accent-cyan-500" @input="updateFrameRate" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label class="block mb-2 text-white">Frame: {{ currentFrame + 1 }} of {{ sprites.length }}</label>
|
||||
<input type="range" v-model.number="currentFrame" :min="0" :max="sprites.length - 1" step="1" class="w-full accent-cyan-500" @input="stopAnimation" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label class="block mb-2 text-white">Zoom: {{ zoomLevel }}%</label>
|
||||
<input type="range" v-model.number="zoomLevel" min="10" max="200" step="10" class="w-full accent-cyan-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SpriteImage } from '@/application/types'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
sprites: SpriteImage[]
|
||||
frameRate: number
|
||||
isModalOpen?: boolean
|
||||
tempOffsetIndex?: number
|
||||
tempOffset?: { x: number; y: number }
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:frameRate', value: number): void
|
||||
(e: 'update:isModalOpen', value: boolean): void
|
||||
}>()
|
||||
|
||||
const currentFrame = ref(0)
|
||||
const maxWidth = ref(250)
|
||||
const maxHeight = ref(250)
|
||||
const localFrameRate = ref(props.frameRate)
|
||||
const zoomLevel = ref(100)
|
||||
let animationInterval: number | null = null
|
||||
|
||||
const totalDuration = computed(() => {
|
||||
if (props.frameRate <= 0) return 0
|
||||
return (props.sprites.length / props.frameRate).toFixed(2)
|
||||
})
|
||||
|
||||
const spritesWithTempOffset = computed(() => {
|
||||
return props.sprites.map((sprite, index) => {
|
||||
if (index === props.tempOffsetIndex && props.tempOffset) {
|
||||
return { ...sprite, offset: props.tempOffset }
|
||||
}
|
||||
return sprite
|
||||
})
|
||||
})
|
||||
|
||||
function updateContainerSize(event: Event) {
|
||||
const img = event.target as HTMLImageElement
|
||||
maxWidth.value = Math.max(maxWidth.value, img.naturalWidth)
|
||||
maxHeight.value = Math.max(maxHeight.value, img.naturalHeight)
|
||||
}
|
||||
|
||||
function updateAnimation() {
|
||||
stopAnimation()
|
||||
if (props.frameRate <= 0 || props.sprites.length === 0) {
|
||||
currentFrame.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
animationInterval = window.setInterval(() => {
|
||||
currentFrame.value = (currentFrame.value + 1) % props.sprites.length
|
||||
}, 1000 / props.frameRate)
|
||||
}
|
||||
|
||||
function stopAnimation() {
|
||||
if (animationInterval) {
|
||||
clearInterval(animationInterval)
|
||||
animationInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
function updateFrameRate() {
|
||||
emit('update:frameRate', localFrameRate.value)
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
emit('update:isModalOpen', false)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.frameRate,
|
||||
(newValue) => {
|
||||
localFrameRate.value = newValue
|
||||
updateAnimation()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(() => props.sprites, updateAnimation, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
updateAnimation()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAnimation()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sprite-container {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
@ -26,14 +26,15 @@
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Tile } from '@/application/types'
|
||||
import { downloadCache } from '@/application/utilities'
|
||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { TileStorage } from '@/storage/storages'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
const tileStorage = new TileStorage()
|
||||
|
||||
const selectedTile = computed(() => assetManagerStore.selectedTile)
|
||||
|
||||
@ -56,19 +57,18 @@ watch(selectedTile, (tile: Tile | null) => {
|
||||
})
|
||||
|
||||
async function deleteTile() {
|
||||
socketManager.emit(SocketEvent.GM_TILE_DELETE, { id: selectedTile.value?.id }, async (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_TILE_DELETE, { id: selectedTile.value?.id }, async (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to delete tile')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('tiles', new TileStorage())
|
||||
await refreshTileList()
|
||||
await tileStorage.delete(selectedTile.value!.id)
|
||||
refreshTileList()
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshTileList(unsetSelectedTile = true) {
|
||||
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||
function refreshTileList(unsetSelectedTile = true) {
|
||||
gameStore.connection?.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||
assetManagerStore.setTileList(response)
|
||||
|
||||
if (unsetSelectedTile) {
|
||||
@ -77,27 +77,25 @@ async function refreshTileList(unsetSelectedTile = true) {
|
||||
})
|
||||
}
|
||||
|
||||
async function saveTile() {
|
||||
function saveTile() {
|
||||
if (!selectedTile.value) {
|
||||
console.error('No tile selected')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(
|
||||
gameStore.connection?.emit(
|
||||
'gm:tile:update',
|
||||
{
|
||||
id: selectedTile.value.id,
|
||||
name: tileName.value,
|
||||
tags: tileTags.value
|
||||
},
|
||||
async (response: boolean) => {
|
||||
(response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save tile')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('tiles', new TileStorage())
|
||||
await refreshTileList(false)
|
||||
refreshTileList(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -31,7 +31,6 @@
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Tile } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
@ -49,13 +48,13 @@ const elementToScroll = ref()
|
||||
const handleFileUpload = (e: Event) => {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files) return
|
||||
socketManager.emit(SocketEvent.GM_TILE_UPLOAD, files, (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_TILE_UPLOAD, files, (response: boolean) => {
|
||||
if (!response) {
|
||||
if (config.environment === 'development') console.error('Failed to upload tile')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||
assetManagerStore.setTileList(response)
|
||||
})
|
||||
})
|
||||
@ -94,7 +93,7 @@ function toTop() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||
assetManagerStore.setTileList(response)
|
||||
})
|
||||
})
|
||||
|
@ -10,9 +10,9 @@ import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEven
|
||||
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
|
||||
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { cloneArray, createTileLayer, createTileMap, placeTiles } from '@/services/mapService'
|
||||
import { cloneArray, createTileArray, createTileLayer, createTileMap, placeTiles } from '@/services/mapService'
|
||||
import { TileStorage } from '@/storage/storages'
|
||||
import { useRefHistory } from '@vueuse/core'
|
||||
import { useManualRefHistory, useRefHistory } from '@vueuse/core'
|
||||
import { useScene } from 'phavuer'
|
||||
import { onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
|
||||
|
||||
@ -32,7 +32,7 @@ let commandIndex = ref(0)
|
||||
|
||||
let originTiles: string[][] = []
|
||||
let originEventTiles: MapEventTile[] = []
|
||||
let originObjects = ref<PlacedMapObjectT[]>(mapEditor.currentMap.value?.placedMapObjects ?? [])
|
||||
let originObjects = ref<PlacedMapObjectT[]>(mapEditor.currentMap.value.placedMapObjects)
|
||||
|
||||
const { undo, redo, commit, pause, resume, canUndo, canRedo } = useRefHistory(originObjects, { deep: true, capacity: 9 })
|
||||
|
||||
@ -58,7 +58,6 @@ watch(
|
||||
mapTiles.value!.clearTiles()
|
||||
eventTiles.value!.clearTiles()
|
||||
mapEditor.currentMap.value.placedMapObjects = []
|
||||
mapEditor.currentMap.value.mapEventTiles = []
|
||||
updateAndCommit(mapEditor.currentMap.value)
|
||||
mapEditor.resetClearTilesFlag()
|
||||
}
|
||||
@ -235,10 +234,6 @@ onUnmounted(() => {
|
||||
mapEditor.reset()
|
||||
})
|
||||
|
||||
setInterval(() => {
|
||||
scene.children.queueDepthSort()
|
||||
}, 0.2)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
@ -46,9 +46,7 @@ class EventTileCommand implements EditorCommand {
|
||||
}
|
||||
}
|
||||
|
||||
function createCommandUpdate(tile?: MapEventTile | null, operation: 'draw' | 'erase' | 'clear' = 'draw') {
|
||||
if (!tile) return
|
||||
|
||||
function createCommandUpdate(tile?: MapEventTile, operation: 'draw' | 'erase' | 'clear') {
|
||||
if (!currentCommand) {
|
||||
currentCommand = new EventTileCommand(operation)
|
||||
}
|
||||
@ -88,17 +86,19 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
||||
if (existingEventTile) return
|
||||
|
||||
// If teleport, check if there is a selected map
|
||||
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMap) return
|
||||
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMapId) return
|
||||
|
||||
const newEventTile = {
|
||||
id: uuidv4() as UUID,
|
||||
mapId: map.id,
|
||||
map: map,
|
||||
type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
|
||||
positionX: tile.x,
|
||||
positionY: tile.y,
|
||||
teleport:
|
||||
mapEditor.drawMode.value === 'teleport'
|
||||
? {
|
||||
toMap: mapEditor.teleportSettings.value.toMap,
|
||||
toMap: mapEditor.teleportSettings.value.toMapId,
|
||||
toPositionX: mapEditor.teleportSettings.value.toPositionX,
|
||||
toPositionY: mapEditor.teleportSettings.value.toPositionY,
|
||||
toRotation: mapEditor.teleportSettings.value.toRotation
|
||||
@ -106,9 +106,9 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
||||
: undefined
|
||||
}
|
||||
|
||||
createCommandUpdate(newEventTile as MapEventTile, 'draw')
|
||||
createCommandUpdate(newEventTile, 'draw')
|
||||
|
||||
map.mapEventTiles.push(newEventTile as MapEventTile)
|
||||
map.mapEventTiles.push(newEventTile)
|
||||
}
|
||||
|
||||
function erase(pointer: Phaser.Input.Pointer, map: MapT) {
|
||||
@ -149,7 +149,7 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
|
||||
}
|
||||
|
||||
function clearTiles() {
|
||||
if (mapEditor.currentMap.value?.mapEventTiles.length === 0) return
|
||||
if (mapEditor.currentMap.value.mapEventTiles.length === 0) return
|
||||
createCommandUpdate(null, 'clear')
|
||||
finalizeCommand()
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
:placedMapObject="previewPlacedMapObject as PlacedMapObjectT"
|
||||
/>
|
||||
<SelectedPlacedMapObjectComponent v-if="mapEditor.selectedPlacedObject.value" :key="mapEditor.selectedPlacedObject.value.id" :map :placedMapObject="mapEditor.selectedPlacedObject.value" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
|
||||
<PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap :tileMapLayer :placedMapObject @pointerdown="clickPlacedMapObject(placedMapObject)" :key="`${placedMapObject.id}-${placedMapObjectKey}`" />
|
||||
<PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap :tileMapLayer :placedMapObject @pointerdown="clickPlacedMapObject(placedMapObject)" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -26,7 +26,6 @@ import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||
const scene = useScene()
|
||||
const mapEditor = useMapEditorComposable()
|
||||
const map = computed(() => mapEditor.currentMap.value!)
|
||||
const placedMapObjectKey = computed(() => mapEditor.refreshMapObject.value)
|
||||
|
||||
const emit = defineEmits<{ (e: 'update', map: MapT): void; (e: 'updateAndCommit', map: MapT): void; (e: 'pauseObjectTracking'): void; (e: 'resumeObjectTracking'): void }>()
|
||||
|
||||
@ -130,7 +129,7 @@ function moveMapObject(id: string, map: MapT) {
|
||||
|
||||
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
||||
if (!tile) return
|
||||
|
||||
console.log(id)
|
||||
map.placedMapObjects.map((placed) => {
|
||||
if (placed.id === id) {
|
||||
placed.positionX = tile.x
|
||||
|
@ -39,7 +39,6 @@ import { SocketEvent } from '@/application/enums'
|
||||
import type { Map } from '@/application/types'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { MapStorage } from '@/storage/storages'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { ref, useTemplateRef } from 'vue'
|
||||
@ -58,7 +57,7 @@ const pvp = ref(false)
|
||||
defineExpose({ open: () => modalRef.value?.open() })
|
||||
|
||||
async function submit() {
|
||||
socketManager.emit(SocketEvent.GM_MAP_CREATE, { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_MAP_CREATE, { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => {
|
||||
if (!response) {
|
||||
return
|
||||
}
|
||||
|
@ -1,15 +1,17 @@
|
||||
<template>
|
||||
<Modal ref="modalRef" :is-resizable="false" :modal-width="300" :modal-height="360" bg-style="none">
|
||||
<template #modalHeader>
|
||||
<div class="flex items-center">
|
||||
<button class="btn-cyan w-7 h-7 font-normal flex items-center justify-center" @click="createMapModal?.open">+</button>
|
||||
<h3 class="text-lg text-white ml-2">Maps</h3>
|
||||
</div>
|
||||
<h3 class="text-lg text-white">Maps</h3>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<div class="mx-auto h-full">
|
||||
<div class="overflow-y-auto h-[calc(100%)]">
|
||||
<div class="my-4 mx-auto h-full">
|
||||
<div class="text-center mb-4 px-2 flex gap-2.5">
|
||||
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchMaps">Refresh</button>
|
||||
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="createMapModal?.open">New</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto h-[calc(100%-20px)]">
|
||||
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(map, index) in mapList" :key="map.id">
|
||||
<div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div>
|
||||
<div class="flex gap-3 items-center w-full" @click="() => loadMap(map.id)">
|
||||
<span>{{ map.name }}</span>
|
||||
<span class="ml-auto gap-1 flex">
|
||||
@ -22,6 +24,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<CreateMap ref="createMapModal" @create="fetchMaps" />
|
||||
</template>
|
||||
|
||||
@ -31,7 +34,6 @@ import type { Map } from '@/application/types'
|
||||
import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { MapStorage } from '@/storage/storages'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onMounted, ref, useTemplateRef } from 'vue'
|
||||
@ -59,14 +61,14 @@ async function fetchMaps() {
|
||||
}
|
||||
|
||||
function loadMap(id: string) {
|
||||
socketManager.emit(SocketEvent.GM_MAP_REQUEST, { mapId: id }, (response: Map) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_MAP_REQUEST, { mapId: id }, (response: Map) => {
|
||||
mapEditor.loadMap(response)
|
||||
})
|
||||
modalRef.value?.close()
|
||||
}
|
||||
|
||||
async function deleteMap(id: string) {
|
||||
socketManager.emit(SocketEvent.GM_MAP_DELETE, { mapId: id }, async (response: boolean) => {
|
||||
gameStore.connection?.emit(SocketEvent.GM_MAP_DELETE, { mapId: id }, async (response: boolean) => {
|
||||
if (!response) {
|
||||
gameStore.addNotification({
|
||||
title: 'Error',
|
||||
|
@ -1,16 +1,11 @@
|
||||
<template>
|
||||
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object'">
|
||||
<div class="flex flex-col gap-2.5 p-2.5">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex-grow">
|
||||
<div class="relative flex">
|
||||
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
||||
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
|
||||
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
||||
</div>
|
||||
</div>
|
||||
<img src="/assets/icons/mapEditor/dropdown-chevron.svg" class="w-12 h-12 ml-2 cursor-pointer hover:opacity-80 -rotate-90" alt="Close" @click="mapEditor.setTool('move')" />
|
||||
</div>
|
||||
<div class="flex">
|
||||
<select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
|
||||
<option value="tile">Tiles</option>
|
||||
|
@ -45,7 +45,6 @@ import { SocketEvent } from '@/application/enums'
|
||||
import type { MapObject, Map as MapT, PlacedMapObject } from '@/application/types'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { MapObjectStorage } from '@/storage/storages'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onMounted, ref } from 'vue'
|
||||
@ -82,7 +81,7 @@ const handleDelete = () => {
|
||||
async function handleUpdate() {
|
||||
if (!mapObject.value) return
|
||||
|
||||
socketManager.emit(
|
||||
gameStore.connection?.emit(
|
||||
SocketEvent.GM_MAPOBJECT_UPDATE,
|
||||
{
|
||||
id: props.placedMapObject.mapObject as string,
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Modal v-if="showTeleportModal" ref="modalRef" @modal:close="() => mapEditor.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" bg-style="none">
|
||||
<Modal ref="modalRef" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" bg-style="none">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
||||
</template>
|
||||
@ -28,7 +28,7 @@
|
||||
<label for="toMap">Map to teleport to</label>
|
||||
<select v-model="toMap" class="input-field" name="toMap" id="toMap">
|
||||
<option :value="null">Select map</option>
|
||||
<option v-for="map in mapList" :key="map.id" :value="map.id">{{ map.name }}</option>
|
||||
<option v-for="map in mapList" :key="map.id" :value="map">{{ map.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -39,50 +39,51 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Map } from '@/application/types'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { MapStorage } from '@/storage/storages'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
const showTeleportModal = computed(() => mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'teleport')
|
||||
const mapStorage = new MapStorage()
|
||||
const mapEditor = useMapEditorComposable()
|
||||
const modalRef = useTemplateRef('modalRef')
|
||||
const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport')
|
||||
const mapEditorStore = useMapEditorStore()
|
||||
const gameStore = useGameStore()
|
||||
const mapList = ref<Map[]>([])
|
||||
const modalRef = useTemplateRef('modalRef')
|
||||
|
||||
defineExpose({
|
||||
open: () => modalRef.value?.open()
|
||||
})
|
||||
|
||||
onMounted(fetchMaps)
|
||||
|
||||
function fetchMaps() {
|
||||
// gameStore.connection?.emit(SocketEvent.GM_MAP_LIST, {}, (response: Map[]) => {
|
||||
// mapList.value = response
|
||||
// })
|
||||
}
|
||||
|
||||
const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings()
|
||||
|
||||
function useRefTeleportSettings() {
|
||||
const settings = mapEditor.teleportSettings.value
|
||||
const settings = mapEditorStore.teleportSettings
|
||||
return {
|
||||
toPositionX: ref(settings.toPositionX),
|
||||
toPositionY: ref(settings.toPositionY),
|
||||
toRotation: ref(settings.toRotation),
|
||||
toMap: ref(settings.toMap)
|
||||
toMap: ref(settings.toMapId)
|
||||
}
|
||||
}
|
||||
|
||||
watch([toPositionX, toPositionY, toRotation, toMap], updateTeleportSettings)
|
||||
|
||||
function updateTeleportSettings() {
|
||||
mapEditor.setTeleportSettings({
|
||||
mapEditorStore.setTeleportSettings({
|
||||
toPositionX: toPositionX.value,
|
||||
toPositionY: toPositionY.value,
|
||||
toRotation: toRotation.value,
|
||||
toMap: toMap.value
|
||||
toMapId: toMap.value
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchMaps() {
|
||||
mapList.value = await mapStorage.getAll()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchMaps()
|
||||
})
|
||||
</script>
|
||||
|
@ -1,16 +1,11 @@
|
||||
<template>
|
||||
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="(mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'tile') || mapEditor.tool.value === 'paint'">
|
||||
<div class="flex flex-col gap-2.5 p-2.5">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex-grow">
|
||||
<div class="relative flex">
|
||||
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
||||
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
|
||||
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
||||
</div>
|
||||
</div>
|
||||
<img src="/assets/icons/mapEditor/dropdown-chevron.svg" class="w-12 h-12 ml-2 cursor-pointer hover:opacity-80 -rotate-90" alt="Close" @click="mapEditor.setTool('move')" />
|
||||
</div>
|
||||
<div class="flex">
|
||||
<select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
|
||||
<option value="tile">Tiles</option>
|
||||
|
@ -117,7 +117,7 @@ const selectPencilOpen = ref(false)
|
||||
const selectEraserOpen = ref(false)
|
||||
const isContinuousDrawingEnabled = ref<Boolean>(false)
|
||||
const isShowPlacedMapObjectPreviewEnabled = ref<Boolean>(mapEditor.isPlacedMapObjectPreviewEnabled.value)
|
||||
const listOpen = computed(() => (mapEditor.tool.value === 'pencil' && (mapEditor.drawMode.value === 'tile' || mapEditor.drawMode.value === 'map_object')) || mapEditor.tool.value === 'paint')
|
||||
const listOpen = computed(() => mapEditor.tool.value === 'pencil' && (mapEditor.drawMode.value === 'tile' || mapEditor.drawMode.value === 'map_object'))
|
||||
|
||||
// drawMode
|
||||
function setDrawMode(value: string) {
|
||||
@ -148,7 +148,6 @@ function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
|
||||
|
||||
function handleClick(tool: string) {
|
||||
mapEditor.setTool(tool)
|
||||
if (tool === 'paint') mapEditor.setDrawMode('tile')
|
||||
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
|
||||
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
|
||||
if (mapEditor.drawMode.value === 'teleport') emit('open-teleport')
|
||||
|
@ -26,7 +26,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { login } from '@/services/authenticationService'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
@ -54,7 +53,7 @@ async function submit() {
|
||||
formError.value = response.error
|
||||
return
|
||||
}
|
||||
socketManager.setToken(response.token)
|
||||
gameStore.setToken(response.token)
|
||||
gameStore.initConnection()
|
||||
return true // Indicate success
|
||||
}
|
||||
|
@ -26,7 +26,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { login, register } from '@/services/authenticationService'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
@ -68,7 +67,7 @@ async function submit() {
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.setToken(loginResponse.token)
|
||||
gameStore.setToken(loginResponse.token)
|
||||
gameStore.initConnection()
|
||||
}
|
||||
</script>
|
||||
|
@ -10,9 +10,8 @@
|
||||
<div class="filler"></div>
|
||||
<div class="w-2/3 max-w-[860px]" v-if="!isLoading">
|
||||
<div class="mb-5 flex flex-col gap-1">
|
||||
<h1 class="text-white font-bold">{{ isCreatingCharacter ? 'CREATE CHARACTER' : 'SELECT CHARACTER TO PLAY' }}</h1>
|
||||
<p class="m-0" v-if="!isCreatingCharacter">Maximum of 4 characters can be created per player</p>
|
||||
<p class="m-0" v-if="isCreatingCharacter">Customize your new character</p>
|
||||
<h1 class="text-white font-bold">SELECT CHARACTER TO PLAY</h1>
|
||||
<p class="m-0">Maximum of 4 characters can be created per player</p>
|
||||
</div>
|
||||
<div class="flex w-full max-lg:flex-col lg:h-[400px] default-border rounded-md bg-gray">
|
||||
<div class="lg:min-w-[285px] max-lg:min-h-[383px] lg:w-1/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center border-0 max-lg:border-b lg:border-r border-solid border-gray-500 max-lg:rounded-t-md lg:rounded-l-md relative">
|
||||
@ -21,15 +20,14 @@
|
||||
<img src="/assets/placeholders/head.png" class="w-9 h-9 object-contain center-element" alt="Player head" />
|
||||
<input class="h-full w-full absolute m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 btn-sound" type="radio" name="character" :value="character.id" v-model="selectedCharacterId" />
|
||||
</div>
|
||||
<div class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')]" :class="{ active: isCreatingCharacter }" v-if="characters.length < characterCreationSettings.maxCharacters">
|
||||
<button class="p-0 h-full w-full flex flex-col justify-between focus-visible:outline-offset-0 btn-sound" @click="startCharacterCreation">
|
||||
<img class="w-6 h-6 object-contain center-element btn-sound" draggable="false" src="/assets/icons/plus-icon.svg" alt="Add character" />
|
||||
<div class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')]" :class="{ active: characters.length == 0 }" v-if="characters.length < 4">
|
||||
<button class="p-0 h-full w-full flex flex-col justify-between focus-visible:outline-offset-0 btn-sound" @click="isCreateNewCharacterModalOpen = true">
|
||||
<img class="w-6 h-6 object-contain center-element btn-sound" draggable="false" src="/assets/icons/plus-icon.svg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6 justify-center">
|
||||
<template v-if="selectedCharacterId && !isCreatingCharacter">
|
||||
<input class="input-field w-[158px]" type="text" name="name" :placeholder="characters.find((c) => c.id == selectedCharacterId)?.name" v-model="newNickname" />
|
||||
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6 justify-center" v-if="selectedCharacterId">
|
||||
<input class="input-field w-[158px]" type="text" name="name" :placeholder="characters.find((c) => c.id == selectedCharacterId)?.name" />
|
||||
<div class="flex flex-col gap-4 items-center">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="bg-[url('/assets/ui-elements/character-select-ui-shape.svg')] w-[190px] h-52 bg-no-repeat bg-center flex items-center justify-between">
|
||||
@ -42,74 +40,48 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO: update gender on (selected) character -->
|
||||
<!-- <div class="flex justify-between w-[190px]">-->
|
||||
<!-- <button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === 'MALE' }">-->
|
||||
<!-- <img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />-->
|
||||
<!-- <span class="text-white">Male</span>-->
|
||||
<!-- </button>-->
|
||||
<!-- <button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === 'FEMALE' }">-->
|
||||
<!-- <img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />-->
|
||||
<!-- <span class="text-white">Female</span>-->
|
||||
<!-- </button>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="isCreatingCharacter">
|
||||
<div class="flex flex-col gap-4 items-center">
|
||||
<input class="input-field" v-model="newCharacterName" name="name" id="name" placeholder="Enter a nickname..." />
|
||||
<div class="bg-[url('/assets/ui-elements/character-select-ui-shape.svg')] w-[190px] h-52 bg-no-repeat bg-center flex items-center justify-center">
|
||||
<button class="ml-6 w-4 h-8 p-0">
|
||||
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" />
|
||||
</button>
|
||||
<img class="w-24 object-contain mb-3.5 max-h-[70%]" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + defaultCharacterTypeId + '/' + (selectedHairId ?? 'default')" />
|
||||
<button class="mr-6 w-4 h-8 p-0">
|
||||
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex justify-between w-[190px]">
|
||||
<button class="btn-empty flex gap-2" :class="{ selected: selectedGender === 'MALE' }" @click="selectedGender = 'MALE'">
|
||||
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
|
||||
<span class="text-white">Male</span>
|
||||
</button>
|
||||
<button class="btn-empty flex gap-2" :class="{ selected: selectedGender === 'FEMALE' }" @click="selectedGender = 'FEMALE'">
|
||||
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Female symbol" />
|
||||
<span class="text-white">Female</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 lg:w-2/3 max-lg:min-h-[212px] h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center max-lg:rounded-bl-md rounded-r-md">
|
||||
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-10" v-if="selectedCharacterId || isCreatingCharacter">
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
<span class="text-sm">Hair color</span>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<div
|
||||
class="hair-deselect relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-white focus-visible:bg-cyan has-[:checked]:bg-cyan has-[:checked]:border-transparent"
|
||||
>
|
||||
<img src="/assets/icons/x-button-gray.svg" class="w-4 h-4" alt="Empty button" />
|
||||
<input type="radio" name="hair-color" :value="null" v-model="selectedHairColor" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
|
||||
</div>
|
||||
<div
|
||||
v-for="color in uniqueHairColors"
|
||||
class="relative flex justify-center items-center default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent"
|
||||
>
|
||||
<div class="w-full h-full rounded-sm" :style="getHairColorStyle(color)"></div>
|
||||
<input type="radio" name="hair-color" :value="color" v-model="selectedHairColor" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-10" v-if="selectedCharacterId">
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
<span class="text-sm">Hairstyle</span>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<div class="flex gap-2 flex-wrap max-h-20 overflow-y-auto scrollbar">
|
||||
<div
|
||||
class="hair-deselect relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-white focus-visible:bg-cyan has-[:checked]:bg-cyan has-[:checked]:border-transparent"
|
||||
>
|
||||
<img src="/assets/icons/x-button-gray.svg" class="w-4 h-4" alt="Empty button" />
|
||||
<input type="radio" name="hair" :value="null" v-model="selectedHairId" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
|
||||
</div>
|
||||
<!-- TODO #255: make radio button so we can set a value, do the same with swatches -->
|
||||
<div
|
||||
v-for="hair in filteredHairs"
|
||||
class="relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent overflow-hidden"
|
||||
v-for="hair in characterHairs"
|
||||
class="relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent"
|
||||
>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<img class="h-16 object-contain scale-[1] mt-8 origin-center" :src="config.server_endpoint + '/textures/sprites/' + hair.sprite + '/front.png'" alt="Hair sprite" />
|
||||
</div>
|
||||
<img class="h-4 object-contain" :src="config.server_endpoint + '/textures/sprites/' + hair.sprite + '/front.png'" alt="Hair sprite" />
|
||||
<input type="radio" name="hair" :value="hair.id" v-model="selectedHairId" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
<span class="text-sm">Hair color</span>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<!-- TODO: replace with hair colors -->
|
||||
<input type="radio" name="hair-color" v-for="n in 10" class="bg-red w-6 h-6 m-0 rounded-sm hover:cursor-pointer checked:outline checked:outline-1 checked:outline-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -117,102 +89,77 @@
|
||||
<div v-else>
|
||||
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
|
||||
</div>
|
||||
|
||||
<div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading">
|
||||
<template v-if="!isCreatingCharacter">
|
||||
<button class="btn-empty min-w-48" @click.stop="gameStore.disconnectSocket()">Back</button>
|
||||
<button class="btn-cyan min-w-48 disabled:bg-cyan-800 disabled:cursor-not-allowed" :disabled="!selectedCharacterId" @click="loginWithCharacter()">Play now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CREATE CHARACTER MODAL -->
|
||||
<Modal :isModalOpen="isCreateNewCharacterModalOpen" @modal:close="isCreateNewCharacterModalOpen = false" :modal-width="430" :modal-height="275">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium text-white">Create your character</h3>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="btn-empty min-w-48" @click="cancelCharacterCreation">Cancel</button>
|
||||
<button class="btn-cyan min-w-48" @click="createCharacter">Create</button>
|
||||
|
||||
<template #modalBody>
|
||||
<div class="p-4 h-[calc(100%_-_32px)]">
|
||||
<form method="post" @submit.prevent="createCharacter" class="h-full flex flex-col justify-between">
|
||||
<div class="form-field-full">
|
||||
<label for="name" class="text-white">Nickname</label>
|
||||
<input class="input-field" v-model="newCharacterName" name="name" id="name" placeholder="Enter a nickname..." />
|
||||
</div>
|
||||
<div class="grid grid-flow-col justify-stretch gap-4">
|
||||
<button type="button" class="btn-empty py-1.5 px-4 inline-block" @click.prevent="isCreateNewCharacterModalOpen = false">Cancel</button>
|
||||
<button class="btn-cyan py-1.5 px-4 inline-block" type="submit">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { CharacterHairStorage, CharacterTypeStorage } from '@/storage/storages'
|
||||
import { CharacterHairStorage } from '@/storage/storages'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const characterCreationSettings = {
|
||||
maxCharacters: 4,
|
||||
minNameLength: 3,
|
||||
maxNameLength: 20,
|
||||
defaultGender: 'MALE' as const
|
||||
}
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const { playSound } = useSoundComposable()
|
||||
const gameStore = useGameStore()
|
||||
const isLoading = ref<boolean>(true)
|
||||
const characters = ref<CharacterT[]>([])
|
||||
const selectedCharacterId = ref<string | null>(null)
|
||||
const newNickname = ref<string>('')
|
||||
const isCreateNewCharacterModalOpen = ref<boolean>(false)
|
||||
const newCharacterName = ref<string>('')
|
||||
const characterHairs = ref<CharacterHair[]>([])
|
||||
const selectedHairId = ref<string | null>(null)
|
||||
const defaultCharacterTypeId = ref<string>('')
|
||||
const isCreatingCharacter = ref<boolean>(false)
|
||||
const selectedGender = ref()
|
||||
|
||||
const selectedHairColor = ref<string | null>(null)
|
||||
const uniqueHairColors = computed(() => {
|
||||
return [...new Set(characterHairs.value.map((hair) => hair.color))]
|
||||
})
|
||||
|
||||
const filteredHairs = computed(() => {
|
||||
if (!selectedHairColor.value) return characterHairs.value
|
||||
return characterHairs.value.filter((hair) => hair.color === selectedHairColor.value)
|
||||
})
|
||||
|
||||
function getHairColorStyle(color: string | null) {
|
||||
return {
|
||||
backgroundColor: color,
|
||||
border: selectedHairColor.value === color ? '1px solid white' : '1px solid rgba(255, 255, 255, 0.2)'
|
||||
}
|
||||
}
|
||||
|
||||
function startCharacterCreation() {
|
||||
isCreatingCharacter.value = true
|
||||
selectedCharacterId.value = null
|
||||
newCharacterName.value = ''
|
||||
selectedHairId.value = null
|
||||
selectedHairColor.value = null
|
||||
selectedGender.value = characterCreationSettings.defaultGender
|
||||
}
|
||||
|
||||
function cancelCharacterCreation() {
|
||||
isCreatingCharacter.value = false
|
||||
newCharacterName.value = ''
|
||||
selectedHairId.value = null
|
||||
selectedHairColor.value = null
|
||||
}
|
||||
|
||||
// Fetch characters
|
||||
setTimeout(() => {
|
||||
socketManager.emit(SocketEvent.CHARACTER_LIST)
|
||||
console.log(SocketEvent.CHARACTER_LIST)
|
||||
gameStore.connection?.emit(SocketEvent.CHARACTER_LIST)
|
||||
}, 750)
|
||||
|
||||
socketManager.on(SocketEvent.CHARACTER_LIST, (data: any) => {
|
||||
gameStore.connection?.on(SocketEvent.CHARACTER_LIST, (data: any) => {
|
||||
characters.value = data
|
||||
isLoading.value = false
|
||||
})
|
||||
|
||||
// Select character logics
|
||||
function loginWithCharacter() {
|
||||
if (!selectedCharacterId.value) return
|
||||
|
||||
socketManager.emit(
|
||||
gameStore.connection?.emit(
|
||||
SocketEvent.CHARACTER_CONNECT,
|
||||
{
|
||||
characterId: selectedCharacterId.value,
|
||||
characterHairId: selectedHairId.value,
|
||||
newNickname: newNickname.value
|
||||
characterHairId: selectedHairId.value
|
||||
},
|
||||
(response: { character: CharacterT; map: Map; characters: CharacterT[] }) => {
|
||||
gameStore.setCharacter(response.character)
|
||||
@ -222,51 +169,27 @@ function loginWithCharacter() {
|
||||
|
||||
// Create character logics
|
||||
function createCharacter() {
|
||||
if (newCharacterName.value.length < characterCreationSettings.minNameLength || newCharacterName.value.length > characterCreationSettings.maxNameLength) {
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(
|
||||
SocketEvent.CHARACTER_CREATE,
|
||||
{
|
||||
name: newCharacterName.value,
|
||||
gender: selectedGender.value,
|
||||
hairId: selectedHairId.value
|
||||
},
|
||||
(success: boolean) => {
|
||||
if (success) {
|
||||
cancelCharacterCreation()
|
||||
socketManager.emit(SocketEvent.CHARACTER_LIST)
|
||||
}
|
||||
}
|
||||
)
|
||||
gameStore.connection?.emit(SocketEvent.CHARACTER_CREATE, { name: newCharacterName.value }, (success: boolean) => {
|
||||
if (success) return
|
||||
isCreateNewCharacterModalOpen.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// Watch changes for selected character and update hairs
|
||||
watch(selectedCharacterId, (characterId) => {
|
||||
if (!characterId) return
|
||||
newNickname.value = ''
|
||||
isCreatingCharacter.value = false
|
||||
selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHair ?? null
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await playSound('/assets/music/intro.mp3')
|
||||
playSound('/assets/music/intro.mp3')
|
||||
const characterHairStorage = new CharacterHairStorage()
|
||||
const characterTypeStorage = new CharacterTypeStorage()
|
||||
characterHairs.value = await characterHairStorage.getAll()
|
||||
|
||||
// Get the first available character type for preview
|
||||
const types = await characterTypeStorage.getAll()
|
||||
const defaultType = types.find((type) => type.isSelectable)
|
||||
if (defaultType) {
|
||||
defaultCharacterTypeId.value = defaultType.id
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
socketManager.off(SocketEvent.CHARACTER_LIST)
|
||||
socketManager.off(SocketEvent.CHARACTER_CONNECT)
|
||||
socketManager.off(SocketEvent.CHARACTER_CREATE)
|
||||
gameStore.connection?.off(SocketEvent.CHARACTER_LIST)
|
||||
gameStore.connection?.off(SocketEvent.CHARACTER_CONNECT)
|
||||
gameStore.connection?.off(SocketEvent.CHARACTER_CREATE)
|
||||
})
|
||||
</script>
|
||||
|
@ -20,10 +20,8 @@
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import 'phaser'
|
||||
import type { Map as MapT } from '@/application/types'
|
||||
import { downloadCache } from '@/application/utilities'
|
||||
import Map from '@/components/gameMaster/mapEditor/Map.vue'
|
||||
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
|
||||
import MapObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
|
||||
@ -34,10 +32,13 @@ import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { loadAllTileTextures } from '@/services/mapService'
|
||||
import { MapStorage } from '@/storage/storages'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { Game, Scene } from 'phavuer'
|
||||
import { ref, toRaw, useTemplateRef } from 'vue'
|
||||
import { ref, useTemplateRef } from 'vue'
|
||||
|
||||
const mapStorage = new MapStorage()
|
||||
const mapEditor = useMapEditorComposable()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
const mapModal = useTemplateRef('mapModal')
|
||||
const mapSettingsModal = useTemplateRef('mapSettingsModal')
|
||||
@ -82,8 +83,8 @@ const preloadScene = async (scene: Phaser.Scene) => {
|
||||
})
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const currentMap = toRaw(mapEditor.currentMap.value)
|
||||
function save() {
|
||||
const currentMap = mapEditor.currentMap.value
|
||||
if (!currentMap) return
|
||||
|
||||
const data = {
|
||||
@ -91,9 +92,8 @@ async function save() {
|
||||
mapId: currentMap.id
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_MAP_UPDATE, data, async (response: MapT) => {
|
||||
if (!response.id) return
|
||||
await downloadCache('maps', new MapStorage())
|
||||
gameStore.connection?.emit(SocketEvent.GM_MAP_UPDATE, data, (response: MapT) => {
|
||||
mapStorage.update(response.id, response)
|
||||
})
|
||||
}
|
||||
|
||||
|
22
src/components/utilities/Accordion.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="mb-4 flex flex-col gap-3">
|
||||
<div @click="toggle" class="p-3 bg-gray-300 bg-opacity-50 rounded hover:bg-gray-400 text-white font-default cursor-pointer">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<transition enter-active-class="transition-all duration-300 ease-in-out" leave-active-class="transition-all duration-300 ease-in-out" enter-from-class="opacity-0 max-h-0" enter-to-class="opacity-100 max-h-96" leave-from-class="opacity-100 max-h-96" leave-to-class="opacity-0 max-h-0">
|
||||
<div v-if="isOpen" class="overflow-hidden">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const toggle = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
</script>
|
@ -1,14 +1,10 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { login } from '@/services/authenticationService'
|
||||
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SoundStorage, SpriteStorage, TileStorage } from '@/storage/storages'
|
||||
import { TextureStorage } from '@/storage/textureStorage'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const mapStorage = new MapStorage()
|
||||
const tileStorage = new TileStorage()
|
||||
const mapObjectStorage = new MapObjectStorage()
|
||||
@ -41,17 +37,6 @@ async function handleKeyPress(event: KeyboardEvent) {
|
||||
currentString = '' // Reset
|
||||
}
|
||||
|
||||
if (currentString.includes('11')) {
|
||||
if (socketManager.token) return
|
||||
const response = await login('root', 'password')
|
||||
|
||||
if (response.success === undefined) {
|
||||
return
|
||||
}
|
||||
socketManager.setToken(response.token)
|
||||
gameStore.initConnection()
|
||||
}
|
||||
|
||||
// Reset string after a certain amount of time
|
||||
setTimeout(() => {
|
||||
currentString = ''
|
||||
|
@ -12,9 +12,8 @@
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import { onBeforeMount, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
|
||||
@ -37,13 +36,13 @@ function setupNotificationListener(connection: any) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const connection = socketManager.connection
|
||||
const connection = gameStore.connection
|
||||
if (connection) {
|
||||
setupNotificationListener(connection)
|
||||
} else {
|
||||
// Watch for changes in the socket connection
|
||||
watch(
|
||||
() => socketManager.connection,
|
||||
() => gameStore.connection,
|
||||
(newConnection) => {
|
||||
if (newConnection) setupNotificationListener(newConnection)
|
||||
}
|
||||
@ -52,7 +51,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
const connection = socketManager.connection
|
||||
const connection = gameStore.connection
|
||||
if (connection) {
|
||||
connection.off(SocketEvent.NOTIFICATION)
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { getTile } from '@/services/mapService'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import type { Ref } from 'vue'
|
||||
@ -8,77 +7,7 @@ import { useBaseControlsComposable } from './useBaseControlsComposable'
|
||||
export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
||||
const gameStore = useGameStore()
|
||||
const baseHandlers = useBaseControlsComposable(scene, layer, waypoint, camera)
|
||||
const pressedKeys = new Set<string>()
|
||||
|
||||
let moveTimeout: NodeJS.Timeout | null = null
|
||||
let currentPosition = {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
|
||||
// Movement constants
|
||||
const MOVEMENT_DELAY = 110 // Milliseconds between moves
|
||||
const ARROW_KEYS = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'] as const
|
||||
|
||||
function updateCurrentPosition() {
|
||||
if (!gameStore.character) return
|
||||
currentPosition = {
|
||||
x: gameStore.character.positionX,
|
||||
y: gameStore.character.positionY
|
||||
}
|
||||
}
|
||||
|
||||
function calculateNewPosition() {
|
||||
let newX = currentPosition.x
|
||||
let newY = currentPosition.y
|
||||
|
||||
if (pressedKeys.has('ArrowLeft')) newX--
|
||||
if (pressedKeys.has('ArrowRight')) newX++
|
||||
if (pressedKeys.has('ArrowUp')) newY--
|
||||
if (pressedKeys.has('ArrowDown')) newY++
|
||||
|
||||
return { newX, newY }
|
||||
}
|
||||
|
||||
function emitMovement(x: number, y: number) {
|
||||
if (x === currentPosition.x && y === currentPosition.y) return
|
||||
|
||||
socketManager.emit(SocketEvent.MAP_CHARACTER_MOVE, [x, y])
|
||||
socketManager.on(SocketEvent.MAP_CHARACTER_MOVE, ([characterId, posX, posY, rot, isMoving]: [string, number, number, number, boolean]) => {
|
||||
if (characterId !== gameStore.character?.id) return
|
||||
currentPosition = { x: posX, y: posY }
|
||||
})
|
||||
|
||||
currentPosition = { x, y }
|
||||
}
|
||||
|
||||
function startMovementLoop() {
|
||||
if (moveTimeout) return
|
||||
|
||||
const move = () => {
|
||||
if (pressedKeys.size === 0) {
|
||||
stopMovementLoop()
|
||||
return
|
||||
}
|
||||
|
||||
updateCurrentPosition()
|
||||
const { newX, newY } = calculateNewPosition()
|
||||
emitMovement(newX, newY)
|
||||
|
||||
moveTimeout = setTimeout(move, MOVEMENT_DELAY)
|
||||
}
|
||||
|
||||
move()
|
||||
}
|
||||
|
||||
function stopMovementLoop() {
|
||||
if (moveTimeout) {
|
||||
clearTimeout(moveTimeout)
|
||||
moveTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
// Pointer Handlers
|
||||
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
||||
baseHandlers.startDragging(pointer)
|
||||
}
|
||||
@ -94,37 +23,70 @@ export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Til
|
||||
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
|
||||
if (!pointerTile) return
|
||||
|
||||
emitMovement(pointerTile.x, pointerTile.y)
|
||||
gameStore.connection?.emit(SocketEvent.MAP_CHARACTER_MOVE, {
|
||||
positionX: pointerTile.x,
|
||||
positionY: pointerTile.y
|
||||
})
|
||||
}
|
||||
|
||||
// Keyboard Handlers
|
||||
const pressedKeys = new Set<string>()
|
||||
let moveInterval: number | null = null
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (!gameStore.character) return
|
||||
|
||||
if (ARROW_KEYS.includes(event.key as (typeof ARROW_KEYS)[number])) {
|
||||
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
|
||||
// Prevent key repeat events
|
||||
if (event.repeat) return
|
||||
|
||||
pressedKeys.add(event.key)
|
||||
updateCurrentPosition()
|
||||
startMovementLoop()
|
||||
|
||||
// Start movement loop if not already running
|
||||
if (!moveInterval) {
|
||||
moveInterval = window.setInterval(moveCharacter, 80) // Increased interval to match server throttle `MOVEMENT_THROTTLE`
|
||||
moveCharacter() // Move immediately on first press
|
||||
}
|
||||
}
|
||||
|
||||
// Attack on CTRL
|
||||
if (event.key === 'Control') {
|
||||
socketManager.emit(SocketEvent.MAP_CHARACTER_ATTACK)
|
||||
gameStore.connection?.emit(SocketEvent.MAP_CHARACTER_ATTACK)
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyUp(event: KeyboardEvent) {
|
||||
pressedKeys.delete(event.key)
|
||||
|
||||
if (pressedKeys.size === 0) {
|
||||
stopMovementLoop()
|
||||
// If no movement keys are pressed, clear the interval
|
||||
if (pressedKeys.size === 0 && moveInterval) {
|
||||
clearInterval(moveInterval)
|
||||
moveInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
function moveCharacter() {
|
||||
if (!gameStore.character) return
|
||||
|
||||
const { positionX, positionY } = gameStore.character
|
||||
let newX = positionX
|
||||
let newY = positionY
|
||||
|
||||
// Calculate new position based on pressed keys
|
||||
if (pressedKeys.has('ArrowLeft')) newX--
|
||||
if (pressedKeys.has('ArrowRight')) newX++
|
||||
if (pressedKeys.has('ArrowUp')) newY--
|
||||
if (pressedKeys.has('ArrowDown')) newY++
|
||||
|
||||
// Only emit if position changed
|
||||
if (newX !== positionX || newY !== positionY) {
|
||||
gameStore.connection?.emit(SocketEvent.MAP_CHARACTER_MOVE, {
|
||||
positionX: newX,
|
||||
positionY: newY
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const setupControls = () => {
|
||||
updateCurrentPosition() // Initialize position
|
||||
|
||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||
@ -134,9 +96,6 @@ export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Til
|
||||
}
|
||||
|
||||
const cleanupControls = () => {
|
||||
stopMovementLoop()
|
||||
pressedKeys.clear()
|
||||
|
||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||
|
@ -1,9 +1,9 @@
|
||||
import config from '@/application/config'
|
||||
import { Direction } from '@/application/enums'
|
||||
import { type MapCharacter, type SpriteAction } from '@/application/types'
|
||||
import { type MapCharacter } from '@/application/types'
|
||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/services/mapService'
|
||||
import { loadSpriteTextures } from '@/services/textureService'
|
||||
import { CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
|
||||
import { CharacterTypeStorage } from '@/storage/storages'
|
||||
import { refObj } from 'phavuer'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@ -18,7 +18,7 @@ export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phase
|
||||
const tween = ref<Phaser.Tweens.Tween | null>(null)
|
||||
|
||||
const updateIsometricDepth = (positionX: number, positionY: number) => {
|
||||
isometricDepth.value = calculateIsometricDepth(positionX, positionY)
|
||||
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 30, 95)
|
||||
}
|
||||
|
||||
const updatePosition = (positionX: number, positionY: number) => {
|
||||
@ -72,7 +72,7 @@ export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phase
|
||||
// Add new listener
|
||||
characterSprite.value.on(Phaser.Animations.Events.ANIMATION_COMPLETE, () => {
|
||||
characterSprite.value!.setFrame(0)
|
||||
characterSprite.value!.setTexture(spriteSpriteActionId.value)
|
||||
characterSprite.value!.setTexture(charTexture.value)
|
||||
})
|
||||
|
||||
characterSprite.value.anims.play(
|
||||
@ -96,23 +96,11 @@ export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phase
|
||||
return [0, 6].includes(mapCharacter.character.rotation ?? 0) ? 'left_up' : 'right_down'
|
||||
})
|
||||
|
||||
const getSpriteHeightByAction = async (action: string = '') => {
|
||||
if (!characterSpriteId.value) return 0
|
||||
const spriteStorage = new SpriteStorage()
|
||||
const sprite = await spriteStorage.getById(characterSpriteId.value)
|
||||
let actionWithDirection = `${currentAction.value}_${currentDirection.value}`
|
||||
if (action) actionWithDirection = action
|
||||
|
||||
return sprite?.spriteActions?.find((spriteAction: SpriteAction) => spriteAction.action === actionWithDirection)?.frameHeight ?? 0
|
||||
}
|
||||
|
||||
const spriteHeight = computed(() => getSpriteHeightByAction(currentAction.value))
|
||||
|
||||
const currentAction = computed(() => {
|
||||
return mapCharacter.isMoving ? 'walk' : 'idle'
|
||||
})
|
||||
|
||||
const spriteSpriteActionId = computed(() => {
|
||||
const charTexture = computed(() => {
|
||||
const spriteId = characterSpriteId.value ?? 'idle_right_down'
|
||||
return `${spriteId}-${currentAction.value}_${currentDirection.value}`
|
||||
})
|
||||
@ -121,11 +109,11 @@ export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phase
|
||||
if (!characterSprite.value) return
|
||||
|
||||
if (mapCharacter.isMoving) {
|
||||
characterSprite.value.anims.play(spriteSpriteActionId.value, true)
|
||||
characterSprite.value.anims.play(charTexture.value, true)
|
||||
} else {
|
||||
characterSprite.value.anims.stop()
|
||||
characterSprite.value.setFrame(0)
|
||||
characterSprite.value.setTexture(spriteSpriteActionId.value)
|
||||
characterSprite.value.setTexture(charTexture.value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,8 +130,7 @@ export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phase
|
||||
}
|
||||
|
||||
if (characterSprite.value) {
|
||||
characterSprite!.value?.setOrigin(0.5, 1)
|
||||
characterSprite.value.setTexture(spriteSpriteActionId.value)
|
||||
characterSprite.value.setTexture(charTexture.value)
|
||||
characterSprite.value.setFlipX(isFlippedX.value)
|
||||
}
|
||||
|
||||
@ -159,15 +146,10 @@ export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phase
|
||||
characterContainer,
|
||||
characterSpriteId,
|
||||
characterSprite,
|
||||
spriteHeight,
|
||||
currentAction,
|
||||
spriteSpriteActionId,
|
||||
currentPositionX,
|
||||
currentPositionY,
|
||||
currentDirection,
|
||||
isometricDepth,
|
||||
isFlippedX,
|
||||
getSpriteHeightByAction,
|
||||
updatePosition,
|
||||
playAnimation,
|
||||
calcDirection,
|
||||
|
@ -1,9 +1,8 @@
|
||||
import type { Map, MapObject, PlacedMapObject, UUID } from '@/application/types'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export type TeleportSettings = {
|
||||
toMap: string
|
||||
toMapId: string
|
||||
toPositionX: number
|
||||
toPositionY: number
|
||||
toRotation: number
|
||||
@ -21,7 +20,7 @@ const movingPlacedObject = ref<PlacedMapObject | null>(null)
|
||||
const selectedPlacedObject = ref<PlacedMapObject | null>(null)
|
||||
const shouldClearTiles = ref(false)
|
||||
const teleportSettings = ref<TeleportSettings>({
|
||||
toMap: '1000',
|
||||
toMapId: '1000',
|
||||
toPositionX: 0,
|
||||
toPositionY: 0,
|
||||
toRotation: 0
|
||||
@ -47,8 +46,6 @@ export function useMapEditorComposable() {
|
||||
const toggleActive = () => {
|
||||
if (active.value) reset()
|
||||
active.value = !active.value
|
||||
const gameStore = useGameStore()
|
||||
gameStore.uiSettings.isGmPanelOpen = false
|
||||
}
|
||||
|
||||
const togglePlacedMapObjectPreview = () => {
|
||||
@ -98,7 +95,6 @@ export function useMapEditorComposable() {
|
||||
selectedTile.value = ''
|
||||
isPlacedMapObjectPreviewEnabled.value = false
|
||||
selectedMapObject.value = null
|
||||
selectedPlacedObject.value = null
|
||||
shouldClearTiles.value = false
|
||||
refreshMapObject.value = 0
|
||||
}
|
||||
|
@ -1,76 +0,0 @@
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
|
||||
class SocketManager {
|
||||
private static instance: SocketManager
|
||||
private _connection = shallowRef<Socket | null>(null)
|
||||
private _token = ref('')
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): SocketManager {
|
||||
if (!SocketManager.instance) {
|
||||
SocketManager.instance = new SocketManager()
|
||||
}
|
||||
return SocketManager.instance
|
||||
}
|
||||
|
||||
public get connection() {
|
||||
return this._connection.value
|
||||
}
|
||||
|
||||
public get token() {
|
||||
return this._token.value
|
||||
}
|
||||
|
||||
public setToken(token: string) {
|
||||
this._token.value = token
|
||||
}
|
||||
|
||||
public initConnection(): Socket {
|
||||
if (this._connection.value) return this._connection.value
|
||||
|
||||
const socket = io(config.server_endpoint, {
|
||||
secure: config.environment === 'production',
|
||||
withCredentials: true,
|
||||
transports: ['websocket'],
|
||||
reconnectionAttempts: 5
|
||||
})
|
||||
|
||||
this._connection.value = socket
|
||||
return socket
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
if (!this._connection.value) return
|
||||
|
||||
this._connection.value.off(SocketEvent.CONNECT_ERROR)
|
||||
this._connection.value.off(SocketEvent.RECONNECT_FAILED)
|
||||
this._connection.value.off(SocketEvent.DATE)
|
||||
this._connection.value.disconnect()
|
||||
|
||||
useCookies().remove('token', {
|
||||
domain: config.domain
|
||||
})
|
||||
|
||||
this._connection.value = null
|
||||
this._token.value = ''
|
||||
}
|
||||
|
||||
public emit(event: string, ...args: any[]): void {
|
||||
this._connection.value?.emit(event, ...args)
|
||||
}
|
||||
|
||||
public on(event: string, callback: (...args: any[]) => void): void {
|
||||
this._connection.value?.on(event, callback)
|
||||
}
|
||||
|
||||
public off(event: string, callback?: (...args: any[]) => void): void {
|
||||
this._connection.value?.off(event, callback)
|
||||
}
|
||||
}
|
||||
|
||||
export const socketManager = SocketManager.getInstance()
|
@ -62,8 +62,12 @@ export function createTileArray(width: number, height: number, tile: string = 'b
|
||||
return Array.from({ length: height }, () => Array.from({ length: width }, () => tile))
|
||||
}
|
||||
|
||||
export const calculateIsometricDepth = (positionX: number, positionY: number) => {
|
||||
return positionX + positionY
|
||||
export const calculateIsometricDepth = (positionX: number, positionY: number, width: number = 0, height: number = 0, isCharacter: boolean = false) => {
|
||||
const baseDepth = positionX + positionY
|
||||
if (isCharacter) {
|
||||
return baseDepth
|
||||
}
|
||||
return baseDepth + (width + height) / (2 * config.tile_size.width)
|
||||
}
|
||||
|
||||
async function loadTileTextures(tiles: TileT[], scene: Phaser.Scene) {
|
||||
|
@ -1,12 +1,16 @@
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Character, Notification, User, WorldSettings } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
import { defineStore } from 'pinia'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
|
||||
export const useGameStore = defineStore('game', {
|
||||
state: () => {
|
||||
return {
|
||||
notifications: [] as Notification[],
|
||||
token: '',
|
||||
connection: null as Socket | null,
|
||||
user: null as User | null,
|
||||
character: null as Character | null,
|
||||
world: {
|
||||
@ -47,6 +51,9 @@ export const useGameStore = defineStore('game', {
|
||||
removeNotification(id: string) {
|
||||
this.notifications = this.notifications.filter((notification: Notification) => notification.id !== id)
|
||||
},
|
||||
setToken(token: string) {
|
||||
this.token = token
|
||||
},
|
||||
setUser(user: User | null) {
|
||||
this.user = user
|
||||
},
|
||||
@ -66,34 +73,49 @@ export const useGameStore = defineStore('game', {
|
||||
this.uiSettings.isCharacterProfileOpen = !this.uiSettings.isCharacterProfileOpen
|
||||
},
|
||||
initConnection() {
|
||||
const socket = socketManager.initConnection()
|
||||
this.connection = io(config.server_endpoint, {
|
||||
secure: config.environment === 'production',
|
||||
withCredentials: true,
|
||||
transports: ['websocket'],
|
||||
reconnectionAttempts: 5
|
||||
})
|
||||
|
||||
// Handle connect error
|
||||
socket.on(SocketEvent.CONNECT_ERROR, () => {
|
||||
// #99 - If we can't connect, disconnect
|
||||
this.connection.on('connect_error', () => {
|
||||
this.disconnectSocket()
|
||||
})
|
||||
|
||||
// Handle failed reconnection
|
||||
socket.on(SocketEvent.RECONNECT_FAILED, () => {
|
||||
this.disconnectSocket()
|
||||
})
|
||||
// Let the server know the user is logged in
|
||||
this.connection.emit(SocketEvent.LOGIN)
|
||||
|
||||
// Emit login event
|
||||
socketManager.emit(SocketEvent.LOGIN)
|
||||
|
||||
// Handle logged in event
|
||||
socketManager.on(SocketEvent.LOGGED_IN, (user: User) => {
|
||||
// set user
|
||||
this.connection.on(SocketEvent.LOGGED_IN, (user: User) => {
|
||||
this.setUser(user)
|
||||
})
|
||||
|
||||
// Handle date updates
|
||||
socketManager.on(SocketEvent.DATE, (data: Date) => {
|
||||
// When we can't reconnect, disconnect
|
||||
this.connection.on('reconnect_failed', () => {
|
||||
this.disconnectSocket()
|
||||
})
|
||||
|
||||
// Listen for new date from socket
|
||||
this.connection.on(SocketEvent.DATE, (data: Date) => {
|
||||
this.world.date = new Date(data)
|
||||
})
|
||||
},
|
||||
disconnectSocket() {
|
||||
socketManager.disconnect()
|
||||
// Remove event listeners
|
||||
this.connection?.off('connect_error')
|
||||
this.connection?.off('reconnect_failed')
|
||||
this.connection?.off(SocketEvent.DATE)
|
||||
this.connection?.disconnect()
|
||||
|
||||
useCookies().remove('token', {
|
||||
domain: config.domain
|
||||
})
|
||||
|
||||
this.connection = null
|
||||
this.token = ''
|
||||
this.user = null
|
||||
this.character = null
|
||||
|
||||
|
@ -2,7 +2,7 @@ import type { MapObject, Map as MapT } from '@/application/types'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export type TeleportSettings = {
|
||||
toMap: string
|
||||
toMapId: string
|
||||
toPositionX: number
|
||||
toPositionY: number
|
||||
toRotation: number
|
||||
@ -18,7 +18,7 @@ export const useMapEditorStore = defineStore('mapEditor', {
|
||||
selectedMapObject: null as MapObject | null,
|
||||
shouldClearTiles: false,
|
||||
teleportSettings: {
|
||||
toMap: '',
|
||||
toMapId: '',
|
||||
toPositionX: 0,
|
||||
toPositionY: 0,
|
||||
toRotation: 0
|
||||
|
@ -35,13 +35,13 @@ export const useMapStore = defineStore('map', {
|
||||
removeCharacter(characterId: UUID) {
|
||||
this.characters = this.characters.filter((char) => char.character.id !== characterId)
|
||||
},
|
||||
updateCharacterPosition([characterId, posX, posY, rot, isMoving]: [UUID, number, number, number, boolean]) {
|
||||
const character = this.characters.find((char) => char.character.id === characterId)
|
||||
updateCharacterPosition(data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) {
|
||||
const character = this.characters.find((char) => char.character.id === data.characterId)
|
||||
if (character) {
|
||||
character.character.positionX = posX
|
||||
character.character.positionY = posY
|
||||
character.character.rotation = rot
|
||||
character.isMoving = isMoving
|
||||
character.character.positionX = data.positionX
|
||||
character.character.positionY = data.positionY
|
||||
character.character.rotation = data.rotation
|
||||
character.isMoving = data.isMoving
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
|