Worked on zone objects, tile tags and searching
This commit is contained in:
parent
2cfe80de11
commit
2fe6f8d9c0
90
package-lock.json
generated
90
package-lock.json
generated
@ -2137,30 +2137,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@volar/language-core": {
|
"node_modules/@volar/language-core": {
|
||||||
"version": "2.4.0-alpha.12",
|
"version": "2.4.0-alpha.13",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.0-alpha.12.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.0-alpha.13.tgz",
|
||||||
"integrity": "sha512-Dj9qTifcGGgzFLfMbU5dCo13kHyNuEyvPJhtWDnoVBBmgwW3GMwFmgWnNxBhjf63m5x0gux1okaxX2CLN7qSww==",
|
"integrity": "sha512-tHeJVIRTJ3dlsdNyRjBlqdKHocWkgORM5eXgf6xcGERoXYe6vBpQpxJgpK1pehA8psXNPqkMN1ryBseA0B+m8A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/source-map": "2.4.0-alpha.12"
|
"@volar/source-map": "2.4.0-alpha.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@volar/source-map": {
|
"node_modules/@volar/source-map": {
|
||||||
"version": "2.4.0-alpha.12",
|
"version": "2.4.0-alpha.13",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.0-alpha.12.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.0-alpha.13.tgz",
|
||||||
"integrity": "sha512-LXATFSj4D7T9sEm7FFj6iBgHjKjrdhAgRPcechVKiNCMQdr3r3GVkkeu8aM+1peaMH3LsCqoDxVZEmh2r7CHiw==",
|
"integrity": "sha512-NABqcuA9QpHsU3FnA5BENP3PI1FOb6hDxqkV1KAHP7gt4fgfQOqSCWpqj3QAS7RV0PtiKTxiDMIJ7doMBhNm7w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@volar/typescript": {
|
"node_modules/@volar/typescript": {
|
||||||
"version": "2.4.0-alpha.12",
|
"version": "2.4.0-alpha.13",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.0-alpha.12.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.0-alpha.13.tgz",
|
||||||
"integrity": "sha512-mLg+OQauMTv/+08a7WBWJo1sev/wc8t2is0zhBZIlFU+j5mG89FM4+4089c2p/zoUFZ400Q/VNg2BPfhpZ8wSA==",
|
"integrity": "sha512-zW/MOPA9SwkCuuVPqADDYfAEPAh68aJQG3/EAqDYozSuK2YNYHEAC0BWYZESSNZC6jxwwx7w0U82fBkDZ9hHEw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/language-core": "2.4.0-alpha.12",
|
"@volar/language-core": "2.4.0-alpha.13",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"vscode-uri": "^3.0.8"
|
"vscode-uri": "^3.0.8"
|
||||||
}
|
}
|
||||||
@ -2667,9 +2667,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.12.0",
|
"version": "8.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||||
"integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
|
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -2959,9 +2959,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001639",
|
"version": "1.0.30001640",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz",
|
||||||
"integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==",
|
"integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -3474,6 +3474,27 @@
|
|||||||
"xmlhttprequest-ssl": "~2.0.0"
|
"xmlhttprequest-ssl": "~2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/engine.io-parser": {
|
"node_modules/engine.io-parser": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
|
||||||
@ -5192,9 +5213,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/npm-run-all2": {
|
"node_modules/npm-run-all2": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-6.2.1.tgz",
|
||||||
"integrity": "sha512-wA7yVIkthe6qJBfiJ2g6aweaaRlw72itsFGF6HuwCHKwtwAx/4BY1vVpk6bw6lS8RLMsexoasOkd0aYOmsFG7Q==",
|
"integrity": "sha512-eX4MWsUYOSm1FhPh9LPAWbqq2quny3u8gEEWIY4HHECi10qOyi1dNaJFCyOOv2uP05ZuTPETwS2p1GZk9oLJsw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -5213,7 +5234,7 @@
|
|||||||
"run-s": "bin/run-s/index.js"
|
"run-s": "bin/run-s/index.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.18.0 || >=16.0.0",
|
"node": "^14.18.0 || ^16.13.0 || >=18.0.0",
|
||||||
"npm": ">= 8"
|
"npm": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -5636,9 +5657,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pkg-types": {
|
"node_modules/pkg-types": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz",
|
||||||
"integrity": "sha512-VEGf1he2DR5yowYRl0XJhWJq5ktm9gYIsH+y8sNJpHlxch7JPDaufgrsl4vYjd9hMUY8QVjoNncKbow9I7exyA==",
|
"integrity": "sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -6665,9 +6686,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.0.16",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
|
||||||
"integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==",
|
"integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -6723,14 +6744,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
|
||||||
"integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==",
|
"integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.39",
|
||||||
"rollup": "^4.13.0"
|
"rollup": "^4.13.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -7273,9 +7294,10 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.17.1",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
2
public/assets/icons/zoneEditor/paint.svg
Normal file
2
public/assets/icons/zoneEditor/paint.svg
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><g fill="#000"><path clip-rule="evenodd" d="m6 5c0-1.65685 1.34315-3 3-3 1.6569 0 3 1.34315 3 3v.99943c.9228.01637 1.808.52521 2.2889 1.39653l3.3612 6.08924c.7139 1.2934.3069 2.9672-.9737 3.7405l-7.27713 4.3945c-1.30875.7904-2.96071.3017-3.68814-1.0161l-3.36116-6.0893c-.71395-1.2934-.30694-2.9671.97368-3.7405l2.67635-1.61618zm4 0v1.74258l-2 1.20777v-2.95035c0-.55228.44772-1 1-1s1 .44772 1 1z" fill-rule="evenodd"/><path d="m19 16c-.4577 0-.7691.253-.9239.4148-.1627.1699-.278.3663-.3589.5273-.1652.3287-.2937.7288-.3908 1.094-.1902.7157-.3264 1.5619-.3264 1.9639 0 1.1046.8954 2 2 2s2-.8954 2-2c0-.402-.1362-1.2482-.3264-1.9639-.0971-.3652-.2256-.7653-.3908-1.094-.0809-.161-.1962-.3574-.3589-.5273-.1548-.1618-.4662-.4148-.9239-.4148z"/></g></svg>
|
After Width: | Height: | Size: 847 B |
@ -1,44 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chip-container">
|
<div class="chip-container">
|
||||||
<div v-for="(chip, i) in chips" :key="i" class="chip">
|
<div v-for="(chip, i) in modelValue" :key="i" class="chip">
|
||||||
<span>{{ chip }}</span>
|
<span>{{ chip }}</span>
|
||||||
<i class="delete-icon" @click="deleteChip(i)">X</i>
|
<i class="delete-icon" @click="deleteChip(i)">X</i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input v-model="currentInput" @keyup.enter="saveChip" @keydown.delete="backspaceDelete" />
|
||||||
v-model="currentInput"
|
|
||||||
@keypress.enter="saveChip"
|
|
||||||
@keydown.delete="backspaceDelete"
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineProps } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const modelValue = defineModel('modelValue', { type: Array, default: () => [] })
|
||||||
set: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const chips = ref([])
|
|
||||||
const currentInput = ref('')
|
const currentInput = ref('')
|
||||||
|
|
||||||
const saveChip = () => {
|
const saveChip = () => {
|
||||||
if ((props.set && !chips.value.includes(currentInput.value)) || !props.set) {
|
if (currentInput.value.trim() && !modelValue.value.includes(currentInput.value)) {
|
||||||
chips.value.push(currentInput.value)
|
modelValue.value = [...modelValue.value, currentInput.value]
|
||||||
|
currentInput.value = ''
|
||||||
}
|
}
|
||||||
currentInput.value = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteChip = (index) => {
|
const deleteChip = (index) => {
|
||||||
chips.value.splice(index, 1)
|
modelValue.value = modelValue.value.filter((_, i) => i !== index)
|
||||||
}
|
}
|
||||||
|
|
||||||
const backspaceDelete = (event) => {
|
const backspaceDelete = (event) => {
|
||||||
if (event.key === 'Backspace' && currentInput.value === '') {
|
if (event.key === 'Backspace' && currentInput.value === '' && modelValue.value.length > 0) {
|
||||||
chips.value.pop()
|
modelValue.value = modelValue.value.slice(0, -1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -78,11 +68,7 @@ const backspaceDelete = (event) => {
|
|||||||
outline: none;
|
outline: none;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
color:white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :isModalOpen="gmPanelStore.isOpen" :modal-width="1000" :modal-height="650">
|
<Modal :isModalOpen="gmPanelStore.isOpen" @modal:close="() => gmPanelStore.toggle()" :modal-width="1000" :modal-height="650">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="modal-title">GM Panel</h3>
|
<h3 class="modal-title">GM Panel</h3>
|
||||||
<div class="gm-selector">
|
<div class="gm-selector">
|
||||||
|
@ -16,7 +16,6 @@ import Modal from '@/components/utilities/Modal.vue'
|
|||||||
import { useZoneEditorStore } from '@/stores/zoneEditor'
|
import { useZoneEditorStore } from '@/stores/zoneEditor'
|
||||||
import { useGmPanelStore } from '@/stores/gmPanel'
|
import { useGmPanelStore } from '@/stores/gmPanel'
|
||||||
|
|
||||||
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const gmPanelStore = useGmPanelStore()
|
const gmPanelStore = useGmPanelStore()
|
||||||
</script>
|
</script>
|
||||||
|
@ -31,16 +31,18 @@
|
|||||||
<!-- Asset details -->
|
<!-- Asset details -->
|
||||||
<div class="asset-info">
|
<div class="asset-info">
|
||||||
<TileDetails :tile="selectedTile" v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
<TileDetails :tile="selectedTile" v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
||||||
|
<ObjectDetails :object="selectedTile" v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useSocketStore } from '@/stores/socket'
|
import { useSocketStore } from '@/stores/socket'
|
||||||
import TileList from '@/components/utilities/assetManager/partials/TileList.vue'
|
import TileList from '@/components/utilities/assetManager/partials/TileList.vue'
|
||||||
import TileDetails from '@/components/utilities/assetManager/partials/TileDetails.vue'
|
import TileDetails from '@/components/utilities/assetManager/partials/TileDetails.vue'
|
||||||
import ObjectList from '@/components/utilities/assetManager/partials/ObjectList.vue'
|
import ObjectList from '@/components/utilities/assetManager/partials/ObjectList.vue'
|
||||||
|
import ObjectDetails from '@/components/utilities/assetManager/partials/ObjectDetails.vue'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManager'
|
import { useAssetManagerStore } from '@/stores/assetManager'
|
||||||
|
|
||||||
const socket = useSocketStore()
|
const socket = useSocketStore()
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="object-manager">
|
||||||
|
<div class="image-container">
|
||||||
|
<img :src="objectImageUrl" :alt="'Object ' + selectedObject" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-form asset-manager">
|
||||||
|
<form class="form-fields" @submit.prevent>
|
||||||
|
<div class="form-field name">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input class="input-cyan" type="text" name="name" placeholder="Wall #1" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field name">
|
||||||
|
<label for="name">Origin X</label>
|
||||||
|
<!-- @TODO only allow numbers here -->
|
||||||
|
<input class="input-cyan" type="text" name="name" placeholder="Origin X" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field name">
|
||||||
|
<label for="name">Origin Y</label>
|
||||||
|
<!-- @TODO only allow numbers here -->
|
||||||
|
<input class="input-cyan" type="text" name="name" placeholder="Origin Y" />
|
||||||
|
</div>
|
||||||
|
<div class="submit">
|
||||||
|
<button class="btn-cyan" type="button" @click="removeObject">Save</button>
|
||||||
|
<button class="btn-bordeaux" type="button" @click="removeObject">Remove</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted } from 'vue'
|
||||||
|
import { useAssetManagerStore } from '@/stores/assetManager'
|
||||||
|
import { useSocketStore } from '@/stores/socket'
|
||||||
|
import config from '@/config'
|
||||||
|
|
||||||
|
const socket = useSocketStore()
|
||||||
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
const selectedObject = computed(() => assetManagerStore.selectedObject)
|
||||||
|
const objectImageUrl = computed(() => `${config.server_endpoint}/assets/objects/${selectedObject.value}.png`)
|
||||||
|
const objectDetails = computed(() => assetManagerStore.objectDetails)
|
||||||
|
|
||||||
|
function removeObject() {
|
||||||
|
socket.connection.emit('gm:object:remove', { object: selectedObject.value }, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to remove object')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshObjectList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshObjectList() {
|
||||||
|
socket.connection.emit('gm:object:list', {}, (response: string[]) => {
|
||||||
|
assetManagerStore.setObjectList(response)
|
||||||
|
assetManagerStore.setSelectedObject('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!selectedObject.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
socket.connection.emit('gm:object:details', { object: selectedObject.value }, (response: any) => {
|
||||||
|
assetManagerStore.setObjectDetails(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
assetManagerStore.setSelectedObject('')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
/**
|
||||||
|
* @TODO when scrolling here, the vertical line is cut off
|
||||||
|
*/
|
||||||
|
.modal-form {
|
||||||
|
padding: 10px; // @TODO why dis no work? fixme
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,45 +1,47 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="asset">
|
<div class="asset add-new">
|
||||||
|
<label for="upload-asset" class="file-upload">
|
||||||
|
<input id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
||||||
|
Upload object(s)
|
||||||
|
</label>
|
||||||
<input class="input-cyan search-field" placeholder="Search..." />
|
<input class="input-cyan search-field" placeholder="Search..." />
|
||||||
</div>
|
</div>
|
||||||
|
<a class="asset" :class="{ active: assetManagerStore.selectedObject === object }" v-for="(object, index) in assetManagerStore.objectList" :key="index" @click="assetManagerStore.setSelectedObject(object)">
|
||||||
<!-- TODO: use the passed :name in props to switch out assets-->
|
|
||||||
<a class="asset" :class="{ active: name === 'tiles' }" v-for="(tile, index) in tiles" :key="index">
|
|
||||||
<div class="asset-details">
|
<div class="asset-details">
|
||||||
<img :src="`${config.server_endpoint}/assets/tiles/${tile}`" />
|
<img :src="`${config.server_endpoint}/assets/objects/${object}.png`" alt="Object" />
|
||||||
<span class="asset-name">{{ tile }}</span>
|
<span class="asset-name">{{ object }}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useSocketStore } from '@/stores/socket'
|
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { onMounted, ref, defineProps } from 'vue'
|
import { useSocketStore } from '@/stores/socket'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
const props = defineProps<{ name: string }>()
|
import { useAssetManagerStore } from '@/stores/assetManager'
|
||||||
|
|
||||||
const socket = useSocketStore()
|
const socket = useSocketStore()
|
||||||
const tileUploadField = ref(null)
|
const objectUploadField = ref(null)
|
||||||
const tiles = ref()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
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
|
||||||
socket.connection.emit('gm:tile:upload', files, (response: boolean) => {
|
socket.connection.emit('gm:object:upload', files, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to upload tile')
|
if (config.development) console.error('Failed to upload object')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
socket.connection.emit('gm:tile:list', {}, (response: string[]) => {
|
socket.connection.emit('gm:object:list', {}, (response: string[]) => {
|
||||||
tiles.value = response
|
assetManagerStore.setObjectList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
socket.connection.emit('gm:tile:list', {}, (response: string[]) => {
|
socket.connection.emit('gm:object:list', {}, (response: string[]) => {
|
||||||
tiles.value = response
|
if (config.development) console.log(response)
|
||||||
|
assetManagerStore.setObjectList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -48,9 +50,10 @@ onMounted(() => {
|
|||||||
@import '@/assets/scss/main';
|
@import '@/assets/scss/main';
|
||||||
|
|
||||||
.asset {
|
.asset {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&.add-new {
|
&.add-new {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
gap: 10px 20px;
|
gap: 10px 20px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
.asset-name {
|
.asset-name {
|
||||||
|
@ -1,45 +1,94 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="image-container">
|
<div class="tile-manager">
|
||||||
<img :src="`${config.server_endpoint}/assets/tiles/${assetManagerStore.selectedTile}.png`" alt="Tile" />
|
<div class="image-container">
|
||||||
</div>
|
<img :src="tileImageUrl" :alt="'Tile ' + selectedTile" />
|
||||||
<div class="modal-form asset-manager">
|
</div>
|
||||||
<form class="form-fields" @submit.prevent>
|
<div class="modal-form asset-manager">
|
||||||
<div class="form-field name">
|
<form class="form-fields" @submit.prevent>
|
||||||
<label for="name">Name</label>
|
<div class="form-field tags">
|
||||||
<input class="input-cyan" type="text" name="name" placeholder="E.g. grass" />
|
<label for="tags">Tags</label>
|
||||||
</div>
|
<ChipsInput v-model="tags" @update:modelValue="handleTagsUpdate" />
|
||||||
<div class="form-field tags">
|
</div>
|
||||||
<label for="tags">Tags</label>
|
<div class="submit">
|
||||||
<ChipsInput />
|
<button class="btn-bordeaux" type="button" @click="removeTile">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="submit">
|
</form>
|
||||||
<button class="btn-cyan" type="submit">Save</button>
|
</div>
|
||||||
<button class="btn-bordeaux" type="button" @click="removeTile">Remove</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import { ref, computed, watch, onBeforeUnmount, onMounted } from 'vue'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManager'
|
import { useAssetManagerStore } from '@/stores/assetManager'
|
||||||
import { useSocketStore } from '@/stores/socket'
|
import { useSocketStore } from '@/stores/socket'
|
||||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||||
|
import config from '@/config'
|
||||||
|
|
||||||
const socket = useSocketStore()
|
const socket = useSocketStore()
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
function removeTile() {
|
const tags = ref<string[]>([])
|
||||||
socket.connection.emit('gm:tile:remove', { tile: assetManagerStore.selectedTile }, (response: boolean) => {
|
|
||||||
if (!response) {
|
const selectedTile = computed(() => assetManagerStore.selectedTile)
|
||||||
return
|
|
||||||
}
|
const tileImageUrl = computed(() => `${config.server_endpoint}/assets/tiles/${selectedTile.value}.png`)
|
||||||
socket.connection.emit('gm:tile:list', {}, (response: string[]) => {
|
|
||||||
assetManagerStore.setTileList(response)
|
watch(selectedTile, fetchTileTags)
|
||||||
assetManagerStore.setSelectedTile('')
|
|
||||||
})
|
function fetchTileTags(tile: string) {
|
||||||
|
if (config.development) console.log('P241 selectedTile', tile)
|
||||||
|
socket.connection.emit('gm:tile:tags', { tile }, (response: string[]) => {
|
||||||
|
tags.value = response
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleTagsUpdate(newTags: string[]) {
|
||||||
|
if (config.development) console.log(newTags)
|
||||||
|
saveTags(newTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTags(tagsToSave: string[]) {
|
||||||
|
socket.connection.emit(
|
||||||
|
'gm:tile:tags:update',
|
||||||
|
{
|
||||||
|
tile: selectedTile.value,
|
||||||
|
tags: tagsToSave
|
||||||
|
},
|
||||||
|
(response: boolean) => {
|
||||||
|
if (!response) console.error('Failed to save tags')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTile() {
|
||||||
|
socket.connection.emit('gm:tile:remove', { tile: selectedTile.value }, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to remove tile')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshTileList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshTileList() {
|
||||||
|
socket.connection.emit('gm:tile:list', {}, (response: string[]) => {
|
||||||
|
assetManagerStore.setTileList(response)
|
||||||
|
assetManagerStore.setSelectedTile('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!selectedTile.value) return
|
||||||
|
fetchTileTags(selectedTile.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
assetManagerStore.setSelectedTile('')
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss"></style>
|
<style lang="scss">
|
||||||
|
.modal-form {
|
||||||
|
padding: 10px; // @TODO why dis no work? fixme
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -29,7 +29,7 @@ const handleFileUpload = (e: Event) => {
|
|||||||
if (!files) return
|
if (!files) return
|
||||||
socket.connection.emit('gm:tile:upload', files, (response: boolean) => {
|
socket.connection.emit('gm:tile:upload', files, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to upload tile')
|
if (config.development) console.error('Failed to upload tile')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
socket.connection.emit('gm:tile:list', {}, (response: string[]) => {
|
socket.connection.emit('gm:tile:list', {}, (response: string[]) => {
|
||||||
@ -40,6 +40,7 @@ const handleFileUpload = (e: Event) => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
socket.connection.emit('gm:tile:list', {}, (response: string[]) => {
|
socket.connection.emit('gm:tile:list', {}, (response: string[]) => {
|
||||||
|
if (config.development) console.log(response)
|
||||||
assetManagerStore.setTileList(response)
|
assetManagerStore.setTileList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,120 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Teleport to="body">
|
|
||||||
<Modal v-if="isModalOpen" :isModalOpen="true" :closable="false" :modal-width="745" :modal-height="460">
|
|
||||||
<template #modalHeader>
|
|
||||||
<h3 class="modal-title">Decorations</h3>
|
|
||||||
</template>
|
|
||||||
<template #modalBody>
|
|
||||||
<div class="container decorations">
|
|
||||||
<div class="buttons">
|
|
||||||
<button class="btn-cyan" @click="zoneEditorStore.setDrawMode('tile')">Walls</button>
|
|
||||||
<button class="btn-cyan" @click="zoneEditorStore.setDrawMode('tile')">Decorations</button>
|
|
||||||
<button class="btn-cyan" @click="zoneEditorStore.setDrawMode('tile')">NPC</button>
|
|
||||||
</div>
|
|
||||||
<canvas ref="canvas" :width="decorationWidth" :height="decorationHeight" style="display: none"></canvas>
|
|
||||||
<div class="decorations">
|
|
||||||
<img v-for="(decoration, index) in decorations" :key="index" :src="decoration" alt="Decoration" @click="zoneEditorStore.setSelectedDecoration(index)" :class="{ selected: zoneEditorStore.selectedDecoration && zoneEditorStore.selectedDecoration === index }" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted, nextTick } from 'vue'
|
|
||||||
import config from '@/config'
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditor'
|
|
||||||
|
|
||||||
const decorationWidth = config.wall_size.x
|
|
||||||
const decorationHeight = config.wall_size.y
|
|
||||||
const decorations = ref<number[][]>([])
|
|
||||||
const selectedDecoration = ref<number | null>(null)
|
|
||||||
const canvas = ref<HTMLCanvasElement | null>(null)
|
|
||||||
const isModalOpen = ref(false)
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
|
||||||
|
|
||||||
// Hardcoded image path
|
|
||||||
const imagePath = '/assets/zone/walls.png'
|
|
||||||
|
|
||||||
const loadImage = (src: string): Promise<HTMLImageElement> => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const img = new Image()
|
|
||||||
img.onload = () => resolve(img)
|
|
||||||
img.src = src
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitDecorations = (img: HTMLImageElement) => {
|
|
||||||
if (!canvas.value) {
|
|
||||||
console.error('Canvas not found')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const ctx = canvas.value.getContext('2d')
|
|
||||||
if (!ctx) {
|
|
||||||
console.error('Failed to get canvas context')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const decorationsetWidth = img.width
|
|
||||||
const decorationsetHeight = img.height
|
|
||||||
const columns = Math.floor(decorationsetWidth / decorationWidth)
|
|
||||||
const rows = Math.floor(decorationsetHeight / decorationHeight)
|
|
||||||
|
|
||||||
decorations.value = []
|
|
||||||
selectedDecoration.value = null
|
|
||||||
|
|
||||||
for (let row = 0; row < rows; row++) {
|
|
||||||
for (let col = 0; col < columns; col++) {
|
|
||||||
const x = col * decorationWidth
|
|
||||||
const y = row * decorationHeight
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, decorationWidth, decorationHeight)
|
|
||||||
ctx.drawImage(img, x, y, decorationWidth, decorationHeight, 0, 0, decorationWidth, decorationHeight)
|
|
||||||
|
|
||||||
const decorationDataURL = canvas.value.toDataURL()
|
|
||||||
decorations.value.push(decorationDataURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectDecoration = (index: number) => {
|
|
||||||
selectedDecoration.value = index
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
isModalOpen.value = true
|
|
||||||
const img = await loadImage(imagePath)
|
|
||||||
await nextTick()
|
|
||||||
splitDecorations(img)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import '@/assets/scss/main';
|
|
||||||
|
|
||||||
.decorations {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.decorations img {
|
|
||||||
width: 30px;
|
|
||||||
height: 130px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
transition: border 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.decorations img.selected {
|
|
||||||
border: 2px solid $red;
|
|
||||||
}
|
|
||||||
</style>
|
|
65
src/components/utilities/zoneEditor/Objects.vue
Normal file
65
src/components/utilities/zoneEditor/Objects.vue
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Modal v-if="isModalOpen" :isModalOpen="true" :closable="false" :modal-width="645" :modal-height="260">
|
||||||
|
<template #modalHeader>
|
||||||
|
<h3 class="modal-title">Objects</h3>
|
||||||
|
</template>
|
||||||
|
<template #modalBody>
|
||||||
|
<div class="container objects">
|
||||||
|
<div class="objects">
|
||||||
|
<img v-for="(object, index) in objects" :key="index" :src="`${config.server_endpoint}/assets/objects/${object}.png`" alt="Object" @click="zoneEditorStore.setSelectedObject(object)" :class="{ selected: zoneEditorStore.selectedObject && zoneEditorStore.selectedObject === object }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/config'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditor'
|
||||||
|
import { useSocketStore } from '@/stores/socket'
|
||||||
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
|
|
||||||
|
const socket = useSocketStore()
|
||||||
|
const objects = ref<string[]>([])
|
||||||
|
const isModalOpen = ref(false)
|
||||||
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
isModalOpen.value = true
|
||||||
|
socket.connection.emit('gm:object:list', {}, (response: string[]) => {
|
||||||
|
objects.value = response
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '@/assets/scss/main.scss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
@TODO add masonry layout
|
||||||
|
https://www.smashingmagazine.com/native-css-masonry-layout-css-grid/
|
||||||
|
*/
|
||||||
|
|
||||||
|
.objects {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objects img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: border 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objects img.selected {
|
||||||
|
border: 2px solid $red;
|
||||||
|
}
|
||||||
|
</style>
|
@ -31,7 +31,7 @@ const zoneEditorStore = useZoneEditorStore()
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
isModalOpen.value = true
|
isModalOpen.value = true
|
||||||
socket.connection.emit('gm:tile:list', {}, (response: string[]) => {
|
socket.connection.emit('gm:tile:list', {}, (response: string[]) => {
|
||||||
tiles.value = response;
|
tiles.value = response
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="options" v-show="selectPencilOpen && zoneEditorStore.tool === 'pencil'">
|
<div class="options" v-show="selectPencilOpen && zoneEditorStore.tool === 'pencil'">
|
||||||
<span class="option" @click="setDrawMode('tile')">Tile</span>
|
<span class="option" @click="setDrawMode('tile')">Tile</span>
|
||||||
<span class="option" @click="setDrawMode('decoration')">Decoration</span>
|
<span class="option" @click="setDrawMode('object')">Object</span>
|
||||||
<span class="option" @click="setDrawMode('teleport')">Teleport</span>
|
<span class="option" @click="setDrawMode('teleport')">Teleport</span>
|
||||||
<span class="option" @click="setDrawMode('blocking tile')">Blocking tile</span>
|
<span class="option" @click="setDrawMode('blocking tile')">Blocking tile</span>
|
||||||
</div>
|
</div>
|
||||||
@ -26,6 +26,12 @@
|
|||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<button class="tool paint" :class="{ active: zoneEditorStore.tool === 'paint' }" @click="zoneEditorStore.setTool('paint')">
|
||||||
|
<img src="/assets/icons/zoneEditor/paint.svg" alt="Paint bucket" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
<button class="tool eraser" :class="{ active: zoneEditorStore.tool === 'eraser' }" @click="zoneEditorStore.setTool('eraser')">
|
<button class="tool eraser" :class="{ active: zoneEditorStore.tool === 'eraser' }" @click="zoneEditorStore.setTool('eraser')">
|
||||||
<img src="/assets/icons/zoneEditor/eraser.svg" alt="Eraser" />
|
<img src="/assets/icons/zoneEditor/eraser.svg" alt="Eraser" />
|
||||||
<div class="select" v-if="zoneEditorStore.tool === 'eraser'">
|
<div class="select" v-if="zoneEditorStore.tool === 'eraser'">
|
||||||
@ -43,6 +49,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<button class="tool settings" @click="() => zoneEditorStore.toggleSettingsModal()">
|
<button class="tool settings" @click="() => zoneEditorStore.toggleSettingsModal()">
|
||||||
<img src="/assets/icons/zoneEditor/gear.svg" alt="Zone settings" />
|
<img src="/assets/icons/zoneEditor/gear.svg" alt="Zone settings" />
|
||||||
</button>
|
</button>
|
||||||
@ -61,9 +68,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { getTile, tileToWorldXY } from '@/services/zone'
|
import { getTile } from '@/services/zone'
|
||||||
import config from '@/config'
|
|
||||||
import { useZoneStore } from '@/stores/zone'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditor'
|
import { useZoneEditorStore } from '@/stores/zoneEditor'
|
||||||
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
@ -4,14 +4,13 @@
|
|||||||
<Controls :layer="tiles" />
|
<Controls :layer="tiles" />
|
||||||
<!-- @TODO: inside asset manager we need to be able to set the originX and originY per individial asset -->
|
<!-- @TODO: inside asset manager we need to be able to set the originX and originY per individial asset -->
|
||||||
<Container>
|
<Container>
|
||||||
<!-- <Image :texture="'wall1'" :x="pos.position_x" :y="pos.position_y" :originY="1.13" :originX="1" />-->
|
<!-- <Image :texture="'wall1'" :x="pos.position_x" :y="pos.position_y" :originY="1.13" :originX="1" />-->
|
||||||
<!-- <Image :texture="'wall1'" :x="pos2.position_x" :y="pos2.position_y" :originY="1.13" :originX="1" />-->
|
<Image v-for="object in zoneObjects" :key="object.id" :texture="object.object" :x="object.position_x" :y="object.position_y" />
|
||||||
<!-- <Image :texture="'wall2'" :x="pos3.position_x" :y="pos3.position_y" :originY="1.255" :originX="1" />-->
|
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Toolbar :layer="tiles" @eraser="eraser" @pencil="pencil" @save="save" />
|
<Toolbar :layer="tiles" @eraser="eraser" @pencil="pencil" @save="save" />
|
||||||
<Tiles v-if="(zoneEditorStore.tool === 'pencil' || zoneEditorStore.tool === 'eraser') && zoneEditorStore.drawMode === 'tile'" />
|
<Tiles v-if="(zoneEditorStore.tool === 'pencil' || zoneEditorStore.tool === 'eraser') && zoneEditorStore.drawMode === 'tile'" />
|
||||||
<Decorations v-if="(zoneEditorStore.tool === 'pencil' || zoneEditorStore.tool === 'eraser') && zoneEditorStore.drawMode === 'decoration'" />
|
<Objects v-if="(zoneEditorStore.tool === 'pencil' || zoneEditorStore.tool === 'eraser') && zoneEditorStore.drawMode === 'object'" />
|
||||||
<ZoneSettings v-if="zoneEditorStore.isSettingsModalShown" />
|
<ZoneSettings v-if="zoneEditorStore.isSettingsModalShown" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -20,17 +19,17 @@ import config from '@/config'
|
|||||||
import Tileset = Phaser.Tilemaps.Tileset
|
import Tileset = Phaser.Tilemaps.Tileset
|
||||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||||
import { Container, TilemapLayer as TilemapLayerC, useScene, Image } from 'phavuer'
|
import { Container, TilemapLayer as TilemapLayerC, useScene, Image } from 'phavuer'
|
||||||
import { onBeforeMount, onBeforeUnmount, onMounted, toRaw } from 'vue'
|
import { onBeforeMount, onBeforeUnmount, onMounted, ref, toRaw } from 'vue'
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
import { useSocketStore } from '@/stores/socket'
|
import { useSocketStore } from '@/stores/socket'
|
||||||
import Toolbar from '@/components/utilities/zoneEditor/Toolbar.vue'
|
import Toolbar from '@/components/utilities/zoneEditor/Toolbar.vue'
|
||||||
import Tiles from '@/components/utilities/zoneEditor/Tiles.vue'
|
import Tiles from '@/components/utilities/zoneEditor/Tiles.vue'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditor'
|
import { useZoneEditorStore } from '@/stores/zoneEditor'
|
||||||
import ZoneSettings from '@/components/utilities/zoneEditor/ZoneSettings.vue'
|
import ZoneSettings from '@/components/utilities/zoneEditor/ZoneSettings.vue'
|
||||||
import Decorations from '@/components/utilities/zoneEditor/Decorations.vue'
|
|
||||||
import { placeTile, tileToWorldXY } from '@/services/zone'
|
import { placeTile, tileToWorldXY } from '@/services/zone'
|
||||||
import GmPanel from '@/components/utilities/GmPanel.vue'
|
|
||||||
import { useAssetStore } from '@/stores/assets'
|
import { useAssetStore } from '@/stores/assets'
|
||||||
|
import Objects from '@/components/utilities/zoneEditor/Objects.vue'
|
||||||
|
import { randomUUID } from 'crypto'
|
||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const socket = useSocketStore()
|
const socket = useSocketStore()
|
||||||
@ -48,12 +47,20 @@ const zoneData = new Phaser.Tilemaps.MapData({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const zone = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
const zone = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
||||||
const tilesetImages: Tileset[] = [];
|
const tilesetImages: Tileset[] = []
|
||||||
|
|
||||||
|
type ZoneObject = {
|
||||||
|
id: string
|
||||||
|
object: string
|
||||||
|
position_x: number
|
||||||
|
position_y: number
|
||||||
|
}
|
||||||
|
const zoneObjects = ref<ZoneObject[]>([])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Walk through tiles and add them to the zone as tilesetImages
|
* Walk through tiles and add them to the zone as tilesetImages
|
||||||
*/
|
*/
|
||||||
let tileCount = 1;
|
let tileCount = 1
|
||||||
toRaw(assetStore.assets).forEach((asset) => {
|
toRaw(assetStore.assets).forEach((asset) => {
|
||||||
if (asset.group !== 'tiles') return
|
if (asset.group !== 'tiles') return
|
||||||
tilesetImages.push(zone.addTilesetImage(asset.key, asset.key, config.tile_size.x, config.tile_size.y, 0, 0, tileCount++) as Tileset)
|
tilesetImages.push(zone.addTilesetImage(asset.key, asset.key, config.tile_size.x, config.tile_size.y, 0, 0, tileCount++) as Tileset)
|
||||||
@ -65,7 +72,6 @@ const exampleTilesArray = Array.from({ length: zoneEditorStore.width }, () => Ar
|
|||||||
|
|
||||||
placeTile(zone, tiles, 0, 0, 'blank_tile')
|
placeTile(zone, tiles, 0, 0, 'blank_tile')
|
||||||
|
|
||||||
|
|
||||||
const pos = tileToWorldXY(tiles, 1, 1)
|
const pos = tileToWorldXY(tiles, 1, 1)
|
||||||
const pos2 = tileToWorldXY(tiles, 1, 2)
|
const pos2 = tileToWorldXY(tiles, 1, 2)
|
||||||
const pos3 = tileToWorldXY(tiles, 2, 1)
|
const pos3 = tileToWorldXY(tiles, 2, 1)
|
||||||
@ -108,11 +114,16 @@ function pencil(tile: Phaser.Tilemaps.Tile) {
|
|||||||
// zoneEditorStore.setTiles(tile.x, tile.y, zoneEditorStore.selectedTile)
|
// zoneEditorStore.setTiles(tile.x, tile.y, zoneEditorStore.selectedTile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (zoneEditorStore.drawMode === 'wall') {
|
if (zoneEditorStore.drawMode === 'object') {
|
||||||
// @TODO fix position
|
// @TODO fix position
|
||||||
if (zoneEditorStore.selectedWall === null) return
|
if (zoneEditorStore.selectedObject === null) return
|
||||||
walls.putTileAt(zoneEditorStore.selectedWall, tile.x, tile.y)
|
if (config.development) console.log('placing object', tile.x, tile.y, zoneEditorStore.selectedObject)
|
||||||
zoneEditorStore.updateWall(tile.x, tile.y, zoneEditorStore.selectedWall)
|
zoneObjects.value.push({
|
||||||
|
id: Math.random().toString(10),
|
||||||
|
object: zoneEditorStore.selectedObject,
|
||||||
|
position_x: tileToWorldXY(tiles, tile.x, tile.y).position_x,
|
||||||
|
position_y: tileToWorldXY(tiles, tile.x, tile.y).position_y
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +134,7 @@ function save() {
|
|||||||
width: zoneEditorStore.width,
|
width: zoneEditorStore.width,
|
||||||
height: zoneEditorStore.height,
|
height: zoneEditorStore.height,
|
||||||
tiles: zoneEditorStore.tiles,
|
tiles: zoneEditorStore.tiles,
|
||||||
walls: zoneEditorStore.walls
|
objects: zoneEditorStore.objects
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +85,7 @@ const preloadScene = (scene: Phaser.Scene) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
||||||
|
scene.load.image('blank_object', '/assets/zone/blank_tile.png')
|
||||||
scene.load.image('waypoint', '/assets/waypoint.png')
|
scene.load.image('waypoint', '/assets/waypoint.png')
|
||||||
scene.textures.addBase64(
|
scene.textures.addBase64(
|
||||||
'character',
|
'character',
|
||||||
|
@ -31,7 +31,7 @@ import { onMounted, ref } from 'vue'
|
|||||||
import { login, register } from '@/services/authentication'
|
import { login, register } from '@/services/authentication'
|
||||||
import { useNotificationStore } from '@/stores/notifications'
|
import { useNotificationStore } from '@/stores/notifications'
|
||||||
import { useSocketStore } from '@/stores/socket'
|
import { useSocketStore } from '@/stores/socket'
|
||||||
import {useAssetStore} from '@/stores/assets'
|
import { useAssetStore } from '@/stores/assets'
|
||||||
|
|
||||||
const bgm = ref('bgm')
|
const bgm = ref('bgm')
|
||||||
if (bgm.value.paused) {
|
if (bgm.value.paused) {
|
||||||
@ -50,7 +50,7 @@ onMounted(async () => {
|
|||||||
/**
|
/**
|
||||||
* Fetch assets from the server
|
* Fetch assets from the server
|
||||||
*/
|
*/
|
||||||
assetStore.fetchAssets();
|
assetStore.fetchAssets()
|
||||||
|
|
||||||
const response = await login('ethereal', 'kanker123')
|
const response = await login('ethereal', 'kanker123')
|
||||||
|
|
||||||
|
@ -27,9 +27,9 @@ export function tileToWorldXY(layer: Phaser.Tilemaps.TilemapLayer, pos_x: number
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function placeTile(zone: Tilemap, layer: TilemapLayer, x: number, y: number, tileName: string) {
|
export function placeTile(zone: Tilemap, layer: TilemapLayer, x: number, y: number, tileName: string) {
|
||||||
const tileImg = zone.getTileset(tileName) as Tileset;
|
const tileImg = zone.getTileset(tileName) as Tileset
|
||||||
if (!tileImg) return
|
if (!tileImg) return
|
||||||
layer.putTileAt(tileImg.firstgid, x, y);
|
layer.putTileAt(tileImg.firstgid, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateTilemap(scene: Phaser.Scene, width: number, height: number) {}
|
export function generateTilemap(scene: Phaser.Scene, width: number, height: number) {}
|
||||||
|
@ -1,20 +1,50 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
export const useAssetManagerStore = defineStore('assetManager', {
|
export const useAssetManagerStore = defineStore('assetManager', () => {
|
||||||
state: () => ({
|
const tileList = ref<string[]>([])
|
||||||
tileList: [] as string[],
|
const objectList = ref<string[]>([])
|
||||||
selectedTile: ''
|
const selectedTile = ref<string | null>(null)
|
||||||
}),
|
const selectedObject = ref<string | null>(null)
|
||||||
actions: {
|
const objectDetails = ref<Record<string, any>>({})
|
||||||
setTileList(tiles: string[]) {
|
|
||||||
this.tileList = tiles
|
function setTileList(tiles: string[]) {
|
||||||
},
|
tileList.value = tiles
|
||||||
setSelectedTile(tile: string) {
|
}
|
||||||
this.selectedTile = tile
|
|
||||||
},
|
function setObjectList(objects: string[]) {
|
||||||
reset() {
|
objectList.value = objects
|
||||||
this.tileList = []
|
}
|
||||||
this.selectedTile = ''
|
|
||||||
}
|
function setSelectedTile(tile: string) {
|
||||||
|
selectedTile.value = tile
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedObject(object: string) {
|
||||||
|
selectedObject.value = object
|
||||||
|
}
|
||||||
|
|
||||||
|
function setObjectDetails(object: Record<string, any>) {
|
||||||
|
objectDetails.value = object
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
tileList.value = []
|
||||||
|
selectedTile.value = null
|
||||||
|
selectedObject.value = null
|
||||||
|
objectDetails.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tileList,
|
||||||
|
objectList,
|
||||||
|
setTileList,
|
||||||
|
setObjectList,
|
||||||
|
selectedTile,
|
||||||
|
selectedObject,
|
||||||
|
setSelectedTile,
|
||||||
|
setSelectedObject,
|
||||||
|
objectDetails,
|
||||||
|
setObjectDetails
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -4,7 +4,7 @@ import config from '@/config'
|
|||||||
|
|
||||||
export const useGmPanelStore = defineStore('gmPanel', {
|
export const useGmPanelStore = defineStore('gmPanel', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
isOpen: false,
|
isOpen: false
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
toggle() {
|
toggle() {
|
||||||
@ -15,6 +15,6 @@ export const useGmPanelStore = defineStore('gmPanel', {
|
|||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
this.isOpen = false
|
this.isOpen = false
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -7,12 +7,11 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
|||||||
width: 10,
|
width: 10,
|
||||||
height: 10,
|
height: 10,
|
||||||
tiles: [] as number[][],
|
tiles: [] as number[][],
|
||||||
decorations: [] as number[][],
|
objects: [] as number[][],
|
||||||
tool: 'move',
|
tool: 'move',
|
||||||
drawMode: 'tile',
|
drawMode: 'tile',
|
||||||
selectedTile: '',
|
selectedTile: '',
|
||||||
selectedWall: null,
|
selectedObject: null,
|
||||||
selectedDecoration: null,
|
|
||||||
isSettingsModalShown: false
|
isSettingsModalShown: false
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
@ -34,11 +33,11 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
|||||||
updateTile(x: number, y: number, tile: number) {
|
updateTile(x: number, y: number, tile: number) {
|
||||||
this.tiles[y][x] = tile
|
this.tiles[y][x] = tile
|
||||||
},
|
},
|
||||||
setDecorations(decorations: number[][]) {
|
setObjects(objects: number[][]) {
|
||||||
this.decorations = decorations
|
this.objects = objects
|
||||||
},
|
},
|
||||||
updateDecoration(x: number, y: number, decoration: number) {
|
updateObject(x: number, y: number, object: number) {
|
||||||
this.decorations[y][x] = decoration
|
this.objects[y][x] = object
|
||||||
},
|
},
|
||||||
setTool(tool: string) {
|
setTool(tool: string) {
|
||||||
this.tool = tool
|
this.tool = tool
|
||||||
@ -49,11 +48,8 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
|||||||
setSelectedTile(tile: string) {
|
setSelectedTile(tile: string) {
|
||||||
this.selectedTile = tile
|
this.selectedTile = tile
|
||||||
},
|
},
|
||||||
setSelectedWall(wall: any) {
|
setSelectedObject(object: any) {
|
||||||
this.selectedWall = wall
|
this.selectedObject = object
|
||||||
},
|
|
||||||
setSelectedDecoration(decoration: any) {
|
|
||||||
this.selectedDecoration = decoration
|
|
||||||
},
|
},
|
||||||
toggleSettingsModal() {
|
toggleSettingsModal() {
|
||||||
this.isSettingsModalShown = !this.isSettingsModalShown
|
this.isSettingsModalShown = !this.isSettingsModalShown
|
||||||
@ -66,8 +62,7 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
|||||||
this.tool = 'move'
|
this.tool = 'move'
|
||||||
this.drawMode = 'tile'
|
this.drawMode = 'tile'
|
||||||
this.selectedTile = ''
|
this.selectedTile = ''
|
||||||
this.selectedWall = null
|
this.selectedObject = null
|
||||||
this.selectedDecoration = null
|
|
||||||
this.isSettingsModalShown = false
|
this.isSettingsModalShown = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
64
src/types.ts
64
src/types.ts
@ -3,7 +3,33 @@ export type Notification = {
|
|||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// User model
|
export type Asset = {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
group: 'tiles' | 'objects' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||||
|
type: 'base64' | 'link'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Object = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
origin_x: number
|
||||||
|
origin_y: number
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
ZoneObject: ZoneObject[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Item = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
stackable: boolean
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
characters: CharacterItem[]
|
||||||
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@ -11,7 +37,6 @@ export type User = {
|
|||||||
characters: Character[]
|
characters: Character[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Character model
|
|
||||||
export type Character = {
|
export type Character = {
|
||||||
id: number
|
id: number
|
||||||
userId: number
|
userId: number
|
||||||
@ -28,33 +53,47 @@ export type Character = {
|
|||||||
zoneId: number
|
zoneId: number
|
||||||
zone: Zone
|
zone: Zone
|
||||||
chats: Chat[]
|
chats: Chat[]
|
||||||
|
items: CharacterItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CharacterItem = {
|
||||||
|
id: number
|
||||||
|
characterId: number
|
||||||
|
character: Character
|
||||||
|
itemId: string
|
||||||
|
item: Item
|
||||||
|
quantity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TileTag = {
|
||||||
|
tile: string
|
||||||
|
tags: any // Using 'any' for Json type, consider using a more specific type if possible
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zone model
|
|
||||||
export type Zone = {
|
export type Zone = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
tiles: number[][]
|
tiles: any // Using 'any' for Json type, consider using a more specific type if possible
|
||||||
walls: number[][]
|
walls: any // Using 'any' for Json type, consider using a more specific type if possible
|
||||||
decorations: ZoneDecoration[]
|
zoneObjects: ZoneObject[]
|
||||||
characters: Character[]
|
characters: Character[]
|
||||||
chats: Chat[]
|
chats: Chat[]
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneDecoration = {
|
export type ZoneObject = {
|
||||||
id: number
|
id: number
|
||||||
zoneId: number
|
zoneId: number
|
||||||
zone: Zone
|
zone: Zone
|
||||||
type: number
|
objectId: string
|
||||||
|
object: Object
|
||||||
position_x: number
|
position_x: number
|
||||||
position_y: number
|
position_y: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chat model
|
|
||||||
export type Chat = {
|
export type Chat = {
|
||||||
id: number
|
id: number
|
||||||
characterId: number
|
characterId: number
|
||||||
@ -64,10 +103,3 @@ export type Chat = {
|
|||||||
message: string
|
message: string
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Asset = {
|
|
||||||
key: string
|
|
||||||
value: string
|
|
||||||
group: 'tiles' | 'objects' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
|
||||||
type: 'base64' | 'link'
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user