Compare commits

..

17 Commits

Author SHA1 Message Date
ed6f592606 WIP character select 2024-11-06 23:05:37 +01:00
46ebfaec01 npm update 2024-11-05 23:18:10 +01:00
1384f50406 npm run format 2024-11-05 23:16:47 +01:00
d71f4e7b59 #192: Update light and other effects based on server date / weather state 2024-11-05 23:07:05 +01:00
58929290ab #184: Listen for weather updates from client 2024-11-05 22:46:21 +01:00
63146106c0 Moved clock pos. 2024-11-05 22:02:46 +01:00
7c5602f204 #197: Added background image loader 2024-11-05 22:01:28 +01:00
e711e124ce Map editor tiles improvement 2024-11-05 21:31:28 +01:00
e1b39c42ec Several map editor improvements 2024-11-05 21:28:12 +01:00
d81c889426 Removed GM tools, added event listener for shift + G to open GM panel 2024-11-05 20:53:39 +01:00
afb0edacf6 #184: Added clock component 2024-11-05 20:42:52 +01:00
6d7d568746 Moved login partial components 2024-11-05 20:36:00 +01:00
8df5b6eb76 #239: Add loading indicator to password reset submit button for better UX 2024-11-05 20:21:52 +01:00
270d12821a Renamed component, inform user when password reset mail has been sent, added comment for #238 2024-11-05 01:02:27 +01:00
9c244e980c Cleaned component 2024-11-05 00:35:26 +01:00
25ba54c8ac Moved ResetPassword component to correct dir 2024-11-05 00:33:28 +01:00
9c4bef864b Updated default value 2024-11-05 00:01:36 +01:00
22 changed files with 340 additions and 129 deletions

36
package-lock.json generated
View File

