forked from noxious/client
Worked on zone manager
This commit is contained in:
103
src/components/utilities/zoneEditor/Tiles.vue
Normal file
103
src/components/utilities/zoneEditor/Tiles.vue
Normal file
@ -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>
|
125
src/components/utilities/zoneEditor/Toolbar.vue
Normal file
125
src/components/utilities/zoneEditor/Toolbar.vue
Normal file
@ -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>
|
@ -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>
|
@ -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>
|
Reference in New Issue
Block a user