Compare commits
74 Commits
feature/ma
...
main
Author | SHA1 | Date | |
---|---|---|---|
af5e2449b5 | |||
5392093d71 | |||
27c775821a | |||
0d5acd48ce | |||
fc34a488d9 | |||
0b1e95f80f | |||
9f176aae45 | |||
4903a83c71 | |||
ba3ed8c099 | |||
2881d5f251 | |||
32ca61cc50 | |||
6897ad0f1e | |||
db1766026e | |||
3f28d85c30 | |||
a85ad94f15 | |||
e6c684e066 | |||
142d991265 | |||
9e5dcc31fa | |||
b5c5837105 | |||
57b503142e | |||
208b58d05f | |||
87c04b6de5 | |||
84f8db5e10 | |||
84b34c4f85 | |||
2495e14ece | |||
febb924f75 | |||
dc2afba82b | |||
ed0a02a795 | |||
3670eb8736 | |||
d85bf4846b | |||
51e885cfdf | |||
ffc7efb17c | |||
a0da0266d3 | |||
2281c2c5e0 | |||
0e3a0e3dba | |||
ed992e1c2d | |||
65b011982a | |||
489c6c3ba0 | |||
db650449ac | |||
2d7d598c94 | |||
7097eb1580 | |||
d51fbc8030 | |||
b5b6d0adcc | |||
4b7b6e4885 | |||
4042808d4e | |||
a6d6d894a9 | |||
0c61fe77de | |||
bfb2bcb939 | |||
af5a97f66d | |||
79fa54b1bb | |||
dbb4cae154 | |||
9a8220e4e0 | |||
bc0db8b32b | |||
ad611ef593 | |||
d819a84a37 | |||
15dc331a43 | |||
920baaebde | |||
b569888682 | |||
94eab073e6 | |||
d843b954ab | |||
337446497b | |||
d8805dd775 | |||
4c040c21d6 | |||
d0af83ec60 | |||
2de34d2034 | |||
132121c082 | |||
201f628bfa | |||
af99d66595 | |||
56f30093f6 | |||
8f26a40a0e | |||
110fd4e608 | |||
c1edf31ca0 | |||
90c0ed3141 | |||
bcf0d2832d |
902
package-lock.json
generated
19
package.json
@ -16,19 +16,22 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^10.5.0",
|
"@vueuse/core": "^10.5.0",
|
||||||
"@vueuse/integrations": "^10.5.0",
|
"@vueuse/integrations": "^10.5.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.9",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.11",
|
||||||
"phaser": "3.87.0",
|
"phaser": "^3.88.2",
|
||||||
"pinia": "^2.1.6",
|
"phaser3-rex-plugins": "^1.80.13",
|
||||||
|
"phavuer": "^0.16.5",
|
||||||
|
"pinia": "^2.3.1",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"socket.io-client": "^4.8.0",
|
"socket.io-client": "^4.8.1",
|
||||||
"universal-cookie": "^6.1.3",
|
"universal-cookie": "^6.1.3",
|
||||||
"vite-plugin-image-optimizer": "^1.1.8",
|
"vite-plugin-image-optimizer": "^1.1.8",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.13",
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
||||||
|
"@tauri-apps/cli": "^2.2.7",
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^20.14.11",
|
"@types/node": "^20.14.11",
|
||||||
@ -38,8 +41,6 @@
|
|||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"jsdom": "^24.1.1",
|
"jsdom": "^24.1.1",
|
||||||
"npm-run-all2": "^6.2.3",
|
"npm-run-all2": "^6.2.3",
|
||||||
"phaser3-rex-plugins": "^1.80.8",
|
|
||||||
"phavuer": "^0.16.1",
|
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"sass": "^1.79.4",
|
"sass": "^1.79.4",
|
||||||
|
4
src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
/gen/schemas
|
5149
src-tauri/Cargo.lock
generated
Normal file
25
src-tauri/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[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"
|
3
src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
11
src-tauri/capabilities/default.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "enables the default permissions",
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default"
|
||||||
|
]
|
||||||
|
}
|
BIN
src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 49 KiB |
16
src-tauri/src/lib.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#[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");
|
||||||
|
}
|
6
src-tauri/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
app_lib::run();
|
||||||
|
}
|
37
src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$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,6 +16,7 @@ import Debug from '@/components/utilities/Debug.vue'
|
|||||||
import Notifications from '@/components/utilities/Notifications.vue'
|
import Notifications from '@/components/utilities/Notifications.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
|
|
||||||
@ -26,8 +27,8 @@ const { playSound } = useSoundComposable()
|
|||||||
|
|
||||||
const currentScreen = computed(() => {
|
const currentScreen = computed(() => {
|
||||||
if (!gameStore.game.isLoaded) return Loading
|
if (!gameStore.game.isLoaded) return Loading
|
||||||
if (!gameStore.connection) return Login
|
if (!socketManager.connection) return Login
|
||||||
if (!gameStore.token) return Login
|
if (!socketManager.token) return Login
|
||||||
if (!gameStore.character) return Characters
|
if (!gameStore.character) return Characters
|
||||||
if (mapEditor.active.value) return MapEditor
|
if (mapEditor.active.value) return MapEditor
|
||||||
return Game
|
return Game
|
||||||
|
@ -5,6 +5,8 @@ export enum Direction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum SocketEvent {
|
export enum SocketEvent {
|
||||||
|
CONNECT_ERROR = 'connect_error',
|
||||||
|
RECONNECT_FAILED = 'reconnect_failed',
|
||||||
CLOSE = '52',
|
CLOSE = '52',
|
||||||
DATA = '51',
|
DATA = '51',
|
||||||
CHARACTER_CONNECT = '50',
|
CHARACTER_CONNECT = '50',
|
||||||
@ -41,7 +43,7 @@ export enum SocketEvent {
|
|||||||
GM_MAP_REQUEST = '19',
|
GM_MAP_REQUEST = '19',
|
||||||
GM_MAP_UPDATE = '18',
|
GM_MAP_UPDATE = '18',
|
||||||
MAP_CHARACTER_MOVEERROR = '17',
|
MAP_CHARACTER_MOVEERROR = '17',
|
||||||
DISCONNECT = '16',
|
DISCONNECT = 'disconnect',
|
||||||
USER_DISCONNECT = '15',
|
USER_DISCONNECT = '15',
|
||||||
LOGIN = '14',
|
LOGIN = '14',
|
||||||
LOGGED_IN = '13',
|
LOGGED_IN = '13',
|
||||||
@ -49,7 +51,7 @@ export enum SocketEvent {
|
|||||||
DATE = '11',
|
DATE = '11',
|
||||||
FAILED = '10',
|
FAILED = '10',
|
||||||
COMPLETED = '9',
|
COMPLETED = '9',
|
||||||
CONNECTION = '8',
|
CONNECTION = 'connection',
|
||||||
WEATHER = '7',
|
WEATHER = '7',
|
||||||
CHARACTER_DISCONNECT = '6',
|
CHARACTER_DISCONNECT = '6',
|
||||||
MAP_CHARACTER_ATTACK = '5',
|
MAP_CHARACTER_ATTACK = '5',
|
||||||
|
@ -36,7 +36,8 @@ export type Tile = {
|
|||||||
export type MapObject = {
|
export type MapObject = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
tags: any | null
|
tags: string[]
|
||||||
|
depthOffsets: number[]
|
||||||
originX: number
|
originX: number
|
||||||
originY: number
|
originY: number
|
||||||
frameRate: number
|
frameRate: number
|
||||||
@ -100,7 +101,7 @@ export enum MapEventTileType {
|
|||||||
|
|
||||||
export type MapEventTile = {
|
export type MapEventTile = {
|
||||||
id: string
|
id: string
|
||||||
mapid: string
|
map: string
|
||||||
type: MapEventTileType
|
type: MapEventTileType
|
||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
@ -150,8 +151,9 @@ export type CharacterType = {
|
|||||||
export type CharacterHair = {
|
export type CharacterHair = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
sprite?: Sprite
|
sprite: string | Sprite
|
||||||
gender: CharacterGender
|
gender: CharacterGender
|
||||||
|
color: string
|
||||||
isSelectable: boolean
|
isSelectable: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,6 +210,8 @@ export enum CharacterEquipmentSlotType {
|
|||||||
export type Sprite = {
|
export type Sprite = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
width: number | null
|
||||||
|
height: number | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
spriteActions: SpriteAction[]
|
spriteActions: SpriteAction[]
|
||||||
|
@ -21,7 +21,17 @@ export async function downloadCache<T extends { id: string; updatedAt: Date }>(e
|
|||||||
}
|
}
|
||||||
|
|
||||||
const items = response.data ?? []
|
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) {
|
for (const item of items) {
|
||||||
let overwrite = false
|
let overwrite = false
|
||||||
const existingItem = await storage.getById(item.id)
|
const existingItem = await storage.getById(item.id)
|
||||||
|
@ -124,7 +124,7 @@ button {
|
|||||||
|
|
||||||
&.active,
|
&.active,
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-red-400;
|
@apply bg-red-500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
<Container ref="characterContainer" :x="currentPositionX" :y="currentPositionY" :depth="isometricDepth">
|
<Container ref="characterContainer" :x="currentPositionX" :y="currentPositionY" :depth="isometricDepth">
|
||||||
<ChatBubble :mapCharacter="props.mapCharacter" />
|
<ChatBubble :mapCharacter="props.mapCharacter" />
|
||||||
<HealthBar :mapCharacter="props.mapCharacter" />
|
<HealthBar :mapCharacter="props.mapCharacter" />
|
||||||
<CharacterHair :mapCharacter="props.mapCharacter" />
|
<CharacterHair :mapCharacter="props.mapCharacter" :flipX="isFlippedX" />
|
||||||
<Sprite ref="characterSprite" :origin-y="1" :flipX="isFlippedX" />
|
<Sprite ref="characterSprite" :flipX="isFlippedX" />
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -84,13 +84,14 @@ watch(
|
|||||||
rotation: props.mapCharacter.character.rotation,
|
rotation: props.mapCharacter.character.rotation,
|
||||||
isAttacking: props.mapCharacter.isAttacking
|
isAttacking: props.mapCharacter.isAttacking
|
||||||
}),
|
}),
|
||||||
(oldValues, newValues) => {
|
async (oldValues, newValues) => {
|
||||||
handlePositionUpdate(oldValues, newValues)
|
handlePositionUpdate(oldValues, newValues)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await initializeSprite()
|
await initializeSprite()
|
||||||
|
|
||||||
if (props.mapCharacter.character.id === gameStore.character!.id) {
|
if (props.mapCharacter.character.id === gameStore.character!.id) {
|
||||||
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
|
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
|
||||||
}
|
}
|
||||||
|
@ -1,54 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<Image v-bind="imageProps" v-if="gameStore.isTextureLoaded(texture)" />
|
<Image ref="image" v-if="hairSpriteId" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
|
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
|
||||||
import { loadSpriteTextures } from '@/services/textureService'
|
import { loadSpriteTextures } from '@/services/textureService'
|
||||||
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
|
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { Image, refObj, useScene } from 'phavuer'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mapCharacter: MapCharacter
|
mapCharacter: MapCharacter
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const hairSpriteId = ref('')
|
const hairSpriteId = ref('')
|
||||||
const sprite = ref<SpriteT | null>(null)
|
const hairSprite = ref<SpriteT | null>(null)
|
||||||
|
const characterSpriteHeight = ref(0)
|
||||||
|
const image = refObj<Phaser.GameObjects.Image>()
|
||||||
|
|
||||||
|
const flipX = computed(() => [6, 0].includes(props.mapCharacter.character.rotation ?? 0))
|
||||||
const texture = computed(() => {
|
const texture = computed(() => {
|
||||||
const { rotation } = props.mapCharacter.character
|
const direction = flipX.value ? 'back' : 'front'
|
||||||
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
|
||||||
|
|
||||||
return `${hairSpriteId.value}-${direction}`
|
return `${hairSpriteId.value}-${direction}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
|
watch(
|
||||||
|
() => props.mapCharacter.character,
|
||||||
const imageProps = computed(() => {
|
(newValue) => {
|
||||||
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
if (!image.value) return
|
||||||
const spriteAction = sprite.value?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
image.value.setTexture(texture.value)
|
||||||
|
},
|
||||||
return {
|
{ deep: true }
|
||||||
depth: 9999,
|
)
|
||||||
originX: Number(spriteAction?.originX) ?? 0,
|
|
||||||
originY: Number(spriteAction?.originY) ?? 0,
|
|
||||||
flipX: isFlippedX.value,
|
|
||||||
texture: texture.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const characterHairStorage = new CharacterHairStorage()
|
if (!props.mapCharacter.character.characterType || !props.mapCharacter.character.characterHair) return
|
||||||
const spriteId = await characterHairStorage.getSpriteId(props.mapCharacter.character.characterHair!)
|
|
||||||
if (!spriteId) return
|
|
||||||
|
|
||||||
hairSpriteId.value = spriteId
|
const characterTypeStorage = new CharacterTypeStorage()
|
||||||
|
const characterHairStorage = new CharacterHairStorage()
|
||||||
const spriteStorage = new SpriteStorage()
|
const spriteStorage = new SpriteStorage()
|
||||||
sprite.value = await spriteStorage.getById(spriteId)
|
|
||||||
await loadSpriteTextures(scene, spriteId)
|
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)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { onClickOutside, useFocus } from '@vueuse/core'
|
import { onClickOutside, useFocus } from '@vueuse/core'
|
||||||
@ -30,7 +31,6 @@ import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const mapStore = useMapStore()
|
|
||||||
|
|
||||||
const message = ref('')
|
const message = ref('')
|
||||||
const chats = ref<{ character: string; message: string }[]>([])
|
const chats = ref<{ character: string; message: string }[]>([])
|
||||||
@ -55,7 +55,7 @@ function unfocusChat(event: Event, targetElement: HTMLElement) {
|
|||||||
|
|
||||||
const sendMessage = () => {
|
const sendMessage = () => {
|
||||||
if (!message.value.trim()) return
|
if (!message.value.trim()) return
|
||||||
gameStore.connection?.emit(SocketEvent.CHAT_MESSAGE, { message: message.value }, (response: boolean) => {})
|
socketManager.emit(SocketEvent.CHAT_MESSAGE, { message: message.value }, (response: boolean) => {})
|
||||||
message.value = ''
|
message.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ const scrollToBottom = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.on(SocketEvent.CHAT_MESSAGE, (data: { character: string; message: string }) => {
|
socketManager.on(SocketEvent.CHAT_MESSAGE, (data: { character: string; message: string }) => {
|
||||||
if (!data.character || !data.message) return
|
if (!data.character || !data.message) return
|
||||||
|
|
||||||
chats.value.push({ character: data.character, message: data.message })
|
chats.value.push({ character: data.character, message: data.message })
|
||||||
@ -153,7 +153,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
gameStore.connection?.off(SocketEvent.CHAT_MESSAGE)
|
socketManager.off(SocketEvent.CHAT_MESSAGE)
|
||||||
removeEventListener('keydown', focusChat)
|
removeEventListener('keydown', focusChat)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useDateFormat } from '@vueuse/core'
|
import { useDateFormat } from '@vueuse/core'
|
||||||
import { onUnmounted } from 'vue'
|
import { onUnmounted } from 'vue'
|
||||||
@ -15,6 +16,6 @@ import { onUnmounted } from 'vue'
|
|||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
gameStore.connection?.off(SocketEvent.DATE)
|
socketManager.off(SocketEvent.DATE)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { MapCharacter, UUID } from '@/application/types'
|
import type { MapCharacter, UUID } from '@/application/types'
|
||||||
import Character from '@/components/game/character/Character.vue'
|
import Character from '@/components/game/character/Character.vue'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { onUnmounted } from 'vue'
|
import { onUnmounted } from 'vue'
|
||||||
@ -17,32 +18,32 @@ const props = defineProps<{
|
|||||||
tileMap: Phaser.Tilemaps.Tilemap
|
tileMap: Phaser.Tilemaps.Tilemap
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_JOIN, (data: MapCharacter) => {
|
socketManager.on(SocketEvent.MAP_CHARACTER_JOIN, (data: MapCharacter) => {
|
||||||
mapStore.addCharacter(data)
|
mapStore.addCharacter(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_LEAVE, (characterId: UUID) => {
|
socketManager.on(SocketEvent.MAP_CHARACTER_LEAVE, (characterId: UUID) => {
|
||||||
mapStore.removeCharacter(characterId)
|
mapStore.removeCharacter(characterId)
|
||||||
})
|
})
|
||||||
|
|
||||||
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_MOVE, (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
|
socketManager.on(SocketEvent.MAP_CHARACTER_MOVE, ([characterId, posX, posY, rot, isMoving]: [UUID, number, number, number, boolean]) => {
|
||||||
mapStore.updateCharacterPosition(data)
|
mapStore.updateCharacterPosition([characterId, posX, posY, rot, isMoving])
|
||||||
// @TODO: Replace with universal class, composable or store
|
|
||||||
if (data.characterId === gameStore.character?.id) {
|
if (characterId === gameStore.character?.id) {
|
||||||
gameStore.character!.positionX = data.positionX
|
gameStore.character!.positionX = posX
|
||||||
gameStore.character!.positionY = data.positionY
|
gameStore.character!.positionY = posY
|
||||||
gameStore.character!.rotation = data.rotation
|
gameStore.character!.rotation = rot
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_ATTACK, (characterId: UUID) => {
|
socketManager.on(SocketEvent.MAP_CHARACTER_ATTACK, (characterId: UUID) => {
|
||||||
mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
|
mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_ATTACK)
|
socketManager.off(SocketEvent.MAP_CHARACTER_ATTACK)
|
||||||
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_MOVE)
|
socketManager.off(SocketEvent.MAP_CHARACTER_MOVE)
|
||||||
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_JOIN)
|
socketManager.off(SocketEvent.MAP_CHARACTER_JOIN)
|
||||||
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_LEAVE)
|
socketManager.off(SocketEvent.MAP_CHARACTER_LEAVE)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -11,6 +11,7 @@ import { unduplicateArray } from '@/application/utilities'
|
|||||||
import Characters from '@/components/game/map/Characters.vue'
|
import Characters from '@/components/game/map/Characters.vue'
|
||||||
import MapTiles from '@/components/game/map/MapTiles.vue'
|
import MapTiles from '@/components/game/map/MapTiles.vue'
|
||||||
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
|
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { createTileLayer, createTileMap, loadTileTexturesFromMapTileArray } from '@/services/mapService'
|
import { createTileLayer, createTileMap, loadTileTexturesFromMapTileArray } from '@/services/mapService'
|
||||||
import { MapStorage } from '@/storage/storages'
|
import { MapStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
@ -29,7 +30,7 @@ const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
|||||||
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_TELEPORT, (data: mapLoadData) => {
|
socketManager.on(SocketEvent.MAP_CHARACTER_TELEPORT, (data: mapLoadData) => {
|
||||||
mapStore.setMapId(data.mapId)
|
mapStore.setMapId(data.mapId)
|
||||||
mapStore.setCharacters(data.characters)
|
mapStore.setCharacters(data.characters)
|
||||||
})
|
})
|
||||||
@ -65,6 +66,6 @@ onUnmounted(() => {
|
|||||||
tileMap.value.destroy()
|
tileMap.value.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_TELEPORT)
|
socketManager.off(SocketEvent.MAP_CHARACTER_TELEPORT)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
102
src/components/game/map/partials/ImageGroup.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<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,16 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<Image v-if="mapObject && gameStore.isTextureLoaded(props.placedMapObject.mapObject as string)" v-bind="imageProps" />
|
<ImageGroup v-bind="groupProps" v-if="mapObject && gameStore.isTextureLoaded(props.placedMapObject.mapObject as string)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
|
||||||
import type { MapObject, PlacedMapObject } from '@/application/types'
|
import type { MapObject, PlacedMapObject } from '@/application/types'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import ImageGroup from '@/components/game/map/partials/ImageGroup.vue'
|
||||||
import { calculateIsometricDepth, loadMapObjectTextures, tileToWorldXY } from '@/services/mapService'
|
import { loadMapObjectTextures, tileToWorldXY } from '@/services/mapService'
|
||||||
import { MapObjectStorage } from '@/storage/storages'
|
import { MapObjectStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import Tilemap = Phaser.Tilemaps.Tilemap
|
import Tilemap = Phaser.Tilemaps.Tilemap
|
||||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||||
@ -24,10 +23,15 @@ const props = defineProps<{
|
|||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
|
|
||||||
const mapObject = ref<MapObject>()
|
const mapObject = ref<MapObject>()
|
||||||
|
|
||||||
|
const groupProps = computed(() => ({
|
||||||
|
...calculateObjectPlacement(props.placedMapObject),
|
||||||
|
mapObj: mapObject.value,
|
||||||
|
obj: props.placedMapObject
|
||||||
|
}))
|
||||||
|
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
if (!props.placedMapObject.mapObject) return
|
if (!props.placedMapObject.mapObject) return
|
||||||
|
|
||||||
@ -44,6 +48,8 @@ async function initialize() {
|
|||||||
const _mapObject = await mapObjectStorage.getById(props.placedMapObject.mapObject as string)
|
const _mapObject = await mapObjectStorage.getById(props.placedMapObject.mapObject as string)
|
||||||
if (!_mapObject) return
|
if (!_mapObject) return
|
||||||
|
|
||||||
|
console.log(_mapObject)
|
||||||
|
|
||||||
mapObject.value = _mapObject
|
mapObject.value = _mapObject
|
||||||
|
|
||||||
await loadMapObjectTextures([_mapObject], scene)
|
await loadMapObjectTextures([_mapObject], scene)
|
||||||
@ -53,29 +59,11 @@ function calculateObjectPlacement(mapObj: PlacedMapObject): { x: number; y: numb
|
|||||||
let position = tileToWorldXY(props.tileMapLayer, mapObj.positionX, mapObj.positionY)
|
let position = tileToWorldXY(props.tileMapLayer, mapObj.positionX, mapObj.positionY)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: position.worldPositionX - mapObject.value!.frameWidth / 2,
|
x: position.worldPositionX,
|
||||||
y: position.worldPositionY - mapObject.value!.frameHeight / 2 + config.tile_size.height
|
y: position.worldPositionY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 () => {
|
onMounted(async () => {
|
||||||
await initialize()
|
await initialize()
|
||||||
})
|
})
|
||||||
|
@ -20,12 +20,29 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<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>
|
<label for="spriteId">Sprite</label>
|
||||||
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
||||||
<option disabled selected value="">Select sprite</option>
|
<option disabled selected value="">Select sprite</option>
|
||||||
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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-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>
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterHair">Remove</button>
|
||||||
</form>
|
</form>
|
||||||
@ -34,49 +51,50 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
|
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 { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
const selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
|
const selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
|
||||||
|
|
||||||
const characterName = ref('')
|
const characterName = ref('')
|
||||||
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
||||||
|
const characterColor = ref<string>('#000000')
|
||||||
const characterIsSelectable = ref<boolean>(false)
|
const characterIsSelectable = ref<boolean>(false)
|
||||||
const characterSpriteId = ref<string | null | undefined>(null)
|
const characterSpriteId = ref<string | null | undefined>(null)
|
||||||
|
|
||||||
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
||||||
|
|
||||||
if (!selectedCharacterHair.value) {
|
|
||||||
console.error('No character hair selected')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCharacterHair.value) {
|
if (selectedCharacterHair.value) {
|
||||||
characterName.value = selectedCharacterHair.value.name
|
characterName.value = selectedCharacterHair.value.name
|
||||||
characterGender.value = selectedCharacterHair.value.gender
|
characterGender.value = selectedCharacterHair.value.gender
|
||||||
|
characterColor.value = selectedCharacterHair.value.color
|
||||||
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
|
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
|
||||||
characterSpriteId.value = selectedCharacterHair.value.sprite?.id
|
characterSpriteId.value = selectedCharacterHair.value.sprite?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCharacterHair() {
|
async function removeCharacterHair() {
|
||||||
if (!selectedCharacterHair.value) return
|
if (!selectedCharacterHair.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_REMOVE, { id: selectedCharacterHair.value.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_REMOVE, { id: selectedCharacterHair.value.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove character hair')
|
console.error('Failed to remove character hair')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshCharacterHairList()
|
|
||||||
|
await downloadCache('character_hair', new CharacterHairStorage())
|
||||||
|
await refreshCharacterHairList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
async function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||||
assetManagerStore.setCharacterHairList(response)
|
assetManagerStore.setCharacterHairList(response)
|
||||||
|
|
||||||
if (unsetSelectedCharacterHair) {
|
if (unsetSelectedCharacterHair) {
|
||||||
@ -85,21 +103,24 @@ function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCharacterHair() {
|
async function saveCharacterHair() {
|
||||||
const characterHairData = {
|
const characterHairData = {
|
||||||
id: selectedCharacterHair.value!.id,
|
id: selectedCharacterHair.value!.id,
|
||||||
name: characterName.value,
|
name: characterName.value,
|
||||||
gender: characterGender.value,
|
gender: characterGender.value,
|
||||||
|
color: characterColor.value,
|
||||||
isSelectable: characterIsSelectable.value,
|
isSelectable: characterIsSelectable.value,
|
||||||
spriteId: characterSpriteId.value
|
spriteId: characterSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_UPDATE, characterHairData, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_UPDATE, characterHairData, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save character type')
|
console.error('Failed to save character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshCharacterHairList(false)
|
|
||||||
|
await downloadCache('character_hair', new CharacterHairStorage())
|
||||||
|
await refreshCharacterHairList(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +128,7 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
|
|||||||
if (!characterHair) return
|
if (!characterHair) return
|
||||||
characterName.value = characterHair.name
|
characterName.value = characterHair.name
|
||||||
characterGender.value = characterHair.gender
|
characterGender.value = characterHair.gender
|
||||||
|
characterColor.value = characterHair.color
|
||||||
characterIsSelectable.value = characterHair.isSelectable
|
characterIsSelectable.value = characterHair.isSelectable
|
||||||
characterSpriteId.value = characterHair.sprite?.id
|
characterSpriteId.value = characterHair.sprite?.id
|
||||||
})
|
})
|
||||||
@ -114,7 +136,7 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedCharacterHair.value) return
|
if (!selectedCharacterHair.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { CharacterHair } from '@/application/types'
|
import type { CharacterHair } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -53,13 +54,13 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewCharacterHair = () => {
|
const createNewCharacterHair = () => {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_CREATE, {}, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_CREATE, {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to create new character type')
|
console.error('Failed to create new character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||||
assetManagerStore.setCharacterHairList(response)
|
assetManagerStore.setCharacterHairList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -93,7 +94,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||||
assetManagerStore.setCharacterHairList(response)
|
assetManagerStore.setCharacterHairList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -42,11 +42,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
|
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 { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
const selectedCharacterType = computed(() => assetManagerStore.selectedCharacterType)
|
const selectedCharacterType = computed(() => assetManagerStore.selectedCharacterType)
|
||||||
@ -72,20 +73,22 @@ if (selectedCharacterType.value) {
|
|||||||
characterSpriteId.value = selectedCharacterType.value.sprite?.id
|
characterSpriteId.value = selectedCharacterType.value.sprite?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCharacterType() {
|
async function removeCharacterType() {
|
||||||
if (!selectedCharacterType.value) return
|
if (!selectedCharacterType.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_REMOVE, { id: selectedCharacterType.value.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_REMOVE, { id: selectedCharacterType.value.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove character type')
|
console.error('Failed to remove character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshCharacterTypeList()
|
|
||||||
|
await downloadCache('character_types', new CharacterTypeStorage())
|
||||||
|
await refreshCharacterTypeList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
async function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||||
assetManagerStore.setCharacterTypeList(response)
|
assetManagerStore.setCharacterTypeList(response)
|
||||||
|
|
||||||
if (unsetSelectedCharacterType) {
|
if (unsetSelectedCharacterType) {
|
||||||
@ -94,7 +97,7 @@ function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCharacterType() {
|
async function saveCharacterType() {
|
||||||
const characterTypeData = {
|
const characterTypeData = {
|
||||||
id: selectedCharacterType.value!.id,
|
id: selectedCharacterType.value!.id,
|
||||||
name: characterName.value,
|
name: characterName.value,
|
||||||
@ -104,12 +107,14 @@ function saveCharacterType() {
|
|||||||
spriteId: characterSpriteId.value
|
spriteId: characterSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_UPDATE, characterTypeData, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_UPDATE, characterTypeData, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save character type')
|
console.error('Failed to save character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshCharacterTypeList(false)
|
|
||||||
|
await downloadCache('character_types', new CharacterTypeStorage())
|
||||||
|
await refreshCharacterTypeList(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +130,7 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedCharacterType.value) return
|
if (!selectedCharacterType.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { CharacterType } from '@/application/types'
|
import type { CharacterType } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -53,13 +54,13 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewCharacterType = () => {
|
const createNewCharacterType = () => {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_CREATE, {}, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_CREATE, {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to create new character type')
|
console.error('Failed to create new character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||||
assetManagerStore.setCharacterTypeList(response)
|
assetManagerStore.setCharacterTypeList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -93,7 +94,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||||
assetManagerStore.setCharacterTypeList(response)
|
assetManagerStore.setCharacterTypeList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
|
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
@ -81,7 +82,7 @@ if (selectedItem.value) {
|
|||||||
function removeItem() {
|
function removeItem() {
|
||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_ITEM_REMOVE, { id: selectedItem.value.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_ITEM_REMOVE, { id: selectedItem.value.id }, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove item')
|
console.error('Failed to remove item')
|
||||||
return
|
return
|
||||||
@ -91,7 +92,7 @@ function removeItem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshItemList(unsetSelectedItem = true) {
|
function refreshItemList(unsetSelectedItem = true) {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||||
assetManagerStore.setItemList(response)
|
assetManagerStore.setItemList(response)
|
||||||
|
|
||||||
if (unsetSelectedItem) {
|
if (unsetSelectedItem) {
|
||||||
@ -111,7 +112,7 @@ function saveItem() {
|
|||||||
spriteId: itemSpriteId.value
|
spriteId: itemSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_ITEM_UPDATE, itemData, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_ITEM_UPDATE, itemData, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save item')
|
console.error('Failed to save item')
|
||||||
return
|
return
|
||||||
@ -133,7 +134,7 @@ watch(selectedItem, (item: Item | null) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Item } from '@/application/types'
|
import type { Item } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -49,13 +50,13 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewItem = () => {
|
const createNewItem = () => {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_ITEM_CREATE, {}, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_ITEM_CREATE, {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to create new item')
|
console.error('Failed to create new item')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||||
assetManagerStore.setItemList(response)
|
assetManagerStore.setItemList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -89,7 +90,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||||
assetManagerStore.setItemList(response)
|
assetManagerStore.setItemList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,8 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<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="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||||
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" />
|
<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>
|
||||||
</div>
|
</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">
|
<div class="mt-5 block">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
@ -46,23 +68,32 @@
|
|||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { MapObject } from '@/application/types'
|
import type { MapObject } from '@/application/types'
|
||||||
|
import { downloadCache } from '@/application/utilities'
|
||||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { MapObjectStorage } from '@/storage/storages'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useElementSize } from '@vueuse/core'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { Rectangle } from 'phavuer'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
|
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
|
||||||
|
const svg = useTemplateRef('svg')
|
||||||
|
const { width, height } = useElementSize(svg)
|
||||||
|
|
||||||
const mapObjectName = ref('')
|
const mapObjectName = ref('')
|
||||||
const mapObjectTags = ref<string[]>([])
|
const mapObjectTags = ref<string[]>([])
|
||||||
|
const mapObjectDepthOffsets = ref<number[]>([])
|
||||||
const mapObjectOriginX = ref(0)
|
const mapObjectOriginX = ref(0)
|
||||||
const mapObjectOriginY = ref(0)
|
const mapObjectOriginY = ref(0)
|
||||||
const mapObjectFrameRate = ref(0)
|
const mapObjectFrameRate = ref(0)
|
||||||
const mapObjectFrameWidth = ref(0)
|
const mapObjectFrameWidth = ref(0)
|
||||||
const mapObjectFrameHeight = ref(0)
|
const mapObjectFrameHeight = ref(0)
|
||||||
|
const imageRef = ref<HTMLImageElement | null>(null)
|
||||||
|
const showOrigin = ref(true)
|
||||||
|
const showPartitionOverlay = ref(true)
|
||||||
|
|
||||||
if (!selectedMapObject.value) {
|
if (!selectedMapObject.value) {
|
||||||
console.error('No map mapObject selected')
|
console.error('No map mapObject selected')
|
||||||
@ -71,6 +102,7 @@ if (!selectedMapObject.value) {
|
|||||||
if (selectedMapObject.value) {
|
if (selectedMapObject.value) {
|
||||||
mapObjectName.value = selectedMapObject.value.name
|
mapObjectName.value = selectedMapObject.value.name
|
||||||
mapObjectTags.value = selectedMapObject.value.tags
|
mapObjectTags.value = selectedMapObject.value.tags
|
||||||
|
mapObjectDepthOffsets.value = selectedMapObject.value.depthOffsets
|
||||||
mapObjectOriginX.value = selectedMapObject.value.originX
|
mapObjectOriginX.value = selectedMapObject.value.originX
|
||||||
mapObjectOriginY.value = selectedMapObject.value.originY
|
mapObjectOriginY.value = selectedMapObject.value.originY
|
||||||
mapObjectFrameRate.value = selectedMapObject.value.frameRate
|
mapObjectFrameRate.value = selectedMapObject.value.frameRate
|
||||||
@ -78,18 +110,23 @@ if (selectedMapObject.value) {
|
|||||||
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
|
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeObject() {
|
const setPartitionDepth = (event: any, idx: number) => (mapObjectDepthOffsets.value[idx] = Number.parseInt(event.target.value))
|
||||||
gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_REMOVE, { mapObject: selectedMapObject.value?.id }, (response: boolean) => {
|
|
||||||
|
async function removeObject() {
|
||||||
|
if (!selectedMapObject.value) return
|
||||||
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_REMOVE, { mapObjectId: selectedMapObject.value.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove mapObject')
|
console.error('Failed to remove mapObject')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshObjectList()
|
|
||||||
|
await downloadCache('map_objects', new MapObjectStorage())
|
||||||
|
await refreshObjectList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshObjectList(unsetSelectedMapObject = true) {
|
async function refreshObjectList(unsetSelectedMapObject = true) {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||||
assetManagerStore.setMapObjectList(response)
|
assetManagerStore.setMapObjectList(response)
|
||||||
|
|
||||||
if (unsetSelectedMapObject) {
|
if (unsetSelectedMapObject) {
|
||||||
@ -98,30 +135,32 @@ function refreshObjectList(unsetSelectedMapObject = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveObject() {
|
async function saveObject() {
|
||||||
if (!selectedMapObject.value) {
|
if (!selectedMapObject.value) {
|
||||||
console.error('No mapObject selected')
|
console.error('No mapObject selected')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
socketManager.emit(
|
||||||
gameStore.connection?.emit(
|
SocketEvent.GM_MAPOBJECT_UPDATE,
|
||||||
'gm:mapObject:update',
|
|
||||||
{
|
{
|
||||||
id: selectedMapObject.value.id,
|
id: selectedMapObject.value.id,
|
||||||
name: mapObjectName.value,
|
name: mapObjectName.value,
|
||||||
tags: mapObjectTags.value,
|
tags: mapObjectTags.value,
|
||||||
|
depthOffsets: mapObjectDepthOffsets.value,
|
||||||
originX: mapObjectOriginX.value,
|
originX: mapObjectOriginX.value,
|
||||||
originY: mapObjectOriginY.value,
|
originY: mapObjectOriginY.value,
|
||||||
frameRate: mapObjectFrameRate.value,
|
frameRate: mapObjectFrameRate.value,
|
||||||
frameWidth: mapObjectFrameWidth.value,
|
frameWidth: mapObjectFrameWidth.value,
|
||||||
frameHeight: mapObjectFrameHeight.value
|
frameHeight: mapObjectFrameHeight.value
|
||||||
},
|
},
|
||||||
(response: boolean) => {
|
async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save mapObject')
|
console.error('Failed to save mapObject')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshObjectList(false)
|
|
||||||
|
await downloadCache('map_objects', new MapObjectStorage())
|
||||||
|
await refreshObjectList(false)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -130,6 +169,7 @@ watch(selectedMapObject, (mapObject: MapObject | null) => {
|
|||||||
if (!mapObject) return
|
if (!mapObject) return
|
||||||
mapObjectName.value = mapObject.name
|
mapObjectName.value = mapObject.name
|
||||||
mapObjectTags.value = mapObject.tags
|
mapObjectTags.value = mapObject.tags
|
||||||
|
mapObjectDepthOffsets.value = mapObject.depthOffsets
|
||||||
mapObjectOriginX.value = mapObject.originX
|
mapObjectOriginX.value = mapObject.originX
|
||||||
mapObjectOriginY.value = mapObject.originY
|
mapObjectOriginY.value = mapObject.originY
|
||||||
mapObjectFrameRate.value = mapObject.frameRate
|
mapObjectFrameRate.value = mapObject.frameRate
|
||||||
@ -141,7 +181,37 @@ onMounted(() => {
|
|||||||
if (!selectedMapObject.value) return
|
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(() => {
|
onBeforeUnmount(() => {
|
||||||
assetManagerStore.setSelectedMapObject(null)
|
assetManagerStore.setSelectedMapObject(null)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pointer-events-none {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { MapObject } from '@/application/types'
|
import type { MapObject } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -48,13 +49,13 @@ const elementToScroll = ref()
|
|||||||
const handleFileUpload = (e: Event) => {
|
const handleFileUpload = (e: Event) => {
|
||||||
const files = (e.target as HTMLInputElement).files
|
const files = (e.target as HTMLInputElement).files
|
||||||
if (!files) return
|
if (!files) return
|
||||||
gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_UPLOAD, files, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_UPLOAD, files, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
if (config.environment === 'development') console.error('Failed to upload map object')
|
if (config.environment === 'development') console.error('Failed to upload map object')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||||
assetManagerStore.setMapObjectList(response)
|
assetManagerStore.setMapObjectList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -93,7 +94,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||||
assetManagerStore.setMapObjectList(response)
|
assetManagerStore.setMapObjectList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="relative flex flex-col">
|
<div class="relative flex flex-col">
|
||||||
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray">
|
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray mb-4">
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<label class="mb-1.5 font-titles" for="name">Name</label>
|
<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" />
|
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
<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-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>
|
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
||||||
@ -15,53 +14,39 @@
|
|||||||
<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" />
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn-cyan px-4" type="button" @click.prevent="addNewImage">New action</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-for="action in spriteActions" :key="action.id">
|
||||||
<button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button>
|
<div class="flex flex-wrap gap-3 mb-3">
|
||||||
<Accordion v-for="action in spriteActions" :key="action.id">
|
<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">
|
||||||
<template #header>
|
<img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" />
|
||||||
<div class="flex items-center">
|
<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>
|
||||||
{{ 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>
|
</div>
|
||||||
</template>
|
<div class="flex items-center mb-3">
|
||||||
<template #content>
|
<div class="mr-3 space-x-2">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveSprite">
|
<button class="btn-cyan px-4 py-1.5 min-w-24 text-left" type="button" @click.stop.prevent="openEditorModal(action)">
|
||||||
<div class="form-field-full">
|
Editor
|
||||||
<label for="action">Action</label>
|
<div class="flex">
|
||||||
<input v-model="action.action" class="input-field" type="text" name="action" placeholder="Action" />
|
<small class="text-xs font-default">{{ action.action }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
</button>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
</div>
|
||||||
<div class="form-field-full">
|
<SpriteEditor
|
||||||
<SpriteActionsInput v-model="action.sprites" @tempOffsetChange="(index, offset) => handleTempOffsetChange(action, index, offset)" />
|
v-for="[actionId, editorData] in Array.from(openEditors.entries())"
|
||||||
</div>
|
:key="actionId"
|
||||||
</form>
|
:sprite="selectedSprite!"
|
||||||
</template>
|
:sprites="editorData.action.sprites"
|
||||||
</Accordion>
|
:frame-rate="editorData.action.frameRate"
|
||||||
<SpritePreview
|
:is-modal-open="editorData.isOpen"
|
||||||
v-if="selectedAction"
|
:temp-offset-index="getTempOffsetIndex(editorData.action)"
|
||||||
:sprites="selectedAction.sprites"
|
:temp-offset="getTempOffset(editorData.action)"
|
||||||
:frame-rate="selectedAction.frameRate"
|
@update:frame-rate="(value) => updateFrameRate(editorData.action, value)"
|
||||||
:is-modal-open="isModalOpen"
|
@update:is-modal-open="(value) => handleEditorModalClose(editorData.action, value)"
|
||||||
:temp-offset-index="tempOffsetData.index"
|
@update:temp-offset="(index, offset) => handleTempOffsetChange(editorData.action, index, offset)"
|
||||||
:temp-offset="tempOffsetData.offset"
|
|
||||||
@update:frame-rate="updateFrameRate"
|
|
||||||
@update:is-modal-open="isModalOpen = $event"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -69,24 +54,22 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Sprite, SpriteAction, UUID } from '@/application/types'
|
import type { Sprite, SpriteAction } from '@/application/types'
|
||||||
import { uuidv4 } from '@/application/utilities'
|
import { downloadCache, uuidv4 } from '@/application/utilities'
|
||||||
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
import SpriteEditor from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteEditor.vue'
|
||||||
import SpritePreview from '@/components/gameMaster/assetManager/partials/sprite/partials/SpritePreview.vue'
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import Accordion from '@/components/utilities/Accordion.vue'
|
import { SpriteStorage } from '@/storage/storages'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
const selectedSprite = computed(() => assetManagerStore.selectedSprite)
|
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 spriteName = ref('')
|
||||||
const spriteActions = ref<SpriteAction[]>([])
|
const spriteActions = ref<SpriteAction[]>([])
|
||||||
const isModalOpen = ref(false)
|
|
||||||
const selectedAction = ref<SpriteAction | null>(null)
|
const openEditors = ref(new Map<string, { action: SpriteAction; isOpen: boolean }>())
|
||||||
|
|
||||||
if (!selectedSprite.value) {
|
if (!selectedSprite.value) {
|
||||||
console.error('No sprite selected')
|
console.error('No sprite selected')
|
||||||
@ -97,28 +80,32 @@ if (selectedSprite.value) {
|
|||||||
spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
|
spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSprite() {
|
async function deleteSprite() {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_DELETE, { id: selectedSprite.value?.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_DELETE, { id: selectedSprite.value?.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to delete sprite')
|
console.error('Failed to delete sprite')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshSpriteList()
|
|
||||||
|
await downloadCache('sprites', new SpriteStorage())
|
||||||
|
await refreshSpriteList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function copySprite() {
|
async function copySprite() {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_COPY, { id: selectedSprite.value?.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_COPY, { id: selectedSprite.value?.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to copy sprite')
|
console.error('Failed to copy sprite')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshSpriteList(false)
|
|
||||||
|
await downloadCache('sprites', new SpriteStorage())
|
||||||
|
await refreshSpriteList(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshSpriteList(unsetSelectedSprite = true) {
|
async function refreshSpriteList(unsetSelectedSprite = true) {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
|
|
||||||
if (unsetSelectedSprite) {
|
if (unsetSelectedSprite) {
|
||||||
@ -127,7 +114,7 @@ function refreshSpriteList(unsetSelectedSprite = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSprite() {
|
async function saveSprite() {
|
||||||
if (!selectedSprite.value) {
|
if (!selectedSprite.value) {
|
||||||
console.error('No sprite selected')
|
console.error('No sprite selected')
|
||||||
return
|
return
|
||||||
@ -150,12 +137,14 @@ function saveSprite() {
|
|||||||
}) ?? []
|
}) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_UPDATE, updatedSprite, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_UPDATE, updatedSprite, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save sprite')
|
console.error('Failed to save sprite')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshSpriteList(false)
|
|
||||||
|
await downloadCache('sprites', new SpriteStorage())
|
||||||
|
await refreshSpriteList(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,39 +175,69 @@ function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
|
|||||||
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPreviewModal(action: SpriteAction) {
|
function openEditorModal(action: SpriteAction) {
|
||||||
selectedAction.value = action
|
const newOpenEditors = new Map(openEditors.value)
|
||||||
isModalOpen.value = true
|
newOpenEditors.set(action.id, { action, isOpen: true })
|
||||||
|
openEditors.value = newOpenEditors
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFrameRate(value: number) {
|
function updateFrameRate(action: SpriteAction, value: number) {
|
||||||
if (selectedAction.value) {
|
console.log('update frame rate', action)
|
||||||
selectedAction.value.frameRate = value
|
action.frameRate = value
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempOffsetData = ref<{ index: number | undefined; offset: { x: number; y: number } | undefined }>({
|
function handleEditorModalClose(action: SpriteAction, isOpen: boolean) {
|
||||||
index: undefined,
|
if (isOpen) return
|
||||||
offset: undefined
|
const newOpenEditors = new Map(openEditors.value)
|
||||||
})
|
newOpenEditors.delete(action.id)
|
||||||
|
openEditors.value = newOpenEditors
|
||||||
|
}
|
||||||
|
|
||||||
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
|
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
|
||||||
if (selectedAction.value === action) {
|
// Update the temporary offset data for this action
|
||||||
tempOffsetData.value = { index, offset }
|
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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => {
|
watch(selectedSprite, (sprite: Sprite | null) => {
|
||||||
if (!sprite) return
|
if (!sprite) return
|
||||||
spriteName.value = sprite.name
|
spriteName.value = sprite.name
|
||||||
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
||||||
|
openEditors.value = new Map()
|
||||||
|
tempOffsetData.value = new Map() // Reset temp offset data when sprite changes
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(isModalOpen, (newValue) => {
|
interface SpriteImage {
|
||||||
if (!newValue) {
|
url: string
|
||||||
selectedAction.value = null
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedSprite.value) return
|
if (!selectedSprite.value) return
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Sprite } from '@/application/types'
|
import type { Sprite } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -41,13 +42,13 @@ const hasScrolled = ref(false)
|
|||||||
const elementToScroll = ref()
|
const elementToScroll = ref()
|
||||||
|
|
||||||
function newButtonClickHandler() {
|
function newButtonClickHandler() {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_CREATE, {}, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_CREATE, {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
if (config.environment === 'development') console.error('Failed to create new sprite')
|
if (config.environment === 'development') console.error('Failed to create new sprite')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -86,7 +87,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,366 @@
|
|||||||
|
<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>
|
@ -1,185 +0,0 @@
|
|||||||
<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>
|
|
@ -1,151 +0,0 @@
|
|||||||
<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,15 +26,14 @@
|
|||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Tile } from '@/application/types'
|
import type { Tile } from '@/application/types'
|
||||||
|
import { downloadCache } from '@/application/utilities'
|
||||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { TileStorage } from '@/storage/storages'
|
import { TileStorage } from '@/storage/storages'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
const tileStorage = new TileStorage()
|
|
||||||
|
|
||||||
const selectedTile = computed(() => assetManagerStore.selectedTile)
|
const selectedTile = computed(() => assetManagerStore.selectedTile)
|
||||||
|
|
||||||
@ -57,18 +56,19 @@ watch(selectedTile, (tile: Tile | null) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function deleteTile() {
|
async function deleteTile() {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_TILE_DELETE, { id: selectedTile.value?.id }, async (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_TILE_DELETE, { id: selectedTile.value?.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to delete tile')
|
console.error('Failed to delete tile')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await tileStorage.delete(selectedTile.value!.id)
|
|
||||||
refreshTileList()
|
await downloadCache('tiles', new TileStorage())
|
||||||
|
await refreshTileList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshTileList(unsetSelectedTile = true) {
|
async function refreshTileList(unsetSelectedTile = true) {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||||
assetManagerStore.setTileList(response)
|
assetManagerStore.setTileList(response)
|
||||||
|
|
||||||
if (unsetSelectedTile) {
|
if (unsetSelectedTile) {
|
||||||
@ -77,25 +77,27 @@ function refreshTileList(unsetSelectedTile = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTile() {
|
async function saveTile() {
|
||||||
if (!selectedTile.value) {
|
if (!selectedTile.value) {
|
||||||
console.error('No tile selected')
|
console.error('No tile selected')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit(
|
socketManager.emit(
|
||||||
'gm:tile:update',
|
'gm:tile:update',
|
||||||
{
|
{
|
||||||
id: selectedTile.value.id,
|
id: selectedTile.value.id,
|
||||||
name: tileName.value,
|
name: tileName.value,
|
||||||
tags: tileTags.value
|
tags: tileTags.value
|
||||||
},
|
},
|
||||||
(response: boolean) => {
|
async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save tile')
|
console.error('Failed to save tile')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshTileList(false)
|
|
||||||
|
await downloadCache('tiles', new TileStorage())
|
||||||
|
await refreshTileList(false)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Tile } from '@/application/types'
|
import type { Tile } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -48,13 +49,13 @@ const elementToScroll = ref()
|
|||||||
const handleFileUpload = (e: Event) => {
|
const handleFileUpload = (e: Event) => {
|
||||||
const files = (e.target as HTMLInputElement).files
|
const files = (e.target as HTMLInputElement).files
|
||||||
if (!files) return
|
if (!files) return
|
||||||
gameStore.connection?.emit(SocketEvent.GM_TILE_UPLOAD, files, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_TILE_UPLOAD, files, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
if (config.environment === 'development') console.error('Failed to upload tile')
|
if (config.environment === 'development') console.error('Failed to upload tile')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||||
assetManagerStore.setTileList(response)
|
assetManagerStore.setTileList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -93,7 +94,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||||
assetManagerStore.setTileList(response)
|
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 MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
|
||||||
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
|
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { cloneArray, createTileArray, createTileLayer, createTileMap, placeTiles } from '@/services/mapService'
|
import { cloneArray, createTileLayer, createTileMap, placeTiles } from '@/services/mapService'
|
||||||
import { TileStorage } from '@/storage/storages'
|
import { TileStorage } from '@/storage/storages'
|
||||||
import { useManualRefHistory, useRefHistory } from '@vueuse/core'
|
import { useRefHistory } from '@vueuse/core'
|
||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
|
import { onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ let commandIndex = ref(0)
|
|||||||
|
|
||||||
let originTiles: string[][] = []
|
let originTiles: string[][] = []
|
||||||
let originEventTiles: MapEventTile[] = []
|
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 })
|
const { undo, redo, commit, pause, resume, canUndo, canRedo } = useRefHistory(originObjects, { deep: true, capacity: 9 })
|
||||||
|
|
||||||
@ -58,6 +58,7 @@ watch(
|
|||||||
mapTiles.value!.clearTiles()
|
mapTiles.value!.clearTiles()
|
||||||
eventTiles.value!.clearTiles()
|
eventTiles.value!.clearTiles()
|
||||||
mapEditor.currentMap.value.placedMapObjects = []
|
mapEditor.currentMap.value.placedMapObjects = []
|
||||||
|
mapEditor.currentMap.value.mapEventTiles = []
|
||||||
updateAndCommit(mapEditor.currentMap.value)
|
updateAndCommit(mapEditor.currentMap.value)
|
||||||
mapEditor.resetClearTilesFlag()
|
mapEditor.resetClearTilesFlag()
|
||||||
}
|
}
|
||||||
@ -234,6 +235,10 @@ onUnmounted(() => {
|
|||||||
mapEditor.reset()
|
mapEditor.reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
scene.children.queueDepthSort()
|
||||||
|
}, 0.2)
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
removeEventListener('keydown', handleKeyDown)
|
removeEventListener('keydown', handleKeyDown)
|
||||||
})
|
})
|
||||||
|
@ -46,7 +46,9 @@ class EventTileCommand implements EditorCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCommandUpdate(tile?: MapEventTile, operation: 'draw' | 'erase' | 'clear') {
|
function createCommandUpdate(tile?: MapEventTile | null, operation: 'draw' | 'erase' | 'clear' = 'draw') {
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
if (!currentCommand) {
|
if (!currentCommand) {
|
||||||
currentCommand = new EventTileCommand(operation)
|
currentCommand = new EventTileCommand(operation)
|
||||||
}
|
}
|
||||||
@ -86,19 +88,17 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
|||||||
if (existingEventTile) return
|
if (existingEventTile) return
|
||||||
|
|
||||||
// If teleport, check if there is a selected map
|
// If teleport, check if there is a selected map
|
||||||
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMapId) return
|
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMap) return
|
||||||
|
|
||||||
const newEventTile = {
|
const newEventTile = {
|
||||||
id: uuidv4() as UUID,
|
id: uuidv4() as UUID,
|
||||||
mapId: map.id,
|
|
||||||
map: map,
|
|
||||||
type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
|
type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
|
||||||
positionX: tile.x,
|
positionX: tile.x,
|
||||||
positionY: tile.y,
|
positionY: tile.y,
|
||||||
teleport:
|
teleport:
|
||||||
mapEditor.drawMode.value === 'teleport'
|
mapEditor.drawMode.value === 'teleport'
|
||||||
? {
|
? {
|
||||||
toMap: mapEditor.teleportSettings.value.toMapId,
|
toMap: mapEditor.teleportSettings.value.toMap,
|
||||||
toPositionX: mapEditor.teleportSettings.value.toPositionX,
|
toPositionX: mapEditor.teleportSettings.value.toPositionX,
|
||||||
toPositionY: mapEditor.teleportSettings.value.toPositionY,
|
toPositionY: mapEditor.teleportSettings.value.toPositionY,
|
||||||
toRotation: mapEditor.teleportSettings.value.toRotation
|
toRotation: mapEditor.teleportSettings.value.toRotation
|
||||||
@ -106,9 +106,9 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
createCommandUpdate(newEventTile, 'draw')
|
createCommandUpdate(newEventTile as MapEventTile, 'draw')
|
||||||
|
|
||||||
map.mapEventTiles.push(newEventTile)
|
map.mapEventTiles.push(newEventTile as MapEventTile)
|
||||||
}
|
}
|
||||||
|
|
||||||
function erase(pointer: Phaser.Input.Pointer, map: MapT) {
|
function erase(pointer: Phaser.Input.Pointer, map: MapT) {
|
||||||
@ -149,7 +149,7 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearTiles() {
|
function clearTiles() {
|
||||||
if (mapEditor.currentMap.value.mapEventTiles.length === 0) return
|
if (mapEditor.currentMap.value?.mapEventTiles.length === 0) return
|
||||||
createCommandUpdate(null, 'clear')
|
createCommandUpdate(null, 'clear')
|
||||||
finalizeCommand()
|
finalizeCommand()
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
:placedMapObject="previewPlacedMapObject as PlacedMapObjectT"
|
:placedMapObject="previewPlacedMapObject as PlacedMapObjectT"
|
||||||
/>
|
/>
|
||||||
<SelectedPlacedMapObjectComponent v-if="mapEditor.selectedPlacedObject.value" :key="mapEditor.selectedPlacedObject.value.id" :map :placedMapObject="mapEditor.selectedPlacedObject.value" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
|
<SelectedPlacedMapObjectComponent v-if="mapEditor.selectedPlacedObject.value" :key="mapEditor.selectedPlacedObject.value.id" :map :placedMapObject="mapEditor.selectedPlacedObject.value" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
|
||||||
<PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap :tileMapLayer :placedMapObject @pointerdown="clickPlacedMapObject(placedMapObject)" />
|
<PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap :tileMapLayer :placedMapObject @pointerdown="clickPlacedMapObject(placedMapObject)" :key="`${placedMapObject.id}-${placedMapObjectKey}`" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -26,6 +26,7 @@ import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
|||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
const map = computed(() => mapEditor.currentMap.value!)
|
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 }>()
|
const emit = defineEmits<{ (e: 'update', map: MapT): void; (e: 'updateAndCommit', map: MapT): void; (e: 'pauseObjectTracking'): void; (e: 'resumeObjectTracking'): void }>()
|
||||||
|
|
||||||
@ -129,7 +130,7 @@ function moveMapObject(id: string, map: MapT) {
|
|||||||
|
|
||||||
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
console.log(id)
|
|
||||||
map.placedMapObjects.map((placed) => {
|
map.placedMapObjects.map((placed) => {
|
||||||
if (placed.id === id) {
|
if (placed.id === id) {
|
||||||
placed.positionX = tile.x
|
placed.positionX = tile.x
|
||||||
|
@ -39,6 +39,7 @@ import { SocketEvent } from '@/application/enums'
|
|||||||
import type { Map } from '@/application/types'
|
import type { Map } from '@/application/types'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { MapStorage } from '@/storage/storages'
|
import { MapStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { ref, useTemplateRef } from 'vue'
|
import { ref, useTemplateRef } from 'vue'
|
||||||
@ -57,7 +58,7 @@ const pvp = ref(false)
|
|||||||
defineExpose({ open: () => modalRef.value?.open() })
|
defineExpose({ open: () => modalRef.value?.open() })
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_MAP_CREATE, { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => {
|
socketManager.emit(SocketEvent.GM_MAP_CREATE, { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal ref="modalRef" :is-resizable="false" :modal-width="300" :modal-height="360" bg-style="none">
|
<Modal ref="modalRef" :is-resizable="false" :modal-width="300" :modal-height="360" bg-style="none">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg text-white">Maps</h3>
|
<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>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
<div class="my-4 mx-auto h-full">
|
<div class="mx-auto h-full">
|
||||||
<div class="text-center mb-4 px-2 flex gap-2.5">
|
<div class="overflow-y-auto h-[calc(100%)]">
|
||||||
<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="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)">
|
<div class="flex gap-3 items-center w-full" @click="() => loadMap(map.id)">
|
||||||
<span>{{ map.name }}</span>
|
<span>{{ map.name }}</span>
|
||||||
<span class="ml-auto gap-1 flex">
|
<span class="ml-auto gap-1 flex">
|
||||||
@ -24,7 +22,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<CreateMap ref="createMapModal" @create="fetchMaps" />
|
<CreateMap ref="createMapModal" @create="fetchMaps" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -34,6 +31,7 @@ import type { Map } from '@/application/types'
|
|||||||
import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue'
|
import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { MapStorage } from '@/storage/storages'
|
import { MapStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onMounted, ref, useTemplateRef } from 'vue'
|
import { onMounted, ref, useTemplateRef } from 'vue'
|
||||||
@ -61,14 +59,14 @@ async function fetchMaps() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadMap(id: string) {
|
function loadMap(id: string) {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_MAP_REQUEST, { mapId: id }, (response: Map) => {
|
socketManager.emit(SocketEvent.GM_MAP_REQUEST, { mapId: id }, (response: Map) => {
|
||||||
mapEditor.loadMap(response)
|
mapEditor.loadMap(response)
|
||||||
})
|
})
|
||||||
modalRef.value?.close()
|
modalRef.value?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteMap(id: string) {
|
async function deleteMap(id: string) {
|
||||||
gameStore.connection?.emit(SocketEvent.GM_MAP_DELETE, { mapId: id }, async (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_MAP_DELETE, { mapId: id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
gameStore.addNotification({
|
gameStore.addNotification({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object'">
|
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object'">
|
||||||
<div class="flex flex-col gap-2.5 p-2.5">
|
<div class="flex flex-col gap-2.5 p-2.5">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="flex-grow">
|
||||||
<div class="relative flex">
|
<div class="relative flex">
|
||||||
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
|
<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" />
|
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
||||||
</div>
|
</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">
|
<div class="flex">
|
||||||
<select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
|
<select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
|
||||||
<option value="tile">Tiles</option>
|
<option value="tile">Tiles</option>
|
||||||
|
@ -45,6 +45,7 @@ import { SocketEvent } from '@/application/enums'
|
|||||||
import type { MapObject, Map as MapT, PlacedMapObject } from '@/application/types'
|
import type { MapObject, Map as MapT, PlacedMapObject } from '@/application/types'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { MapObjectStorage } from '@/storage/storages'
|
import { MapObjectStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
@ -81,7 +82,7 @@ const handleDelete = () => {
|
|||||||
async function handleUpdate() {
|
async function handleUpdate() {
|
||||||
if (!mapObject.value) return
|
if (!mapObject.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit(
|
socketManager.emit(
|
||||||
SocketEvent.GM_MAPOBJECT_UPDATE,
|
SocketEvent.GM_MAPOBJECT_UPDATE,
|
||||||
{
|
{
|
||||||
id: props.placedMapObject.mapObject as string,
|
id: props.placedMapObject.mapObject as string,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal ref="modalRef" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" bg-style="none">
|
<Modal v-if="showTeleportModal" ref="modalRef" @modal:close="() => mapEditor.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" bg-style="none">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
<label for="toMap">Map to teleport to</label>
|
<label for="toMap">Map to teleport to</label>
|
||||||
<select v-model="toMap" class="input-field" name="toMap" id="toMap">
|
<select v-model="toMap" class="input-field" name="toMap" id="toMap">
|
||||||
<option :value="null">Select map</option>
|
<option :value="null">Select map</option>
|
||||||
<option v-for="map in mapList" :key="map.id" :value="map">{{ map.name }}</option>
|
<option v-for="map in mapList" :key="map.id" :value="map.id">{{ map.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -39,51 +39,50 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import type { Map } from '@/application/types'
|
import type { Map } from '@/application/types'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
import { MapStorage } from '@/storage/storages'
|
||||||
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
|
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||||
|
|
||||||
const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport')
|
const showTeleportModal = computed(() => mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'teleport')
|
||||||
const mapEditorStore = useMapEditorStore()
|
const mapStorage = new MapStorage()
|
||||||
const gameStore = useGameStore()
|
const mapEditor = useMapEditorComposable()
|
||||||
const mapList = ref<Map[]>([])
|
|
||||||
const modalRef = useTemplateRef('modalRef')
|
const modalRef = useTemplateRef('modalRef')
|
||||||
|
const mapList = ref<Map[]>([])
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
open: () => modalRef.value?.open()
|
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()
|
const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings()
|
||||||
|
|
||||||
function useRefTeleportSettings() {
|
function useRefTeleportSettings() {
|
||||||
const settings = mapEditorStore.teleportSettings
|
const settings = mapEditor.teleportSettings.value
|
||||||
return {
|
return {
|
||||||
toPositionX: ref(settings.toPositionX),
|
toPositionX: ref(settings.toPositionX),
|
||||||
toPositionY: ref(settings.toPositionY),
|
toPositionY: ref(settings.toPositionY),
|
||||||
toRotation: ref(settings.toRotation),
|
toRotation: ref(settings.toRotation),
|
||||||
toMap: ref(settings.toMapId)
|
toMap: ref(settings.toMap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([toPositionX, toPositionY, toRotation, toMap], updateTeleportSettings)
|
watch([toPositionX, toPositionY, toRotation, toMap], updateTeleportSettings)
|
||||||
|
|
||||||
function updateTeleportSettings() {
|
function updateTeleportSettings() {
|
||||||
mapEditorStore.setTeleportSettings({
|
mapEditor.setTeleportSettings({
|
||||||
toPositionX: toPositionX.value,
|
toPositionX: toPositionX.value,
|
||||||
toPositionY: toPositionY.value,
|
toPositionY: toPositionY.value,
|
||||||
toRotation: toRotation.value,
|
toRotation: toRotation.value,
|
||||||
toMapId: toMap.value
|
toMap: toMap.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchMaps() {
|
||||||
|
mapList.value = await mapStorage.getAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchMaps()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="(mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'tile') || mapEditor.tool.value === 'paint'">
|
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="(mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'tile') || mapEditor.tool.value === 'paint'">
|
||||||
<div class="flex flex-col gap-2.5 p-2.5">
|
<div class="flex flex-col gap-2.5 p-2.5">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="flex-grow">
|
||||||
<div class="relative flex">
|
<div class="relative flex">
|
||||||
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
|
<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" />
|
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
||||||
</div>
|
</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">
|
<div class="flex">
|
||||||
<select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
|
<select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
|
||||||
<option value="tile">Tiles</option>
|
<option value="tile">Tiles</option>
|
||||||
|
@ -117,7 +117,7 @@ const selectPencilOpen = ref(false)
|
|||||||
const selectEraserOpen = ref(false)
|
const selectEraserOpen = ref(false)
|
||||||
const isContinuousDrawingEnabled = ref<Boolean>(false)
|
const isContinuousDrawingEnabled = ref<Boolean>(false)
|
||||||
const isShowPlacedMapObjectPreviewEnabled = ref<Boolean>(mapEditor.isPlacedMapObjectPreviewEnabled.value)
|
const isShowPlacedMapObjectPreviewEnabled = ref<Boolean>(mapEditor.isPlacedMapObjectPreviewEnabled.value)
|
||||||
const listOpen = computed(() => mapEditor.tool.value === 'pencil' && (mapEditor.drawMode.value === 'tile' || mapEditor.drawMode.value === 'map_object'))
|
const listOpen = computed(() => (mapEditor.tool.value === 'pencil' && (mapEditor.drawMode.value === 'tile' || mapEditor.drawMode.value === 'map_object')) || mapEditor.tool.value === 'paint')
|
||||||
|
|
||||||
// drawMode
|
// drawMode
|
||||||
function setDrawMode(value: string) {
|
function setDrawMode(value: string) {
|
||||||
@ -148,6 +148,7 @@ function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
|
|||||||
|
|
||||||
function handleClick(tool: string) {
|
function handleClick(tool: string) {
|
||||||
mapEditor.setTool(tool)
|
mapEditor.setTool(tool)
|
||||||
|
if (tool === 'paint') mapEditor.setDrawMode('tile')
|
||||||
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
|
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
|
||||||
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
|
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
|
||||||
if (mapEditor.drawMode.value === 'teleport') emit('open-teleport')
|
if (mapEditor.drawMode.value === 'teleport') emit('open-teleport')
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { login } from '@/services/authenticationService'
|
import { login } from '@/services/authenticationService'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
@ -53,7 +54,7 @@ async function submit() {
|
|||||||
formError.value = response.error
|
formError.value = response.error
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gameStore.setToken(response.token)
|
socketManager.setToken(response.token)
|
||||||
gameStore.initConnection()
|
gameStore.initConnection()
|
||||||
return true // Indicate success
|
return true // Indicate success
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { login, register } from '@/services/authenticationService'
|
import { login, register } from '@/services/authenticationService'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
@ -67,7 +68,7 @@ async function submit() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.setToken(loginResponse.token)
|
socketManager.setToken(loginResponse.token)
|
||||||
gameStore.initConnection()
|
gameStore.initConnection()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -10,8 +10,9 @@
|
|||||||
<div class="filler"></div>
|
<div class="filler"></div>
|
||||||
<div class="w-2/3 max-w-[860px]" v-if="!isLoading">
|
<div class="w-2/3 max-w-[860px]" v-if="!isLoading">
|
||||||
<div class="mb-5 flex flex-col gap-1">
|
<div class="mb-5 flex flex-col gap-1">
|
||||||
<h1 class="text-white font-bold">SELECT CHARACTER TO PLAY</h1>
|
<h1 class="text-white font-bold">{{ isCreatingCharacter ? 'CREATE CHARACTER' : 'SELECT CHARACTER TO PLAY' }}</h1>
|
||||||
<p class="m-0">Maximum of 4 characters can be created per player</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full max-lg:flex-col lg:h-[400px] default-border rounded-md bg-gray">
|
<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">
|
<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">
|
||||||
@ -20,14 +21,15 @@
|
|||||||
<img src="/assets/placeholders/head.png" class="w-9 h-9 object-contain center-element" alt="Player head" />
|
<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" />
|
<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>
|
||||||
<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">
|
<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="isCreateNewCharacterModalOpen = true">
|
<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" />
|
<img class="w-6 h-6 object-contain center-element btn-sound" draggable="false" src="/assets/icons/plus-icon.svg" alt="Add character" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6 justify-center" v-if="selectedCharacterId">
|
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6 justify-center">
|
||||||
<input class="input-field w-[158px]" type="text" name="name" :placeholder="characters.find((c) => c.id == selectedCharacterId)?.name" />
|
<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="flex flex-col gap-4 items-center">
|
<div class="flex flex-col gap-4 items-center">
|
||||||
<div class="flex flex-col gap-3">
|
<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">
|
<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">
|
||||||
@ -40,48 +42,74 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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>
|
</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="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">
|
<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="flex flex-col gap-3 w-full">
|
<div class="flex flex-col gap-3 w-full">
|
||||||
<span class="text-sm">Hairstyle</span>
|
<span class="text-sm">Hairstyle</span>
|
||||||
<div class="flex gap-2 flex-wrap max-h-20 overflow-y-auto scrollbar">
|
<div class="flex gap-2 flex-wrap">
|
||||||
<div
|
<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"
|
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" />
|
<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" />
|
<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>
|
</div>
|
||||||
<!-- TODO #255: make radio button so we can set a value, do the same with swatches -->
|
|
||||||
<div
|
<div
|
||||||
v-for="hair in characterHairs"
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<img class="h-4 object-contain" :src="config.server_endpoint + '/textures/sprites/' + hair.sprite + '/front.png'" alt="Hair sprite" />
|
<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>
|
||||||
<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" />
|
<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>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -89,77 +117,102 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
|
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading">
|
<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-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>
|
<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>
|
||||||
|
<template v-else>
|
||||||
<template #modalBody>
|
<button class="btn-empty min-w-48" @click="cancelCharacterCreation">Cancel</button>
|
||||||
<div class="p-4 h-[calc(100%_-_32px)]">
|
<button class="btn-cyan min-w-48" @click="createCharacter">Create</button>
|
||||||
<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>
|
</template>
|
||||||
</Modal>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types'
|
import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
|
||||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||||
import { CharacterHairStorage } from '@/storage/storages'
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { CharacterHairStorage, CharacterTypeStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const characterCreationSettings = {
|
||||||
|
maxCharacters: 4,
|
||||||
|
minNameLength: 3,
|
||||||
|
maxNameLength: 20,
|
||||||
|
defaultGender: 'MALE' as const
|
||||||
|
}
|
||||||
|
|
||||||
const { playSound } = useSoundComposable()
|
const { playSound } = useSoundComposable()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const isLoading = ref<boolean>(true)
|
const isLoading = ref<boolean>(true)
|
||||||
const characters = ref<CharacterT[]>([])
|
const characters = ref<CharacterT[]>([])
|
||||||
const selectedCharacterId = ref<string | null>(null)
|
const selectedCharacterId = ref<string | null>(null)
|
||||||
const isCreateNewCharacterModalOpen = ref<boolean>(false)
|
const newNickname = ref<string>('')
|
||||||
const newCharacterName = ref<string>('')
|
const newCharacterName = ref<string>('')
|
||||||
const characterHairs = ref<CharacterHair[]>([])
|
const characterHairs = ref<CharacterHair[]>([])
|
||||||
const selectedHairId = ref<string | null>(null)
|
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
|
// Fetch characters
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log(SocketEvent.CHARACTER_LIST)
|
socketManager.emit(SocketEvent.CHARACTER_LIST)
|
||||||
gameStore.connection?.emit(SocketEvent.CHARACTER_LIST)
|
|
||||||
}, 750)
|
}, 750)
|
||||||
|
|
||||||
gameStore.connection?.on(SocketEvent.CHARACTER_LIST, (data: any) => {
|
socketManager.on(SocketEvent.CHARACTER_LIST, (data: any) => {
|
||||||
characters.value = data
|
characters.value = data
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
// Select character logics
|
|
||||||
function loginWithCharacter() {
|
function loginWithCharacter() {
|
||||||
if (!selectedCharacterId.value) return
|
if (!selectedCharacterId.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit(
|
socketManager.emit(
|
||||||
SocketEvent.CHARACTER_CONNECT,
|
SocketEvent.CHARACTER_CONNECT,
|
||||||
{
|
{
|
||||||
characterId: selectedCharacterId.value,
|
characterId: selectedCharacterId.value,
|
||||||
characterHairId: selectedHairId.value
|
characterHairId: selectedHairId.value,
|
||||||
|
newNickname: newNickname.value
|
||||||
},
|
},
|
||||||
(response: { character: CharacterT; map: Map; characters: CharacterT[] }) => {
|
(response: { character: CharacterT; map: Map; characters: CharacterT[] }) => {
|
||||||
gameStore.setCharacter(response.character)
|
gameStore.setCharacter(response.character)
|
||||||
@ -169,27 +222,51 @@ function loginWithCharacter() {
|
|||||||
|
|
||||||
// Create character logics
|
// Create character logics
|
||||||
function createCharacter() {
|
function createCharacter() {
|
||||||
gameStore.connection?.emit(SocketEvent.CHARACTER_CREATE, { name: newCharacterName.value }, (success: boolean) => {
|
if (newCharacterName.value.length < characterCreationSettings.minNameLength || newCharacterName.value.length > characterCreationSettings.maxNameLength) {
|
||||||
if (success) return
|
return
|
||||||
isCreateNewCharacterModalOpen.value = false
|
}
|
||||||
})
|
|
||||||
|
socketManager.emit(
|
||||||
|
SocketEvent.CHARACTER_CREATE,
|
||||||
|
{
|
||||||
|
name: newCharacterName.value,
|
||||||
|
gender: selectedGender.value,
|
||||||
|
hairId: selectedHairId.value
|
||||||
|
},
|
||||||
|
(success: boolean) => {
|
||||||
|
if (success) {
|
||||||
|
cancelCharacterCreation()
|
||||||
|
socketManager.emit(SocketEvent.CHARACTER_LIST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch changes for selected character and update hairs
|
// Watch changes for selected character and update hairs
|
||||||
watch(selectedCharacterId, (characterId) => {
|
watch(selectedCharacterId, (characterId) => {
|
||||||
if (!characterId) return
|
if (!characterId) return
|
||||||
|
newNickname.value = ''
|
||||||
|
isCreatingCharacter.value = false
|
||||||
selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHair ?? null
|
selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHair ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
playSound('/assets/music/intro.mp3')
|
await playSound('/assets/music/intro.mp3')
|
||||||
const characterHairStorage = new CharacterHairStorage()
|
const characterHairStorage = new CharacterHairStorage()
|
||||||
|
const characterTypeStorage = new CharacterTypeStorage()
|
||||||
characterHairs.value = await characterHairStorage.getAll()
|
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(() => {
|
onBeforeUnmount(() => {
|
||||||
gameStore.connection?.off(SocketEvent.CHARACTER_LIST)
|
socketManager.off(SocketEvent.CHARACTER_LIST)
|
||||||
gameStore.connection?.off(SocketEvent.CHARACTER_CONNECT)
|
socketManager.off(SocketEvent.CHARACTER_CONNECT)
|
||||||
gameStore.connection?.off(SocketEvent.CHARACTER_CREATE)
|
socketManager.off(SocketEvent.CHARACTER_CREATE)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -20,8 +20,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import 'phaser'
|
import 'phaser'
|
||||||
import type { Map as MapT } from '@/application/types'
|
import type { Map as MapT } from '@/application/types'
|
||||||
|
import { downloadCache } from '@/application/utilities'
|
||||||
import Map from '@/components/gameMaster/mapEditor/Map.vue'
|
import Map from '@/components/gameMaster/mapEditor/Map.vue'
|
||||||
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
|
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
|
||||||
import MapObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
|
import MapObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
|
||||||
@ -32,13 +34,10 @@ import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
|
|||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { loadAllTileTextures } from '@/services/mapService'
|
import { loadAllTileTextures } from '@/services/mapService'
|
||||||
import { MapStorage } from '@/storage/storages'
|
import { MapStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { Game, Scene } from 'phavuer'
|
import { Game, Scene } from 'phavuer'
|
||||||
import { ref, useTemplateRef } from 'vue'
|
import { ref, toRaw, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
const mapStorage = new MapStorage()
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
const gameStore = useGameStore()
|
|
||||||
|
|
||||||
const mapModal = useTemplateRef('mapModal')
|
const mapModal = useTemplateRef('mapModal')
|
||||||
const mapSettingsModal = useTemplateRef('mapSettingsModal')
|
const mapSettingsModal = useTemplateRef('mapSettingsModal')
|
||||||
@ -83,8 +82,8 @@ const preloadScene = async (scene: Phaser.Scene) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
async function save() {
|
||||||
const currentMap = mapEditor.currentMap.value
|
const currentMap = toRaw(mapEditor.currentMap.value)
|
||||||
if (!currentMap) return
|
if (!currentMap) return
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
@ -92,8 +91,9 @@ function save() {
|
|||||||
mapId: currentMap.id
|
mapId: currentMap.id
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.GM_MAP_UPDATE, data, (response: MapT) => {
|
socketManager.emit(SocketEvent.GM_MAP_UPDATE, data, async (response: MapT) => {
|
||||||
mapStorage.update(response.id, response)
|
if (!response.id) return
|
||||||
|
await downloadCache('maps', new MapStorage())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
<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,10 +1,14 @@
|
|||||||
<template></template>
|
<template></template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SoundStorage, SpriteStorage, TileStorage } from '@/storage/storages'
|
||||||
import { TextureStorage } from '@/storage/textureStorage'
|
import { TextureStorage } from '@/storage/textureStorage'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
const mapStorage = new MapStorage()
|
const mapStorage = new MapStorage()
|
||||||
const tileStorage = new TileStorage()
|
const tileStorage = new TileStorage()
|
||||||
const mapObjectStorage = new MapObjectStorage()
|
const mapObjectStorage = new MapObjectStorage()
|
||||||
@ -37,6 +41,17 @@ async function handleKeyPress(event: KeyboardEvent) {
|
|||||||
currentString = '' // Reset
|
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
|
// Reset string after a certain amount of time
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
currentString = ''
|
currentString = ''
|
||||||
|
@ -12,8 +12,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onBeforeMount, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue'
|
import { onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
@ -36,13 +37,13 @@ function setupNotificationListener(connection: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const connection = gameStore.connection
|
const connection = socketManager.connection
|
||||||
if (connection) {
|
if (connection) {
|
||||||
setupNotificationListener(connection)
|
setupNotificationListener(connection)
|
||||||
} else {
|
} else {
|
||||||
// Watch for changes in the socket connection
|
// Watch for changes in the socket connection
|
||||||
watch(
|
watch(
|
||||||
() => gameStore.connection,
|
() => socketManager.connection,
|
||||||
(newConnection) => {
|
(newConnection) => {
|
||||||
if (newConnection) setupNotificationListener(newConnection)
|
if (newConnection) setupNotificationListener(newConnection)
|
||||||
}
|
}
|
||||||
@ -51,7 +52,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
const connection = gameStore.connection
|
const connection = socketManager.connection
|
||||||
if (connection) {
|
if (connection) {
|
||||||
connection.off(SocketEvent.NOTIFICATION)
|
connection.off(SocketEvent.NOTIFICATION)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { getTile } from '@/services/mapService'
|
import { getTile } from '@/services/mapService'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
@ -7,7 +8,77 @@ 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) {
|
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 gameStore = useGameStore()
|
||||||
const baseHandlers = useBaseControlsComposable(scene, layer, waypoint, camera)
|
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) {
|
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
||||||
baseHandlers.startDragging(pointer)
|
baseHandlers.startDragging(pointer)
|
||||||
}
|
}
|
||||||
@ -23,70 +94,37 @@ export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Til
|
|||||||
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
|
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
|
||||||
if (!pointerTile) return
|
if (!pointerTile) return
|
||||||
|
|
||||||
gameStore.connection?.emit(SocketEvent.MAP_CHARACTER_MOVE, {
|
emitMovement(pointerTile.x, pointerTile.y)
|
||||||
positionX: pointerTile.x,
|
|
||||||
positionY: pointerTile.y
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pressedKeys = new Set<string>()
|
// Keyboard Handlers
|
||||||
let moveInterval: number | null = null
|
|
||||||
|
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
if (!gameStore.character) return
|
if (!gameStore.character) return
|
||||||
|
|
||||||
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
|
if (ARROW_KEYS.includes(event.key as (typeof ARROW_KEYS)[number])) {
|
||||||
// Prevent key repeat events
|
|
||||||
if (event.repeat) return
|
if (event.repeat) return
|
||||||
|
|
||||||
pressedKeys.add(event.key)
|
pressedKeys.add(event.key)
|
||||||
|
updateCurrentPosition()
|
||||||
// Start movement loop if not already running
|
startMovementLoop()
|
||||||
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') {
|
if (event.key === 'Control') {
|
||||||
gameStore.connection?.emit(SocketEvent.MAP_CHARACTER_ATTACK)
|
socketManager.emit(SocketEvent.MAP_CHARACTER_ATTACK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyUp(event: KeyboardEvent) {
|
function handleKeyUp(event: KeyboardEvent) {
|
||||||
pressedKeys.delete(event.key)
|
pressedKeys.delete(event.key)
|
||||||
|
|
||||||
// If no movement keys are pressed, clear the interval
|
if (pressedKeys.size === 0) {
|
||||||
if (pressedKeys.size === 0 && moveInterval) {
|
stopMovementLoop()
|
||||||
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 = () => {
|
const setupControls = () => {
|
||||||
|
updateCurrentPosition() // Initialize position
|
||||||
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
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_MOVE, handlePointerMove)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
@ -96,6 +134,9 @@ export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Til
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cleanupControls = () => {
|
const cleanupControls = () => {
|
||||||
|
stopMovementLoop()
|
||||||
|
pressedKeys.clear()
|
||||||
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
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_MOVE, handlePointerMove)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { Direction } from '@/application/enums'
|
import { Direction } from '@/application/enums'
|
||||||
import { type MapCharacter } from '@/application/types'
|
import { type MapCharacter, type SpriteAction } from '@/application/types'
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/services/mapService'
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/services/mapService'
|
||||||
import { loadSpriteTextures } from '@/services/textureService'
|
import { loadSpriteTextures } from '@/services/textureService'
|
||||||
import { CharacterTypeStorage } from '@/storage/storages'
|
import { CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
|
||||||
import { refObj } from 'phavuer'
|
import { refObj } from 'phavuer'
|
||||||
import { computed, ref } from 'vue'
|
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 tween = ref<Phaser.Tweens.Tween | null>(null)
|
||||||
|
|
||||||
const updateIsometricDepth = (positionX: number, positionY: number) => {
|
const updateIsometricDepth = (positionX: number, positionY: number) => {
|
||||||
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 30, 95)
|
isometricDepth.value = calculateIsometricDepth(positionX, positionY)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePosition = (positionX: number, positionY: number) => {
|
const updatePosition = (positionX: number, positionY: number) => {
|
||||||
@ -72,7 +72,7 @@ export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phase
|
|||||||
// Add new listener
|
// Add new listener
|
||||||
characterSprite.value.on(Phaser.Animations.Events.ANIMATION_COMPLETE, () => {
|
characterSprite.value.on(Phaser.Animations.Events.ANIMATION_COMPLETE, () => {
|
||||||
characterSprite.value!.setFrame(0)
|
characterSprite.value!.setFrame(0)
|
||||||
characterSprite.value!.setTexture(charTexture.value)
|
characterSprite.value!.setTexture(spriteSpriteActionId.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
characterSprite.value.anims.play(
|
characterSprite.value.anims.play(
|
||||||
@ -96,11 +96,23 @@ export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phase
|
|||||||
return [0, 6].includes(mapCharacter.character.rotation ?? 0) ? 'left_up' : 'right_down'
|
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(() => {
|
const currentAction = computed(() => {
|
||||||
return mapCharacter.isMoving ? 'walk' : 'idle'
|
return mapCharacter.isMoving ? 'walk' : 'idle'
|
||||||
})
|
})
|
||||||
|
|
||||||
const charTexture = computed(() => {
|
const spriteSpriteActionId = computed(() => {
|
||||||
const spriteId = characterSpriteId.value ?? 'idle_right_down'
|
const spriteId = characterSpriteId.value ?? 'idle_right_down'
|
||||||
return `${spriteId}-${currentAction.value}_${currentDirection.value}`
|
return `${spriteId}-${currentAction.value}_${currentDirection.value}`
|
||||||
})
|
})
|
||||||
@ -109,11 +121,11 @@ export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phase
|
|||||||
if (!characterSprite.value) return
|
if (!characterSprite.value) return
|
||||||
|
|
||||||
if (mapCharacter.isMoving) {
|
if (mapCharacter.isMoving) {
|
||||||
characterSprite.value.anims.play(charTexture.value, true)
|
characterSprite.value.anims.play(spriteSpriteActionId.value, true)
|
||||||
} else {
|
} else {
|
||||||
characterSprite.value.anims.stop()
|
characterSprite.value.anims.stop()
|
||||||
characterSprite.value.setFrame(0)
|
characterSprite.value.setFrame(0)
|
||||||
characterSprite.value.setTexture(charTexture.value)
|
characterSprite.value.setTexture(spriteSpriteActionId.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +142,8 @@ export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phase
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (characterSprite.value) {
|
if (characterSprite.value) {
|
||||||
characterSprite.value.setTexture(charTexture.value)
|
characterSprite!.value?.setOrigin(0.5, 1)
|
||||||
|
characterSprite.value.setTexture(spriteSpriteActionId.value)
|
||||||
characterSprite.value.setFlipX(isFlippedX.value)
|
characterSprite.value.setFlipX(isFlippedX.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,10 +159,15 @@ export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phase
|
|||||||
characterContainer,
|
characterContainer,
|
||||||
characterSpriteId,
|
characterSpriteId,
|
||||||
characterSprite,
|
characterSprite,
|
||||||
|
spriteHeight,
|
||||||
|
currentAction,
|
||||||
|
spriteSpriteActionId,
|
||||||
currentPositionX,
|
currentPositionX,
|
||||||
currentPositionY,
|
currentPositionY,
|
||||||
|
currentDirection,
|
||||||
isometricDepth,
|
isometricDepth,
|
||||||
isFlippedX,
|
isFlippedX,
|
||||||
|
getSpriteHeightByAction,
|
||||||
updatePosition,
|
updatePosition,
|
||||||
playAnimation,
|
playAnimation,
|
||||||
calcDirection,
|
calcDirection,
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import type { Map, MapObject, PlacedMapObject, UUID } from '@/application/types'
|
import type { Map, MapObject, PlacedMapObject, UUID } from '@/application/types'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export type TeleportSettings = {
|
export type TeleportSettings = {
|
||||||
toMapId: string
|
toMap: string
|
||||||
toPositionX: number
|
toPositionX: number
|
||||||
toPositionY: number
|
toPositionY: number
|
||||||
toRotation: number
|
toRotation: number
|
||||||
@ -20,7 +21,7 @@ const movingPlacedObject = ref<PlacedMapObject | null>(null)
|
|||||||
const selectedPlacedObject = ref<PlacedMapObject | null>(null)
|
const selectedPlacedObject = ref<PlacedMapObject | null>(null)
|
||||||
const shouldClearTiles = ref(false)
|
const shouldClearTiles = ref(false)
|
||||||
const teleportSettings = ref<TeleportSettings>({
|
const teleportSettings = ref<TeleportSettings>({
|
||||||
toMapId: '1000',
|
toMap: '1000',
|
||||||
toPositionX: 0,
|
toPositionX: 0,
|
||||||
toPositionY: 0,
|
toPositionY: 0,
|
||||||
toRotation: 0
|
toRotation: 0
|
||||||
@ -46,6 +47,8 @@ export function useMapEditorComposable() {
|
|||||||
const toggleActive = () => {
|
const toggleActive = () => {
|
||||||
if (active.value) reset()
|
if (active.value) reset()
|
||||||
active.value = !active.value
|
active.value = !active.value
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
gameStore.uiSettings.isGmPanelOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const togglePlacedMapObjectPreview = () => {
|
const togglePlacedMapObjectPreview = () => {
|
||||||
@ -95,6 +98,7 @@ export function useMapEditorComposable() {
|
|||||||
selectedTile.value = ''
|
selectedTile.value = ''
|
||||||
isPlacedMapObjectPreviewEnabled.value = false
|
isPlacedMapObjectPreviewEnabled.value = false
|
||||||
selectedMapObject.value = null
|
selectedMapObject.value = null
|
||||||
|
selectedPlacedObject.value = null
|
||||||
shouldClearTiles.value = false
|
shouldClearTiles.value = false
|
||||||
refreshMapObject.value = 0
|
refreshMapObject.value = 0
|
||||||
}
|
}
|
||||||
|
76
src/managers/SocketManager.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
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,12 +62,8 @@ export function createTileArray(width: number, height: number, tile: string = 'b
|
|||||||
return Array.from({ length: height }, () => Array.from({ length: width }, () => tile))
|
return Array.from({ length: height }, () => Array.from({ length: width }, () => tile))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const calculateIsometricDepth = (positionX: number, positionY: number, width: number = 0, height: number = 0, isCharacter: boolean = false) => {
|
export const calculateIsometricDepth = (positionX: number, positionY: number) => {
|
||||||
const baseDepth = positionX + positionY
|
return positionX + positionY
|
||||||
if (isCharacter) {
|
|
||||||
return baseDepth
|
|
||||||
}
|
|
||||||
return baseDepth + (width + height) / (2 * config.tile_size.width)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTileTextures(tiles: TileT[], scene: Phaser.Scene) {
|
async function loadTileTextures(tiles: TileT[], scene: Phaser.Scene) {
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
import config from '@/application/config'
|
|
||||||
import { SocketEvent } from '@/application/enums'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Character, Notification, User, WorldSettings } from '@/application/types'
|
import type { Character, Notification, User, WorldSettings } from '@/application/types'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { io, Socket } from 'socket.io-client'
|
|
||||||
|
|
||||||
export const useGameStore = defineStore('game', {
|
export const useGameStore = defineStore('game', {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
notifications: [] as Notification[],
|
notifications: [] as Notification[],
|
||||||
token: '',
|
|
||||||
connection: null as Socket | null,
|
|
||||||
user: null as User | null,
|
user: null as User | null,
|
||||||
character: null as Character | null,
|
character: null as Character | null,
|
||||||
world: {
|
world: {
|
||||||
@ -51,9 +47,6 @@ export const useGameStore = defineStore('game', {
|
|||||||
removeNotification(id: string) {
|
removeNotification(id: string) {
|
||||||
this.notifications = this.notifications.filter((notification: Notification) => notification.id !== id)
|
this.notifications = this.notifications.filter((notification: Notification) => notification.id !== id)
|
||||||
},
|
},
|
||||||
setToken(token: string) {
|
|
||||||
this.token = token
|
|
||||||
},
|
|
||||||
setUser(user: User | null) {
|
setUser(user: User | null) {
|
||||||
this.user = user
|
this.user = user
|
||||||
},
|
},
|
||||||
@ -73,49 +66,34 @@ export const useGameStore = defineStore('game', {
|
|||||||
this.uiSettings.isCharacterProfileOpen = !this.uiSettings.isCharacterProfileOpen
|
this.uiSettings.isCharacterProfileOpen = !this.uiSettings.isCharacterProfileOpen
|
||||||
},
|
},
|
||||||
initConnection() {
|
initConnection() {
|
||||||
this.connection = io(config.server_endpoint, {
|
const socket = socketManager.initConnection()
|
||||||
secure: config.environment === 'production',
|
|
||||||
withCredentials: true,
|
|
||||||
transports: ['websocket'],
|
|
||||||
reconnectionAttempts: 5
|
|
||||||
})
|
|
||||||
|
|
||||||
// #99 - If we can't connect, disconnect
|
// Handle connect error
|
||||||
this.connection.on('connect_error', () => {
|
socket.on(SocketEvent.CONNECT_ERROR, () => {
|
||||||
this.disconnectSocket()
|
this.disconnectSocket()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Let the server know the user is logged in
|
// Handle failed reconnection
|
||||||
this.connection.emit(SocketEvent.LOGIN)
|
socket.on(SocketEvent.RECONNECT_FAILED, () => {
|
||||||
|
this.disconnectSocket()
|
||||||
|
})
|
||||||
|
|
||||||
// set user
|
// Emit login event
|
||||||
this.connection.on(SocketEvent.LOGGED_IN, (user: User) => {
|
socketManager.emit(SocketEvent.LOGIN)
|
||||||
|
|
||||||
|
// Handle logged in event
|
||||||
|
socketManager.on(SocketEvent.LOGGED_IN, (user: User) => {
|
||||||
this.setUser(user)
|
this.setUser(user)
|
||||||
})
|
})
|
||||||
|
|
||||||
// When we can't reconnect, disconnect
|
// Handle date updates
|
||||||
this.connection.on('reconnect_failed', () => {
|
socketManager.on(SocketEvent.DATE, (data: Date) => {
|
||||||
this.disconnectSocket()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Listen for new date from socket
|
|
||||||
this.connection.on(SocketEvent.DATE, (data: Date) => {
|
|
||||||
this.world.date = new Date(data)
|
this.world.date = new Date(data)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
disconnectSocket() {
|
disconnectSocket() {
|
||||||
// Remove event listeners
|
socketManager.disconnect()
|
||||||
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.user = null
|
||||||
this.character = null
|
this.character = null
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import type { MapObject, Map as MapT } from '@/application/types'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
export type TeleportSettings = {
|
export type TeleportSettings = {
|
||||||
toMapId: string
|
toMap: string
|
||||||
toPositionX: number
|
toPositionX: number
|
||||||
toPositionY: number
|
toPositionY: number
|
||||||
toRotation: number
|
toRotation: number
|
||||||
@ -18,7 +18,7 @@ export const useMapEditorStore = defineStore('mapEditor', {
|
|||||||
selectedMapObject: null as MapObject | null,
|
selectedMapObject: null as MapObject | null,
|
||||||
shouldClearTiles: false,
|
shouldClearTiles: false,
|
||||||
teleportSettings: {
|
teleportSettings: {
|
||||||
toMapId: '',
|
toMap: '',
|
||||||
toPositionX: 0,
|
toPositionX: 0,
|
||||||
toPositionY: 0,
|
toPositionY: 0,
|
||||||
toRotation: 0
|
toRotation: 0
|
||||||
|
@ -35,13 +35,13 @@ export const useMapStore = defineStore('map', {
|
|||||||
removeCharacter(characterId: UUID) {
|
removeCharacter(characterId: UUID) {
|
||||||
this.characters = this.characters.filter((char) => char.character.id !== characterId)
|
this.characters = this.characters.filter((char) => char.character.id !== characterId)
|
||||||
},
|
},
|
||||||
updateCharacterPosition(data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) {
|
updateCharacterPosition([characterId, posX, posY, rot, isMoving]: [UUID, number, number, number, boolean]) {
|
||||||
const character = this.characters.find((char) => char.character.id === data.characterId)
|
const character = this.characters.find((char) => char.character.id === characterId)
|
||||||
if (character) {
|
if (character) {
|
||||||
character.character.positionX = data.positionX
|
character.character.positionX = posX
|
||||||
character.character.positionY = data.positionY
|
character.character.positionY = posY
|
||||||
character.character.rotation = data.rotation
|
character.character.rotation = rot
|
||||||
character.isMoving = data.isMoving
|
character.isMoving = isMoving
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
|