@ -2451,14 +2451,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/devtools-core": { "node_modules/@vue/devtools-core": {
"version": "7.6.2", "version": "7.6.3",
"resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.6.2.tgz", "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.6.3.tgz",
"integrity": "sha512-hJfjNR3ai94Mb6i0PB42kxUPkPreS6Dl07FUaHAcw+umtkUX55jTXe7+mhsHx9NI6NFT+1WMFREIy8O81KLYyA==", "integrity": "sha512-C7FOuh3Z+EmXXzDU9eRjHQL7zW7/CFovM6yCNNpUb+zXxhrn4fiqTum+a3gNau9DuzYfEtQXwZ9F7MeK0JKYVw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/devtools-kit": "^7.6.2", "@vue/devtools-kit": "^7.6.3",
"@vue/devtools-shared": "^7.6.2", "@vue/devtools-shared": "^7.6.3",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nanoid": "^3.3.4", "nanoid": "^3.3.4",
"pathe": "^1.1.2", "pathe": "^1.1.2",
@ -2469,13 +2469,13 @@
} }
}, },
"node_modules/@vue/devtools-kit": { "node_modules/@vue/devtools-kit": {
"version": "7.6.2", "version": "7.6.3",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.6.2.tgz", "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.6.3.tgz",
"integrity": "sha512-k61BxHRmcTtIQZFouF9QWt9nCCNtSdw12lhg8VNtHq5/XOBGD+ewiK27a40UJ8UPYoCJvi80hbvbYr5E/Zeu1g==", "integrity": "sha512-ETsFc8GlOp04rSFN79tB2TpVloWfsSx9BoCSElV3w3CaJTSBfz42KsIi5Ka+dNTJs1jY7QVLTDeoBmUGgA9h2A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/devtools-shared": "^7.6.2", "@vue/devtools-shared": "^7.6.3",
"birpc": "^0.2.19", "birpc": "^0.2.19",
"hookable": "^5.5.3", "hookable": "^5.5.3",
"mitt": "^3.0.1", "mitt": "^3.0.1",
@ -2485,9 +2485,9 @@
} }
}, },
"node_modules/@vue/devtools-shared": { "node_modules/@vue/devtools-shared": {
"version": "7.6.2", "version": "7.6.3",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.6.2.tgz", "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.6.3.tgz",
"integrity": "sha512-lcjyJ7hCC0W0kNwnCGMLVTMvDLoZgjcq9BvboPgS+6jQyDul7fpzRSKTGtGhCHoxrDox7qBAKGbAl2Rcf7GE1A==", "integrity": "sha512-wJW5QF27i16+sNQIaes8QoEZg1eqEgF83GkiPUlEQe9k7ZoHXHV7PRrnrxOKem42sIHPU813J2V/ZK1uqTJe6g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -7430,15 +7430,15 @@
} }
}, },
"node_modules/vite-plugin-vue-devtools": { "node_modules/vite-plugin-vue-devtools": {
"version": "7.6.2", "version": "7.6.3",
"resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.6.2.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.6.3.tgz",
"integrity": "sha512-YPE/8AIBsomvHadZ02Kkp8yZo2FR0SFNjbC2lcMgW+hNA1ZoXu9b5oi18gTMzJcLLFRNNSMNjShA4RLqXlIR/A==", "integrity": "sha512-p1rZMKzreWqxj9U05RaxY1vDoOhGYhA6iX8vKfo4nD6jqTmVoGjjk+U1g5HYwwTCdr/eck3kzO2f4gnPCjqVKA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/devtools-core": "^7.6.2", "@vue/devtools-core": "^7.6.3",
"@vue/devtools-kit": "^7.6.2", "@vue/devtools-kit": "^7.6.3",
"@vue/devtools-shared": "^7.6.2", "@vue/devtools-shared": "^7.6.3",
"execa": "^8.0.1", "execa": "^8.0.1",
"sirv": "^3.0.0", "sirv": "^3.0.0",
"vite-plugin-inspect": "^0.8.7", "vite-plugin-inspect": "^0.8.7",

View File

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_598_541)">
<path d="M10.0327 5.69111L12.3905 3.33333H9.33333V2H14.6667V7.33333H13.3333V4.27614L10.9755 6.63392C11.6183 7.47513 12 8.52633 12 9.66667C12 12.4281 9.7614 14.6667 7 14.6667C4.23857 14.6667 2 12.4281 2 9.66667C2 6.90527 4.23857 4.66667 7 4.66667C8.14033 4.66667 9.19153 5.04843 10.0327 5.69111ZM7 13.3333C9.02507 13.3333 10.6667 11.6917 10.6667 9.66667C10.6667 7.6416 9.02507 6 7 6C4.97495 6 3.33333 7.6416 3.33333 9.66667C3.33333 11.6917 4.97495 13.3333 7 13.3333Z" fill="#999999"/>
</g>
<defs>
<clipPath id="clip0_598_541">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 729 B

View File

@ -0,0 +1,18 @@
<svg width="190" height="202" viewBox="0 0 190 202" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_i_598_514)">
<path d="M0 3.60002C0 1.61179 1.61177 0 3.6 0H186.4C188.388 0 190 1.61177 190 3.6V193.658C190 195.646 188.388 197.258 186.4 197.258H184.894C183.584 197.258 182.523 198.32 182.523 199.629C182.523 200.938 181.461 202 180.152 202H9.84847C8.53901 202 7.47748 200.938 7.47748 199.629C7.47748 198.32 6.41596 197.258 5.1065 197.258H3.6C1.61178 197.258 0 195.646 0 193.658V3.60002Z" fill="#181818"/>
</g>
<path d="M0.3 3.60002C0.3 1.77747 1.77746 0.3 3.6 0.3H186.4C188.223 0.3 189.7 1.77746 189.7 3.6V193.658C189.7 195.481 188.223 196.958 186.4 196.958H184.894C183.418 196.958 182.223 198.154 182.223 199.629C182.223 200.773 181.295 201.7 180.152 201.7H9.84847C8.7047 201.7 7.77748 200.773 7.77748 199.629C7.77748 198.154 6.58164 196.958 5.1065 196.958H3.6C1.77746 196.958 0.3 195.481 0.3 193.658V3.60002Z" stroke="#454442" stroke-width="0.6"/>
<defs>
<filter id="filter0_i_598_514" x="0" y="0" width="190" height="204.4" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2.4"/>
<feGaussianBlur stdDeviation="2.34"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.13 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_598_514"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,6 +1,6 @@
<template> <template>
<Notifications /> <Notifications />
<GmTools v-if="gameStore.character?.role === 'gm'" /> <BackgroundImageLoader />
<GmPanel v-if="gameStore.character?.role === 'gm'" /> <GmPanel v-if="gameStore.character?.role === 'gm'" />
<component :is="currentScreen" /> <component :is="currentScreen" />
</template> </template>
@ -9,7 +9,7 @@
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import Notifications from '@/components/utilities/Notifications.vue' import Notifications from '@/components/utilities/Notifications.vue'
import GmTools from '@/components/gameMaster/GmTools.vue' import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
import GmPanel from '@/components/gameMaster/GmPanel.vue' import GmPanel from '@/components/gameMaster/GmPanel.vue'
import Login from '@/components/screens/Login.vue' import Login from '@/components/screens/Login.vue'
import Characters from '@/components/screens/Characters.vue' import Characters from '@/components/screens/Characters.vue'
@ -44,4 +44,16 @@ addEventListener('click', (event) => {
const audio = new Audio('/assets/music/click-btn.mp3') const audio = new Audio('/assets/music/click-btn.mp3')
audio.play() audio.play()
}) })
// Watch for "G" key press and toggle the gm panel
addEventListener('keydown', (event) => {
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
// Check if no input is active
if (event.repeat || event.isComposing || event.defaultPrevented) return
if (event.key === 'G') {
gameStore.toggleGmPanel()
}
})
</script> </script>

