forked from noxious/client
Worked on zone manager
This commit is contained in:
@ -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>
|
||||
</style>
|
||||
|
@ -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')
|
||||
})
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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">
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</style>
|
||||
|
@ -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>
|
||||
</style>
|
||||
|
@ -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')
|
||||
}
|
||||
|
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