From 8e174a063a8641be5d3ee651466a484dc1484815 Mon Sep 17 00:00:00 2001 From: Dennis Postma <dennis@directonline.io> Date: Tue, 11 Jun 2024 01:10:22 +0200 Subject: [PATCH] Worked on zone manager --- package-lock.json | 2 + .../assets/icons/zoneEditor/eraser-tool.svg | 1 - public/assets/icons/zoneEditor/eraser.svg | 44 ++++++ public/assets/icons/zoneEditor/move.svg | 78 ++++++++++ public/assets/icons/zoneEditor/tiles.svg | 33 ++-- src/App.vue | 3 +- src/assets/scss/_variables.scss | 2 +- src/assets/scss/main.scss | 26 +++- src/components/Game.vue | 70 ++++----- src/components/World.vue | 46 +++--- src/components/{game => gui}/Chat.vue | 0 src/components/{game => gui}/Hud.vue | 53 +++++-- src/components/{game => gui}/Menu.vue | 0 src/components/screens/Characters.vue | 34 ++-- src/components/screens/Login.vue | 10 +- src/components/sprites/Character.vue | 50 ++---- src/components/utilities/Controls.vue | 6 +- src/components/utilities/GmTools.vue | 10 +- src/components/utilities/Modal.vue | 41 +++-- src/components/utilities/Notifications.vue | 12 +- src/components/utilities/zoneEditor/Tiles.vue | 103 ++++++++++++ .../utilities/zoneEditor/Toolbar.vue | 125 +++++++++++++++ .../utilities/zoneEditor/ZoneEditor.vue | 146 +++++++----------- .../zoneEditor/ZoneEditorToolbar.vue | 81 ---------- src/services/authentication.ts | 4 +- src/services/zone.ts | 4 +- src/stores/socket.ts | 6 - src/stores/zone.ts | 29 +--- src/stores/zoneEditor.ts | 18 ++- 29 files changed, 658 insertions(+), 379 deletions(-) delete mode 100644 public/assets/icons/zoneEditor/eraser-tool.svg create mode 100644 public/assets/icons/zoneEditor/eraser.svg create mode 100644 public/assets/icons/zoneEditor/move.svg rename src/components/{game => gui}/Chat.vue (100%) rename src/components/{game => gui}/Hud.vue (64%) rename src/components/{game => gui}/Menu.vue (100%) create mode 100644 src/components/utilities/zoneEditor/Tiles.vue create mode 100644 src/components/utilities/zoneEditor/Toolbar.vue delete mode 100644 src/components/utilities/zoneEditor/ZoneEditorToolbar.vue diff --git a/package-lock.json b/package-lock.json index f7d9b25..f91a0ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1221,6 +1221,7 @@ "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1274,6 +1275,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true, "license": "BSD-3-Clause" }, diff --git a/public/assets/icons/zoneEditor/eraser-tool.svg b/public/assets/icons/zoneEditor/eraser-tool.svg deleted file mode 100644 index c0e1bf9..0000000 --- a/public/assets/icons/zoneEditor/eraser-tool.svg +++ /dev/null @@ -1 +0,0 @@ -<svg fill="none" height="25" viewBox="0 0 24 25" width="24" xmlns="http://www.w3.org/2000/svg"><g fill="#000"><path d="m11.671 20.8824h-2.342c-.668 0-1.296-.26-1.768-.732l-3-3c-.472-.472-.732-1.1-.732-1.768s.26-1.295.732-1.768l11.585-11.58497c.188-.188.52-.188.707 0l7 7c.195.195.195.512 0 .707l-10.414 10.41397c-.472.472-1.1.732-1.768.732zm4.829-17.79297-11.232 11.23297c-.283.283-.439.66-.439 1.061s.156.777.439 1.061l3 3c.284.283.66.439 1.061.439h2.343c.401 0 .777-.156 1.061-.439l10.06-10.06197z"/><path d="m15.5 17.8824c-.128 0-.256-.049-.354-.146l-7-7c-.195-.195-.195-.512 0-.707.195-.19497.512-.19497.707 0l7 7c.195.195.195.512 0 .707-.097.097-.225.146-.353.146z"/><path d="m5.5 20.8824h-5c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h5c.276 0 .5.224.5.5s-.224.5-.5.5z"/><path d="m14.5 22.8824h-9c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h9c.276 0 .5.224.5.5s-.224.5-.5.5z"/></g></svg> \ No newline at end of file diff --git a/public/assets/icons/zoneEditor/eraser.svg b/public/assets/icons/zoneEditor/eraser.svg new file mode 100644 index 0000000..94962b4 --- /dev/null +++ b/public/assets/icons/zoneEditor/eraser.svg @@ -0,0 +1,44 @@ +<?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 469.333 469.333" style="enable-background:new 0 0 469.333 469.333;" xml:space="preserve"> +<g> + <g> + <path d="M456.833,172.237L318.167,33.439c-8.061-8.068-19.109-12.103-30.159-12.105c-11.055-0.002-22.11,4.033-30.175,12.105 + L12.5,279.006C4.437,287.076,0,297.794,0,309.201c0,11.407,4.406,22.094,12.594,30.289l95.51,93.318 + c10.021,9.791,23.25,15.192,37.271,15.192h71.771c14.115,0,27.417-5.464,37.479-15.4l202.208-199.972 + c8.063-8.07,12.5-18.789,12.5-30.195S464.896,180.308,456.833,172.237z M224.656,402.25c-2.052,2.021-4.646,3.083-7.51,3.083 + h-71.771c-2.844,0-5.417-1.042-7.458-3.042l-95.25-92.958l110.708-110.708l137.844,137.854L224.656,402.25z"/> + </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> diff --git a/public/assets/icons/zoneEditor/move.svg b/public/assets/icons/zoneEditor/move.svg new file mode 100644 index 0000000..5946299 --- /dev/null +++ b/public/assets/icons/zoneEditor/move.svg @@ -0,0 +1,78 @@ +<?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="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 492.009 492.009" style="enable-background:new 0 0 492.009 492.009;" xml:space="preserve"> +<g> + <g> + <path d="M314.343,62.977L255.399,4.033c-2.672-2.672-6.236-4.04-9.92-4.032c-3.752-0.036-7.396,1.36-10.068,4.032l-57.728,57.728 + c-5.408,5.408-5.408,14.2,0,19.604l7.444,7.444c5.22,5.22,14.332,5.22,19.556,0l22.1-22.148v81.388 + c0,0.248,0.144,0.452,0.188,0.684c0.6,7.092,6.548,12.704,13.8,12.704h10.52c7.644,0,13.928-6.208,13.928-13.852v-9.088 + c0-0.04,0-0.068,0-0.1V67.869l22.108,22.152c5.408,5.408,14.18,5.408,19.584,0l7.432-7.436 + C319.751,77.173,319.751,68.377,314.343,62.977z"/> + </g> +</g> +<g> + <g> + <path d="M314.335,409.437l-7.44-7.456c-5.22-5.228-14.336-5.228-19.564,0l-22.108,22.152v-70.216c0-0.04,0-0.064,0-0.1v-9.088 + c0-7.648-6.288-14.16-13.924-14.16h-10.528c-7.244,0-13.192,5.756-13.796,12.856c-0.044,0.236-0.188,0.596-0.188,0.84v81.084 + l-22.1-22.148c-5.224-5.224-14.356-5.224-19.58,0l-7.44,7.444c-5.4,5.404-5.392,14.2,0.016,19.608l57.732,57.724 + c2.604,2.612,6.08,4.032,9.668,4.032h0.52c3.716,0,7.184-1.416,9.792-4.032l58.94-58.94 + C319.743,423.633,319.743,414.841,314.335,409.437z"/> + </g> +</g> +<g> + <g> + <path d="M147.251,226.781l-1.184,0h-7.948c-0.028,0-0.056,0-0.088,0h-69.88l22.152-22.032c2.612-2.608,4.048-6.032,4.048-9.74 + c0-3.712-1.436-7.164-4.048-9.768l-7.444-7.428c-5.408-5.408-14.204-5.4-19.604,0.008l-58.944,58.94 + c-2.672,2.668-4.1,6.248-4.028,9.92c-0.076,3.82,1.356,7.396,4.028,10.068l57.728,57.732c2.704,2.704,6.252,4.056,9.804,4.056 + s7.1-1.352,9.804-4.056l7.44-7.44c2.612-2.608,4.052-6.092,4.052-9.8c0-3.712-1.436-7.232-4.052-9.836l-22.144-22.184h80.728 + c0.244,0,0.644-0.06,0.876-0.104c7.096-0.6,12.892-6.468,12.892-13.716v-10.536C161.439,233.229,154.895,226.781,147.251,226.781z + "/> + </g> +</g> +<g> + <g> + <path d="M487.695,236.765l-58.944-58.936c-5.404-5.408-14.2-5.408-19.604,0l-7.436,7.444c-2.612,2.604-4.052,6.088-4.052,9.796 + c0,3.712,1.436,7.072,4.052,9.68l22.148,22.032h-70.328c-0.036,0-0.064,0-0.096,0h-9.084c-7.644,0-13.78,6.444-13.78,14.084 + v10.536c0,7.248,5.564,13.108,12.664,13.712c0.236,0.048,0.408,0.108,0.648,0.108h81.188l-22.156,22.18 + c-2.608,2.604-4.048,6.116-4.048,9.816c0,3.716,1.436,7.208,4.048,9.816l7.448,7.444c2.7,2.704,6.248,4.06,9.8,4.06 + s7.096-1.352,9.8-4.056l57.736-57.732c2.664-2.664,4.092-6.244,4.028-9.92C491.787,243.009,490.359,239.429,487.695,236.765z"/> + </g> +</g> +<g> + <g> + <path d="M246.011,207.541c-21.204,0-38.456,17.252-38.456,38.46c0,21.204,17.252,38.46,38.456,38.46 + c21.204,0,38.46-17.256,38.46-38.46C284.471,224.793,267.215,207.541,246.011,207.541z"/> + </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> diff --git a/public/assets/icons/zoneEditor/tiles.svg b/public/assets/icons/zoneEditor/tiles.svg index 26cad8a..5a815b3 100644 --- a/public/assets/icons/zoneEditor/tiles.svg +++ b/public/assets/icons/zoneEditor/tiles.svg @@ -1,18 +1,29 @@ <?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="Layer_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"> +<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 443.733 443.733" style="enable-background:new 0 0 443.733 443.733;" xml:space="preserve"> <g> <g> - <path d="M506.816,268.181l-145.984-87.595l18.219-11.584c3.072-1.963,4.928-5.355,4.928-9.003s-1.856-7.04-4.928-9.003 - L261.718,76.331c-3.499-2.219-7.957-2.219-11.456,0l-117.333,74.667C129.856,152.96,128,156.352,128,160s1.856,7.04,4.928,9.003 - l18.219,11.605L5.184,268.181c-3.2,1.92-5.184,5.376-5.184,9.109c-0.021,3.733,1.92,7.211,5.12,9.152l245.333,149.333 - c1.707,1.045,3.627,1.557,5.547,1.557c1.92,0,3.84-0.512,5.547-1.557L506.88,286.443c3.2-1.963,5.12-5.419,5.12-9.152 - C511.979,273.557,510.016,270.101,506.816,268.181z M256,97.984L353.472,160L256,222.016L158.528,160L256,97.984z - M171.264,193.408l78.997,50.261c1.749,1.109,3.755,1.664,5.739,1.664c1.984,0,3.989-0.555,5.717-1.664l78.997-50.283 - l14.059,8.448L256,264.704l-98.795-62.869L171.264,193.408z M31.296,277.355l105.472-63.253l99.371,63.232l-100.181,63.744 - L31.296,277.355z M256,414.165l-99.733-60.715L256,289.984l99.733,63.467L256,414.165z M376.043,341.099l-100.181-63.765 - l99.371-63.232l105.472,63.275L376.043,341.099z"/> + <path d="M187.733,0H17.067C7.641,0,0,7.641,0,17.067v170.667c0,9.426,7.641,17.067,17.067,17.067h170.667 + c9.426,0,17.067-7.641,17.067-17.067V17.067C204.8,7.641,197.159,0,187.733,0z"/> + </g> +</g> +<g> + <g> + <path d="M426.667,0H256c-9.426,0-17.067,7.641-17.067,17.067v170.667c0,9.426,7.641,17.067,17.067,17.067h170.667 + c9.426,0,17.067-7.641,17.067-17.067V17.067C443.733,7.641,436.092,0,426.667,0z"/> + </g> +</g> +<g> + <g> + <path d="M187.733,238.933H17.067C7.641,238.933,0,246.574,0,256v170.667c0,9.426,7.641,17.067,17.067,17.067h170.667 + c9.426,0,17.067-7.641,17.067-17.067V256C204.8,246.574,197.159,238.933,187.733,238.933z"/> + </g> +</g> +<g> + <g> + <path d="M426.667,238.933H256c-9.426,0-17.067,7.641-17.067,17.067v170.667c0,9.426,7.641,17.067,17.067,17.067h170.667 + c9.426,0,17.067-7.641,17.067-17.067V256C443.733,246.574,436.092,238.933,426.667,238.933z"/> </g> </g> <g> diff --git a/src/App.vue b/src/App.vue index 10285f8..0a0551b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -18,7 +18,8 @@ import Game from '@/components/Game.vue' const screen: Ref<string> = ref('login') const socket = useSocketStore() -socket.$subscribe((mutation, state) => { +socket.$subscribe( + (mutation, state) => { if (!state.connection) { screen.value = 'login' } diff --git a/src/assets/scss/_variables.scss b/src/assets/scss/_variables.scss index ad7825c..b7811f5 100644 --- a/src/assets/scss/_variables.scss +++ b/src/assets/scss/_variables.scss @@ -12,4 +12,4 @@ $blue-gray: #778899; $cyan: #368f8b; $dark-cyan: #376362; $light-cyan: #00b3b3; -$green: #09ad19; \ No newline at end of file +$green: #09ad19; diff --git a/src/assets/scss/main.scss b/src/assets/scss/main.scss index c9bf4df..1353186 100644 --- a/src/assets/scss/main.scss +++ b/src/assets/scss/main.scss @@ -15,22 +15,34 @@ body { user-select: none; /* Standard syntax */ } -h1, h2, h3, h4, h5, h6, button, a { - font-family: "Poppins", serif; +h1, +h2, +h3, +h4, +h5, +h6, +button, +a { + font-family: 'Poppins', serif; color: $white; font-weight: 500; } -p, span, li, label { - font-family: "Inter", serif; +p, +span, +li, +label { + font-family: 'Inter', serif; color: $white; } -button, a { +button, +a { font-weight: 500; text-shadow: 0 4px 6px rgba($black, 0.25); } -button, input { +button, +input { border: none; background-color: transparent; } @@ -60,4 +72,4 @@ button { ::-webkit-scrollbar { display: none; -} \ No newline at end of file +} diff --git a/src/components/Game.vue b/src/components/Game.vue index c2ca608..fe699a2 100644 --- a/src/components/Game.vue +++ b/src/components/Game.vue @@ -1,25 +1,18 @@ <template> <div class="game-container"> - <GmTools /> - <div class="top-ui" v-if="!zone.getEditorIsOpen"> - <Hud /> - </div> - - <Game :config="gameConfig" class="game" @create="createGame"> - <Scene name="main" @preload="preloadScene" @create="createScene" @play="playScene"> - <World /> + <Game class="game" :config="gameConfig" @create="createGame"> + <Scene name="main" @preload="preloadScene" @create="createScene"> + <GmTools /> + <div v-if="!zoneEditorStore.isActive"> + <div class="top-ui"><Hud /></div> + <World /> + <div class="bottom-ui"><Chat /> <Menubar /></div> + </div> + <div v-else> + <ZoneEditor /> + </div> </Scene> </Game> - - <div class="bottom-ui"> - <div v-if="!zone.getEditorIsOpen"> - <Chat /> - <Menubar /> - </div> - <div v-else> - <ZoneEditorToolbar /> - </div> - </div> </div> </template> @@ -27,25 +20,28 @@ import 'phaser' import { Game, Scene } from 'phavuer' import World from '@/components/World.vue' -import Pointer = Phaser.Input.Pointer -import { useSocketStore } from '@/stores/socket' -import Hud from '@/components/game/Hud.vue' -import Chat from '@/components/game/Chat.vue' -import Menubar from '@/components/game/Menu.vue' +import Hud from '@/components/gui/Hud.vue' +import Chat from '@/components/gui/Chat.vue' +import Menubar from '@/components/gui/Menu.vue' import { onUnmounted } from 'vue' -import ZoneEditor from '@/components/utilities/zoneEditor/ZoneEditor.vue' -import Modal from '@/components/utilities/Modal.vue' import GmTools from '@/components/utilities/GmTools.vue' -import { useZoneStore } from '@/stores/zone' -import ZoneEditorToolbar from '@/components/utilities/zoneEditor/ZoneEditorToolbar.vue' +import { useSocketStore } from '@/stores/socket' +import { useZoneEditorStore } from '@/stores/zoneEditor' +import Toolbar from '@/components/utilities/zoneEditor/Toolbar.vue' +import ZoneEditor from '@/components/utilities/zoneEditor/ZoneEditor.vue' const socket = useSocketStore() -const zone = useZoneStore() +const zoneEditorStore = useZoneEditorStore() onUnmounted(() => { socket.disconnectSocket() }) +// on page close +addEventListener('beforeunload', () => { + socket.disconnectSocket() +}) + const gameConfig = { name: 'New Quest', width: window.innerWidth, @@ -55,7 +51,7 @@ const gameConfig = { } const createGame = (game: Phaser.Game) => { - window.addEventListener('resize', () => { + addEventListener('resize', () => { game.scale.resize(window.innerWidth, window.innerHeight) }) } @@ -75,25 +71,23 @@ const preloadScene = (scene: Phaser.Scene) => { scene.load.spritesheet('characterW', '/assets/avatar/default/walk.png', { frameWidth: 36, frameHeight: 94 }) } -const playScene = (scene: Phaser.Scene) => { -} +const playScene = (scene: Phaser.Scene) => {} const createScene = (scene: Phaser.Scene) => { // Camera drag system let cam = scene.cameras.main - scene.input.on('pointermove', function (pointer: Pointer) { + scene.input.on('pointermove', function (pointer: Phaser.Input.Pointer) { if (!pointer.isDown) return cam.scrollX -= (pointer.x - pointer.prevPosition.x) / cam.zoom cam.scrollY -= (pointer.y - pointer.prevPosition.y) / cam.zoom }) scene.anims.create({ - key: "walk", + key: 'walk', frameRate: 7, - frames: scene.anims.generateFrameNumbers("characterW", { start: 0, end: 3 }), - repeat: -1, - }); - + frames: scene.anims.generateFrameNumbers('characterW', { start: 0, end: 3 }), + repeat: -1 + }) // const grid = scene.add.grid(0, 0, window.innerWidth, window.innerHeight, 64, 32, 0, 0, 0xff0000, 0.5).setOrigin(0, 0); // @@ -132,4 +126,4 @@ const createScene = (scene: Phaser.Scene) => { bottom: 100px; height: 100px; } -</style> \ No newline at end of file +</style> diff --git a/src/components/World.vue b/src/components/World.vue index bbffa75..fdb185e 100644 --- a/src/components/World.vue +++ b/src/components/World.vue @@ -1,9 +1,9 @@ <template> - <TilemapLayerC v-if="zoneStore.isLoaded" :tilemap="tileMap" :tileset="zoneStore.getTiles" ref="tilemapLayer" :layerIndex="0" :cull-padding-x="10" :cull-padding-y="10" /> + <TilemapLayerC :tilemap="tileMap" :tileset="zoneStore.tiles" ref="tilemapLayer" :layerIndex="0" :cull-padding-x="10" :cull-padding-y="10" /> <Controls :layer="layer" /> -<!-- <Container v-if="zoneStore.isLoaded && !zoneEditorStore.isLoaded">--> -<!-- <Character :layer="layer" v-for="character in zoneStore.getCharacters" :key="character.id" :character="character" />--> -<!-- </Container>--> + <Container> + <Character :layer="layer" v-for="character in zoneStore.characters" :key="character.id" :character="character" /> + </Container> </template> <script setup lang="ts"> @@ -13,11 +13,10 @@ import TilemapLayer = Phaser.Tilemaps.TilemapLayer import { Container, TilemapLayer as TilemapLayerC, useScene } from 'phavuer' import Character from '@/components/sprites/Character.vue' import { type Character as CharacterType } from '@/types' -import { onBeforeMount, ref, type Ref, watch } from 'vue' +import { onBeforeMount, onBeforeUnmount, ref, type Ref, watch } from 'vue' import Controls from '@/components/utilities/Controls.vue' import { useSocketStore } from '@/stores/socket' import { useZoneStore } from '@/stores/zone' -import { useZoneEditorStore } from '@/stores/zoneEditor' // Phavuer logic let scene = useScene() @@ -41,44 +40,53 @@ scene.cameras.main.centerOn(centerX, centerY) // Multiplayer / server logics const zoneStore = useZoneStore() -const zoneEditorStore = useZoneEditorStore() const socket = useSocketStore() // Watch for changes in the zoneStore and update the layer -watch(() => zoneStore.tiles, () => { +watch( + () => zoneStore.tiles, + () => { // @TODO : change to tiles for when loading other maps - zoneStore.getTiles.forEach((row, y) => row.forEach((tile, x) => layer.putTileAt(tile, x, y))) - }, { deep: true } + zoneStore.tiles.forEach((row, y) => row.forEach((tile, x) => layer.putTileAt(tile, x, y))) + }, + { deep: true } ) // Load the zone from the server onBeforeMount(() => { - socket.getConnection.emit('character:zone:load', {zoneId: socket.character.zoneId}) + socket.connection.emit('character:zone:load', { zoneId: socket.character.zoneId }) }) // Listen for the zone event from the server and load the zone -socket.getConnection.on('character:zone:load', (data) => { +socket.connection.on('character:zone:load', (data) => { console.log('character:zone:load', data) - zoneStore.loadTiles(data.zone.tiles) + zoneStore.setTiles(data.zone.tiles) - let characters = data.characters; - zoneStore.setCharacters(characters); + let characters = data.characters + zoneStore.setCharacters(characters) }) // Listen for player join events -socket.getConnection.on('zone:character:join', (data: CharacterType) => { +socket.connection.on('zone:character:join', (data: CharacterType) => { console.log('character:zone:join', data) zoneStore.addCharacter(data) }) // Listen for user:disconnect -socket.getConnection.on('user:disconnect', (data: CharacterType) => { +socket.connection.on('user:disconnect', (data: CharacterType) => { zoneStore.removeCharacter(data) }) -socket.getConnection.on('character:moved', (data: CharacterType) => { +socket.connection.on('character:moved', (data: CharacterType) => { console.log('character:moved', data) - zoneStore.updateCharacter(data); + zoneStore.updateCharacter(data) +}) + +onBeforeUnmount(() => { + socket.connection.off('character:zone:load') + socket.connection.off('zone:character:join') + socket.connection.off('user:disconnect') + socket.connection.off('character:moved') }) /** diff --git a/src/components/game/Chat.vue b/src/components/gui/Chat.vue similarity index 100% rename from src/components/game/Chat.vue rename to src/components/gui/Chat.vue diff --git a/src/components/game/Hud.vue b/src/components/gui/Hud.vue similarity index 64% rename from src/components/game/Hud.vue rename to src/components/gui/Hud.vue index 4267b3e..bb20de6 100644 --- a/src/components/game/Hud.vue +++ b/src/components/gui/Hud.vue @@ -6,16 +6,16 @@ <div class="hud"> <div class="stats"> <div class="player-details"> - <span class="player-name">{{ socket.getCharacter.name }}</span> - <span class="player-lvl">lvl. {{ socket.getCharacter.level }}</span> + <span class="player-name">{{ socket.character.name }}</span> + <span class="player-lvl">lvl. {{ socket.character.level }}</span> </div> <div class="bar"> <label for="hp">HP</label> - <progress id="hp" :value="socket.getCharacter.hitpoints" max="100">{{ socket.getCharacter.hitpoints }}%</progress> + <progress id="hp" :value="socket.character.hitpoints" max="100">{{ socket.character.hitpoints }}%</progress> </div> <div class="bar"> <label for="mp">MP</label> - <progress id="mp" :value="socket.getCharacter.mana" max="100">{{ socket.getCharacter.mana }}%</progress> + <progress id="mp" :value="socket.character.mana" max="100">{{ socket.character.mana }}%</progress> </div> </div> </div> @@ -25,7 +25,7 @@ <script setup lang="ts"> import { useSocketStore } from '@/stores/socket' -const socket = useSocketStore(); +const socket = useSocketStore() </script> <style scoped lang="scss"> @@ -35,7 +35,8 @@ const socket = useSocketStore(); position: relative; left: -32px; - .hud, &::before { + .hud, + &::before { position: absolute; top: 32px; left: 32px; @@ -74,12 +75,14 @@ const socket = useSocketStore(); justify-content: center; padding: 0 20px 0 50px; - .player-details, .bar { + .player-details, + .bar { width: 100%; display: flex; align-items: center; justify-content: space-between; - span, label { + span, + label { font-size: 14px; } .player-name { @@ -105,18 +108,40 @@ const socket = useSocketStore(); &#hp { accent-color: $red; // Chrome, Safari, Edge, Opera - &::-webkit-progress-value { background: $red; border-radius: 8px; } - &::-webkit-progress-bar { background: $white; border-radius: 8px; border: 2px solid $white; } + &::-webkit-progress-value { + background: $red; + border-radius: 8px; + } + &::-webkit-progress-bar { + background: $white; + border-radius: 8px; + border: 2px solid $white; + } // Firefox - &::-moz-progress-bar { background: $red; border-radius: 8px; border: 2px solid $white;} + &::-moz-progress-bar { + background: $red; + border-radius: 8px; + border: 2px solid $white; + } } &#mp { accent-color: $light-blue; // Chrome, Safari, Edge, Opera - &::-webkit-progress-value { background: $light-blue; border-radius: 8px; } - &::-webkit-progress-bar { background: $white; border-radius: 8px; border: 2px solid $white;} + &::-webkit-progress-value { + background: $light-blue; + border-radius: 8px; + } + &::-webkit-progress-bar { + background: $white; + border-radius: 8px; + border: 2px solid $white; + } // Firefox - &::-moz-progress-bar { background: $light-blue; border-radius: 8px; border: 2px solid $white;} + &::-moz-progress-bar { + background: $light-blue; + border-radius: 8px; + border: 2px solid $white; + } } } } diff --git a/src/components/game/Menu.vue b/src/components/gui/Menu.vue similarity index 100% rename from src/components/game/Menu.vue rename to src/components/gui/Menu.vue diff --git a/src/components/screens/Characters.vue b/src/components/screens/Characters.vue index 9bfc8b3..53f387d 100644 --- a/src/components/screens/Characters.vue +++ b/src/components/screens/Characters.vue @@ -7,9 +7,9 @@ <label :for="character.id">{{ character.name }}</label> <!-- @TODO : Add a confirmation dialog --> <button class="delete" @click="delete_character(character.id)"> - <img draggable="false" src="/assets/icons/trashcan.svg"> + <img draggable="false" src="/assets/icons/trashcan.svg" /> </button> - + <div class="sprite-container"> <img draggable="false" src="/assets/avatar/default/0.png" /> </div> @@ -32,7 +32,7 @@ <div class="button-wrapper" v-if="!isLoading"> <button class="btn-cyan" :disabled="!selected_character" @click="select_character()"> PLAY - <img draggable="false" src="/assets/icons/arrow.svg"> + <img draggable="false" src="/assets/icons/arrow.svg" /> </button> </div> </div> @@ -60,51 +60,57 @@ <script setup lang="ts"> import { useSocketStore } from '@/stores/socket' -import { onBeforeMount, onMounted, ref } from 'vue' +import { onBeforeMount, onBeforeUnmount, onMounted, ref } from 'vue' import Modal from '@/components/utilities/Modal.vue' -import {type Character as CharacterT} from '@/types' +import { type Character as CharacterT } from '@/types' const isLoading = ref(true) const characters = ref([]) -const socket = useSocketStore(); +const socket = useSocketStore() // Fetch characters -socket.getConnection.on('character:list', (data: any) => { +socket.connection.on('character:list', (data: any) => { characters.value = data }) onMounted(() => { // wait 1.5 sec setTimeout(() => { - socket.getConnection.emit('character:list') + socket.connection.emit('character:list') isLoading.value = false }, 1000) -}); +}) // Select character logics const selected_character = ref(null) function select_character() { if (!selected_character.value) return - socket.getConnection.emit('character:connect', { character_id: selected_character.value }) - socket.getConnection.on('character:connect', (data: CharacterT) => socket.setCharacter(data)) + socket.connection.emit('character:connect', { character_id: selected_character.value }) + socket.connection.on('character:connect', (data: CharacterT) => socket.setCharacter(data)) } // Delete character logics function delete_character(character_id: number) { if (!character_id) return - socket.getConnection.emit('character:delete', { character_id: character_id }) + socket.connection.emit('character:delete', { character_id: character_id }) } // Create character logics const isModalOpen = ref(false) const name = ref('') function create() { - socket.getConnection.on('character:create:success', (data: CharacterT) => { + socket.connection.on('character:create:success', (data: CharacterT) => { socket.setCharacter(data) isModalOpen.value = false }) - socket.getConnection.emit('character:create', { name: name.value }) + socket.connection.emit('character:create', { name: name.value }) } + +onBeforeUnmount(() => { + socket.connection.off('character:list') + socket.connection.off('character:connect') + socket.connection.off('character:create:success') +}) </script> <style lang="scss"> diff --git a/src/components/screens/Login.vue b/src/components/screens/Login.vue index 6202814..982dc71 100644 --- a/src/components/screens/Login.vue +++ b/src/components/screens/Login.vue @@ -30,14 +30,12 @@ import { ref } from 'vue' import { login, register } from '@/services/authentication' import { useNotificationStore } from '@/stores/notifications' -import ZoneEditor from '@/components/utilities/zoneEditor/ZoneEditor.vue' -import Modal from '@/components/utilities/Modal.vue' import { useSocketStore } from '@/stores/socket' const bgm = ref('bgm') if (bgm.value.paused) { - window.addEventListener('click', () => bgm.value.play()) - window.addEventListener('keydown', () => bgm.value.play()) + addEventListener('click', () => bgm.value.play()) + addEventListener('keydown', () => bgm.value.play()) } const socket = useSocketStore() @@ -61,7 +59,7 @@ async function loginFunc() { } socket.setToken(response.token) - socket.initConnection(); + socket.initConnection() } async function registerFunc() { @@ -80,7 +78,7 @@ async function registerFunc() { } socket.setToken(response.token) - socket.initConnection(); + socket.initConnection() } </script> diff --git a/src/components/sprites/Character.vue b/src/components/sprites/Character.vue index a5a4ea5..07555d2 100644 --- a/src/components/sprites/Character.vue +++ b/src/components/sprites/Character.vue @@ -1,23 +1,7 @@ <template> <Container> - <Rectangle - :x="tileToWorldX(layer, props.character?.position_x, props.character?.position_y)" - :y="tileToWorldY(layer, props.character?.position_x, props.character?.position_y)" - :origin-x="0.5" - :origin-y="10.5" - :fillColor="0xFFFFFF" - :width="74" - :height="8" - > - <Rectangle - :x="tileToWorldX(layer, props.character?.position_x, props.character?.position_y)" - :y="tileToWorldY(layer, props.character?.position_x, props.character?.position_y)" - :origin-x="0.5" - :origin-y="20.5" - :fillColor="0x09ad19" - :width="70" - :height="4" - /> + <Rectangle :x="tileToWorldX(layer, props.character?.position_x, props.character?.position_y)" :y="tileToWorldY(layer, props.character?.position_x, props.character?.position_y)" :origin-x="0.5" :origin-y="10.5" :fillColor="0xffffff" :width="74" :height="8"> + <Rectangle :x="tileToWorldX(layer, props.character?.position_x, props.character?.position_y)" :y="tileToWorldY(layer, props.character?.position_x, props.character?.position_y)" :origin-x="0.5" :origin-y="20.5" :fillColor="0x09ad19" :width="70" :height="4" /> </Rectangle> <Text @create="createText" @@ -29,20 +13,16 @@ :style="{ fontFamily: 'Helvetica, Arial', color: '#FFF', - fontSize: '14px', + fontSize: '14px' }" /> - <Sprite - ref="sprite" - :x="tileToWorldX(layer, props.character?.position_x, props.character?.position_y)" - :y="tileToWorldY(layer, props.character?.position_x, props.character?.position_y)" - play="walk" /> + <Sprite ref="sprite" :x="tileToWorldX(layer, props.character?.position_x, props.character?.position_y)" :y="tileToWorldY(layer, props.character?.position_x, props.character?.position_y)" play="walk" /> </Container> </template> <script lang="ts" setup> import { Container, Rectangle, Sprite, Text, useScene } from 'phavuer' -import { onMounted, ref } from 'vue' +import { onBeforeMount, onMounted, ref } from 'vue' import { useSocketStore } from '@/stores/socket' import { type Character as CharacterT } from '@/types' import { getTile, tileToWorldX, tileToWorldXY, tileToWorldY } from '@/services/zone' @@ -55,33 +35,32 @@ const props = defineProps({ }) const scene = useScene() -const isSelf = props.character.id === socket.character.id; +const isSelf = props.character.id === socket.character.id const createText = (text: Phaser.GameObjects.Text) => { -text.setLetterSpacing(1.5); + text.setLetterSpacing(1.5) } -onMounted(() => { +onBeforeMount(() => { if (isSelf) setupSelf() -}); +}) -function setupSelf() -{ +function setupSelf() { scene.input.on(Phaser.Input.Events.POINTER_UP, onPointerClick) } function onPointerClick(pointer: Phaser.Input.Pointer) { - if (!isSelf) return; + if (!isSelf) return const px = scene.cameras.main.worldView.x + pointer.x const py = scene.cameras.main.worldView.y + pointer.y const pointer_tile = getTile(px, py, props.layer) as Phaser.Tilemaps.Tile if (!pointer_tile) { - return; + return } - socket.getConnection.emit('character:move', { position_x: pointer_tile.x, position_y: pointer_tile.y }) + socket.connection.emit('character:move', { position_x: pointer_tile.x, position_y: pointer_tile.y }) //Directions for player sprites + animations if (px < 0 && py > 0) { @@ -95,9 +74,6 @@ function onPointerClick(pointer: Phaser.Input.Pointer) { } } -// spacing - - /** * Resources: * https://www.youtube.com/watch?v=9sWrGohw9qo diff --git a/src/components/utilities/Controls.vue b/src/components/utilities/Controls.vue index 875a4c9..03eaf8e 100644 --- a/src/components/utilities/Controls.vue +++ b/src/components/utilities/Controls.vue @@ -4,7 +4,7 @@ <script setup lang="ts"> import { Image, useScene } from 'phavuer' -import { ref } from 'vue' +import { onBeforeUnmount, ref } from 'vue' import config from '@/config' import { getTile, tileToWorldXY } from '@/services/zone' @@ -37,4 +37,8 @@ function onPointerMove(pointer: Phaser.Input.Pointer) { } scene.input.on(Phaser.Input.Events.POINTER_MOVE, onPointerMove) + +onBeforeUnmount(() => { + scene.input.off(Phaser.Input.Events.POINTER_MOVE, onPointerMove) +}) </script> diff --git a/src/components/utilities/GmTools.vue b/src/components/utilities/GmTools.vue index 3175a38..3d4243f 100644 --- a/src/components/utilities/GmTools.vue +++ b/src/components/utilities/GmTools.vue @@ -1,11 +1,11 @@ <template> - <Modal :isModalOpen="true" :closable="false" :modal-width="200" :modal-height="260"> + <Modal :isModalOpen="true" :closable="false" :is-resizable="false" :modal-width="200" :modal-height="260"> <template #modalHeader> <h3 class="modal-title">GM tools</h3> </template> <template #modalBody> <div class="content"> - <button class="btn-cyan w-full" type="button">Zone manager</button> + <button class="btn-cyan w-full" type="button" @click="() => zoneEditorStore.toggleActive()">Zone manager</button> <button class="btn-cyan w-full" type="button">Player manager</button> <button class="btn-cyan w-full" type="button">Item manager</button> <button class="btn-cyan w-full" type="button">NPC manager</button> @@ -14,8 +14,10 @@ </Modal> </template> <script setup lang="ts"> -import ZoneEditor from '@/components/utilities/zoneEditor/ZoneEditor.vue' import Modal from '@/components/utilities/Modal.vue' +import { useZoneEditorStore } from '@/stores/zoneEditor' + +const zoneEditorStore = useZoneEditorStore() </script> <style lang="scss"> @@ -24,4 +26,4 @@ import Modal from '@/components/utilities/Modal.vue' flex-direction: column; gap: 0.8rem; } -</style> \ No newline at end of file +</style> diff --git a/src/components/utilities/Modal.vue b/src/components/utilities/Modal.vue index af4cfc4..1cf166c 100644 --- a/src/components/utilities/Modal.vue +++ b/src/components/utilities/Modal.vue @@ -4,7 +4,7 @@ <div class="modal-header" @mousedown="startDrag"> <slot name="modalHeader" /> <div class="buttons"> -<!-- <button><img alt="resize" draggable="false" src="/assets/icons/modalFullscreen.svg" /></button>--> + <!-- <button><img alt="resize" draggable="false" src="/assets/icons/modalFullscreen.svg" /></button>--> <button @click="close" v-if="closable"><img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" /></button> </div> </div> @@ -45,7 +45,9 @@ const properties = defineProps({ } }) -watch(() => properties.isModalOpen, (value) => { +watch( + () => properties.isModalOpen, + (value) => { isModalOpenRef.value = value } ) @@ -122,12 +124,16 @@ const stopDrag = () => { isDragging.value = false } -watch(() => properties.modalWidth, (value) => { +watch( + () => properties.modalWidth, + (value) => { width.value = value } ) -watch(() => properties.modalHeight, (value) => { +watch( + () => properties.modalHeight, + (value) => { height.value = value } ) @@ -148,14 +154,25 @@ onUnmounted(() => { removeEventListener('resize', handleResize) }) - // Make sure modal doesn't go off screen -watch(() => x.value, (value) => { - if (value < 0) { x.value = 0 } else if (value + width.value > window.innerWidth) { x.value = window.innerWidth - width.value } +watch( + () => x.value, + (value) => { + if (value < 0) { + x.value = 0 + } else if (value + width.value > window.innerWidth) { + x.value = window.innerWidth - width.value + } } ) -watch(() => y.value, (value) => { - if (value < 0) { y.value = 0 } else if (value + height.value > window.innerHeight) { y.value = window.innerHeight - height.value } +watch( + () => y.value, + (value) => { + if (value < 0) { + y.value = 0 + } else if (value + height.value > window.innerHeight) { + y.value = window.innerHeight - height.value + } } ) @@ -264,12 +281,12 @@ function handleResize() { label { margin-bottom: 10px; - font-family: "Poppins"; + font-family: 'Poppins'; } input { max-width: 250px; - font-family: "Poppins"; + font-family: 'Poppins'; border: 1px solid $cyan; border-radius: 5px; background-color: rgba($white, 0.8); @@ -296,4 +313,4 @@ function handleResize() { } } } -</style> \ No newline at end of file +</style> diff --git a/src/components/utilities/Notifications.vue b/src/components/utilities/Notifications.vue index 8498e06..a08942c 100644 --- a/src/components/utilities/Notifications.vue +++ b/src/components/utilities/Notifications.vue @@ -15,7 +15,7 @@ import { useNotificationStore } from '@/stores/notifications' import { useSocketStore } from '@/stores/socket' import Modal from '@/components/utilities/Modal.vue' -import { onMounted, onUnmounted, ref, watch } from 'vue' +import { onBeforeMount, onBeforeUnmount, watch } from 'vue' const notifications = useNotificationStore() const socket = useSocketStore() @@ -32,14 +32,14 @@ function setupNotificationListener(connection: any) { }) } -onMounted(() => { - const connection = socket.getConnection +onBeforeMount(() => { + const connection = socket.connection if (connection) { setupNotificationListener(connection) } else { // Watch for changes in the socket connection watch( - () => socket.getConnection, + () => socket.connection, (newConnection) => { if (newConnection) setupNotificationListener(newConnection) } @@ -47,8 +47,8 @@ onMounted(() => { } }) -onUnmounted(() => { - const connection = socket.getConnection +onBeforeUnmount(() => { + const connection = socket.connection if (connection) { connection.off('notification') } diff --git a/src/components/utilities/zoneEditor/Tiles.vue b/src/components/utilities/zoneEditor/Tiles.vue new file mode 100644 index 0000000..9e943e7 --- /dev/null +++ b/src/components/utilities/zoneEditor/Tiles.vue @@ -0,0 +1,103 @@ +<template> + <Teleport to="body"> + <Modal v-if="isModalOpen" :isModalOpen="true" :modal-width="645" :modal-height="260"> + <template #modalHeader> + <h3 class="modal-title">Tiles</h3> + </template> + <template #modalBody> + <canvas ref="canvas" :width="tileWidth" :height="tileHeight" style="display: none"></canvas> + <div class="tiles"> + <img v-for="(tile, index) in tiles" :key="index" :src="tile" alt="Tile" @click="selectTile(index)" :class="{ selected: selectedTile === index }" /> + </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' + +const tileWidth = config.tile_size.x +const tileHeight = config.tile_size.y +const tiles = ref<string[]>([]) +const selectedTile = ref<number | null>(null) +const canvas = ref<HTMLCanvasElement | null>(null) +const isModalOpen = ref(false) + +// Hardcoded image path +const imagePath = '/assets/tiles/default.png' + +const loadImage = (src: string): Promise<HTMLImageElement> => { + return new Promise((resolve) => { + const img = new Image() + img.onload = () => resolve(img) + img.src = src + }) +} + +const splitTiles = (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 tilesetWidth = img.width + const tilesetHeight = img.height + const columns = Math.floor(tilesetWidth / tileWidth) + const rows = Math.floor(tilesetHeight / tileHeight) + + tiles.value = [] + selectedTile.value = null + + for (let row = 0; row < rows; row++) { + for (let col = 0; col < columns; col++) { + const x = col * tileWidth + const y = row * tileHeight + + ctx.clearRect(0, 0, tileWidth, tileHeight) + ctx.drawImage(img, x, y, tileWidth, tileHeight, 0, 0, tileWidth, tileHeight) + + const tileDataURL = canvas.value.toDataURL() + tiles.value.push(tileDataURL) + } + } +} + +const selectTile = (index: number) => { + selectedTile.value = index +} + +onMounted(async () => { + isModalOpen.value = true + const img = await loadImage(imagePath) + await nextTick() + splitTiles(img) +}) +</script> + +<style lang="scss"> +.tiles { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.tiles img { + width: 64px; + height: 32px; + cursor: pointer; + border: 2px solid transparent; + transition: border 0.3s ease; +} + +.tiles img.selected { + border: 2px solid #ff0000; +} +</style> diff --git a/src/components/utilities/zoneEditor/Toolbar.vue b/src/components/utilities/zoneEditor/Toolbar.vue new file mode 100644 index 0000000..e64785c --- /dev/null +++ b/src/components/utilities/zoneEditor/Toolbar.vue @@ -0,0 +1,125 @@ +<template> + <div class="wrapper"> + <div class="toolbar"> + <div class="tools"> + <button :class="{ active: activeTool === 'move' }" @click="activeTool = 'move'"> + <img src="/assets/icons/zoneEditor/move.svg" alt="Eraser tool" /> + </button> + <div class="divider"></div> + <button :class="{ active: activeTool === 'tiles' }" @click="activeTool = 'tiles'"> + <img src="/assets/icons/zoneEditor/tiles.svg" alt="Eraser tool" /> + </button> + <div class="divider"></div> + <button :class="{ active: activeTool === 'eraser' }" @click="activeTool = 'eraser'"> + <img src="/assets/icons/zoneEditor/eraser.svg" alt="Eraser tool" /> + </button> + </div> + <div class="buttons"> + <button class="btn-cyan">Save</button> + <button class="btn-cyan">Load</button> + <button class="btn-cyan">Clear</button> + <button class="btn-cyan" @click="() => zoneEditorStore.toggleActive()">Exit</button> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { onBeforeUnmount, ref } from 'vue' +import { useScene } from 'phavuer' +import { getTile, tileToWorldXY } from '@/services/zone' +import config from '@/config' +import { useZoneStore } from '@/stores/zone' +import { useZoneEditorStore } from '@/stores/zoneEditor' + +const zoneEditorStore = useZoneEditorStore() + +const props = defineProps({ + layer: Phaser.Tilemaps.TilemapLayer +}) +const scene = useScene() +const activeTool = ref('move') +const emit = defineEmits(['erase', 'move', 'tile']) + +function onPointerClick(pointer: Phaser.Input.Pointer) { + const px = scene.cameras.main.worldView.x + pointer.x + const py = scene.cameras.main.worldView.y + pointer.y + + const pointer_tile = getTile(px, py, props.layer) as Phaser.Tilemaps.Tile + if (!pointer_tile) { + return + } + + if (activeTool.value === 'eraser') { + emit('erase', pointer_tile) + } +} + +scene.input.on(Phaser.Input.Events.POINTER_UP, onPointerClick) + +onBeforeUnmount(() => { + scene.input.off(Phaser.Input.Events.POINTER_UP, onPointerClick) +}) +</script> + +<style scoped lang="scss"> +@import '@/assets/scss/main'; + +.wrapper { + display: flex; + justify-content: center; + margin: 10px; +} + +.toolbar { + position: fixed; + top: 20px; + border-radius: 5px; + opacity: 0.8; + display: flex; + background: $dark-gray; + border: 2px solid $cyan; + color: $light-gray; + padding: 5px; + min-width: 90%; + height: 40px; + + .tools { + display: flex; + gap: 10px; + + .divider { + width: 1px; + background: $cyan; + } + + // vertical center + button { + display: flex; + justify-content: center; + align-items: center; + + &.active { + border-bottom: 3px solid $light-cyan; + } + } + + img { + filter: invert(1); + width: 20px; + height: 20px; + } + } + + .buttons { + display: flex; + gap: 10px; + margin-left: auto; + + button { + padding-left: 15px; + padding-right: 15px; + } + } +} +</style> diff --git a/src/components/utilities/zoneEditor/ZoneEditor.vue b/src/components/utilities/zoneEditor/ZoneEditor.vue index ce6c7e4..a4a3f7a 100644 --- a/src/components/utilities/zoneEditor/ZoneEditor.vue +++ b/src/components/utilities/zoneEditor/ZoneEditor.vue @@ -1,99 +1,73 @@ <template> - <div> - <canvas ref="tileCanvas" :width="tileWidth" :height="tileHeight" style="display: none;"></canvas> - <div class="tiles"> - <img - v-for="(tile, index) in tiles" - :key="index" - :src="tile" - alt="Tile" - @click="selectTile(index)" - :class="{ selected: selectedTile === index }" - /> - </div> - </div> + <TilemapLayerC :tilemap="tileMap" :tileset="zoneStore.tiles" ref="tilemapLayer" :layerIndex="0" :cull-padding-x="10" :cull-padding-y="10" /> + <Controls :layer="layer" /> + <Toolbar :layer="layer" @erase="erase" @tile="tile" /> + <Tiles /> </template> <script setup lang="ts"> -import { ref, onMounted } from 'vue'; import config from '@/config' +import Tileset = Phaser.Tilemaps.Tileset +import TilemapLayer = Phaser.Tilemaps.TilemapLayer +import { Container, TilemapLayer as TilemapLayerC, useScene } from 'phavuer' +import { onBeforeMount, ref, type Ref, watch } from 'vue' +import Controls from '@/components/utilities/Controls.vue' +import { useSocketStore } from '@/stores/socket' +import { useZoneStore } from '@/stores/zone' +import Toolbar from '@/components/utilities/zoneEditor/Toolbar.vue' +import Tiles from '@/components/utilities/zoneEditor/Tiles.vue' -const tileWidth = config.tile_size.x; -const tileHeight = config.tile_size.y; -const tiles = ref([]); -const selectedTile = ref(null); -const tileCanvas = ref(null); +// Phavuer logic +let scene = useScene() +let tilemapLayer = ref() +let zoneData = new Phaser.Tilemaps.MapData({ + width: 10, // @TODO : get this from the server + height: 10, // @TODO : get this from the server + tileWidth: config.tile_size.x, + tileHeight: config.tile_size.y, + orientation: Phaser.Tilemaps.Orientation.ISOMETRIC, + format: Phaser.Tilemaps.Formats.ARRAY_2D +}) +let tileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData) +let tileset: Tileset = tileMap.addTilesetImage('default', 'tiles') as Tileset +let layer: TilemapLayer = tileMap.createBlankLayer('layer', tileset, 0, config.tile_size.y) as TilemapLayer -// Hardcoded image path -const imagePath = '/assets/tiles/default.png'; +// center camera +const centerY = (tileMap.height * tileMap.tileHeight) / 2 +const centerX = (tileMap.width * tileMap.tileWidth) / 2 +scene.cameras.main.centerOn(centerX, centerY) -const loadImage = (src) => { - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => resolve(img); - img.src = src; - }); -}; +// Multiplayer / server logics +const zoneStore = useZoneStore() +const socket = useSocketStore() -const splitTiles = (img) => { - const canvas = tileCanvas.value; - const ctx = canvas.getContext('2d'); +zoneStore.setTiles([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +]) +zoneStore.tiles.forEach((row, y) => row.forEach((tile, x) => layer.putTileAt(tile, x, y))) - const tilesetWidth = img.width; - const tilesetHeight = img.height; - const columns = Math.floor(tilesetWidth / tileWidth); - const rows = Math.floor(tilesetHeight / tileHeight); +// Watch for changes in the zoneStore and update the layer +watch( + () => zoneStore.tiles, + () => { + // @TODO : change to tiles for when loading other maps + zoneStore.tiles.forEach((row, y) => row.forEach((tile, x) => layer.putTileAt(tile, x, y))) + }, + { deep: true } +) - tiles.value = []; - selectedTile.value = null; +function erase(tile: Phaser.Tilemaps.Tile) { + layer.removeTileAt(tile.x, tile.y) +} - for (let row = 0; row < rows; row++) { - for (let col = 0; col < columns; col++) { - const x = col * tileWidth; - const y = row * tileHeight; - - ctx.clearRect(0, 0, tileWidth, tileHeight); - ctx.drawImage(img, x, y, tileWidth, tileHeight, 0, 0, tileWidth, tileHeight); - - const tileDataURL = canvas.toDataURL(); - tiles.value.push(tileDataURL); - } - } -}; - -const selectTile = (index) => { - selectedTile.value = index; -}; - -onMounted(async () => { - const img = await loadImage(imagePath); - splitTiles(img); -}); - -/** - * Resources: - * https://codepen.io/Xymota/pen/gOOyxWB - * https://www.dynetisgames.com/2022/06/09/update-how-to-manage-big-isometric-maps-with-phaser-3-5/ - * https://stackoverflow.com/questions/11533606/javascript-splitting-a-tileset-image-to-be-stored-in-2d-image-array - */ +function tile() {} </script> - -<style lang="scss"> -.tiles { - display: flex; - flex-wrap: wrap; - gap: 10px; -} - -.tiles img { - width: 64px; - height: 32px; - cursor: pointer; - border: 2px solid transparent; - transition: border 0.3s ease; -} - -.tiles img.selected { - border: 2px solid #ff0000; -} -</style> \ No newline at end of file diff --git a/src/components/utilities/zoneEditor/ZoneEditorToolbar.vue b/src/components/utilities/zoneEditor/ZoneEditorToolbar.vue deleted file mode 100644 index 6187f16..0000000 --- a/src/components/utilities/zoneEditor/ZoneEditorToolbar.vue +++ /dev/null @@ -1,81 +0,0 @@ -<template> - <div class="toolbar"> - <div class="tools"> - <button :class="{ active: activeTool === 'tiles' }" @click="activeTool = 'tiles'"> - <img src="/assets/icons/zoneEditor/tiles.svg" alt="Eraser tool" /> - </button> - <div class="divider"></div> - <button :class="{ active: activeTool === 'eraser' }" @click="activeTool = 'eraser'"> - <img src="/assets/icons/zoneEditor/eraser-tool.svg" alt="Eraser tool" /> - </button> - </div> - <div class="buttons"> - <button class="btn-cyan">Save</button> - <button class="btn-cyan">Load</button> - <button class="btn-cyan">Clear</button> - <button class="btn-cyan">Exit</button> - </div> - </div> -</template> - -<script setup lang="ts"> -import { ref } from 'vue' - -const activeTool = ref('tiles'); -</script> - -<style scoped lang="scss"> -@import '@/assets/scss/main'; - -.toolbar { - position: absolute; - border-radius: 5px; - opacity: 0.8; - display: flex; - background: $dark-gray; - border: 2px solid $cyan; - color: $light-gray; - padding: 5px; - min-width: 100%; - height: 40px; - - .tools { - display: flex; - gap: 10px; - - .divider { - width: 1px; - background: $cyan; - } - - // vertical center - button { - display: flex; - justify-content: center; - align-items: center; - - &.active { - border-bottom: 3px solid $light-cyan; - border-radius: 5px; - } - } - - img { - filter: invert(1); - width: 35px; - height: 35px; - } - } - - .buttons { - display: flex; - gap: 10px; - margin-left: auto; - - button { - padding-left: 10px; - padding-right: 10px; - } - } -} -</style> \ No newline at end of file diff --git a/src/services/authentication.ts b/src/services/authentication.ts index c874e6d..566614b 100644 --- a/src/services/authentication.ts +++ b/src/services/authentication.ts @@ -7,7 +7,7 @@ export async function register(username: string, password: string, socketStore = try { const response = await axios.post(`${config.server_endpoint}/register`, { username, password }) useCookies().set('token', response.data.token as string) - return { success: true, token: response.data.token} + return { success: true, token: response.data.token } } catch (error: any) { return { error: error.response.data.message } } @@ -17,7 +17,7 @@ export async function login(username: string, password: string, socketStore = us try { const response = await axios.post(`${config.server_endpoint}/login`, { username, password }) useCookies().set('token', response.data.token as string) - return { success: true, token: response.data.token} + return { success: true, token: response.data.token } } catch (error: any) { return { error: error.response.data.message } } diff --git a/src/services/zone.ts b/src/services/zone.ts index 3eb7203..f8c9cef 100644 --- a/src/services/zone.ts +++ b/src/services/zone.ts @@ -2,7 +2,7 @@ import config from '@/config' export function getTile(x: number, y: number, layer: Phaser.Tilemaps.TilemapLayer): Phaser.Tilemaps.Tile | undefined { const tile: Phaser.Tilemaps.Tile = layer.getTileAtWorldXY(x, y) - if (!tile) return undefined; + if (!tile) return undefined return tile } @@ -22,4 +22,4 @@ export function tileToWorldXY(layer: Phaser.Tilemaps.TilemapLayer, pos_x: number const position_y = worldPoint.y return { position_x, position_y } -} \ No newline at end of file +} diff --git a/src/stores/socket.ts b/src/stores/socket.ts index f23a15b..068460e 100644 --- a/src/stores/socket.ts +++ b/src/stores/socket.ts @@ -11,12 +11,6 @@ export const useSocketStore: StoreDefinition = defineStore('socket', { user: null as User | null, character: null as Character | null }), - getters: { - getToken: (state: any) => state.token as string, - getConnection: (state: any) => state.connection as Socket, - getUser: (state: any) => state.user as User, - getCharacter: (state: any) => state.character as Character - }, actions: { initConnection() { this.connection = io(config.server_endpoint, { diff --git a/src/stores/zone.ts b/src/stores/zone.ts index 9e9b61f..c5079bd 100644 --- a/src/stores/zone.ts +++ b/src/stores/zone.ts @@ -3,21 +3,12 @@ import type { Character } from '@/types' export const useZoneStore = defineStore('zone', { state: () => ({ - loaded: false, - tiles: undefined, - characters: [] as Character[], - editorIsOpen: true + tiles: [] as number[][], + characters: [] as Character[] }), - getters: { - isLoaded: (state) => state.loaded, - getTiles: (state) => state.tiles, - getCharacters: (state) => state.characters, - getEditorIsOpen: (state) => state.editorIsOpen - }, actions: { - loadTiles(tiles: any) { + setTiles(tiles: number[][]) { this.tiles = tiles - this.loaded = true }, setCharacters(characters: Character[]) { this.characters = characters @@ -25,19 +16,13 @@ export const useZoneStore = defineStore('zone', { addCharacter(character: Character) { this.characters.push(character) }, - removeCharacter(character: Character) { - this.characters = this.characters.filter((c: Character) => c.id !== character.id) - }, updateCharacter(character: Character) { const index = this.characters.findIndex((c) => c.id === character.id) - if (index !== -1) { - this.characters[index] = character - } else { - console.error(`Character with id ${character.id} not found`) - } + if (index === -1) return + this.characters[index] = character }, - setEditorIsOpen(isOpen: boolean) { - this.editorIsOpen = isOpen + removeCharacter(character: Character) { + this.characters = this.characters.filter((c: Character) => c.id !== character.id) } } }) diff --git a/src/stores/zoneEditor.ts b/src/stores/zoneEditor.ts index d411022..f27d703 100644 --- a/src/stores/zoneEditor.ts +++ b/src/stores/zoneEditor.ts @@ -1,18 +1,20 @@ import { defineStore } from 'pinia' +import config from '@/config' export const useZoneEditorStore = defineStore('zoneEditor', { state: () => ({ - loaded: false, - tiles: undefined, + active: true }), getters: { - isLoaded: (state) => state.loaded, - getTiles: (state) => state.tiles, + isActive: (state) => state.active }, actions: { - loadTiles(tiles: any) { - this.tiles = tiles - this.loaded = true - }, + toggleActive() { + this.active = !this.active + } } }) + +/** + * [[0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,0],[0,1,1,1,1,1,1,1,1,0],[0,1,1,1,1,1,1,1,1,0],[0,1,1,1,1,1,1,1,1,0],[0,1,1,1,1,1,1,1,1,0],[0,1,1,1,1,1,1,1,1,0],[0,1,1,1,1,1,1,1,1,0],[0,1,1,1,1,1,1,1,1,0],[0,0,0,0,0,0,0,0,0,0]] + */