View File

@ -125,6 +125,12 @@ button {
} }
} }
.character {
&.active {
@apply pr-px border-r-0;
}
}
.text-pixel { .text-pixel {
@apply text-white font-ui drop-shadow-pixel-black; @apply text-white font-ui drop-shadow-pixel-black;
} }

View File

@ -1,21 +1,41 @@
<template> <template>
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene"> </Scene> <Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Scene } from 'phavuer' import { Scene } from 'phavuer'
import { useZoneStore } from '@/stores/zoneStore' import { useZoneStore } from '@/stores/zoneStore'
import { useGameStore } from '@/stores/gameStore'
import { onBeforeUnmount, ref, watch } from 'vue' import { onBeforeUnmount, ref, watch } from 'vue'
import type { WeatherState } from '@/types'
// Constants
const SUNRISE_HOUR = 6
const SUNSET_HOUR = 20
const DAY_STRENGTH = 100
const NIGHT_STRENGTH = 30
// Stores
const gameStore = useGameStore()
const zoneStore = useZoneStore() const zoneStore = useZoneStore()
// Scene ref
const sceneRef = ref<Phaser.Scene | null>(null) const sceneRef = ref<Phaser.Scene | null>(null)
// Effect-related refs // Effect refs
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null) const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null)
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null) const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null) const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
// State refs
const weatherState = ref<WeatherState>({
isRainEnabled: false,
rainPercentage: 0,
isFogEnabled: false,
fogDensity: 0
})
// Scene lifecycle methods
const preloadScene = async (scene: Phaser.Scene) => { const preloadScene = async (scene: Phaser.Scene) => {
scene.load.image('raindrop', 'assets/raindrop.png') scene.load.image('raindrop', 'assets/raindrop.png')
scene.load.image('fog', 'assets/fog.png') scene.load.image('fog', 'assets/fog.png')
@ -23,15 +43,21 @@ const preloadScene = async (scene: Phaser.Scene) => {
const createScene = async (scene: Phaser.Scene) => { const createScene = async (scene: Phaser.Scene) => {
sceneRef.value = scene sceneRef.value = scene
createLightEffect(scene) setupEffects(scene)
createRainEffect(scene) setupSocketListeners()
createFogEffect(scene)
} }
const updateScene = () => { const updateScene = () => {
updateEffects() updateEffects()
} }
// Effect setup
const setupEffects = (scene: Phaser.Scene) => {
createLightEffect(scene)
createRainEffect(scene)
createFogEffect(scene)
}
const createLightEffect = (scene: Phaser.Scene) => { const createLightEffect = (scene: Phaser.Scene) => {
lightEffect.value = scene.add.graphics() lightEffect.value = scene.add.graphics()
lightEffect.value.setDepth(1000) lightEffect.value.setDepth(1000)
@ -59,14 +85,55 @@ const createFogEffect = (scene: Phaser.Scene) => {
fogSprite.value.setDepth(950) fogSprite.value.setDepth(950)
} }
// Lighting calculations
const calculateLightStrength = (time: Date): number => {
const hour = time.getHours()
const minute = time.getMinutes()
let strength = DAY_STRENGTH
// Night time (10 PM - 6 AM)
if (hour >= SUNSET_HOUR || hour < SUNRISE_HOUR) {
strength = NIGHT_STRENGTH
}
// Full daylight (7 AM - 7 PM)
else if (hour > SUNRISE_HOUR && hour < SUNSET_HOUR - 2) {
strength = DAY_STRENGTH
}
// Sunrise transition (6 AM - 7 AM)
else if (hour === SUNRISE_HOUR) {
strength = NIGHT_STRENGTH + ((DAY_STRENGTH - NIGHT_STRENGTH) * minute) / 60
}
// Sunset transition (8 PM - 10 PM)
else if (hour >= SUNSET_HOUR - 2 && hour < SUNSET_HOUR) {
const totalMinutes = (hour - (SUNSET_HOUR - 2)) * 60 + minute
const transitionProgress = totalMinutes / 120 // 2 hours = 120 minutes
strength = DAY_STRENGTH - (DAY_STRENGTH - NIGHT_STRENGTH) * transitionProgress
}
return strength
}
// Effect updates
const updateEffects = () => { const updateEffects = () => {
const effects = zoneStore.zone?.zoneEffects || [] const effects = zoneStore.zone?.zoneEffects || []
if (effects.length > 0) {
updateZoneEffects(effects)
} else {
// Make sure we're getting the current time
const lightStrength = calculateLightStrength(gameStore.world.date)
updateLightEffect(lightStrength)
updateWeatherEffects()
}
}
const updateZoneEffects = (effects: any[]) => {
// Always update light based on time when zone effects are present
updateLightEffect(calculateLightStrength(gameStore.world.date))
effects.forEach((effect) => { effects.forEach((effect) => {
switch (effect.effect) { switch (effect.effect) {
case 'light':
updateLightEffect(effect.strength)
break
case 'rain': case 'rain':
updateRainEffect(effect.strength) updateRainEffect(effect.strength)
break break
@ -77,6 +144,11 @@ const updateEffects = () => {
}) })
} }
const updateWeatherEffects = () => {
updateRainEffect(weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0)
updateFogEffect(weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0)
}
const updateLightEffect = (strength: number) => { const updateLightEffect = (strength: number) => {
if (!lightEffect.value) return if (!lightEffect.value) return
const darkness = 1 - strength / 100 const darkness = 1 - strength / 100
@ -100,11 +172,34 @@ const updateFogEffect = (strength: number) => {
fogSprite.value.setAlpha(strength / 100) fogSprite.value.setAlpha(strength / 100)
} }
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true }) // Socket handlers
const setupSocketListeners = () => {
onBeforeUnmount(() => { // Initial weather state
if (sceneRef.value) sceneRef.value.scene.remove('effects') gameStore.connection?.emit('weather', (response: WeatherState) => {
if (zoneStore.zone?.zoneEffects) return
weatherState.value = response
updateEffects()
}) })
// @TODO : Fix resize issue // Weather updates
gameStore.connection?.on('weather', (data: WeatherState) => {
weatherState.value = data
updateEffects()
})
// Time updates
gameStore.connection?.on('date', () => {
if (zoneStore.zone?.zoneEffects) return
updateEffects()
})
}
// Watchers
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
// Cleanup
onBeforeUnmount(() => {
if (sceneRef.value) sceneRef.value.scene.remove('effects')
gameStore.connection?.off('weather')
})
</script> </script>

View File

@ -6,6 +6,7 @@
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button> <button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button> <button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button> <button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => zoneEditorStore.toggleActive()">Zone manager</button>
</div> </div>
</template> </template>
<template #modalBody> <template #modalBody>
@ -21,8 +22,10 @@ import { ref } from 'vue'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue' import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
let toggle = ref('asset-manager') let toggle = ref('asset-manager')
</script> </script>

View File

@ -1,44 +0,0 @@
<template>
<Modal :isModalOpen="true" :closable="false" :is-resizable="false" :modal-width="modalWidth" :modal-height="modalHeight" :modal-position-x="posXY.x" :modal-position-y="posXY.y">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">GM tools</h3>
</template>
<template #modalBody>
<div class="content flex flex-col gap-2.5 m-4 h-20">
<button class="btn-cyan py-1.5 px-4 w-full" type="button" @click="gameStore.toggleGmPanel()">Toggle GM panel</button>
<button class="btn-cyan py-1.5 px-4 w-full" type="button" @click="() => zoneEditorStore.toggleActive()">Zone manager</button>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import Modal from '@/components/utilities/Modal.vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import { onMounted, ref } from 'vue'
const zoneEditorStore = useZoneEditorStore()
const gameStore = useGameStore()
const modalWidth = ref(200)
const modalHeight = ref(170)
let posXY = ref({ x: 0, y: 0 })
onMounted(() => {
window.addEventListener('resize', () => {
posXY.value = customPositionGmPanel(modalWidth.value)
})
})
const customPositionGmPanel = (modalWidth: number) => {
const padding = 25
const width = window.innerWidth
const x = width - (modalWidth + 4) - 25
const y = padding
return { x, y }
}
posXY.value = customPositionGmPanel(modalWidth.value)
</script>

View File

@ -18,7 +18,6 @@ import { onUnmounted, ref } from 'vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { type Zone } from '@/types' import { type Zone } from '@/types'
// Components // Components
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue' import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue' import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
@ -32,6 +31,7 @@ import ZoneEventTiles from '@/components/gameMaster/zoneEditor/zonePartials/Zone
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
console.log(zoneEditorStore.zone)
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null) const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
@ -64,6 +64,7 @@ function save() {
} }
gameStore.connection?.emit('gm:zone_editor:zone:update', data, (response: Zone) => { gameStore.connection?.emit('gm:zone_editor:zone:update', data, (response: Zone) => {
console.log(response.updatedAt)
zoneEditorStore.setZone(response) zoneEditorStore.setZone(response)
}) })
} }

View File

@ -223,7 +223,7 @@ function selectTile(tile: string) {
} }
function isActiveTile(tile: Tile): boolean { function isActiveTile(tile: Tile): boolean {
return zoneEditorStore.selectedTile?.id === tile.id return zoneEditorStore.selectedTile === tile.id
} }
onMounted(async () => { onMounted(async () => {

View File

@ -191,7 +191,22 @@ onMounted(() => {
if (!zoneEditorStore.zone?.tiles) { if (!zoneEditorStore.zone?.tiles) {
return return
} }
setLayerTiles(tileMap, tileLayer, zoneEditorStore.zone.tiles)
// First fill the entire map with blank tiles using current zone dimensions
const blankTiles = createTileArray(zoneEditorStore.zone.width, zoneEditorStore.zone.height, 'blank_tile')
// Then overlay the zone tiles, but only within the current zone dimensions
const zoneTiles = zoneEditorStore.zone.tiles
for (let y = 0; y < zoneEditorStore.zone.height; y++) {
for (let x = 0; x < zoneEditorStore.zone.width; x++) {
// Only copy if the source tiles array has this position
if (zoneTiles[y] && zoneTiles[y][x] !== undefined) {
blankTiles[y][x] = zoneTiles[y][x]
}
}
}
setLayerTiles(tileMap, tileLayer, blankTiles)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil) scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser) scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)

View File

@ -0,0 +1,21 @@
<template>
<div class="absolute top-0 right-4 hidden lg:block">
<p class="text-white text-lg">{{ gameStore.world.date.toLocaleString() }}</p>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { onUnmounted } from 'vue'
const gameStore = useGameStore()
// Listen for new date from socket
gameStore.connection?.on('date', (data: Date) => {
gameStore.world.date = new Date(data)
})
onUnmounted(() => {
gameStore.connection?.off('date')
})
</script>

View File

@ -59,6 +59,15 @@ async function newPasswordFunc() {
newPasswordError.value = response.error newPasswordError.value = response.error
return return
} }
/**
* @TODO: #238, this wont work if we redirect to the login page
* Find a way to just "close" this screen instead of redirecting
*/
gameStore.addNotification({
title: 'Success',
message: 'Password changed successfully'
})
window.location.href = '/' window.location.href = '/'
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal @modal:close="() => emit('close')" :modal-width="400" :modal-height="300" :is-resizable="false"> <Modal :isModalOpen="true" :modal-width="400" :modal-height="300" :is-resizable="false" @modal:close="() => emit('close')">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Reset Password</h3> <h3 class="m-0 font-medium shrink-0 text-white">Reset Password</h3>
</template> </template>
@ -14,7 +14,13 @@
</div> </div>
<div class="grid grid-flow-col justify-stretch gap-4"> <div class="grid grid-flow-col justify-stretch gap-4">
<button class="btn-empty py-1.5 px-4 min-w-24 inline-block" @click.stop="() => emit('close')">Cancel</button> <button class="btn-empty py-1.5 px-4 min-w-24 inline-block" @click.stop="() => emit('close')">Cancel</button>
<button class="btn-cyan py-1.5 px-4 min-w-24 inline-block" type="submit">Send mail</button> <button class="btn-cyan py-1.5 px-4 min-w-24 inline-flex items-center justify-center" type="submit">
<svg v-if="isLoading" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 animate-spin mr-2" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Send mail
</button>
</div> </div>
</form> </form>
</div> </div>
@ -23,17 +29,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { ref } from 'vue'
import { resetPassword } from '@/services/authentication' import { resetPassword } from '@/services/authentication'
import { useGameStore } from '@/stores/gameStore'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore'
const emit = defineEmits(['close']) const emit = defineEmits(['close'])
const gameStore = useGameStore() const gameStore = useGameStore()
const isLoading = ref(false)
const email = ref('') const email = ref('')
const resetPasswordError = ref('') const resetPasswordError = ref('')
const isPasswordResetOpen = ref(false)
async function resetPasswordFunc() { async function resetPasswordFunc() {
// check if email is valid // check if email is valid
@ -42,14 +48,24 @@ async function resetPasswordFunc() {
return return
} }
isLoading.value = true
// send reset password event to server // send reset password event to server
const response = await resetPassword(email.value) const response = await resetPassword(email.value)
if (response.success === undefined) { if (response.success === undefined) {
resetPasswordError.value = response.error resetPasswordError.value = response.error
isLoading.value = false
return return
} }
gameStore.addNotification({
title: 'Success',
message: 'Password reset email sent'
})
isLoading.value = false
emit('close') emit('close')
} }
</script> </script>

View File

@ -1,60 +1,76 @@
<template> <template>
<div class="bg-gray-900 relative"> <div class="relative max-lg:h-dvh flex flex-row-reverse">
<div class="absolute bg-[url('/assets/shapes/select-screen-bg-shape.svg')] bg-no-repeat bg-center w-full h-full"></div> <div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
<div class="ui-wrapper h-dvh flex flex-col justify-center items-center gap-20 px-10 sm:px-20"> <div class="bg-[url('/assets/login/login-bg.png')] opacity-20 w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center grayscale"></div>
<div class="filler"></div> <div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative"></div>
<div class="flex gap-14 w-full max-h-[650px] overflow-x-auto" v-if="!isLoading"> <div class="absolute top-8 right-0 py-[18px] pr-[15px] pl-32 bg-gradient-to-r from-transparent to-cyan-900 z-20">
<!-- CHARACTER LIST --> <h2 class="text-white">CHARACTER SELECTION</h2>
<div v-for="character in characters" :key="character.id" class="group first:ml-auto last:mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 relative shadow-character" :class="{ active: selected_character == character.id }">
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" />
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" alt="UI box inner" />
<input class="opacity-0 h-full w-full absolute m-0 z-10" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" />
<label class="font-bold absolute left-1/2 top-4 max-w-32 -translate-x-1/2 -translate-y-1/2 text-center text-ellipsis overflow-hidden whitespace-nowrap drop-shadow-text" :for="character.id">{{ character.name }}</label>
<button
class="delete bg-red w-8 h-8 p-[3px] rounded-full absolute -right-4 top-0 -translate-y-1/2 z-10 border-2 border-solid border-white hover:bg-red-300"
@click="
() => {
deletingCharacter = character
}
"
>
<img draggable="false" src="/assets/icons/trashcan.svg" />
</button>
<div class="sprite-container flex flex-col items-center m-auto">
<img class="drop-shadow-20" draggable="false" src="/assets/avatar/default/0.png" />
</div> </div>
<span class="absolute bottom-6 w-full text-center translate-y-1/2 z-10">Lvl. {{ character.level }}</span> <div class="ui-wrapper h-dvh w-[calc(100%_-_80px)] sm:w-[calc(100%_-_160px)] absolute flex flex-col justify-center items-center gap-14 px-10 sm:px-20 z-30">
<div class="selected-character group-[.active]:max-w-[170px] absolute max-w-0 w-4/6 h-[3px] bg-gray-500 rounded-[3px] left-1/2 -bottom-4 -translate-x-1/2 transition-all ease-in-out duration-300"></div>
</div>
<div class="character new-character first:ml-auto mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 rounded-2xl relative bg-gray-500/50 bg-no-repeat shadow-character" v-if="characters.length < 4">
<button class="h-full w-full py-10 flex flex-col justify-between" @click="isModalOpen = true">
<div class="filler"></div> <div class="filler"></div>
<img class="w-24 h-24 m-auto" draggable="false" src="/assets/icons/plus-icon.svg" /> <div class="w-2/3 max-w-[860px]" v-if="!isLoading">
<span class="self-center text-base absolute bottom-5 w-full text-center translate-y-1/2 z-10">Create new</span> <div class="mb-5 flex flex-col gap-1">
<h1 class="text-white font-bold">SELECT CHARACTER TO PLAY</h1>
<p class="m-0">Maximum of 4 characters can be selected per player</p>
</div>
<div class="flex w-full h-[400px] border border-solid border-gray-500 rounded-md rounded-tl-none bg-gray">
<div class="w-1/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center border-0 border-r border-solid border-gray-500 rounded-bl-md relative">
<div class="absolute right-full -top-px flex gap-1 flex-col">
<div v-for="character in characters" :key="character.id" class="character relative rounded-l border border-solid border-gray-500 w-9 h-[50px] bg-[url('/assets/ui-texture.png')]" :class="{ 'active': selected_character == character.id }">
<img src="/assets/avatar/default/head.png" class="w-9 h-9 object-contain absolute top-1/2 -translate-y-1/2" alt="Player head" />
<input class="opacity-0 h-full w-full absolute m-0 z-10 hover:cursor-pointer" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" />
</div>
<div class="character relative rounded-l border border-solid border-gray-500 w-9 h-[50px] bg-[url('/assets/ui-texture.png')]" :class="{'active': characters.length == 0}" v-if="characters.length < 4">
<button class="p-0 h-full w-full flex flex-col justify-between" @click="isModalOpen = true">
<img class="w-6 h-6 object-contain absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2" draggable="false" src="/assets/icons/plus-icon.svg" />
</button> </button>
</div> </div>
</div> </div>
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6" v-if="selected_character">
<input class="input-field w-[158px]" type="text" name="name" :placeholder="characters.find(c => c.id == selected_character)?.name" />
<div class="flex flex-col gap-4 items-center">
<div class="flex flex-col gap-3">
<div class="bg-[url('/assets/ui-elements/ui-border-2-corners-bottom.svg')] w-[190px] h-52 bg-no-repeat bg-center flex items-center justify-center">
<img class="w-12 object-contain mb-3.5" src="/assets/avatar/default/0.png" alt="Player avatar"/>
</div>
<div class="flex justify-between w-[190px]">
<!-- TODO: replace with color swatches -->
<div v-for="n in 9" class="w-4 h-4 rounded-sm bg-white"></div>
</div>
</div>
<div class="flex justify-between w-[190px]">
<button class="btn-empty flex gap-2">
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
<span class="text-white">Male</span>
</button>
<button class="btn-empty flex gap-2">
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
<span class="text-white">Female</span>
</button>
</div>
</div>
</div>
</div>
<div class="w-2/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center rounded-r-md"></div>
</div>
</div>
<div v-else> <div v-else>
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" /> <img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" />
</div> </div>
<div class="button-wrapper flex gap-8" v-if="!isLoading"> <div class="button-wrapper flex self-center justify-end gap-4 max-w-[860px] w-full" v-if="!isLoading">
<button <button
class="btn-red py-2 pr-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-red/50 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]" class="btn-empty min-w-48"
@click.stop="gameStore.disconnectSocket()" @click.stop="gameStore.disconnectSocket()"
> >
<img class="h-8 drop-shadow-20 rotate-180" draggable="false" src="/assets/icons/arrow.svg" alt="Logout icon" /> Back
</button> </button>
<button <button
class="btn-cyan py-2 px-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-cyan-800 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]" class="btn-cyan min-w-48 disabled:bg-cyan-800 disabled:cursor-not-allowed"
:disabled="!selected_character" :disabled="!selected_character"
@click="select_character()" @click="select_character()"
> >
PLAY Play now
<img class="h-8 drop-shadow-20" draggable="false" src="/assets/icons/arrow.svg" alt="Play icon" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -5,7 +5,7 @@
<Menu /> <Menu />
<Hud /> <Hud />
<Hotkeys /> <Hotkeys />
<Minimap /> <Clock />
<Zone /> <Zone />
<Chat /> <Chat />
<ExpBar /> <ExpBar />
@ -30,7 +30,8 @@ import Hotkeys from '@/components/gui/Hotkeys.vue'
import Chat from '@/components/gui/Chat.vue' import Chat from '@/components/gui/Chat.vue'
import CharacterProfile from '@/components/gui/CharacterProfile.vue' import CharacterProfile from '@/components/gui/CharacterProfile.vue'
import Effects from '@/components/Effects.vue' import Effects from '@/components/Effects.vue'
import Minimap from '@/components/gui/Minimap.vue' // import Minimap from '@/components/gui/Minimap.vue'
import Clock from '@/components/gui/Clock.vue'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin' import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
const gameStore = useGameStore() const gameStore = useGameStore()

View File

@ -29,10 +29,10 @@
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
import LoginForm from '@/components/screens/partials/login/LoginForm.vue' import LoginForm from '@/components/login/LoginForm.vue'
import RegisterForm from '@/components/screens/partials/login/RegisterForm.vue' import RegisterForm from '@/components/login/RegisterForm.vue'
import NewPasswordForm from '@/components/screens/partials/login/NewPasswordForm.vue' import NewPasswordForm from '@/components/login/NewPasswordForm.vue'
import ResetPassword from '@/components/utilities/ResetPassword.vue' import ResetPassword from '@/components/login/ResetPasswordModal.vue'
const isPasswordResetFormShown = ref(false) const isPasswordResetFormShown = ref(false)
const doesUrlHaveToken = window.location.hash.includes('#') const doesUrlHaveToken = window.location.hash.includes('#')

View File

@ -0,0 +1,23 @@
<template>
<div style="display: none">
<img v-for="(url, index) in imageUrls" :key="index" :src="url" alt="" @load="handleImageLoad(index)" @error="handleImageError(index)" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// Internal array of images to preload
const imageUrls = ref<string[]>(['/assets/ui-elements/ui-border-4-corners.svg', '/assets/ui-elements/ui-border-4-corners-light.svg', '/assets/ui-elements/ui-border-4-corners-small.svg'])
const loadedImages = ref<Set<number>>(new Set())
const handleImageLoad = (index: number) => {
loadedImages.value.add(index)
console.log(`Image ${index} loaded:`, imageUrls.value[index])
}
const handleImageError = (index: number) => {
console.log(`Image ${index} failed to load:`, imageUrls.value[index])
}
</script>

View File

@ -12,7 +12,7 @@ export type TeleportSettings = {
export const useZoneEditorStore = defineStore('zoneEditor', { export const useZoneEditorStore = defineStore('zoneEditor', {
state: () => { state: () => {
return { return {
active: true, active: false,
zone: null as Zone | null, zone: null as Zone | null,
tool: 'move', tool: 'move',
drawMode: 'tile', drawMode: 'tile',
@ -123,6 +123,8 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
this.drawMode = 'tile' this.drawMode = 'tile'
this.selectedTile = '' this.selectedTile = ''
this.selectedObject = null this.selectedObject = null
this.isTileListModalShown = false
this.isObjectListModalShown = false
this.isSettingsModalShown = false this.isSettingsModalShown = false
this.isZoneListModalShown = false this.isZoneListModalShown = false
this.isCreateZoneModalShown = false this.isCreateZoneModalShown = false

View File

@ -224,3 +224,10 @@ export type WorldSettings = {
isFogEnabled: boolean isFogEnabled: boolean
fogDensity: number fogDensity: number
} }
export type WeatherState = {
isRainEnabled: boolean
rainPercentage: number
isFogEnabled: boolean
fogDensity: number
}