Compare commits
1 Commits
main
...
feature/#2
Author | SHA1 | Date | |
---|---|---|---|
c1360899e1 |
@ -1,6 +1,5 @@
|
||||
VITE_NAME=Noxious
|
||||
VITE_DOMAIN=localhost
|
||||
VITE_ENVIRONMENT=development
|
||||
VITE_DEVELOPMENT=true
|
||||
VITE_SERVER_ENDPOINT=http://localhost:4000
|
||||
VITE_TILE_SIZE_WIDTH=64
|
||||
VITE_TILE_SIZE_HEIGHT=32
|
||||
VITE_TILE_SIZE_X=64
|
||||
VITE_TILE_SIZE_Y=32
|
13
.eslintrc.cjs
Normal file
@ -0,0 +1,13 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript', '@vue/eslint-config-prettier/skip-formatting'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
},
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off'
|
||||
}
|
||||
}
|
1
.vscode/extensions.json
vendored
@ -1,6 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
|
70
Caddyfile
@ -1,70 +0,0 @@
|
||||
{
|
||||
# Global options
|
||||
admin off # Disable admin API
|
||||
|
||||
# Global logging configuration
|
||||
log {
|
||||
output file /var/log/caddy/access.log
|
||||
format json
|
||||
level INFO
|
||||
}
|
||||
}
|
||||
|
||||
noxious.gg {
|
||||
# Root directory for your Vue app
|
||||
root * ./dist
|
||||
|
||||
# Enable compression with optimal settings
|
||||
encode zstd gzip
|
||||
|
||||
# Handle SPA routing
|
||||
try_files {path} /index.html
|
||||
|
||||
# Serve static files with optimizations
|
||||
file_server
|
||||
|
||||
# Enhanced security headers
|
||||
header {
|
||||
# Existing headers with improvements
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
X-Content-Type-Options "nosniff"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
|
||||
# Additional security headers
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
|
||||
|
||||
# Remove server information
|
||||
-Server
|
||||
}
|
||||
|
||||
# Improved cache configuration for static assets
|
||||
@static {
|
||||
file
|
||||
path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff *.woff2 *.ttf *.eot
|
||||
}
|
||||
header @static {
|
||||
Cache-Control "public, max-age=31536000, immutable"
|
||||
Vary Accept-Encoding
|
||||
}
|
||||
|
||||
# Cache control for HTML files
|
||||
@html {
|
||||
file
|
||||
path *.html
|
||||
}
|
||||
header @html {
|
||||
Cache-Control "no-cache, must-revalidate"
|
||||
}
|
||||
|
||||
# Handle errors
|
||||
handle_errors {
|
||||
respond "{http.error.status_code} {http.error.status_text}" {http.error.status_code}
|
||||
}
|
||||
}
|
||||
|
||||
# Improved redirect configuration
|
||||
www.noxious.gg {
|
||||
redir https://noxious.gg{uri} permanent
|
||||
}
|
32
Dockerfile
Normal file
@ -0,0 +1,32 @@
|
||||
# Build stage
|
||||
FROM node:22.4.1-alpine as builder
|
||||
WORKDIR /usr/src/app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
|
||||
# Set environment variables
|
||||
ARG VITE_NAME=${VITE_NAME}
|
||||
ENV VITE_NAME=${VITE_NAME}
|
||||
|
||||
ARG VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
|
||||
ENV VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
|
||||
|
||||
ARG VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
|
||||
ENV VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
|
||||
|
||||
ARG VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
|
||||
ENV VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
|
||||
|
||||
ARG VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
|
||||
ENV VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
|
||||
|
||||
# Build the application
|
||||
RUN npm run build-ntc
|
||||
|
||||
# Production stage
|
||||
FROM nginx:1.26.1-alpine
|
||||
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
4
captain-definition
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"dockerfilePath" :"./Dockerfile"
|
||||
}
|
16
nginx.conf
Normal file
@ -0,0 +1,16 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Redirect example
|
||||
location /discord {
|
||||
return 301 https://discord.gg/JTev3nzeDa;
|
||||
}
|
||||
|
||||
# Serve static files
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
4748
package-lock.json
generated
28
package.json
@ -11,36 +11,39 @@
|
||||
"test:unit": "vitest",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.5.0",
|
||||
"@vueuse/integrations": "^10.5.0",
|
||||
"axios": "^1.7.9",
|
||||
"dexie": "^4.0.11",
|
||||
"phaser": "^3.88.2",
|
||||
"phaser3-rex-plugins": "^1.80.13",
|
||||
"phavuer": "^0.16.5",
|
||||
"pinia": "^2.3.1",
|
||||
"sharp": "^0.33.5",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"axios": "^1.7.7",
|
||||
"dexie": "^4.0.8",
|
||||
"phaser": "^3.86.0",
|
||||
"pinia": "^2.1.6",
|
||||
"socket.io-client": "^4.8.0",
|
||||
"universal-cookie": "^6.1.3",
|
||||
"vite-plugin-image-optimizer": "^1.1.8",
|
||||
"vue": "^3.5.13",
|
||||
"zod": "^3.24.2"
|
||||
"vue": "^3.5.12",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
||||
"@tauri-apps/cli": "^2.2.7",
|
||||
"@rushstack/eslint-patch": "^1.10.3",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^20.14.11",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"jsdom": "^24.1.1",
|
||||
"npm-run-all2": "^6.2.3",
|
||||
"phaser3-rex-plugins": "^1.80.8",
|
||||
"phavuer": "^0.16.1",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.3.3",
|
||||
"sass": "^1.79.4",
|
||||
@ -48,6 +51,7 @@
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^5.4.9",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-vue-devtools": "^7.5.2",
|
||||
"vitest": "^2.0.3",
|
||||
"vue-tsc": "^1.6.5"
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 9.5L12 14.5L7 9.5" stroke="#fff" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 325 B |
@ -1,3 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.3333 17.5L11.0833 12.25C10.6667 12.5833 10.1875 12.8472 9.64583 13.0417C9.10417 13.2361 8.52778 13.3333 7.91667 13.3333C6.40278 13.3333 5.12153 12.809 4.07292 11.7604C3.02431 10.7118 2.5 9.43056 2.5 7.91667C2.5 6.40278 3.02431 5.12153 4.07292 4.07292C5.12153 3.02431 6.40278 2.5 7.91667 2.5C9.43056 2.5 10.7118 3.02431 11.7604 4.07292C12.809 5.12153 13.3333 6.40278 13.3333 7.91667C13.3333 8.52778 13.2361 9.10417 13.0417 9.64583C12.8472 10.1875 12.5833 10.6667 12.25 11.0833L17.5 16.3333L16.3333 17.5ZM7.91667 11.6667C8.95833 11.6667 9.84375 11.3021 10.5729 10.5729C11.3021 9.84375 11.6667 8.95833 11.6667 7.91667C11.6667 6.875 11.3021 5.98958 10.5729 5.26042C9.84375 4.53125 8.95833 4.16667 7.91667 4.16667C6.875 4.16667 5.98958 4.53125 5.26042 5.26042C4.53125 5.98958 4.16667 6.875 4.16667 7.91667C4.16667 8.95833 4.53125 9.84375 5.26042 10.5729C5.98958 11.3021 6.875 11.6667 7.91667 11.6667Z" fill="#808080"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.0 KiB |
@ -1,59 +0,0 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M490.667,405.333h-56.811C424.619,374.592,396.373,352,362.667,352s-61.931,22.592-71.189,53.333H21.333
|
||||
C9.557,405.333,0,414.891,0,426.667S9.557,448,21.333,448h270.144c9.237,30.741,37.483,53.333,71.189,53.333
|
||||
s61.931-22.592,71.189-53.333h56.811c11.797,0,21.333-9.557,21.333-21.333S502.464,405.333,490.667,405.333z M362.667,458.667
|
||||
c-17.643,0-32-14.357-32-32s14.357-32,32-32s32,14.357,32,32S380.309,458.667,362.667,458.667z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M490.667,64h-56.811c-9.259-30.741-37.483-53.333-71.189-53.333S300.736,33.259,291.477,64H21.333
|
||||
C9.557,64,0,73.557,0,85.333s9.557,21.333,21.333,21.333h270.144C300.736,137.408,328.96,160,362.667,160
|
||||
s61.931-22.592,71.189-53.333h56.811c11.797,0,21.333-9.557,21.333-21.333S502.464,64,490.667,64z M362.667,117.333
|
||||
c-17.643,0-32-14.357-32-32c0-17.643,14.357-32,32-32s32,14.357,32,32C394.667,102.976,380.309,117.333,362.667,117.333z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M490.667,234.667H220.523c-9.259-30.741-37.483-53.333-71.189-53.333s-61.931,22.592-71.189,53.333H21.333
|
||||
C9.557,234.667,0,244.224,0,256c0,11.776,9.557,21.333,21.333,21.333h56.811c9.259,30.741,37.483,53.333,71.189,53.333
|
||||
s61.931-22.592,71.189-53.333h270.144c11.797,0,21.333-9.557,21.333-21.333C512,244.224,502.464,234.667,490.667,234.667z
|
||||
M149.333,288c-17.643,0-32-14.357-32-32s14.357-32,32-32c17.643,0,32,14.357,32,32S166.976,288,149.333,288z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 325 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 847 B |
Before Width: | Height: | Size: 745 B After Width: | Height: | Size: 745 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 946 KiB After Width: | Height: | Size: 1.1 MiB |
BIN
public/assets/music/click-btn.mp3
Normal file
Before Width: | Height: | Size: 454 KiB After Width: | Height: | Size: 453 KiB |
Before Width: | Height: | Size: 109 B After Width: | Height: | Size: 109 B |
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 696 B |
Before Width: | Height: | Size: 708 B After Width: | Height: | Size: 708 B |
4
src-tauri/.gitignore
vendored
@ -1,4 +0,0 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
5149
src-tauri/Cargo.lock
generated
@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.4", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
tauri = { version = "2.2.4", features = [] }
|
||||
tauri-plugin-log = "2.0.0-rc"
|
@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default"
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 49 KiB |
@ -1,16 +0,0 @@
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
.build(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
app_lib::run();
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "noxious",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.noxious.app",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build-only"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Noxious",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
41
src/App.vue
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Debug />
|
||||
<Notifications />
|
||||
<BackgroundImageLoader />
|
||||
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||
<component :is="currentScreen" />
|
||||
</template>
|
||||
@ -9,47 +9,40 @@
|
||||
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
||||
import Characters from '@/components/screens/Characters.vue'
|
||||
import Game from '@/components/screens/Game.vue'
|
||||
import Loading from '@/components/screens/Loading.vue'
|
||||
import Login from '@/components/screens/Login.vue'
|
||||
import MapEditor from '@/components/screens/MapEditor.vue'
|
||||
import Debug from '@/components/utilities/Debug.vue'
|
||||
import ZoneEditor from '@/components/screens/ZoneEditor.vue'
|
||||
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
|
||||
import Notifications from '@/components/utilities/Notifications.vue'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
|
||||
const mapEditor = useMapEditorComposable()
|
||||
const { playSound } = useSoundComposable()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
const currentScreen = computed(() => {
|
||||
if (!gameStore.game.isLoaded) return Loading
|
||||
if (!socketManager.connection) return Login
|
||||
if (!socketManager.token) return Login
|
||||
if (!gameStore.connection) return Login
|
||||
if (!gameStore.token) return Login
|
||||
if (!gameStore.character) return Characters
|
||||
if (mapEditor.active.value) return MapEditor
|
||||
if (zoneEditorStore.active) return ZoneEditor
|
||||
return Game
|
||||
})
|
||||
|
||||
// Watch mapEditor.active and empty gameStore.game.loadedAssets
|
||||
// Watch zoneEditorStore.active and empty gameStore.game.loadedAssets
|
||||
watch(
|
||||
() => mapEditor.active.value,
|
||||
() => zoneEditorStore.active,
|
||||
() => {
|
||||
gameStore.game.loadedTextures = []
|
||||
gameStore.game.loadedAssets = []
|
||||
}
|
||||
)
|
||||
|
||||
// #209: Play sound when a button is pressed
|
||||
addEventListener('click', (event) => {
|
||||
const classList = ['btn-cyan', 'btn-red', 'btn-indigo', 'btn-empty', 'btn-sound']
|
||||
const target = event.target as HTMLElement
|
||||
// console.log(target) // Uncomment to log the clicked element
|
||||
if (classList.some((className) => target.classList.contains(className))) {
|
||||
playSound('/assets/sounds/button-click.wav')
|
||||
if (!(event.target instanceof HTMLButtonElement)) {
|
||||
return
|
||||
}
|
||||
const audio = new Audio('/assets/music/click-btn.mp3')
|
||||
audio.play()
|
||||
})
|
||||
|
||||
// Watch for "G" key press and toggle the gm panel
|
||||
@ -57,9 +50,7 @@ addEventListener('keydown', (event) => {
|
||||
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
|
||||
|
||||
// Check if no input is active or focus is on an input
|
||||
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') {
|
||||
return
|
||||
}
|
||||
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') return
|
||||
|
||||
if (event.key === 'G') {
|
||||
gameStore.toggleGmPanel()
|
||||
|
81
src/application/assets.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import config from '@/application/config'
|
||||
import type { AssetDataT } from '@/application/types'
|
||||
import Dexie from 'dexie'
|
||||
|
||||
export class Assets {
|
||||
private db: Dexie
|
||||
|
||||
constructor() {
|
||||
this.db = new Dexie('assets')
|
||||
this.db.version(1).stores({
|
||||
assets: 'key, group'
|
||||
})
|
||||
}
|
||||
|
||||
async download(asset: AssetDataT) {
|
||||
try {
|
||||
// Check if the asset already exists, then check if updatedAt is newer
|
||||
const _asset = await this.db.table('assets').get(asset.key)
|
||||
if (_asset && _asset.updatedAt > asset.updatedAt) {
|
||||
return
|
||||
}
|
||||
|
||||
// Download the asset
|
||||
const response = await fetch(config.server_endpoint + asset.data)
|
||||
const blob = await response.blob()
|
||||
|
||||
// Store the asset in the database
|
||||
await this.db.table('assets').put({
|
||||
key: asset.key,
|
||||
data: blob,
|
||||
group: asset.group,
|
||||
updatedAt: asset.updatedAt,
|
||||
originX: asset.originX,
|
||||
originY: asset.originY,
|
||||
isAnimated: asset.isAnimated,
|
||||
frameRate: asset.frameRate,
|
||||
frameWidth: asset.frameWidth,
|
||||
frameHeight: asset.frameHeight,
|
||||
frameCount: asset.frameCount
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Failed to add asset ${asset.key}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
async get(key: string) {
|
||||
try {
|
||||
const asset = await this.db.table('assets').get(key)
|
||||
if (asset) {
|
||||
return {
|
||||
...asset,
|
||||
data: URL.createObjectURL(asset.data) // Convert blob to data URL
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to retrieve asset ${key}:`, error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async getByGroup(group: string) {
|
||||
try {
|
||||
const assets = await this.db.table('assets').where('group').equals(group).toArray()
|
||||
return assets.map((asset) => ({
|
||||
...asset,
|
||||
data: URL.createObjectURL(asset.data) // Convert blob to data URL
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error(`Failed to retrieve assets for group ${group}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key: string) {
|
||||
try {
|
||||
await this.db.table('assets').delete(key)
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete asset ${key}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
export default {
|
||||
name: import.meta.env.VITE_NAME,
|
||||
domain: import.meta.env.VITE_DOMAIN,
|
||||
environment: import.meta.env.VITE_ENVIRONMENT,
|
||||
development: import.meta.env.VITE_DEVELOPMENT === 'true',
|
||||
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
|
||||
tile_size: {
|
||||
width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH),
|
||||
height: Number(import.meta.env.VITE_TILE_SIZE_HEIGHT)
|
||||
x: Number(import.meta.env.VITE_TILE_SIZE_X),
|
||||
y: Number(import.meta.env.VITE_TILE_SIZE_Y)
|
||||
}
|
||||
}
|
||||
|
@ -1,63 +0,0 @@
|
||||
export enum Direction {
|
||||
POSITIVE,
|
||||
NEGATIVE,
|
||||
UNCHANGED
|
||||
}
|
||||
|
||||
export enum SocketEvent {
|
||||
CONNECT_ERROR = 'connect_error',
|
||||
RECONNECT_FAILED = 'reconnect_failed',
|
||||
CLOSE = '52',
|
||||
DATA = '51',
|
||||
CHARACTER_CONNECT = '50',
|
||||
CHARACTER_CREATE = '49',
|
||||
CHARACTER_DELETE = '48',
|
||||
CHARACTER_LIST = '47',
|
||||
GM_CHARACTERHAIR_CREATE = '46',
|
||||
GM_CHARACTERHAIR_REMOVE = '45',
|
||||
GM_CHARACTERHAIR_LIST = '44',
|
||||
GM_CHARACTERHAIR_UPDATE = '43',
|
||||
GM_CHARACTERTYPE_CREATE = '42',
|
||||
GM_CHARACTERTYPE_REMOVE = '41',
|
||||
GM_CHARACTERTYPE_LIST = '40',
|
||||
GM_CHARACTERTYPE_UPDATE = '39',
|
||||
GM_ITEM_CREATE = '38',
|
||||
GM_ITEM_REMOVE = '37',
|
||||
GM_ITEM_LIST = '36',
|
||||
GM_ITEM_UPDATE = '35',
|
||||
GM_MAPOBJECT_LIST = '34',
|
||||
GM_MAPOBJECT_REMOVE = '33',
|
||||
GM_MAPOBJECT_UPDATE = '32',
|
||||
GM_MAPOBJECT_UPLOAD = '31',
|
||||
GM_SPRITE_COPY = '30',
|
||||
GM_SPRITE_CREATE = '29',
|
||||
GM_SPRITE_DELETE = '28',
|
||||
GM_SPRITE_LIST = '27',
|
||||
GM_SPRITE_UPDATE = '26',
|
||||
GM_TILE_DELETE = '25',
|
||||
GM_TILE_LIST = '24',
|
||||
GM_TILE_UPDATE = '23',
|
||||
GM_TILE_UPLOAD = '22',
|
||||
GM_MAP_CREATE = '21',
|
||||
GM_MAP_DELETE = '20',
|
||||
GM_MAP_REQUEST = '19',
|
||||
GM_MAP_UPDATE = '18',
|
||||
MAP_CHARACTER_MOVEERROR = '17',
|
||||
DISCONNECT = 'disconnect',
|
||||
USER_DISCONNECT = '15',
|
||||
LOGIN = '14',
|
||||
LOGGED_IN = '13',
|
||||
NOTIFICATION = '12',
|
||||
DATE = '11',
|
||||
FAILED = '10',
|
||||
COMPLETED = '9',
|
||||
CONNECTION = 'connection',
|
||||
WEATHER = '7',
|
||||
CHARACTER_DISCONNECT = '6',
|
||||
MAP_CHARACTER_ATTACK = '5',
|
||||
MAP_CHARACTER_TELEPORT = '4',
|
||||
MAP_CHARACTER_JOIN = '3',
|
||||
MAP_CHARACTER_LEAVE = '2',
|
||||
MAP_CHARACTER_MOVE = '1',
|
||||
CHAT_MESSAGE = '0'
|
||||
}
|
@ -12,13 +12,14 @@ export type HttpResponse<T> = {
|
||||
data?: T
|
||||
}
|
||||
|
||||
export type TextureData = {
|
||||
export type AssetDataT = {
|
||||
key: string
|
||||
data: string // URL or Base64 encoded blob
|
||||
group: 'tiles' | 'map_objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||
data: string
|
||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||
updatedAt: Date
|
||||
originX?: number
|
||||
originY?: number
|
||||
isAnimated?: boolean
|
||||
frameRate?: number
|
||||
frameWidth?: number
|
||||
frameHeight?: number
|
||||
@ -26,34 +27,36 @@ export type TextureData = {
|
||||
}
|
||||
|
||||
export type Tile = {
|
||||
id: string
|
||||
id: UUID
|
||||
name: string
|
||||
tags: any | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export type MapObject = {
|
||||
id: string
|
||||
export type Object = {
|
||||
id: UUID
|
||||
name: string
|
||||
tags: string[]
|
||||
depthOffsets: number[]
|
||||
tags: any | null
|
||||
originX: number
|
||||
originY: number
|
||||
isAnimated: boolean
|
||||
frameRate: number
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
ZoneObject: ZoneObject[]
|
||||
}
|
||||
|
||||
export type Item = {
|
||||
id: string
|
||||
id: UUID
|
||||
name: string
|
||||
description: string | null
|
||||
itemType: ItemType
|
||||
stackable: boolean
|
||||
rarity: ItemRarity
|
||||
spriteId: UUID | null
|
||||
sprite?: Sprite
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
@ -62,63 +65,72 @@ export type Item = {
|
||||
export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVES' | 'RING' | 'NECKLACE'
|
||||
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
|
||||
|
||||
export type Map = {
|
||||
id: string
|
||||
export type Zone = {
|
||||
id: UUID
|
||||
name: string
|
||||
width: number
|
||||
height: number
|
||||
tiles: string[][]
|
||||
tiles: any | null
|
||||
pvp: boolean
|
||||
mapEffects: MapEffect[]
|
||||
mapEventTiles: MapEventTile[]
|
||||
placedMapObjects: PlacedMapObject[]
|
||||
zoneEffects: ZoneEffect[]
|
||||
zoneEventTiles: ZoneEventTile[]
|
||||
zoneObjects: ZoneObject[]
|
||||
characters: Character[]
|
||||
chats: Chat[]
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export type MapEffect = {
|
||||
id: string
|
||||
export type ZoneEffect = {
|
||||
id: UUID
|
||||
zoneId: UUID
|
||||
zone: Zone
|
||||
effect: string
|
||||
strength: number
|
||||
}
|
||||
|
||||
export type PlacedMapObject = {
|
||||
id: string
|
||||
mapObject: MapObject | string
|
||||
export type ZoneObject = {
|
||||
id: UUID
|
||||
zoneId: UUID
|
||||
zone: Zone
|
||||
objectId: UUID
|
||||
object: Object
|
||||
depth: number
|
||||
isRotated: boolean
|
||||
positionX: number
|
||||
positionY: number
|
||||
}
|
||||
|
||||
export enum MapEventTileType {
|
||||
export enum ZoneEventTileType {
|
||||
BLOCK = 'BLOCK',
|
||||
TELEPORT = 'TELEPORT',
|
||||
NPC = 'NPC',
|
||||
ITEM = 'ITEM'
|
||||
}
|
||||
|
||||
export type MapEventTile = {
|
||||
id: string
|
||||
map: string
|
||||
type: MapEventTileType
|
||||
export type ZoneEventTile = {
|
||||
id: UUID
|
||||
zoneId: UUID
|
||||
zone: Zone
|
||||
type: ZoneEventTileType
|
||||
positionX: number
|
||||
positionY: number
|
||||
teleport?: MapEventTileTeleport
|
||||
teleport?: ZoneEventTileTeleport
|
||||
}
|
||||
|
||||
export type MapEventTileTeleport = {
|
||||
id: string
|
||||
mapEventTile: MapEventTile
|
||||
toMap: Map
|
||||
export type ZoneEventTileTeleport = {
|
||||
id: UUID
|
||||
zoneEventTileId: UUID
|
||||
zoneEventTile: ZoneEventTile
|
||||
toZoneId: UUID
|
||||
toZone: Zone
|
||||
toPositionX: number
|
||||
toPositionY: number
|
||||
toRotation: number
|
||||
}
|
||||
|
||||
export type User = {
|
||||
id: string
|
||||
id: UUID
|
||||
username: string
|
||||
password: string
|
||||
characters: Character[]
|
||||
@ -138,28 +150,29 @@ export enum CharacterRace {
|
||||
}
|
||||
|
||||
export type CharacterType = {
|
||||
id: string
|
||||
id: UUID
|
||||
name: string
|
||||
gender: CharacterGender
|
||||
race: CharacterRace
|
||||
isSelectable: boolean
|
||||
characters: Character[]
|
||||
spriteId?: string
|
||||
sprite?: Sprite
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export type CharacterHair = {
|
||||
id: string
|
||||
id: UUID
|
||||
name: string
|
||||
sprite: string | Sprite
|
||||
sprite: Sprite
|
||||
gender: CharacterGender
|
||||
color: string
|
||||
isSelectable: boolean
|
||||
}
|
||||
|
||||
export type Character = {
|
||||
id: string
|
||||
userid: string
|
||||
id: UUID
|
||||
userId: UUID
|
||||
user: User
|
||||
name: string
|
||||
hitpoints: number
|
||||
@ -171,30 +184,35 @@ export type Character = {
|
||||
positionX: number
|
||||
positionY: number
|
||||
rotation: number
|
||||
characterType: UUID | null
|
||||
characterHair: UUID | null
|
||||
map: UUID
|
||||
characterTypeId: UUID | null
|
||||
characterType: CharacterType | null | string
|
||||
characterHairId: UUID | null
|
||||
characterHair: CharacterHair | null
|
||||
zoneId: UUID
|
||||
zone: Zone
|
||||
chats: Chat[]
|
||||
items: CharacterItem[]
|
||||
equipment: CharacterEquipment[]
|
||||
}
|
||||
|
||||
export type MapCharacter = {
|
||||
export type ZoneCharacter = {
|
||||
character: Character
|
||||
isMoving: boolean
|
||||
isAttacking?: boolean
|
||||
isMoving?: boolean
|
||||
}
|
||||
|
||||
export type CharacterItem = {
|
||||
id: string
|
||||
id: UUID
|
||||
characterId: UUID
|
||||
character: Character
|
||||
itemId: UUID
|
||||
item: Item
|
||||
quantity: number
|
||||
}
|
||||
|
||||
export type CharacterEquipment = {
|
||||
id: string
|
||||
id: UUID
|
||||
slot: CharacterEquipmentSlotType
|
||||
characterItemId: UUID
|
||||
characterItem: CharacterItem
|
||||
}
|
||||
|
||||
@ -208,55 +226,53 @@ export enum CharacterEquipmentSlotType {
|
||||
}
|
||||
|
||||
export type Sprite = {
|
||||
id: string
|
||||
id: UUID
|
||||
name: string
|
||||
width: number | null
|
||||
height: number | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
spriteActions: SpriteAction[]
|
||||
characterTypes: CharacterType[]
|
||||
}
|
||||
|
||||
export interface SpriteImage {
|
||||
url: string
|
||||
offset: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
}
|
||||
|
||||
export type SpriteAction = {
|
||||
id: string
|
||||
sprite: string
|
||||
id: UUID
|
||||
sprite: Sprite
|
||||
action: string
|
||||
sprites: SpriteImage[]
|
||||
sprites: string[]
|
||||
originX: number
|
||||
originY: number
|
||||
isAnimated: boolean
|
||||
isLooping: boolean
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
frameRate: number
|
||||
}
|
||||
|
||||
export type Chat = {
|
||||
id: string
|
||||
id: UUID
|
||||
characterId: UUID
|
||||
character: Character
|
||||
map: Map
|
||||
zoneId: UUID
|
||||
zone: Zone
|
||||
message: string
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export type WorldSettings = {
|
||||
date: Date
|
||||
weatherState: WeatherState
|
||||
}
|
||||
|
||||
export type WeatherState = {
|
||||
rainPercentage: number
|
||||
isRainEnabled: boolean
|
||||
isFogEnabled: boolean
|
||||
fogDensity: number
|
||||
}
|
||||
|
||||
export type mapLoadData = {
|
||||
mapId: string
|
||||
characters: MapCharacter[]
|
||||
export type WeatherState = {
|
||||
isRainEnabled: boolean
|
||||
rainPercentage: number
|
||||
isFogEnabled: boolean
|
||||
fogDensity: number
|
||||
}
|
||||
|
||||
export type zoneLoadData = {
|
||||
zone: Zone
|
||||
characters: ZoneCharacter[]
|
||||
}
|
||||
|
@ -1,45 +1,25 @@
|
||||
import config from '@/application/config'
|
||||
import type { HttpResponse } from '@/application/types'
|
||||
import type { BaseStorage } from '@/storage/baseStorage'
|
||||
|
||||
export function uuidv4() {
|
||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
|
||||
}
|
||||
|
||||
export function unduplicateArray(array: any[]) {
|
||||
const arrayToProcess = typeof array.flat === 'function' ? array.flat() : array
|
||||
return [...new Set(arrayToProcess)]
|
||||
return [...new Set(array.flat())]
|
||||
}
|
||||
|
||||
export async function downloadCache<T extends { id: string; updatedAt: Date }>(endpoint: string, storage: BaseStorage<T>) {
|
||||
const request = await fetch(`${config.server_endpoint}/cache/${endpoint}`)
|
||||
const response = (await request.json()) as HttpResponse<T[]>
|
||||
|
||||
if (!response.success) {
|
||||
console.error(`Failed to download ${endpoint}:`, response.message)
|
||||
return
|
||||
export function getDomain() {
|
||||
// Check if not localhost
|
||||
if (window.location.hostname !== 'localhost') {
|
||||
return window.location.hostname
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// Check if not IP address
|
||||
if (window.location.hostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
|
||||
return window.location.hostname
|
||||
}
|
||||
|
||||
// Update or add new items
|
||||
for (const item of items) {
|
||||
let overwrite = false
|
||||
const existingItem = await storage.getById(item.id)
|
||||
|
||||
if (!existingItem || item.updatedAt > existingItem.updatedAt) {
|
||||
overwrite = true
|
||||
if (window.location.hostname.split('.').length < 3) {
|
||||
return window.location.hostname
|
||||
}
|
||||
|
||||
await storage.add(item, overwrite)
|
||||
}
|
||||
return window.location.hostname.split('.').slice(-2).join('.')
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ input {
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@apply px-4 py-2.5 text-base leading-5 bg-gray border border-solid border-gray-500 rounded text-gray-300 font-default;
|
||||
@apply px-4 py-2.5 text-base leading-5 bg-gray border border-solid border-gray-500 rounded text-gray-300;
|
||||
&:focus-visible {
|
||||
@apply outline-none border-cyan rounded bg-gray-900;
|
||||
}
|
||||
@ -88,12 +88,6 @@ input {
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
&.input-field {
|
||||
@apply appearance-none bg-[url('/assets/icons/mapEditor/dropdown-chevron.svg')] bg-no-repeat bg-[calc(100%_-_10px)_center] bg-[length:20px] text-white;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field-full {
|
||||
@apply w-full flex flex-col mb-5;
|
||||
label {
|
||||
@ -124,16 +118,7 @@ button {
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
@apply bg-red-500;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-indigo {
|
||||
@apply bg-indigo-500 text-gray-50 text-base leading-5 rounded py-2.5;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
@apply bg-indigo-600;
|
||||
@apply bg-red-400;
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,10 +149,6 @@ button {
|
||||
@apply bg-gray bg-none;
|
||||
}
|
||||
|
||||
.list-open {
|
||||
@apply w-[calc(75%_-_40px)] max-xl:w-[calc(100%_-_360px)];
|
||||
}
|
||||
|
||||
.hair-deselect:has(:checked) {
|
||||
img {
|
||||
@apply brightness-200;
|
||||
|
179
src/components/Effects.vue
Normal file
@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { WeatherState } from '@/application/types'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { Scene } from 'phavuer'
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
// Constants
|
||||
const LIGHT_CONFIG = {
|
||||
SUNRISE_HOUR: 6,
|
||||
SUNSET_HOUR: 20,
|
||||
DAY_STRENGTH: 100,
|
||||
NIGHT_STRENGTH: 30
|
||||
}
|
||||
|
||||
// Stores and refs
|
||||
const gameStore = useGameStore()
|
||||
const zoneStore = useZoneStore()
|
||||
const sceneRef = ref<Phaser.Scene | null>(null)
|
||||
const zoneEffectsReady = ref(false)
|
||||
|
||||
// Effect objects
|
||||
const effects = {
|
||||
light: ref<Phaser.GameObjects.Graphics | null>(null),
|
||||
rain: ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null),
|
||||
fog: ref<Phaser.GameObjects.Sprite | null>(null)
|
||||
}
|
||||
|
||||
// Weather state
|
||||
const weatherState = ref<WeatherState>({
|
||||
isRainEnabled: false,
|
||||
rainPercentage: 0,
|
||||
isFogEnabled: false,
|
||||
fogDensity: 0
|
||||
})
|
||||
|
||||
// Scene setup
|
||||
const preloadScene = (scene: Phaser.Scene) => {
|
||||
scene.load.image('raindrop', 'assets/raindrop.png')
|
||||
scene.load.image('fog', 'assets/fog.png')
|
||||
}
|
||||
|
||||
const createScene = (scene: Phaser.Scene) => {
|
||||
sceneRef.value = scene
|
||||
initializeEffects(scene)
|
||||
setupSocketListeners()
|
||||
}
|
||||
|
||||
const initializeEffects = (scene: Phaser.Scene) => {
|
||||
// Light
|
||||
effects.light.value = scene.add.graphics().setDepth(1000)
|
||||
|
||||
// Rain
|
||||
effects.rain.value = scene.add
|
||||
.particles(0, 0, 'raindrop', {
|
||||
x: { min: 0, max: window.innerWidth },
|
||||
y: -50,
|
||||
quantity: 5,
|
||||
lifespan: 2000,
|
||||
speedY: { min: 300, max: 500 },
|
||||
scale: { start: 0.005, end: 0.005 },
|
||||
alpha: { start: 0.5, end: 0 },
|
||||
blendMode: 'ADD'
|
||||
})
|
||||
.setDepth(900)
|
||||
effects.rain.value.stop()
|
||||
|
||||
// Fog
|
||||
effects.fog.value = scene.add
|
||||
.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
|
||||
.setScale(2)
|
||||
.setAlpha(0)
|
||||
.setDepth(950)
|
||||
}
|
||||
|
||||
// Effect updates
|
||||
const updateScene = () => {
|
||||
const timeBasedLight = calculateLightStrength(gameStore.world.date)
|
||||
const zoneEffects = zoneStore.zone?.zoneEffects?.reduce(
|
||||
(acc, curr) => ({
|
||||
...acc,
|
||||
[curr.effect]: curr.strength
|
||||
}),
|
||||
{}
|
||||
) as { [key: string]: number }
|
||||
|
||||
// Only update effects once zoneEffects are loaded
|
||||
if (!zoneEffectsReady.value) {
|
||||
if (zoneEffects && Object.keys(zoneEffects).length) {
|
||||
zoneEffectsReady.value = true
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const finalEffects =
|
||||
zoneEffects && Object.keys(zoneEffects).length
|
||||
? zoneEffects
|
||||
: {
|
||||
light: timeBasedLight,
|
||||
rain: weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0,
|
||||
fog: weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0
|
||||
}
|
||||
|
||||
applyEffects(finalEffects)
|
||||
}
|
||||
|
||||
const applyEffects = (effectValues: any) => {
|
||||
if (effects.light.value) {
|
||||
const darkness = 1 - (effectValues.light ?? 100) / 100
|
||||
effects.light.value.clear().fillStyle(0x000000, darkness).fillRect(0, 0, window.innerWidth, window.innerHeight)
|
||||
}
|
||||
|
||||
if (effects.rain.value) {
|
||||
const strength = effectValues.rain ?? 0
|
||||
strength > 0 ? effects.rain.value.start().setQuantity(Math.floor((strength / 100) * 10)) : effects.rain.value.stop()
|
||||
}
|
||||
|
||||
if (effects.fog.value) {
|
||||
effects.fog.value.setAlpha((effectValues.fog ?? 0) / 100)
|
||||
}
|
||||
}
|
||||
|
||||
const calculateLightStrength = (time: Date): number => {
|
||||
const hour = time.getHours()
|
||||
const minute = time.getMinutes()
|
||||
|
||||
if (hour >= LIGHT_CONFIG.SUNSET_HOUR || hour < LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH
|
||||
|
||||
if (hour > LIGHT_CONFIG.SUNRISE_HOUR && hour < LIGHT_CONFIG.SUNSET_HOUR - 2) return LIGHT_CONFIG.DAY_STRENGTH
|
||||
|
||||
if (hour === LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH + ((LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * minute) / 60
|
||||
|
||||
const totalMinutes = (hour - (LIGHT_CONFIG.SUNSET_HOUR - 2)) * 60 + minute
|
||||
return LIGHT_CONFIG.DAY_STRENGTH - (LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * (totalMinutes / 120)
|
||||
}
|
||||
|
||||
// Socket and window handlers
|
||||
const setupSocketListeners = () => {
|
||||
gameStore.connection?.emit('weather', (response: WeatherState) => {
|
||||
weatherState.value = response
|
||||
updateScene()
|
||||
})
|
||||
|
||||
gameStore.connection!.on('weather', (data: WeatherState) => {
|
||||
weatherState.value = data
|
||||
updateScene()
|
||||
})
|
||||
|
||||
gameStore.connection!.on('date', updateScene)
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
if (effects.rain.value) effects.rain.value.updateConfig({ x: { min: 0, max: window.innerWidth } })
|
||||
if (effects.fog.value) effects.fog.value.setPosition(window.innerWidth / 2, window.innerHeight / 2)
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
watch(
|
||||
() => zoneStore.zone,
|
||||
() => {
|
||||
zoneEffectsReady.value = false
|
||||
updateScene()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
onMounted(() => window.addEventListener('resize', handleResize))
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
||||
gameStore.connection?.off('weather')
|
||||
})
|
||||
</script>
|
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center input-field gap-1" @click="focusInput">
|
||||
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2" role="listitem">
|
||||
<div class="flex flex-wrap items-center input-field gap-1">
|
||||
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2">
|
||||
<span class="text-xs text-white">{{ chip }}</span>
|
||||
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click.stop="deleteChip(i)" aria-label="Remove tag">×</button>
|
||||
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click="deleteChip(i)" aria-label="Remove chip">×</button>
|
||||
</div>
|
||||
<input ref="inputRef" class="outline-none border-none p-1 text-gray-300 min-w-[60px] flex-grow" :placeholder="placeholder" v-model.trim="currentInput" @keydown="handleKeydown" @paste="handlePaste" :maxlength="maxChipLength" aria-label="Add new tag" />
|
||||
<input class="outline-none border-none p-1 text-gray-300" placeholder="Tag name" v-model="currentInput" @keypress.enter.prevent="addChip" @keydown.backspace="handleBackspace" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -14,29 +14,20 @@ import type { Ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string[]
|
||||
maxChips?: number
|
||||
maxChipLength?: number
|
||||
placeholder?: string
|
||||
allowDuplicates?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => [],
|
||||
maxChips: 10,
|
||||
maxChipLength: 20,
|
||||
placeholder: 'Add tag',
|
||||
allowDuplicates: false
|
||||
modelValue: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string[]): void
|
||||
(e: 'error', message: string): void
|
||||
}>()
|
||||
|
||||
const currentInput: Ref<string> = ref('')
|
||||
const internalValue = ref<string[]>([])
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
// Initialize internalValue with props.modelValue
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
@ -45,27 +36,9 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const validateChip = (chip: string): boolean => {
|
||||
if (!chip) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!props.allowDuplicates && internalValue.value.includes(chip)) {
|
||||
emit('error', 'Duplicate tags are not allowed')
|
||||
return false
|
||||
}
|
||||
|
||||
if (internalValue.value.length >= props.maxChips) {
|
||||
emit('error', `Maximum ${props.maxChips} tags allowed`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const addChip = () => {
|
||||
const trimmedInput = currentInput.value.trim()
|
||||
if (validateChip(trimmedInput)) {
|
||||
if (trimmedInput && !internalValue.value.includes(trimmedInput)) {
|
||||
internalValue.value.push(trimmedInput)
|
||||
emit('update:modelValue', internalValue.value)
|
||||
currentInput.value = ''
|
||||
@ -77,36 +50,10 @@ const deleteChip = (index: number) => {
|
||||
emit('update:modelValue', internalValue.value)
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
event.preventDefault()
|
||||
addChip()
|
||||
break
|
||||
case 'Backspace':
|
||||
if (currentInput.value === '' && internalValue.value.length > 0) {
|
||||
deleteChip(internalValue.value.length - 1)
|
||||
const handleBackspace = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Backspace' && currentInput.value === '' && internalValue.value.length > 0) {
|
||||
internalValue.value.pop()
|
||||
emit('update:modelValue', internalValue.value)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handlePaste = (event: ClipboardEvent) => {
|
||||
event.preventDefault()
|
||||
const pastedText = event.clipboardData?.getData('text')
|
||||
if (pastedText) {
|
||||
const chips = pastedText
|
||||
.split(/[,\n]/)
|
||||
.map((chip) => chip.trim())
|
||||
.filter(Boolean)
|
||||
chips.forEach((chip) => {
|
||||
currentInput.value = chip
|
||||
addChip()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const focusInput = () => {
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
</script>
|
||||
|
@ -1,103 +1,192 @@
|
||||
<template>
|
||||
<Container ref="characterContainer" :x="currentPositionX" :y="currentPositionY" :depth="isometricDepth">
|
||||
<ChatBubble :mapCharacter="props.mapCharacter" />
|
||||
<HealthBar :mapCharacter="props.mapCharacter" />
|
||||
<CharacterHair :mapCharacter="props.mapCharacter" :flipX="isFlippedX" />
|
||||
<Sprite ref="characterSprite" :flipX="isFlippedX" />
|
||||
<ChatBubble :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
|
||||
<Healthbar :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
|
||||
<Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY">
|
||||
<CharacterHair :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
|
||||
<!-- <CharacterChest :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />-->
|
||||
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" />
|
||||
</Container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type MapCharacter } from '@/application/types'
|
||||
import config from '@/application/config'
|
||||
import { type Sprite as SpriteT, type ZoneCharacter } from '@/application/types'
|
||||
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
|
||||
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
|
||||
import HealthBar from '@/components/game/character/partials/HealthBar.vue'
|
||||
import { useCharacterSpriteComposable } from '@/composables/useCharacterSpriteComposable'
|
||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||
import Healthbar from '@/components/game/character/partials/Healthbar.vue'
|
||||
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useMapStore } from '@/stores/mapStore'
|
||||
import { Container, Sprite, useScene } from 'phavuer'
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { Container, refObj, Sprite, useScene } from 'phavuer'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
tileMap: Phaser.Tilemaps.Tilemap
|
||||
mapCharacter: MapCharacter
|
||||
}>()
|
||||
// import CharacterChest from '@/components/game/character/partials/CharacterChest.vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const mapStore = useMapStore()
|
||||
const scene = useScene()
|
||||
|
||||
const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, playAnimation, updateSprite, initializeSprite, cleanup } = useCharacterSpriteComposable(scene, props.tileMap, props.mapCharacter)
|
||||
const { playSound, stopSound } = useSoundComposable()
|
||||
|
||||
const handlePositionUpdate = (newValues: any, oldValues: any) => {
|
||||
if (!newValues) return
|
||||
|
||||
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
|
||||
updatePosition(newValues.positionX, newValues.positionY)
|
||||
enum Direction {
|
||||
POSITIVE,
|
||||
NEGATIVE,
|
||||
UNCHANGED
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
tilemap: Phaser.Tilemaps.Tilemap
|
||||
zoneCharacter: ZoneCharacter
|
||||
}>()
|
||||
|
||||
const charContainer = refObj<Phaser.GameObjects.Container>()
|
||||
const charSprite = refObj<Phaser.GameObjects.Sprite>()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const zoneStore = useZoneStore()
|
||||
const scene = useScene()
|
||||
|
||||
const currentX = ref(0)
|
||||
const currentY = ref(0)
|
||||
const isometricDepth = ref(1)
|
||||
const isInitialPosition = ref(true)
|
||||
const isMoving = ref(false)
|
||||
let animationFrame: number | null = null
|
||||
const moveSpeed = 5.7
|
||||
|
||||
const updateIsometricDepth = (x: number, y: number) => {
|
||||
isometricDepth.value = calculateIsometricDepth(x, y, 28, 94, true)
|
||||
}
|
||||
|
||||
const stopMovement = () => {
|
||||
isMoving.value = false
|
||||
if (animationFrame) {
|
||||
cancelAnimationFrame(animationFrame)
|
||||
animationFrame = null
|
||||
}
|
||||
}
|
||||
|
||||
const updatePosition = (x: number, y: number, direction: Direction) => {
|
||||
const targetX = tileToWorldX(props.tilemap, x, y)
|
||||
const targetY = tileToWorldY(props.tilemap, x, y)
|
||||
|
||||
if (isInitialPosition.value) {
|
||||
currentX.value = targetX
|
||||
currentY.value = targetY
|
||||
isInitialPosition.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (isMoving.value) {
|
||||
stopMovement()
|
||||
}
|
||||
|
||||
const distance = Math.sqrt(Math.pow(targetX - currentX.value, 2) + Math.pow(targetY - currentY.value, 2))
|
||||
|
||||
isMoving.value = true
|
||||
const startTime = performance.now()
|
||||
const startX = currentX.value
|
||||
const startY = currentY.value
|
||||
const duration = distance * moveSpeed
|
||||
|
||||
if (direction === Direction.POSITIVE) {
|
||||
updateIsometricDepth(x, y)
|
||||
}
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
if (!isMoving.value) return
|
||||
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
|
||||
currentX.value = startX + (targetX - startX) * progress
|
||||
currentY.value = startY + (targetY - startY) * progress
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrame = requestAnimationFrame(animate)
|
||||
} else {
|
||||
isMoving.value = false
|
||||
if (direction === Direction.NEGATIVE) {
|
||||
updateIsometricDepth(x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animationFrame = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
const calcDirection = (oldX: number, oldY: number, newX: number, newY: number): Direction => {
|
||||
if (newY < oldY || newX < oldX) return Direction.NEGATIVE
|
||||
if (newX > oldX || newY > oldY) return Direction.POSITIVE
|
||||
return Direction.UNCHANGED
|
||||
}
|
||||
|
||||
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
|
||||
|
||||
const charTexture = computed(() => {
|
||||
const { rotation, characterType } = props.zoneCharacter.character
|
||||
const spriteId = characterType?.sprite ?? 'idle_right_down'
|
||||
const action = props.zoneCharacter.isMoving ? 'walk' : 'idle'
|
||||
const direction = [0, 6].includes(rotation) ? 'left_up' : 'right_down'
|
||||
|
||||
return `${spriteId}-${action}_${direction}`
|
||||
})
|
||||
|
||||
const updateSprite = () => {
|
||||
if (props.zoneCharacter.isMoving) {
|
||||
charSprite.value!.anims.play(charTexture.value, true)
|
||||
return
|
||||
}
|
||||
|
||||
charSprite.value!.anims.stop()
|
||||
charSprite.value!.setFrame(0)
|
||||
charSprite.value!.setTexture(charTexture.value)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => ({
|
||||
x: props.zoneCharacter.character.positionX,
|
||||
y: props.zoneCharacter.character.positionY,
|
||||
isMoving: props.zoneCharacter.isMoving,
|
||||
rotation: props.zoneCharacter.character.rotation
|
||||
}),
|
||||
(newValues, oldValues) => {
|
||||
if (!newValues) return
|
||||
|
||||
if (!oldValues || newValues.x !== oldValues.x || newValues.y !== oldValues.y) {
|
||||
const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.x, oldValues.y, newValues.x, newValues.y)
|
||||
updatePosition(newValues.x, newValues.y, direction)
|
||||
}
|
||||
|
||||
// Handle animation updates
|
||||
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
|
||||
updateSprite()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays walk sound when character is moving
|
||||
*/
|
||||
watch(
|
||||
() => props.mapCharacter.isMoving,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
playSound('/assets/sounds/walk.wav', false, true)
|
||||
} else {
|
||||
stopSound('/assets/sounds/walk.wav')
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
/**
|
||||
* Plays attack animation and sound when character is attacking
|
||||
*/
|
||||
watch(
|
||||
() => props.mapCharacter.isAttacking,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
playAnimation('attack')
|
||||
playSound('/assets/sounds/attack.wav', false, true)
|
||||
} else {
|
||||
stopSound('/assets/sounds/attack.wav')
|
||||
}
|
||||
mapStore.updateCharacterProperty(props.mapCharacter.character.id, 'isAttacking', false)
|
||||
}
|
||||
)
|
||||
watch(() => props.zoneCharacter, updateSprite)
|
||||
|
||||
/**
|
||||
* Handles position updates and movement delay
|
||||
*/
|
||||
watch(
|
||||
() => ({
|
||||
positionX: props.mapCharacter.character.positionX,
|
||||
positionY: props.mapCharacter.character.positionY,
|
||||
isMoving: props.mapCharacter.isMoving,
|
||||
rotation: props.mapCharacter.character.rotation,
|
||||
isAttacking: props.mapCharacter.isAttacking
|
||||
}),
|
||||
async (oldValues, newValues) => {
|
||||
handlePositionUpdate(oldValues, newValues)
|
||||
}
|
||||
)
|
||||
loadSpriteTextures(scene, props.zoneCharacter.character.characterType?.sprite as string)
|
||||
.then(() => {
|
||||
charSprite.value!.setTexture(charTexture.value)
|
||||
charSprite.value!.setFlipX(isFlippedX.value)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error loading texture:', error)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await initializeSprite()
|
||||
onMounted(() => {
|
||||
charContainer.value!.setName(props.zoneCharacter.character!.name)
|
||||
|
||||
if (props.mapCharacter.character.id === gameStore.character!.id) {
|
||||
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
|
||||
if (props.zoneCharacter.character.id === gameStore.character!.id) {
|
||||
zoneStore.setCharacterLoaded(true)
|
||||
|
||||
// #146 : Set camera position to character, need to be improved still
|
||||
// scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
|
||||
// scene.cameras.main.stopFollow()
|
||||
}
|
||||
|
||||
updatePosition(props.zoneCharacter.character.positionX, props.zoneCharacter.character.positionY, props.zoneCharacter.character.rotation)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
stopMovement()
|
||||
})
|
||||
</script>
|
51
src/components/game/character/partials/CharacterChest.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Sprite as SpriteT, ZoneCharacter } from '@/application/types'
|
||||
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
zoneCharacter: ZoneCharacter
|
||||
currentX: number
|
||||
currentY: number
|
||||
}>()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const scene = useScene()
|
||||
|
||||
const texture = computed(() => {
|
||||
const { rotation, characterHair } = props.zoneCharacter.character
|
||||
const spriteId = characterHair?.sprite?.id
|
||||
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
||||
|
||||
return `${spriteId}-${direction}`
|
||||
})
|
||||
|
||||
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
|
||||
|
||||
const imageProps = computed(() => {
|
||||
// Get the current sprite action based on direction
|
||||
const direction = [0, 6].includes(props.zoneCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
||||
const spriteAction = props.zoneCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
||||
|
||||
return {
|
||||
depth: 1,
|
||||
originX: Number(spriteAction?.originX) ?? 0,
|
||||
originY: Number(spriteAction?.originY) ?? 0,
|
||||
flipX: isFlippedX.value,
|
||||
texture: texture.value
|
||||
// y: props.zoneCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
|
||||
}
|
||||
})
|
||||
|
||||
loadSpriteTextures(scene, props.zoneCharacter.character.characterHair?.sprite as SpriteT)
|
||||
.then(() => {})
|
||||
.catch((error) => {
|
||||
console.error('Error loading texture:', error)
|
||||
})
|
||||
</script>
|
@ -1,63 +1,51 @@
|
||||
<template>
|
||||
<Image ref="image" v-if="hairSpriteId" />
|
||||
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
|
||||
import { loadSpriteTextures } from '@/services/textureService'
|
||||
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
|
||||
import { Image, refObj, useScene } from 'phavuer'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import type { Sprite as SpriteT, ZoneCharacter } from '@/application/types'
|
||||
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
mapCharacter: MapCharacter
|
||||
zoneCharacter: ZoneCharacter
|
||||
currentX: number
|
||||
currentY: number
|
||||
}>()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const scene = useScene()
|
||||
const hairSpriteId = ref('')
|
||||
const hairSprite = ref<SpriteT | null>(null)
|
||||
const characterSpriteHeight = ref(0)
|
||||
const image = refObj<Phaser.GameObjects.Image>()
|
||||
|
||||
const flipX = computed(() => [6, 0].includes(props.mapCharacter.character.rotation ?? 0))
|
||||
const texture = computed(() => {
|
||||
const direction = flipX.value ? 'back' : 'front'
|
||||
const { rotation, characterHair } = props.zoneCharacter.character
|
||||
const spriteId = characterHair?.sprite?.id
|
||||
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
||||
|
||||
return `${hairSpriteId.value}-${direction}`
|
||||
return `${spriteId}-${direction}`
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.mapCharacter.character,
|
||||
(newValue) => {
|
||||
if (!image.value) return
|
||||
image.value.setTexture(texture.value)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
|
||||
|
||||
onMounted(async () => {
|
||||
if (!props.mapCharacter.character.characterType || !props.mapCharacter.character.characterHair) return
|
||||
const imageProps = computed(() => {
|
||||
// Get the current sprite action based on direction
|
||||
const direction = [0, 6].includes(props.zoneCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
||||
const spriteAction = props.zoneCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
||||
|
||||
const characterTypeStorage = new CharacterTypeStorage()
|
||||
const characterHairStorage = new CharacterHairStorage()
|
||||
const spriteStorage = new SpriteStorage()
|
||||
return {
|
||||
depth: 1,
|
||||
originX: Number(spriteAction?.originX) ?? 0,
|
||||
originY: Number(spriteAction?.originY) ?? 0,
|
||||
flipX: isFlippedX.value,
|
||||
texture: texture.value,
|
||||
y: props.zoneCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
loadSpriteTextures(scene, props.zoneCharacter.character.characterHair?.sprite as SpriteT)
|
||||
.then(() => {})
|
||||
.catch((error) => {
|
||||
console.error('Error loading texture:', error)
|
||||
})
|
||||
</script>
|
||||
|
@ -1,32 +1,33 @@
|
||||
<template>
|
||||
<Container ref="characterChatContainer">
|
||||
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
|
||||
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
|
||||
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
|
||||
</Container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MapCharacter } from '@/application/types'
|
||||
import type { ZoneCharacter } from '@/application/types'
|
||||
import { Container, refObj, RoundRectangle, Text, useGame } from 'phavuer'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
mapCharacter: MapCharacter
|
||||
zoneCharacter: ZoneCharacter
|
||||
currentX: number
|
||||
currentY: number
|
||||
}>()
|
||||
|
||||
const game = useGame()
|
||||
const characterChatContainer = refObj<Phaser.GameObjects.Container>()
|
||||
const charChatContainer = refObj<Phaser.GameObjects.Container>()
|
||||
|
||||
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
||||
container.setName(`${props.mapCharacter.character.name}_chatBubble`)
|
||||
container.setName(`${props.zoneCharacter.character.name}_chatBubble`)
|
||||
}
|
||||
|
||||
const createChatText = (text: Phaser.GameObjects.Text) => {
|
||||
text.setName(`${props.mapCharacter.character.name}_chatText`)
|
||||
text.setName(`${props.zoneCharacter.character.name}_chatText`)
|
||||
text.setFontSize(13)
|
||||
text.setFontFamily('Arial')
|
||||
text.setOrigin(0.5, 10.9)
|
||||
text.setResolution(2)
|
||||
|
||||
// Fix text alignment on Windows and Android
|
||||
if (game.device.os.windows || game.device.os.android) {
|
||||
@ -39,7 +40,7 @@ const createChatText = (text: Phaser.GameObjects.Text) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
characterChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
|
||||
characterChatContainer.value!.setVisible(false)
|
||||
charChatContainer.value!.setName(`${props.zoneCharacter.character!.name}_chatContainer`)
|
||||
charChatContainer.value!.setVisible(false)
|
||||
})
|
||||
</script>
|
||||
|
@ -1,17 +1,19 @@
|
||||
<template>
|
||||
<Container :depth="999">
|
||||
<Text @create="createNicknameText" :text="props.mapCharacter.character.name" />
|
||||
<Container :depth="999" :x="currentX" :y="currentY">
|
||||
<Text @create="createNicknameText" :text="props.zoneCharacter.character.name" />
|
||||
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
|
||||
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
||||
</Container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MapCharacter } from '@/application/types'
|
||||
import type { ZoneCharacter } from '@/application/types'
|
||||
import { Container, RoundRectangle, Text, useGame } from 'phavuer'
|
||||
|
||||
const props = defineProps<{
|
||||
mapCharacter: MapCharacter
|
||||
zoneCharacter: ZoneCharacter
|
||||
currentX: number
|
||||
currentY: number
|
||||
}>()
|
||||
|
||||
const game = useGame()
|
||||
@ -20,7 +22,6 @@ const createNicknameText = (text: Phaser.GameObjects.Text) => {
|
||||
text.setFontSize(13)
|
||||
text.setFontFamily('Arial')
|
||||
text.setOrigin(0.5, 9)
|
||||
text.setResolution(2)
|
||||
|
||||
// Fix text alignment on Windows and Android
|
||||
if (game.device.os.windows || game.device.os.android) {
|
@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div class="absolute top-0 right-4 hidden lg:block" v-if="gameStore.world.date && typeof gameStore.world.date === 'object'">
|
||||
<p class="text-white text-lg">
|
||||
{{ useDateFormat(gameStore.world.date, 'YYYY/MM/DD HH:mm') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useDateFormat } from '@vueuse/core'
|
||||
import { onUnmounted } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
|
||||
onUnmounted(() => {
|
||||
socketManager.off(SocketEvent.DATE)
|
||||
})
|
||||
</script>
|
@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<Character v-for="item in mapStore.characters" :key="item.character.id" :tileMap :mapCharacter="item" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { MapCharacter, UUID } from '@/application/types'
|
||||
import Character from '@/components/game/character/Character.vue'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useMapStore } from '@/stores/mapStore'
|
||||
import { onUnmounted } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const mapStore = useMapStore()
|
||||
|
||||
const props = defineProps<{
|
||||
tileMap: Phaser.Tilemaps.Tilemap
|
||||
}>()
|
||||
|
||||
socketManager.on(SocketEvent.MAP_CHARACTER_JOIN, (data: MapCharacter) => {
|
||||
mapStore.addCharacter(data)
|
||||
})
|
||||
|
||||
socketManager.on(SocketEvent.MAP_CHARACTER_LEAVE, (characterId: UUID) => {
|
||||
mapStore.removeCharacter(characterId)
|
||||
})
|
||||
|
||||
socketManager.on(SocketEvent.MAP_CHARACTER_MOVE, ([characterId, posX, posY, rot, isMoving]: [UUID, number, number, number, boolean]) => {
|
||||
mapStore.updateCharacterPosition([characterId, posX, posY, rot, isMoving])
|
||||
|
||||
if (characterId === gameStore.character?.id) {
|
||||
gameStore.character!.positionX = posX
|
||||
gameStore.character!.positionY = posY
|
||||
gameStore.character!.rotation = rot
|
||||
}
|
||||
})
|
||||
|
||||
socketManager.on(SocketEvent.MAP_CHARACTER_ATTACK, (characterId: UUID) => {
|
||||
mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
socketManager.off(SocketEvent.MAP_CHARACTER_ATTACK)
|
||||
socketManager.off(SocketEvent.MAP_CHARACTER_MOVE)
|
||||
socketManager.off(SocketEvent.MAP_CHARACTER_JOIN)
|
||||
socketManager.off(SocketEvent.MAP_CHARACTER_LEAVE)
|
||||
})
|
||||
</script>
|
@ -1,71 +0,0 @@
|
||||
<template>
|
||||
<MapTiles v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
||||
<PlacedMapObjects v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
||||
<Characters v-if="tileMap && mapStore.characters" :tileMap />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { mapLoadData } from '@/application/types'
|
||||
import { unduplicateArray } from '@/application/utilities'
|
||||
import Characters from '@/components/game/map/Characters.vue'
|
||||
import MapTiles from '@/components/game/map/MapTiles.vue'
|
||||
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { createTileLayer, createTileMap, loadTileTexturesFromMapTileArray } from '@/services/mapService'
|
||||
import { MapStorage } from '@/storage/storages'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useMapStore } from '@/stores/mapStore'
|
||||
import { useScene } from 'phavuer'
|
||||
import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
|
||||
|
||||
const scene = useScene()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const mapStore = useMapStore()
|
||||
|
||||
const mapStorage = new MapStorage()
|
||||
|
||||
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
||||
|
||||
// Event listeners
|
||||
socketManager.on(SocketEvent.MAP_CHARACTER_TELEPORT, (data: mapLoadData) => {
|
||||
mapStore.setMapId(data.mapId)
|
||||
mapStore.setCharacters(data.characters)
|
||||
})
|
||||
|
||||
async function initialize() {
|
||||
if (!mapStore.mapId) return
|
||||
|
||||
const map = await mapStorage.getById(mapStore.mapId)
|
||||
if (!map) return
|
||||
|
||||
await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
|
||||
|
||||
tileMap.value = createTileMap(scene, map)
|
||||
tileMapLayer.value = createTileLayer(tileMap.value, unduplicateArray(map.tiles))
|
||||
}
|
||||
|
||||
watch(
|
||||
() => mapStore.mapId,
|
||||
async () => {
|
||||
await initialize()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
if (!mapStore.mapId) return
|
||||
await initialize()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (tileMap.value) {
|
||||
tileMap.value.destroyLayer('tiles')
|
||||
tileMap.value.removeAllLayers()
|
||||
tileMap.value.destroy()
|
||||
}
|
||||
|
||||
socketManager.off(SocketEvent.MAP_CHARACTER_TELEPORT)
|
||||
})
|
||||
</script>
|
@ -1,32 +0,0 @@
|
||||
<template>
|
||||
<Controls v-if="tileMapLayer" :layer="tileMapLayer" :depth="0" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Controls from '@/components/utilities/Controls.vue'
|
||||
import { loadTileTexturesFromMapTileArray, placeTiles } from '@/services/mapService'
|
||||
import { MapStorage } from '@/storage/storages'
|
||||
import { useMapStore } from '@/stores/mapStore'
|
||||
import { useScene } from 'phavuer'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const scene = useScene()
|
||||
const mapStore = useMapStore()
|
||||
const mapStorage = new MapStorage()
|
||||
|
||||
const props = defineProps<{
|
||||
tileMap: Phaser.Tilemaps.Tilemap
|
||||
tileMapLayer: Phaser.Tilemaps.TilemapLayer
|
||||
}>()
|
||||
|
||||
onMounted(async () => {
|
||||
if (!mapStore.mapId) return
|
||||
|
||||
const map = await mapStorage.getById(mapStore.mapId)
|
||||
if (!map) return
|
||||
|
||||
await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
|
||||
|
||||
placeTiles(props.tileMap, props.tileMapLayer, map.tiles)
|
||||
})
|
||||
</script>
|
@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<PlacedMapObject v-for="placedMapObject in items" :tileMap :tileMapLayer :placedMapObject />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PlacedMapObject as PlacedMapObjectT } from '@/application/types'
|
||||
import PlacedMapObject from '@/components/game/map/partials/PlacedMapObject.vue'
|
||||
import { MapStorage } from '@/storage/storages'
|
||||
import { useMapStore } from '@/stores/mapStore'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||
|
||||
defineProps<{
|
||||
tileMap: Phaser.Tilemaps.Tilemap
|
||||
tileMapLayer: TilemapLayer
|
||||
}>()
|
||||
|
||||
const mapStore = useMapStore()
|
||||
const mapStorage = new MapStorage()
|
||||
const items = ref<PlacedMapObjectT[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
if (!mapStore.mapId) return
|
||||
|
||||
const map = await mapStorage.getById(mapStore.mapId)
|
||||
if (!map) return
|
||||
|
||||
items.value = map.placedMapObjects
|
||||
})
|
||||
</script>
|
@ -1,102 +0,0 @@
|
||||
<template>
|
||||
<Zone :depth="baseDepth" :origin-x="mapObj?.originX" :origin-y="mapObj?.originY" :width="mapObj?.frameWidth" :height="mapObj?.frameHeight" :x="x" :y="y" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MapObject, PlacedMapObject } from '@/application/types'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { calculateIsometricDepth } from '@/services/mapService'
|
||||
import { onPreUpdate, useScene, Zone } from 'phavuer'
|
||||
import { computed, onUnmounted } from 'vue'
|
||||
|
||||
interface Props {
|
||||
obj?: PlacedMapObject
|
||||
mapObj?: MapObject
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const mapEditor = useMapEditorComposable()
|
||||
const scene = useScene()
|
||||
|
||||
const group = scene.add.group()
|
||||
const partitionPoints = computed(() => {
|
||||
if (!props.mapObj?.frameWidth || !props.mapObj?.depthOffsets.length) return []
|
||||
|
||||
const sliceCount = props.mapObj.depthOffsets.length
|
||||
return Array.from({ length: sliceCount + 1 }, (_, i) => i * (props.mapObj!.frameWidth / sliceCount))
|
||||
})
|
||||
|
||||
let baseDepth = 0
|
||||
|
||||
const createImagePartition = (startX: number, endX: number, depthOffset: number): void => {
|
||||
if (!props.mapObj?.id) return
|
||||
|
||||
const img = scene.add.image(0, 0, props.mapObj.id)
|
||||
img.setOrigin(props.mapObj.originX, props.mapObj.originY)
|
||||
img.setCrop(startX, 0, endX, props.mapObj.frameHeight)
|
||||
img.setDepth(baseDepth + depthOffset)
|
||||
group.add(img)
|
||||
}
|
||||
|
||||
const updateGroupProperties = (): void => {
|
||||
if (!props.obj || !props.x || !props.y) return
|
||||
|
||||
const isMoving = mapEditor.movingPlacedObject.value?.id === props.obj.id
|
||||
const isSelected = mapEditor.selectedMapObject.value?.id === props.obj.id
|
||||
const isPlacedSelected = mapEditor.selectedPlacedObject.value?.id === props.obj.id
|
||||
|
||||
baseDepth = calculateIsometricDepth(props.obj.positionX, props.obj.positionY)
|
||||
|
||||
group.setXY(props.x, props.y)
|
||||
group.setAlpha(isMoving || isSelected ? 0.5 : 1)
|
||||
group.setTint(isPlacedSelected ? 0x00ff00 : 0xffffff)
|
||||
group.setDepth(baseDepth)
|
||||
}
|
||||
|
||||
const updateImageProperties = (): void => {
|
||||
const orderedImages = group.getChildren() as Phaser.GameObjects.Image[]
|
||||
|
||||
orderedImages.forEach((image, index) => {
|
||||
if (!props.obj || !props.mapObj || !props.x) return
|
||||
|
||||
image.flipX = props.obj.isRotated
|
||||
|
||||
if (props.obj.isRotated) {
|
||||
const offsetNum = props.mapObj.depthOffsets.length
|
||||
const xOffset = props.mapObj.frameWidth / offsetNum
|
||||
image.x = props.x + (index < offsetNum / 2 ? -xOffset : xOffset)
|
||||
image.setDepth(baseDepth - props.mapObj.depthOffsets[index])
|
||||
} else {
|
||||
image.x = props.x
|
||||
image.setDepth(baseDepth + props.mapObj.depthOffsets[index])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onPreUpdate(() => {
|
||||
updateGroupProperties()
|
||||
updateImageProperties()
|
||||
})
|
||||
|
||||
// Initial setup
|
||||
const initializeGroup = (): void => {
|
||||
if (!props.mapObj || !props.x || !props.y || !props.obj) return
|
||||
|
||||
baseDepth = calculateIsometricDepth(props.obj.positionX, props.obj.positionY)
|
||||
group.setXY(props.x, props.y)
|
||||
group.setOrigin(props.mapObj.originX, props.mapObj.originY)
|
||||
|
||||
const points = partitionPoints.value
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
createImagePartition(points[i], points[i + 1], props.mapObj.depthOffsets[i])
|
||||
}
|
||||
}
|
||||
|
||||
initializeGroup()
|
||||
|
||||
onUnmounted(() => {
|
||||
group.destroy(true, true)
|
||||
})
|
||||
</script>
|
@ -1,70 +0,0 @@
|
||||
<template>
|
||||
<ImageGroup v-bind="groupProps" v-if="mapObject && gameStore.isTextureLoaded(props.placedMapObject.mapObject as string)" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MapObject, PlacedMapObject } from '@/application/types'
|
||||
import ImageGroup from '@/components/game/map/partials/ImageGroup.vue'
|
||||
import { loadMapObjectTextures, tileToWorldXY } from '@/services/mapService'
|
||||
import { MapObjectStorage } from '@/storage/storages'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useScene } from 'phavuer'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import Tilemap = Phaser.Tilemaps.Tilemap
|
||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||
|
||||
const props = defineProps<{
|
||||
placedMapObject: PlacedMapObject
|
||||
tileMap: Tilemap
|
||||
tileMapLayer: TilemapLayer
|
||||
}>()
|
||||
|
||||
const scene = useScene()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
|
||||
const mapObject = ref<MapObject>()
|
||||
|
||||
const groupProps = computed(() => ({
|
||||
...calculateObjectPlacement(props.placedMapObject),
|
||||
mapObj: mapObject.value,
|
||||
obj: props.placedMapObject
|
||||
}))
|
||||
|
||||
async function initialize() {
|
||||
if (!props.placedMapObject.mapObject) return
|
||||
|
||||
/**
|
||||
* Check if mapObject is an string or object, if its an object we assume its a mapObject and change it to a string
|
||||
* We do this because this component is shared with the map editor, which gets sent the mapObject as an object by the server
|
||||
*/
|
||||
if (typeof props.placedMapObject.mapObject === 'object') {
|
||||
// @ts-ignore
|
||||
props.placedMapObject.mapObject = props.placedMapObject.mapObject.id
|
||||
}
|
||||
|
||||
const mapObjectStorage = new MapObjectStorage()
|
||||
const _mapObject = await mapObjectStorage.getById(props.placedMapObject.mapObject as string)
|
||||
if (!_mapObject) return
|
||||
|
||||
console.log(_mapObject)
|
||||
|
||||
mapObject.value = _mapObject
|
||||
|
||||
await loadMapObjectTextures([_mapObject], scene)
|
||||
}
|
||||
|
||||
function calculateObjectPlacement(mapObj: PlacedMapObject): { x: number; y: number } {
|
||||
let position = tileToWorldXY(props.tileMapLayer, mapObj.positionX, mapObj.positionY)
|
||||
|
||||
return {
|
||||
x: position.worldPositionX,
|
||||
y: position.worldPositionY
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initialize()
|
||||
})
|
||||
</script>
|
14
src/components/game/zone/Characters.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<Character v-for="item in zoneStore.characters" :key="item.character.id" :tilemap="tilemap" :zoneCharacter="item" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Character from '@/components/game/character/Character.vue'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
|
||||
const zoneStore = useZoneStore()
|
||||
|
||||
const props = defineProps<{
|
||||
tilemap: Phaser.Tilemaps.Tilemap
|
||||
}>()
|
||||
</script>
|
50
src/components/game/zone/Zone.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tileMap:create="tileMap = $event" />
|
||||
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ZoneCharacter, zoneLoadData } from '@/application/types'
|
||||
import Characters from '@/components/game/zone/Characters.vue'
|
||||
import ZoneObjects from '@/components/game/zone/ZoneObjects.vue'
|
||||
import ZoneTiles from '@/components/game/zone/ZoneTiles.vue'
|
||||
import { loadZoneTilesIntoScene } from '@/composables/zoneComposable'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { useScene } from 'phavuer'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
|
||||
const scene = useScene()
|
||||
const gameStore = useGameStore()
|
||||
const zoneStore = useZoneStore()
|
||||
|
||||
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
||||
|
||||
onUnmounted(() => {
|
||||
zoneStore.reset()
|
||||
gameStore.connection!.off('zone:character:teleport')
|
||||
gameStore.connection!.off('zone:character:join')
|
||||
gameStore.connection!.off('zone:character:leave')
|
||||
gameStore.connection!.off('zone:character:move')
|
||||
})
|
||||
|
||||
// Event listeners
|
||||
gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => {
|
||||
await loadZoneTilesIntoScene(data.zone.id, scene)
|
||||
zoneStore.setZone(data.zone)
|
||||
zoneStore.setCharacters(data.characters)
|
||||
})
|
||||
|
||||
gameStore.connection!.on('zone:character:join', async (data: ZoneCharacter) => {
|
||||
zoneStore.addCharacter(data)
|
||||
})
|
||||
|
||||
gameStore.connection!.on('zone:character:leave', (characterId: number) => {
|
||||
zoneStore.removeCharacter(characterId)
|
||||
})
|
||||
|
||||
gameStore.connection!.on('zone:character:move', (data: { characterId: number; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
|
||||
zoneStore.updateCharacterPosition(data)
|
||||
})
|
||||
</script>
|
14
src/components/game/zone/ZoneObjects.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<ZoneObject v-for="zoneObject in zoneStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ZoneObject from '@/components/game/zone/partials/ZoneObject.vue'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
|
||||
const zoneStore = useZoneStore()
|
||||
|
||||
defineProps<{
|
||||
tilemap: Phaser.Tilemaps.Tilemap
|
||||
}>()
|
||||
</script>
|
69
src/components/game/zone/ZoneTiles.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<Controls :layer="tileLayer" :depth="0" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import { unduplicateArray } from '@/application/utilities'
|
||||
import Controls from '@/components/utilities/Controls.vue'
|
||||
import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { useScene } from 'phavuer'
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
|
||||
const emit = defineEmits(['tileMap:create'])
|
||||
|
||||
const scene = useScene()
|
||||
const zoneStore = useZoneStore()
|
||||
const tileMap = createTileMap()
|
||||
const tileLayer = createTileLayer()
|
||||
|
||||
/**
|
||||
* A Tilemap is a container for Tilemap data.
|
||||
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
|
||||
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
|
||||
*/
|
||||
function createTileMap() {
|
||||
const zoneData = new Phaser.Tilemaps.MapData({
|
||||
width: zoneStore.zone?.width,
|
||||
height: zoneStore.zone?.height,
|
||||
tileWidth: config.tile_size.x,
|
||||
tileHeight: config.tile_size.y,
|
||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
||||
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
||||
})
|
||||
|
||||
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
||||
emit('tileMap:create', newTileMap)
|
||||
|
||||
return newTileMap
|
||||
}
|
||||
|
||||
/**
|
||||
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
|
||||
*/
|
||||
function createTileLayer() {
|
||||
const tilesArray = unduplicateArray(FlattenZoneArray(zoneStore.zone?.tiles ?? []))
|
||||
|
||||
const tilesetImages = Array.from(tilesArray).map((tile: any, index: number) => {
|
||||
return tileMap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
|
||||
}) as any
|
||||
|
||||
// Add blank tile
|
||||
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
||||
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||
|
||||
layer.setDepth(0)
|
||||
layer.setCullPadding(2, 2)
|
||||
|
||||
return layer
|
||||
}
|
||||
|
||||
setLayerTiles(tileMap, tileLayer, zoneStore.zone?.tiles)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
tileMap.destroyLayer('tiles')
|
||||
tileMap.removeAllLayers()
|
||||
tileMap.destroy()
|
||||
})
|
||||
</script>
|
41
src/components/game/zone/partials/ZoneObject.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AssetDataT, ZoneObject } from '@/application/types'
|
||||
import { loadTexture } from '@/composables/gameComposable'
|
||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
tilemap: Phaser.Tilemaps.Tilemap
|
||||
zoneObject: ZoneObject
|
||||
}>()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const scene = useScene()
|
||||
|
||||
const imageProps = computed(() => ({
|
||||
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
|
||||
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
||||
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
||||
flipX: props.zoneObject.isRotated,
|
||||
texture: props.zoneObject.object.id,
|
||||
originY: Number(props.zoneObject.object.originX),
|
||||
originX: Number(props.zoneObject.object.originY)
|
||||
}))
|
||||
|
||||
loadTexture(scene, {
|
||||
key: props.zoneObject.object.id,
|
||||
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
|
||||
group: 'objects',
|
||||
updatedAt: props.zoneObject.object.updatedAt,
|
||||
frameWidth: props.zoneObject.object.frameWidth,
|
||||
frameHeight: props.zoneObject.object.frameHeight
|
||||
} as AssetDataT).catch((error) => {
|
||||
console.error('Error loading texture:', error)
|
||||
})
|
||||
</script>
|
@ -6,7 +6,7 @@
|
||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
|
||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
|
||||
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
|
||||
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="mapEditor.toggleActive()">Map editor</button>
|
||||
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => zoneEditorStore.toggleActive()">Map editor</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
@ -20,12 +20,12 @@
|
||||
<script setup lang="ts">
|
||||
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const mapEditor = useMapEditorComposable()
|
||||
const gameStore = useGameStore()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
let toggle = ref('asset-manager')
|
||||
</script>
|
||||
|
@ -5,8 +5,8 @@
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'map_objects' }" @click="() => (selectedCategory = 'map_objects')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'map_objects' }">Map objects</span>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
|
||||
@ -40,7 +40,7 @@
|
||||
<!-- Assets list -->
|
||||
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
||||
<TileList v-if="selectedCategory === 'tiles'" />
|
||||
<MapObjectList v-if="selectedCategory === 'map_objects'" />
|
||||
<ObjectList v-if="selectedCategory === 'objects'" />
|
||||
<SpriteList v-if="selectedCategory === 'sprites'" />
|
||||
<ItemList v-if="selectedCategory === 'items'" />
|
||||
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
|
||||
@ -50,7 +50,7 @@
|
||||
<!-- Asset details -->
|
||||
<div class="flex w-7/12 after:hidden flex-col relative overflow-auto">
|
||||
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
||||
<MapObjectDetails v-if="selectedCategory === 'map_objects' && assetManagerStore.selectedMapObject" />
|
||||
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
|
||||
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
|
||||
<ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" />
|
||||
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
|
||||
@ -66,8 +66,8 @@ import CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/
|
||||
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
|
||||
import ItemDetails from '@/components/gameMaster/assetManager/partials/item/itemDetails.vue'
|
||||
import ItemList from '@/components/gameMaster/assetManager/partials/item/itemList.vue'
|
||||
import MapObjectDetails from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectDetails.vue'
|
||||
import MapObjectList from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectList.vue'
|
||||
import ObjectDetails from '@/components/gameMaster/assetManager/partials/object/ObjectDetails.vue'
|
||||
import ObjectList from '@/components/gameMaster/assetManager/partials/object/ObjectList.vue'
|
||||
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
|
||||
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
|
||||
import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue'
|
||||
|
@ -20,29 +20,12 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<div class="space-x-6 flex items-center">
|
||||
<label for="color">Color</label>
|
||||
<input v-model="characterColor" class="input-field" type="text" name="color" placeholder="Character Hair Color" />
|
||||
<div class="h-[38px] w-[38px] rounded" :style="{ backgroundColor: characterColor }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="spriteId">Sprite</label>
|
||||
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
||||
<option disabled selected value="">Select sprite</option>
|
||||
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label>Preview</label>
|
||||
<div v-if="characterSpriteId" class="flex flex-col">
|
||||
<div class="p-3 pb-5 min-h-32 block rounded-md default-border bg-gray-800">
|
||||
<div class="flex items-center justify-center p-1 h-full bg-gray-700 rounded">
|
||||
<img :src="config.server_endpoint + '/textures/sprites/' + characterSpriteId + '/front.png'" class="max-w-[200px] max-h-[200px] object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterHair">Remove</button>
|
||||
</form>
|
||||
@ -51,50 +34,48 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
|
||||
import { downloadCache } from '@/application/utilities'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { CharacterHairStorage } from '@/storage/storages'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
|
||||
|
||||
const characterName = ref('')
|
||||
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
||||
const characterColor = ref<string>('#000000')
|
||||
const characterIsSelectable = ref<boolean>(false)
|
||||
const characterSpriteId = ref<string | null | undefined>(null)
|
||||
|
||||
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
||||
|
||||
if (!selectedCharacterHair.value) {
|
||||
console.error('No character hair selected')
|
||||
}
|
||||
|
||||
if (selectedCharacterHair.value) {
|
||||
characterName.value = selectedCharacterHair.value.name
|
||||
characterGender.value = selectedCharacterHair.value.gender
|
||||
characterColor.value = selectedCharacterHair.value.color
|
||||
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
|
||||
characterSpriteId.value = selectedCharacterHair.value.sprite?.id
|
||||
characterSpriteId.value = selectedCharacterHair.value.spriteId
|
||||
}
|
||||
|
||||
async function removeCharacterHair() {
|
||||
function removeCharacterHair() {
|
||||
if (!selectedCharacterHair.value) return
|
||||
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_REMOVE, { id: selectedCharacterHair.value.id }, async (response: boolean) => {
|
||||
gameStore.connection?.emit('gm:characterHair:remove', { id: selectedCharacterHair.value.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to remove character hair')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('character_hair', new CharacterHairStorage())
|
||||
await refreshCharacterHairList()
|
||||
refreshCharacterHairList()
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||
function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
||||
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
||||
assetManagerStore.setCharacterHairList(response)
|
||||
|
||||
if (unsetSelectedCharacterHair) {
|
||||
@ -103,24 +84,21 @@ async function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
||||
})
|
||||
}
|
||||
|
||||
async function saveCharacterHair() {
|
||||
function saveCharacterHair() {
|
||||
const characterHairData = {
|
||||
id: selectedCharacterHair.value!.id,
|
||||
name: characterName.value,
|
||||
gender: characterGender.value,
|
||||
color: characterColor.value,
|
||||
isSelectable: characterIsSelectable.value,
|
||||
spriteId: characterSpriteId.value
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_UPDATE, characterHairData, async (response: boolean) => {
|
||||
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save character type')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('character_hair', new CharacterHairStorage())
|
||||
await refreshCharacterHairList(false)
|
||||
refreshCharacterHairList(false)
|
||||
})
|
||||
}
|
||||
|
||||
@ -128,15 +106,14 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
|
||||
if (!characterHair) return
|
||||
characterName.value = characterHair.name
|
||||
characterGender.value = characterHair.gender
|
||||
characterColor.value = characterHair.color
|
||||
characterIsSelectable.value = characterHair.isSelectable
|
||||
characterSpriteId.value = characterHair.sprite?.id
|
||||
characterSpriteId.value = characterHair.spriteId
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!selectedCharacterHair.value) return
|
||||
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
|
@ -9,8 +9,8 @@
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||
<a
|
||||
v-for="{ data: characterHair } in list"
|
||||
:key="characterHair.id"
|
||||
@ -25,16 +25,14 @@
|
||||
</div>
|
||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { CharacterHair } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
@ -54,13 +52,13 @@ const handleSearch = () => {
|
||||
}
|
||||
|
||||
const createNewCharacterHair = () => {
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_CREATE, {}, (response: boolean) => {
|
||||
gameStore.connection?.emit('gm:characterHair:create', {}, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to create new character type')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
||||
assetManagerStore.setCharacterHairList(response)
|
||||
})
|
||||
})
|
||||
@ -94,7 +92,7 @@ function toTop() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
||||
assetManagerStore.setCharacterHairList(response)
|
||||
})
|
||||
})
|
||||
|
@ -40,14 +40,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
|
||||
import { downloadCache } from '@/application/utilities'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { CharacterTypeStorage } from '@/storage/storages'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const selectedCharacterType = computed(() => assetManagerStore.selectedCharacterType)
|
||||
@ -70,25 +68,23 @@ if (selectedCharacterType.value) {
|
||||
characterGender.value = selectedCharacterType.value.gender
|
||||
characterRace.value = selectedCharacterType.value.race
|
||||
characterIsSelectable.value = selectedCharacterType.value.isSelectable
|
||||
characterSpriteId.value = selectedCharacterType.value.sprite?.id
|
||||
characterSpriteId.value = selectedCharacterType.value.spriteId
|
||||
}
|
||||
|
||||
async function removeCharacterType() {
|
||||
function removeCharacterType() {
|
||||
if (!selectedCharacterType.value) return
|
||||
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_REMOVE, { id: selectedCharacterType.value.id }, async (response: boolean) => {
|
||||
gameStore.connection?.emit('gm:characterType:remove', { id: selectedCharacterType.value.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to remove character type')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('character_types', new CharacterTypeStorage())
|
||||
await refreshCharacterTypeList()
|
||||
refreshCharacterTypeList()
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||
function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
||||
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
||||
assetManagerStore.setCharacterTypeList(response)
|
||||
|
||||
if (unsetSelectedCharacterType) {
|
||||
@ -97,7 +93,7 @@ async function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
||||
})
|
||||
}
|
||||
|
||||
async function saveCharacterType() {
|
||||
function saveCharacterType() {
|
||||
const characterTypeData = {
|
||||
id: selectedCharacterType.value!.id,
|
||||
name: characterName.value,
|
||||
@ -107,14 +103,12 @@ async function saveCharacterType() {
|
||||
spriteId: characterSpriteId.value
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_UPDATE, characterTypeData, async (response: boolean) => {
|
||||
gameStore.connection?.emit('gm:characterType:update', characterTypeData, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save character type')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('character_types', new CharacterTypeStorage())
|
||||
await refreshCharacterTypeList(false)
|
||||
refreshCharacterTypeList(false)
|
||||
})
|
||||
}
|
||||
|
||||
@ -124,13 +118,13 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
|
||||
characterGender.value = characterType.gender
|
||||
characterRace.value = characterType.race
|
||||
characterIsSelectable.value = characterType.isSelectable
|
||||
characterSpriteId.value = characterType.sprite?.id
|
||||
characterSpriteId.value = characterType.spriteId
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!selectedCharacterType.value) return
|
||||
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
|
@ -9,8 +9,8 @@
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||
<a
|
||||
v-for="{ data: characterType } in list"
|
||||
:key="characterType.id"
|
||||
@ -25,16 +25,14 @@
|
||||
</div>
|
||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { CharacterType } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
@ -54,13 +52,13 @@ const handleSearch = () => {
|
||||
}
|
||||
|
||||
const createNewCharacterType = () => {
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_CREATE, {}, (response: boolean) => {
|
||||
gameStore.connection?.emit('gm:characterType:create', {}, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to create new character type')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
||||
assetManagerStore.setCharacterTypeList(response)
|
||||
})
|
||||
})
|
||||
@ -94,7 +92,7 @@ function toTop() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
||||
assetManagerStore.setCharacterTypeList(response)
|
||||
})
|
||||
})
|
||||
|
@ -44,9 +44,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import type { Item, ItemRarity, ItemType } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
@ -82,7 +80,7 @@ if (selectedItem.value) {
|
||||
function removeItem() {
|
||||
if (!selectedItem.value) return
|
||||
|
||||
socketManager.emit(SocketEvent.GM_ITEM_REMOVE, { id: selectedItem.value.id }, (response: boolean) => {
|
||||
gameStore.connection?.emit('gm:item:remove', { id: selectedItem.value.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to remove item')
|
||||
return
|
||||
@ -92,7 +90,7 @@ function removeItem() {
|
||||
}
|
||||
|
||||
function refreshItemList(unsetSelectedItem = true) {
|
||||
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||
assetManagerStore.setItemList(response)
|
||||
|
||||
if (unsetSelectedItem) {
|
||||
@ -112,7 +110,7 @@ function saveItem() {
|
||||
spriteId: itemSpriteId.value
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_ITEM_UPDATE, itemData, (response: boolean) => {
|
||||
gameStore.connection?.emit('gm:item:update', itemData, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save item')
|
||||
return
|
||||
@ -134,7 +132,7 @@ watch(selectedItem, (item: Item | null) => {
|
||||
onMounted(() => {
|
||||
if (!selectedItem.value) return
|
||||
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
|
@ -9,8 +9,8 @@
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||
<a v-for="{ data: item } in list" :key="item.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedItem?.id === item.id }" @click="assetManagerStore.setSelectedItem(item as Item)">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedItem?.id === item.id }">
|
||||
@ -22,16 +22,14 @@
|
||||
</div>
|
||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Item } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
@ -50,13 +48,13 @@ const handleSearch = () => {
|
||||
}
|
||||
|
||||
const createNewItem = () => {
|
||||
socketManager.emit(SocketEvent.GM_ITEM_CREATE, {}, (response: boolean) => {
|
||||
gameStore.connection?.emit('gm:item:create', {}, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to create new item')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||
assetManagerStore.setItemList(response)
|
||||
})
|
||||
})
|
||||
@ -90,7 +88,7 @@ function toTop() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||
assetManagerStore.setItemList(response)
|
||||
})
|
||||
})
|
||||
|
@ -1,217 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||
<div class="grid grid-cols-[160px_auto_max-content] gap-12">
|
||||
<div>
|
||||
<input type="checkbox" checked v-model="showOrigin" /><label>Show Origin</label>
|
||||
<br />
|
||||
<input type="checkbox" checked v-model="showPartitionOverlay" /><label>Show Partitions</label>
|
||||
</div>
|
||||
<div class="relative w-fit h-fit">
|
||||
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" ref="imageRef" />
|
||||
<svg ref="svg" class="absolute top-0 left-0 w-full h-full inline-block pointer-events-none">
|
||||
<circle v-if="showOrigin && svg" r="4" :cx="mapObjectOriginX * width" :cy="mapObjectOriginY * height" stroke="white" stroke-width="2" />
|
||||
<rect v-if="showPartitionOverlay && svg" v-for="(offset, index) in mapObjectDepthOffsets" style="opacity: 0.5" stroke="red" :x="index * (width / mapObjectDepthOffsets.length)" :width="width / mapObjectDepthOffsets.length" :y="0" :height="height" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" @click="mapObjectDepthOffsets.push(0)">Add Partition</button>
|
||||
<p>Depth Offset</p>
|
||||
<div class="text-white grid grid-cols-[120px_80px_auto] items-baseline gap-2" v-for="(offset, index) in mapObjectDepthOffsets">
|
||||
<input class="input-field max-h-4 mt-2" type="number" :value="offset" @change="setPartitionDepth($event, index)" />
|
||||
<button @click="mapObjectDepthOffsets.splice(index, 1)">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 block">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
<input v-model="mapObjectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-x">Origin X</label>
|
||||
<input v-model="mapObjectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-y">Origin Y</label>
|
||||
<input v-model="mapObjectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="tags">Tags</label>
|
||||
<ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="frame-speed">Frame rate</label>
|
||||
<input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="frame-width">Frame width</label>
|
||||
<input v-model="mapObjectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="frame-height">Frame height</label>
|
||||
<input v-model="mapObjectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<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="removeObject">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { MapObject } from '@/application/types'
|
||||
import { downloadCache } from '@/application/utilities'
|
||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { MapObjectStorage } from '@/storage/storages'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { Rectangle } from 'phavuer'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
|
||||
const svg = useTemplateRef('svg')
|
||||
const { width, height } = useElementSize(svg)
|
||||
|
||||
const mapObjectName = ref('')
|
||||
const mapObjectTags = ref<string[]>([])
|
||||
const mapObjectDepthOffsets = ref<number[]>([])
|
||||
const mapObjectOriginX = ref(0)
|
||||
const mapObjectOriginY = ref(0)
|
||||
const mapObjectFrameRate = ref(0)
|
||||
const mapObjectFrameWidth = ref(0)
|
||||
const mapObjectFrameHeight = ref(0)
|
||||
const imageRef = ref<HTMLImageElement | null>(null)
|
||||
const showOrigin = ref(true)
|
||||
const showPartitionOverlay = ref(true)
|
||||
|
||||
if (!selectedMapObject.value) {
|
||||
console.error('No map mapObject selected')
|
||||
}
|
||||
|
||||
if (selectedMapObject.value) {
|
||||
mapObjectName.value = selectedMapObject.value.name
|
||||
mapObjectTags.value = selectedMapObject.value.tags
|
||||
mapObjectDepthOffsets.value = selectedMapObject.value.depthOffsets
|
||||
mapObjectOriginX.value = selectedMapObject.value.originX
|
||||
mapObjectOriginY.value = selectedMapObject.value.originY
|
||||
mapObjectFrameRate.value = selectedMapObject.value.frameRate
|
||||
mapObjectFrameWidth.value = selectedMapObject.value.frameWidth
|
||||
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
|
||||
}
|
||||
|
||||
const setPartitionDepth = (event: any, idx: number) => (mapObjectDepthOffsets.value[idx] = Number.parseInt(event.target.value))
|
||||
|
||||
async function removeObject() {
|
||||
if (!selectedMapObject.value) return
|
||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_REMOVE, { mapObjectId: selectedMapObject.value.id }, async (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to remove mapObject')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('map_objects', new MapObjectStorage())
|
||||
await refreshObjectList()
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshObjectList(unsetSelectedMapObject = true) {
|
||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||
assetManagerStore.setMapObjectList(response)
|
||||
|
||||
if (unsetSelectedMapObject) {
|
||||
assetManagerStore.setSelectedMapObject(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function saveObject() {
|
||||
if (!selectedMapObject.value) {
|
||||
console.error('No mapObject selected')
|
||||
return
|
||||
}
|
||||
socketManager.emit(
|
||||
SocketEvent.GM_MAPOBJECT_UPDATE,
|
||||
{
|
||||
id: selectedMapObject.value.id,
|
||||
name: mapObjectName.value,
|
||||
tags: mapObjectTags.value,
|
||||
depthOffsets: mapObjectDepthOffsets.value,
|
||||
originX: mapObjectOriginX.value,
|
||||
originY: mapObjectOriginY.value,
|
||||
frameRate: mapObjectFrameRate.value,
|
||||
frameWidth: mapObjectFrameWidth.value,
|
||||
frameHeight: mapObjectFrameHeight.value
|
||||
},
|
||||
async (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save mapObject')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('map_objects', new MapObjectStorage())
|
||||
await refreshObjectList(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
watch(selectedMapObject, (mapObject: MapObject | null) => {
|
||||
if (!mapObject) return
|
||||
mapObjectName.value = mapObject.name
|
||||
mapObjectTags.value = mapObject.tags
|
||||
mapObjectDepthOffsets.value = mapObject.depthOffsets
|
||||
mapObjectOriginX.value = mapObject.originX
|
||||
mapObjectOriginY.value = mapObject.originY
|
||||
mapObjectFrameRate.value = mapObject.frameRate
|
||||
mapObjectFrameWidth.value = mapObject.frameWidth
|
||||
mapObjectFrameHeight.value = mapObject.frameHeight
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!selectedMapObject.value) return
|
||||
})
|
||||
|
||||
// function startDragging(index: number, event: MouseEvent) {
|
||||
// isDragging.value = true
|
||||
// draggedPointIndex.value = index
|
||||
//
|
||||
// const moveHandler = (e: MouseEvent) => {
|
||||
// if (!isDragging.value || !imageRef.value) return
|
||||
// const rect = imageRef.value.getBoundingClientRect()
|
||||
// mapObjectPivotPoints.value[draggedPointIndex.value] = {
|
||||
// x: e.clientX - rect.left,
|
||||
// y: e.clientY - rect.top
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// const upHandler = () => {
|
||||
// isDragging.value = false
|
||||
// draggedPointIndex.value = -1
|
||||
// window.removeEventListener('mousemove', moveHandler)
|
||||
// window.removeEventListener('mouseup', upHandler)
|
||||
// }
|
||||
//
|
||||
// window.addEventListener('mousemove', moveHandler)
|
||||
// window.addEventListener('mouseup', upHandler)
|
||||
// }
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
assetManagerStore.setSelectedMapObject(null)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pointer-events-none {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
|
||||
</div>
|
||||
<div class="mt-5 block">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
<input v-model="objectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-x">Origin X</label>
|
||||
<input v-model="objectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-y">Origin Y</label>
|
||||
<input v-model="objectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="tags">Tags</label>
|
||||
<ChipsInput v-model="objectTags" @update:modelValue="objectTags = $event" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="is-animated">Is animated</label>
|
||||
<select v-model="objectIsAnimated" class="input-field" name="is-animated">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="frame-speed">Frame rate</label>
|
||||
<input v-model="objectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="frame-width">Frame width</label>
|
||||
<input v-model="objectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="frame-height">Frame height</label>
|
||||
<input v-model="objectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<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="removeObject">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import type { Object } from '@/application/types'
|
||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
const selectedObject = computed(() => assetManagerStore.selectedObject)
|
||||
|
||||
const objectName = ref('')
|
||||
const objectTags = ref<string[]>([])
|
||||
const objectOriginX = ref(0)
|
||||
const objectOriginY = ref(0)
|
||||
const objectIsAnimated = ref(false)
|
||||
const objectFrameRate = ref(0)
|
||||
const objectFrameWidth = ref(0)
|
||||
const objectFrameHeight = ref(0)
|
||||
|
||||
if (!selectedObject.value) {
|
||||
console.error('No object selected')
|
||||
}
|
||||
|
||||
if (selectedObject.value) {
|
||||
objectName.value = selectedObject.value.name
|
||||
objectTags.value = selectedObject.value.tags
|
||||
objectOriginX.value = selectedObject.value.originX
|
||||
objectOriginY.value = selectedObject.value.originY
|
||||
objectIsAnimated.value = selectedObject.value.isAnimated
|
||||
objectFrameRate.value = selectedObject.value.frameRate
|
||||
objectFrameWidth.value = selectedObject.value.frameWidth
|
||||
objectFrameHeight.value = selectedObject.value.frameHeight
|
||||
}
|
||||
|
||||
function removeObject() {
|
||||
gameStore.connection?.emit('gm:object:remove', { object: selectedObject.value?.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to remove object')
|
||||
return
|
||||
}
|
||||
refreshObjectList()
|
||||
})
|
||||
}
|
||||
|
||||
function refreshObjectList(unsetSelectedObject = true) {
|
||||
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
||||
assetManagerStore.setObjectList(response)
|
||||
|
||||
if (unsetSelectedObject) {
|
||||
assetManagerStore.setSelectedObject(null)
|
||||
}
|
||||
|
||||
if (zoneEditorStore.active) {
|
||||
zoneEditorStore.setObjectList(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function saveObject() {
|
||||
if (!selectedObject.value) {
|
||||
console.error('No object selected')
|
||||
return
|
||||
}
|
||||
|
||||
gameStore.connection?.emit(
|
||||
'gm:object:update',
|
||||
{
|
||||
id: selectedObject.value.id,
|
||||
name: objectName.value,
|
||||
tags: objectTags.value,
|
||||
originX: objectOriginX.value,
|
||||
originY: objectOriginY.value,
|
||||
isAnimated: objectIsAnimated.value,
|
||||
frameRate: objectFrameRate.value,
|
||||
frameWidth: objectFrameWidth.value,
|
||||
frameHeight: objectFrameHeight.value
|
||||
},
|
||||
(response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save object')
|
||||
return
|
||||
}
|
||||
refreshObjectList(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
watch(selectedObject, (object: Object | null) => {
|
||||
if (!object) return
|
||||
objectName.value = object.name
|
||||
objectTags.value = object.tags
|
||||
objectOriginX.value = object.originX
|
||||
objectOriginY.value = object.originY
|
||||
objectIsAnimated.value = object.isAnimated
|
||||
objectFrameRate.value = object.frameRate
|
||||
objectFrameWidth.value = object.frameWidth
|
||||
objectFrameHeight.value = object.frameHeight
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!selectedObject.value) return
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
assetManagerStore.setSelectedObject(null)
|
||||
})
|
||||
</script>
|
@ -8,20 +8,20 @@
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||
<a v-for="{ data: mapObject } in list" :key="mapObject.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedMapObject?.id === mapObject.id }" @click="assetManagerStore.setSelectedMapObject(mapObject as MapObject)">
|
||||
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||
<a v-for="{ data: object } in list" :key="object.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedObject?.id === object.id }" @click="assetManagerStore.setSelectedObject(object as Object)">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="h-7 w-16 max-w-16 flex justify-center">
|
||||
<img class="h-7" :src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`" alt="Object" />
|
||||
<img class="h-7" :src="`${config.server_endpoint}/assets/objects/${object.id}.png`" alt="Object" />
|
||||
</div>
|
||||
<span :class="{ 'text-white': assetManagerStore.selectedMapObject?.id === mapObject.id }">{{ mapObject.name }}</span>
|
||||
<span :class="{ 'text-white': assetManagerStore.selectedObject?.id === object.id }">{{ object.name }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -29,9 +29,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { MapObject } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import type { Object } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
@ -49,14 +47,14 @@ const elementToScroll = ref()
|
||||
const handleFileUpload = (e: Event) => {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files) return
|
||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_UPLOAD, files, (response: boolean) => {
|
||||
gameStore.connection?.emit('gm:object:upload', files, (response: boolean) => {
|
||||
if (!response) {
|
||||
if (config.environment === 'development') console.error('Failed to upload map object')
|
||||
if (config.development) console.error('Failed to upload object')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||
assetManagerStore.setMapObjectList(response)
|
||||
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
||||
assetManagerStore.setObjectList(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -68,9 +66,9 @@ const handleSearch = () => {
|
||||
|
||||
const filteredObjects = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return assetManagerStore.mapObjectList
|
||||
return assetManagerStore.objectList
|
||||
}
|
||||
return assetManagerStore.mapObjectList.filter((object) => object.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||
return assetManagerStore.objectList.filter((object) => object.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||
})
|
||||
|
||||
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredObjects, {
|
||||
@ -94,8 +92,8 @@ function toTop() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||
assetManagerStore.setMapObjectList(response)
|
||||
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
||||
assetManagerStore.setObjectList(response)
|
||||
})
|
||||
})
|
||||
</script>
|
@ -1,76 +1,90 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="relative flex flex-col">
|
||||
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray mb-4">
|
||||
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray">
|
||||
<div class="w-full flex flex-col">
|
||||
<label class="mb-1.5 font-titles" for="name">Name</label>
|
||||
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
|
||||
</div>
|
||||
|
||||
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
||||
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
||||
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
||||
<button class="btn-indigo px-4 py-2 flex-1 sm:flex-none" type="button" @click.prevent="copySprite">
|
||||
<button class="btn bg-indigo-500 hover:bg-indigo-600 rounded text-white px-4 py-2 flex-1 sm:flex-none" type="button" @click.prevent="copySprite">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-cyan px-4" type="button" @click.prevent="addNewImage">New action</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="action in spriteActions" :key="action.id">
|
||||
<div class="flex flex-wrap gap-3 mb-3">
|
||||
<div v-for="(image, index) in action.sprites" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group">
|
||||
<img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" />
|
||||
<div v-if="imageDimensions[index]" class="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-1 py-0.5 rounded transition-opacity font-default">{{ imageDimensions[index].width }}x{{ imageDimensions[index].height }}</div>
|
||||
|
||||
<button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button>
|
||||
<Accordion v-for="action in spriteActions" :key="action.id">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
{{ action.action }}
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveSprite">
|
||||
<div class="form-field-full">
|
||||
<label for="action">Action</label>
|
||||
<input v-model="action.action" class="input-field" type="text" name="action" placeholder="Action" />
|
||||
</div>
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="mr-3 space-x-2">
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24 text-left" type="button" @click.stop.prevent="openEditorModal(action)">
|
||||
Editor
|
||||
<div class="flex">
|
||||
<small class="text-xs font-default">{{ action.action }}</small>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-x">Origin X</label>
|
||||
<input v-model.number="action.originX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
</div>
|
||||
</button>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-y">Origin Y</label>
|
||||
<input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="is-animated">Is animated</label>
|
||||
<select v-model="action.isAnimated" class="input-field" name="is-animated">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-half" v-if="action.isAnimated">
|
||||
<label for="is-looping">Is looping</label>
|
||||
<select v-model="action.isLooping" class="input-field" name="is-looping">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<SpriteEditor
|
||||
v-for="[actionId, editorData] in Array.from(openEditors.entries())"
|
||||
:key="actionId"
|
||||
:sprite="selectedSprite!"
|
||||
:sprites="editorData.action.sprites"
|
||||
:frame-rate="editorData.action.frameRate"
|
||||
:is-modal-open="editorData.isOpen"
|
||||
:temp-offset-index="getTempOffsetIndex(editorData.action)"
|
||||
:temp-offset="getTempOffset(editorData.action)"
|
||||
@update:frame-rate="(value) => updateFrameRate(editorData.action, value)"
|
||||
@update:is-modal-open="(value) => handleEditorModalClose(editorData.action, value)"
|
||||
@update:temp-offset="(index, offset) => handleTempOffsetChange(editorData.action, index, offset)"
|
||||
/>
|
||||
<div class="form-field-full" v-if="action.isAnimated">
|
||||
<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 class="form-field-full">
|
||||
<SpriteActionsInput v-model="action.sprites" />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Sprite, SpriteAction } from '@/application/types'
|
||||
import { downloadCache, uuidv4 } from '@/application/utilities'
|
||||
import SpriteEditor from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteEditor.vue'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { SpriteStorage } from '@/storage/storages'
|
||||
import { uuidv4 } from '@/application/utilities'
|
||||
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
||||
import Accordion from '@/components/utilities/Accordion.vue'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
||||
const selectedSprite = computed(() => assetManagerStore.selectedSprite)
|
||||
const tempOffsetData = ref<Map<string, { index: number | undefined; offset: { x: number; y: number } | undefined }>>(new Map())
|
||||
|
||||
const spriteName = ref('')
|
||||
const spriteActions = ref<SpriteAction[]>([])
|
||||
|
||||
const openEditors = ref(new Map<string, { action: SpriteAction; isOpen: boolean }>())
|
||||
|
||||
if (!selectedSprite.value) {
|
||||
console.error('No sprite selected')
|
||||
}
|
||||
@ -80,32 +94,28 @@ if (selectedSprite.value) {
|
||||
spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
|
||||
}
|
||||
|
||||
async function deleteSprite() {
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_DELETE, { id: selectedSprite.value?.id }, async (response: boolean) => {
|
||||
function deleteSprite() {
|
||||
gameStore.connection?.emit('gm:sprite:delete', { id: selectedSprite.value?.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to delete sprite')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('sprites', new SpriteStorage())
|
||||
await refreshSpriteList()
|
||||
refreshSpriteList()
|
||||
})
|
||||
}
|
||||
|
||||
async function copySprite() {
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_COPY, { id: selectedSprite.value?.id }, async (response: boolean) => {
|
||||
function copySprite() {
|
||||
gameStore.connection?.emit('gm:sprite:copy', { id: selectedSprite.value?.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to copy sprite')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('sprites', new SpriteStorage())
|
||||
await refreshSpriteList(false)
|
||||
refreshSpriteList(false)
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshSpriteList(unsetSelectedSprite = true) {
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
function refreshSpriteList(unsetSelectedSprite = true) {
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
|
||||
if (unsetSelectedSprite) {
|
||||
@ -114,7 +124,7 @@ async function refreshSpriteList(unsetSelectedSprite = true) {
|
||||
})
|
||||
}
|
||||
|
||||
async function saveSprite() {
|
||||
function saveSprite() {
|
||||
if (!selectedSprite.value) {
|
||||
console.error('No sprite selected')
|
||||
return
|
||||
@ -130,6 +140,8 @@ async function saveSprite() {
|
||||
sprites: action.sprites,
|
||||
originX: action.originX,
|
||||
originY: action.originY,
|
||||
isAnimated: action.isAnimated,
|
||||
isLooping: action.isLooping,
|
||||
frameRate: action.frameRate,
|
||||
frameWidth: action.frameWidth,
|
||||
frameHeight: action.frameHeight
|
||||
@ -137,14 +149,12 @@ async function saveSprite() {
|
||||
}) ?? []
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_UPDATE, updatedSprite, async (response: boolean) => {
|
||||
gameStore.connection?.emit('gm:sprite:update', updatedSprite, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save sprite')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('sprites', new SpriteStorage())
|
||||
await refreshSpriteList(false)
|
||||
refreshSpriteList(false)
|
||||
})
|
||||
}
|
||||
|
||||
@ -153,11 +163,14 @@ function addNewImage() {
|
||||
|
||||
const newImage: SpriteAction = {
|
||||
id: uuidv4(),
|
||||
sprite: selectedSprite.value.id,
|
||||
spriteId: selectedSprite.value.id,
|
||||
sprite: selectedSprite.value,
|
||||
action: 'new_action',
|
||||
sprites: [],
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
isAnimated: false,
|
||||
isLooping: false,
|
||||
frameRate: 0,
|
||||
frameWidth: 0,
|
||||
frameHeight: 0
|
||||
@ -171,74 +184,15 @@ function addNewImage() {
|
||||
}
|
||||
|
||||
function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
|
||||
if (!actions) return []
|
||||
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
||||
}
|
||||
|
||||
function openEditorModal(action: SpriteAction) {
|
||||
const newOpenEditors = new Map(openEditors.value)
|
||||
newOpenEditors.set(action.id, { action, isOpen: true })
|
||||
openEditors.value = newOpenEditors
|
||||
}
|
||||
|
||||
function updateFrameRate(action: SpriteAction, value: number) {
|
||||
console.log('update frame rate', action)
|
||||
action.frameRate = value
|
||||
}
|
||||
|
||||
function handleEditorModalClose(action: SpriteAction, isOpen: boolean) {
|
||||
if (isOpen) return
|
||||
const newOpenEditors = new Map(openEditors.value)
|
||||
newOpenEditors.delete(action.id)
|
||||
openEditors.value = newOpenEditors
|
||||
}
|
||||
|
||||
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
|
||||
// Update the temporary offset data for this action
|
||||
const newTempOffsetData = new Map(tempOffsetData.value)
|
||||
newTempOffsetData.set(action.id, { index, offset })
|
||||
tempOffsetData.value = newTempOffsetData
|
||||
|
||||
// Also update the actual sprite data so changes persist
|
||||
if (action.sprites && action.sprites[index]) {
|
||||
action.sprites[index].offset = { ...offset };
|
||||
}
|
||||
}
|
||||
|
||||
function getTempOffsetIndex(action: SpriteAction): number | undefined {
|
||||
return tempOffsetData.value.get(action.id)?.index
|
||||
}
|
||||
|
||||
function getTempOffset(action: SpriteAction): { x: number; y: number } | undefined {
|
||||
return tempOffsetData.value.get(action.id)?.offset
|
||||
}
|
||||
|
||||
watch(selectedSprite, (sprite: Sprite | null) => {
|
||||
if (!sprite) return
|
||||
spriteName.value = sprite.name
|
||||
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
||||
openEditors.value = new Map()
|
||||
tempOffsetData.value = new Map() // Reset temp offset data when sprite changes
|
||||
})
|
||||
|
||||
interface SpriteImage {
|
||||
url: string
|
||||
offset: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
}
|
||||
|
||||
const imageDimensions = ref<{ [key: number]: { width: number; height: number } }>({})
|
||||
|
||||
const updateImageDimensions = (event: Event, index: number) => {
|
||||
const img = event.target as HTMLImageElement
|
||||
imageDimensions.value[index] = {
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!selectedSprite.value) return
|
||||
})
|
||||
|
@ -7,8 +7,8 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||
<a v-for="{ data: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span :class="{ 'text-white': assetManagerStore.selectedSprite?.id === sprite.id }">{{ sprite.name }}</span>
|
||||
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -25,9 +25,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Sprite } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
@ -42,13 +40,13 @@ const hasScrolled = ref(false)
|
||||
const elementToScroll = ref()
|
||||
|
||||
function newButtonClickHandler() {
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_CREATE, {}, (response: boolean) => {
|
||||
gameStore.connection?.emit('gm:sprite:create', {}, (response: boolean) => {
|
||||
if (!response) {
|
||||
if (config.environment === 'development') console.error('Failed to create new sprite')
|
||||
if (config.development) console.error('Failed to create new sprite')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
@ -87,7 +85,7 @@ function toTop() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
|
@ -1,366 +0,0 @@
|
||||
<template>
|
||||
<Modal :is-modal-open="isModalOpen" :modal-width="700" :modal-height="330" :can-full-screen="true" bg-style="none" @modal:close="closeModal">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">Sprite editor</h3>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<div class="m-4 flex gap-4 h-full">
|
||||
<!-- Settings -->
|
||||
<div class="w-80 h-full flex flex-col overflow-y-auto">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col">
|
||||
<label class="block mb-1 text-white text-sm">Frame Rate: {{ frameRate }} FPS</label>
|
||||
<div class="text-xs font-default text-gray-400 mb-1">Duration: {{ totalDuration }}s</div>
|
||||
<input type="range" v-model.number="localFrameRate" min="0" max="60" step="1" class="w-full accent-cyan-500" @input="updateFrameRate" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label class="block mb-1 text-white text-sm">Frame: {{ currentFrame + 1 }} of {{ sprites.length }}</label>
|
||||
<input type="range" v-model.number="currentFrame" :min="0" :max="sprites.length - 1" step="1" class="w-full accent-cyan-500" @input="stopAnimation" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label class="block mb-1 text-white text-sm">Zoom: {{ zoomLevel }}%</label>
|
||||
<input type="range" v-model.number="zoomLevel" min="10" max="600" step="10" class="w-full accent-cyan-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 space-y-2">
|
||||
<button @click="toggleAnimation" class="px-3 py-1 bg-cyan-600 hover:bg-cyan-700 text-white rounded transition-colors w-full">
|
||||
{{ isAnimating ? 'Pause' : 'Play' }}
|
||||
</button>
|
||||
<button @click="toggleReferenceSprites" class="px-3 py-1 bg-cyan-600 hover:bg-cyan-700 text-white rounded transition-colors w-full">
|
||||
{{ showReferenceSprites ? 'Hide References' : 'Show References' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="">
|
||||
<div class="relative flex py-5 items-center">
|
||||
<div class="flex-grow border-t border-gray-400"></div>
|
||||
<span class="flex-shrink mx-4 text-gray-400">Sprite action</span>
|
||||
<div class="flex-grow border-solid border-gray-200"></div>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="action">Name</label>
|
||||
<input class="input-field" type="text" name="action" placeholder="Action" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-x">Origin X</label>
|
||||
<input class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-y">Origin Y</label>
|
||||
<input class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
</div>
|
||||
<div class="relative flex py-5 items-center">
|
||||
<div class="flex-grow border-t border-gray-400"></div>
|
||||
<span class="flex-shrink mx-4 text-gray-400">Sprite action image</span>
|
||||
<div class="flex-grow border-t border-gray-400"></div>
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="offset-x">Offset X</label>
|
||||
<input class="input-field" type="number" step="1" v-model.number="offsetXModel" :disabled="isAnimating" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="offset-y">Offset Y</label>
|
||||
<input class="input-field" type="number" step="1" v-model.number="offsetYModel" :disabled="isAnimating" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="frame-speed">Frame rate</label>
|
||||
<input class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sprite thumbnails -->
|
||||
<div class="flex-1 flex flex-col h-full">
|
||||
<div class="bg-gray-800 border-solid border-white/10 rounded flex-grow mb-2 relative overflow-hidden" @mousedown="startDrag" @mousemove="onDrag" @mouseup="stopDrag" @mouseleave="stopDrag">
|
||||
<!-- Background reference sprites (semi-transparent) -->
|
||||
<img
|
||||
v-for="(sprite, index) in spritesWithTempOffset"
|
||||
:key="`bg-${index}`"
|
||||
:src="sprite.url"
|
||||
alt="Reference sprite"
|
||||
v-show="index !== currentFrame && showReferenceSprites"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: `${(sprite.offset?.x || 0) * (zoomLevel / 100)}px`,
|
||||
bottom: `${(sprite.offset?.y || 0) * (zoomLevel / 100)}px`,
|
||||
opacity: 0.3,
|
||||
transform: `scale(${zoomLevel / 100})`,
|
||||
transformOrigin: 'bottom left',
|
||||
pointerEvents: 'none'
|
||||
}"
|
||||
/>
|
||||
<!-- Current sprite (draggable) -->
|
||||
<img
|
||||
v-for="(sprite, index) in spritesWithTempOffset"
|
||||
:key="index"
|
||||
:src="sprite.url"
|
||||
alt="Sprite"
|
||||
:class="{ 'cursor-move': currentFrame === index }"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: `${(sprite.offset?.x || 0) * (zoomLevel / 100)}px`,
|
||||
bottom: `${(sprite.offset?.y || 0) * (zoomLevel / 100)}px`,
|
||||
display: currentFrame === index ? 'block' : 'none',
|
||||
transform: `scale(${zoomLevel / 100})`,
|
||||
transformOrigin: 'bottom left',
|
||||
userSelect: 'none',
|
||||
pointerEvents: currentFrame === index ? 'auto' : 'none'
|
||||
}"
|
||||
@dragstart.prevent
|
||||
/>
|
||||
</div>
|
||||
<div class="bg-gray-800 p-2 overflow-x-auto border-solid border-white/10 rounded mb-8 h-24 min-h-16">
|
||||
<div class="flex gap-2">
|
||||
<div
|
||||
v-for="(sprite, index) in sprites"
|
||||
:key="`thumb-${index}`"
|
||||
class="relative cursor-pointer border-solid transition-all duration-200 rounded flex-shrink-0 p-3 px-12"
|
||||
:class="currentFrame === index ? 'border-cyan-600 bg-cyan-500/10' : 'border-transparent hover:border-white/30'"
|
||||
@click="selectFrame(index)"
|
||||
>
|
||||
<img :src="sprite.url" alt="Sprite thumbnail" class="h-16 w-auto object-contain rounded" />
|
||||
<div class="absolute top-0 right-0 bg-gray-400 text-white text-xs font-default px-1 rounded-bl">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Sprite, SpriteImage } from '@/application/types'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
sprite: Sprite
|
||||
sprites: SpriteImage[]
|
||||
frameRate: number
|
||||
isModalOpen?: boolean
|
||||
tempOffsetIndex?: number
|
||||
tempOffset?: { x: number; y: number }
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:frameRate', value: number): void
|
||||
(e: 'update:isModalOpen', value: boolean): void
|
||||
(e: 'update:tempOffset', index: number, offset: { x: number; y: number }): void
|
||||
}>()
|
||||
|
||||
const currentFrame = ref(0)
|
||||
const localFrameRate = ref(props.frameRate)
|
||||
const zoomLevel = ref(100)
|
||||
const isAnimating = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const startDragPos = ref({ x: 0, y: 0 })
|
||||
const currentOffset = ref({ x: 0, y: 0 })
|
||||
let animationInterval: number | null = null
|
||||
|
||||
const totalDuration = computed(() => {
|
||||
if (props.frameRate <= 0) return 0
|
||||
return (props.sprites.length / props.frameRate).toFixed(2)
|
||||
})
|
||||
|
||||
const spritesWithTempOffset = computed(() => {
|
||||
return props.sprites.map((sprite, index) => {
|
||||
if (index === props.tempOffsetIndex && props.tempOffset) {
|
||||
return { ...sprite, offset: props.tempOffset }
|
||||
}
|
||||
return sprite
|
||||
})
|
||||
})
|
||||
|
||||
const currentSprite = computed(() => {
|
||||
if (currentFrame.value >= 0 && currentFrame.value < spritesWithTempOffset.value.length) {
|
||||
return spritesWithTempOffset.value[currentFrame.value]
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// Create computed properties with getters and setters for two-way binding
|
||||
const offsetXModel = computed({
|
||||
get: () => currentSprite.value?.offset?.x || 0,
|
||||
set: (value) => {
|
||||
if (isAnimating.value) return
|
||||
const newOffset = {
|
||||
x: value,
|
||||
y: currentSprite.value?.offset?.y || 0
|
||||
}
|
||||
emit('update:tempOffset', currentFrame.value, newOffset)
|
||||
}
|
||||
})
|
||||
|
||||
const offsetYModel = computed({
|
||||
get: () => currentSprite.value?.offset?.y || 0,
|
||||
set: (value) => {
|
||||
if (isAnimating.value) return
|
||||
const newOffset = {
|
||||
x: currentSprite.value?.offset?.x || 0,
|
||||
y: value
|
||||
}
|
||||
emit('update:tempOffset', currentFrame.value, newOffset)
|
||||
}
|
||||
})
|
||||
|
||||
// Toggle for showing reference sprites
|
||||
const showReferenceSprites = ref(true)
|
||||
|
||||
function updateAnimation() {
|
||||
stopAnimation()
|
||||
if (props.frameRate <= 0 || props.sprites.length === 0) {
|
||||
currentFrame.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (isAnimating.value) {
|
||||
animationInterval = window.setInterval(() => {
|
||||
currentFrame.value = (currentFrame.value + 1) % props.sprites.length
|
||||
}, 1000 / props.frameRate)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAnimation() {
|
||||
isAnimating.value = !isAnimating.value
|
||||
if (isAnimating.value) {
|
||||
updateAnimation()
|
||||
} else {
|
||||
stopAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
function stopAnimation() {
|
||||
if (animationInterval) {
|
||||
clearInterval(animationInterval)
|
||||
animationInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
function selectFrame(index: number) {
|
||||
currentFrame.value = index
|
||||
stopAnimation()
|
||||
isAnimating.value = false
|
||||
}
|
||||
|
||||
function updateFrameRate() {
|
||||
emit('update:frameRate', localFrameRate.value)
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
emit('update:isModalOpen', false)
|
||||
}
|
||||
|
||||
function startDrag(event: MouseEvent) {
|
||||
if (isAnimating.value) return
|
||||
|
||||
const previewContainer = event.currentTarget as HTMLElement
|
||||
const rect = previewContainer.getBoundingClientRect()
|
||||
|
||||
// Store initial mouse position
|
||||
startDragPos.value = {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
}
|
||||
|
||||
// Store current offset
|
||||
if (currentSprite.value && currentSprite.value.offset) {
|
||||
currentOffset.value = {
|
||||
x: currentSprite.value.offset.x,
|
||||
y: currentSprite.value.offset.y
|
||||
}
|
||||
} else {
|
||||
currentOffset.value = { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
function onDrag(event: MouseEvent) {
|
||||
if (!isDragging.value) return
|
||||
|
||||
// Calculate the difference from the start position
|
||||
const deltaX = event.clientX - startDragPos.value.x
|
||||
const deltaY = startDragPos.value.y - event.clientY // Inverted for bottom positioning
|
||||
|
||||
// Apply the zoom factor to the delta
|
||||
// This ensures that the movement in screen pixels is converted to the correct
|
||||
// number of pixels at the sprite's natural size, regardless of zoom level
|
||||
const zoomFactor = 100 / zoomLevel.value
|
||||
const scaledDeltaX = deltaX * zoomFactor
|
||||
const scaledDeltaY = deltaY * zoomFactor
|
||||
|
||||
// Calculate new offset
|
||||
// These offsets are in the sprite's natural coordinate space (as if zoom was 100%)
|
||||
const newOffset = {
|
||||
x: currentOffset.value.x + scaledDeltaX,
|
||||
y: currentOffset.value.y + scaledDeltaY
|
||||
}
|
||||
|
||||
// Emit the new offset
|
||||
emit('update:tempOffset', currentFrame.value, newOffset)
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
if (isDragging.value && currentSprite.value?.offset) {
|
||||
// Ensure the final offset is applied when dragging stops
|
||||
emit('update:tempOffset', currentFrame.value, {
|
||||
x: currentSprite.value.offset.x,
|
||||
y: currentSprite.value.offset.y
|
||||
})
|
||||
}
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
function toggleReferenceSprites() {
|
||||
showReferenceSprites.value = !showReferenceSprites.value
|
||||
}
|
||||
|
||||
function updateOffset(event: Event, axis: 'x' | 'y') {
|
||||
if (isAnimating.value) return
|
||||
|
||||
const input = event.target as HTMLInputElement
|
||||
const value = parseInt(input.value) || 0
|
||||
|
||||
if (currentSprite.value && currentSprite.value.offset) {
|
||||
const newOffset = { ...currentSprite.value.offset }
|
||||
newOffset[axis] = value
|
||||
emit('update:tempOffset', currentFrame.value, newOffset)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.frameRate,
|
||||
(newValue) => {
|
||||
localFrameRate.value = newValue
|
||||
updateAnimation()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(() => props.sprites, updateAnimation, { immediate: true })
|
||||
|
||||
watch(
|
||||
() => isAnimating.value,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
updateAnimation()
|
||||
} else {
|
||||
stopAnimation()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
isAnimating.value = props.frameRate > 0
|
||||
if (isAnimating.value) {
|
||||
updateAnimation()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAnimation()
|
||||
})
|
||||
</script>
|
@ -0,0 +1,104 @@
|
||||
<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" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" />
|
||||
<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 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 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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</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 { ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: string[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string[]): void
|
||||
}>()
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const draggedIndex = ref<number | null>(null)
|
||||
|
||||
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/')) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (typeof e.target?.result === 'string') {
|
||||
updateImages([...props.modelValue, e.target.result])
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateImages = (newImages: string[]) => {
|
||||
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
|
||||
}
|
||||
</script>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||
<img class="max-h-72" :src="`${config.server_endpoint}/textures/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
||||
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
||||
</div>
|
||||
<div class="mt-5 block">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
||||
@ -24,16 +24,16 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Tile } from '@/application/types'
|
||||
import { downloadCache } from '@/application/utilities'
|
||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { TileStorage } from '@/storage/storages'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
const selectedTile = computed(() => assetManagerStore.selectedTile)
|
||||
|
||||
@ -55,49 +55,49 @@ watch(selectedTile, (tile: Tile | null) => {
|
||||
tileTags.value = tile.tags
|
||||
})
|
||||
|
||||
async function deleteTile() {
|
||||
socketManager.emit(SocketEvent.GM_TILE_DELETE, { id: selectedTile.value?.id }, async (response: boolean) => {
|
||||
function deleteTile() {
|
||||
gameStore.connection?.emit('gm:tile:delete', { id: selectedTile.value?.id }, (response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to delete tile')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('tiles', new TileStorage())
|
||||
await refreshTileList()
|
||||
refreshTileList()
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshTileList(unsetSelectedTile = true) {
|
||||
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||
function refreshTileList(unsetSelectedTile = true) {
|
||||
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
||||
assetManagerStore.setTileList(response)
|
||||
|
||||
if (unsetSelectedTile) {
|
||||
assetManagerStore.setSelectedTile(null)
|
||||
}
|
||||
|
||||
if (zoneEditorStore.active) {
|
||||
zoneEditorStore.setTileList(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function saveTile() {
|
||||
function saveTile() {
|
||||
if (!selectedTile.value) {
|
||||
console.error('No tile selected')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(
|
||||
gameStore.connection?.emit(
|
||||
'gm:tile:update',
|
||||
{
|
||||
id: selectedTile.value.id,
|
||||
name: tileName.value,
|
||||
tags: tileTags.value
|
||||
},
|
||||
async (response: boolean) => {
|
||||
(response: boolean) => {
|
||||
if (!response) {
|
||||
console.error('Failed to save tile')
|
||||
return
|
||||
}
|
||||
|
||||
await downloadCache('tiles', new TileStorage())
|
||||
await refreshTileList(false)
|
||||
refreshTileList(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -8,12 +8,12 @@
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||
<a v-for="{ data: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="h-7 w-16 max-w-16 flex justify-center">
|
||||
<img class="h-7" :src="`${config.server_endpoint}/textures/tiles/${tile.id}.png`" alt="Tile" />
|
||||
<img class="h-7" :src="`${config.server_endpoint}/assets/tiles/${tile.id}.png`" alt="Tile" />
|
||||
</div>
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedTile?.id === tile.id }">{{ tile.name }}</span>
|
||||
</div>
|
||||
@ -21,7 +21,7 @@
|
||||
</div>
|
||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -29,9 +29,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/application/config'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import type { Tile } from '@/application/types'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
@ -49,13 +47,13 @@ const elementToScroll = ref()
|
||||
const handleFileUpload = (e: Event) => {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files) return
|
||||
socketManager.emit(SocketEvent.GM_TILE_UPLOAD, files, (response: boolean) => {
|
||||
gameStore.connection?.emit('gm:tile:upload', files, (response: boolean) => {
|
||||
if (!response) {
|
||||
if (config.environment === 'development') console.error('Failed to upload tile')
|
||||
if (config.development) console.error('Failed to upload tile')
|
||||
return
|
||||
}
|
||||
|
||||
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
||||
assetManagerStore.setTileList(response)
|
||||
})
|
||||
})
|
||||
@ -94,7 +92,7 @@ function toTop() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
||||
assetManagerStore.setTileList(response)
|
||||
})
|
||||
})
|
||||
|
@ -1,245 +0,0 @@
|
||||
<template>
|
||||
<MapTiles ref="mapTiles" @createCommand="addCommand" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
||||
<PlacedMapObjects ref="mapObjects" @update="updateMapObjects" @updateAndCommit="updateAndCommit" @pauseObjectTracking="pause" @resumeObjectTracking="resume" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
||||
<MapEventTiles ref="eventTiles" @createCommand="addCommand" v-if="tileMap" :tileMap />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MapEventTile, Map as MapT, PlacedMapObject as PlacedMapObjectT } from '@/application/types'
|
||||
import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue'
|
||||
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
|
||||
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { cloneArray, createTileLayer, createTileMap, placeTiles } from '@/services/mapService'
|
||||
import { TileStorage } from '@/storage/storages'
|
||||
import { useRefHistory } from '@vueuse/core'
|
||||
import { useScene } from 'phavuer'
|
||||
import { onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
|
||||
|
||||
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
||||
|
||||
const mapEditor = useMapEditorComposable()
|
||||
const scene = useScene()
|
||||
|
||||
const mapTiles = useTemplateRef('mapTiles')
|
||||
const mapObjects = useTemplateRef('mapObjects')
|
||||
const eventTiles = useTemplateRef('eventTiles')
|
||||
|
||||
//Record of commands
|
||||
let commandStack: (EditorCommand | number)[] = []
|
||||
let commandIndex = ref(0)
|
||||
|
||||
let originTiles: string[][] = []
|
||||
let originEventTiles: MapEventTile[] = []
|
||||
let originObjects = ref<PlacedMapObjectT[]>(mapEditor.currentMap.value?.placedMapObjects ?? [])
|
||||
|
||||
const { undo, redo, commit, pause, resume, canUndo, canRedo } = useRefHistory(originObjects, { deep: true, capacity: 9 })
|
||||
|
||||
//Command Pattern basic interface, extended to store what elements have been changed by each edit
|
||||
export interface EditorCommand {
|
||||
apply: (elements: any[]) => any[]
|
||||
type: 'tile' | 'map_object' | 'event_tile'
|
||||
operation: 'draw' | 'erase' | 'clear'
|
||||
}
|
||||
|
||||
function applyCommands(tiles: any[], ...commands: EditorCommand[]): any[] {
|
||||
let tileVersion = cloneArray(tiles)
|
||||
for (let command of commands) {
|
||||
tileVersion = command.apply(tileVersion)
|
||||
}
|
||||
return tileVersion
|
||||
}
|
||||
|
||||
watch(
|
||||
() => mapEditor.shouldClearTiles.value,
|
||||
(shouldClear) => {
|
||||
if (shouldClear && mapEditor.currentMap.value) {
|
||||
mapTiles.value!.clearTiles()
|
||||
eventTiles.value!.clearTiles()
|
||||
mapEditor.currentMap.value.placedMapObjects = []
|
||||
mapEditor.currentMap.value.mapEventTiles = []
|
||||
updateAndCommit(mapEditor.currentMap.value)
|
||||
mapEditor.resetClearTilesFlag()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function update(commands: (EditorCommand | number)[]) {
|
||||
if (!mapEditor.currentMap.value) return
|
||||
|
||||
if (commandStack.length >= 9) {
|
||||
if (typeof commandStack[0] !== 'number') {
|
||||
const base = commandStack.shift() as EditorCommand
|
||||
if (base.operation !== 'clear') {
|
||||
switch (base.type) {
|
||||
case 'tile':
|
||||
originTiles = base.apply(originTiles) as string[][]
|
||||
break
|
||||
case 'event_tile':
|
||||
originEventTiles = base.apply(originEventTiles) as MapEventTile[]
|
||||
break
|
||||
}
|
||||
} else {
|
||||
commandStack.shift()
|
||||
}
|
||||
} else if (typeof commandStack[0] === 'number') {
|
||||
commandStack.shift()
|
||||
}
|
||||
}
|
||||
|
||||
let tileCommands = commands.filter((item) => typeof item !== 'number' && item.type === 'tile') as EditorCommand[]
|
||||
let eventTileCommands = commands.filter((item) => typeof item !== 'number' && item.type === 'event_tile') as EditorCommand[]
|
||||
|
||||
let modifiedTiles = applyCommands(originTiles, ...tileCommands)
|
||||
placeTiles(tileMap.value!, tileMapLayer.value!, modifiedTiles)
|
||||
|
||||
let eventTiles = applyCommands(originEventTiles, ...eventTileCommands)
|
||||
|
||||
mapEditor.currentMap.value.tiles = modifiedTiles
|
||||
mapEditor.currentMap.value.mapEventTiles = eventTiles
|
||||
mapEditor.currentMap.value.placedMapObjects = originObjects.value
|
||||
}
|
||||
|
||||
function updateMapObjects(map: MapT) {
|
||||
originObjects.value = map.placedMapObjects
|
||||
}
|
||||
|
||||
function updateAndCommit(map?: MapT) {
|
||||
commandStack = commandStack.slice(0, commandIndex.value)
|
||||
if (map) updateMapObjects(map)
|
||||
commit()
|
||||
commandStack.push(0)
|
||||
commandIndex.value = commandStack.length
|
||||
}
|
||||
|
||||
function addCommand(command: EditorCommand) {
|
||||
commandStack = commandStack.slice(0, commandIndex.value)
|
||||
commandStack.push(command)
|
||||
commandIndex.value = commandStack.length
|
||||
}
|
||||
|
||||
function undoEdit() {
|
||||
if (commandIndex.value > 0) {
|
||||
if (typeof commandStack[--commandIndex.value] === 'number' && canUndo) {
|
||||
undo()
|
||||
}
|
||||
update(commandStack.slice(0, commandIndex.value))
|
||||
}
|
||||
}
|
||||
|
||||
function redoEdit() {
|
||||
if (commandIndex.value <= 9 && commandIndex.value < commandStack.length) {
|
||||
if (typeof commandStack[commandIndex.value++] === 'number' && canRedo) {
|
||||
redo()
|
||||
}
|
||||
update(commandStack.slice(0, commandIndex.value))
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
||||
if (!mapTiles.value || !mapObjects.value || !eventTiles.value) return
|
||||
|
||||
// Check if left mouse button is pressed
|
||||
if (!pointer.isDown) return
|
||||
|
||||
// Check if shift is not pressed, this means we are moving the camera
|
||||
if (pointer.event.shiftKey) return
|
||||
|
||||
// Check if draw mode is tile
|
||||
switch (mapEditor.drawMode.value) {
|
||||
case 'tile':
|
||||
mapTiles.value.handlePointer(pointer)
|
||||
break
|
||||
case 'map_object':
|
||||
mapObjects.value.handlePointer(pointer)
|
||||
break
|
||||
case 'teleport':
|
||||
eventTiles.value.handlePointer(pointer)
|
||||
break
|
||||
case 'blocking tile':
|
||||
eventTiles.value.handlePointer(pointer)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
//CTRL+Y
|
||||
if (event.key === 'y' && event.ctrlKey) {
|
||||
redoEdit()
|
||||
}
|
||||
|
||||
//CTRL+Z
|
||||
if (event.key === 'z' && event.ctrlKey) {
|
||||
undoEdit()
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||
if (mapEditor.inputMode.value === 'hold' && pointer.isDown) {
|
||||
handlePointerDown(pointer)
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
||||
switch (mapEditor.drawMode.value) {
|
||||
case 'tile':
|
||||
mapTiles.value!.finalizeCommand()
|
||||
break
|
||||
case 'map_object':
|
||||
if (mapEditor.tool.value === 'pencil' || mapEditor.tool.value === 'eraser') {
|
||||
resume()
|
||||
updateAndCommit()
|
||||
}
|
||||
break
|
||||
case 'teleport':
|
||||
eventTiles.value!.finalizeCommand()
|
||||
break
|
||||
case 'blocking tile':
|
||||
eventTiles.value!.finalizeCommand()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
let mapValue = mapEditor.currentMap.value
|
||||
if (!mapValue) return
|
||||
|
||||
//Clone
|
||||
originTiles = cloneArray(mapValue.tiles)
|
||||
originEventTiles = cloneArray(mapValue.mapEventTiles)
|
||||
|
||||
const tileStorage = new TileStorage()
|
||||
const allTiles = await tileStorage.getAll()
|
||||
const allTileIds = allTiles.map((tile) => tile.id)
|
||||
|
||||
tileMap.value = createTileMap(scene, mapValue)
|
||||
tileMapLayer.value = createTileLayer(tileMap.value, allTileIds)
|
||||
|
||||
addEventListener('keydown', handleKeyDown)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (tileMap.value) {
|
||||
tileMap.value.destroyLayer('tiles')
|
||||
tileMap.value.removeAllLayers()
|
||||
tileMap.value.destroy()
|
||||
}
|
||||
|
||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||
mapEditor.reset()
|
||||
})
|
||||
|
||||
setInterval(() => {
|
||||
scene.children.queueDepthSort()
|
||||
}, 0.2)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
</script>
|
@ -1,156 +0,0 @@
|
||||
<template>
|
||||
<Image v-for="tile in mapEditor.currentMap.value?.mapEventTiles" v-bind="getImageProps(tile)" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MapEventTileType, type MapEventTile, type Map as MapT, type UUID } from '@/application/types'
|
||||
import { uuidv4 } from '@/application/utilities'
|
||||
import { type EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { cloneArray, getTile, tileToWorldX, tileToWorldY } from '@/services/mapService'
|
||||
import { Image } from 'phavuer'
|
||||
|
||||
const mapEditor = useMapEditorComposable()
|
||||
|
||||
defineExpose({ handlePointer, finalizeCommand, clearTiles })
|
||||
|
||||
const emit = defineEmits(['createCommand'])
|
||||
|
||||
const props = defineProps<{
|
||||
tileMap: Phaser.Tilemaps.Tilemap
|
||||
}>()
|
||||
|
||||
// *** COMMAND STATE ***
|
||||
|
||||
let currentCommand: EventTileCommand | null = null
|
||||
|
||||
class EventTileCommand implements EditorCommand {
|
||||
public operation: 'draw' | 'erase' | 'clear' = 'draw'
|
||||
public type: 'event_tile' = 'event_tile'
|
||||
public affectedTiles: MapEventTile[] = []
|
||||
|
||||
apply(elements: MapEventTile[]) {
|
||||
let tileVersion = cloneArray(elements) as MapEventTile[]
|
||||
if (this.operation === 'draw') {
|
||||
tileVersion = tileVersion.concat(this.affectedTiles)
|
||||
} else if (this.operation === 'erase') {
|
||||
tileVersion = tileVersion.filter((v) => !this.affectedTiles.includes(v))
|
||||
} else if (this.operation === 'clear') {
|
||||
tileVersion = []
|
||||
}
|
||||
return tileVersion
|
||||
}
|
||||
|
||||
constructor(operation: 'draw' | 'erase' | 'clear') {
|
||||
this.operation = operation
|
||||
}
|
||||
}
|
||||
|
||||
function createCommandUpdate(tile?: MapEventTile | null, operation: 'draw' | 'erase' | 'clear' = 'draw') {
|
||||
if (!tile) return
|
||||
|
||||
if (!currentCommand) {
|
||||
currentCommand = new EventTileCommand(operation)
|
||||
}
|
||||
|
||||
//If position is already in, do not proceed
|
||||
for (const priorTile of currentCommand.affectedTiles) {
|
||||
if (priorTile.positionX === tile.positionX && priorTile.positionY == tile.positionY) return
|
||||
}
|
||||
|
||||
currentCommand.affectedTiles.push(tile)
|
||||
}
|
||||
|
||||
function finalizeCommand() {
|
||||
if (!currentCommand) return
|
||||
emit('createCommand', currentCommand)
|
||||
currentCommand = null
|
||||
}
|
||||
|
||||
// *** HANDLERS ***
|
||||
|
||||
function getImageProps(tile: MapEventTile) {
|
||||
return {
|
||||
x: tileToWorldX(props.tileMap, tile.positionX, tile.positionY),
|
||||
y: tileToWorldY(props.tileMap, tile.positionX, tile.positionY),
|
||||
texture: tile.type,
|
||||
depth: 999
|
||||
}
|
||||
}
|
||||
|
||||
function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
||||
// Check if there is a tile
|
||||
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
||||
if (!tile) return
|
||||
|
||||
// Check if event tile already exists on position
|
||||
const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
|
||||
if (existingEventTile) return
|
||||
|
||||
// If teleport, check if there is a selected map
|
||||
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMap) return
|
||||
|
||||
const newEventTile = {
|
||||
id: uuidv4() as UUID,
|
||||
type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
|
||||
positionX: tile.x,
|
||||
positionY: tile.y,
|
||||
teleport:
|
||||
mapEditor.drawMode.value === 'teleport'
|
||||
? {
|
||||
toMap: mapEditor.teleportSettings.value.toMap,
|
||||
toPositionX: mapEditor.teleportSettings.value.toPositionX,
|
||||
toPositionY: mapEditor.teleportSettings.value.toPositionY,
|
||||
toRotation: mapEditor.teleportSettings.value.toRotation
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
createCommandUpdate(newEventTile as MapEventTile, 'draw')
|
||||
|
||||
map.mapEventTiles.push(newEventTile as MapEventTile)
|
||||
}
|
||||
|
||||
function erase(pointer: Phaser.Input.Pointer, map: MapT) {
|
||||
// Check if there is a tile
|
||||
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
||||
if (!tile) return
|
||||
|
||||
// Check if event tile already exists on position
|
||||
const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
|
||||
if (!existingEventTile) return
|
||||
|
||||
if (mapEditor.drawMode.value !== existingEventTile.type.toLowerCase()) {
|
||||
if (mapEditor.drawMode.value === 'blocking tile' && existingEventTile.type === MapEventTileType.BLOCK)
|
||||
null //skip this case
|
||||
else return
|
||||
}
|
||||
|
||||
createCommandUpdate(existingEventTile, 'erase')
|
||||
|
||||
// Remove existing event tile
|
||||
map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
|
||||
}
|
||||
|
||||
function handlePointer(pointer: Phaser.Input.Pointer) {
|
||||
const map = mapEditor.currentMap.value
|
||||
if (!map) return
|
||||
|
||||
if (pointer.event.altKey) return
|
||||
|
||||
switch (mapEditor.tool.value) {
|
||||
case 'pencil':
|
||||
pencil(pointer, map)
|
||||
break
|
||||
case 'eraser':
|
||||
erase(pointer, map)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function clearTiles() {
|
||||
if (mapEditor.currentMap.value?.mapEventTiles.length === 0) return
|
||||
createCommandUpdate(null, 'clear')
|
||||
finalizeCommand()
|
||||
}
|
||||
</script>
|