Compare commits
1 Commits
feature/#3
...
feature/#2
Author | SHA1 | Date | |
---|---|---|---|
175d7c6199 |
@ -1,6 +1,5 @@
|
|||||||
VITE_NAME=Noxious
|
VITE_NAME=Noxious
|
||||||
VITE_DOMAIN=localhost
|
VITE_DEVELOPMENT=true
|
||||||
VITE_ENVIRONMENT=development
|
|
||||||
VITE_SERVER_ENDPOINT=http://localhost:4000
|
VITE_SERVER_ENDPOINT=http://localhost:4000
|
||||||
VITE_TILE_SIZE_WIDTH=64
|
VITE_TILE_SIZE_WIDTH=64
|
||||||
VITE_TILE_SIZE_HEIGHT=32
|
VITE_TILE_SIZE_HEIGHT=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": [
|
"recommendations": [
|
||||||
"Vue.volar",
|
"Vue.volar",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode"
|
"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;
|
||||||
|
}
|
||||||
|
}
|
3024
package-lock.json
generated
26
package.json
@ -11,35 +11,39 @@
|
|||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"type-check": "vue-tsc --build --force",
|
"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/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^10.5.0",
|
"@vueuse/core": "^10.5.0",
|
||||||
"@vueuse/integrations": "^10.5.0",
|
"@vueuse/integrations": "^10.5.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.7",
|
||||||
"dexie": "^4.0.11",
|
"dexie": "^4.0.8",
|
||||||
"phaser": "^3.88.2",
|
"phaser": "^3.86.0",
|
||||||
"phavuer": "^0.16.5",
|
"pinia": "^2.1.6",
|
||||||
"phaser3-rex-plugins": "^1.80.13",
|
"socket.io-client": "^4.8.0",
|
||||||
"pinia": "^2.3.1",
|
|
||||||
"sharp": "^0.33.5",
|
|
||||||
"socket.io-client": "^4.8.1",
|
|
||||||
"universal-cookie": "^6.1.3",
|
"universal-cookie": "^6.1.3",
|
||||||
"vite-plugin-image-optimizer": "^1.1.8",
|
"vue": "^3.5.12",
|
||||||
"vue": "^3.5.13",
|
"zod": "^3.22.2"
|
||||||
"zod": "^3.24.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
||||||
|
"@rushstack/eslint-patch": "^1.10.3",
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^20.14.11",
|
"@types/node": "^20.14.11",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@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/test-utils": "^2.4.6",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-vue": "^9.27.0",
|
||||||
"jsdom": "^24.1.1",
|
"jsdom": "^24.1.1",
|
||||||
"npm-run-all2": "^6.2.3",
|
"npm-run-all2": "^6.2.3",
|
||||||
|
"phaser3-rex-plugins": "^1.80.8",
|
||||||
|
"phavuer": "^0.16.1",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"sass": "^1.79.4",
|
"sass": "^1.79.4",
|
||||||
|
@ -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: 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 |
32
src/App.vue
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Debug />
|
<Debug />
|
||||||
<Notifications />
|
<Notifications />
|
||||||
|
<BackgroundImageLoader />
|
||||||
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||||
<component :is="currentScreen" />
|
<component :is="currentScreen" />
|
||||||
</template>
|
</template>
|
||||||
@ -12,44 +13,43 @@ import Game from '@/components/screens/Game.vue'
|
|||||||
import Loading from '@/components/screens/Loading.vue'
|
import Loading from '@/components/screens/Loading.vue'
|
||||||
import Login from '@/components/screens/Login.vue'
|
import Login from '@/components/screens/Login.vue'
|
||||||
import MapEditor from '@/components/screens/MapEditor.vue'
|
import MapEditor from '@/components/screens/MapEditor.vue'
|
||||||
|
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
|
||||||
import Debug from '@/components/utilities/Debug.vue'
|
import Debug from '@/components/utilities/Debug.vue'
|
||||||
import Notifications from '@/components/utilities/Notifications.vue'
|
import Notifications from '@/components/utilities/Notifications.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
const mapEditorStore = useMapEditorStore()
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
const { playSound } = useSoundComposable()
|
|
||||||
|
|
||||||
const currentScreen = computed(() => {
|
const currentScreen = computed(() => {
|
||||||
if (!gameStore.game.isLoaded) return Loading
|
if (!gameStore.game.isLoaded) return Loading
|
||||||
if (!socketManager.connection) return Login
|
if (!gameStore.connection) return Login
|
||||||
if (!socketManager.token) return Login
|
if (!gameStore.token) return Login
|
||||||
if (!gameStore.character) return Characters
|
if (!gameStore.character) return Characters
|
||||||
if (mapEditor.active.value) return MapEditor
|
if (mapEditorStore.active) return MapEditor
|
||||||
return Game
|
return Game
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch mapEditor.active and empty gameStore.game.loadedAssets
|
// Watch mapEditorStore.active and empty gameStore.game.loadedAssets
|
||||||
watch(
|
watch(
|
||||||
() => mapEditor.active.value,
|
() => mapEditorStore.active,
|
||||||
() => {
|
() => {
|
||||||
gameStore.game.loadedTextures = []
|
gameStore.game.loadedTextures = []
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// #209: Play sound when a button is pressed
|
// #209: Play sound when a button is pressed
|
||||||
|
/**
|
||||||
|
* @TODO: Not all button-like elements will actually be a button, so we need to find a better way to do this
|
||||||
|
*/
|
||||||
addEventListener('click', (event) => {
|
addEventListener('click', (event) => {
|
||||||
const classList = ['btn-cyan', 'btn-red', 'btn-indigo', 'btn-empty', 'btn-sound']
|
if (!(event.target instanceof HTMLButtonElement)) {
|
||||||
const target = event.target as HTMLElement
|
return
|
||||||
// console.log(target) // Uncomment to log the clicked element
|
|
||||||
if (classList.some((className) => target.classList.contains(className))) {
|
|
||||||
playSound('/assets/sounds/button-click.wav')
|
|
||||||
}
|
}
|
||||||
|
const audio = new Audio('/assets/music/click-btn.mp3')
|
||||||
|
audio.play()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for "G" key press and toggle the gm panel
|
// Watch for "G" key press and toggle the gm panel
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
name: import.meta.env.VITE_NAME,
|
name: import.meta.env.VITE_NAME,
|
||||||
domain: import.meta.env.VITE_DOMAIN,
|
development: import.meta.env.VITE_DEVELOPMENT === 'true',
|
||||||
environment: import.meta.env.VITE_ENVIRONMENT,
|
|
||||||
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
|
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
|
||||||
tile_size: {
|
tile_size: {
|
||||||
width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH),
|
width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH),
|
||||||
|
@ -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'
|
|
||||||
}
|
|
@ -19,6 +19,7 @@ export type TextureData = {
|
|||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
originX?: number
|
originX?: number
|
||||||
originY?: number
|
originY?: number
|
||||||
|
isAnimated?: boolean
|
||||||
frameRate?: number
|
frameRate?: number
|
||||||
frameWidth?: number
|
frameWidth?: number
|
||||||
frameHeight?: number
|
frameHeight?: number
|
||||||
@ -26,7 +27,7 @@ export type TextureData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Tile = {
|
export type Tile = {
|
||||||
id: string
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
tags: any | null
|
tags: any | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
@ -34,12 +35,12 @@ export type Tile = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MapObject = {
|
export type MapObject = {
|
||||||
id: string
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
tags: string[]
|
tags: any | null
|
||||||
pivotPoints: { x: number; y: number }[]
|
|
||||||
originX: number
|
originX: number
|
||||||
originY: number
|
originY: number
|
||||||
|
isAnimated: boolean
|
||||||
frameRate: number
|
frameRate: number
|
||||||
frameWidth: number
|
frameWidth: number
|
||||||
frameHeight: number
|
frameHeight: number
|
||||||
@ -48,7 +49,7 @@ export type MapObject = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Item = {
|
export type Item = {
|
||||||
id: string
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
description: string | null
|
description: string | null
|
||||||
itemType: ItemType
|
itemType: ItemType
|
||||||
@ -63,11 +64,11 @@ export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVE
|
|||||||
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
|
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
|
||||||
|
|
||||||
export type Map = {
|
export type Map = {
|
||||||
id: string
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
tiles: string[][]
|
tiles: any | null
|
||||||
pvp: boolean
|
pvp: boolean
|
||||||
mapEffects: MapEffect[]
|
mapEffects: MapEffect[]
|
||||||
mapEventTiles: MapEventTile[]
|
mapEventTiles: MapEventTile[]
|
||||||
@ -79,14 +80,17 @@ export type Map = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MapEffect = {
|
export type MapEffect = {
|
||||||
id: string
|
id: UUID
|
||||||
|
map: Map
|
||||||
effect: string
|
effect: string
|
||||||
strength: number
|
strength: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlacedMapObject = {
|
export type PlacedMapObject = {
|
||||||
id: string
|
id: UUID
|
||||||
mapObject: MapObject | string
|
map: Map
|
||||||
|
mapObject: MapObject
|
||||||
|
depth: number
|
||||||
isRotated: boolean
|
isRotated: boolean
|
||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
@ -100,8 +104,8 @@ export enum MapEventTileType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MapEventTile = {
|
export type MapEventTile = {
|
||||||
id: string
|
id: UUID
|
||||||
map: string
|
map: Map
|
||||||
type: MapEventTileType
|
type: MapEventTileType
|
||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
@ -109,7 +113,7 @@ export type MapEventTile = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MapEventTileTeleport = {
|
export type MapEventTileTeleport = {
|
||||||
id: string
|
id: UUID
|
||||||
mapEventTile: MapEventTile
|
mapEventTile: MapEventTile
|
||||||
toMap: Map
|
toMap: Map
|
||||||
toPositionX: number
|
toPositionX: number
|
||||||
@ -118,7 +122,7 @@ export type MapEventTileTeleport = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: string
|
id: UUID
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
characters: Character[]
|
characters: Character[]
|
||||||
@ -138,7 +142,7 @@ export enum CharacterRace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterType = {
|
export type CharacterType = {
|
||||||
id: string
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
gender: CharacterGender
|
gender: CharacterGender
|
||||||
race: CharacterRace
|
race: CharacterRace
|
||||||
@ -149,7 +153,7 @@ export type CharacterType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterHair = {
|
export type CharacterHair = {
|
||||||
id: string
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
sprite?: Sprite
|
sprite?: Sprite
|
||||||
gender: CharacterGender
|
gender: CharacterGender
|
||||||
@ -157,8 +161,8 @@ export type CharacterHair = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Character = {
|
export type Character = {
|
||||||
id: string
|
id: UUID
|
||||||
userid: string
|
userId: UUID
|
||||||
user: User
|
user: User
|
||||||
name: string
|
name: string
|
||||||
hitpoints: number
|
hitpoints: number
|
||||||
@ -181,18 +185,17 @@ export type Character = {
|
|||||||
export type MapCharacter = {
|
export type MapCharacter = {
|
||||||
character: Character
|
character: Character
|
||||||
isMoving: boolean
|
isMoving: boolean
|
||||||
isAttacking?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterItem = {
|
export type CharacterItem = {
|
||||||
id: string
|
id: UUID
|
||||||
character: Character
|
character: Character
|
||||||
item: Item
|
item: Item
|
||||||
quantity: number
|
quantity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterEquipment = {
|
export type CharacterEquipment = {
|
||||||
id: string
|
id: UUID
|
||||||
slot: CharacterEquipmentSlotType
|
slot: CharacterEquipmentSlotType
|
||||||
characterItem: CharacterItem
|
characterItem: CharacterItem
|
||||||
}
|
}
|
||||||
@ -207,7 +210,7 @@ export enum CharacterEquipmentSlotType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Sprite = {
|
export type Sprite = {
|
||||||
id: string
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
@ -215,28 +218,22 @@ export type Sprite = {
|
|||||||
characterTypes: CharacterType[]
|
characterTypes: CharacterType[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpriteImage {
|
|
||||||
url: string
|
|
||||||
offset: {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SpriteAction = {
|
export type SpriteAction = {
|
||||||
id: string
|
id: UUID
|
||||||
sprite: string
|
sprite: Sprite
|
||||||
action: string
|
action: string
|
||||||
sprites: SpriteImage[]
|
sprites: string[]
|
||||||
originX: number
|
originX: number
|
||||||
originY: number
|
originY: number
|
||||||
|
isAnimated: boolean
|
||||||
|
isLooping: boolean
|
||||||
frameWidth: number
|
frameWidth: number
|
||||||
frameHeight: number
|
frameHeight: number
|
||||||
frameRate: number
|
frameRate: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Chat = {
|
export type Chat = {
|
||||||
id: string
|
id: UUID
|
||||||
character: Character
|
character: Character
|
||||||
map: Map
|
map: Map
|
||||||
message: string
|
message: string
|
||||||
@ -245,15 +242,19 @@ export type Chat = {
|
|||||||
|
|
||||||
export type WorldSettings = {
|
export type WorldSettings = {
|
||||||
date: Date
|
date: Date
|
||||||
weatherState: WeatherState
|
isRainEnabled: boolean
|
||||||
|
isFogEnabled: boolean
|
||||||
|
fogDensity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WeatherState = {
|
export type WeatherState = {
|
||||||
|
isRainEnabled: boolean
|
||||||
rainPercentage: number
|
rainPercentage: number
|
||||||
|
isFogEnabled: boolean
|
||||||
fogDensity: number
|
fogDensity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type mapLoadData = {
|
export type mapLoadData = {
|
||||||
mapId: string
|
mapId: UUID
|
||||||
characters: MapCharacter[]
|
characters: MapCharacter[]
|
||||||
}
|
}
|
||||||
|
@ -1,45 +1,25 @@
|
|||||||
import config from '@/application/config'
|
|
||||||
import type { HttpResponse } from '@/application/types'
|
|
||||||
import type { BaseStorage } from '@/storage/baseStorage'
|
|
||||||
|
|
||||||
export function uuidv4() {
|
export function uuidv4() {
|
||||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
|
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unduplicateArray(array: any[]) {
|
export function unduplicateArray(array: any[]) {
|
||||||
const arrayToProcess = typeof array.flat === 'function' ? array.flat() : array
|
return [...new Set(array.flat())]
|
||||||
return [...new Set(arrayToProcess)]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadCache<T extends { id: string; updatedAt: Date }>(endpoint: string, storage: BaseStorage<T>) {
|
export function getDomain() {
|
||||||
const request = await fetch(`${config.server_endpoint}/cache/${endpoint}`)
|
// Check if not localhost
|
||||||
const response = (await request.json()) as HttpResponse<T[]>
|
if (window.location.hostname !== 'localhost') {
|
||||||
|
return window.location.hostname
|
||||||
if (!response.success) {
|
|
||||||
console.error(`Failed to download ${endpoint}:`, response.message)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = response.data ?? []
|
// Check if not IP address
|
||||||
const serverItemIds = new Set(items.map((item) => item.id))
|
if (window.location.hostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
|
||||||
|
return window.location.hostname
|
||||||
// Remove items that don't exist on server
|
|
||||||
const existingItems = await storage.getAll()
|
|
||||||
for (const existingItem of existingItems) {
|
|
||||||
if (!serverItemIds.has(existingItem.id)) {
|
|
||||||
await storage.delete(existingItem.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update or add new items
|
if (window.location.hostname.split('.').length < 3) {
|
||||||
for (const item of items) {
|
return window.location.hostname
|
||||||
let overwrite = false
|
|
||||||
const existingItem = await storage.getById(item.id)
|
|
||||||
|
|
||||||
if (!existingItem || item.updatedAt > existingItem.updatedAt) {
|
|
||||||
overwrite = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await storage.add(item, overwrite)
|
return window.location.hostname.split('.').slice(-2).join('.')
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,12 @@ body {
|
|||||||
@apply outline-offset-2;
|
@apply outline-offset-2;
|
||||||
@apply rounded-sm;
|
@apply rounded-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media only screen and (orientation:portrait) and (max-width: 768px) {
|
||||||
|
.portrait-mode-notice {
|
||||||
|
@apply block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
@ -73,7 +79,7 @@ input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-field {
|
.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 {
|
&:focus-visible {
|
||||||
@apply outline-none border-cyan rounded bg-gray-900;
|
@apply outline-none border-cyan rounded bg-gray-900;
|
||||||
}
|
}
|
||||||
@ -88,12 +94,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 {
|
.form-field-full {
|
||||||
@apply w-full flex flex-col mb-5;
|
@apply w-full flex flex-col mb-5;
|
||||||
label {
|
label {
|
||||||
@ -128,15 +128,6 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-indigo {
|
|
||||||
@apply bg-indigo-500 text-gray-50 text-base leading-5 rounded py-2.5;
|
|
||||||
|
|
||||||
&.active,
|
|
||||||
&:hover {
|
|
||||||
@apply bg-indigo-600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.btn-empty {
|
&.btn-empty {
|
||||||
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
|
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
|
||||||
|
|
||||||
@ -164,10 +155,6 @@ button {
|
|||||||
@apply bg-gray bg-none;
|
@apply bg-gray bg-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-open {
|
|
||||||
@apply w-[calc(75%_-_40px)] max-xl:w-[calc(100%_-_360px)];
|
|
||||||
}
|
|
||||||
|
|
||||||
.hair-deselect:has(:checked) {
|
.hair-deselect:has(:checked) {
|
||||||
img {
|
img {
|
||||||
@apply brightness-200;
|
@apply brightness-200;
|
||||||
|
@ -1,102 +1,177 @@
|
|||||||
<template>
|
<template>
|
||||||
<Container ref="characterContainer" :x="currentPositionX" :y="currentPositionY" :depth="isometricDepth">
|
<ChatBubble :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
|
||||||
<ChatBubble :mapCharacter="props.mapCharacter" />
|
<Healthbar :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
|
||||||
<HealthBar :mapCharacter="props.mapCharacter" />
|
<Container ref="charContainer" :depth="isometricDepth" :x="currentPositionX" :y="currentPositionY">
|
||||||
<CharacterHair :mapCharacter="props.mapCharacter" />
|
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" />
|
||||||
<Sprite ref="characterSprite" :origin-y="1" :flipX="isFlippedX" />
|
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import config from '@/application/config'
|
||||||
import { type MapCharacter } from '@/application/types'
|
import { type MapCharacter } from '@/application/types'
|
||||||
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
|
|
||||||
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
|
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
|
||||||
import HealthBar from '@/components/game/character/partials/HealthBar.vue'
|
import Healthbar from '@/components/game/character/partials/Healthbar.vue'
|
||||||
import { useCharacterSpriteComposable } from '@/composables/useCharacterSpriteComposable'
|
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
|
||||||
|
import { CharacterTypeStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { Container, Sprite, useScene } from 'phavuer'
|
import { Container, refObj, Sprite, useScene } from 'phavuer'
|
||||||
import { onMounted, onUnmounted, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
enum Direction {
|
||||||
|
POSITIVE,
|
||||||
|
NEGATIVE,
|
||||||
|
UNCHANGED
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tileMap: Phaser.Tilemaps.Tilemap
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
mapCharacter: MapCharacter
|
mapCharacter: MapCharacter
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const charContainer = refObj<Phaser.GameObjects.Container>()
|
||||||
|
const charSprite = refObj<Phaser.GameObjects.Sprite>()
|
||||||
|
const charSpriteId = ref('')
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const mapStore = useMapStore()
|
const mapStore = useMapStore()
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
|
|
||||||
const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, playAnimation, updateSprite, initializeSprite, cleanup } = useCharacterSpriteComposable(scene, props.tileMap, props.mapCharacter)
|
const currentPositionX = ref(0)
|
||||||
const { playSound, stopSound } = useSoundComposable()
|
const currentPositionY = ref(0)
|
||||||
|
const isometricDepth = ref(1)
|
||||||
|
const isInitialPosition = ref(true)
|
||||||
|
const tween = ref<Phaser.Tweens.Tween | null>(null)
|
||||||
|
|
||||||
const handlePositionUpdate = (newValues: any, oldValues: any) => {
|
const updateIsometricDepth = (positionX: number, positionY: number) => {
|
||||||
if (!newValues) return
|
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 28, 94, true)
|
||||||
|
}
|
||||||
|
|
||||||
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
|
const updatePosition = (positionX: number, positionY: number, direction: Direction) => {
|
||||||
updatePosition(newValues.positionX, newValues.positionY)
|
const newPositionX = tileToWorldX(props.tilemap, positionX, positionY)
|
||||||
|
const newPositionY = tileToWorldY(props.tilemap, positionX, positionY)
|
||||||
|
|
||||||
|
if (isInitialPosition.value) {
|
||||||
|
currentPositionX.value = newPositionX
|
||||||
|
currentPositionY.value = newPositionY
|
||||||
|
isInitialPosition.value = false
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
|
if (tween.value?.isPlaying()) {
|
||||||
updateSprite()
|
tween.value.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = Math.sqrt(Math.pow(newPositionX - currentPositionX.value, 2) + Math.pow(newPositionY - currentPositionY.value, 2))
|
||||||
|
|
||||||
|
if (distance >= config.tile_size.width / 1.1) {
|
||||||
|
currentPositionX.value = newPositionX
|
||||||
|
currentPositionY.value = newPositionY
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = distance * 5.7
|
||||||
|
|
||||||
|
tween.value = props.tilemap.scene.tweens.add({
|
||||||
|
targets: { x: currentPositionX.value, y: currentPositionY.value },
|
||||||
|
x: newPositionX,
|
||||||
|
y: newPositionY,
|
||||||
|
duration,
|
||||||
|
ease: 'Linear',
|
||||||
|
onStart: () => {
|
||||||
|
if (direction === Direction.POSITIVE) {
|
||||||
|
updateIsometricDepth(positionX, positionY)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUpdate: (tween) => {
|
||||||
|
// @ts-ignore
|
||||||
|
currentPositionX.value = tween.targets[0].x
|
||||||
|
// @ts-ignore
|
||||||
|
currentPositionY.value = tween.targets[0].y
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
if (direction === Direction.NEGATIVE) {
|
||||||
|
updateIsometricDepth(positionX, positionY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const calcDirection = (oldPositionX: number, oldPositionY: number, newPositionX: number, newPositionY: number): Direction => {
|
||||||
|
if (newPositionY < oldPositionY || newPositionX < oldPositionX) return Direction.NEGATIVE
|
||||||
|
if (newPositionX > oldPositionX || newPositionY > oldPositionY) return Direction.POSITIVE
|
||||||
|
return Direction.UNCHANGED
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
|
||||||
|
|
||||||
|
const charTexture = computed(() => {
|
||||||
|
const spriteId = charSpriteId.value ?? 'idle_right_down'
|
||||||
|
const action = props.mapCharacter.isMoving ? 'walk' : 'idle'
|
||||||
|
const direction = [0, 6].includes(props.mapCharacter.character.rotation) ? 'left_up' : 'right_down'
|
||||||
|
|
||||||
|
return `${spriteId}-${action}_${direction}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateSprite = () => {
|
||||||
|
if (props.mapCharacter.isMoving) {
|
||||||
|
charSprite.value!.anims.play(charTexture.value, true)
|
||||||
|
} else {
|
||||||
|
charSprite.value!.anims.stop()
|
||||||
|
charSprite.value!.setFrame(0)
|
||||||
|
charSprite.value!.setTexture(charTexture.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles position updates and movement delay
|
|
||||||
*/
|
|
||||||
watch(
|
watch(
|
||||||
() => ({
|
() => ({
|
||||||
positionX: props.mapCharacter.character.positionX,
|
positionX: props.mapCharacter.character.positionX,
|
||||||
positionY: props.mapCharacter.character.positionY,
|
positionY: props.mapCharacter.character.positionY,
|
||||||
isMoving: props.mapCharacter.isMoving,
|
isMoving: props.mapCharacter.isMoving,
|
||||||
rotation: props.mapCharacter.character.rotation,
|
rotation: props.mapCharacter.character.rotation
|
||||||
isAttacking: props.mapCharacter.isAttacking
|
|
||||||
}),
|
}),
|
||||||
(oldValues, newValues) => {
|
(newValues, oldValues) => {
|
||||||
handlePositionUpdate(oldValues, newValues)
|
if (!newValues) return
|
||||||
|
|
||||||
|
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
|
||||||
|
const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.positionX, oldValues.positionY, newValues.positionX, newValues.positionY)
|
||||||
|
updatePosition(newValues.positionX, newValues.positionY, direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle animation updates
|
||||||
|
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
|
||||||
|
updateSprite()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await initializeSprite()
|
const characterTypeStorage = new CharacterTypeStorage()
|
||||||
|
|
||||||
|
const spriteId = await characterTypeStorage.getSpriteId(props.mapCharacter.character.characterType!)
|
||||||
|
if (!spriteId) return
|
||||||
|
|
||||||
|
charSpriteId.value = spriteId
|
||||||
|
|
||||||
|
await loadSpriteTextures(scene, spriteId)
|
||||||
|
|
||||||
|
charSprite.value!.setTexture(charTexture.value)
|
||||||
|
charSprite.value!.setFlipX(isFlippedX.value)
|
||||||
|
|
||||||
|
charContainer.value!.setName(props.mapCharacter.character!.name)
|
||||||
|
|
||||||
if (props.mapCharacter.character.id === gameStore.character!.id) {
|
if (props.mapCharacter.character.id === gameStore.character!.id) {
|
||||||
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
|
mapStore.setCharacterLoaded(true)
|
||||||
|
|
||||||
|
// #146 : Set camera position to character, need to be improved still
|
||||||
|
scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatePosition(props.mapCharacter.character.positionX, props.mapCharacter.character.positionY, props.mapCharacter.character.rotation)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
cleanup()
|
tween.value?.stop()
|
||||||
})
|
})
|
||||||
</script>
|
</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 { MapCharacter, Sprite as SpriteT } 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
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const texture = computed(() => {
|
||||||
|
const { rotation, characterHair } = props.mapCharacter.character
|
||||||
|
const spriteId = characterHair?.sprite?.id
|
||||||
|
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
||||||
|
|
||||||
|
return `${spriteId}-${direction}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
|
||||||
|
|
||||||
|
const imageProps = computed(() => {
|
||||||
|
// Get the current sprite action based on direction
|
||||||
|
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
||||||
|
const spriteAction = props.mapCharacter.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.mapCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadSpriteTextures(scene, props.mapCharacter.character.characterHair?.sprite as SpriteT)
|
||||||
|
.then(() => {})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,54 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<Image v-bind="imageProps" v-if="gameStore.isTextureLoaded(texture)" />
|
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
|
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
|
||||||
import { loadSpriteTextures } from '@/services/textureService'
|
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||||
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { Image, useScene } from 'phavuer'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mapCharacter: MapCharacter
|
mapCharacter: MapCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const hairSpriteId = ref('')
|
|
||||||
const sprite = ref<SpriteT | null>(null)
|
|
||||||
|
|
||||||
const texture = computed(() => {
|
const texture = computed(() => {
|
||||||
const { rotation } = props.mapCharacter.character
|
const { rotation, characterHair } = props.mapCharacter.character
|
||||||
|
const spriteId = characterHair?.sprite?.id
|
||||||
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
||||||
|
|
||||||
return `${hairSpriteId.value}-${direction}`
|
return `${spriteId}-${direction}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
|
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
|
||||||
|
|
||||||
const imageProps = computed(() => {
|
const imageProps = computed(() => {
|
||||||
|
// Get the current sprite action based on direction
|
||||||
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
||||||
const spriteAction = sprite.value?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
const spriteAction = props.mapCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
depth: 9999,
|
depth: 1,
|
||||||
originX: Number(spriteAction?.originX) ?? 0,
|
originX: Number(spriteAction?.originX) ?? 0,
|
||||||
originY: Number(spriteAction?.originY) ?? 0,
|
originY: Number(spriteAction?.originY) ?? 0,
|
||||||
flipX: isFlippedX.value,
|
flipX: isFlippedX.value,
|
||||||
texture: texture.value
|
texture: texture.value,
|
||||||
|
y: props.mapCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
loadSpriteTextures(scene, props.mapCharacter.character.characterHair?.sprite as SpriteT)
|
||||||
const characterHairStorage = new CharacterHairStorage()
|
.then(() => {})
|
||||||
const spriteId = await characterHairStorage.getSpriteId(props.mapCharacter.character.characterHair!)
|
.catch((error) => {
|
||||||
if (!spriteId) return
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
hairSpriteId.value = spriteId
|
|
||||||
const spriteStorage = new SpriteStorage()
|
|
||||||
sprite.value = await spriteStorage.getById(spriteId)
|
|
||||||
await loadSpriteTextures(scene, spriteId)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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" />
|
<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' }" />
|
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
|
||||||
</Container>
|
</Container>
|
||||||
@ -12,10 +12,12 @@ import { onMounted } from 'vue'
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mapCharacter: MapCharacter
|
mapCharacter: MapCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const game = useGame()
|
const game = useGame()
|
||||||
const characterChatContainer = refObj<Phaser.GameObjects.Container>()
|
const charChatContainer = refObj<Phaser.GameObjects.Container>()
|
||||||
|
|
||||||
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
||||||
container.setName(`${props.mapCharacter.character.name}_chatBubble`)
|
container.setName(`${props.mapCharacter.character.name}_chatBubble`)
|
||||||
@ -39,7 +41,7 @@ const createChatText = (text: Phaser.GameObjects.Text) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
characterChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
|
charChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
|
||||||
characterChatContainer.value!.setVisible(false)
|
charChatContainer.value!.setVisible(false)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Container :depth="999">
|
<Container :depth="999" :x="currentX" :y="currentY">
|
||||||
<Text @create="createNicknameText" :text="props.mapCharacter.character.name" />
|
<Text @create="createNicknameText" :text="props.mapCharacter.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="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" />
|
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
||||||
@ -12,6 +12,8 @@ import { Container, RoundRectangle, Text, useGame } from 'phavuer'
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mapCharacter: MapCharacter
|
mapCharacter: MapCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const game = useGame()
|
const game = useGame()
|
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
|
<div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" alt="" />
|
<img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" />
|
||||||
<img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" alt="" />
|
<img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" />
|
||||||
<div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative">
|
<div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative">
|
||||||
<span class="text-xs text-white font-thin">Character Profile [Alt+C]</span>
|
<span class="text-xs text-white font-thin">Character Profile [Alt+C]</span>
|
||||||
<button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
<button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
||||||
<img draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" alt="Close button icon" />
|
<img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-4 px-6 flex flex-col gap-7 relative z-10">
|
<div class="py-4 px-6 flex flex-col gap-7 relative z-10">
|
||||||
@ -17,7 +17,7 @@
|
|||||||
<span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
|
<span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
|
||||||
</div>
|
</div>
|
||||||
<a class="hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]">
|
<a class="hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]">
|
||||||
<img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" alt="Plus button icon" />
|
<img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
@ -37,20 +37,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" alt="Player character sprite" />
|
<img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" />
|
||||||
<div class="flex flex-col items-end gap-0.5">
|
<div class="flex flex-col items-end gap-0.5">
|
||||||
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
<img class="w-6 h-6 center-element" src="/assets/icons/profile/helmet.svg" alt="Helmet icon" />
|
<img class="w-6 h-6 center-element" src="/assets/icons/profile/helmet.svg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
<img class="w-6 h-6 center-element" src="/assets/icons/profile/chestplate.svg" alt="Chestplate icon" />
|
<img class="w-6 h-6 center-element" src="/assets/icons/profile/chestplate.svg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-0.5 items-end">
|
<div class="flex gap-0.5 items-end">
|
||||||
<div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
<div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
<img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" alt="Boots icon" />
|
<img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
<img class="w-6 h-6 center-element" src="/assets/icons/profile/legs.svg" alt="Legs icon" />
|
<img class="w-6 h-6 center-element" src="/assets/icons/profile/legs.svg" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -119,44 +119,111 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
const width = ref(286)
|
let startX = 0
|
||||||
const height = ref(483)
|
let startY = 0
|
||||||
const x = ref(window.innerWidth / 2 - 143)
|
let initialX = 0
|
||||||
const y = ref(window.innerHeight / 2 - 241)
|
let initialY = 0
|
||||||
|
let modalPositionX = 0
|
||||||
|
let modalPositionY = 0
|
||||||
|
let modalWidth = 286
|
||||||
|
let modalHeight = 483
|
||||||
|
|
||||||
|
const width = ref(modalWidth)
|
||||||
|
const height = ref(modalHeight)
|
||||||
|
const x = ref(0)
|
||||||
|
const y = ref(0)
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
|
|
||||||
const modalStyle = computed(() => ({
|
const modalStyle = computed(() => ({
|
||||||
top: `${y.value}px`,
|
top: `${y.value}px`,
|
||||||
left: `${x.value}px`,
|
left: `${x.value}px`,
|
||||||
width: `${width.value}px`,
|
width: `${width.value}px`,
|
||||||
height: `${height.value}px`
|
height: `${height.value}px`,
|
||||||
|
maxWidth: '100vw',
|
||||||
|
maxHeight: '100vh'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function startDrag(event: MouseEvent) {
|
function startDrag(event: MouseEvent) {
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
const startX = event.clientX - x.value
|
startX = event.clientX
|
||||||
const startY = event.clientY - y.value
|
startY = event.clientY
|
||||||
|
initialX = x.value
|
||||||
function drag(event: MouseEvent) {
|
initialY = y.value
|
||||||
if (!isDragging.value) return
|
event.preventDefault()
|
||||||
x.value = event.clientX - startX
|
|
||||||
y.value = event.clientY - startY
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopDrag() {
|
|
||||||
isDragging.value = false
|
|
||||||
removeEventListener('mousemove', drag)
|
|
||||||
removeEventListener('mouseup', stopDrag)
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListener('mousemove', drag)
|
|
||||||
addEventListener('mouseup', stopDrag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drag(event: MouseEvent) {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
const dx = event.clientX - startX
|
||||||
|
const dy = event.clientY - startY
|
||||||
|
x.value = initialX + dx
|
||||||
|
y.value = initialY + dy
|
||||||
|
adjustPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDrag() {
|
||||||
|
isDragging.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustPosition() {
|
||||||
|
x.value = Math.min(x.value, window.innerWidth - width.value)
|
||||||
|
y.value = Math.min(y.value, window.innerHeight - height.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializePosition() {
|
||||||
|
width.value = Math.min(modalWidth, window.innerWidth)
|
||||||
|
height.value = Math.min(modalHeight, window.innerHeight)
|
||||||
|
if (modalPositionX !== 0 && modalPositionY !== 0) {
|
||||||
|
x.value = modalPositionX
|
||||||
|
y.value = modalPositionY
|
||||||
|
} else {
|
||||||
|
x.value = (window.innerWidth - width.value) / 2
|
||||||
|
y.value = (window.innerHeight - height.value) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => gameStore.uiSettings.isCharacterProfileOpen,
|
||||||
|
(value) => {
|
||||||
|
gameStore.uiSettings.isCharacterProfileOpen = value
|
||||||
|
if (value) {
|
||||||
|
initializePosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => modalWidth,
|
||||||
|
(value) => {
|
||||||
|
width.value = Math.min(value, window.innerWidth)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => modalHeight,
|
||||||
|
(value) => {
|
||||||
|
height.value = Math.min(value, window.innerHeight)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => modalPositionX,
|
||||||
|
(value) => {
|
||||||
|
x.value = value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => modalPositionY,
|
||||||
|
(value) => {
|
||||||
|
y.value = value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
function keyPress(event: KeyboardEvent) {
|
function keyPress(event: KeyboardEvent) {
|
||||||
if (event.altKey && event.key === 'c') {
|
if (event.altKey && event.key === 'c') {
|
||||||
gameStore.toggleCharacterProfile()
|
gameStore.toggleCharacterProfile()
|
||||||
@ -165,9 +232,14 @@ function keyPress(event: KeyboardEvent) {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
addEventListener('keydown', keyPress)
|
addEventListener('keydown', keyPress)
|
||||||
|
addEventListener('mousemove', drag)
|
||||||
|
addEventListener('mouseup', stopDrag)
|
||||||
|
initializePosition()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
removeEventListener('keydown', keyPress)
|
removeEventListener('keydown', keyPress)
|
||||||
|
removeEventListener('mousemove', drag)
|
||||||
|
removeEventListener('mouseup', stopDrag)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col absolute left-1/2 -translate-x-1/2 bottom-5">
|
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col absolute left-1/2 -translate-x-1/2 bottom-5">
|
||||||
<div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray rounded-md border-2 border-solid border-gray-500 text-gray-300" v-show="gameStore.uiSettings.isChatOpen">
|
<div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray rounded-md border-2 border-solid border-gray-500 text-gray-300" v-show="gameStore.uiSettings.isChatOpen">
|
||||||
<div v-for="message in chats" class="flex-col py-2 items-center p-3">
|
<div v-for="message in chats" class="flex-col py-2 items-center p-3">
|
||||||
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{ 'text-cyan-300': gameStore.character?.role == 'gm' }">{{ message.character }}</span>
|
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{ 'text-cyan-300': gameStore.character?.role == 'gm' }">{{ message.character.name }}</span>
|
||||||
<p class="text-gray-50 m-0">{{ message.message }}</p>
|
<p class="text-gray-50 m-0">{{ message.message }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-96 mx-auto relative">
|
<div class="w-96 mx-auto relative">
|
||||||
<img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" alt="" />
|
<img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" />
|
||||||
<input
|
<input
|
||||||
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
|
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
|
||||||
placeholder="Type something..."
|
placeholder="Type something..."
|
||||||
@ -21,8 +21,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import type { Chat } from '@/application/types'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { onClickOutside, useFocus } from '@vueuse/core'
|
import { onClickOutside, useFocus } from '@vueuse/core'
|
||||||
@ -31,9 +30,10 @@ import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
const mapStore = useMapStore()
|
||||||
|
|
||||||
const message = ref('')
|
const message = ref('')
|
||||||
const chats = ref<{ character: string; message: string }[]>([])
|
const chats = ref([] as Chat[])
|
||||||
const chatWindow = ref<HTMLElement | null>(null)
|
const chatWindow = ref<HTMLElement | null>(null)
|
||||||
const chatInput = ref<HTMLElement | null>(null)
|
const chatInput = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ function unfocusChat(event: Event, targetElement: HTMLElement) {
|
|||||||
|
|
||||||
const sendMessage = () => {
|
const sendMessage = () => {
|
||||||
if (!message.value.trim()) return
|
if (!message.value.trim()) return
|
||||||
socketManager.emit(SocketEvent.CHAT_MESSAGE, { message: message.value }, (response: boolean) => {})
|
gameStore.connection?.emit('chat:message', { message: message.value }, (response: boolean) => {})
|
||||||
message.value = ''
|
message.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,30 +79,18 @@ const scrollToBottom = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.on(SocketEvent.CHAT_MESSAGE, (data: { character: string; message: string }) => {
|
gameStore.connection?.on('chat:message', (data: Chat) => {
|
||||||
if (!data.character || !data.message) return
|
chats.value.push(data)
|
||||||
|
|
||||||
chats.value.push({ character: data.character, message: data.message })
|
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
||||||
const characterContainer = scene.children.getByName(data.character) as Phaser.GameObjects.Container
|
if (!mapStore.characterLoaded) return
|
||||||
if (!characterContainer) {
|
|
||||||
console.log('No character container found')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const characterChatContainer = characterContainer.getByName(data.character + '_chatContainer') as Phaser.GameObjects.Container
|
const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
|
||||||
if (!characterChatContainer) {
|
if (!charChatContainer) return
|
||||||
console.log('No character chat container found')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatBubble = characterChatContainer.getByName(data.character + '_chatBubble') as Phaser.GameObjects.Container
|
const chatBubble = charChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container
|
||||||
const chatText = characterChatContainer.getByName(data.character + '_chatText') as Phaser.GameObjects.Text
|
const chatText = charChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text
|
||||||
if (!chatText || !chatBubble) {
|
if (!chatText || !chatBubble) return
|
||||||
console.log('No chat text or bubble found')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateTextWidth(text: string, font: string, fontSize: number): number {
|
function calculateTextWidth(text: string, font: string, fontSize: number): number {
|
||||||
// Create a canvas element
|
// Create a canvas element
|
||||||
@ -127,24 +115,24 @@ socketManager.on(SocketEvent.CHAT_MESSAGE, (data: { character: string; message:
|
|||||||
// setText but with max. char limit of 90
|
// setText but with max. char limit of 90
|
||||||
chatText.setText(data.message.substring(0, 90))
|
chatText.setText(data.message.substring(0, 90))
|
||||||
|
|
||||||
characterChatContainer.setVisible(true)
|
charChatContainer.setVisible(true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide chat bubble after a few seconds
|
* Hide chat bubble after a few seconds
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Clear any existing hide timer
|
// Clear any existing hide timer
|
||||||
if (characterChatContainer.getData('hideTimer')) {
|
if (charChatContainer.getData('hideTimer')) {
|
||||||
clearTimeout(characterChatContainer.getData('hideTimer'))
|
clearTimeout(charChatContainer.getData('hideTimer'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a new hide timer
|
// Set a new hide timer
|
||||||
const hideTimer = setTimeout(() => {
|
const hideTimer = setTimeout(() => {
|
||||||
characterChatContainer.setVisible(false)
|
charChatContainer.setVisible(false)
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
|
||||||
// Store the timer on the container itself
|
// Store the timer on the container itself
|
||||||
characterChatContainer.setData('hideTimer', hideTimer)
|
charChatContainer.setData('hideTimer', hideTimer)
|
||||||
})
|
})
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
||||||
@ -153,7 +141,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
socketManager.off(SocketEvent.CHAT_MESSAGE)
|
gameStore.connection?.off('chat:message')
|
||||||
removeEventListener('keydown', focusChat)
|
removeEventListener('keydown', focusChat)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute top-0 right-4 hidden lg:block" v-if="gameStore.world.date && typeof gameStore.world.date === 'object'">
|
<div class="absolute top-0 right-4 hidden lg:block">
|
||||||
<p class="text-white text-lg">
|
<p class="text-white text-lg">{{ gameStore.world.date.toLocaleString() }}</p>
|
||||||
{{ useDateFormat(gameStore.world.date, 'YYYY/MM/DD HH:mm') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useDateFormat } from '@vueuse/core'
|
|
||||||
import { onUnmounted } from 'vue'
|
import { onUnmounted } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
|
// Listen for new date from socket
|
||||||
|
gameStore.connection?.on('date', (data: Date) => {
|
||||||
|
gameStore.world.date = new Date(data)
|
||||||
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
socketManager.off(SocketEvent.DATE)
|
gameStore.connection?.off('date')
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
||||||
<img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/ingameUI/menu-icon.svg" alt="Menu button icon" />
|
<img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/ingameUI/menu-icon.svg" />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item group relative" @click="gameStore.toggleCharacterProfile">
|
<li class="menu-item group relative" @click="gameStore.toggleCharacterProfile">
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
||||||
<img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/placeholders/head.png" alt="User profile button icon" />
|
<img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/placeholders/head.png" />
|
||||||
<p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p>
|
<p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -25,7 +25,7 @@
|
|||||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
||||||
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/chat-icon.svg" alt="Open chat button icon" />
|
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/chat-icon.svg" />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item group relative">
|
<li class="menu-item group relative">
|
||||||
@ -34,7 +34,7 @@
|
|||||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
||||||
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/map-icon.svg" alt="World map button icon" />
|
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/map-icon.svg" />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item group relative">
|
<li class="menu-item group relative">
|
||||||
@ -43,7 +43,7 @@
|
|||||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
||||||
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/socials-icon.svg" alt="Users button icon" />
|
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/socials-icon.svg" />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -5,12 +5,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
|
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
|
||||||
<button class="w-6 h-6 relative p-0">
|
<button class="w-6 h-6 relative p-0">
|
||||||
<img class="w-3 h-3 center-element" src="/assets/icons/plus-icon.svg" alt="Zoom-in button icon" />
|
<img class="w-3 h-3 center-element" src="/assets/icons/plus-icon.svg" />
|
||||||
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" alt="" />
|
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" />
|
||||||
</button>
|
</button>
|
||||||
<button class="w-6 h-6 relative p-0">
|
<button class="w-6 h-6 relative p-0">
|
||||||
<img class="w-3 h-3 center-element" src="/assets/icons/minus-icon.svg" alt="Zoom-out button icon" />
|
<img class="w-3 h-3 center-element" src="/assets/icons/minus-icon.svg" />
|
||||||
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" alt="" />
|
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
42
src/components/game/gui/UserPanel.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="false">
|
||||||
|
<div class="center-element max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray border-solid border-2 border-gray-500 rounded-md z-50 flex flex-col backdrop-blur-sm shadow-lg">
|
||||||
|
<div class="p-2.5 flex max-sm:flex-wrap justify-between items-center gap-5 border-solid border-0 border-b border-gray-500">
|
||||||
|
<h3 class="m-0 font-medium shrink-0">Game menu</h3>
|
||||||
|
<div class="hidden sm:flex gap-1.5 flex-wrap">
|
||||||
|
<button @click.stop="userPanelScreen = 'inventory'" :class="{ active: userPanelScreen === 'inventory' }" class="btn-cyan py-1.5 px-4 min-w-24">Inventory</button>
|
||||||
|
<button @click.stop="userPanelScreen = 'equipment'" :class="{ active: userPanelScreen === 'equipment' }" class="btn-cyan py-1.5 px-4 min-w-24">Equipment</button>
|
||||||
|
<button @click.stop="userPanelScreen = 'characterScreen'" :class="{ active: userPanelScreen === 'characterScreen' }" class="btn-cyan py-1.5 px-4 min-w-24">Character</button>
|
||||||
|
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2.5">
|
||||||
|
<button class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
||||||
|
<img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex sm:hidden gap-1.5 flex-wrap">
|
||||||
|
<button @click.stop="userPanelScreen = 'inventory'" :class="{ active: userPanelScreen === 'inventory' }" class="btn-cyan py-1.5 px-4 min-w-24">Inventory</button>
|
||||||
|
<button @click.stop="userPanelScreen = 'equipment'" :class="{ active: userPanelScreen === 'equipment' }" class="btn-cyan py-1.5 px-4 min-w-24">Equipment</button>
|
||||||
|
<button @click.stop="userPanelScreen = 'characterScreen'" :class="{ active: userPanelScreen === 'characterScreen' }" class="btn-cyan py-1.5 px-4 min-w-24">Character</button>
|
||||||
|
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Inventory v-show="userPanelScreen === 'inventory'" />
|
||||||
|
<Equipment v-show="userPanelScreen === 'equipment'" />
|
||||||
|
<CharacterScreen v-show="userPanelScreen === 'characterScreen'" />
|
||||||
|
<Settings v-show="userPanelScreen === 'settings'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import CharacterScreen from '@/components/game/gui/partials/CharacterScreen.vue'
|
||||||
|
import Equipment from '@/components/game/gui/partials/Equipment.vue'
|
||||||
|
import Inventory from '@/components/game/gui/partials/Inventory.vue'
|
||||||
|
import Settings from '@/components/game/gui/partials/Settings.vue'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
let userPanelScreen = ref('inventory')
|
||||||
|
</script>
|
68
src/components/game/gui/partials/CharacterScreen.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div class="grow flex flex-col w-full h-full relative overflow-auto">
|
||||||
|
<div class="m-4 relative">
|
||||||
|
<h4 class="font-medium text-lg max-w-[375px]">Character</h4>
|
||||||
|
<div class="flex justify-center flex-wrap gap-20">
|
||||||
|
<div class="flex gap-3 mt-2 flex-wrap max-w-[375px]">
|
||||||
|
<div class="h-full flex flex-col justify-center items-center">
|
||||||
|
<img class="h-72 my-2 mx-auto" src="/assets/placeholders/inventory_player.png" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col gap-3 mx-5 mt-2">
|
||||||
|
<h3>{{ gameStore.character?.name }}</h3>
|
||||||
|
<div class="flex gap-4 flex-wrap max-w-[375px]">
|
||||||
|
<ul class="p-0 m-0 list-none">
|
||||||
|
<li class="leading-6 text-lg">Class:</li>
|
||||||
|
<li class="leading-6 text-lg">Race:</li>
|
||||||
|
<li class="leading-6 text-lg">Hit Points:</li>
|
||||||
|
<li class="leading-6 text-lg">Mana Points:</li>
|
||||||
|
<li class="leading-6 text-lg">Level:</li>
|
||||||
|
<li class="leading-6 text-lg">Stat Points:</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="p-0 m-0 list-none">
|
||||||
|
<li class="leading-6 text-lg min-h-6">Knight</li>
|
||||||
|
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.characterType?.race }}</li>
|
||||||
|
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.hitpoints }} <span class="text-green">(+15)</span></li>
|
||||||
|
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.mana }}</li>
|
||||||
|
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.level }}</li>
|
||||||
|
<li class="leading-6 text-lg min-h-6">3</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-3 mx-5 mt-2">
|
||||||
|
<h3>Alignment</h3>
|
||||||
|
<div class="h-8 w-64 rounded border border-solid border-white bg-gradient-to-r from-red to-blue relative">
|
||||||
|
<!-- TODO: Give slider left value based on alignment (0-100), new characters start with 50 -->
|
||||||
|
<div class="rounded border-2 border-solid border-white h-10 w-2 absolute top-1/2 -translate-y-1/2 -translate-x-1/2" :style="{ left: gameStore.character?.alignment + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<h4 class="font-medium text-lg max-w-[375px]">Character stats</h4>
|
||||||
|
<div class="flex gap-3 mt-4 flex-wrap max-w-[375px]">
|
||||||
|
<ul class="p-0 m-0 list-none">
|
||||||
|
<li class="leading-6">Health:</li>
|
||||||
|
<li class="leading-6">Defense:</li>
|
||||||
|
<li class="leading-6">More stats:</li>
|
||||||
|
<li class="leading-6">Extra stats:</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="p-0 m-0 list-none text-right">
|
||||||
|
<li class="leading-6">100 <span class="text-green">(+15)</span></li>
|
||||||
|
<li class="leading-6">1000 <span class="text-green">(+500)</span></li>
|
||||||
|
<li class="leading-6"></li>
|
||||||
|
<li class="leading-6"></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
</script>
|
89
src/components/game/gui/partials/Equipment.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<div class="grow flex flex-col w-full h-full relative overflow-auto">
|
||||||
|
<div class="m-4 relative">
|
||||||
|
<h4 class="font-medium text-lg max-w-[375px]">Equipment</h4>
|
||||||
|
<div class="flex justify-center items-center flex-wrap gap-20">
|
||||||
|
<div class="flex gap-3 mt-2 flex-wrap max-w-[375px]">
|
||||||
|
<div class="h-full flex flex-col justify-center items-center">
|
||||||
|
<h3>{{ gameStore.character?.name }}</h3>
|
||||||
|
<img class="h-60 my-2 mx-auto" src="/assets/placeholders/inventory_player.png" />
|
||||||
|
<span class="block text-sm">Level {{ gameStore.character?.level }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-3 mx-5 mt-2">
|
||||||
|
<div class="flex gap-3 justify-center">
|
||||||
|
<!-- Helmet -->
|
||||||
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
|
||||||
|
<img src="/assets/icons/inventory/helmet.svg" class="center-element w-11/12 opacity-20" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Head charm -->
|
||||||
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
|
||||||
|
<img src="/assets/icons/inventory/head_charm.svg" class="center-element w-11/12 opacity-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3 justify-center">
|
||||||
|
<!-- Bracers -->
|
||||||
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] relative hover:bg-gray-200">
|
||||||
|
<img src="/assets/icons/inventory/bracers.svg" class="center-element w-11/12 opacity-20" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chestplate -->
|
||||||
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square w-[104px] h-[104px] relative hover:bg-gray-200">
|
||||||
|
<img src="/assets/icons/inventory/chestplate.svg" class="center-element w-10/12 opacity-20" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Primary Weapon -->
|
||||||
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
|
||||||
|
<img src="/assets/icons/inventory/primary_weapon.svg" class="center-element w-11/12 opacity-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3 justify-center">
|
||||||
|
<!-- Legs -->
|
||||||
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
|
||||||
|
<img src="/assets/icons/inventory/legs.svg" class="center-element w-11/12 opacity-20" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<!-- Belt/pouch -->
|
||||||
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
|
||||||
|
<img src="/assets/icons/inventory/pouch.svg" class="center-element w-11/12 opacity-20" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Boots -->
|
||||||
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
|
||||||
|
<img src="/assets/icons/inventory/boots.svg" class="center-element w-11/12 opacity-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<h4 class="font-medium text-lg max-w-[375px]">Equipment Bonus</h4>
|
||||||
|
<div class="flex gap-3 mt-4 flex-wrap max-w-[375px]">
|
||||||
|
<ul class="p-0 m-0 list-none">
|
||||||
|
<li class="leading-6">Health:</li>
|
||||||
|
<li class="leading-6">Defense:</li>
|
||||||
|
<li class="leading-6">More stats:</li>
|
||||||
|
<li class="leading-6">Extra stats:</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="p-0 m-0 list-none text-right">
|
||||||
|
<li class="leading-6">+15</li>
|
||||||
|
<li class="leading-6">500</li>
|
||||||
|
<li class="leading-6"></li>
|
||||||
|
<li class="leading-6"></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
</script>
|
17
src/components/game/gui/partials/Inventory.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<div class="grow flex flex-col w-full h-full relative overflow-auto">
|
||||||
|
<div class="m-4 relative">
|
||||||
|
<h4 class="m-auto font-medium text-lg max-w-[375px]">Inventory</h4>
|
||||||
|
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
|
||||||
|
<div v-for="n in 24" class="bg-gray-300/80 border-solid border-2 border-gray-500 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<h4 class="m-auto font-medium text-lg max-w-[375px]">Chest items</h4>
|
||||||
|
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
|
||||||
|
<div v-for="n in 12" class="bg-gray-300/80 border-solid border-2 border-gray-500 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
40
src/components/game/gui/partials/Settings.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full w-full relative">
|
||||||
|
<div class="w-2/12 flex flex-col relative">
|
||||||
|
<!-- Settings Categories -->
|
||||||
|
<div class="relative p-2.5">
|
||||||
|
<h3>Settings</h3>
|
||||||
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
|
</div>
|
||||||
|
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'character' }" @click.stop="settingCategory = 'character'">
|
||||||
|
<span>Character</span>
|
||||||
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
|
</a>
|
||||||
|
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'account' }" @click.stop="settingCategory = 'account'">
|
||||||
|
<span>Account</span>
|
||||||
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
|
</a>
|
||||||
|
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'audio' }" @click.stop="settingCategory = 'audio'">
|
||||||
|
<span>Audio</span>
|
||||||
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
|
</a>
|
||||||
|
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'video' }" @click.stop="settingCategory = 'video'">
|
||||||
|
<span>Video</span>
|
||||||
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
|
||||||
|
|
||||||
|
<!-- Assets list -->
|
||||||
|
<div class="overflow-auto h-full w-10/12 flex flex-col relative">
|
||||||
|
<CharacterSettings v-show="settingCategory == 'character'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import CharacterSettings from '@/components/game/gui/partials/settings/CharacterSettings.vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
let settingCategory = ref('character')
|
||||||
|
</script>
|
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full overflow-auto">
|
||||||
|
<div class="relative p-2.5 flex flex-col gap-5 h-72">
|
||||||
|
<h3>Character details</h3>
|
||||||
|
<button @click="toggle" class="btn-cyan px-4 py-1.5 w-24">Edit</button>
|
||||||
|
<form class="flex gap-2.5 flex-wrap">
|
||||||
|
<div class="form-field-half max-w-[300px]">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input class="input-field" :class="{ inactive: !editCharacter }" type="text" name="name" placeholder="Ethereal" :disabled="!editCharacter" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half max-w-[300px] relative">
|
||||||
|
<label for="class">Class</label>
|
||||||
|
<select class="input-field" v-model="characterClass" :class="{ inactive: !editCharacter }" name="class" :disabled="!editCharacter">
|
||||||
|
<option value="Knight" :selected="characterClass == 'Knight'" :disabled="characterClass == 'Knight'">Knight</option>
|
||||||
|
<option value="Paladin" :selected="characterClass == 'Paladin'" :disabled="characterClass == 'Paladin'">Paladin</option>
|
||||||
|
</select>
|
||||||
|
<span v-if="!editCharacter" class="absolute bottom-[9px] left-[14px] text-sm text-gray-300/50">{{ characterClass }}</span>
|
||||||
|
</div>
|
||||||
|
<button v-if="editCharacter" @click="toggle" class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const editCharacter = ref(false)
|
||||||
|
const characterClass = ref('')
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
editCharacter.value = !editCharacter.value
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,49 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<Character v-for="item in mapStore.characters" :key="item.character.id" :tileMap :mapCharacter="item" />
|
<Character v-for="item in mapStore.characters" :key="item.character.id" :tilemap="tilemap" :mapCharacter="item" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 Character from '@/components/game/character/Character.vue'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { onUnmounted } from 'vue'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const mapStore = useMapStore()
|
const mapStore = useMapStore()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tileMap: Phaser.Tilemaps.Tilemap
|
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>
|
</script>
|
||||||
|
199
src/components/game/map/Effects.vue
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Map, WeatherState } from '@/application/types'
|
||||||
|
import { MapStorage } from '@/storage/storages'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
|
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 mapStore = useMapStore()
|
||||||
|
const mapStorage = new MapStorage()
|
||||||
|
const sceneRef = ref<Phaser.Scene | null>(null)
|
||||||
|
const mapEffectsReady = ref(false)
|
||||||
|
const mapObject = ref<Map | null>(null)
|
||||||
|
|
||||||
|
// 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 loadMap = async () => {
|
||||||
|
if (!mapStore.mapId) return
|
||||||
|
mapObject.value = await mapStorage.get(mapStore.mapId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for mapId changes and load map when it's available
|
||||||
|
watch(
|
||||||
|
() => mapStore.mapId,
|
||||||
|
async (newMapId) => {
|
||||||
|
if (newMapId) {
|
||||||
|
mapEffectsReady.value = false
|
||||||
|
await loadMap()
|
||||||
|
updateScene()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
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 mapEffects = mapObject.value?.mapEffects?.reduce(
|
||||||
|
(acc, curr) => ({
|
||||||
|
...acc,
|
||||||
|
[curr.effect]: curr.strength
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
) as { [key: string]: number }
|
||||||
|
|
||||||
|
// Only update effects once mapEffects are loaded
|
||||||
|
if (!mapEffectsReady.value) {
|
||||||
|
if (mapObject.value) {
|
||||||
|
mapEffectsReady.value = true
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalEffects =
|
||||||
|
mapEffects && Object.keys(mapEffects).length
|
||||||
|
? mapEffects
|
||||||
|
: {
|
||||||
|
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(
|
||||||
|
() => mapObject.value,
|
||||||
|
() => {
|
||||||
|
mapEffectsReady.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,71 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<MapTiles v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
<MapTiles :key="mapStore.mapId" @tileMap:create="tileMap = $event" />
|
||||||
<PlacedMapObjects v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
<PlacedMapObjects v-if="tileMap" :key="mapStore.mapId" :tilemap="tileMap" />
|
||||||
<Characters v-if="tileMap && mapStore.characters" :tileMap />
|
<Characters v-if="tileMap && mapStore.characters" :tilemap="tileMap" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import type { MapCharacter, mapLoadData, UUID } from '@/application/types'
|
||||||
import type { mapLoadData } from '@/application/types'
|
|
||||||
import { unduplicateArray } from '@/application/utilities'
|
|
||||||
import Characters from '@/components/game/map/Characters.vue'
|
import Characters from '@/components/game/map/Characters.vue'
|
||||||
import MapTiles from '@/components/game/map/MapTiles.vue'
|
import MapTiles from '@/components/game/map/MapTiles.vue'
|
||||||
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
|
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { createTileLayer, createTileMap, loadTileTexturesFromMapTileArray } from '@/services/mapService'
|
|
||||||
import { MapStorage } from '@/storage/storages'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { useScene } from 'phavuer'
|
import { onUnmounted, shallowRef } from 'vue'
|
||||||
import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
|
|
||||||
|
|
||||||
const scene = useScene()
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const mapStore = useMapStore()
|
const mapStore = useMapStore()
|
||||||
|
|
||||||
const mapStorage = new MapStorage()
|
|
||||||
|
|
||||||
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||||
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
socketManager.on(SocketEvent.MAP_CHARACTER_TELEPORT, (data: mapLoadData) => {
|
gameStore.connection?.on('map:character:teleport', async (data: mapLoadData) => {
|
||||||
mapStore.setMapId(data.mapId)
|
mapStore.setMapId(data.mapId)
|
||||||
mapStore.setCharacters(data.characters)
|
mapStore.setCharacters(data.characters)
|
||||||
})
|
})
|
||||||
|
|
||||||
async function initialize() {
|
gameStore.connection?.on('map:character:join', async (data: MapCharacter) => {
|
||||||
if (!mapStore.mapId) return
|
mapStore.addCharacter(data)
|
||||||
|
})
|
||||||
|
|
||||||
const map = await mapStorage.getById(mapStore.mapId)
|
gameStore.connection?.on('map:character:leave', (characterId: UUID) => {
|
||||||
if (!map) return
|
mapStore.removeCharacter(characterId)
|
||||||
|
})
|
||||||
|
|
||||||
await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
|
gameStore.connection?.on('map:character:move', (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
|
||||||
|
mapStore.updateCharacterPosition(data)
|
||||||
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(() => {
|
onUnmounted(() => {
|
||||||
if (tileMap.value) {
|
mapStore.reset()
|
||||||
tileMap.value.destroyLayer('tiles')
|
gameStore.connection?.off('map:character:teleport')
|
||||||
tileMap.value.removeAllLayers()
|
gameStore.connection?.off('map:character:join')
|
||||||
tileMap.value.destroy()
|
gameStore.connection?.off('map:character:leave')
|
||||||
}
|
gameStore.connection?.off('map:character:move')
|
||||||
|
|
||||||
socketManager.off(SocketEvent.MAP_CHARACTER_TELEPORT)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,32 +1,73 @@
|
|||||||
<template>
|
<template>
|
||||||
<Controls v-if="tileMapLayer" :layer="tileMapLayer" :depth="0" />
|
<Controls v-if="tileLayer" :layer="tileLayer" :depth="0" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
|
import type { UUID } from '@/application/types'
|
||||||
|
import { unduplicateArray } from '@/application/utilities'
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
import { loadTileTexturesFromMapTileArray, placeTiles } from '@/services/mapService'
|
import { FlattenMapArray, loadMapTilesIntoScene, setLayerTiles } from '@/composables/mapComposable'
|
||||||
import { MapStorage } from '@/storage/storages'
|
import { MapStorage } from '@/storage/storages'
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { onMounted } from 'vue'
|
import { onBeforeUnmount, shallowRef } from 'vue'
|
||||||
|
|
||||||
|
import Tileset = Phaser.Tilemaps.Tileset
|
||||||
|
|
||||||
|
const emit = defineEmits(['tileMap:create'])
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const mapStore = useMapStore()
|
const mapStore = useMapStore()
|
||||||
const mapStorage = new MapStorage()
|
const mapStorage = new MapStorage()
|
||||||
|
|
||||||
const props = defineProps<{
|
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||||
tileMap: Phaser.Tilemaps.Tilemap
|
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
||||||
tileMapLayer: Phaser.Tilemaps.TilemapLayer
|
|
||||||
}>()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
function createTileMap(mapData: any) {
|
||||||
if (!mapStore.mapId) return
|
const mapConfig = new Phaser.Tilemaps.MapData({
|
||||||
|
width: mapData?.width,
|
||||||
|
height: mapData?.height,
|
||||||
|
tileWidth: config.tile_size.width,
|
||||||
|
tileHeight: config.tile_size.height,
|
||||||
|
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
||||||
|
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
||||||
|
})
|
||||||
|
|
||||||
const map = await mapStorage.getById(mapStore.mapId)
|
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapConfig)
|
||||||
if (!map) return
|
emit('tileMap:create', newTileMap)
|
||||||
|
return newTileMap
|
||||||
|
}
|
||||||
|
|
||||||
await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
|
function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap, mapData: any) {
|
||||||
|
const tilesArray = unduplicateArray(FlattenMapArray(mapData?.tiles ?? []))
|
||||||
|
|
||||||
placeTiles(props.tileMap, props.tileMapLayer, map.tiles)
|
const tilesetImages = tilesArray.map((tile: string, index: number) => {
|
||||||
|
return currentTileMap.addTilesetImage(tile, tile, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add blank tile
|
||||||
|
tilesetImages.push(currentTileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height }))
|
||||||
|
|
||||||
|
const layer = currentTileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
|
layer.setDepth(0)
|
||||||
|
layer.setCullPadding(2, 2)
|
||||||
|
return layer
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMapTilesIntoScene(mapStore.mapId as UUID, scene)
|
||||||
|
.then(() => mapStorage.get(mapStore.mapId))
|
||||||
|
.then((mapData) => {
|
||||||
|
tileMap.value = createTileMap(mapData)
|
||||||
|
tileLayer.value = createTileLayer(tileMap.value, mapData)
|
||||||
|
setLayerTiles(tileMap.value, tileLayer.value, mapData?.tiles)
|
||||||
|
})
|
||||||
|
.catch((error) => console.error('Failed to initialize map:', error))
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (!tileMap.value) return
|
||||||
|
tileMap.value.destroyLayer('tiles')
|
||||||
|
tileMap.value.removeAllLayers()
|
||||||
|
tileMap.value.destroy()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<PlacedMapObject v-for="placedMapObject in items" :tileMap :tileMapLayer :placedMapObject />
|
<PlacedMapObject v-for="placedMapObject in items" :tilemap="tilemap" :placedMapObject />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -9,11 +9,8 @@ import { MapStorage } from '@/storage/storages'
|
|||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
tileMap: Phaser.Tilemaps.Tilemap
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
tileMapLayer: TilemapLayer
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const mapStore = useMapStore()
|
const mapStore = useMapStore()
|
||||||
@ -23,7 +20,7 @@ const items = ref<PlacedMapObjectT[]>([])
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!mapStore.mapId) return
|
if (!mapStore.mapId) return
|
||||||
|
|
||||||
const map = await mapStorage.getById(mapStore.mapId)
|
const map = await mapStorage.get(mapStore.mapId)
|
||||||
if (!map) return
|
if (!map) return
|
||||||
|
|
||||||
items.value = map.placedMapObjects
|
items.value = map.placedMapObjects
|
||||||
|
@ -1,82 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<Image v-if="mapObject && gameStore.isTextureLoaded(props.placedMapObject.mapObject as string)" v-bind="imageProps" />
|
<Image v-if="gameStore.isTextureLoaded(props.placedMapObject.mapObject.id)" v-bind="imageProps" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import type { PlacedMapObject, TextureData } from '@/application/types'
|
||||||
import type { MapObject, PlacedMapObject } from '@/application/types'
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
|
||||||
import { calculateIsometricDepth, loadMapObjectTextures, tileToWorldXY } from '@/services/mapService'
|
|
||||||
import { MapObjectStorage } from '@/storage/storages'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { Image, useScene } from 'phavuer'
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
|
|
||||||
import Tilemap = Phaser.Tilemaps.Tilemap
|
|
||||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
placedMapObject: PlacedMapObject
|
placedMapObject: PlacedMapObject
|
||||||
tileMap: Tilemap
|
|
||||||
tileMapLayer: TilemapLayer
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
|
|
||||||
const mapObject = ref<MapObject>()
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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 - mapObject.value!.frameWidth / 2,
|
|
||||||
y: position.worldPositionY - mapObject.value!.frameHeight / 2 + config.tile_size.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageProps = computed(() => ({
|
const imageProps = computed(() => ({
|
||||||
alpha: mapEditor.movingPlacedObject.value?.id == props.placedMapObject.id || mapEditor.selectedMapObject.value?.id == props.placedMapObject.id ? 0.5 : 1,
|
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, props.placedMapObject.mapObject.frameWidth, props.placedMapObject.mapObject.frameHeight),
|
||||||
tint: mapEditor.selectedPlacedObject.value?.id == props.placedMapObject.id ? 0x00ff00 : 0xffffff,
|
x: tileToWorldX(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
|
||||||
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY),
|
y: tileToWorldY(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
|
||||||
...calculateObjectPlacement(props.placedMapObject),
|
|
||||||
flipX: props.placedMapObject.isRotated,
|
flipX: props.placedMapObject.isRotated,
|
||||||
texture: mapObject.value!.id,
|
texture: props.placedMapObject.mapObject.id,
|
||||||
originX: mapObject.value!.originX,
|
originY: Number(props.placedMapObject.mapObject.originX),
|
||||||
originY: mapObject.value!.originY
|
originX: Number(props.placedMapObject.mapObject.originY)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
watch(
|
loadTexture(scene, {
|
||||||
() => mapEditor.refreshMapObject.value,
|
key: props.placedMapObject.mapObject.id,
|
||||||
async () => {
|
data: '/textures/map_objects/' + props.placedMapObject.mapObject.id + '.png',
|
||||||
await initialize()
|
group: 'map_objects',
|
||||||
}
|
updatedAt: props.placedMapObject.mapObject.updatedAt,
|
||||||
)
|
frameWidth: props.placedMapObject.mapObject.frameWidth,
|
||||||
|
frameHeight: props.placedMapObject.mapObject.frameHeight
|
||||||
onMounted(async () => {
|
} as TextureData).catch((error) => {
|
||||||
await initialize()
|
console.error('Error loading texture:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {})
|
||||||
</script>
|
</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">Users</button>
|
||||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</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 @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="() => mapEditorStore.toggleActive()">Map editor</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
@ -20,12 +20,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
|
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
const mapEditorStore = useMapEditorStore()
|
||||||
|
|
||||||
let toggle = ref('asset-manager')
|
let toggle = ref('asset-manager')
|
||||||
</script>
|
</script>
|
||||||
|
@ -34,9 +34,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
|
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
@ -67,7 +65,7 @@ if (selectedCharacterHair.value) {
|
|||||||
function removeCharacterHair() {
|
function removeCharacterHair() {
|
||||||
if (!selectedCharacterHair.value) return
|
if (!selectedCharacterHair.value) return
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_REMOVE, { id: selectedCharacterHair.value.id }, (response: boolean) => {
|
gameStore.connection?.emit('gm:characterHair:remove', { id: selectedCharacterHair.value.id }, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove character hair')
|
console.error('Failed to remove character hair')
|
||||||
return
|
return
|
||||||
@ -77,7 +75,7 @@ function removeCharacterHair() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
||||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
||||||
assetManagerStore.setCharacterHairList(response)
|
assetManagerStore.setCharacterHairList(response)
|
||||||
|
|
||||||
if (unsetSelectedCharacterHair) {
|
if (unsetSelectedCharacterHair) {
|
||||||
@ -95,7 +93,7 @@ function saveCharacterHair() {
|
|||||||
spriteId: characterSpriteId.value
|
spriteId: characterSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_UPDATE, characterHairData, (response: boolean) => {
|
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save character type')
|
console.error('Failed to save character type')
|
||||||
return
|
return
|
||||||
@ -115,7 +113,7 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedCharacterHair.value) return
|
if (!selectedCharacterHair.value) return
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -32,9 +32,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import type { CharacterHair } from '@/application/types'
|
import type { CharacterHair } from '@/application/types'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -54,13 +52,13 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewCharacterHair = () => {
|
const createNewCharacterHair = () => {
|
||||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_CREATE, {}, (response: boolean) => {
|
gameStore.connection?.emit('gm:characterHair:create', {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to create new character type')
|
console.error('Failed to create new character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
||||||
assetManagerStore.setCharacterHairList(response)
|
assetManagerStore.setCharacterHairList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -94,7 +92,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
||||||
assetManagerStore.setCharacterHairList(response)
|
assetManagerStore.setCharacterHairList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -40,9 +40,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
|
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
@ -76,7 +74,7 @@ if (selectedCharacterType.value) {
|
|||||||
function removeCharacterType() {
|
function removeCharacterType() {
|
||||||
if (!selectedCharacterType.value) return
|
if (!selectedCharacterType.value) return
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_REMOVE, { id: selectedCharacterType.value.id }, (response: boolean) => {
|
gameStore.connection?.emit('gm:characterType:remove', { id: selectedCharacterType.value.id }, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove character type')
|
console.error('Failed to remove character type')
|
||||||
return
|
return
|
||||||
@ -86,7 +84,7 @@ function removeCharacterType() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
||||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
||||||
assetManagerStore.setCharacterTypeList(response)
|
assetManagerStore.setCharacterTypeList(response)
|
||||||
|
|
||||||
if (unsetSelectedCharacterType) {
|
if (unsetSelectedCharacterType) {
|
||||||
@ -105,7 +103,7 @@ function saveCharacterType() {
|
|||||||
spriteId: characterSpriteId.value
|
spriteId: characterSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_UPDATE, characterTypeData, (response: boolean) => {
|
gameStore.connection?.emit('gm:characterType:update', characterTypeData, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save character type')
|
console.error('Failed to save character type')
|
||||||
return
|
return
|
||||||
@ -126,7 +124,7 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedCharacterType.value) return
|
if (!selectedCharacterType.value) return
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -32,9 +32,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import type { CharacterType } from '@/application/types'
|
import type { CharacterType } from '@/application/types'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -54,13 +52,13 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewCharacterType = () => {
|
const createNewCharacterType = () => {
|
||||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_CREATE, {}, (response: boolean) => {
|
gameStore.connection?.emit('gm:characterType:create', {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to create new character type')
|
console.error('Failed to create new character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
||||||
assetManagerStore.setCharacterTypeList(response)
|
assetManagerStore.setCharacterTypeList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -94,7 +92,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
||||||
assetManagerStore.setCharacterTypeList(response)
|
assetManagerStore.setCharacterTypeList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -44,9 +44,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
|
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
@ -82,7 +80,7 @@ if (selectedItem.value) {
|
|||||||
function removeItem() {
|
function removeItem() {
|
||||||
if (!selectedItem.value) return
|
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) {
|
if (!response) {
|
||||||
console.error('Failed to remove item')
|
console.error('Failed to remove item')
|
||||||
return
|
return
|
||||||
@ -92,7 +90,7 @@ function removeItem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshItemList(unsetSelectedItem = true) {
|
function refreshItemList(unsetSelectedItem = true) {
|
||||||
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||||
assetManagerStore.setItemList(response)
|
assetManagerStore.setItemList(response)
|
||||||
|
|
||||||
if (unsetSelectedItem) {
|
if (unsetSelectedItem) {
|
||||||
@ -112,7 +110,7 @@ function saveItem() {
|
|||||||
spriteId: itemSpriteId.value
|
spriteId: itemSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_ITEM_UPDATE, itemData, (response: boolean) => {
|
gameStore.connection?.emit('gm:item:update', itemData, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save item')
|
console.error('Failed to save item')
|
||||||
return
|
return
|
||||||
@ -134,7 +132,7 @@ watch(selectedItem, (item: Item | null) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -29,9 +29,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import type { Item } from '@/application/types'
|
import type { Item } from '@/application/types'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -50,13 +48,13 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewItem = () => {
|
const createNewItem = () => {
|
||||||
socketManager.emit(SocketEvent.GM_ITEM_CREATE, {}, (response: boolean) => {
|
gameStore.connection?.emit('gm:item:create', {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to create new item')
|
console.error('Failed to create new item')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||||
assetManagerStore.setItemList(response)
|
assetManagerStore.setItemList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -90,7 +88,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||||
assetManagerStore.setItemList(response)
|
assetManagerStore.setItemList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,20 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||||
<div class="relative">
|
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" />
|
||||||
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" @click="addPivotPoint" ref="imageRef" />
|
|
||||||
<svg class="absolute bottom-1 left-0 w-full h-full pointer-events-none">
|
|
||||||
<line v-for="(_, index) in mapObjectPivotPoints.slice(0, -1)" :key="index" :x1="mapObjectPivotPoints[index].x" :y1="mapObjectPivotPoints[index].y" :x2="mapObjectPivotPoints[index + 1].x" :y2="mapObjectPivotPoints[index + 1].y" stroke="white" stroke-width="2" />
|
|
||||||
</svg>
|
|
||||||
<div
|
|
||||||
v-for="(point, index) in mapObjectPivotPoints"
|
|
||||||
:key="index"
|
|
||||||
class="absolute w-2 h-2 bg-white rounded-full cursor-move -translate-x-1.5 -translate-y-1.5 ring-2 ring-black"
|
|
||||||
:style="{ left: point.x + 'px', top: point.y + 'px' }"
|
|
||||||
@mousedown="startDragging(index, $event)"
|
|
||||||
@contextmenu.prevent="removePivotPoint(index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 block">
|
<div class="mt-5 block">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||||
@ -34,6 +21,13 @@
|
|||||||
<label for="tags">Tags</label>
|
<label for="tags">Tags</label>
|
||||||
<ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" />
|
<ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="is-animated">Is animated</label>
|
||||||
|
<select v-model="mapObjectIsAnimated" class="input-field" name="is-animated">
|
||||||
|
<option :value="false">No</option>
|
||||||
|
<option :value="true">Yes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="frame-speed">Frame rate</label>
|
<label for="frame-speed">Frame rate</label>
|
||||||
<input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
<input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||||
@ -57,30 +51,27 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import type { MapObject } from '@/application/types'
|
import type { MapObject } from '@/application/types'
|
||||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
const mapEditorStore = useMapEditorStore()
|
||||||
|
|
||||||
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
|
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
|
||||||
|
|
||||||
const mapObjectName = ref('')
|
const mapObjectName = ref('')
|
||||||
const mapObjectTags = ref<string[]>([])
|
const mapObjectTags = ref<string[]>([])
|
||||||
const mapObjectPivotPoints = ref<Array<{ x: number; y: number }>>([])
|
|
||||||
const mapObjectOriginX = ref(0)
|
const mapObjectOriginX = ref(0)
|
||||||
const mapObjectOriginY = ref(0)
|
const mapObjectOriginY = ref(0)
|
||||||
|
const mapObjectIsAnimated = ref(false)
|
||||||
const mapObjectFrameRate = ref(0)
|
const mapObjectFrameRate = ref(0)
|
||||||
const mapObjectFrameWidth = ref(0)
|
const mapObjectFrameWidth = ref(0)
|
||||||
const mapObjectFrameHeight = ref(0)
|
const mapObjectFrameHeight = ref(0)
|
||||||
const imageRef = ref<HTMLImageElement | null>(null)
|
|
||||||
const isDragging = ref(false)
|
|
||||||
const draggedPointIndex = ref(-1)
|
|
||||||
|
|
||||||
if (!selectedMapObject.value) {
|
if (!selectedMapObject.value) {
|
||||||
console.error('No map mapObject selected')
|
console.error('No map mapObject selected')
|
||||||
@ -89,16 +80,16 @@ if (!selectedMapObject.value) {
|
|||||||
if (selectedMapObject.value) {
|
if (selectedMapObject.value) {
|
||||||
mapObjectName.value = selectedMapObject.value.name
|
mapObjectName.value = selectedMapObject.value.name
|
||||||
mapObjectTags.value = selectedMapObject.value.tags
|
mapObjectTags.value = selectedMapObject.value.tags
|
||||||
mapObjectPivotPoints.value = selectedMapObject.value.pivotPoints
|
|
||||||
mapObjectOriginX.value = selectedMapObject.value.originX
|
mapObjectOriginX.value = selectedMapObject.value.originX
|
||||||
mapObjectOriginY.value = selectedMapObject.value.originY
|
mapObjectOriginY.value = selectedMapObject.value.originY
|
||||||
|
mapObjectIsAnimated.value = selectedMapObject.value.isAnimated
|
||||||
mapObjectFrameRate.value = selectedMapObject.value.frameRate
|
mapObjectFrameRate.value = selectedMapObject.value.frameRate
|
||||||
mapObjectFrameWidth.value = selectedMapObject.value.frameWidth
|
mapObjectFrameWidth.value = selectedMapObject.value.frameWidth
|
||||||
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
|
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeObject() {
|
function removeObject() {
|
||||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_REMOVE, { mapObject: selectedMapObject.value?.id }, (response: boolean) => {
|
gameStore.connection?.emit('gm:mapObject:remove', { mapObject: selectedMapObject.value?.id }, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove mapObject')
|
console.error('Failed to remove mapObject')
|
||||||
return
|
return
|
||||||
@ -108,12 +99,16 @@ function removeObject() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshObjectList(unsetSelectedMapObject = true) {
|
function refreshObjectList(unsetSelectedMapObject = true) {
|
||||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
|
||||||
assetManagerStore.setMapObjectList(response)
|
assetManagerStore.setMapObjectList(response)
|
||||||
|
|
||||||
if (unsetSelectedMapObject) {
|
if (unsetSelectedMapObject) {
|
||||||
assetManagerStore.setSelectedMapObject(null)
|
assetManagerStore.setSelectedMapObject(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mapEditorStore.active) {
|
||||||
|
mapEditorStore.setMapObjectList(response)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,15 +118,15 @@ function saveObject() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.emit(
|
gameStore.connection?.emit(
|
||||||
SocketEvent.GM_MAPOBJECT_UPDATE,
|
'gm:mapObject:update',
|
||||||
{
|
{
|
||||||
id: selectedMapObject.value.id,
|
id: selectedMapObject.value.id,
|
||||||
name: mapObjectName.value,
|
name: mapObjectName.value,
|
||||||
tags: mapObjectTags.value,
|
tags: mapObjectTags.value,
|
||||||
pivotPoints: mapObjectPivotPoints.value,
|
|
||||||
originX: mapObjectOriginX.value,
|
originX: mapObjectOriginX.value,
|
||||||
originY: mapObjectOriginY.value,
|
originY: mapObjectOriginY.value,
|
||||||
|
isAnimated: mapObjectIsAnimated.value,
|
||||||
frameRate: mapObjectFrameRate.value,
|
frameRate: mapObjectFrameRate.value,
|
||||||
frameWidth: mapObjectFrameWidth.value,
|
frameWidth: mapObjectFrameWidth.value,
|
||||||
frameHeight: mapObjectFrameHeight.value
|
frameHeight: mapObjectFrameHeight.value
|
||||||
@ -150,9 +145,9 @@ watch(selectedMapObject, (mapObject: MapObject | null) => {
|
|||||||
if (!mapObject) return
|
if (!mapObject) return
|
||||||
mapObjectName.value = mapObject.name
|
mapObjectName.value = mapObject.name
|
||||||
mapObjectTags.value = mapObject.tags
|
mapObjectTags.value = mapObject.tags
|
||||||
mapObjectPivotPoints.value = mapObject.pivotPoints
|
|
||||||
mapObjectOriginX.value = mapObject.originX
|
mapObjectOriginX.value = mapObject.originX
|
||||||
mapObjectOriginY.value = mapObject.originY
|
mapObjectOriginY.value = mapObject.originY
|
||||||
|
mapObjectIsAnimated.value = mapObject.isAnimated
|
||||||
mapObjectFrameRate.value = mapObject.frameRate
|
mapObjectFrameRate.value = mapObject.frameRate
|
||||||
mapObjectFrameWidth.value = mapObject.frameWidth
|
mapObjectFrameWidth.value = mapObject.frameWidth
|
||||||
mapObjectFrameHeight.value = mapObject.frameHeight
|
mapObjectFrameHeight.value = mapObject.frameHeight
|
||||||
@ -162,51 +157,7 @@ onMounted(() => {
|
|||||||
if (!selectedMapObject.value) return
|
if (!selectedMapObject.value) return
|
||||||
})
|
})
|
||||||
|
|
||||||
function addPivotPoint(event: MouseEvent) {
|
|
||||||
if (!imageRef.value) return
|
|
||||||
// Max 2
|
|
||||||
if (mapObjectPivotPoints.value.length >= 2) return
|
|
||||||
const rect = imageRef.value.getBoundingClientRect()
|
|
||||||
const x = event.clientX - rect.left
|
|
||||||
const y = event.clientY - rect.top
|
|
||||||
mapObjectPivotPoints.value.push({ x, y })
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePivotPoint(index: number) {
|
|
||||||
mapObjectPivotPoints.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
assetManagerStore.setSelectedMapObject(null)
|
assetManagerStore.setSelectedMapObject(null)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.pointer-events-none {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -29,9 +29,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import type { MapObject } from '@/application/types'
|
import type { MapObject } from '@/application/types'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -49,13 +47,13 @@ const elementToScroll = ref()
|
|||||||
const handleFileUpload = (e: Event) => {
|
const handleFileUpload = (e: Event) => {
|
||||||
const files = (e.target as HTMLInputElement).files
|
const files = (e.target as HTMLInputElement).files
|
||||||
if (!files) return
|
if (!files) return
|
||||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_UPLOAD, files, (response: boolean) => {
|
gameStore.connection?.emit('gm:mapObject:upload', files, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
if (config.environment === 'development') console.error('Failed to upload map object')
|
if (config.development) console.error('Failed to upload object')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
|
||||||
assetManagerStore.setMapObjectList(response)
|
assetManagerStore.setMapObjectList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -94,7 +92,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
|
||||||
assetManagerStore.setMapObjectList(response)
|
assetManagerStore.setMapObjectList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
||||||
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
||||||
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
||||||
<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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -21,12 +21,9 @@
|
|||||||
<button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button>
|
<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">
|
<Accordion v-for="action in spriteActions" :key="action.id">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center">
|
<div class="flex justify-between items-center">
|
||||||
{{ action.action }}
|
{{ action.action }}
|
||||||
<div class="ml-auto space-x-2">
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="openPreviewModal(action)">View</button>
|
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
@ -43,38 +40,39 @@
|
|||||||
<label for="origin-y">Origin Y</label>
|
<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" />
|
<input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<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>
|
||||||
|
<div class="form-field-full" v-if="action.isAnimated">
|
||||||
<label for="frame-speed">Frame rate</label>
|
<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" />
|
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<SpriteActionsInput v-model="action.sprites" @tempOffsetChange="(index, offset) => handleTempOffsetChange(action, index, offset)" />
|
<SpriteActionsInput v-model="action.sprites" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<SpritePreview
|
|
||||||
v-if="selectedAction"
|
|
||||||
:sprites="selectedAction.sprites"
|
|
||||||
:frame-rate="selectedAction.frameRate"
|
|
||||||
:is-modal-open="isModalOpen"
|
|
||||||
:temp-offset-index="tempOffsetData.index"
|
|
||||||
:temp-offset="tempOffsetData.offset"
|
|
||||||
@update:frame-rate="updateFrameRate"
|
|
||||||
@update:is-modal-open="isModalOpen = $event"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import type { Sprite, SpriteAction } from '@/application/types'
|
||||||
import type { Sprite, SpriteAction, UUID } from '@/application/types'
|
|
||||||
import { uuidv4 } from '@/application/utilities'
|
import { uuidv4 } from '@/application/utilities'
|
||||||
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
||||||
import SpritePreview from '@/components/gameMaster/assetManager/partials/sprite/partials/SpritePreview.vue'
|
|
||||||
import Accordion from '@/components/utilities/Accordion.vue'
|
import Accordion from '@/components/utilities/Accordion.vue'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
@ -86,8 +84,6 @@ const selectedSprite = computed(() => assetManagerStore.selectedSprite)
|
|||||||
|
|
||||||
const spriteName = ref('')
|
const spriteName = ref('')
|
||||||
const spriteActions = ref<SpriteAction[]>([])
|
const spriteActions = ref<SpriteAction[]>([])
|
||||||
const isModalOpen = ref(false)
|
|
||||||
const selectedAction = ref<SpriteAction | null>(null)
|
|
||||||
|
|
||||||
if (!selectedSprite.value) {
|
if (!selectedSprite.value) {
|
||||||
console.error('No sprite selected')
|
console.error('No sprite selected')
|
||||||
@ -99,7 +95,7 @@ if (selectedSprite.value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteSprite() {
|
function deleteSprite() {
|
||||||
socketManager.emit(SocketEvent.GM_SPRITE_DELETE, { id: selectedSprite.value?.id }, (response: boolean) => {
|
gameStore.connection?.emit('gm:sprite:delete', { id: selectedSprite.value?.id }, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to delete sprite')
|
console.error('Failed to delete sprite')
|
||||||
return
|
return
|
||||||
@ -109,7 +105,7 @@ function deleteSprite() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function copySprite() {
|
function copySprite() {
|
||||||
socketManager.emit(SocketEvent.GM_SPRITE_COPY, { id: selectedSprite.value?.id }, (response: boolean) => {
|
gameStore.connection?.emit('gm:sprite:copy', { id: selectedSprite.value?.id }, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to copy sprite')
|
console.error('Failed to copy sprite')
|
||||||
return
|
return
|
||||||
@ -119,7 +115,7 @@ function copySprite() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshSpriteList(unsetSelectedSprite = true) {
|
function refreshSpriteList(unsetSelectedSprite = true) {
|
||||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
|
|
||||||
if (unsetSelectedSprite) {
|
if (unsetSelectedSprite) {
|
||||||
@ -144,6 +140,8 @@ function saveSprite() {
|
|||||||
sprites: action.sprites,
|
sprites: action.sprites,
|
||||||
originX: action.originX,
|
originX: action.originX,
|
||||||
originY: action.originY,
|
originY: action.originY,
|
||||||
|
isAnimated: action.isAnimated,
|
||||||
|
isLooping: action.isLooping,
|
||||||
frameRate: action.frameRate,
|
frameRate: action.frameRate,
|
||||||
frameWidth: action.frameWidth,
|
frameWidth: action.frameWidth,
|
||||||
frameHeight: action.frameHeight
|
frameHeight: action.frameHeight
|
||||||
@ -151,7 +149,7 @@ function saveSprite() {
|
|||||||
}) ?? []
|
}) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_SPRITE_UPDATE, updatedSprite, (response: boolean) => {
|
gameStore.connection?.emit('gm:sprite:update', updatedSprite, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save sprite')
|
console.error('Failed to save sprite')
|
||||||
return
|
return
|
||||||
@ -165,11 +163,14 @@ function addNewImage() {
|
|||||||
|
|
||||||
const newImage: SpriteAction = {
|
const newImage: SpriteAction = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
sprite: selectedSprite.value.id,
|
spriteId: selectedSprite.value.id,
|
||||||
|
sprite: selectedSprite.value,
|
||||||
action: 'new_action',
|
action: 'new_action',
|
||||||
sprites: [],
|
sprites: [],
|
||||||
originX: 0,
|
originX: 0,
|
||||||
originY: 0,
|
originY: 0,
|
||||||
|
isAnimated: false,
|
||||||
|
isLooping: false,
|
||||||
frameRate: 0,
|
frameRate: 0,
|
||||||
frameWidth: 0,
|
frameWidth: 0,
|
||||||
frameHeight: 0
|
frameHeight: 0
|
||||||
@ -187,40 +188,12 @@ function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
|
|||||||
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPreviewModal(action: SpriteAction) {
|
|
||||||
selectedAction.value = action
|
|
||||||
isModalOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFrameRate(value: number) {
|
|
||||||
if (selectedAction.value) {
|
|
||||||
selectedAction.value.frameRate = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tempOffsetData = ref<{ index: number | undefined; offset: { x: number; y: number } | undefined }>({
|
|
||||||
index: undefined,
|
|
||||||
offset: undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
|
|
||||||
if (selectedAction.value === action) {
|
|
||||||
tempOffsetData.value = { index, offset }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(selectedSprite, (sprite: Sprite | null) => {
|
watch(selectedSprite, (sprite: Sprite | null) => {
|
||||||
if (!sprite) return
|
if (!sprite) return
|
||||||
spriteName.value = sprite.name
|
spriteName.value = sprite.name
|
||||||
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(isModalOpen, (newValue) => {
|
|
||||||
if (!newValue) {
|
|
||||||
selectedAction.value = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedSprite.value) return
|
if (!selectedSprite.value) return
|
||||||
})
|
})
|
||||||
|
@ -25,9 +25,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import type { Sprite } from '@/application/types'
|
import type { Sprite } from '@/application/types'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -42,13 +40,13 @@ const hasScrolled = ref(false)
|
|||||||
const elementToScroll = ref()
|
const elementToScroll = ref()
|
||||||
|
|
||||||
function newButtonClickHandler() {
|
function newButtonClickHandler() {
|
||||||
socketManager.emit(SocketEvent.GM_SPRITE_CREATE, {}, (response: boolean) => {
|
gameStore.connection?.emit('gm:sprite:create', {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
if (config.environment === 'development') console.error('Failed to create new sprite')
|
if (config.development) console.error('Failed to create new sprite')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -87,7 +85,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,44 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap gap-3">
|
<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)">
|
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
|
||||||
<img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" />
|
<img :src="image" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" />
|
||||||
<div v-if="imageDimensions[index]" class="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-1 py-0.5 rounded transition-opacity font-default">{{ imageDimensions[index].width }}x{{ imageDimensions[index].height }}</div>
|
|
||||||
<div class="absolute top-1 left-1 flex-row space-y-1">
|
<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">
|
<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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button @click.stop="openOffsetModal(index)" class="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Scope image">
|
<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 width="50px" height="50px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path d="M8.29289 3.70711L1 11V15H5L12.2929 7.70711L8.29289 3.70711Z" fill="white" />
|
<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" />
|
||||||
<path d="M9.70711 2.29289L13.7071 6.29289L15.1716 4.82843C15.702 4.29799 16 3.57857 16 2.82843C16 1.26633 14.7337 0 13.1716 0C12.4214 0 11.702 0.297995 11.1716 0.828428L9.70711 2.29289Z" fill="white" />
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal :is-modal-open="selectedImageIndex === index" :modal-width="300" :modal-height="210" :is-resizable="false" bg-style="none" @modal:close="closeOffsetModal">
|
|
||||||
<template #modalHeader>
|
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Action offset ({{ selectedImageIndex }})</h3>
|
|
||||||
</template>
|
|
||||||
<template #modalBody>
|
|
||||||
<div class="m-4">
|
|
||||||
<form method="post" @submit.prevent="saveOffset(index)" class="inline">
|
|
||||||
<div class="gap-2.5 flex flex-wrap">
|
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="offsetX">Offset X</label>
|
|
||||||
<input class="input-field max-w-64" v-model="tempOffset.x" name="offsetX" id="offsetX" type="number" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="offsetY">Offset Y</label>
|
|
||||||
<input class="input-field max-w-64" v-model="tempOffset.y" name="offsetY" id="offsetY" type="number" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<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">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@ -50,19 +25,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import { ref } from 'vue'
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
|
|
||||||
interface SpriteImage {
|
|
||||||
url: string
|
|
||||||
offset: {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: SpriteImage[]
|
modelValue: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@ -70,15 +36,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: SpriteImage[]): void
|
(e: 'update:modelValue', value: string[]): void
|
||||||
(e: 'close'): void
|
|
||||||
(e: 'tempOffsetChange', index: number, offset: { x: number; y: number }): void
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const fileInput = ref<HTMLInputElement | null>(null)
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
const draggedIndex = ref<number | null>(null)
|
const draggedIndex = ref<number | null>(null)
|
||||||
const selectedImageIndex = ref<number | null>(null)
|
|
||||||
const tempOffset = ref({ x: 0, y: 0 })
|
|
||||||
|
|
||||||
const triggerFileInput = () => {
|
const triggerFileInput = () => {
|
||||||
fileInput.value?.click()
|
fileInput.value?.click()
|
||||||
@ -99,25 +61,19 @@ const onDrop = (event: DragEvent) => {
|
|||||||
|
|
||||||
const handleFiles = (files: FileList) => {
|
const handleFiles = (files: FileList) => {
|
||||||
Array.from(files).forEach((file) => {
|
Array.from(files).forEach((file) => {
|
||||||
if (!file.type.startsWith('image/')) {
|
if (file.type.startsWith('image/')) {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
if (typeof e.target?.result === 'string') {
|
if (typeof e.target?.result === 'string') {
|
||||||
const newImage: SpriteImage = {
|
updateImages([...props.modelValue, e.target.result])
|
||||||
url: e.target.result,
|
|
||||||
offset: { x: 0, y: 0 }
|
|
||||||
}
|
|
||||||
updateImages([...props.modelValue, newImage])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateImages = (newImages: SpriteImage[]) => {
|
const updateImages = (newImages: string[]) => {
|
||||||
emit('update:modelValue', newImages)
|
emit('update:modelValue', newImages)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,41 +101,4 @@ const drop = (event: DragEvent, dropIndex: number) => {
|
|||||||
}
|
}
|
||||||
draggedIndex.value = null
|
draggedIndex.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const openOffsetModal = (index: number) => {
|
|
||||||
selectedImageIndex.value = index
|
|
||||||
tempOffset.value = { ...props.modelValue[index].offset }
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeOffsetModal = () => {
|
|
||||||
selectedImageIndex.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveOffset = (index: number) => {
|
|
||||||
const newImages = [...props.modelValue]
|
|
||||||
newImages[index] = {
|
|
||||||
...newImages[index],
|
|
||||||
offset: { ...tempOffset.value }
|
|
||||||
}
|
|
||||||
updateImages(newImages)
|
|
||||||
closeOffsetModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onOffsetChange = () => {
|
|
||||||
if (selectedImageIndex.value !== null) {
|
|
||||||
emit('tempOffsetChange', selectedImageIndex.value, tempOffset.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(tempOffset, onOffsetChange, { deep: true })
|
|
||||||
|
|
||||||
const imageDimensions = ref<{ [key: number]: { width: number; height: number } }>({})
|
|
||||||
|
|
||||||
const updateImageDimensions = (event: Event, index: number) => {
|
|
||||||
const img = event.target as HTMLImageElement
|
|
||||||
imageDimensions.value[index] = {
|
|
||||||
width: img.naturalWidth,
|
|
||||||
height: img.naturalHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,151 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Modal :is-modal-open="isModalOpen" :modal-width="700" :modal-height="330" bg-style="none" @modal:close="closeModal">
|
|
||||||
<template #modalHeader>
|
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">View sprite</h3>
|
|
||||||
</template>
|
|
||||||
<template #modalBody>
|
|
||||||
<div class="m-4 flex gap-8">
|
|
||||||
<div class="relative">
|
|
||||||
<div
|
|
||||||
class="sprite-container bg-gray-800"
|
|
||||||
:style="{
|
|
||||||
width: `${maxWidth}px`,
|
|
||||||
height: `${maxHeight}px`,
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-for="(sprite, index) in spritesWithTempOffset"
|
|
||||||
:key="index"
|
|
||||||
:src="sprite.url"
|
|
||||||
alt="Sprite"
|
|
||||||
:style="{
|
|
||||||
position: 'absolute',
|
|
||||||
left: `${sprite.offset?.x || 0}px`,
|
|
||||||
bottom: `${sprite.offset?.y || 0}px`,
|
|
||||||
display: currentFrame === index ? 'block' : 'none',
|
|
||||||
transform: `scale(${zoomLevel / 100})`,
|
|
||||||
transformOrigin: 'bottom left'
|
|
||||||
}"
|
|
||||||
@load="updateContainerSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-center gap-8 flex-1">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<label class="block mb-2 text-white">Frame Rate: {{ frameRate }} FPS (Duration: {{ totalDuration }}s)</label>
|
|
||||||
<input type="range" v-model.number="localFrameRate" min="0" max="60" step="1" class="w-full accent-cyan-500" @input="updateFrameRate" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<label class="block mb-2 text-white">Frame: {{ currentFrame + 1 }} of {{ sprites.length }}</label>
|
|
||||||
<input type="range" v-model.number="currentFrame" :min="0" :max="sprites.length - 1" step="1" class="w-full accent-cyan-500" @input="stopAnimation" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<label class="block mb-2 text-white">Zoom: {{ zoomLevel }}%</label>
|
|
||||||
<input type="range" v-model.number="zoomLevel" min="10" max="200" step="10" class="w-full accent-cyan-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { SpriteImage } from '@/application/types'
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
sprites: SpriteImage[]
|
|
||||||
frameRate: number
|
|
||||||
isModalOpen?: boolean
|
|
||||||
tempOffsetIndex?: number
|
|
||||||
tempOffset?: { x: number; y: number }
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:frameRate', value: number): void
|
|
||||||
(e: 'update:isModalOpen', value: boolean): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const currentFrame = ref(0)
|
|
||||||
const maxWidth = ref(250)
|
|
||||||
const maxHeight = ref(250)
|
|
||||||
const localFrameRate = ref(props.frameRate)
|
|
||||||
const zoomLevel = ref(100)
|
|
||||||
let animationInterval: number | null = null
|
|
||||||
|
|
||||||
const totalDuration = computed(() => {
|
|
||||||
if (props.frameRate <= 0) return 0
|
|
||||||
return (props.sprites.length / props.frameRate).toFixed(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
const spritesWithTempOffset = computed(() => {
|
|
||||||
return props.sprites.map((sprite, index) => {
|
|
||||||
if (index === props.tempOffsetIndex && props.tempOffset) {
|
|
||||||
return { ...sprite, offset: props.tempOffset }
|
|
||||||
}
|
|
||||||
return sprite
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function updateContainerSize(event: Event) {
|
|
||||||
const img = event.target as HTMLImageElement
|
|
||||||
maxWidth.value = Math.max(maxWidth.value, img.naturalWidth)
|
|
||||||
maxHeight.value = Math.max(maxHeight.value, img.naturalHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAnimation() {
|
|
||||||
stopAnimation()
|
|
||||||
if (props.frameRate <= 0 || props.sprites.length === 0) {
|
|
||||||
currentFrame.value = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
animationInterval = window.setInterval(() => {
|
|
||||||
currentFrame.value = (currentFrame.value + 1) % props.sprites.length
|
|
||||||
}, 1000 / props.frameRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopAnimation() {
|
|
||||||
if (animationInterval) {
|
|
||||||
clearInterval(animationInterval)
|
|
||||||
animationInterval = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFrameRate() {
|
|
||||||
emit('update:frameRate', localFrameRate.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal() {
|
|
||||||
emit('update:isModalOpen', false)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.frameRate,
|
|
||||||
(newValue) => {
|
|
||||||
localFrameRate.value = newValue
|
|
||||||
updateAnimation()
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(() => props.sprites, updateAnimation, { immediate: true })
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
updateAnimation()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopAnimation()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.sprite-container {
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -24,18 +24,16 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import type { Tile } from '@/application/types'
|
import type { Tile } from '@/application/types'
|
||||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { TileStorage } from '@/storage/storages'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
const tileStorage = new TileStorage()
|
const mapEditorStore = useMapEditorStore()
|
||||||
|
|
||||||
const selectedTile = computed(() => assetManagerStore.selectedTile)
|
const selectedTile = computed(() => assetManagerStore.selectedTile)
|
||||||
|
|
||||||
@ -57,24 +55,27 @@ watch(selectedTile, (tile: Tile | null) => {
|
|||||||
tileTags.value = tile.tags
|
tileTags.value = tile.tags
|
||||||
})
|
})
|
||||||
|
|
||||||
async function deleteTile() {
|
function deleteTile() {
|
||||||
socketManager.emit(SocketEvent.GM_TILE_DELETE, { id: selectedTile.value?.id }, async (response: boolean) => {
|
gameStore.connection?.emit('gm:tile:delete', { id: selectedTile.value?.id }, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to delete tile')
|
console.error('Failed to delete tile')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await tileStorage.delete(selectedTile.value!.id)
|
|
||||||
refreshTileList()
|
refreshTileList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshTileList(unsetSelectedTile = true) {
|
function refreshTileList(unsetSelectedTile = true) {
|
||||||
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
||||||
assetManagerStore.setTileList(response)
|
assetManagerStore.setTileList(response)
|
||||||
|
|
||||||
if (unsetSelectedTile) {
|
if (unsetSelectedTile) {
|
||||||
assetManagerStore.setSelectedTile(null)
|
assetManagerStore.setSelectedTile(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mapEditorStore.active) {
|
||||||
|
mapEditorStore.setTileList(response)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +85,7 @@ function saveTile() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.emit(
|
gameStore.connection?.emit(
|
||||||
'gm:tile:update',
|
'gm:tile:update',
|
||||||
{
|
{
|
||||||
id: selectedTile.value.id,
|
id: selectedTile.value.id,
|
||||||
|
@ -29,9 +29,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import type { Tile } from '@/application/types'
|
import type { Tile } from '@/application/types'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -49,13 +47,13 @@ const elementToScroll = ref()
|
|||||||
const handleFileUpload = (e: Event) => {
|
const handleFileUpload = (e: Event) => {
|
||||||
const files = (e.target as HTMLInputElement).files
|
const files = (e.target as HTMLInputElement).files
|
||||||
if (!files) return
|
if (!files) return
|
||||||
socketManager.emit(SocketEvent.GM_TILE_UPLOAD, files, (response: boolean) => {
|
gameStore.connection?.emit('gm:tile:upload', files, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
if (config.environment === 'development') console.error('Failed to upload tile')
|
if (config.development) console.error('Failed to upload tile')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
||||||
assetManagerStore.setTileList(response)
|
assetManagerStore.setTileList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -94,7 +92,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
||||||
assetManagerStore.setTileList(response)
|
assetManagerStore.setTileList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,240 +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, createTileArray, createTileLayer, createTileMap, placeTiles } from '@/services/mapService'
|
|
||||||
import { TileStorage } from '@/storage/storages'
|
|
||||||
import { useManualRefHistory, 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 = []
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
removeEventListener('keydown', handleKeyDown)
|
|
||||||
})
|
|
||||||
</script>
|
|
72
src/components/gameMaster/mapEditor/MapEditor.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<MapTiles @tileMap:create="tileMap = $event" />
|
||||||
|
<PlacedMapObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
|
<MapEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
|
|
||||||
|
<Toolbar @save="save" @clear="clear" />
|
||||||
|
|
||||||
|
<MapList />
|
||||||
|
<TileList />
|
||||||
|
<ObjectList />
|
||||||
|
|
||||||
|
<MapSettings />
|
||||||
|
<TeleportModal />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type Map } 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 MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
|
||||||
|
import ObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
|
||||||
|
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
|
||||||
|
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
|
||||||
|
import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue'
|
||||||
|
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
|
import { onUnmounted, shallowRef } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const mapEditorStore = useMapEditorStore()
|
||||||
|
|
||||||
|
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
|
// Clear objects, event tiles and tiles
|
||||||
|
mapEditorStore.map.placedMapObjects = []
|
||||||
|
mapEditorStore.map.mapEventTiles = []
|
||||||
|
mapEditorStore.triggerClearTiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
mapId: mapEditorStore.map.id,
|
||||||
|
name: mapEditorStore.mapSettings.name,
|
||||||
|
width: mapEditorStore.mapSettings.width,
|
||||||
|
height: mapEditorStore.mapSettings.height,
|
||||||
|
tiles: mapEditorStore.map.tiles,
|
||||||
|
pvp: mapEditorStore.map.pvp,
|
||||||
|
mapEffects: mapEditorStore.map.mapEffects?.map(({ id, effect, strength }) => ({ id, effect, strength })) ?? [],
|
||||||
|
mapEventTiles: mapEditorStore.map.mapEventTiles?.map(({ id, type, positionX, positionY, teleport }) => ({ id, type, positionX, positionY, teleport })) ?? [],
|
||||||
|
placedMapObjects: mapEditorStore.map.placedMapObjects?.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapEditorStore.isSettingsModalShown) {
|
||||||
|
mapEditorStore.toggleSettingsModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:map:update', data, (response: Map) => {
|
||||||
|
mapEditorStore.setMap(response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
mapEditorStore.reset()
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,156 +1,118 @@
|
|||||||
<template>
|
<template>
|
||||||
<Image v-for="tile in mapEditor.currentMap.value?.mapEventTiles" v-bind="getImageProps(tile)" />
|
<Image v-for="tile in mapEditorStore.map?.mapEventTiles" v-bind="getImageProps(tile)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { MapEventTileType, type MapEventTile, type Map as MapT, type UUID } from '@/application/types'
|
import { MapEventTileType, type MapEventTile } from '@/application/types'
|
||||||
import { uuidv4 } from '@/application/utilities'
|
import { uuidv4 } from '@/application/utilities'
|
||||||
import { type EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue'
|
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
import { cloneArray, getTile, tileToWorldX, tileToWorldY } from '@/services/mapService'
|
import { Image, useScene } from 'phavuer'
|
||||||
import { Image } from 'phavuer'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const scene = useScene()
|
||||||
|
const mapEditorStore = useMapEditorStore()
|
||||||
defineExpose({ handlePointer, finalizeCommand, clearTiles })
|
|
||||||
|
|
||||||
const emit = defineEmits(['createCommand'])
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tileMap: Phaser.Tilemaps.Tilemap
|
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) {
|
function getImageProps(tile: MapEventTile) {
|
||||||
return {
|
return {
|
||||||
x: tileToWorldX(props.tileMap, tile.positionX, tile.positionY),
|
x: tileToWorldX(props.tilemap, tile.positionX, tile.positionY),
|
||||||
y: tileToWorldY(props.tileMap, tile.positionX, tile.positionY),
|
y: tileToWorldY(props.tilemap, tile.positionX, tile.positionY),
|
||||||
texture: tile.type,
|
texture: tile.type,
|
||||||
depth: 999
|
depth: 999
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
function pencil(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if map is set
|
||||||
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (mapEditorStore.tool !== 'pencil') return
|
||||||
|
|
||||||
|
// Check if draw mode is blocking tile or teleport
|
||||||
|
if (mapEditorStore.drawMode !== 'blocking tile' && mapEditorStore.drawMode !== 'teleport') return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
|
|
||||||
// Check if event tile already exists on position
|
// Check if event tile already exists on position
|
||||||
const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
|
const existingEventTile = mapEditorStore.map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
|
||||||
if (existingEventTile) return
|
if (existingEventTile) return
|
||||||
|
|
||||||
// If teleport, check if there is a selected map
|
// If teleport, check if there is a selected map
|
||||||
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMap) return
|
if (mapEditorStore.drawMode === 'teleport' && !mapEditorStore.teleportSettings.toMap) return
|
||||||
|
|
||||||
const newEventTile = {
|
const newEventTile = {
|
||||||
id: uuidv4() as UUID,
|
id: uuidv4(),
|
||||||
type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
|
mapId: mapEditorStore.map.id,
|
||||||
|
map: mapEditorStore.map,
|
||||||
|
type: mapEditorStore.drawMode === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
|
||||||
positionX: tile.x,
|
positionX: tile.x,
|
||||||
positionY: tile.y,
|
positionY: tile.y,
|
||||||
teleport:
|
teleport:
|
||||||
mapEditor.drawMode.value === 'teleport'
|
mapEditorStore.drawMode === 'teleport'
|
||||||
? {
|
? {
|
||||||
toMap: mapEditor.teleportSettings.value.toMap,
|
toMap: mapEditorStore.teleportSettings.toMap,
|
||||||
toPositionX: mapEditor.teleportSettings.value.toPositionX,
|
toPositionX: mapEditorStore.teleportSettings.toPositionX,
|
||||||
toPositionY: mapEditor.teleportSettings.value.toPositionY,
|
toPositionY: mapEditorStore.teleportSettings.toPositionY,
|
||||||
toRotation: mapEditor.teleportSettings.value.toRotation
|
toRotation: mapEditorStore.teleportSettings.toRotation
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
createCommandUpdate(newEventTile as MapEventTile, 'draw')
|
mapEditorStore.map.mapEventTiles = mapEditorStore.map.mapEventTiles.concat(newEventTile as MapEventTile)
|
||||||
|
|
||||||
map.mapEventTiles.push(newEventTile as MapEventTile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function erase(pointer: Phaser.Input.Pointer, map: MapT) {
|
function eraser(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if map is set
|
||||||
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (mapEditorStore.tool !== 'eraser') return
|
||||||
|
|
||||||
|
// Check if draw mode is blocking tile or teleport
|
||||||
|
if (mapEditorStore.eraserMode !== 'blocking tile' && mapEditorStore.eraserMode !== 'teleport') return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
|
|
||||||
// Check if event tile already exists on position
|
// Check if event tile already exists on position
|
||||||
const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
|
const existingEventTile = mapEditorStore.map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
|
||||||
if (!existingEventTile) return
|
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
|
// Remove existing event tile
|
||||||
map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
|
mapEditorStore.map.mapEventTiles = mapEditorStore.map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePointer(pointer: Phaser.Input.Pointer) {
|
onMounted(() => {
|
||||||
const map = mapEditor.currentMap.value
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
if (!map) return
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
})
|
||||||
|
|
||||||
if (pointer.event.altKey) return
|
onUnmounted(() => {
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
switch (mapEditor.tool.value) {
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
case 'pencil':
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||||
pencil(pointer, map)
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
break
|
})
|
||||||
case 'eraser':
|
|
||||||
erase(pointer, map)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearTiles() {
|
|
||||||
if (mapEditor.currentMap.value?.mapEventTiles.length === 0) return
|
|
||||||
createCommandUpdate(null, 'clear')
|
|
||||||
finalizeCommand()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,157 +1,231 @@
|
|||||||
<template>
|
<template>
|
||||||
<Controls v-if="tileMapLayer" :layer="tileMapLayer" :depth="0" />
|
<Controls v-if="tileLayer" :layer="tileLayer" :depth="0" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue'
|
import config from '@/application/config'
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { createTileArray, getTile, loadAllTilesIntoScene, placeTile, setLayerTiles } from '@/composables/mapComposable'
|
||||||
import { cloneArray, createTileArray, getTile, placeTile, placeTiles } from '@/services/mapService'
|
import { TileStorage } from '@/storage/storages'
|
||||||
import { onMounted, ref, watch } from 'vue'
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
|
import { onBeforeMount, onMounted, onUnmounted, shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
import Tileset = Phaser.Tilemaps.Tileset
|
||||||
|
|
||||||
defineExpose({ handlePointer, finalizeCommand, clearTiles })
|
const emit = defineEmits(['tileMap:create'])
|
||||||
|
|
||||||
const emit = defineEmits(['createCommand'])
|
const scene = useScene()
|
||||||
|
const mapEditorStore = useMapEditorStore()
|
||||||
|
const tileStorage = new TileStorage()
|
||||||
|
|
||||||
const props = defineProps<{
|
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||||
tileMap: Phaser.Tilemaps.Tilemap
|
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
||||||
tileMapLayer: Phaser.Tilemaps.TilemapLayer
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// *** COMMAND STATE ***
|
function createTileMap() {
|
||||||
|
const mapData = new Phaser.Tilemaps.MapData({
|
||||||
|
width: mapEditorStore.map?.width,
|
||||||
|
height: mapEditorStore.map?.height,
|
||||||
|
tileWidth: config.tile_size.width,
|
||||||
|
tileHeight: config.tile_size.height,
|
||||||
|
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
||||||
|
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
||||||
|
})
|
||||||
|
|
||||||
let currentCommand: TileCommand | null = null
|
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapData)
|
||||||
|
emit('tileMap:create', newTileMap)
|
||||||
class TileCommand implements EditorCommand {
|
return newTileMap
|
||||||
public operation: 'draw' | 'erase' | 'clear' = 'draw'
|
|
||||||
public type: 'tile' = 'tile'
|
|
||||||
public tileName: string = 'blank_tile'
|
|
||||||
public affectedTiles: number[][] = []
|
|
||||||
|
|
||||||
apply(elements: string[][]) {
|
|
||||||
let tileVersion
|
|
||||||
if (this.operation === 'clear') {
|
|
||||||
tileVersion = createTileArray(props.tileMapLayer.width, props.tileMapLayer.height, 'blank_tile')
|
|
||||||
} else {
|
|
||||||
tileVersion = cloneArray(elements) as string[][]
|
|
||||||
for (const position of this.affectedTiles) {
|
|
||||||
tileVersion[position[1]][position[0]] = this.tileName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tileVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(operation: 'draw' | 'erase' | 'clear', tileName: string) {
|
|
||||||
this.operation = operation
|
|
||||||
this.tileName = tileName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCommandUpdate(x: number, y: number, tileName: string, operation: 'draw' | 'erase' | 'clear') {
|
async function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap) {
|
||||||
if (!currentCommand) {
|
const tiles = await tileStorage.getAll()
|
||||||
currentCommand = new TileCommand(operation, tileName)
|
const tilesetImages = []
|
||||||
|
|
||||||
|
for (const tile of tiles) {
|
||||||
|
tilesetImages.push(currentTileMap.addTilesetImage(tile.id, tile.id, config.tile_size.width, config.tile_size.height, 1, 2, tilesetImages.length + 1, { x: 0, y: -config.tile_size.height }))
|
||||||
}
|
}
|
||||||
|
|
||||||
//If position is already in, do not proceed
|
// Add blank tile
|
||||||
for (const vec of currentCommand.affectedTiles) {
|
tilesetImages.push(currentTileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height }))
|
||||||
if (vec[0] === x && vec[1] === y) return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentCommand.affectedTiles.push([x, y])
|
const layer = currentTileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
|
layer.setDepth(0)
|
||||||
|
layer.setCullPadding(2, 2)
|
||||||
|
return layer
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalizeCommand() {
|
function pencil(pointer: Phaser.Input.Pointer) {
|
||||||
if (!currentCommand) return
|
if (!tileMap.value || !tileLayer.value) return
|
||||||
emit('createCommand', currentCommand)
|
|
||||||
currentCommand = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// *** HANDLERS ***
|
// Check if map is set
|
||||||
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
function draw(pointer: Phaser.Input.Pointer, tileName: string) {
|
// Check if tool is pencil
|
||||||
let map = mapEditor.currentMap.value
|
if (mapEditorStore.tool !== 'pencil') return
|
||||||
if (!map) return
|
|
||||||
|
// Check if draw mode is tile
|
||||||
|
if (mapEditorStore.drawMode !== 'tile') return
|
||||||
|
|
||||||
|
// Check if there is a selected tile
|
||||||
|
if (!mapEditorStore.selectedTile) return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(props.tileMapLayer, pointer.worldX, pointer.worldY)
|
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
|
|
||||||
// Place tile
|
// Place tile
|
||||||
placeTile(props.tileMap, props.tileMapLayer, tile.x, tile.y, tileName)
|
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditorStore.selectedTile)
|
||||||
|
|
||||||
createCommandUpdate(tile.x, tile.y, tileName, tileName === 'blank_tile' ? 'erase' : 'draw')
|
|
||||||
|
|
||||||
// Adjust mapEditorStore.map.tiles
|
// Adjust mapEditorStore.map.tiles
|
||||||
map.tiles[tile.y][tile.x] = tileName
|
mapEditorStore.map.tiles[tile.y][tile.x] = mapEditorStore.selectedTile
|
||||||
}
|
}
|
||||||
|
|
||||||
function paint(pointer: Phaser.Input.Pointer) {
|
function eraser(pointer: Phaser.Input.Pointer) {
|
||||||
let map = mapEditor.currentMap.value
|
if (!tileMap.value || !tileLayer.value) return
|
||||||
if (!map) return
|
|
||||||
|
|
||||||
// Set new tileArray with selected tile
|
// Check if map is set
|
||||||
const tileArray = createTileArray(props.tileMap.width, props.tileMap.height, mapEditor.selectedTile.value)
|
if (!mapEditorStore.map) return
|
||||||
placeTiles(props.tileMap, props.tileMapLayer, tileArray)
|
|
||||||
|
|
||||||
// Adjust mapEditorStore.map.tiles
|
// Check if tool is pencil
|
||||||
map.tiles = tileArray
|
if (mapEditorStore.tool !== 'eraser') return
|
||||||
}
|
|
||||||
|
|
||||||
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
|
// Check if draw mode is tile
|
||||||
function tilePicker(pointer: Phaser.Input.Pointer) {
|
if (mapEditorStore.eraserMode !== 'tile') return
|
||||||
let map = mapEditor.currentMap.value
|
|
||||||
if (!map) return
|
|
||||||
|
|
||||||
// Check if there is a tile
|
|
||||||
const tile = getTile(props.tileMapLayer, pointer.worldX, pointer.worldY)
|
|
||||||
if (!tile) return
|
|
||||||
|
|
||||||
// Select the tile
|
|
||||||
mapEditor.setSelectedTile(map.tiles[tile.y][tile.x])
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointer(pointer: Phaser.Input.Pointer) {
|
|
||||||
// Check if left mouse button is pressed
|
// Check if left mouse button is pressed
|
||||||
if (!pointer.isDown && pointer.button === 0) return
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
// Check if shift is not pressed, this means we are moving the camera
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
if (pointer.event.shiftKey) return
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
// Check if alt is pressed
|
// Check if alt is pressed
|
||||||
if (pointer.event.altKey) {
|
if (pointer.event.altKey) return
|
||||||
tilePicker(pointer)
|
|
||||||
return
|
// Check if there is a tile
|
||||||
}
|
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
// Place tile
|
||||||
|
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile')
|
||||||
|
|
||||||
|
// Adjust mapEditorStore.map.tiles
|
||||||
|
mapEditorStore.map.tiles[tile.y][tile.x] = 'blank_tile'
|
||||||
|
}
|
||||||
|
|
||||||
|
function paint(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (!tileMap.value || !tileLayer.value) return
|
||||||
|
|
||||||
|
// Check if map is set
|
||||||
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (mapEditorStore.tool !== 'paint') return
|
||||||
|
|
||||||
|
// Check if there is a selected tile
|
||||||
|
if (!mapEditorStore.selectedTile) return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed
|
||||||
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Set new tileArray with selected tile
|
||||||
|
setLayerTiles(tileMap.value, tileLayer.value, createTileArray(tileMap.value.width, tileMap.value.height, mapEditorStore.selectedTile))
|
||||||
|
|
||||||
|
// Adjust mapEditorStore.map.tiles
|
||||||
|
mapEditorStore.map.tiles = createTileArray(tileMap.value.width, tileMap.value.height, mapEditorStore.selectedTile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
|
||||||
|
function tilePicker(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (!tileMap.value || !tileLayer.value) return
|
||||||
|
|
||||||
|
// Check if map is set
|
||||||
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (mapEditorStore.tool !== 'pencil') return
|
||||||
|
|
||||||
// Check if draw mode is tile
|
// Check if draw mode is tile
|
||||||
switch (mapEditor.tool.value) {
|
if (mapEditorStore.drawMode !== 'tile') return
|
||||||
case 'pencil':
|
|
||||||
draw(pointer, mapEditor.selectedTile.value!)
|
// Check if left mouse button is pressed
|
||||||
break
|
if (!pointer.isDown) return
|
||||||
case 'eraser':
|
|
||||||
draw(pointer, 'blank_tile')
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
break
|
if (pointer.event.shiftKey) return
|
||||||
case 'paint':
|
|
||||||
paint(pointer)
|
// Check if alt is pressed
|
||||||
break
|
if (!pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Check if there is a tile
|
||||||
|
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
// Select the tile
|
||||||
|
mapEditorStore.setSelectedTile(mapEditorStore.map.tiles[tile.y][tile.x])
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => mapEditorStore.shouldClearTiles,
|
||||||
|
(shouldClear) => {
|
||||||
|
if (shouldClear && mapEditorStore.map && tileMap.value && tileLayer.value) {
|
||||||
|
const blankTiles = createTileArray(tileMap.value.width, tileMap.value.height, 'blank_tile')
|
||||||
|
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
|
||||||
|
mapEditorStore.map.tiles = blankTiles
|
||||||
|
mapEditorStore.resetClearTilesFlag()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
// *** LIFECYCLE ***
|
|
||||||
|
|
||||||
function clearTiles() {
|
|
||||||
const tileArray = createTileArray(props.tileMap.width, props.tileMap.height, 'blank_tile')
|
|
||||||
placeTiles(props.tileMap, props.tileMapLayer, tileArray)
|
|
||||||
createCommandUpdate(0, 0, 'blank_tile', 'clear')
|
|
||||||
finalizeCommand()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!mapEditor.currentMap.value) return
|
if (!mapEditorStore.map?.tiles) return
|
||||||
const mapState = mapEditor.currentMap.value
|
|
||||||
|
|
||||||
placeTiles(props.tileMap, props.tileMapLayer, mapState.tiles)
|
tileMap.value = createTileMap()
|
||||||
|
tileLayer.value = await createTileLayer(tileMap.value)
|
||||||
|
|
||||||
|
// First fill the entire map with blank tiles using current map dimensions
|
||||||
|
const blankTiles = createTileArray(mapEditorStore.map.width, mapEditorStore.map.height, 'blank_tile')
|
||||||
|
|
||||||
|
// Then overlay the map tiles, but only within the current map dimensions
|
||||||
|
const mapTiles = mapEditorStore.map.tiles
|
||||||
|
for (let y = 0; y < mapEditorStore.map.height; y++) {
|
||||||
|
for (let x = 0; x < mapEditorStore.map.width; x++) {
|
||||||
|
if (mapTiles[y] && mapTiles[y][x] !== undefined) {
|
||||||
|
blankTiles[y][x] = mapTiles[y][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
|
||||||
|
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, tilePicker)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
|
||||||
|
|
||||||
|
if (tileMap.value) {
|
||||||
|
tileMap.value.destroyLayer('tiles')
|
||||||
|
tileMap.value.removeAllLayers()
|
||||||
|
tileMap.value.destroy()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<Image v-if="gameStore.isTextureLoaded(props.placedMapObject.mapObject.id)" v-bind="imageProps" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PlacedMapObject, TextureData } from '@/application/types'
|
||||||
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { Image, useScene } from 'phavuer'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
|
placedMapObject: PlacedMapObject
|
||||||
|
selectedPlacedMapObject: PlacedMapObject | null
|
||||||
|
movingPlacedMapObject: PlacedMapObject | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const imageProps = computed(() => ({
|
||||||
|
alpha: props.movingPlacedMapObject?.id === props.placedMapObject.id ? 0.5 : 1,
|
||||||
|
tint: props.selectedPlacedMapObject?.id === props.placedMapObject.id ? 0x00ff00 : 0xffffff,
|
||||||
|
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, props.placedMapObject.mapObject.frameWidth, props.placedMapObject.mapObject.frameHeight),
|
||||||
|
x: tileToWorldX(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
|
||||||
|
y: tileToWorldY(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
|
||||||
|
flipX: props.placedMapObject.isRotated,
|
||||||
|
texture: props.placedMapObject.mapObject.id,
|
||||||
|
originY: Number(props.placedMapObject.mapObject.originX),
|
||||||
|
originX: Number(props.placedMapObject.mapObject.originY)
|
||||||
|
}))
|
||||||
|
|
||||||
|
loadTexture(scene, {
|
||||||
|
key: props.placedMapObject.mapObject.id,
|
||||||
|
data: '/textures/map_objects/' + props.placedMapObject.mapObject.id + '.png',
|
||||||
|
group: 'map_objects',
|
||||||
|
updatedAt: props.placedMapObject.mapObject.updatedAt,
|
||||||
|
frameWidth: props.placedMapObject.mapObject.frameWidth,
|
||||||
|
frameHeight: props.placedMapObject.mapObject.frameHeight
|
||||||
|
} as TextureData).catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,201 +1,246 @@
|
|||||||
<template>
|
<template>
|
||||||
<PlacedMapObject
|
<SelectedPlacedMapObjectComponent v-if="selectedPlacedMapObject" :placedMapObject="selectedPlacedMapObject" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
|
||||||
v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object' && mapEditor.isPlacedMapObjectPreviewEnabled.value && mapEditor.selectedMapObject.value && previewPlacedMapObject"
|
<PlacedMapObject v-for="placedMapObject in mapEditorStore.map?.placedMapObjects" :tilemap="tilemap" :placedMapObject :selectedPlacedMapObject :movingPlacedMapObject @pointerup="clickPlacedMapObject(placedMapObject)" />
|
||||||
:tileMap
|
|
||||||
:tileMapLayer
|
|
||||||
:key="previewPlacedMapObject?.id"
|
|
||||||
:placedMapObject="previewPlacedMapObject as PlacedMapObjectT"
|
|
||||||
/>
|
|
||||||
<SelectedPlacedMapObjectComponent v-if="mapEditor.selectedPlacedObject.value" :key="mapEditor.selectedPlacedObject.value.id" :map :placedMapObject="mapEditor.selectedPlacedObject.value" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
|
|
||||||
<PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap :tileMapLayer :placedMapObject @pointerdown="clickPlacedMapObject(placedMapObject)" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { MapObject, Map as MapT, PlacedMapObject as PlacedMapObjectT } from '@/application/types'
|
import type { PlacedMapObject as PlacedMapObjectT } from '@/application/types'
|
||||||
import { uuidv4 } from '@/application/utilities'
|
import { uuidv4 } from '@/application/utilities'
|
||||||
import PlacedMapObject from '@/components/game/map/partials/PlacedMapObject.vue'
|
import PlacedMapObject from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObject.vue'
|
||||||
import SelectedPlacedMapObjectComponent from '@/components/gameMaster/mapEditor/partials/SelectedPlacedMapObject.vue'
|
import SelectedPlacedMapObjectComponent from '@/components/gameMaster/mapEditor/partials/SelectedPlacedMapObject.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { getTile } from '@/composables/mapComposable'
|
||||||
import { getTile } from '@/services/mapService'
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import Tilemap = Phaser.Tilemaps.Tilemap
|
|
||||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
|
||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditorStore = useMapEditorStore()
|
||||||
const map = computed(() => mapEditor.currentMap.value!)
|
const selectedPlacedMapObject = ref<PlacedMapObjectT | null>(null)
|
||||||
|
const movingPlacedMapObject = ref<PlacedMapObjectT | null>(null)
|
||||||
const emit = defineEmits<{ (e: 'update', map: MapT): void; (e: 'updateAndCommit', map: MapT): void; (e: 'pauseObjectTracking'): void; (e: 'resumeObjectTracking'): void }>()
|
|
||||||
|
|
||||||
defineExpose({ handlePointer })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tileMap: Tilemap
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
tileMapLayer: TilemapLayer
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const previewPosition = ref({ x: 0, y: 0 })
|
function pencil(pointer: Phaser.Input.Pointer) {
|
||||||
const previewPlacedMapObject = computed(() => ({
|
// Check if map is set
|
||||||
id: mapEditor.selectedMapObject.value!.id,
|
if (!mapEditorStore.map) return
|
||||||
mapObject: mapEditor.selectedMapObject.value!.id,
|
|
||||||
isRotated: false,
|
|
||||||
positionX: previewPosition.value.x,
|
|
||||||
positionY: previewPosition.value.y
|
|
||||||
}))
|
|
||||||
|
|
||||||
function updatePreviewPosition(pointer: Phaser.Input.Pointer) {
|
// Check if tool is pencil
|
||||||
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
if (mapEditorStore.tool !== 'pencil') return
|
||||||
if (!tile || (previewPosition.value.x === tile.x && previewPosition.value.y === tile.y)) return
|
|
||||||
|
|
||||||
previewPosition.value = { x: tile.x, y: tile.y }
|
// Check if draw mode is map_object
|
||||||
}
|
if (mapEditorStore.drawMode !== 'map_object') return
|
||||||
|
|
||||||
function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
// Check if there is a selected object
|
||||||
emit('pauseObjectTracking')
|
if (!mapEditorStore.selectedMapObject) return
|
||||||
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed, this means we are selecting the object
|
||||||
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Check if there is a tile
|
||||||
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
|
|
||||||
// Check if object already exists on position
|
// Check if object already exists on position
|
||||||
const existingPlacedMapObject = findObjectByPointer(pointer, mapEditor.currentMap.value!)
|
const existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
|
||||||
if (existingPlacedMapObject) return
|
if (existingPlacedMapObject) return
|
||||||
|
|
||||||
if (!mapEditor.selectedMapObject.value) return
|
const newPlacedMapObject = {
|
||||||
|
|
||||||
const newPlacedMapObject: PlacedMapObjectT = {
|
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
mapObject: mapEditor.selectedMapObject.value.id,
|
map: mapEditorStore.map,
|
||||||
|
mapObject: mapEditorStore.selectedMapObject,
|
||||||
|
depth: 0,
|
||||||
isRotated: false,
|
isRotated: false,
|
||||||
positionX: tile.x,
|
positionX: tile.x,
|
||||||
positionY: tile.y
|
positionY: tile.y
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new object to mapObjects
|
// Add new object to mapObjects
|
||||||
mapEditor.selectedPlacedObject.value = newPlacedMapObject
|
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.concat(newPlacedMapObject as PlacedMapObjectT)
|
||||||
map.placedMapObjects.push(newPlacedMapObject)
|
|
||||||
|
|
||||||
emit('update', map)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function eraser(pointer: Phaser.Input.Pointer, map: MapT) {
|
function eraser(pointer: Phaser.Input.Pointer) {
|
||||||
emit('pauseObjectTracking')
|
// Check if map is set
|
||||||
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
// Check if object already exists on position
|
// Check if tool is eraser
|
||||||
const existingPlacedMapObject = findObjectByPointer(pointer, map)
|
if (mapEditorStore.tool !== 'eraser') return
|
||||||
if (!existingPlacedMapObject) return
|
|
||||||
|
|
||||||
// Remove existing object
|
// Check if draw mode is map_object
|
||||||
map.placedMapObjects = map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id)
|
if (mapEditorStore.eraserMode !== 'map_object') return
|
||||||
|
|
||||||
emit('update', map)
|
// Check if left mouse button is pressed
|
||||||
}
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
function findObjectByPointer(pointer: Phaser.Input.Pointer, map: MapT): PlacedMapObjectT | undefined {
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
if (pointer.event.shiftKey) return
|
||||||
if (!tile) return undefined
|
|
||||||
|
|
||||||
return map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
function objectPicker(pointer: Phaser.Input.Pointer, map: MapT) {
|
|
||||||
// Check if object already exists on position
|
|
||||||
const existingPlacedMapObject = findObjectByPointer(pointer, map)
|
|
||||||
if (!existingPlacedMapObject) return
|
|
||||||
|
|
||||||
// Select the object
|
|
||||||
mapEditor.setSelectedMapObject(existingPlacedMapObject.mapObject as MapObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveMapObject(id: string, map: MapT) {
|
|
||||||
mapEditor.movingPlacedObject.value = map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT
|
|
||||||
|
|
||||||
emit('pauseObjectTracking')
|
|
||||||
|
|
||||||
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
|
||||||
if (!mapEditor.movingPlacedObject.value) return
|
|
||||||
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
|
||||||
if (!tile) return
|
|
||||||
|
|
||||||
mapEditor.movingPlacedObject.value.positionX = tile.x
|
|
||||||
mapEditor.movingPlacedObject.value.positionY = tile.y
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
|
||||||
|
|
||||||
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
|
||||||
|
|
||||||
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
|
||||||
if (!tile) return
|
|
||||||
|
|
||||||
map.placedMapObjects.map((placed) => {
|
|
||||||
if (placed.id === id) {
|
|
||||||
placed.positionX = tile.x
|
|
||||||
placed.positionY = tile.y
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
mapEditor.movingPlacedObject.value = null
|
|
||||||
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
|
||||||
emit('resumeObjectTracking')
|
|
||||||
emit('updateAndCommit', map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotatePlacedMapObject(id: string, map: MapT) {
|
|
||||||
map.placedMapObjects.map((placed) => {
|
|
||||||
if (placed.id === id) {
|
|
||||||
placed.isRotated = !placed.isRotated
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
emit('updateAndCommit', map)
|
|
||||||
}
|
|
||||||
|
|
||||||
function deletePlacedMapObject(id: string, map: MapT) {
|
|
||||||
map.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id)
|
|
||||||
mapEditor.selectedPlacedObject.value = null
|
|
||||||
emit('updateAndCommit', map)
|
|
||||||
}
|
|
||||||
|
|
||||||
function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) {
|
|
||||||
mapEditor.selectedPlacedObject.value = placedMapObject
|
|
||||||
|
|
||||||
// If alt is pressed, select the object
|
|
||||||
if (scene.input.activePointer.event.altKey) {
|
|
||||||
mapEditor.setSelectedMapObject(placedMapObject.mapObject as MapObject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointer(pointer: Phaser.Input.Pointer) {
|
|
||||||
const map = mapEditor.currentMap.value
|
|
||||||
if (!map) return
|
|
||||||
|
|
||||||
// Check if alt is pressed, this means we are selecting the object
|
// Check if alt is pressed, this means we are selecting the object
|
||||||
if (pointer.event.altKey) return
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Check if there is a tile
|
||||||
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
// Check if object already exists on position
|
||||||
|
const existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
|
||||||
|
if (!existingPlacedMapObject) return
|
||||||
|
|
||||||
|
// Remove existing object
|
||||||
|
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectPicker(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if map is set
|
||||||
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
// Check if tool is pencil
|
// Check if tool is pencil
|
||||||
switch (mapEditor.tool.value) {
|
if (mapEditorStore.tool !== 'pencil') return
|
||||||
case 'pencil':
|
|
||||||
pencil(pointer, map)
|
// Check if draw mode is map_object
|
||||||
break
|
if (mapEditorStore.drawMode !== 'map_object') return
|
||||||
case 'eraser':
|
|
||||||
eraser(pointer, map)
|
// Check if left mouse button is pressed
|
||||||
break
|
if (!pointer.isDown) return
|
||||||
case 'object picker':
|
|
||||||
objectPicker(pointer, map)
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
break
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// If alt is not pressed, return
|
||||||
|
if (!pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Check if there is a tile
|
||||||
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
// Check if object already exists on position
|
||||||
|
const existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
|
||||||
|
if (!existingPlacedMapObject) return
|
||||||
|
|
||||||
|
// Select the object
|
||||||
|
mapEditorStore.setSelectedMapObject(existingPlacedMapObject.mapObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveMapObject(id: string) {
|
||||||
|
// Check if map is set
|
||||||
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
|
movingPlacedMapObject.value = mapEditorStore.map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT
|
||||||
|
|
||||||
|
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (!movingPlacedMapObject.value) return
|
||||||
|
|
||||||
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
movingPlacedMapObject.value.positionX = tile.x
|
||||||
|
movingPlacedMapObject.value.positionY = tile.y
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||||
|
|
||||||
|
function handlePointerUp() {
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||||
|
movingPlacedMapObject.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotatePlacedMapObject(id: string) {
|
||||||
|
// Check if map is set
|
||||||
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
|
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.map((placedMapObject) => {
|
||||||
|
if (placedMapObject.id === id) {
|
||||||
|
return {
|
||||||
|
...placedMapObject,
|
||||||
|
isRotated: !placedMapObject.isRotated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return placedMapObject
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function deletePlacedMapObject(id: string) {
|
||||||
|
// Check if map is set
|
||||||
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
|
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.filter((object) => object.id !== id)
|
||||||
|
selectedPlacedMapObject.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) {
|
||||||
|
selectedPlacedMapObject.value = placedMapObject
|
||||||
|
|
||||||
|
// If alt is pressed, select the object
|
||||||
|
if (scene.input.activePointer.event.altKey) {
|
||||||
|
mapEditorStore.setSelectedMapObject(placedMapObject.mapObject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, updatePreviewPosition)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, objectPicker)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, updatePreviewPosition)
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, objectPicker)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// watch mapEditorStore.mapObjectList and update originX and originY of objects in mapObjects
|
||||||
|
watch(
|
||||||
|
() => mapEditorStore.mapObjectList,
|
||||||
|
(newMapObjects) => {
|
||||||
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
|
const updatedMapObjects = mapEditorStore.map.placedMapObjects.map((mapObject) => {
|
||||||
|
const updatedMapObject = newMapObjects.find((obj) => obj.id === mapObject.mapObject.id)
|
||||||
|
if (updatedMapObject) {
|
||||||
|
return {
|
||||||
|
...mapObject,
|
||||||
|
mapObject: {
|
||||||
|
...mapObject.mapObject,
|
||||||
|
originX: updatedMapObject.originX,
|
||||||
|
originY: updatedMapObject.originY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mapObject
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the map with the new mapObjects
|
||||||
|
mapEditorStore.setMap({
|
||||||
|
...mapEditorStore.map,
|
||||||
|
placedMapObjects: updatedMapObjects
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update selectedMapObject if it's set
|
||||||
|
if (mapEditorStore.selectedMapObject) {
|
||||||
|
const updatedMapObject = newMapObjects.find((obj) => obj.id === mapEditorStore.selectedMapObject?.id)
|
||||||
|
if (updatedMapObject) {
|
||||||
|
mapEditorStore.setSelectedMapObject({
|
||||||
|
...mapEditorStore.selectedMapObject,
|
||||||
|
originX: updatedMapObject.originX,
|
||||||
|
originY: updatedMapObject.originY
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// { deep: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal ref="modalRef" :modal-width="300" :modal-height="420" :is-resizable="false" bg-style="none">
|
<Modal :isModalOpen="true" @modal:close="() => mapEditorStore.toggleCreateMapModal()" :modal-width="300" :modal-height="420" :is-resizable="false" :bg-style="'none'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Create new map</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Create new map</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
<div class="m-4">
|
<div class="m-4">
|
||||||
<form method="post" @submit.prevent="submit" class="inline">
|
<form method="post" @submit.prevent="submit" class="inline">
|
||||||
@ -13,15 +14,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="name">Width</label>
|
<label for="name">Width</label>
|
||||||
<input class="input-field max-w-64" v-model="width" name="width" id="width" type="number" />
|
<input class="input-field max-w-64" v-model="width" name="name" id="name" type="number" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="name">Height</label>
|
<label for="name">Height</label>
|
||||||
<input class="input-field max-w-64" v-model="height" name="height" id="height" type="number" />
|
<input class="input-field max-w-64" v-model="height" name="name" id="name" type="number" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">PVP enabled</label>
|
<label for="name">PVP enabled</label>
|
||||||
<select class="input-field" v-model="pvp" name="pvp" id="pvp">
|
<select class="input-field" name="pvp" id="pvp">
|
||||||
<option :value="false">No</option>
|
<option :value="false">No</option>
|
||||||
<option :value="true">Yes</option>
|
<option :value="true">Yes</option>
|
||||||
</select>
|
</select>
|
||||||
@ -35,48 +36,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import type { Map } from '@/application/types'
|
import type { Map } from '@/application/types'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { MapStorage } from '@/storage/storages'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { ref, useTemplateRef } from 'vue'
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
|
import { ref } from 'vue'
|
||||||
const emit = defineEmits(['create'])
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const mapStorage = new MapStorage()
|
const mapEditorStore = useMapEditorStore()
|
||||||
const modalRef = useTemplateRef('modalRef')
|
|
||||||
|
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
const width = ref(0)
|
const width = ref(0)
|
||||||
const height = ref(0)
|
const height = ref(0)
|
||||||
const pvp = ref(false)
|
|
||||||
|
|
||||||
defineExpose({ open: () => modalRef.value?.open() })
|
function submit() {
|
||||||
|
gameStore.connection?.emit('gm:map:create', { name: name.value, width: width.value, height: height.value }, (response: Map[]) => {
|
||||||
async function submit() {
|
mapEditorStore.setMapList(response)
|
||||||
socketManager.emit(SocketEvent.GM_MAP_CREATE, { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => {
|
|
||||||
if (!response) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset form
|
|
||||||
name.value = ''
|
|
||||||
width.value = 0
|
|
||||||
height.value = 0
|
|
||||||
pvp.value = false
|
|
||||||
|
|
||||||
// Add map to storage
|
|
||||||
await mapStorage.add(response)
|
|
||||||
|
|
||||||
// Let list know to fetch new maps
|
|
||||||
emit('create')
|
|
||||||
})
|
})
|
||||||
|
mapEditorStore.toggleCreateMapModal()
|
||||||
// Close modal
|
|
||||||
modalRef.value?.close()
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal ref="modalRef" :is-resizable="false" :modal-width="300" :modal-height="360" bg-style="none">
|
<CreateMap v-if="mapEditorStore.isCreateMapModalShown" />
|
||||||
|
<Modal :is-modal-open="mapEditorStore.isMapListModalShown" @modal:close="() => mapEditorStore.toggleMapListModal()" :is-resizable="false" :modal-width="300" :modal-height="360" :bg-style="'none'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg text-white">Maps</h3>
|
<h3 class="text-lg text-white">Maps</h3>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
<div class="my-4 mx-auto h-full">
|
<div class="my-4 mx-auto">
|
||||||
<div class="text-center mb-4 px-2 flex gap-2.5">
|
<div class="text-center mb-4 px-2 flex gap-2.5">
|
||||||
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchMaps">Refresh</button>
|
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchMaps">Refresh</button>
|
||||||
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="createMapModal?.open">New</button>
|
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="() => mapEditorStore.toggleCreateMapModal()">New</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-y-auto h-[calc(100%-20px)]">
|
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(map, index) in mapEditorStore.mapList" :key="map.id">
|
||||||
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(map, index) in mapList" :key="map.id">
|
|
||||||
<div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div>
|
<div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div>
|
||||||
<div class="flex gap-3 items-center w-full" @click="() => loadMap(map.id)">
|
<div class="flex gap-3 items-center w-full" @click="() => loadMap(map.id)">
|
||||||
<span>{{ map.name }}</span>
|
<span>{{ map.name }}</span>
|
||||||
@ -21,65 +21,41 @@
|
|||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<CreateMap ref="createMapModal" @create="fetchMaps" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import type { Map, UUID } from '@/application/types'
|
||||||
import type { Map } from '@/application/types'
|
|
||||||
import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue'
|
import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { MapStorage } from '@/storage/storages'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onMounted, ref, useTemplateRef } from 'vue'
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
const mapEditorStore = useMapEditorStore()
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
const mapStorage = new MapStorage()
|
|
||||||
const mapList = ref<Map[]>([])
|
|
||||||
const modalRef = useTemplateRef('modalRef')
|
|
||||||
const createMapModal = useTemplateRef('createMapModal')
|
|
||||||
|
|
||||||
defineEmits(['open-create-map'])
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open: () => modalRef.value?.open()
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchMaps()
|
fetchMaps()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function fetchMaps() {
|
function fetchMaps() {
|
||||||
mapList.value = await mapStorage.getAll()
|
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => {
|
||||||
|
mapEditorStore.setMapList(response)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadMap(id: string) {
|
function loadMap(id: UUID) {
|
||||||
socketManager.emit(SocketEvent.GM_MAP_REQUEST, { mapId: id }, (response: Map) => {
|
gameStore.connection?.emit('gm:map:request', { mapId: id }, (response: Map) => {
|
||||||
mapEditor.loadMap(response)
|
mapEditorStore.setMap(response)
|
||||||
})
|
})
|
||||||
modalRef.value?.close()
|
mapEditorStore.toggleMapListModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteMap(id: string) {
|
function deleteMap(id: UUID) {
|
||||||
socketManager.emit(SocketEvent.GM_MAP_DELETE, { mapId: id }, async (response: boolean) => {
|
gameStore.connection?.emit('gm:map:delete', { mapId: id }, () => {
|
||||||
if (!response) {
|
fetchMaps()
|
||||||
gameStore.addNotification({
|
|
||||||
title: 'Error',
|
|
||||||
message: 'Failed to delete map.'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await mapStorage.delete(id)
|
|
||||||
await fetchMaps()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,70 +1,72 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object'">
|
<Modal :isModalOpen="mapEditorStore.isMapObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (mapEditorStore.isMapObjectListModalShown = false)" :bg-style="'none'">
|
||||||
<div class="flex flex-col gap-2.5 p-2.5">
|
<template #modalHeader>
|
||||||
<div class="flex justify-between items-center">
|
<h3 class="text-lg text-white">Map objects</h3>
|
||||||
<div class="flex-grow">
|
</template>
|
||||||
<div class="relative flex">
|
<template #modalBody>
|
||||||
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
<div class="flex pt-4 pl-4">
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
|
<div class="w-full flex gap-1.5 flex-row">
|
||||||
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
<div>
|
||||||
|
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
||||||
|
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img src="/assets/icons/mapEditor/dropdown-chevron.svg" class="w-12 h-12 ml-2 cursor-pointer hover:opacity-80 -rotate-90" alt="Close" @click="mapEditor.setTool('move')" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex flex-col h-full p-4">
|
||||||
<select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
|
<div class="mb-4 flex flex-wrap gap-2">
|
||||||
<option value="tile">Tiles</option>
|
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
|
||||||
<option value="map_object">Objects</option>
|
{{ tag }}
|
||||||
</select>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5">
|
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="flex justify-between flex-wrap gap-2.5 items-center">
|
<div class="flex justify-between flex-wrap gap-2.5 items-center">
|
||||||
<div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block">
|
<div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block">
|
||||||
<img
|
<img
|
||||||
class="border-2 border-solid rounded max-w-full"
|
class="border-2 border-solid max-w-full"
|
||||||
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
|
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
|
||||||
alt="Object"
|
alt="Object"
|
||||||
@click="mapEditor.setSelectedMapObject(mapObject)"
|
@click="mapEditorStore.setSelectedMapObject(mapObject)"
|
||||||
:class="{
|
:class="{
|
||||||
'cursor-pointer transition-all duration-300': true,
|
'cursor-pointer transition-all duration-300': true,
|
||||||
'border-cyan shadow-lg': mapEditor.selectedMapObject.value?.id === mapObject.id,
|
'border-cyan shadow-lg scale-105': mapEditorStore.selectedMapObject?.id === mapObject.id,
|
||||||
'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id
|
'border-transparent hover:border-gray-300': mapEditorStore.selectedMapObject?.id !== mapObject.id
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500">
|
</template>
|
||||||
<span>Tags:</span>
|
</Modal>
|
||||||
<div class="flex grow items-center flex-wrap gap-1.5 overflow-auto">
|
|
||||||
<span class="m-auto">No tags selected</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import type { MapObject } from '@/application/types'
|
import type { MapObject } from '@/application/types'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { MapObjectStorage } from '@/storage/storages'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { liveQuery } from 'dexie'
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const mapObjectStorage = new MapObjectStorage()
|
const gameStore = useGameStore()
|
||||||
const mapEditor = useMapEditorComposable()
|
const isModalOpen = ref(false)
|
||||||
|
const mapEditorStore = useMapEditorStore()
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const selectedTags = ref<string[]>([])
|
const selectedTags = ref<string[]>([])
|
||||||
const mapObjectList = ref<MapObject[]>([])
|
|
||||||
|
|
||||||
const uniqueTags = computed(() => {
|
const uniqueTags = computed(() => {
|
||||||
const allTags = mapObjectList.value.flatMap((obj) => obj.tags || [])
|
const allTags = mapEditorStore.mapObjectList.flatMap((obj) => obj.tags || [])
|
||||||
return Array.from(new Set(allTags))
|
return Array.from(new Set(allTags))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const filteredMapObjects = computed(() => {
|
||||||
|
return mapEditorStore.mapObjectList.filter((object) => {
|
||||||
|
const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
|
const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
|
||||||
|
return matchesSearch && matchesTags
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const toggleTag = (tag: string) => {
|
const toggleTag = (tag: string) => {
|
||||||
if (selectedTags.value.includes(tag)) {
|
if (selectedTags.value.includes(tag)) {
|
||||||
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
||||||
@ -73,29 +75,10 @@ const toggleTag = (tag: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredMapObjects = computed(() => {
|
onMounted(async () => {
|
||||||
return mapObjectList.value.filter((object) => {
|
isModalOpen.value = true
|
||||||
const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
|
||||||
const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
|
mapEditorStore.setMapObjectList(response)
|
||||||
return matchesSearch && matchesTags
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
let subscription: any = null
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({
|
|
||||||
next: (result) => {
|
|
||||||
mapObjectList.value = result
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Failed to fetch tiles:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (!subscription) return
|
|
||||||
subscription.unsubscribe()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal ref="modalRef" :modal-width="600" :modal-height="430" bg-style="none">
|
<Modal :is-modal-open="mapEditorStore.isSettingsModalShown" @modal:close="() => mapEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="430" :bg-style="'none'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Map settings</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Map settings</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -14,19 +14,22 @@
|
|||||||
<div class="gap-2.5 flex flex-wrap mt-4">
|
<div class="gap-2.5 flex flex-wrap mt-4">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input class="input-field" v-model="name" @input="updateValue" name="name" id="name" />
|
<input class="input-field" v-model="name" name="name" id="name" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="width">Width</label>
|
<label for="width">Width</label>
|
||||||
<input class="input-field" v-model="width" @input="updateValue" name="width" id="width" type="number" />
|
<input class="input-field" v-model="width" name="width" id="width" type="number" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="height">Height</label>
|
<label for="height">Height</label>
|
||||||
<input class="input-field" v-model="height" @input="updateValue" name="height" id="height" type="number" />
|
<input class="input-field" v-model="height" name="height" id="height" type="number" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="form-field-full">
|
||||||
<label class="mr-4" for="pvp">PVP enabled</label>
|
<label for="pvp">PVP enabled</label>
|
||||||
<input type="checkbox" v-model="pvp" @input="updateValue" class="input-field scale-125" name="pvp" id="pvp" />
|
<select v-model="pvp" class="input-field" name="pvp" id="pvp">
|
||||||
|
<option :value="false">No</option>
|
||||||
|
<option :value="true">Yes</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -44,55 +47,60 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { UUID } from '@/application/types'
|
|
||||||
import { uuidv4 } from '@/application/utilities'
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
import { onMounted, ref, useTemplateRef, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditorStore = useMapEditorStore()
|
||||||
const screen = ref('settings')
|
const screen = ref('settings')
|
||||||
|
|
||||||
const name = ref<string | undefined>('Map')
|
mapEditorStore.setMapName(mapEditorStore.map?.name)
|
||||||
const width = ref<number>(0)
|
mapEditorStore.setMapWidth(mapEditorStore.map?.width)
|
||||||
const height = ref<number>(0)
|
mapEditorStore.setMapHeight(mapEditorStore.map?.height)
|
||||||
const pvp = ref<boolean>(false)
|
mapEditorStore.setMapPvp(mapEditorStore.map?.pvp)
|
||||||
const mapEffects = ref(mapEditor.currentMap.value?.mapEffects || [])
|
mapEditorStore.setMapEffects(mapEditorStore.map?.mapEffects)
|
||||||
const modalRef = useTemplateRef('modalRef')
|
|
||||||
|
|
||||||
defineExpose({
|
const name = ref(mapEditorStore.mapSettings?.name)
|
||||||
open: () => modalRef.value?.open()
|
const width = ref(mapEditorStore.mapSettings?.width)
|
||||||
|
const height = ref(mapEditorStore.mapSettings?.height)
|
||||||
|
const pvp = ref(mapEditorStore.mapSettings?.pvp)
|
||||||
|
const mapEffects = ref(mapEditorStore.mapSettings?.mapEffects || [])
|
||||||
|
|
||||||
|
watch(name, (value) => {
|
||||||
|
mapEditorStore.setMapName(value)
|
||||||
})
|
})
|
||||||
|
|
||||||
function updateValue(event: Event) {
|
watch(width, (value) => {
|
||||||
let ev = event.target as HTMLInputElement
|
mapEditorStore.setMapWidth(value)
|
||||||
const value = ev.name === 'pvp' ? (ev.checked ? 1 : 0) : ev.value
|
})
|
||||||
mapEditor.updateProperty(ev.name as 'name' | 'width' | 'height' | 'pvp' | 'mapEffects', value)
|
|
||||||
}
|
watch(height, (value) => {
|
||||||
|
mapEditorStore.setMapHeight(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(pvp, (value) => {
|
||||||
|
mapEditorStore.setMapPvp(value)
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => mapEditor.currentMap.value,
|
mapEffects,
|
||||||
(map) => {
|
(value) => {
|
||||||
if (!map) return
|
mapEditorStore.setMapEffects(value)
|
||||||
name.value = map.name
|
},
|
||||||
width.value = map.width
|
{ deep: true }
|
||||||
height.value = map.height
|
|
||||||
pvp.value = map.pvp
|
|
||||||
mapEffects.value = map.mapEffects
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const addEffect = () => {
|
const addEffect = () => {
|
||||||
mapEffects.value.push({
|
mapEffects.value.push({
|
||||||
id: uuidv4(),
|
id: Date.now().toString(), // Simple unique id generation
|
||||||
|
mapId: mapEditorStore.map?.id,
|
||||||
|
map: mapEditorStore.map,
|
||||||
effect: '',
|
effect: '',
|
||||||
strength: 1
|
strength: 1
|
||||||
})
|
})
|
||||||
mapEditor.updateProperty('mapEffects', mapEffects.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeEffect = (index: number) => {
|
const removeEffect = (index) => {
|
||||||
mapEffects.value.splice(index, 1)
|
mapEffects.value.splice(index, 1)
|
||||||
mapEditor.updateProperty('mapEffects', mapEffects.value)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,115 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center px-5 py-1 fixed bottom-20 left-0 z-20">
|
<div class="flex flex-col items-center py-5 px-3 fixed bottom-14 right-0">
|
||||||
<div class="flex h-10 gap-2">
|
<div class="self-end mt-2 flex gap-2">
|
||||||
<button @mousedown.stop @click="handleDelete" class="btn-red !py-3 px-4">
|
<button @mousedown.stop @click="handleDelete" class="btn-red py-1.5 px-4">
|
||||||
<img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" />
|
<img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" />
|
||||||
</button>
|
</button>
|
||||||
<button @mousedown.stop @click="showMapObjectSettings = !showMapObjectSettings" class="btn-indigo !py-3 px-4">
|
|
||||||
<img src="/assets/icons/mapEditor/gear.svg" class="w-4 h-4 invert" alt="Delete" />
|
|
||||||
</button>
|
|
||||||
<button @mousedown.stop @click="handleRotate" class="btn-cyan py-1.5 px-4">Rotate</button>
|
<button @mousedown.stop @click="handleRotate" class="btn-cyan py-1.5 px-4">Rotate</button>
|
||||||
<button @mousedown.stop @click="handleMove" class="btn-cyan py-1.5 px-4 min-w-24">Move</button>
|
<button @mousedown.stop @click="handleMove" class="btn-cyan py-1.5 px-4 min-w-24">Move</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal :is-modal-open="showMapObjectSettings" @modal:close="showMapObjectSettings = false" :modal-height="320" bg-style="none">
|
|
||||||
<template #modalHeader>
|
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Map object settings</h3>
|
|
||||||
</template>
|
|
||||||
<template #modalBody>
|
|
||||||
<div class="m-4">
|
|
||||||
<form method="post" @submit.prevent="" class="inline">
|
|
||||||
<div class="gap-2.5 flex flex-wrap">
|
|
||||||
<div class="form-field-full">
|
|
||||||
<label for="name">Name</label>
|
|
||||||
<input class="input-field" v-model="mapObjectName" name="name" id="name" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="originX">Origin X</label>
|
|
||||||
<input class="input-field" v-model="mapObjectOriginX" name="originX" id="originX" type="number" min="0.0" step="0.01" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="originY">Origin Y</label>
|
|
||||||
<input class="input-field" v-model="mapObjectOriginY" name="originY" id="originY" type="number" min="0.0" step="0.01" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" @click="handleUpdate">Save</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
import type { PlacedMapObject } from '@/application/types'
|
||||||
import type { MapObject, Map as MapT, PlacedMapObject } from '@/application/types'
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { MapObjectStorage } from '@/storage/storages'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
placedMapObject: PlacedMapObject
|
placedMapObject: PlacedMapObject
|
||||||
map: MapT
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(['move', 'rotate', 'delete'])
|
const emit = defineEmits(['move', 'rotate', 'delete'])
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
|
|
||||||
const mapObjectStorage = new MapObjectStorage()
|
|
||||||
const mapObject = ref<MapObject | null>(null)
|
|
||||||
const showMapObjectSettings = ref(false)
|
|
||||||
const mapObjectName = ref('')
|
|
||||||
const mapObjectOriginX = ref(0)
|
|
||||||
const mapObjectOriginY = ref(0)
|
|
||||||
|
|
||||||
const handleMove = () => {
|
const handleMove = () => {
|
||||||
emit('move', props.placedMapObject.id, props.map)
|
emit('move', props.placedMapObject.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRotate = () => {
|
const handleRotate = () => {
|
||||||
emit('rotate', props.placedMapObject.id, props.map)
|
emit('rotate', props.placedMapObject.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
emit('delete', props.placedMapObject.id, props.map)
|
emit('delete', props.placedMapObject.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUpdate() {
|
|
||||||
if (!mapObject.value) return
|
|
||||||
|
|
||||||
socketManager.emit(
|
|
||||||
SocketEvent.GM_MAPOBJECT_UPDATE,
|
|
||||||
{
|
|
||||||
id: props.placedMapObject.mapObject as string,
|
|
||||||
name: mapObjectName.value,
|
|
||||||
originX: mapObjectOriginX.value,
|
|
||||||
originY: mapObjectOriginY.value
|
|
||||||
},
|
|
||||||
async (response: boolean) => {
|
|
||||||
if (!response) return
|
|
||||||
await mapObjectStorage.update(mapObject.value!.id, {
|
|
||||||
name: mapObjectName.value,
|
|
||||||
originX: mapObjectOriginX.value,
|
|
||||||
originY: mapObjectOriginY.value
|
|
||||||
})
|
|
||||||
mapEditor.triggerMapObjectRefresh()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (!props.placedMapObject.mapObject) return
|
|
||||||
|
|
||||||
mapObject.value = await mapObjectStorage.getById(props.placedMapObject.mapObject as string)
|
|
||||||
if (!mapObject.value) return
|
|
||||||
|
|
||||||
mapObjectName.value = mapObject.value.name
|
|
||||||
mapObjectOriginX.value = mapObject.value.originX
|
|
||||||
mapObjectOriginY.value = mapObject.value.originY
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal v-if="showTeleportModal" ref="modalRef" @modal:close="() => mapEditor.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" bg-style="none">
|
<Modal :is-modal-open="showTeleportModal" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
<label for="toMap">Map to teleport to</label>
|
<label for="toMap">Map to teleport to</label>
|
||||||
<select v-model="toMap" class="input-field" name="toMap" id="toMap">
|
<select v-model="toMap" class="input-field" name="toMap" id="toMap">
|
||||||
<option :value="null">Select map</option>
|
<option :value="null">Select map</option>
|
||||||
<option v-for="map in mapList" :key="map.id" :value="map.id">{{ map.name }}</option>
|
<option v-for="map in mapEditorStore.mapList" :key="map.id" :value="map">{{ map.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -41,24 +41,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Map } from '@/application/types'
|
import type { Map } from '@/application/types'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { MapStorage } from '@/storage/storages'
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const showTeleportModal = computed(() => mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'teleport')
|
const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport')
|
||||||
const mapStorage = new MapStorage()
|
const mapEditorStore = useMapEditorStore()
|
||||||
const mapEditor = useMapEditorComposable()
|
const gameStore = useGameStore()
|
||||||
const modalRef = useTemplateRef('modalRef')
|
|
||||||
const mapList = ref<Map[]>([])
|
|
||||||
|
|
||||||
defineExpose({
|
onMounted(fetchMaps)
|
||||||
open: () => modalRef.value?.open()
|
|
||||||
})
|
function fetchMaps() {
|
||||||
|
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => {
|
||||||
|
mapEditorStore.setMapList(response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings()
|
const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings()
|
||||||
|
|
||||||
function useRefTeleportSettings() {
|
function useRefTeleportSettings() {
|
||||||
const settings = mapEditor.teleportSettings.value
|
const settings = mapEditorStore.teleportSettings
|
||||||
return {
|
return {
|
||||||
toPositionX: ref(settings.toPositionX),
|
toPositionX: ref(settings.toPositionX),
|
||||||
toPositionY: ref(settings.toPositionY),
|
toPositionY: ref(settings.toPositionY),
|
||||||
@ -70,19 +72,11 @@ function useRefTeleportSettings() {
|
|||||||
watch([toPositionX, toPositionY, toRotation, toMap], updateTeleportSettings)
|
watch([toPositionX, toPositionY, toRotation, toMap], updateTeleportSettings)
|
||||||
|
|
||||||
function updateTeleportSettings() {
|
function updateTeleportSettings() {
|
||||||
mapEditor.setTeleportSettings({
|
mapEditorStore.setTeleportSettings({
|
||||||
toPositionX: toPositionX.value,
|
toPositionX: toPositionX.value,
|
||||||
toPositionY: toPositionY.value,
|
toPositionY: toPositionY.value,
|
||||||
toRotation: toRotation.value,
|
toRotation: toRotation.value,
|
||||||
toMap: toMap.value
|
toMap: toMap.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMaps() {
|
|
||||||
mapList.value = await mapStorage.getAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await fetchMaps()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,35 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="(mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'tile') || mapEditor.tool.value === 'paint'">
|
<Modal :isModalOpen="mapEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (mapEditorStore.isTileListModalShown = false)" :bg-style="'none'">
|
||||||
<div class="flex flex-col gap-2.5 p-2.5">
|
<template #modalHeader>
|
||||||
<div class="flex justify-between items-center">
|
<h3 class="text-lg text-white">Tiles</h3>
|
||||||
<div class="flex-grow">
|
</template>
|
||||||
<div class="relative flex">
|
<template #modalBody>
|
||||||
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
<div class="h-full overflow-auto" v-if="!selectedGroup">
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
|
<div class="flex pt-4 pl-4">
|
||||||
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
<div class="w-full flex gap-1.5 flex-row">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
||||||
|
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img src="/assets/icons/mapEditor/dropdown-chevron.svg" class="w-12 h-12 ml-2 cursor-pointer hover:opacity-80 -rotate-90" alt="Close" @click="mapEditor.setTool('move')" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex flex-col h-full p-4">
|
||||||
<select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
|
<div class="mb-4 flex flex-wrap gap-2">
|
||||||
<option value="tile">Tiles</option>
|
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
|
||||||
<option value="map_object">Objects</option>
|
{{ tag }}
|
||||||
</select>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="h-[calc(100%_-_60px)] flex-grow overflow-y-auto">
|
||||||
<div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5">
|
<div class="grid grid-cols-8 gap-2 justify-items-center">
|
||||||
<div class="h-full" v-if="!selectedGroup">
|
|
||||||
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
|
||||||
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
|
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
|
||||||
<img
|
<img
|
||||||
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
|
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
|
||||||
:alt="group.parent.name"
|
:alt="group.parent.name"
|
||||||
@click="openGroup(group)"
|
@click="openGroup(group)"
|
||||||
@load="() => tileProcessor.processTile(group.parent)"
|
@load="() => processTile(group.parent)"
|
||||||
:class="{
|
:class="{
|
||||||
'border-cyan shadow-lg': isActiveTile(group.parent),
|
'border-cyan shadow-lg scale-105': isActiveTile(group.parent),
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@ -40,20 +40,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else class="h-full overflow-auto">
|
<div v-else class="h-full overflow-auto">
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div class="text-center mb-8">
|
<button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button>
|
||||||
<button @click="closeGroup" class="hover:text-white">Back to all tiles</button>
|
<h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4>
|
||||||
</div>
|
<div class="grid grid-cols-8 gap-2 justify-items-center">
|
||||||
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
|
||||||
<div class="flex flex-col items-center justify-center">
|
<div class="flex flex-col items-center justify-center">
|
||||||
<img
|
<img
|
||||||
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`"
|
:src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`"
|
||||||
:alt="selectedGroup.parent.name"
|
:alt="selectedGroup.parent.name"
|
||||||
@click="selectTile(selectedGroup.parent.id)"
|
@click="selectTile(selectedGroup.parent.id)"
|
||||||
:class="{
|
:class="{
|
||||||
'border-cyan shadow-lg': isActiveTile(selectedGroup.parent),
|
'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent),
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@ -61,12 +62,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
|
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
|
||||||
<img
|
<img
|
||||||
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`"
|
:src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`"
|
||||||
:alt="childTile.name"
|
:alt="childTile.name"
|
||||||
@click="selectTile(childTile.id)"
|
@click="selectTile(childTile.id)"
|
||||||
:class="{
|
:class="{
|
||||||
'border-cyan shadow-lg': isActiveTile(childTile),
|
'border-cyan shadow-lg scale-105': isActiveTile(childTile),
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@ -75,48 +76,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500">
|
</Modal>
|
||||||
<span>Tags:</span>
|
|
||||||
<div class="flex grow items-center flex-wrap gap-1.5 overflow-auto">
|
|
||||||
<span class="m-auto">No tags selected</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import type { Tile } from '@/application/types'
|
import type { Tile } from '@/application/types'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useTileProcessingComposable } from '@/composables/useTileProcessingComposable'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { TileStorage } from '@/storage/storages'
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const tileStorage = new TileStorage()
|
const gameStore = useGameStore()
|
||||||
const mapEditor = useMapEditorComposable()
|
const isModalOpen = ref(false)
|
||||||
const tileProcessor = useTileProcessingComposable()
|
const mapEditorStore = useMapEditorStore()
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const selectedTags = ref<string[]>([])
|
const selectedTags = ref<string[]>([])
|
||||||
const tileCategories = ref<Map<string, string>>(new Map())
|
const tileCategories = ref<Map<string, string>>(new Map())
|
||||||
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
|
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
|
||||||
const tiles = ref<Tile[]>([])
|
|
||||||
|
|
||||||
const uniqueTags = computed(() => {
|
const uniqueTags = computed(() => {
|
||||||
const allTags = tiles.value.flatMap((tile) => tile.tags || [])
|
const allTags = mapEditorStore.tileList.flatMap((tile) => tile.tags || [])
|
||||||
return Array.from(new Set(allTags))
|
return Array.from(new Set(allTags))
|
||||||
})
|
})
|
||||||
|
|
||||||
const groupedTiles = computed(() => {
|
const groupedTiles = computed(() => {
|
||||||
const groups: { parent: Tile; children: Tile[] }[] = []
|
const groups: { parent: Tile; children: Tile[] }[] = []
|
||||||
const filteredTiles = tiles.value.filter((tile) => {
|
const filteredTiles = mapEditorStore.tileList.filter((tile) => {
|
||||||
const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag)))
|
const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag)))
|
||||||
return matchesSearch && matchesTags
|
return matchesSearch && matchesTags
|
||||||
})
|
})
|
||||||
|
|
||||||
filteredTiles.forEach((tile) => {
|
filteredTiles.forEach((tile) => {
|
||||||
const parentGroup = groups.find((group) => tileProcessor.areTilesRelated(group.parent, tile))
|
const parentGroup = groups.find((group) => areTilesRelated(group.parent, tile))
|
||||||
if (parentGroup && parentGroup.parent.id !== tile.id) {
|
if (parentGroup && parentGroup.parent.id !== tile.id) {
|
||||||
parentGroup.children.push(tile)
|
parentGroup.children.push(tile)
|
||||||
} else {
|
} else {
|
||||||
@ -127,6 +121,32 @@ const groupedTiles = computed(() => {
|
|||||||
return groups
|
return groups
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const tileColorData = ref<Map<string, { r: number; g: number; b: number }>>(new Map())
|
||||||
|
const tileEdgeData = ref<Map<string, number>>(new Map())
|
||||||
|
|
||||||
|
function areTilesRelated(tile1: Tile, tile2: Tile): boolean {
|
||||||
|
const colorSimilarityThreshold = 30 // Adjust this value as needed
|
||||||
|
const edgeComplexitySimilarityThreshold = 20 // Adjust this value as needed
|
||||||
|
|
||||||
|
const color1 = tileColorData.value.get(tile1.id)
|
||||||
|
const color2 = tileColorData.value.get(tile2.id)
|
||||||
|
const edge1 = tileEdgeData.value.get(tile1.id)
|
||||||
|
const edge2 = tileEdgeData.value.get(tile2.id)
|
||||||
|
|
||||||
|
if (!color1 || !color2 || edge1 === undefined || edge2 === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorDifference = Math.sqrt(Math.pow(color1.r - color2.r, 2) + Math.pow(color1.g - color2.g, 2) + Math.pow(color1.b - color2.b, 2))
|
||||||
|
|
||||||
|
const edgeComplexityDifference = Math.abs(edge1 - edge2)
|
||||||
|
|
||||||
|
const namePrefix1 = tile1.name.split('_')[0]
|
||||||
|
const namePrefix2 = tile2.name.split('_')[0]
|
||||||
|
|
||||||
|
return colorDifference <= colorSimilarityThreshold && edgeComplexityDifference <= edgeComplexitySimilarityThreshold && namePrefix1 === namePrefix2
|
||||||
|
}
|
||||||
|
|
||||||
const toggleTag = (tag: string) => {
|
const toggleTag = (tag: string) => {
|
||||||
if (selectedTags.value.includes(tag)) {
|
if (selectedTags.value.includes(tag)) {
|
||||||
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
||||||
@ -135,6 +155,57 @@ const toggleTag = (tag: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function processTile(tile: Tile) {
|
||||||
|
const img = new Image()
|
||||||
|
img.crossOrigin = 'Anonymous'
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
canvas.width = img.width
|
||||||
|
canvas.height = img.height
|
||||||
|
ctx!.drawImage(img, 0, 0, img.width, img.height)
|
||||||
|
|
||||||
|
const imageData = ctx!.getImageData(0, 0, canvas.width, canvas.height)
|
||||||
|
tileColorData.value.set(tile.id, getDominantColor(imageData))
|
||||||
|
tileEdgeData.value.set(tile.id, getEdgeComplexity(imageData))
|
||||||
|
}
|
||||||
|
img.src = `${config.server_endpoint}/textures/tiles/${tile.id}.png`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDominantColor(imageData: ImageData) {
|
||||||
|
let r = 0,
|
||||||
|
g = 0,
|
||||||
|
b = 0,
|
||||||
|
total = 0
|
||||||
|
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||||
|
if (imageData.data[i + 3] > 0) {
|
||||||
|
// Only consider non-transparent pixels
|
||||||
|
r += imageData.data[i]
|
||||||
|
g += imageData.data[i + 1]
|
||||||
|
b += imageData.data[i + 2]
|
||||||
|
total++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
r: Math.round(r / total),
|
||||||
|
g: Math.round(g / total),
|
||||||
|
b: Math.round(b / total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEdgeComplexity(imageData: ImageData) {
|
||||||
|
let edgePixels = 0
|
||||||
|
for (let y = 0; y < imageData.height; y++) {
|
||||||
|
for (let x = 0; x < imageData.width; x++) {
|
||||||
|
const i = (y * imageData.width + x) * 4
|
||||||
|
if (imageData.data[i + 3] > 0 && (x === 0 || y === 0 || x === imageData.width - 1 || y === imageData.height - 1 || imageData.data[i - 1] === 0 || imageData.data[i + 7] === 0)) {
|
||||||
|
edgePixels++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return edgePixels
|
||||||
|
}
|
||||||
|
|
||||||
function getTileCategory(tile: Tile): string {
|
function getTileCategory(tile: Tile): string {
|
||||||
return tileCategories.value.get(tile.id) || ''
|
return tileCategories.value.get(tile.id) || ''
|
||||||
}
|
}
|
||||||
@ -148,26 +219,18 @@ function closeGroup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectTile(tile: string) {
|
function selectTile(tile: string) {
|
||||||
mapEditor.setSelectedTile(tile)
|
mapEditorStore.setSelectedTile(tile)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isActiveTile(tile: Tile): boolean {
|
function isActiveTile(tile: Tile): boolean {
|
||||||
return mapEditor.selectedTile.value === tile.id
|
return mapEditorStore.selectedTile === tile.id
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
tiles.value = await tileStorage.getAll()
|
isModalOpen.value = true
|
||||||
const initialBatchSize = 20
|
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
||||||
const initialTiles = tiles.value.slice(0, initialBatchSize)
|
mapEditorStore.setTileList(response)
|
||||||
initialTiles.forEach((tile) => tileProcessor.processTile(tile))
|
response.forEach((tile) => processTile(tile))
|
||||||
|
})
|
||||||
// Process remaining tiles in background
|
|
||||||
setTimeout(() => {
|
|
||||||
tiles.value.slice(initialBatchSize).forEach((tile) => tileProcessor.processTile(tile))
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
tileProcessor.cleanup()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,189 +1,160 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-between p-5 w-[calc(100%_-_40px)] fixed bottom-0 left-0 z-20" :class="{ 'list-open': listOpen }">
|
<div class="flex justify-center p-5">
|
||||||
<div class="toolbar rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
|
<div class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
|
||||||
<div ref="toolbar" class="tools flex gap-2.5" v-if="mapEditor.currentMap.value">
|
<div ref="toolbar" class="tools flex gap-2.5" v-if="mapEditorStore.map">
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'move' }" @click="handleClick('move')">
|
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'move' }" @click="handleClick('move')">
|
||||||
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'move' }">(M)</span>
|
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'move' }">(M)</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="w-px bg-cyan"></div>
|
<div class="w-px bg-cyan"></div>
|
||||||
|
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'pencil' }" @click="handleClick('pencil')">
|
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'pencil' }" @click="handleClick('pencil')">
|
||||||
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'pencil' }">(P)</span>
|
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'pencil' }">(P)</span>
|
||||||
<div class="select" v-if="mapEditor.tool.value === 'pencil'">
|
<div class="select" v-if="mapEditorStore.tool === 'pencil'">
|
||||||
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
|
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
|
||||||
{{ mapEditor.drawMode.value.replace('_', ' ') }}
|
{{ mapEditorStore.drawMode.replace('_', ' ') }}
|
||||||
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && mapEditor.tool.value === 'pencil'">
|
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && mapEditorStore.tool === 'pencil'">
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('tile', 'pencil')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('tile')">
|
||||||
Tile
|
Tile
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('map_object', 'pencil')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('map_object')">
|
||||||
Map object
|
Map object
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('teleport', 'pencil')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('teleport')">
|
||||||
Teleport
|
Teleport
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('blocking tile', 'pencil')">Blocking tile</span>
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('blocking tile')">Blocking tile</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="w-px bg-cyan"></div>
|
<div class="w-px bg-cyan"></div>
|
||||||
|
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'eraser' }" @click="handleClick('eraser')">
|
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'eraser' }" @click="handleClick('eraser')">
|
||||||
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'eraser' }">(E)</span>
|
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'eraser' }">(E)</span>
|
||||||
<div class="select" v-if="mapEditor.tool.value === 'eraser'">
|
<div class="select" v-if="mapEditorStore.tool === 'eraser'">
|
||||||
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
|
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
|
||||||
{{ mapEditor.drawMode.value.replace('_', ' ') }}
|
{{ mapEditorStore.eraserMode.replace('_', ' ') }}
|
||||||
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
|
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('tile', 'eraser')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('tile')">
|
||||||
Tile
|
Tile
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('map_object', 'eraser')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('map_object')">
|
||||||
Map object
|
Map object
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('teleport', 'eraser')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('teleport')">
|
||||||
Teleport
|
Teleport
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('blocking tile', 'eraser')">Blocking tile</span>
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('blocking tile')">Blocking tile</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="w-px bg-cyan"></div>
|
<div class="w-px bg-cyan"></div>
|
||||||
|
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'paint' }" @click="handleClick('paint')">
|
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'paint' }" @click="handleClick('paint')">
|
||||||
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'paint' }">(B)</span>
|
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'paint' }">(B)</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="w-px bg-cyan"></div>
|
<div class="w-px bg-cyan"></div>
|
||||||
|
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="isMapEditorSettingsModalOpen = !isMapEditorSettingsModalOpen"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button>
|
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')" v-if="mapEditorStore.map"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button>
|
||||||
|
|
||||||
<div class="w-px bg-cyan"></div>
|
|
||||||
|
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="isMapEditorSettingsModalOpen = !isMapEditorSettingsModalOpen"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/settings.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(S)</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="toolbar rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
|
|
||||||
<button class="btn-cyan px-3.5" @click="() => emit('open-maps')">Load</button>
|
|
||||||
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditor.currentMap.value">Save</button>
|
|
||||||
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditor.currentMap.value">Clear</button>
|
|
||||||
<button class="btn-cyan px-3.5" @click="() => mapEditor.toggleActive()">Exit</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal :isModalOpen="isMapEditorSettingsModalOpen" @modal:close="() => (isMapEditorSettingsModalOpen = false)" :modal-width="300" :modal-height="350" :is-resizable="false" bg-style="none">
|
<div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
|
||||||
<template #modalHeader>
|
<button class="btn-cyan px-3.5" @click="() => mapEditorStore.toggleMapListModal()">Load</button>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Map editor settings</h3>
|
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditorStore.map">Save</button>
|
||||||
</template>
|
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditorStore.map">Clear</button>
|
||||||
<template #modalBody>
|
<button class="btn-cyan px-3.5" @click="() => mapEditorStore.toggleActive()">Exit</button>
|
||||||
<div class="m-4 flex items-center space-x-2">
|
</div>
|
||||||
<input id="continuous-drawing" @change="toggleContinuousDrawing" v-model="isContinuousDrawingEnabled" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
|
|
||||||
<label for="continuous-drawing" class="text-sm font-medium text-gray-200 cursor-pointer"> Continuous Drawing </label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="m-4 flex items-center space-x-2">
|
|
||||||
<input id="show-placed-map-object-preview" @change="mapEditor.togglePlacedMapObjectPreview()" v-model="isShowPlacedMapObjectPreviewEnabled" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
|
|
||||||
<label for="show-placed-map-object-preview" class="text-sm font-medium text-gray-200 cursor-pointer"> Show placed map object preview </label>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { onClickOutside } from '@vueuse/core'
|
import { onClickOutside } from '@vueuse/core'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditorStore = useMapEditorStore()
|
||||||
|
|
||||||
const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'open-teleport'])
|
const emit = defineEmits(['save', 'clear'])
|
||||||
|
|
||||||
// States
|
// track when clicked outside of toolbar items
|
||||||
const toolbar = ref(null)
|
const toolbar = ref(null)
|
||||||
const isMapEditorSettingsModalOpen = ref(false)
|
|
||||||
const selectPencilOpen = ref(false)
|
// track select state
|
||||||
const selectEraserOpen = ref(false)
|
let selectPencilOpen = ref(false)
|
||||||
const isContinuousDrawingEnabled = ref<Boolean>(false)
|
let selectEraserOpen = ref(false)
|
||||||
const isShowPlacedMapObjectPreviewEnabled = ref<Boolean>(mapEditor.isPlacedMapObjectPreviewEnabled.value)
|
|
||||||
const listOpen = computed(() => (mapEditor.tool.value === 'pencil' && (mapEditor.drawMode.value === 'tile' || mapEditor.drawMode.value === 'map_object')) || mapEditor.tool.value === 'paint')
|
|
||||||
|
|
||||||
// drawMode
|
// drawMode
|
||||||
function setDrawMode(value: string) {
|
function setDrawMode(value: string) {
|
||||||
mapEditor.setDrawMode(value)
|
mapEditorStore.isTileListModalShown = value === 'tile'
|
||||||
selectPencilOpen.value = false
|
mapEditorStore.isMapObjectListModalShown = value === 'map_object'
|
||||||
selectEraserOpen.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPencilMode() {
|
mapEditorStore.setDrawMode(value)
|
||||||
mapEditor.setTool('pencil')
|
|
||||||
selectPencilOpen.value = false
|
selectPencilOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawMode
|
// drawMode
|
||||||
function setEraserMode() {
|
function setEraserMode(value: string) {
|
||||||
mapEditor.setTool('eraser')
|
mapEditorStore.setEraserMode(value)
|
||||||
selectEraserOpen.value = false
|
selectEraserOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleContinuousDrawing() {
|
|
||||||
mapEditor.setInputMode(isContinuousDrawingEnabled.value ? 'hold' : 'tap')
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
|
|
||||||
setDrawMode(mode)
|
|
||||||
type === 'pencil' ? setPencilMode() : setEraserMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClick(tool: string) {
|
function handleClick(tool: string) {
|
||||||
mapEditor.setTool(tool)
|
if (tool === 'settings') {
|
||||||
if (tool === 'paint') mapEditor.setDrawMode('tile')
|
mapEditorStore.toggleSettingsModal()
|
||||||
|
} else {
|
||||||
|
mapEditorStore.setTool(tool)
|
||||||
|
}
|
||||||
|
|
||||||
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
|
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
|
||||||
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
|
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
|
||||||
if (mapEditor.drawMode.value === 'teleport') emit('open-teleport')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cycleToolMode(tool: 'pencil' | 'eraser') {
|
function cycleToolMode(tool: 'pencil' | 'eraser') {
|
||||||
const modes = ['tile', 'map_object', 'teleport', 'blocking tile']
|
const modes = ['tile', 'object', 'teleport', 'blocking tile']
|
||||||
const currentIndex = modes.indexOf(mapEditor.drawMode.value)
|
const currentMode = tool === 'pencil' ? mapEditorStore.drawMode : mapEditorStore.eraserMode
|
||||||
|
const currentIndex = modes.indexOf(currentMode)
|
||||||
const nextIndex = (currentIndex + 1) % modes.length
|
const nextIndex = (currentIndex + 1) % modes.length
|
||||||
const nextMode = modes[nextIndex]
|
const nextMode = modes[nextIndex]
|
||||||
|
|
||||||
|
if (tool === 'pencil') {
|
||||||
setDrawMode(nextMode)
|
setDrawMode(nextMode)
|
||||||
|
} else {
|
||||||
|
setEraserMode(nextMode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initKeyShortcuts(event: KeyboardEvent) {
|
function initKeyShortcuts(event: KeyboardEvent) {
|
||||||
// Check if map is set
|
// Check if map is set
|
||||||
if (!mapEditor.currentMap.value) return
|
if (!mapEditorStore.map) return
|
||||||
|
|
||||||
// prevent if focused on inputs
|
// prevent if focused on composables
|
||||||
if (document.activeElement?.tagName === 'INPUT') return
|
if (document.activeElement?.tagName === 'INPUT') return
|
||||||
|
|
||||||
if (event.ctrlKey) return
|
|
||||||
|
|
||||||
const keyActions: { [key: string]: string } = {
|
const keyActions: { [key: string]: string } = {
|
||||||
m: 'move',
|
m: 'move',
|
||||||
p: 'pencil',
|
p: 'pencil',
|
||||||
e: 'eraser',
|
e: 'eraser',
|
||||||
b: 'paint',
|
b: 'paint',
|
||||||
z: 'settings',
|
z: 'settings'
|
||||||
s: 'mapEditorSettings'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyActions.hasOwnProperty(event.key)) {
|
if (keyActions.hasOwnProperty(event.key)) {
|
||||||
const tool = keyActions[event.key]
|
const tool = keyActions[event.key]
|
||||||
if ((tool === 'pencil' || tool === 'eraser') && mapEditor.tool.value === tool) {
|
if ((tool === 'pencil' || tool === 'eraser') && mapEditorStore.tool === tool) {
|
||||||
cycleToolMode(tool)
|
cycleToolMode(tool)
|
||||||
} else {
|
} else {
|
||||||
handleClick(tool)
|
handleClick(tool)
|
||||||
|
@ -26,8 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
import { login } from '@/services/authentication'
|
||||||
import { login } from '@/services/authenticationService'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
@ -40,6 +39,15 @@ const password = ref('')
|
|||||||
const formError = ref('')
|
const formError = ref('')
|
||||||
const showPassword = ref(false)
|
const showPassword = ref(false)
|
||||||
|
|
||||||
|
// automatic login because of development
|
||||||
|
onMounted(async () => {
|
||||||
|
const token = useCookies().get('token')
|
||||||
|
if (token) {
|
||||||
|
gameStore.setToken(token)
|
||||||
|
gameStore.initConnection()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
// check if username and password are valid
|
// check if username and password are valid
|
||||||
if (username.value === '' || password.value === '') {
|
if (username.value === '' || password.value === '') {
|
||||||
@ -54,7 +62,7 @@ async function submit() {
|
|||||||
formError.value = response.error
|
formError.value = response.error
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
socketManager.setToken(response.token)
|
gameStore.setToken(response.token)
|
||||||
gameStore.initConnection()
|
gameStore.initConnection()
|
||||||
return true // Indicate success
|
return true // Indicate success
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { newPassword } from '@/services/authenticationService'
|
import { newPassword } from '@/services/authentication'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
@ -34,6 +34,15 @@ const password = ref('')
|
|||||||
const newPasswordError = ref('')
|
const newPasswordError = ref('')
|
||||||
const showPassword = ref(false)
|
const showPassword = ref(false)
|
||||||
|
|
||||||
|
// automatic login because of development
|
||||||
|
onMounted(async () => {
|
||||||
|
const token = useCookies().get('token')
|
||||||
|
if (token) {
|
||||||
|
gameStore.setToken(token)
|
||||||
|
gameStore.initConnection()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function newPasswordFunc() {
|
async function newPasswordFunc() {
|
||||||
// check if username and password are valid
|
// check if username and password are valid
|
||||||
if (password.value === '') {
|
if (password.value === '') {
|
||||||
|
@ -26,8 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
import { login, register } from '@/services/authentication'
|
||||||
import { login, register } from '@/services/authenticationService'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
@ -41,6 +40,15 @@ const email = ref('')
|
|||||||
const formError = ref('')
|
const formError = ref('')
|
||||||
const showPassword = ref(false)
|
const showPassword = ref(false)
|
||||||
|
|
||||||
|
// automatic login because of development
|
||||||
|
onMounted(async () => {
|
||||||
|
const token = useCookies().get('token')
|
||||||
|
if (token) {
|
||||||
|
gameStore.setToken(token)
|
||||||
|
gameStore.initConnection()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
// check if username and password are valid
|
// check if username and password are valid
|
||||||
if (username.value === '' || email.value === '' || password.value === '') {
|
if (username.value === '' || email.value === '' || password.value === '') {
|
||||||
@ -68,7 +76,7 @@ async function submit() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
socketManager.setToken(loginResponse.token)
|
gameStore.setToken(loginResponse.token)
|
||||||
gameStore.initConnection()
|
gameStore.initConnection()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { resetPassword } from '@/services/authenticationService'
|
import { resetPassword } from '@/services/authentication'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative max-lg:h-dvh flex flex-row-reverse">
|
<div class="relative max-lg:h-dvh flex flex-row-reverse">
|
||||||
|
<div class="portrait-mode-notice hidden absolute h-[calc(100%_-_80px)] w-[calc(100%_-_80px)] left-0 top-0 bg-gray z-50 p-10 text-center">
|
||||||
|
<span class="text-lg">Noxious is not compatible with portrait mode on smaller screens. Please switch to landscape mode to play.</span>
|
||||||
|
</div>
|
||||||
<div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[65dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
|
<div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[65dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
|
||||||
<div class="bg-[url('/assets/login/login-bg.png')] opacity-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center grayscale"></div>
|
<div class="bg-[url('/assets/login/login-bg.png')] opacity-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center grayscale"></div>
|
||||||
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[35dvh] lg:h-dvh relative"></div>
|
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[35dvh] lg:h-dvh relative"></div>
|
||||||
@ -18,11 +21,11 @@
|
|||||||
<div class="absolute right-[calc(100%_+_16px)] -top-px flex gap-2 flex-col">
|
<div class="absolute right-[calc(100%_+_16px)] -top-px flex gap-2 flex-col">
|
||||||
<div v-for="character in characters" :key="character.id" class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')] after:absolute after:w-full after:h-px after:bg-gray-500" :class="{ active: selectedCharacterId === character.id }">
|
<div v-for="character in characters" :key="character.id" class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')] after:absolute after:w-full after:h-px after:bg-gray-500" :class="{ active: selectedCharacterId === character.id }">
|
||||||
<img src="/assets/placeholders/head.png" class="w-9 h-9 object-contain center-element" alt="Player head" />
|
<img src="/assets/placeholders/head.png" class="w-9 h-9 object-contain center-element" alt="Player head" />
|
||||||
<input class="h-full w-full absolute m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 btn-sound" type="radio" name="character" :value="character.id" v-model="selectedCharacterId" />
|
<input class="h-full w-full absolute m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0" type="radio" name="character" :value="character.id" v-model="selectedCharacterId" />
|
||||||
</div>
|
</div>
|
||||||
<div class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')]" :class="{ active: characters.length == 0 }" v-if="characters.length < 4">
|
<div class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')]" :class="{ active: characters.length == 0 }" v-if="characters.length < 4">
|
||||||
<button class="p-0 h-full w-full flex flex-col justify-between focus-visible:outline-offset-0 btn-sound" @click="isCreateNewCharacterModalOpen = true">
|
<button class="p-0 h-full w-full flex flex-col justify-between focus-visible:outline-offset-0" @click="isCreateNewCharacterModalOpen = true">
|
||||||
<img class="w-6 h-6 object-contain center-element btn-sound" draggable="false" src="/assets/icons/plus-icon.svg" />
|
<img class="w-6 h-6 object-contain center-element" draggable="false" src="/assets/icons/plus-icon.svg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -34,7 +37,7 @@
|
|||||||
<button class="ml-6 w-4 h-8 p-0">
|
<button class="ml-6 w-4 h-8 p-0">
|
||||||
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" />
|
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" />
|
||||||
</button>
|
</button>
|
||||||
<img class="w-24 object-contain mb-3.5 max-h-[70%]" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + characters.find((c) => c.id === selectedCharacterId)?.characterType + '/' + (selectedHairId ?? 'default')" />
|
<img class="w-24 object-contain mb-3.5" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + characters.find((c) => c.id === selectedCharacterId)?.characterType + '/' + (selectedHairId ?? 'default')" />
|
||||||
<button class="mr-6 w-4 h-8 p-0">
|
<button class="mr-6 w-4 h-8 p-0">
|
||||||
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" />
|
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" />
|
||||||
</button>
|
</button>
|
||||||
@ -87,7 +90,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
|
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading">
|
<div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading">
|
||||||
@ -122,31 +125,27 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types'
|
import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { CharacterHairStorage } from '@/storage/storages'
|
import { CharacterHairStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const { playSound } = useSoundComposable()
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const isLoading = ref<boolean>(true)
|
const isLoading = ref<boolean>(true)
|
||||||
const characters = ref<CharacterT[]>([])
|
const characters = ref<CharacterT[]>([])
|
||||||
const selectedCharacterId = ref<string | null>(null)
|
const selectedCharacterId = ref<number | null>(null)
|
||||||
const isCreateNewCharacterModalOpen = ref<boolean>(false)
|
const isCreateNewCharacterModalOpen = ref<boolean>(false)
|
||||||
const newCharacterName = ref<string>('')
|
const newCharacterName = ref<string>('')
|
||||||
const characterHairs = ref<CharacterHair[]>([])
|
const characterHairs = ref<CharacterHair[]>([])
|
||||||
const selectedHairId = ref<string | null>(null)
|
const selectedHairId = ref<number | null>(null)
|
||||||
|
|
||||||
// Fetch characters
|
// Fetch characters
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
socketManager.emit(SocketEvent.CHARACTER_LIST)
|
gameStore.connection?.emit('character:list')
|
||||||
}, 750)
|
}, 750)
|
||||||
|
|
||||||
socketManager.on(SocketEvent.CHARACTER_LIST, (data: any) => {
|
gameStore.connection?.on('character:list', (data: any) => {
|
||||||
characters.value = data
|
characters.value = data
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
})
|
})
|
||||||
@ -155,8 +154,8 @@ socketManager.on(SocketEvent.CHARACTER_LIST, (data: any) => {
|
|||||||
function loginWithCharacter() {
|
function loginWithCharacter() {
|
||||||
if (!selectedCharacterId.value) return
|
if (!selectedCharacterId.value) return
|
||||||
|
|
||||||
socketManager.emit(
|
gameStore.connection?.emit(
|
||||||
SocketEvent.CHARACTER_CONNECT,
|
'character:connect',
|
||||||
{
|
{
|
||||||
characterId: selectedCharacterId.value,
|
characterId: selectedCharacterId.value,
|
||||||
characterHairId: selectedHairId.value
|
characterHairId: selectedHairId.value
|
||||||
@ -169,27 +168,27 @@ function loginWithCharacter() {
|
|||||||
|
|
||||||
// Create character logics
|
// Create character logics
|
||||||
function createCharacter() {
|
function createCharacter() {
|
||||||
socketManager.emit(SocketEvent.CHARACTER_CREATE, { name: newCharacterName.value }, (success: boolean) => {
|
gameStore.connection?.on('character:create:success', (data: CharacterT) => {
|
||||||
if (success) return
|
gameStore.setCharacter(data)
|
||||||
isCreateNewCharacterModalOpen.value = false
|
isCreateNewCharacterModalOpen.value = false
|
||||||
})
|
})
|
||||||
|
gameStore.connection?.emit('character:create', { name: newCharacterName.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch changes for selected character and update hairs
|
// Watch changes for selected character and update hairs
|
||||||
watch(selectedCharacterId, (characterId) => {
|
watch(selectedCharacterId, (characterId) => {
|
||||||
if (!characterId) return
|
if (!characterId) return
|
||||||
selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHair ?? null
|
// selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHairId ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
playSound('/assets/music/intro.mp3')
|
|
||||||
const characterHairStorage = new CharacterHairStorage()
|
const characterHairStorage = new CharacterHairStorage()
|
||||||
characterHairs.value = await characterHairStorage.getAll()
|
characterHairs.value = await characterHairStorage.getAll()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
socketManager.off(SocketEvent.CHARACTER_LIST)
|
gameStore.connection?.off('character:list')
|
||||||
socketManager.off(SocketEvent.CHARACTER_CONNECT)
|
gameStore.connection?.off('character:connect')
|
||||||
socketManager.off(SocketEvent.CHARACTER_CREATE)
|
gameStore.connection?.off('character:create:success')
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center items-center h-dvh relative">
|
<div class="flex justify-center items-center h-dvh relative">
|
||||||
|
<div class="portrait-mode-notice hidden absolute h-[calc(100%_-_80px)] w-[calc(100%_-_80px)] left-0 top-0 bg-gray z-50 p-10 text-center">
|
||||||
|
<span class="text-lg">Noxious is not compatible with portrait mode on smaller screens. Please switch to landscape mode to play.</span>
|
||||||
|
</div>
|
||||||
<Game :config="gameConfig" @create="createGame">
|
<Game :config="gameConfig" @create="createGame">
|
||||||
<Scene name="main" @preload="preloadScene">
|
<Scene name="main" @preload="preloadScene" @create="createScene">
|
||||||
<Menu />
|
<Menu />
|
||||||
<Hud />
|
<Hud />
|
||||||
<Hotkeys />
|
<Hotkeys />
|
||||||
@ -11,6 +14,7 @@
|
|||||||
<ExpBar />
|
<ExpBar />
|
||||||
|
|
||||||
<CharacterProfile />
|
<CharacterProfile />
|
||||||
|
<Effects />
|
||||||
</Scene>
|
</Scene>
|
||||||
</Game>
|
</Game>
|
||||||
</div>
|
</div>
|
||||||
@ -26,24 +30,20 @@ import ExpBar from '@/components/game/gui/ExpBar.vue'
|
|||||||
import Hotkeys from '@/components/game/gui/Hotkeys.vue'
|
import Hotkeys from '@/components/game/gui/Hotkeys.vue'
|
||||||
import Hud from '@/components/game/gui/Hud.vue'
|
import Hud from '@/components/game/gui/Hud.vue'
|
||||||
import Menu from '@/components/game/gui/Menu.vue'
|
import Menu from '@/components/game/gui/Menu.vue'
|
||||||
|
import Effects from '@/components/game/map/Effects.vue'
|
||||||
import Map from '@/components/game/map/Map.vue'
|
import Map from '@/components/game/map/Map.vue'
|
||||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { Game, Scene } from 'phavuer'
|
import { Game, Scene } from 'phavuer'
|
||||||
import { onMounted } from 'vue'
|
import { onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const { playSound, stopSound } = useSoundComposable()
|
|
||||||
|
|
||||||
const gameConfig = {
|
const gameConfig = {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
||||||
resolution: 5,
|
resolution: 5
|
||||||
input: {
|
|
||||||
windowEvents: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createGame = (game: Phaser.Game) => {
|
const createGame = (game: Phaser.Game) => {
|
||||||
@ -60,8 +60,6 @@ const createGame = (game: Phaser.Game) => {
|
|||||||
})
|
})
|
||||||
gameStore.disconnectSocket()
|
gameStore.disconnectSocket()
|
||||||
}
|
}
|
||||||
|
|
||||||
playSound('/assets/sounds/connect.wav')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function preloadScene(scene: Phaser.Scene) {
|
function preloadScene(scene: Phaser.Scene) {
|
||||||
@ -70,7 +68,7 @@ function preloadScene(scene: Phaser.Scene) {
|
|||||||
scene.load.image('waypoint', '/assets/waypoint.png')
|
scene.load.image('waypoint', '/assets/waypoint.png')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
function createScene(scene: Phaser.Scene) {}
|
||||||
stopSound('/assets/music/intro.mp3')
|
|
||||||
})
|
onBeforeUnmount(() => {})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,12 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col justify-center items-center h-dvh relative col">
|
<div class="flex flex-col justify-center items-center h-dvh relative col">
|
||||||
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
|
<svg width="40" height="40" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="4" cy="12" r="3" fill="white">
|
||||||
|
<animate id="spinner_qFRN" begin="0;spinner_OcgL.end+0.25s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="12" cy="12" r="3" fill="white">
|
||||||
|
<animate begin="spinner_qFRN.begin+0.1s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="20" cy="12" r="3" fill="white">
|
||||||
|
<animate id="spinner_OcgL" begin="spinner_qFRN.begin+0.2s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
|
||||||
|
</circle>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts" async>
|
<script setup lang="ts" async>
|
||||||
import { downloadCache } from '@/application/utilities'
|
import config from '@/application/config'
|
||||||
|
import type { HttpResponse, MapObject } from '@/application/types'
|
||||||
|
import type { BaseStorage } from '@/storage/baseStorage'
|
||||||
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages'
|
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages'
|
||||||
|
// import type { Map } from '@/application/types'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
@ -15,13 +28,32 @@ const gameStore = useGameStore()
|
|||||||
const totalItems = ref(0)
|
const totalItems = ref(0)
|
||||||
const currentItem = ref(0)
|
const currentItem = ref(0)
|
||||||
|
|
||||||
|
async function downloadAndStore<T extends { id: string }>(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
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = response.data ?? []
|
||||||
|
for (const item of items) {
|
||||||
|
await storage.add(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tileStorage = new TileStorage()
|
||||||
|
const mapStorage = new MapStorage()
|
||||||
|
const mapObjectStorage = new MapObjectStorage()
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
downloadCache('tiles', new TileStorage()),
|
downloadAndStore('tiles', tileStorage),
|
||||||
downloadCache('maps', new MapStorage()),
|
downloadAndStore('maps', mapStorage),
|
||||||
downloadCache('map_objects', new MapObjectStorage()),
|
downloadAndStore('map_objects', mapObjectStorage),
|
||||||
downloadCache('sprites', new SpriteStorage()),
|
downloadAndStore('sprites', new SpriteStorage()),
|
||||||
downloadCache('character_types', new CharacterTypeStorage()),
|
downloadAndStore('character_types', new CharacterTypeStorage()),
|
||||||
downloadCache('character_hair', new CharacterHairStorage())
|
downloadAndStore('character_hair', new CharacterHairStorage())
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
gameStore.game.isLoaded = true
|
gameStore.game.isLoaded = true
|
||||||
})
|
})
|
||||||
|
@ -8,8 +8,8 @@
|
|||||||
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
|
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
|
||||||
<!-- <img src="/assets/tlogo.png" class="mb-10 w-52" alt="Noxious logo" />-->
|
<!-- <img src="/assets/tlogo.png" class="mb-10 w-52" alt="Noxious logo" />-->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="" />
|
<img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" />
|
||||||
<img src="/assets/ui-elements/login-ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)]" alt="" />
|
<img src="/assets/ui-elements/login-ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)]" alt="UI box inner" />
|
||||||
|
|
||||||
<!-- Login Form -->
|
<!-- Login Form -->
|
||||||
<LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" />
|
<LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" />
|
||||||
@ -44,4 +44,13 @@ function switchToLogin() {
|
|||||||
currentForm.value = 'login'
|
currentForm.value = 'login'
|
||||||
doesUrlHaveToken.value = false
|
doesUrlHaveToken.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// automatic login because of development
|
||||||
|
onMounted(async () => {
|
||||||
|
const token = useCookies().get('token')
|
||||||
|
if (token) {
|
||||||
|
gameStore.setToken(token)
|
||||||
|
gameStore.initConnection()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,17 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center items-center h-dvh relative">
|
<div class="flex justify-center items-center h-dvh relative">
|
||||||
<Game :config="gameConfig" @create="createGame">
|
<Game :config="gameConfig" @create="createGame">
|
||||||
<Scene name="main" @preload="preloadScene">
|
<Scene name="main" @preload="preloadScene" @create="createScene">
|
||||||
<div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div>
|
<MapEditor :key="JSON.stringify(`${mapEditorStore.map?.id}_${mapEditorStore.map?.createdAt}_${mapEditorStore.map?.updatedAt}`)" v-if="isLoaded" />
|
||||||
<div v-else>
|
<div v-else class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div>
|
||||||
<Map v-if="mapEditor.currentMap.value" :key="mapEditor.currentMap.value?.id" />
|
|
||||||
<Toolbar ref="toolbar" @save="save" @clear="clear" @open-maps="mapModal?.open" @open-settings="mapSettingsModal?.open" @open-teleport="teleportModal?.open" />
|
|
||||||
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
|
|
||||||
<TileList />
|
|
||||||
<MapObjectList />
|
|
||||||
<MapSettings ref="mapSettingsModal" />
|
|
||||||
<TeleportModal ref="teleportModal" />
|
|
||||||
</div>
|
|
||||||
</Scene>
|
</Scene>
|
||||||
</Game>
|
</Game>
|
||||||
</div>
|
</div>
|
||||||
@ -19,31 +11,16 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import 'phaser'
|
import 'phaser'
|
||||||
import type { Map as MapT } from '@/application/types'
|
import MapEditor from '@/components/gameMaster/mapEditor/MapEditor.vue'
|
||||||
import Map from '@/components/gameMaster/mapEditor/Map.vue'
|
import { loadAllTilesIntoScene } from '@/composables/mapComposable'
|
||||||
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
|
|
||||||
import MapObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
|
|
||||||
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
|
|
||||||
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
|
|
||||||
import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue'
|
|
||||||
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
|
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { loadAllTileTextures } from '@/services/mapService'
|
|
||||||
import { MapStorage } from '@/storage/storages'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
import { Game, Scene } from 'phavuer'
|
import { Game, Scene } from 'phavuer'
|
||||||
import { ref, useTemplateRef } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const mapStorage = new MapStorage()
|
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
const mapEditorStore = useMapEditorStore()
|
||||||
const mapModal = useTemplateRef('mapModal')
|
|
||||||
const mapSettingsModal = useTemplateRef('mapSettingsModal')
|
|
||||||
const teleportModal = useTemplateRef('teleportModal')
|
|
||||||
|
|
||||||
const isLoaded = ref(false)
|
const isLoaded = ref(false)
|
||||||
|
|
||||||
@ -52,17 +29,23 @@ const gameConfig = {
|
|||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
||||||
resolution: 5,
|
resolution: 5
|
||||||
input: {
|
|
||||||
windowEvents: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createGame = (game: Phaser.Game) => {
|
const createGame = (game: Phaser.Game) => {
|
||||||
// Resize the game when the window is resized
|
// Resize the game when the window is resized
|
||||||
window.addEventListener('resize', () => {
|
addEventListener('resize', () => {
|
||||||
game.scale.resize(window.innerWidth, window.innerHeight)
|
game.scale.resize(window.innerWidth, window.innerHeight)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// We don't support canvas mode, only WebGL
|
||||||
|
if (game.renderer.type === Phaser.CANVAS) {
|
||||||
|
gameStore.addNotification({
|
||||||
|
title: 'Warning',
|
||||||
|
message: 'Your browser does not support WebGL. Please use a modern browser like Chrome, Firefox, or Edge.'
|
||||||
|
})
|
||||||
|
gameStore.disconnectSocket()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const preloadScene = async (scene: Phaser.Scene) => {
|
const preloadScene = async (scene: Phaser.Scene) => {
|
||||||
@ -72,10 +55,8 @@ const preloadScene = async (scene: Phaser.Scene) => {
|
|||||||
scene.load.image('blank_tile', '/assets/map/blank_tile.png')
|
scene.load.image('blank_tile', '/assets/map/blank_tile.png')
|
||||||
scene.load.image('waypoint', '/assets/waypoint.png')
|
scene.load.image('waypoint', '/assets/waypoint.png')
|
||||||
|
|
||||||
// Get all tiles from IndexedDB and load them into the scene
|
await loadAllTilesIntoScene(scene)
|
||||||
await loadAllTileTextures(scene)
|
|
||||||
|
|
||||||
// Wait for all assets to be loaded before continuing
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
scene.load.on(Phaser.Loader.Events.COMPLETE, () => {
|
scene.load.on(Phaser.Loader.Events.COMPLETE, () => {
|
||||||
resolve()
|
resolve()
|
||||||
@ -84,24 +65,5 @@ const preloadScene = async (scene: Phaser.Scene) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
const createScene = async (scene: Phaser.Scene) => {}
|
||||||
const currentMap = mapEditor.currentMap.value
|
|
||||||
if (!currentMap) return
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
...currentMap,
|
|
||||||
mapId: currentMap.id
|
|
||||||
}
|
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.GM_MAP_UPDATE, data, (response: MapT) => {
|
|
||||||
mapStorage.update(response.id, response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear() {
|
|
||||||
if (!mapEditor.currentMap.value) return
|
|
||||||
|
|
||||||
// Clear placed objects, event tiles and tiles
|
|
||||||
mapEditor.triggerClearTiles()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
23
src/components/utilities/BackgroundImageLoader.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div style="display: none">
|
||||||
|
<img v-for="(url, index) in imageUrls" :key="index" :src="url" alt="" @load="handleImageLoad(index)" @error="handleImageError(index)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// Internal array of images to preload
|
||||||
|
const imageUrls = ref<string[]>(['/assets/ui-elements/button-ui-box-textured.svg', '/assets/ui-elements/button-ui-frame-empty.svg', '/assets/ui-elements/button-ui-box-textured-small.svg'])
|
||||||
|
|
||||||
|
const loadedImages = ref<Set<number>>(new Set())
|
||||||
|
|
||||||
|
const handleImageLoad = (index: number) => {
|
||||||
|
loadedImages.value.add(index)
|
||||||
|
console.log(`Image ${index} loaded:`, imageUrls.value[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageError = (index: number) => {
|
||||||
|
console.log(`Image ${index} failed to load:`, imageUrls.value[index])
|
||||||
|
}
|
||||||
|
</script>
|
@ -3,7 +3,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useControlsComposable } from '@/composables/useControlsComposable'
|
import { useCameraControls } from '@/composables/useCameraControls'
|
||||||
|
import { usePointerHandlers } from '@/composables/usePointerHandlers'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { Image, useScene } from 'phavuer'
|
||||||
import { onBeforeUnmount, ref } from 'vue'
|
import { onBeforeUnmount, ref } from 'vue'
|
||||||
|
|
||||||
@ -13,16 +14,45 @@ type WayPoint = { visible: boolean; x: number; y: number }
|
|||||||
// Props
|
// Props
|
||||||
const props = defineProps<{ layer: Phaser.Tilemaps.TilemapLayer }>()
|
const props = defineProps<{ layer: Phaser.Tilemaps.TilemapLayer }>()
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const ZOOM_SETTINGS = {
|
||||||
|
WHEEL_FACTOR: 0.005,
|
||||||
|
KEY_FACTOR: 0.3,
|
||||||
|
MIN: 1,
|
||||||
|
MAX: 3
|
||||||
|
} as const
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const waypoint = ref<WayPoint>({ visible: false, x: 0, y: 0 })
|
const waypoint = ref<WayPoint>({ visible: false, x: 0, y: 0 })
|
||||||
const { setupControls, cleanupControls } = useControlsComposable(scene, props.layer, waypoint)
|
const { camera } = useCameraControls(scene)
|
||||||
|
const { setupPointerHandlers, cleanupPointerHandlers } = usePointerHandlers(scene, props.layer, waypoint, camera)
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
function handleScrollZoom(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (!(pointer.event instanceof WheelEvent && pointer.event.shiftKey)) return
|
||||||
|
|
||||||
|
const zoomLevel = Phaser.Math.Clamp(camera.zoom - pointer.event.deltaY * ZOOM_SETTINGS.WHEEL_FACTOR, ZOOM_SETTINGS.MIN, ZOOM_SETTINGS.MAX)
|
||||||
|
camera.setZoom(zoomLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyComboZoom(event: { keyCodes: number[] }) {
|
||||||
|
const deltaY = event.keyCodes[1] === 38 ? 1 : -1 // 38 is Up, 40 is Down
|
||||||
|
const zoomLevel = Phaser.Math.Clamp(camera.zoom + deltaY * ZOOM_SETTINGS.KEY_FACTOR, ZOOM_SETTINGS.MIN, ZOOM_SETTINGS.MAX)
|
||||||
|
camera.setZoom(zoomLevel)
|
||||||
|
}
|
||||||
|
|
||||||
// Event setup
|
// Event setup
|
||||||
setupControls()
|
setupPointerHandlers()
|
||||||
|
scene.input.keyboard?.createCombo([16, 38], { resetOnMatch: true }) // Shift + Up
|
||||||
|
scene.input.keyboard?.createCombo([16, 40], { resetOnMatch: true }) // Shift + Down
|
||||||
|
scene.input.keyboard?.on('keycombomatch', handleKeyComboZoom)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, handleScrollZoom)
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
cleanupControls()
|
cleanupPointerHandlers()
|
||||||
|
scene.input.keyboard?.off('keycombomatch')
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, handleScrollZoom)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
<template></template>
|
<template></template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages'
|
||||||
import { login } from '@/services/authenticationService'
|
|
||||||
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SoundStorage, SpriteStorage, TileStorage } from '@/storage/storages'
|
|
||||||
import { TextureStorage } from '@/storage/textureStorage'
|
import { TextureStorage } from '@/storage/textureStorage'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const mapStorage = new MapStorage()
|
const mapStorage = new MapStorage()
|
||||||
const tileStorage = new TileStorage()
|
const tileStorage = new TileStorage()
|
||||||
const mapObjectStorage = new MapObjectStorage()
|
const mapObjectStorage = new MapObjectStorage()
|
||||||
@ -16,7 +12,6 @@ const spriteStorage = new SpriteStorage()
|
|||||||
const characterTypeStorage = new CharacterTypeStorage()
|
const characterTypeStorage = new CharacterTypeStorage()
|
||||||
const characterHairStorage = new CharacterHairStorage()
|
const characterHairStorage = new CharacterHairStorage()
|
||||||
const textureStorage = new TextureStorage()
|
const textureStorage = new TextureStorage()
|
||||||
const soundStorage = new SoundStorage()
|
|
||||||
|
|
||||||
let currentString = ''
|
let currentString = ''
|
||||||
|
|
||||||
@ -37,21 +32,9 @@ async function handleKeyPress(event: KeyboardEvent) {
|
|||||||
await characterTypeStorage.destroy()
|
await characterTypeStorage.destroy()
|
||||||
await characterHairStorage.destroy()
|
await characterHairStorage.destroy()
|
||||||
await textureStorage.destroy()
|
await textureStorage.destroy()
|
||||||
await soundStorage.destroy()
|
|
||||||
currentString = '' // Reset
|
currentString = '' // Reset
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentString.includes('11')) {
|
|
||||||
if (socketManager.token) return
|
|
||||||
const response = await login('root', 'password')
|
|
||||||
|
|
||||||
if (response.success === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
socketManager.setToken(response.token)
|
|
||||||
gameStore.initConnection()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset string after a certain amount of time
|
// Reset string after a certain amount of time
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
currentString = ''
|
currentString = ''
|
||||||
|
@ -14,11 +14,11 @@
|
|||||||
<slot name="modalHeader" />
|
<slot name="modalHeader" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2.5">
|
<div class="flex gap-2.5">
|
||||||
<button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out btn-sound">
|
<button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out">
|
||||||
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/modal/minimize.svg' : '/assets/icons/modal/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" />
|
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/modal/minimize.svg' : '/assets/icons/modal/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" />
|
||||||
</button>
|
</button>
|
||||||
<button v-if="closable" @click="closeModal" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out btn-sound">
|
<button v-if="closable" @click="emit('modal:close')" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
||||||
<img alt="close" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full btn-sound" draggable="false" />
|
<img alt="close" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" draggable="false" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -46,7 +46,7 @@
|
|||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
isModalOpen?: boolean
|
isModalOpen: boolean
|
||||||
closable?: boolean
|
closable?: boolean
|
||||||
isResizable?: boolean
|
isResizable?: boolean
|
||||||
isFullScreen?: boolean
|
isFullScreen?: boolean
|
||||||
@ -79,20 +79,10 @@ const props = withDefaults(defineProps<ModalProps>(), {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'modal:open': []
|
|
||||||
'modal:close': []
|
'modal:close': []
|
||||||
|
'character:create': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open: () => (isModalOpenRef.value = true),
|
|
||||||
close: () => (isModalOpenRef.value = false),
|
|
||||||
toggle: () => (isModalOpenRef.value = !isModalOpenRef.value),
|
|
||||||
setPosition: (a: number, b: number) => {
|
|
||||||
x.value = a
|
|
||||||
y.value = b
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const isModalOpenRef = ref(props.isModalOpen)
|
const isModalOpenRef = ref(props.isModalOpen)
|
||||||
const width = ref(props.modalWidth)
|
const width = ref(props.modalWidth)
|
||||||
const height = ref(props.modalHeight)
|
const height = ref(props.modalHeight)
|
||||||
@ -160,11 +150,6 @@ function drag(event: MouseEvent) {
|
|||||||
y.value = dragState.initialY + (event.clientY - dragState.startY)
|
y.value = dragState.initialY + (event.clientY - dragState.startY)
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
|
||||||
isModalOpenRef.value = false
|
|
||||||
emit('modal:close')
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleFullScreen() {
|
function toggleFullScreen() {
|
||||||
if (isFullScreen.value) {
|
if (isFullScreen.value) {
|
||||||
Object.assign({ x, y, width, height }, preFullScreenState)
|
Object.assign({ x, y, width, height }, preFullScreenState)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal v-for="notification in gameStore.notifications" :key="notification.id" :isModalOpen="true" @modal:close="closeNotification(notification!.id as string)">
|
<Modal v-for="notification in gameStore.notifications" :key="notification.id" :isModalOpen="true" @modal:close="closeNotification(notification.id)">
|
||||||
<template #modalHeader v-if="notification.title">
|
<template #modalHeader v-if="notification.title">
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">{{ notification.title }}</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">{{ notification.title }}</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -10,9 +10,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onBeforeMount, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue'
|
import { onBeforeMount, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
|
||||||
@ -28,7 +26,7 @@ type Notification = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupNotificationListener(connection: any) {
|
function setupNotificationListener(connection: any) {
|
||||||
connection.on(SocketEvent.NOTIFICATION, (data: Notification) => {
|
connection.on('notification', (data: Notification) => {
|
||||||
gameStore.addNotification({
|
gameStore.addNotification({
|
||||||
title: data.title,
|
title: data.title,
|
||||||
message: data.message
|
message: data.message
|
||||||
@ -54,7 +52,7 @@ onMounted(() => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
const connection = gameStore.connection
|
const connection = gameStore.connection
|
||||||
if (connection) {
|
if (connection) {
|
||||||
connection.off(SocketEvent.NOTIFICATION)
|
connection.off('notification')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
import config from '@/application/config'
|
|
||||||
import { getTile, tileToWorldXY } from '@/services/mapService'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { ref, type Ref } from 'vue'
|
|
||||||
|
|
||||||
export function useBaseControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const pointerStartPosition = ref({ x: 0, y: 0 })
|
|
||||||
const dragThreshold = 5 // pixels
|
|
||||||
|
|
||||||
function updateWaypoint(worldX: number, worldY: number) {
|
|
||||||
const pointerTile = getTile(layer, worldX, worldY)
|
|
||||||
if (!pointerTile) {
|
|
||||||
waypoint.value.visible = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
|
||||||
if (!worldPoint.worldPositionX || !worldPoint.worldPositionX) return
|
|
||||||
|
|
||||||
waypoint.value = {
|
|
||||||
visible: true,
|
|
||||||
x: worldPoint.worldPositionX,
|
|
||||||
y: worldPoint.worldPositionY + config.tile_size.height + 15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDragMap(pointer: Phaser.Input.Pointer) {
|
|
||||||
if (!gameStore.game.isPlayerDraggingCamera) return
|
|
||||||
|
|
||||||
const deltaX = pointer.x - pointerStartPosition.value.x
|
|
||||||
const deltaY = pointer.y - pointerStartPosition.value.y
|
|
||||||
|
|
||||||
if (Math.abs(deltaX) <= dragThreshold && Math.abs(deltaY) <= dragThreshold) return
|
|
||||||
|
|
||||||
const scrollX = camera.scrollX - deltaX / camera.zoom
|
|
||||||
const scrollY = camera.scrollY - deltaY / camera.zoom
|
|
||||||
|
|
||||||
camera.setScroll(scrollX, scrollY)
|
|
||||||
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
|
|
||||||
}
|
|
||||||
|
|
||||||
function startDragging(pointer: Phaser.Input.Pointer) {
|
|
||||||
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
|
|
||||||
gameStore.setPlayerDraggingCamera(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopDragging() {
|
|
||||||
gameStore.setPlayerDraggingCamera(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleZoom(pointer: Phaser.Input.Pointer) {
|
|
||||||
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
|
|
||||||
const deltaY = pointer.event.deltaY
|
|
||||||
const zoomLevel = camera.zoom - deltaY * 0.005
|
|
||||||
if (zoomLevel > 0 && zoomLevel < 3) {
|
|
||||||
camera.setZoom(zoomLevel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
updateWaypoint,
|
|
||||||
handleDragMap,
|
|
||||||
startDragging,
|
|
||||||
stopDragging,
|
|
||||||
handleZoom,
|
|
||||||
pointerStartPosition
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,149 +0,0 @@
|
|||||||
import { SocketEvent } from '@/application/enums'
|
|
||||||
import { socketManager } from '@/managers/SocketManager'
|
|
||||||
import { getTile } from '@/services/mapService'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import type { Ref } from 'vue'
|
|
||||||
import { useBaseControlsComposable } from './useBaseControlsComposable'
|
|
||||||
|
|
||||||
export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const baseHandlers = useBaseControlsComposable(scene, layer, waypoint, camera)
|
|
||||||
const pressedKeys = new Set<string>()
|
|
||||||
|
|
||||||
let moveTimeout: NodeJS.Timeout | null = null
|
|
||||||
let currentPosition = {
|
|
||||||
x: 0,
|
|
||||||
y: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Movement constants
|
|
||||||
const MOVEMENT_DELAY = 110 // Milliseconds between moves
|
|
||||||
const ARROW_KEYS = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'] as const
|
|
||||||
|
|
||||||
function updateCurrentPosition() {
|
|
||||||
if (!gameStore.character) return
|
|
||||||
currentPosition = {
|
|
||||||
x: gameStore.character.positionX,
|
|
||||||
y: gameStore.character.positionY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateNewPosition() {
|
|
||||||
let newX = currentPosition.x
|
|
||||||
let newY = currentPosition.y
|
|
||||||
|
|
||||||
if (pressedKeys.has('ArrowLeft')) newX--
|
|
||||||
if (pressedKeys.has('ArrowRight')) newX++
|
|
||||||
if (pressedKeys.has('ArrowUp')) newY--
|
|
||||||
if (pressedKeys.has('ArrowDown')) newY++
|
|
||||||
|
|
||||||
return { newX, newY }
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitMovement(x: number, y: number) {
|
|
||||||
if (x === currentPosition.x && y === currentPosition.y) return
|
|
||||||
|
|
||||||
socketManager.emit(SocketEvent.MAP_CHARACTER_MOVE, [x, y])
|
|
||||||
socketManager.on(SocketEvent.MAP_CHARACTER_MOVE, ([characterId, posX, posY, rot, isMoving]: [string, number, number, number, boolean]) => {
|
|
||||||
if (characterId !== gameStore.character?.id) return
|
|
||||||
currentPosition = { x: posX, y: posY }
|
|
||||||
})
|
|
||||||
|
|
||||||
currentPosition = { x, y }
|
|
||||||
}
|
|
||||||
|
|
||||||
function startMovementLoop() {
|
|
||||||
if (moveTimeout) return
|
|
||||||
|
|
||||||
const move = () => {
|
|
||||||
if (pressedKeys.size === 0) {
|
|
||||||
stopMovementLoop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentPosition()
|
|
||||||
const { newX, newY } = calculateNewPosition()
|
|
||||||
emitMovement(newX, newY)
|
|
||||||
|
|
||||||
moveTimeout = setTimeout(move, MOVEMENT_DELAY)
|
|
||||||
}
|
|
||||||
|
|
||||||
move()
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopMovementLoop() {
|
|
||||||
if (moveTimeout) {
|
|
||||||
clearTimeout(moveTimeout)
|
|
||||||
moveTimeout = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pointer Handlers
|
|
||||||
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
|
||||||
baseHandlers.startDragging(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
|
||||||
baseHandlers.updateWaypoint(pointer.worldX, pointer.worldY)
|
|
||||||
baseHandlers.handleDragMap(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
|
||||||
baseHandlers.stopDragging()
|
|
||||||
|
|
||||||
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
|
|
||||||
if (!pointerTile) return
|
|
||||||
|
|
||||||
emitMovement(pointerTile.x, pointerTile.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keyboard Handlers
|
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
|
||||||
if (!gameStore.character) return
|
|
||||||
|
|
||||||
if (ARROW_KEYS.includes(event.key as (typeof ARROW_KEYS)[number])) {
|
|
||||||
if (event.repeat) return
|
|
||||||
|
|
||||||
pressedKeys.add(event.key)
|
|
||||||
updateCurrentPosition()
|
|
||||||
startMovementLoop()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Control') {
|
|
||||||
socketManager.emit(SocketEvent.MAP_CHARACTER_ATTACK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyUp(event: KeyboardEvent) {
|
|
||||||
pressedKeys.delete(event.key)
|
|
||||||
|
|
||||||
if (pressedKeys.size === 0) {
|
|
||||||
stopMovementLoop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupControls = () => {
|
|
||||||
updateCurrentPosition() // Initialize position
|
|
||||||
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
|
|
||||||
scene.input.keyboard!.on('keydown', handleKeyDown)
|
|
||||||
scene.input.keyboard!.on('keyup', handleKeyUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanupControls = () => {
|
|
||||||
stopMovementLoop()
|
|
||||||
pressedKeys.clear()
|
|
||||||
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
|
|
||||||
scene.input.keyboard!.off('keydown', handleKeyDown)
|
|
||||||
scene.input.keyboard!.off('keyup', handleKeyUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { setupControls, cleanupControls }
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { computed, type Ref } from 'vue'
|
|
||||||
import { useBaseControlsComposable } from './useBaseControlsComposable'
|
|
||||||
|
|
||||||
export function useMapEditorControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
const baseHandlers = useBaseControlsComposable(scene, layer, waypoint, camera)
|
|
||||||
const isMoveTool = computed(() => mapEditor.tool.value === 'move')
|
|
||||||
|
|
||||||
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
|
||||||
if (isMoveTool.value || pointer.event.shiftKey) {
|
|
||||||
baseHandlers.startDragging(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
|
||||||
if (isMoveTool.value || pointer.event.shiftKey) {
|
|
||||||
baseHandlers.handleDragMap(pointer)
|
|
||||||
}
|
|
||||||
baseHandlers.updateWaypoint(pointer.worldX, pointer.worldY)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
|
||||||
baseHandlers.stopDragging()
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupControls = () => {
|
|
||||||
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)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanupControls = () => {
|
|
||||||
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)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { setupControls, cleanupControls }
|
|
||||||
}
|
|
@ -59,8 +59,9 @@ export async function loadTexture(scene: Phaser.Scene, textureData: TextureData)
|
|||||||
export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string) {
|
export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string) {
|
||||||
if (!sprite_id) return false
|
if (!sprite_id) return false
|
||||||
|
|
||||||
|
// @TODO: Fix this
|
||||||
const spriteStorage = new SpriteStorage()
|
const spriteStorage = new SpriteStorage()
|
||||||
const sprite = await spriteStorage.getById(sprite_id)
|
const sprite = await spriteStorage.get(sprite_id)
|
||||||
|
|
||||||
if (!sprite) {
|
if (!sprite) {
|
||||||
console.error('Failed to load sprite:', sprite_id)
|
console.error('Failed to load sprite:', sprite_id)
|
||||||
@ -72,17 +73,18 @@ export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string)
|
|||||||
await loadTexture(scene, {
|
await loadTexture(scene, {
|
||||||
key,
|
key,
|
||||||
data: '/textures/sprites/' + sprite.id + '/' + sprite_action.action + '.png',
|
data: '/textures/sprites/' + sprite.id + '/' + sprite_action.action + '.png',
|
||||||
group: sprite_action.frameCount > 1 ? 'sprite_animations' : 'sprites',
|
group: sprite_action.isAnimated ? 'sprite_animations' : 'sprites',
|
||||||
updatedAt: sprite_action.updatedAt,
|
updatedAt: sprite_action.updatedAt,
|
||||||
originX: sprite_action.originX,
|
originX: sprite_action.originX,
|
||||||
originY: sprite_action.originY,
|
originY: sprite_action.originY,
|
||||||
|
isAnimated: sprite_action.isAnimated,
|
||||||
frameWidth: sprite_action.frameWidth,
|
frameWidth: sprite_action.frameWidth,
|
||||||
frameHeight: sprite_action.frameHeight,
|
frameHeight: sprite_action.frameHeight,
|
||||||
frameRate: sprite_action.frameRate
|
frameRate: sprite_action.frameRate
|
||||||
} as TextureData)
|
} as TextureData)
|
||||||
|
|
||||||
// If the sprite has no more than one frame, skip
|
// If the sprite is not animated, skip
|
||||||
if (sprite_action.frameCount <= 1) continue
|
if (!sprite_action.isAnimated) continue
|
||||||
|
|
||||||
// Check if animation already exists
|
// Check if animation already exists
|
||||||
if (scene.anims.get(key)) continue
|
if (scene.anims.get(key)) continue
|
@ -1,7 +1,7 @@
|
|||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import type { MapObject, Map as MapT, TextureData, Tile as TileT, UUID } from '@/application/types'
|
import type { HttpResponse, TextureData, UUID } from '@/application/types'
|
||||||
import { unduplicateArray } from '@/application/utilities'
|
import { unduplicateArray } from '@/application/utilities'
|
||||||
import { loadTexture } from '@/services/textureService'
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
import { MapStorage, TileStorage } from '@/storage/storages'
|
import { MapStorage, TileStorage } from '@/storage/storages'
|
||||||
|
|
||||||
import Tilemap = Phaser.Tilemaps.Tilemap
|
import Tilemap = Phaser.Tilemaps.Tilemap
|
||||||
@ -10,7 +10,9 @@ import Tileset = Phaser.Tilemaps.Tileset
|
|||||||
import Tile = Phaser.Tilemaps.Tile
|
import Tile = Phaser.Tilemaps.Tile
|
||||||
|
|
||||||
export function getTile(layer: TilemapLayer | Tilemap, positionX: number, positionY: number): Tile | null {
|
export function getTile(layer: TilemapLayer | Tilemap, positionX: number, positionY: number): Tile | null {
|
||||||
return layer.getTileAtWorldXY(positionX, positionY)
|
const tile = layer?.getTileAtWorldXY(positionX, positionY)
|
||||||
|
if (!tile) return null
|
||||||
|
return tile
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tileToWorldXY(layer: TilemapLayer | Tilemap, positionX: number, positionY: number) {
|
export function tileToWorldXY(layer: TilemapLayer | Tilemap, positionX: number, positionY: number) {
|
||||||
@ -39,6 +41,11 @@ export function tileToWorldY(layer: TilemapLayer | Tilemap, positionX: number, p
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Can also be used to replace tiles
|
* Can also be used to replace tiles
|
||||||
|
* @param map
|
||||||
|
* @param layer
|
||||||
|
* @param positionX
|
||||||
|
* @param positionY
|
||||||
|
* @param tileName
|
||||||
*/
|
*/
|
||||||
export function placeTile(map: Tilemap, layer: TilemapLayer, positionX: number, positionY: number, tileName: string) {
|
export function placeTile(map: Tilemap, layer: TilemapLayer, positionX: number, positionY: number, tileName: string) {
|
||||||
let tileImg = map.getTileset(tileName) as Tileset
|
let tileImg = map.getTileset(tileName) as Tileset
|
||||||
@ -48,8 +55,8 @@ export function placeTile(map: Tilemap, layer: TilemapLayer, positionX: number,
|
|||||||
layer.putTileAt(tileImg.firstgid, positionX, positionY)
|
layer.putTileAt(tileImg.firstgid, positionX, positionY)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function placeTiles(map: Tilemap, layer: TilemapLayer, tiles: string[][]) {
|
export function setLayerTiles(map: Tilemap, layer: TilemapLayer, tiles: string[][]) {
|
||||||
if (!map || !layer || !tiles) return
|
if (!tiles) return
|
||||||
|
|
||||||
tiles.forEach((row: string[], y: number) => {
|
tiles.forEach((row: string[], y: number) => {
|
||||||
row.forEach((tile: string, x: number) => {
|
row.forEach((tile: string, x: number) => {
|
||||||
@ -62,15 +69,35 @@ export function createTileArray(width: number, height: number, tile: string = 'b
|
|||||||
return Array.from({ length: height }, () => Array.from({ length: width }, () => tile))
|
return Array.from({ length: height }, () => Array.from({ length: width }, () => tile))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const calculateIsometricDepth = (positionX: number, positionY: number, pivotPoints: { x: number; y: number }[] = []) => {
|
export const calculateIsometricDepth = (positionX: number, positionY: number, width: number = 0, height: number = 0, isCharacter: boolean = false) => {
|
||||||
return Math.max(positionX + positionY)
|
const baseDepth = positionX + positionY
|
||||||
|
if (isCharacter) {
|
||||||
|
return baseDepth // @TODO: Fix collision, this is a hack
|
||||||
|
}
|
||||||
|
return baseDepth + (width + height) / (2 * config.tile_size.width)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTileTextures(tiles: TileT[], scene: Phaser.Scene) {
|
export function FlattenMapArray(tiles: string[][]) {
|
||||||
// Load each tile into the scene
|
const normalArray = []
|
||||||
for (let tile of tiles) {
|
|
||||||
if (!tile?.id || !tile?.updatedAt) continue
|
|
||||||
|
|
||||||
|
for (const row of tiles) {
|
||||||
|
normalArray.push(...row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalArray
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadMapTilesIntoScene(map_id: UUID, scene: Phaser.Scene) {
|
||||||
|
const tileStorage = new TileStorage()
|
||||||
|
const mapStorage = new MapStorage()
|
||||||
|
const map = await mapStorage.get(map_id)
|
||||||
|
if (!map) return
|
||||||
|
|
||||||
|
const tileArray = unduplicateArray(FlattenMapArray(map.tiles))
|
||||||
|
const tiles = await tileStorage.getByIds(tileArray)
|
||||||
|
|
||||||
|
// Load each tile into the scene
|
||||||
|
for (const tile of tiles) {
|
||||||
const textureData = {
|
const textureData = {
|
||||||
key: tile.id,
|
key: tile.id,
|
||||||
data: '/textures/tiles/' + tile.id + '.png',
|
data: '/textures/tiles/' + tile.id + '.png',
|
||||||
@ -81,74 +108,35 @@ async function loadTileTextures(tiles: TileT[], scene: Phaser.Scene) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadTileTexturesFromMapTileArray(map_id: string, scene: Phaser.Scene) {
|
export async function loadTilesIntoScene(tileIds: string[], scene: Phaser.Scene) {
|
||||||
const mapStorage = new MapStorage()
|
|
||||||
const tileStorage = new TileStorage()
|
const tileStorage = new TileStorage()
|
||||||
|
|
||||||
const map = await mapStorage.getById(map_id)
|
const tiles = await tileStorage.getByIds(tileIds)
|
||||||
if (!map) return
|
|
||||||
|
|
||||||
const tileArray = unduplicateArray(map.tiles)
|
// Load each tile into the scene
|
||||||
const tiles = await tileStorage.getByIds(tileArray)
|
for (const tile of tiles) {
|
||||||
if (!tiles) return
|
|
||||||
|
|
||||||
await loadTileTextures(tiles, scene)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadAllTileTextures(scene: Phaser.Scene) {
|
|
||||||
const tileStorage = new TileStorage()
|
|
||||||
const tiles = await tileStorage.getAll()
|
|
||||||
if (!tiles) return
|
|
||||||
|
|
||||||
await loadTileTextures(tiles, scene)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadMapObjectTextures(mapObjects: MapObject[], scene: Phaser.Scene) {
|
|
||||||
for (const mapObject of mapObjects) {
|
|
||||||
const textureData = {
|
const textureData = {
|
||||||
key: mapObject.id,
|
key: tile.id,
|
||||||
data: '/textures/map_objects/' + mapObject.id + '.png',
|
data: '/textures/tiles/' + tile.id + '.png',
|
||||||
group: 'map_objects',
|
group: 'tiles',
|
||||||
updatedAt: mapObject.updatedAt,
|
updatedAt: tile.updatedAt
|
||||||
frameWidth: mapObject.frameWidth,
|
|
||||||
frameHeight: mapObject.frameHeight
|
|
||||||
} as TextureData
|
} as TextureData
|
||||||
await loadTexture(scene, textureData)
|
await loadTexture(scene, textureData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTileMap(scene: Phaser.Scene, map: MapT) {
|
export async function loadAllTilesIntoScene(scene: Phaser.Scene) {
|
||||||
const mapConfig = new Phaser.Tilemaps.MapData({
|
const tileStorage = new TileStorage()
|
||||||
width: map.width,
|
const tiles = await tileStorage.getAll()
|
||||||
height: map.height,
|
|
||||||
tileWidth: config.tile_size.width,
|
|
||||||
tileHeight: config.tile_size.height,
|
|
||||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
|
||||||
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Phaser.Tilemaps.Tilemap(scene, mapConfig)
|
// Load each tile into the scene
|
||||||
}
|
for (const tile of tiles) {
|
||||||
|
const textureData = {
|
||||||
export function createTileLayer(tileMap: Phaser.Tilemaps.Tilemap, tilesArray: string[]) {
|
key: tile.id,
|
||||||
// Load tiles into tileset
|
data: '/textures/tiles/' + tile.id + '.png',
|
||||||
const tilesetImages = tilesArray.map((tile: string, index: number) => {
|
group: 'tiles',
|
||||||
return tileMap.addTilesetImage(tile, tile, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height })
|
updatedAt: tile.updatedAt
|
||||||
})
|
} as TextureData
|
||||||
|
await loadTexture(scene, textureData)
|
||||||
// Add blank tile
|
}
|
||||||
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height }))
|
|
||||||
|
|
||||||
// Create layer
|
|
||||||
const layer = tileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
|
|
||||||
|
|
||||||
layer.setDepth(0)
|
|
||||||
layer.setCullPadding(2, 2)
|
|
||||||
|
|
||||||
return layer
|
|
||||||
}
|
|
||||||
|
|
||||||
//Recursive Array Clone
|
|
||||||
export function cloneArray(arr: any[]): any[] {
|
|
||||||
return arr.map((item) => (item instanceof Array ? cloneArray(item) : item))
|
|
||||||
}
|
}
|
77
src/composables/pointerHandlers/useGamePointerHandlers.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import config from '@/application/config'
|
||||||
|
import { getTile, tileToWorldXY } from '@/composables/mapComposable'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { ref, type Ref } from 'vue'
|
||||||
|
|
||||||
|
export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const pointerStartPosition = ref({ x: 0, y: 0 })
|
||||||
|
const dragThreshold = 5 // pixels
|
||||||
|
|
||||||
|
function updateWaypoint(worldX: number, worldY: number) {
|
||||||
|
const pointerTile = getTile(layer, worldX, worldY)
|
||||||
|
if (!pointerTile) {
|
||||||
|
waypoint.value.visible = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
||||||
|
if (!worldPoint.worldPositionX || !worldPoint.worldPositionX) return
|
||||||
|
|
||||||
|
waypoint.value = {
|
||||||
|
visible: true,
|
||||||
|
x: worldPoint.worldPositionX,
|
||||||
|
y: worldPoint.worldPositionY + config.tile_size.height + 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
||||||
|
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
|
||||||
|
gameStore.setPlayerDraggingCamera(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||||
|
updateWaypoint(pointer.worldX, pointer.worldY)
|
||||||
|
|
||||||
|
if (!gameStore.game.isPlayerDraggingCamera) return
|
||||||
|
|
||||||
|
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
||||||
|
|
||||||
|
// If the distance is less than the drag threshold, return
|
||||||
|
// We do this to prevent the camera from scrolling too quickly
|
||||||
|
if (distance <= dragThreshold) return
|
||||||
|
|
||||||
|
camera.setScroll(camera.scrollX - (pointer.x - pointer.prevPosition.x) / camera.zoom, camera.scrollY - (pointer.y - pointer.prevPosition.y) / camera.zoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
||||||
|
gameStore.setPlayerDraggingCamera(false)
|
||||||
|
|
||||||
|
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
||||||
|
|
||||||
|
// If the distance is greater than the drag threshold, return
|
||||||
|
// We do this to prevent the camera from scrolling too quickly
|
||||||
|
if (distance > dragThreshold) return
|
||||||
|
|
||||||
|
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
|
||||||
|
if (!pointerTile) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit('map:character:move', {
|
||||||
|
positionX: pointerTile.x,
|
||||||
|
positionY: pointerTile.y
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupPointerHandlers = () => {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupPointerHandlers = () => {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { setupPointerHandlers, cleanupPointerHandlers }
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
import config from '@/application/config'
|
||||||
|
import { getTile, tileToWorldXY } from '@/composables/mapComposable'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
|
import { computed, ref, type Ref } from 'vue'
|
||||||
|
|
||||||
|
export function useMapEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const mapEditorStore = useMapEditorStore()
|
||||||
|
const isMoveTool = computed(() => mapEditorStore.tool === 'move')
|
||||||
|
const pointerStartPosition = ref({ x: 0, y: 0 })
|
||||||
|
const dragThreshold = 5 // pixels
|
||||||
|
|
||||||
|
function updateWaypoint(worldX: number, worldY: number) {
|
||||||
|
const pointerTile = getTile(layer, worldX, worldY)
|
||||||
|
if (!pointerTile) {
|
||||||
|
waypoint.value.visible = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
||||||
|
if (!worldPoint.worldPositionX || !worldPoint.worldPositionX) return
|
||||||
|
|
||||||
|
waypoint.value = {
|
||||||
|
visible: true,
|
||||||
|
x: worldPoint.worldPositionX,
|
||||||
|
y: worldPoint.worldPositionY + config.tile_size.height + 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
||||||
|
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
|
||||||
|
if (isMoveTool.value || pointer.event.shiftKey) {
|
||||||
|
gameStore.setPlayerDraggingCamera(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragMap(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (!gameStore.game.isPlayerDraggingCamera) return
|
||||||
|
|
||||||
|
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
||||||
|
|
||||||
|
if (distance <= dragThreshold) return
|
||||||
|
|
||||||
|
camera.setScroll(camera.scrollX - (pointer.x - pointer.prevPosition.x) / camera.zoom, camera.scrollY - (pointer.y - pointer.prevPosition.y) / camera.zoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (isMoveTool.value || pointer.event.shiftKey) {
|
||||||
|
dragMap(pointer)
|
||||||
|
}
|
||||||
|
updateWaypoint(pointer.worldX, pointer.worldY)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
||||||
|
gameStore.setPlayerDraggingCamera(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleZoom(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
|
||||||
|
const deltaY = pointer.event.deltaY
|
||||||
|
const zoomLevel = camera.zoom - deltaY * 0.005
|
||||||
|
if (zoomLevel > 0 && zoomLevel < 3) {
|
||||||
|
camera.setZoom(zoomLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupPointerHandlers = () => {
|
||||||
|
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)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupPointerHandlers = () => {
|
||||||
|
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)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { setupPointerHandlers, cleanupPointerHandlers }
|
||||||
|
}
|
15
src/composables/useCameraControls.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
|
|
||||||
|
export function useCameraControls(scene: Phaser.Scene) {
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const camera = scene.cameras.main
|
||||||
|
|
||||||
|
const onPointerDown = () => gameStore.setPlayerDraggingCamera(true)
|
||||||
|
const onPointerUp = () => gameStore.setPlayerDraggingCamera(false)
|
||||||
|
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, onPointerDown)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_UP, onPointerUp)
|
||||||
|
|
||||||
|
return { camera }
|
||||||
|
}
|
@ -1,160 +0,0 @@
|
|||||||
import config from '@/application/config'
|
|
||||||
import { Direction } from '@/application/enums'
|
|
||||||
import { type MapCharacter } from '@/application/types'
|
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/services/mapService'
|
|
||||||
import { loadSpriteTextures } from '@/services/textureService'
|
|
||||||
import { CharacterTypeStorage } from '@/storage/storages'
|
|
||||||
import { refObj } from 'phavuer'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
|
|
||||||
export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phaser.Tilemaps.Tilemap, mapCharacter: MapCharacter) {
|
|
||||||
const characterContainer = refObj<Phaser.GameObjects.Container>()
|
|
||||||
const characterSpriteId = ref('')
|
|
||||||
const characterSprite = refObj<Phaser.GameObjects.Sprite>()
|
|
||||||
const currentPositionX = ref(0)
|
|
||||||
const currentPositionY = ref(0)
|
|
||||||
const isometricDepth = ref(1)
|
|
||||||
const isInitialPosition = ref(true)
|
|
||||||
const tween = ref<Phaser.Tweens.Tween | null>(null)
|
|
||||||
|
|
||||||
const updateIsometricDepth = (positionX: number, positionY: number) => {
|
|
||||||
isometricDepth.value = calculateIsometricDepth(positionX, positionY)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePosition = (positionX: number, positionY: number) => {
|
|
||||||
const newPositionX = tileToWorldX(tilemap, positionX, positionY)
|
|
||||||
const newPositionY = tileToWorldY(tilemap, positionX, positionY)
|
|
||||||
|
|
||||||
if (isInitialPosition.value) {
|
|
||||||
currentPositionX.value = newPositionX
|
|
||||||
currentPositionY.value = newPositionY
|
|
||||||
isInitialPosition.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tween.value?.isPlaying()) {
|
|
||||||
tween.value.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
const tileDistance = Math.sqrt(Math.pow((newPositionX - currentPositionX.value) / config.tile_size.width, 2) + Math.pow((newPositionY - currentPositionY.value) / config.tile_size.height, 2))
|
|
||||||
|
|
||||||
const baseDuration = 300 // milliseconds per tile
|
|
||||||
const duration = Math.min(baseDuration * tileDistance, baseDuration)
|
|
||||||
|
|
||||||
tween.value = tilemap.scene.tweens.add({
|
|
||||||
targets: characterContainer.value,
|
|
||||||
x: newPositionX,
|
|
||||||
y: newPositionY,
|
|
||||||
duration,
|
|
||||||
ease: 'Linear',
|
|
||||||
onStart: () => {
|
|
||||||
updateIsometricDepth(positionX, positionY)
|
|
||||||
},
|
|
||||||
onUpdate: () => {
|
|
||||||
updateIsometricDepth(positionX, positionY)
|
|
||||||
currentPositionX.value = characterContainer.value?.x ?? currentPositionX.value
|
|
||||||
currentPositionY.value = characterContainer.value?.y ?? currentPositionY.value
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
updateIsometricDepth(positionX, positionY)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const playAnimation = (animation: string, loop = false, ignoreIfPlaying = true) => {
|
|
||||||
if (!characterSprite.value || !characterSpriteId.value) return
|
|
||||||
|
|
||||||
const fullAnimationName = `${characterSpriteId.value}-${animation}_${currentDirection.value}`
|
|
||||||
|
|
||||||
// Remove any existing animation complete listeners
|
|
||||||
characterSprite.value.off(Phaser.Animations.Events.ANIMATION_COMPLETE)
|
|
||||||
|
|
||||||
// Add new listener
|
|
||||||
characterSprite.value.on(Phaser.Animations.Events.ANIMATION_COMPLETE, () => {
|
|
||||||
characterSprite.value!.setFrame(0)
|
|
||||||
characterSprite.value!.setTexture(charTexture.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
characterSprite.value.anims.play(
|
|
||||||
{
|
|
||||||
key: fullAnimationName,
|
|
||||||
repeat: loop ? -1 : 0
|
|
||||||
},
|
|
||||||
ignoreIfPlaying
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const calcDirection = (oldPositionX: number, oldPositionY: number, newPositionX: number, newPositionY: number): Direction => {
|
|
||||||
if (newPositionY < oldPositionY || newPositionX < oldPositionX) return Direction.NEGATIVE
|
|
||||||
if (newPositionX > oldPositionX || newPositionY > oldPositionY) return Direction.POSITIVE
|
|
||||||
return Direction.UNCHANGED
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFlippedX = computed(() => [6, 4].includes(mapCharacter.character.rotation ?? 0))
|
|
||||||
|
|
||||||
const currentDirection = computed(() => {
|
|
||||||
return [0, 6].includes(mapCharacter.character.rotation ?? 0) ? 'left_up' : 'right_down'
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentAction = computed(() => {
|
|
||||||
return mapCharacter.isMoving ? 'walk' : 'idle'
|
|
||||||
})
|
|
||||||
|
|
||||||
const charTexture = computed(() => {
|
|
||||||
const spriteId = characterSpriteId.value ?? 'idle_right_down'
|
|
||||||
return `${spriteId}-${currentAction.value}_${currentDirection.value}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateSprite = () => {
|
|
||||||
if (!characterSprite.value) return
|
|
||||||
|
|
||||||
if (mapCharacter.isMoving) {
|
|
||||||
characterSprite.value.anims.play(charTexture.value, true)
|
|
||||||
} else {
|
|
||||||
characterSprite.value.anims.stop()
|
|
||||||
characterSprite.value.setFrame(0)
|
|
||||||
characterSprite.value.setTexture(charTexture.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const initializeSprite = async () => {
|
|
||||||
const characterTypeStorage = new CharacterTypeStorage()
|
|
||||||
const spriteId = await characterTypeStorage.getSpriteId(mapCharacter.character.characterType!)
|
|
||||||
if (!spriteId) return
|
|
||||||
|
|
||||||
characterSpriteId.value = spriteId
|
|
||||||
await loadSpriteTextures(scene, spriteId)
|
|
||||||
|
|
||||||
if (characterContainer.value) {
|
|
||||||
characterContainer.value.setName(mapCharacter.character.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (characterSprite.value) {
|
|
||||||
characterSprite.value.setTexture(charTexture.value)
|
|
||||||
characterSprite.value.setFlipX(isFlippedX.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePosition(mapCharacter.character.positionX, mapCharacter.character.positionY)
|
|
||||||
updateIsometricDepth(mapCharacter.character.positionX, mapCharacter.character.positionY)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
tween.value?.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
characterContainer,
|
|
||||||
characterSpriteId,
|
|
||||||
characterSprite,
|
|
||||||
currentPositionX,
|
|
||||||
currentPositionY,
|
|
||||||
isometricDepth,
|
|
||||||
isFlippedX,
|
|
||||||
updatePosition,
|
|
||||||
playAnimation,
|
|
||||||
calcDirection,
|
|
||||||
updateSprite,
|
|
||||||
initializeSprite,
|
|
||||||
cleanup
|
|
||||||
}
|
|
||||||
}
|
|