Compare commits

..

1 Commits

Author SHA1 Message Date
c1360899e1 #263 - Updated character movement to pure ts (WIP) 2025-01-02 23:34:53 +01:00
109 changed files with 3432 additions and 2624 deletions

View File

@ -1,5 +1,5 @@
VITE_NAME=Noxious VITE_NAME=Noxious
VITE_DEVELOPMENT=true VITE_DEVELOPMENT=true
VITE_SERVER_ENDPOINT=http://localhost:4000 VITE_SERVER_ENDPOINT=http://localhost:4000
VITE_TILE_SIZE_WIDTH=64 VITE_TILE_SIZE_X=64
VITE_TILE_SIZE_HEIGHT=32 VITE_TILE_SIZE_Y=32

1714
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,7 @@
"typescript": "~5.6.2", "typescript": "~5.6.2",
"vite": "^5.4.9", "vite": "^5.4.9",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "^7.5.2",
"vitest": "^2.0.3", "vitest": "^2.0.3",
"vue-tsc": "^1.6.5" "vue-tsc": "^1.6.5"
} }

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 325 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 847 B

View File

Before

Width:  |  Height:  |  Size: 745 B

After

Width:  |  Height:  |  Size: 745 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 453 KiB

View File

Before

Width:  |  Height:  |  Size: 109 B

After

Width:  |  Height:  |  Size: 109 B

View File

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 696 B

View File

Before

Width:  |  Height:  |  Size: 708 B

After

Width:  |  Height:  |  Size: 708 B

View File

@ -1,5 +1,4 @@
<template> <template>
<Debug />
<Notifications /> <Notifications />
<BackgroundImageLoader /> <BackgroundImageLoader />
<GmPanel v-if="gameStore.character?.role === 'gm'" /> <GmPanel v-if="gameStore.character?.role === 'gm'" />
@ -10,40 +9,34 @@
import GmPanel from '@/components/gameMaster/GmPanel.vue' import GmPanel from '@/components/gameMaster/GmPanel.vue'
import Characters from '@/components/screens/Characters.vue' import Characters from '@/components/screens/Characters.vue'
import Game from '@/components/screens/Game.vue' import Game from '@/components/screens/Game.vue'
import Loading from '@/components/screens/Loading.vue'
import Login from '@/components/screens/Login.vue' import Login from '@/components/screens/Login.vue'
import MapEditor from '@/components/screens/MapEditor.vue' import ZoneEditor from '@/components/screens/ZoneEditor.vue'
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue' import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
import Debug from '@/components/utilities/Debug.vue'
import Notifications from '@/components/utilities/Notifications.vue' import Notifications from '@/components/utilities/Notifications.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { computed, watch } from 'vue' import { computed, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore() const zoneEditorStore = useZoneEditorStore()
const currentScreen = computed(() => { const currentScreen = computed(() => {
if (!gameStore.game.isLoaded) return Loading
if (!gameStore.connection) return Login if (!gameStore.connection) return Login
if (!gameStore.token) return Login if (!gameStore.token) return Login
if (!gameStore.character) return Characters if (!gameStore.character) return Characters
if (mapEditorStore.active) return MapEditor if (zoneEditorStore.active) return ZoneEditor
return Game return Game
}) })
// Watch mapEditorStore.active and empty gameStore.game.loadedAssets // Watch zoneEditorStore.active and empty gameStore.game.loadedAssets
watch( watch(
() => mapEditorStore.active, () => zoneEditorStore.active,
() => { () => {
gameStore.game.loadedTextures = [] gameStore.game.loadedAssets = []
} }
) )
// #209: Play sound when a button is pressed // #209: Play sound when a button is pressed
/**
* @TODO: Not all button-like elements will actually be a button, so we need to find a better way to do this
*/
addEventListener('click', (event) => { addEventListener('click', (event) => {
if (!(event.target instanceof HTMLButtonElement)) { if (!(event.target instanceof HTMLButtonElement)) {
return return
@ -57,9 +50,7 @@ addEventListener('keydown', (event) => {
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
// Check if no input is active or focus is on an input // Check if no input is active or focus is on an input
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') { if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') return
return
}
if (event.key === 'G') { if (event.key === 'G') {
gameStore.toggleGmPanel() gameStore.toggleGmPanel()

81
src/application/assets.ts Normal file
View File

@ -0,0 +1,81 @@
import config from '@/application/config'
import type { AssetDataT } from '@/application/types'
import Dexie from 'dexie'
export class Assets {
private db: Dexie
constructor() {
this.db = new Dexie('assets')
this.db.version(1).stores({
assets: 'key, group'
})
}
async download(asset: AssetDataT) {
try {
// Check if the asset already exists, then check if updatedAt is newer
const _asset = await this.db.table('assets').get(asset.key)
if (_asset && _asset.updatedAt > asset.updatedAt) {
return
}
// Download the asset
const response = await fetch(config.server_endpoint + asset.data)
const blob = await response.blob()
// Store the asset in the database
await this.db.table('assets').put({
key: asset.key,
data: blob,
group: asset.group,
updatedAt: asset.updatedAt,
originX: asset.originX,
originY: asset.originY,
isAnimated: asset.isAnimated,
frameRate: asset.frameRate,
frameWidth: asset.frameWidth,
frameHeight: asset.frameHeight,
frameCount: asset.frameCount
})
} catch (error) {
console.error(`Failed to add asset ${asset.key}:`, error)
}
}
async get(key: string) {
try {
const asset = await this.db.table('assets').get(key)
if (asset) {
return {
...asset,
data: URL.createObjectURL(asset.data) // Convert blob to data URL
}
}
} catch (error) {
console.error(`Failed to retrieve asset ${key}:`, error)
}
return null
}
async getByGroup(group: string) {
try {
const assets = await this.db.table('assets').where('group').equals(group).toArray()
return assets.map((asset) => ({
...asset,
data: URL.createObjectURL(asset.data) // Convert blob to data URL
}))
} catch (error) {
console.error(`Failed to retrieve assets for group ${group}:`, error)
return []
}
}
async delete(key: string) {
try {
await this.db.table('assets').delete(key)
} catch (error) {
console.error(`Failed to delete asset ${key}:`, error)
}
}
}

View File

@ -3,7 +3,7 @@ export default {
development: import.meta.env.VITE_DEVELOPMENT === 'true', development: import.meta.env.VITE_DEVELOPMENT === 'true',
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT, server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
tile_size: { tile_size: {
width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH), x: Number(import.meta.env.VITE_TILE_SIZE_X),
height: Number(import.meta.env.VITE_TILE_SIZE_HEIGHT) y: Number(import.meta.env.VITE_TILE_SIZE_Y)
} }
} }

View File

@ -12,10 +12,10 @@ export type HttpResponse<T> = {
data?: T data?: T
} }
export type TextureData = { export type AssetDataT = {
key: string key: string
data: string // URL or Base64 encoded blob data: string
group: 'tiles' | 'map_objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other' group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
updatedAt: Date updatedAt: Date
originX?: number originX?: number
originY?: number originY?: number
@ -34,7 +34,7 @@ export type Tile = {
updatedAt: Date updatedAt: Date
} }
export type MapObject = { export type Object = {
id: UUID id: UUID
name: string name: string
tags: any | null tags: any | null
@ -46,6 +46,7 @@ export type MapObject = {
frameHeight: number frameHeight: number
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
ZoneObject: ZoneObject[]
} }
export type Item = { export type Item = {
@ -55,6 +56,7 @@ export type Item = {
itemType: ItemType itemType: ItemType
stackable: boolean stackable: boolean
rarity: ItemRarity rarity: ItemRarity
spriteId: UUID | null
sprite?: Sprite sprite?: Sprite
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
@ -63,59 +65,65 @@ export type Item = {
export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVES' | 'RING' | 'NECKLACE' export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVES' | 'RING' | 'NECKLACE'
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY' export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
export type Map = { export type Zone = {
id: UUID id: UUID
name: string name: string
width: number width: number
height: number height: number
tiles: any | null tiles: any | null
pvp: boolean pvp: boolean
mapEffects: MapEffect[] zoneEffects: ZoneEffect[]
mapEventTiles: MapEventTile[] zoneEventTiles: ZoneEventTile[]
placedMapObjects: PlacedMapObject[] zoneObjects: ZoneObject[]
characters: Character[] characters: Character[]
chats: Chat[] chats: Chat[]
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
} }
export type MapEffect = { export type ZoneEffect = {
id: UUID id: UUID
map: Map zoneId: UUID
zone: Zone
effect: string effect: string
strength: number strength: number
} }
export type PlacedMapObject = { export type ZoneObject = {
id: UUID id: UUID
map: Map zoneId: UUID
mapObject: MapObject zone: Zone
objectId: UUID
object: Object
depth: number depth: number
isRotated: boolean isRotated: boolean
positionX: number positionX: number
positionY: number positionY: number
} }
export enum MapEventTileType { export enum ZoneEventTileType {
BLOCK = 'BLOCK', BLOCK = 'BLOCK',
TELEPORT = 'TELEPORT', TELEPORT = 'TELEPORT',
NPC = 'NPC', NPC = 'NPC',
ITEM = 'ITEM' ITEM = 'ITEM'
} }
export type MapEventTile = { export type ZoneEventTile = {
id: UUID id: UUID
map: Map zoneId: UUID
type: MapEventTileType zone: Zone
type: ZoneEventTileType
positionX: number positionX: number
positionY: number positionY: number
teleport?: MapEventTileTeleport teleport?: ZoneEventTileTeleport
} }
export type MapEventTileTeleport = { export type ZoneEventTileTeleport = {
id: UUID id: UUID
mapEventTile: MapEventTile zoneEventTileId: UUID
toMap: Map zoneEventTile: ZoneEventTile
toZoneId: UUID
toZone: Zone
toPositionX: number toPositionX: number
toPositionY: number toPositionY: number
toRotation: number toRotation: number
@ -147,6 +155,8 @@ export type CharacterType = {
gender: CharacterGender gender: CharacterGender
race: CharacterRace race: CharacterRace
isSelectable: boolean isSelectable: boolean
characters: Character[]
spriteId?: string
sprite?: Sprite sprite?: Sprite
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
@ -155,7 +165,7 @@ export type CharacterType = {
export type CharacterHair = { export type CharacterHair = {
id: UUID id: UUID
name: string name: string
sprite?: Sprite sprite: Sprite
gender: CharacterGender gender: CharacterGender
isSelectable: boolean isSelectable: boolean
} }
@ -174,22 +184,27 @@ export type Character = {
positionX: number positionX: number
positionY: number positionY: number
rotation: number rotation: number
characterType: UUID | null characterTypeId: UUID | null
characterHair: UUID | null characterType: CharacterType | null | string
map: UUID characterHairId: UUID | null
characterHair: CharacterHair | null
zoneId: UUID
zone: Zone
chats: Chat[] chats: Chat[]
items: CharacterItem[] items: CharacterItem[]
equipment: CharacterEquipment[] equipment: CharacterEquipment[]
} }
export type MapCharacter = { export type ZoneCharacter = {
character: Character character: Character
isMoving: boolean isMoving?: boolean
} }
export type CharacterItem = { export type CharacterItem = {
id: UUID id: UUID
characterId: UUID
character: Character character: Character
itemId: UUID
item: Item item: Item
quantity: number quantity: number
} }
@ -197,6 +212,7 @@ export type CharacterItem = {
export type CharacterEquipment = { export type CharacterEquipment = {
id: UUID id: UUID
slot: CharacterEquipmentSlotType slot: CharacterEquipmentSlotType
characterItemId: UUID
characterItem: CharacterItem characterItem: CharacterItem
} }
@ -234,8 +250,10 @@ export type SpriteAction = {
export type Chat = { export type Chat = {
id: UUID id: UUID
characterId: UUID
character: Character character: Character
map: Map zoneId: UUID
zone: Zone
message: string message: string
createdAt: Date createdAt: Date
} }
@ -254,7 +272,7 @@ export type WeatherState = {
fogDensity: number fogDensity: number
} }
export type mapLoadData = { export type zoneLoadData = {
mapId: UUID zone: Zone
characters: MapCharacter[] characters: ZoneCharacter[]
} }

View File

@ -31,12 +31,6 @@ body {
@apply outline-offset-2; @apply outline-offset-2;
@apply rounded-sm; @apply rounded-sm;
} }
@media only screen and (orientation:portrait) and (max-width: 768px) {
.portrait-mode-notice {
@apply block;
}
}
} }
h1, h1,

View File

@ -3,10 +3,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Map, WeatherState } from '@/application/types' import type { WeatherState } from '@/application/types'
import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore' import { useZoneStore } from '@/stores/zoneStore'
import { Scene } from 'phavuer' import { Scene } from 'phavuer'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue' import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
@ -20,11 +19,9 @@ const LIGHT_CONFIG = {
// Stores and refs // Stores and refs
const gameStore = useGameStore() const gameStore = useGameStore()
const mapStore = useMapStore() const zoneStore = useZoneStore()
const mapStorage = new MapStorage()
const sceneRef = ref<Phaser.Scene | null>(null) const sceneRef = ref<Phaser.Scene | null>(null)
const mapEffectsReady = ref(false) const zoneEffectsReady = ref(false)
const mapObject = ref<Map | null>(null)
// Effect objects // Effect objects
const effects = { const effects = {
@ -53,24 +50,6 @@ const createScene = (scene: Phaser.Scene) => {
setupSocketListeners() setupSocketListeners()
} }
const loadMap = async () => {
if (!mapStore.mapId) return
mapObject.value = await mapStorage.get(mapStore.mapId)
}
// Watch for mapId changes and load map when it's available
watch(
() => mapStore.mapId,
async (newMapId) => {
if (newMapId) {
mapEffectsReady.value = false
await loadMap()
updateScene()
}
},
{ immediate: true }
)
const initializeEffects = (scene: Phaser.Scene) => { const initializeEffects = (scene: Phaser.Scene) => {
// Light // Light
effects.light.value = scene.add.graphics().setDepth(1000) effects.light.value = scene.add.graphics().setDepth(1000)
@ -101,7 +80,7 @@ const initializeEffects = (scene: Phaser.Scene) => {
// Effect updates // Effect updates
const updateScene = () => { const updateScene = () => {
const timeBasedLight = calculateLightStrength(gameStore.world.date) const timeBasedLight = calculateLightStrength(gameStore.world.date)
const mapEffects = mapObject.value?.mapEffects?.reduce( const zoneEffects = zoneStore.zone?.zoneEffects?.reduce(
(acc, curr) => ({ (acc, curr) => ({
...acc, ...acc,
[curr.effect]: curr.strength [curr.effect]: curr.strength
@ -109,23 +88,24 @@ const updateScene = () => {
{} {}
) as { [key: string]: number } ) as { [key: string]: number }
// Only update effects once mapEffects are loaded // Only update effects once zoneEffects are loaded
if (!mapEffectsReady.value) { if (!zoneEffectsReady.value) {
if (mapObject.value) { if (zoneEffects && Object.keys(zoneEffects).length) {
mapEffectsReady.value = true zoneEffectsReady.value = true
} else { } else {
return return
} }
} }
const finalEffects = const finalEffects =
mapEffects && Object.keys(mapEffects).length zoneEffects && Object.keys(zoneEffects).length
? mapEffects ? zoneEffects
: { : {
light: timeBasedLight, light: timeBasedLight,
rain: weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0, rain: weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0,
fog: weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0 fog: weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0
} }
applyEffects(finalEffects) applyEffects(finalEffects)
} }
@ -166,12 +146,12 @@ const setupSocketListeners = () => {
updateScene() updateScene()
}) })
gameStore.connection?.on('weather', (data: WeatherState) => { gameStore.connection!.on('weather', (data: WeatherState) => {
weatherState.value = data weatherState.value = data
updateScene() updateScene()
}) })
gameStore.connection?.on('date', updateScene) gameStore.connection!.on('date', updateScene)
} }
const handleResize = () => { const handleResize = () => {
@ -181,9 +161,9 @@ const handleResize = () => {
// Lifecycle // Lifecycle
watch( watch(
() => mapObject.value, () => zoneStore.zone,
() => { () => {
mapEffectsReady.value = false zoneEffectsReady.value = false
updateScene() updateScene()
}, },
{ deep: true } { deep: true }

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="flex flex-wrap items-center input-field gap-1" @click="focusInput"> <div class="flex flex-wrap items-center input-field gap-1">
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2" role="listitem"> <div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2">
<span class="text-xs text-white">{{ chip }}</span> <span class="text-xs text-white">{{ chip }}</span>
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click.stop="deleteChip(i)" aria-label="Remove tag">×</button> <button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click="deleteChip(i)" aria-label="Remove chip">×</button>
</div> </div>
<input ref="inputRef" class="outline-none border-none p-1 text-gray-300 min-w-[60px] flex-grow" :placeholder="placeholder" v-model.trim="currentInput" @keydown="handleKeydown" @paste="handlePaste" :maxlength="maxChipLength" aria-label="Add new tag" /> <input class="outline-none border-none p-1 text-gray-300" placeholder="Tag name" v-model="currentInput" @keypress.enter.prevent="addChip" @keydown.backspace="handleBackspace" />
</div> </div>
</template> </template>
@ -14,29 +14,20 @@ import type { Ref } from 'vue'
interface Props { interface Props {
modelValue?: string[] modelValue?: string[]
maxChips?: number
maxChipLength?: number
placeholder?: string
allowDuplicates?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
modelValue: () => [], modelValue: () => []
maxChips: 10,
maxChipLength: 20,
placeholder: 'Add tag',
allowDuplicates: false
}) })
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void (e: 'update:modelValue', value: string[]): void
(e: 'error', message: string): void
}>() }>()
const currentInput: Ref<string> = ref('') const currentInput: Ref<string> = ref('')
const internalValue = ref<string[]>([]) const internalValue = ref<string[]>([])
const inputRef = ref<HTMLInputElement | null>(null)
// Initialize internalValue with props.modelValue
watch( watch(
() => props.modelValue, () => props.modelValue,
(newValue) => { (newValue) => {
@ -45,27 +36,9 @@ watch(
{ immediate: true } { immediate: true }
) )
const validateChip = (chip: string): boolean => {
if (!chip) {
return false
}
if (!props.allowDuplicates && internalValue.value.includes(chip)) {
emit('error', 'Duplicate tags are not allowed')
return false
}
if (internalValue.value.length >= props.maxChips) {
emit('error', `Maximum ${props.maxChips} tags allowed`)
return false
}
return true
}
const addChip = () => { const addChip = () => {
const trimmedInput = currentInput.value.trim() const trimmedInput = currentInput.value.trim()
if (validateChip(trimmedInput)) { if (trimmedInput && !internalValue.value.includes(trimmedInput)) {
internalValue.value.push(trimmedInput) internalValue.value.push(trimmedInput)
emit('update:modelValue', internalValue.value) emit('update:modelValue', internalValue.value)
currentInput.value = '' currentInput.value = ''
@ -77,36 +50,10 @@ const deleteChip = (index: number) => {
emit('update:modelValue', internalValue.value) emit('update:modelValue', internalValue.value)
} }
const handleKeydown = (event: KeyboardEvent) => { const handleBackspace = (event: KeyboardEvent) => {
switch (event.key) { if (event.key === 'Backspace' && currentInput.value === '' && internalValue.value.length > 0) {
case 'Enter': internalValue.value.pop()
event.preventDefault() emit('update:modelValue', internalValue.value)
addChip()
break
case 'Backspace':
if (currentInput.value === '' && internalValue.value.length > 0) {
deleteChip(internalValue.value.length - 1)
} }
break
}
}
const handlePaste = (event: ClipboardEvent) => {
event.preventDefault()
const pastedText = event.clipboardData?.getData('text')
if (pastedText) {
const chips = pastedText
.split(/[,\n]/)
.map((chip) => chip.trim())
.filter(Boolean)
chips.forEach((chip) => {
currentInput.value = chip
addChip()
})
}
}
const focusInput = () => {
inputRef.value?.focus()
} }
</script> </script>

View File

@ -1,24 +1,28 @@
<template> <template>
<ChatBubble :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" /> <ChatBubble :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
<Healthbar :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" /> <Healthbar :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
<Container ref="charContainer" :depth="isometricDepth" :x="currentPositionX" :y="currentPositionY"> <Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY">
<CharacterHair :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
<!-- <CharacterChest :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />-->
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" /> <Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" />
</Container> </Container>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import config from '@/application/config' import config from '@/application/config'
import { type MapCharacter } from '@/application/types' import { type Sprite as SpriteT, type ZoneCharacter } from '@/application/types'
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue' import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
import Healthbar from '@/components/game/character/partials/Healthbar.vue' import Healthbar from '@/components/game/character/partials/Healthbar.vue'
import { loadSpriteTextures } from '@/composables/gameComposable' import { loadSpriteTextures } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable' import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { CharacterTypeStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore' import { useZoneStore } from '@/stores/zoneStore'
import { Container, refObj, Sprite, useScene } from 'phavuer' import { Container, refObj, Sprite, useScene } from 'phavuer'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
// import CharacterChest from '@/components/game/character/partials/CharacterChest.vue'
enum Direction { enum Direction {
POSITIVE, POSITIVE,
NEGATIVE, NEGATIVE,
@ -27,151 +31,162 @@ enum Direction {
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tilemap: Phaser.Tilemaps.Tilemap
mapCharacter: MapCharacter zoneCharacter: ZoneCharacter
}>() }>()
const charContainer = refObj<Phaser.GameObjects.Container>() const charContainer = refObj<Phaser.GameObjects.Container>()
const charSprite = refObj<Phaser.GameObjects.Sprite>() const charSprite = refObj<Phaser.GameObjects.Sprite>()
const charSpriteId = ref('')
const gameStore = useGameStore() const gameStore = useGameStore()
const mapStore = useMapStore() const zoneStore = useZoneStore()
const scene = useScene() const scene = useScene()
const currentPositionX = ref(0) const currentX = ref(0)
const currentPositionY = ref(0) const currentY = ref(0)
const isometricDepth = ref(1) const isometricDepth = ref(1)
const isInitialPosition = ref(true) const isInitialPosition = ref(true)
const tween = ref<Phaser.Tweens.Tween | null>(null) const isMoving = ref(false)
let animationFrame: number | null = null
const moveSpeed = 5.7
const updateIsometricDepth = (positionX: number, positionY: number) => { const updateIsometricDepth = (x: number, y: number) => {
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 28, 94, true) isometricDepth.value = calculateIsometricDepth(x, y, 28, 94, true)
} }
const updatePosition = (positionX: number, positionY: number, direction: Direction) => { const stopMovement = () => {
const newPositionX = tileToWorldX(props.tilemap, positionX, positionY) isMoving.value = false
const newPositionY = tileToWorldY(props.tilemap, positionX, positionY) if (animationFrame) {
cancelAnimationFrame(animationFrame)
animationFrame = null
}
}
const updatePosition = (x: number, y: number, direction: Direction) => {
const targetX = tileToWorldX(props.tilemap, x, y)
const targetY = tileToWorldY(props.tilemap, x, y)
if (isInitialPosition.value) { if (isInitialPosition.value) {
currentPositionX.value = newPositionX currentX.value = targetX
currentPositionY.value = newPositionY currentY.value = targetY
isInitialPosition.value = false isInitialPosition.value = false
return return
} }
if (tween.value?.isPlaying()) { if (isMoving.value) {
tween.value.stop() stopMovement()
} }
const distance = Math.sqrt(Math.pow(newPositionX - currentPositionX.value, 2) + Math.pow(newPositionY - currentPositionY.value, 2)) const distance = Math.sqrt(Math.pow(targetX - currentX.value, 2) + Math.pow(targetY - currentY.value, 2))
if (distance >= config.tile_size.width / 1.1) { isMoving.value = true
currentPositionX.value = newPositionX const startTime = performance.now()
currentPositionY.value = newPositionY const startX = currentX.value
return const startY = currentY.value
} const duration = distance * moveSpeed
const duration = distance * 5.7
tween.value = props.tilemap.scene.tweens.add({
targets: { x: currentPositionX.value, y: currentPositionY.value },
x: newPositionX,
y: newPositionY,
duration,
ease: 'Linear',
onStart: () => {
if (direction === Direction.POSITIVE) { if (direction === Direction.POSITIVE) {
updateIsometricDepth(positionX, positionY) updateIsometricDepth(x, y)
} }
},
onUpdate: (tween) => { const animate = (currentTime: number) => {
// @ts-ignore if (!isMoving.value) return
currentPositionX.value = tween.targets[0].x
// @ts-ignore const elapsed = currentTime - startTime
currentPositionY.value = tween.targets[0].y const progress = Math.min(elapsed / duration, 1)
},
onComplete: () => { currentX.value = startX + (targetX - startX) * progress
currentY.value = startY + (targetY - startY) * progress
if (progress < 1) {
animationFrame = requestAnimationFrame(animate)
} else {
isMoving.value = false
if (direction === Direction.NEGATIVE) { if (direction === Direction.NEGATIVE) {
updateIsometricDepth(positionX, positionY) updateIsometricDepth(x, y)
} }
} }
}) }
animationFrame = requestAnimationFrame(animate)
} }
const calcDirection = (oldPositionX: number, oldPositionY: number, newPositionX: number, newPositionY: number): Direction => { const calcDirection = (oldX: number, oldY: number, newX: number, newY: number): Direction => {
if (newPositionY < oldPositionY || newPositionX < oldPositionX) return Direction.NEGATIVE if (newY < oldY || newX < oldX) return Direction.NEGATIVE
if (newPositionX > oldPositionX || newPositionY > oldPositionY) return Direction.POSITIVE if (newX > oldX || newY > oldY) return Direction.POSITIVE
return Direction.UNCHANGED return Direction.UNCHANGED
} }
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0)) const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
const charTexture = computed(() => { const charTexture = computed(() => {
const spriteId = charSpriteId.value ?? 'idle_right_down' const { rotation, characterType } = props.zoneCharacter.character
const action = props.mapCharacter.isMoving ? 'walk' : 'idle' const spriteId = characterType?.sprite ?? 'idle_right_down'
const direction = [0, 6].includes(props.mapCharacter.character.rotation) ? 'left_up' : 'right_down' const action = props.zoneCharacter.isMoving ? 'walk' : 'idle'
const direction = [0, 6].includes(rotation) ? 'left_up' : 'right_down'
return `${spriteId}-${action}_${direction}` return `${spriteId}-${action}_${direction}`
}) })
const updateSprite = () => { const updateSprite = () => {
if (props.mapCharacter.isMoving) { if (props.zoneCharacter.isMoving) {
charSprite.value!.anims.play(charTexture.value, true) charSprite.value!.anims.play(charTexture.value, true)
} else { return
}
charSprite.value!.anims.stop() charSprite.value!.anims.stop()
charSprite.value!.setFrame(0) charSprite.value!.setFrame(0)
charSprite.value!.setTexture(charTexture.value) charSprite.value!.setTexture(charTexture.value)
}
} }
watch( watch(
() => ({ () => ({
positionX: props.mapCharacter.character.positionX, x: props.zoneCharacter.character.positionX,
positionY: props.mapCharacter.character.positionY, y: props.zoneCharacter.character.positionY,
isMoving: props.mapCharacter.isMoving, isMoving: props.zoneCharacter.isMoving,
rotation: props.mapCharacter.character.rotation rotation: props.zoneCharacter.character.rotation
}), }),
(newValues, oldValues) => { (newValues, oldValues) => {
if (!newValues) return if (!newValues) return
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) { if (!oldValues || newValues.x !== oldValues.x || newValues.y !== oldValues.y) {
const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.positionX, oldValues.positionY, newValues.positionX, newValues.positionY) const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.x, oldValues.y, newValues.x, newValues.y)
updatePosition(newValues.positionX, newValues.positionY, direction) updatePosition(newValues.x, newValues.y, direction)
} }
// Handle animation updates // Handle animation updates
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) { if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
updateSprite() updateSprite()
} }
} },
{ deep: true }
) )
onMounted(async () => { watch(() => props.zoneCharacter, updateSprite)
const characterTypeStorage = new CharacterTypeStorage()
const spriteId = await characterTypeStorage.getSpriteId(props.mapCharacter.character.characterType!)
if (!spriteId) return
charSpriteId.value = spriteId
await loadSpriteTextures(scene, spriteId)
loadSpriteTextures(scene, props.zoneCharacter.character.characterType?.sprite as string)
.then(() => {
charSprite.value!.setTexture(charTexture.value) charSprite.value!.setTexture(charTexture.value)
charSprite.value!.setFlipX(isFlippedX.value) charSprite.value!.setFlipX(isFlippedX.value)
})
.catch((error) => {
console.error('Error loading texture:', error)
})
charContainer.value!.setName(props.mapCharacter.character!.name) onMounted(() => {
charContainer.value!.setName(props.zoneCharacter.character!.name)
if (props.mapCharacter.character.id === gameStore.character!.id) { if (props.zoneCharacter.character.id === gameStore.character!.id) {
mapStore.setCharacterLoaded(true) zoneStore.setCharacterLoaded(true)
// #146 : Set camera position to character, need to be improved still // #146 : Set camera position to character, need to be improved still
scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container) // scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
// scene.cameras.main.stopFollow()
} }
updatePosition(props.mapCharacter.character.positionX, props.mapCharacter.character.positionY, props.mapCharacter.character.rotation) updatePosition(props.zoneCharacter.character.positionX, props.zoneCharacter.character.positionY, props.zoneCharacter.character.rotation)
}) })
onUnmounted(() => { onUnmounted(() => {
tween.value?.stop() stopMovement()
}) })
</script> </script>

View File

@ -3,14 +3,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { MapCharacter, Sprite as SpriteT } from '@/application/types' import type { Sprite as SpriteT, ZoneCharacter } from '@/application/types'
import { loadSpriteTextures } from '@/composables/gameComposable' import { loadSpriteTextures } from '@/composables/gameComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { computed } from 'vue' import { computed } from 'vue'
const props = defineProps<{ const props = defineProps<{
mapCharacter: MapCharacter zoneCharacter: ZoneCharacter
currentX: number currentX: number
currentY: number currentY: number
}>() }>()
@ -19,19 +19,19 @@ const gameStore = useGameStore()
const scene = useScene() const scene = useScene()
const texture = computed(() => { const texture = computed(() => {
const { rotation, characterHair } = props.mapCharacter.character const { rotation, characterHair } = props.zoneCharacter.character
const spriteId = characterHair?.sprite?.id const spriteId = characterHair?.sprite?.id
const direction = [0, 6].includes(rotation) ? 'back' : 'front' const direction = [0, 6].includes(rotation) ? 'back' : 'front'
return `${spriteId}-${direction}` return `${spriteId}-${direction}`
}) })
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0)) const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
const imageProps = computed(() => { const imageProps = computed(() => {
// Get the current sprite action based on direction // Get the current sprite action based on direction
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front' const direction = [0, 6].includes(props.zoneCharacter.character.rotation ?? 0) ? 'back' : 'front'
const spriteAction = props.mapCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction) const spriteAction = props.zoneCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
return { return {
depth: 1, depth: 1,
@ -39,11 +39,11 @@ const imageProps = computed(() => {
originY: Number(spriteAction?.originY) ?? 0, originY: Number(spriteAction?.originY) ?? 0,
flipX: isFlippedX.value, flipX: isFlippedX.value,
texture: texture.value texture: texture.value
// y: props.mapCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0 // y: props.zoneCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
} }
}) })
loadSpriteTextures(scene, props.mapCharacter.character.characterHair?.sprite as SpriteT) loadSpriteTextures(scene, props.zoneCharacter.character.characterHair?.sprite as SpriteT)
.then(() => {}) .then(() => {})
.catch((error) => { .catch((error) => {
console.error('Error loading texture:', error) console.error('Error loading texture:', error)

View File

@ -3,14 +3,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { MapCharacter, Sprite as SpriteT } from '@/application/types' import type { Sprite as SpriteT, ZoneCharacter } from '@/application/types'
import { loadSpriteTextures } from '@/composables/gameComposable' import { loadSpriteTextures } from '@/composables/gameComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { computed } from 'vue' import { computed } from 'vue'
const props = defineProps<{ const props = defineProps<{
mapCharacter: MapCharacter zoneCharacter: ZoneCharacter
currentX: number currentX: number
currentY: number currentY: number
}>() }>()
@ -19,19 +19,19 @@ const gameStore = useGameStore()
const scene = useScene() const scene = useScene()
const texture = computed(() => { const texture = computed(() => {
const { rotation, characterHair } = props.mapCharacter.character const { rotation, characterHair } = props.zoneCharacter.character
const spriteId = characterHair?.sprite?.id const spriteId = characterHair?.sprite?.id
const direction = [0, 6].includes(rotation) ? 'back' : 'front' const direction = [0, 6].includes(rotation) ? 'back' : 'front'
return `${spriteId}-${direction}` return `${spriteId}-${direction}`
}) })
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0)) const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
const imageProps = computed(() => { const imageProps = computed(() => {
// Get the current sprite action based on direction // Get the current sprite action based on direction
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front' const direction = [0, 6].includes(props.zoneCharacter.character.rotation ?? 0) ? 'back' : 'front'
const spriteAction = props.mapCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction) const spriteAction = props.zoneCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
return { return {
depth: 1, depth: 1,
@ -39,11 +39,11 @@ const imageProps = computed(() => {
originY: Number(spriteAction?.originY) ?? 0, originY: Number(spriteAction?.originY) ?? 0,
flipX: isFlippedX.value, flipX: isFlippedX.value,
texture: texture.value, texture: texture.value,
y: props.mapCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0 y: props.zoneCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
} }
}) })
loadSpriteTextures(scene, props.mapCharacter.character.characterHair?.sprite as SpriteT) loadSpriteTextures(scene, props.zoneCharacter.character.characterHair?.sprite as SpriteT)
.then(() => {}) .then(() => {})
.catch((error) => { .catch((error) => {
console.error('Error loading texture:', error) console.error('Error loading texture:', error)

View File

@ -6,12 +6,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { MapCharacter } from '@/application/types' import type { ZoneCharacter } from '@/application/types'
import { Container, refObj, RoundRectangle, Text, useGame } from 'phavuer' import { Container, refObj, RoundRectangle, Text, useGame } from 'phavuer'
import { onMounted } from 'vue' import { onMounted } from 'vue'
const props = defineProps<{ const props = defineProps<{
mapCharacter: MapCharacter zoneCharacter: ZoneCharacter
currentX: number currentX: number
currentY: number currentY: number
}>() }>()
@ -20,15 +20,14 @@ const game = useGame()
const charChatContainer = refObj<Phaser.GameObjects.Container>() const charChatContainer = refObj<Phaser.GameObjects.Container>()
const createChatBubble = (container: Phaser.GameObjects.Container) => { const createChatBubble = (container: Phaser.GameObjects.Container) => {
container.setName(`${props.mapCharacter.character.name}_chatBubble`) container.setName(`${props.zoneCharacter.character.name}_chatBubble`)
} }
const createChatText = (text: Phaser.GameObjects.Text) => { const createChatText = (text: Phaser.GameObjects.Text) => {
text.setName(`${props.mapCharacter.character.name}_chatText`) text.setName(`${props.zoneCharacter.character.name}_chatText`)
text.setFontSize(13) text.setFontSize(13)
text.setFontFamily('Arial') text.setFontFamily('Arial')
text.setOrigin(0.5, 10.9) text.setOrigin(0.5, 10.9)
text.setResolution(2)
// Fix text alignment on Windows and Android // Fix text alignment on Windows and Android
if (game.device.os.windows || game.device.os.android) { if (game.device.os.windows || game.device.os.android) {
@ -41,7 +40,7 @@ const createChatText = (text: Phaser.GameObjects.Text) => {
} }
onMounted(() => { onMounted(() => {
charChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`) charChatContainer.value!.setName(`${props.zoneCharacter.character!.name}_chatContainer`)
charChatContainer.value!.setVisible(false) charChatContainer.value!.setVisible(false)
}) })
</script> </script>

View File

@ -1,17 +1,17 @@
<template> <template>
<Container :depth="999" :x="currentX" :y="currentY"> <Container :depth="999" :x="currentX" :y="currentY">
<Text @create="createNicknameText" :text="props.mapCharacter.character.name" /> <Text @create="createNicknameText" :text="props.zoneCharacter.character.name" />
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" /> <RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" /> <RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
</Container> </Container>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { MapCharacter } from '@/application/types' import type { ZoneCharacter } from '@/application/types'
import { Container, RoundRectangle, Text, useGame } from 'phavuer' import { Container, RoundRectangle, Text, useGame } from 'phavuer'
const props = defineProps<{ const props = defineProps<{
mapCharacter: MapCharacter zoneCharacter: ZoneCharacter
currentX: number currentX: number
currentY: number currentY: number
}>() }>()
@ -22,7 +22,6 @@ const createNicknameText = (text: Phaser.GameObjects.Text) => {
text.setFontSize(13) text.setFontSize(13)
text.setFontFamily('Arial') text.setFontFamily('Arial')
text.setOrigin(0.5, 9) text.setOrigin(0.5, 9)
text.setResolution(2)
// Fix text alignment on Windows and Android // Fix text alignment on Windows and Android
if (game.device.os.windows || game.device.os.android) { if (game.device.os.windows || game.device.os.android) {

View File

@ -1,14 +0,0 @@
<template>
<Character v-for="item in mapStore.characters" :key="item.character.id" :tilemap="tilemap" :mapCharacter="item" />
</template>
<script setup lang="ts">
import Character from '@/components/game/character/Character.vue'
import { useMapStore } from '@/stores/mapStore'
const mapStore = useMapStore()
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
</script>

View File

@ -1,46 +0,0 @@
<template>
<MapTiles :key="mapStore.mapId" @tileMap:create="tileMap = $event" />
<PlacedMapObjects v-if="tileMap" :key="mapStore.mapId" :tilemap="tileMap" />
<Characters v-if="tileMap && mapStore.characters" :tilemap="tileMap" />
</template>
<script setup lang="ts">
import type { MapCharacter, mapLoadData, UUID } from '@/application/types'
import Characters from '@/components/game/map/Characters.vue'
import MapTiles from '@/components/game/map/MapTiles.vue'
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore'
import { onUnmounted, shallowRef } from 'vue'
const gameStore = useGameStore()
const mapStore = useMapStore()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
// Event listeners
gameStore.connection?.on('map:character:teleport', async (data: mapLoadData) => {
mapStore.setMapId(data.mapId)
mapStore.setCharacters(data.characters)
})
gameStore.connection?.on('map:character:join', async (data: MapCharacter) => {
mapStore.addCharacter(data)
})
gameStore.connection?.on('map:character:leave', (characterId: UUID) => {
mapStore.removeCharacter(characterId)
})
gameStore.connection?.on('map:character:move', (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
mapStore.updateCharacterPosition(data)
})
onUnmounted(() => {
mapStore.reset()
gameStore.connection?.off('map:character:teleport')
gameStore.connection?.off('map:character:join')
gameStore.connection?.off('map:character:leave')
gameStore.connection?.off('map:character:move')
})
</script>

View File

@ -1,73 +0,0 @@
<template>
<Controls v-if="tileLayer" :layer="tileLayer" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/application/config'
import type { UUID } from '@/application/types'
import { unduplicateArray } from '@/application/utilities'
import Controls from '@/components/utilities/Controls.vue'
import { FlattenMapArray, loadMapTilesIntoScene, setLayerTiles } from '@/composables/mapComposable'
import { MapStorage } from '@/storage/storages'
import { useMapStore } from '@/stores/mapStore'
import { useScene } from 'phavuer'
import { onBeforeUnmount, shallowRef } from 'vue'
import Tileset = Phaser.Tilemaps.Tileset
const emit = defineEmits(['tileMap:create'])
const scene = useScene()
const mapStore = useMapStore()
const mapStorage = new MapStorage()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
function createTileMap(mapData: any) {
const mapConfig = new Phaser.Tilemaps.MapData({
width: mapData?.width,
height: mapData?.height,
tileWidth: config.tile_size.width,
tileHeight: config.tile_size.height,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapConfig)
emit('tileMap:create', newTileMap)
return newTileMap
}
function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap, mapData: any) {
const tilesArray = unduplicateArray(FlattenMapArray(mapData?.tiles ?? []))
const tilesetImages = tilesArray.map((tile: string, index: number) => {
return currentTileMap.addTilesetImage(tile, tile, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height })
})
// Add blank tile
tilesetImages.push(currentTileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height }))
const layer = currentTileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
loadMapTilesIntoScene(mapStore.mapId as UUID, scene)
.then(() => mapStorage.get(mapStore.mapId))
.then((mapData) => {
tileMap.value = createTileMap(mapData)
tileLayer.value = createTileLayer(tileMap.value, mapData)
setLayerTiles(tileMap.value, tileLayer.value, mapData?.tiles)
})
.catch((error) => console.error('Failed to initialize map:', error))
onBeforeUnmount(() => {
if (!tileMap.value) return
tileMap.value.destroyLayer('tiles')
tileMap.value.removeAllLayers()
tileMap.value.destroy()
})
</script>

View File

@ -1,28 +0,0 @@
<template>
<PlacedMapObject v-for="placedMapObject in items" :tilemap="tilemap" :placedMapObject />
</template>
<script setup lang="ts">
import type { PlacedMapObject as PlacedMapObjectT } from '@/application/types'
import PlacedMapObject from '@/components/game/map/partials/PlacedMapObject.vue'
import { MapStorage } from '@/storage/storages'
import { useMapStore } from '@/stores/mapStore'
import { onMounted, ref } from 'vue'
defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
const mapStore = useMapStore()
const mapStorage = new MapStorage()
const items = ref<PlacedMapObjectT[]>([])
onMounted(async () => {
if (!mapStore.mapId) return
const map = await mapStorage.get(mapStore.mapId)
if (!map) return
items.value = map.placedMapObjects
})
</script>

View File

@ -1,43 +0,0 @@
<template>
<Image v-if="gameStore.isTextureLoaded(props.placedMapObject.mapObject.id)" v-bind="imageProps" />
</template>
<script setup lang="ts">
import type { PlacedMapObject, TextureData } from '@/application/types'
import { loadTexture } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer'
import { computed, onMounted } from 'vue'
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
placedMapObject: PlacedMapObject
}>()
const gameStore = useGameStore()
const scene = useScene()
const imageProps = computed(() => ({
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, props.placedMapObject.mapObject.frameWidth, props.placedMapObject.mapObject.frameHeight),
x: tileToWorldX(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
y: tileToWorldY(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
flipX: props.placedMapObject.isRotated,
texture: props.placedMapObject.mapObject.id,
originY: Number(props.placedMapObject.mapObject.originX),
originX: Number(props.placedMapObject.mapObject.originY)
}))
loadTexture(scene, {
key: props.placedMapObject.mapObject.id,
data: '/textures/map_objects/' + props.placedMapObject.mapObject.id + '.png',
group: 'map_objects',
updatedAt: props.placedMapObject.mapObject.updatedAt,
frameWidth: props.placedMapObject.mapObject.frameWidth,
frameHeight: props.placedMapObject.mapObject.frameHeight
} as TextureData).catch((error) => {
console.error('Error loading texture:', error)
})
onMounted(async () => {})
</script>

View File

@ -0,0 +1,14 @@
<template>
<Character v-for="item in zoneStore.characters" :key="item.character.id" :tilemap="tilemap" :zoneCharacter="item" />
</template>
<script setup lang="ts">
import Character from '@/components/game/character/Character.vue'
import { useZoneStore } from '@/stores/zoneStore'
const zoneStore = useZoneStore()
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
</script>

View File

@ -0,0 +1,50 @@
<template>
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tileMap:create="tileMap = $event" />
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
</template>
<script setup lang="ts">
import type { ZoneCharacter, zoneLoadData } from '@/application/types'
import Characters from '@/components/game/zone/Characters.vue'
import ZoneObjects from '@/components/game/zone/ZoneObjects.vue'
import ZoneTiles from '@/components/game/zone/ZoneTiles.vue'
import { loadZoneTilesIntoScene } from '@/composables/zoneComposable'
import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore'
import { useScene } from 'phavuer'
import { onUnmounted, ref } from 'vue'
const scene = useScene()
const gameStore = useGameStore()
const zoneStore = useZoneStore()
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
onUnmounted(() => {
zoneStore.reset()
gameStore.connection!.off('zone:character:teleport')
gameStore.connection!.off('zone:character:join')
gameStore.connection!.off('zone:character:leave')
gameStore.connection!.off('zone:character:move')
})
// Event listeners
gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => {
await loadZoneTilesIntoScene(data.zone.id, scene)
zoneStore.setZone(data.zone)
zoneStore.setCharacters(data.characters)
})
gameStore.connection!.on('zone:character:join', async (data: ZoneCharacter) => {
zoneStore.addCharacter(data)
})
gameStore.connection!.on('zone:character:leave', (characterId: number) => {
zoneStore.removeCharacter(characterId)
})
gameStore.connection!.on('zone:character:move', (data: { characterId: number; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
zoneStore.updateCharacterPosition(data)
})
</script>

View File

@ -0,0 +1,14 @@
<template>
<ZoneObject v-for="zoneObject in zoneStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject />
</template>
<script setup lang="ts">
import ZoneObject from '@/components/game/zone/partials/ZoneObject.vue'
import { useZoneStore } from '@/stores/zoneStore'
const zoneStore = useZoneStore()
defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
</script>

View File

@ -0,0 +1,69 @@
<template>
<Controls :layer="tileLayer" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/application/config'
import { unduplicateArray } from '@/application/utilities'
import Controls from '@/components/utilities/Controls.vue'
import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
import { useZoneStore } from '@/stores/zoneStore'
import { useScene } from 'phavuer'
import { onBeforeUnmount } from 'vue'
const emit = defineEmits(['tileMap:create'])
const scene = useScene()
const zoneStore = useZoneStore()
const tileMap = createTileMap()
const tileLayer = createTileLayer()
/**
* A Tilemap is a container for Tilemap data.
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
*/
function createTileMap() {
const zoneData = new Phaser.Tilemaps.MapData({
width: zoneStore.zone?.width,
height: zoneStore.zone?.height,
tileWidth: config.tile_size.x,
tileHeight: config.tile_size.y,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
emit('tileMap:create', newTileMap)
return newTileMap
}
/**
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
*/
function createTileLayer() {
const tilesArray = unduplicateArray(FlattenZoneArray(zoneStore.zone?.tiles ?? []))
const tilesetImages = Array.from(tilesArray).map((tile: any, index: number) => {
return tileMap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
}) as any
// Add blank tile
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
setLayerTiles(tileMap, tileLayer, zoneStore.zone?.tiles)
onBeforeUnmount(() => {
tileMap.destroyLayer('tiles')
tileMap.removeAllLayers()
tileMap.destroy()
})
</script>

View File

@ -0,0 +1,41 @@
<template>
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
</template>
<script setup lang="ts">
import type { AssetDataT, ZoneObject } from '@/application/types'
import { loadTexture } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer'
import { computed } from 'vue'
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
zoneObject: ZoneObject
}>()
const gameStore = useGameStore()
const scene = useScene()
const imageProps = computed(() => ({
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
flipX: props.zoneObject.isRotated,
texture: props.zoneObject.object.id,
originY: Number(props.zoneObject.object.originX),
originX: Number(props.zoneObject.object.originY)
}))
loadTexture(scene, {
key: props.zoneObject.object.id,
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
group: 'objects',
updatedAt: props.zoneObject.object.updatedAt,
frameWidth: props.zoneObject.object.frameWidth,
frameHeight: props.zoneObject.object.frameHeight
} as AssetDataT).catch((error) => {
console.error('Error loading texture:', error)
})
</script>

View File

@ -6,7 +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="() => mapEditorStore.toggleActive()">Map editor</button> <button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => zoneEditorStore.toggleActive()">Map editor</button>
</div> </div>
</template> </template>
<template #modalBody> <template #modalBody>
@ -21,11 +21,11 @@
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue' import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { ref } from 'vue' import { ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore() const zoneEditorStore = useZoneEditorStore()
let toggle = ref('asset-manager') let toggle = ref('asset-manager')
</script> </script>

View File

@ -5,8 +5,8 @@
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span> <span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'map_objects' }" @click="() => (selectedCategory = 'map_objects')"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'map_objects' }">Map objects</span> <span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span>
</a> </a>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')"> <a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span> <span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
@ -40,7 +40,7 @@
<!-- Assets list --> <!-- Assets list -->
<div class="overflow-auto h-full w-4/12 flex flex-col relative"> <div class="overflow-auto h-full w-4/12 flex flex-col relative">
<TileList v-if="selectedCategory === 'tiles'" /> <TileList v-if="selectedCategory === 'tiles'" />
<MapObjectList v-if="selectedCategory === 'map_objects'" /> <ObjectList v-if="selectedCategory === 'objects'" />
<SpriteList v-if="selectedCategory === 'sprites'" /> <SpriteList v-if="selectedCategory === 'sprites'" />
<ItemList v-if="selectedCategory === 'items'" /> <ItemList v-if="selectedCategory === 'items'" />
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" /> <CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
@ -50,7 +50,7 @@
<!-- Asset details --> <!-- Asset details -->
<div class="flex w-7/12 after:hidden flex-col relative overflow-auto"> <div class="flex w-7/12 after:hidden flex-col relative overflow-auto">
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" /> <TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
<MapObjectDetails v-if="selectedCategory === 'map_objects' && assetManagerStore.selectedMapObject" /> <ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" /> <SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
<ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" /> <ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" />
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" /> <CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
@ -66,8 +66,8 @@ import CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue' import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
import ItemDetails from '@/components/gameMaster/assetManager/partials/item/itemDetails.vue' import ItemDetails from '@/components/gameMaster/assetManager/partials/item/itemDetails.vue'
import ItemList from '@/components/gameMaster/assetManager/partials/item/itemList.vue' import ItemList from '@/components/gameMaster/assetManager/partials/item/itemList.vue'
import MapObjectDetails from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectDetails.vue' import ObjectDetails from '@/components/gameMaster/assetManager/partials/object/ObjectDetails.vue'
import MapObjectList from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectList.vue' import ObjectList from '@/components/gameMaster/assetManager/partials/object/ObjectList.vue'
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue' import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue' import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue' import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue'

View File

@ -59,7 +59,7 @@ if (selectedCharacterHair.value) {
characterName.value = selectedCharacterHair.value.name characterName.value = selectedCharacterHair.value.name
characterGender.value = selectedCharacterHair.value.gender characterGender.value = selectedCharacterHair.value.gender
characterIsSelectable.value = selectedCharacterHair.value.isSelectable characterIsSelectable.value = selectedCharacterHair.value.isSelectable
characterSpriteId.value = selectedCharacterHair.value.sprite?.id characterSpriteId.value = selectedCharacterHair.value.spriteId
} }
function removeCharacterHair() { function removeCharacterHair() {
@ -107,7 +107,7 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
characterName.value = characterHair.name characterName.value = characterHair.name
characterGender.value = characterHair.gender characterGender.value = characterHair.gender
characterIsSelectable.value = characterHair.isSelectable characterIsSelectable.value = characterHair.isSelectable
characterSpriteId.value = characterHair.sprite?.id characterSpriteId.value = characterHair.spriteId
}) })
onMounted(() => { onMounted(() => {

View File

@ -9,8 +9,8 @@
</button> </button>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5"> <div v-bind="wrapperProps" ref="elementToScroll">
<a <a
v-for="{ data: characterHair } in list" v-for="{ data: characterHair } in list"
:key="characterHair.id" :key="characterHair.id"
@ -25,7 +25,7 @@
</div> </div>
<div class="absolute w-12 h-12 bottom-2.5 right-2.5"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" /> <img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -68,7 +68,7 @@ if (selectedCharacterType.value) {
characterGender.value = selectedCharacterType.value.gender characterGender.value = selectedCharacterType.value.gender
characterRace.value = selectedCharacterType.value.race characterRace.value = selectedCharacterType.value.race
characterIsSelectable.value = selectedCharacterType.value.isSelectable characterIsSelectable.value = selectedCharacterType.value.isSelectable
characterSpriteId.value = selectedCharacterType.value.sprite?.id characterSpriteId.value = selectedCharacterType.value.spriteId
} }
function removeCharacterType() { function removeCharacterType() {
@ -118,7 +118,7 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
characterGender.value = characterType.gender characterGender.value = characterType.gender
characterRace.value = characterType.race characterRace.value = characterType.race
characterIsSelectable.value = characterType.isSelectable characterIsSelectable.value = characterType.isSelectable
characterSpriteId.value = characterType.sprite?.id characterSpriteId.value = characterType.spriteId
}) })
onMounted(() => { onMounted(() => {

View File

@ -9,8 +9,8 @@
</button> </button>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5"> <div v-bind="wrapperProps" ref="elementToScroll">
<a <a
v-for="{ data: characterType } in list" v-for="{ data: characterType } in list"
:key="characterType.id" :key="characterType.id"
@ -25,7 +25,7 @@
</div> </div>
<div class="absolute w-12 h-12 bottom-2.5 right-2.5"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" /> <img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -44,7 +44,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types' import type { Item, ItemRarity, ItemType } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'

View File

@ -9,8 +9,8 @@
</button> </button>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5"> <div v-bind="wrapperProps" ref="elementToScroll">
<a v-for="{ data: item } in list" :key="item.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedItem?.id === item.id }" @click="assetManagerStore.setSelectedItem(item as Item)"> <a v-for="{ data: item } in list" :key="item.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedItem?.id === item.id }" @click="assetManagerStore.setSelectedItem(item as Item)">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedItem?.id === item.id }"> <span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedItem?.id === item.id }">
@ -22,7 +22,7 @@
</div> </div>
<div class="absolute w-12 h-12 bottom-2.5 right-2.5"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" /> <img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,163 +0,0 @@
<template>
<div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" />
</div>
<div class="mt-5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
<div class="form-field-full">
<label for="name">Name</label>
<input v-model="mapObjectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
</div>
<div class="form-field-half">
<label for="origin-x">Origin X</label>
<input v-model="mapObjectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
</div>
<div class="form-field-half">
<label for="origin-y">Origin Y</label>
<input v-model="mapObjectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
</div>
<div class="form-field-full">
<label for="tags">Tags</label>
<ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" />
</div>
<div class="form-field-full">
<label for="is-animated">Is animated</label>
<select v-model="mapObjectIsAnimated" class="input-field" name="is-animated">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full">
<label for="frame-speed">Frame rate</label>
<input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
</div>
<div class="form-field-half">
<label for="frame-width">Frame width</label>
<input v-model="mapObjectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
</div>
<div class="form-field-half">
<label for="frame-height">Frame height</label>
<input v-model="mapObjectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
</div>
<div class="flex gap-4">
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Delete</button>
</div>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import config from '@/application/config'
import type { MapObject } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const mapEditorStore = useMapEditorStore()
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
const mapObjectName = ref('')
const mapObjectTags = ref<string[]>([])
const mapObjectOriginX = ref(0)
const mapObjectOriginY = ref(0)
const mapObjectIsAnimated = ref(false)
const mapObjectFrameRate = ref(0)
const mapObjectFrameWidth = ref(0)
const mapObjectFrameHeight = ref(0)
if (!selectedMapObject.value) {
console.error('No map mapObject selected')
}
if (selectedMapObject.value) {
mapObjectName.value = selectedMapObject.value.name
mapObjectTags.value = selectedMapObject.value.tags
mapObjectOriginX.value = selectedMapObject.value.originX
mapObjectOriginY.value = selectedMapObject.value.originY
mapObjectIsAnimated.value = selectedMapObject.value.isAnimated
mapObjectFrameRate.value = selectedMapObject.value.frameRate
mapObjectFrameWidth.value = selectedMapObject.value.frameWidth
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
}
function removeObject() {
gameStore.connection?.emit('gm:mapObject:remove', { mapObject: selectedMapObject.value?.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove mapObject')
return
}
refreshObjectList()
})
}
function refreshObjectList(unsetSelectedMapObject = true) {
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
assetManagerStore.setMapObjectList(response)
if (unsetSelectedMapObject) {
assetManagerStore.setSelectedMapObject(null)
}
if (mapEditorStore.active) {
mapEditorStore.setMapObjectList(response)
}
})
}
function saveObject() {
if (!selectedMapObject.value) {
console.error('No mapObject selected')
return
}
gameStore.connection?.emit(
'gm:mapObject:update',
{
id: selectedMapObject.value.id,
name: mapObjectName.value,
tags: mapObjectTags.value,
originX: mapObjectOriginX.value,
originY: mapObjectOriginY.value,
isAnimated: mapObjectIsAnimated.value,
frameRate: mapObjectFrameRate.value,
frameWidth: mapObjectFrameWidth.value,
frameHeight: mapObjectFrameHeight.value
},
(response: boolean) => {
if (!response) {
console.error('Failed to save mapObject')
return
}
refreshObjectList(false)
}
)
}
watch(selectedMapObject, (mapObject: MapObject | null) => {
if (!mapObject) return
mapObjectName.value = mapObject.name
mapObjectTags.value = mapObject.tags
mapObjectOriginX.value = mapObject.originX
mapObjectOriginY.value = mapObject.originY
mapObjectIsAnimated.value = mapObject.isAnimated
mapObjectFrameRate.value = mapObject.frameRate
mapObjectFrameWidth.value = mapObject.frameWidth
mapObjectFrameHeight.value = mapObject.frameHeight
})
onMounted(() => {
if (!selectedMapObject.value) return
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedMapObject(null)
})
</script>

View File

@ -0,0 +1,163 @@
<template>
<div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
</div>
<div class="mt-5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
<div class="form-field-full">
<label for="name">Name</label>
<input v-model="objectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
</div>
<div class="form-field-half">
<label for="origin-x">Origin X</label>
<input v-model="objectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
</div>
<div class="form-field-half">
<label for="origin-y">Origin Y</label>
<input v-model="objectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
</div>
<div class="form-field-full">
<label for="tags">Tags</label>
<ChipsInput v-model="objectTags" @update:modelValue="objectTags = $event" />
</div>
<div class="form-field-full">
<label for="is-animated">Is animated</label>
<select v-model="objectIsAnimated" class="input-field" name="is-animated">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full">
<label for="frame-speed">Frame rate</label>
<input v-model="objectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
</div>
<div class="form-field-half">
<label for="frame-width">Frame width</label>
<input v-model="objectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
</div>
<div class="form-field-half">
<label for="frame-height">Frame height</label>
<input v-model="objectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
</div>
<div class="flex gap-4">
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Delete</button>
</div>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import config from '@/application/config'
import type { Object } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const zoneEditorStore = useZoneEditorStore()
const selectedObject = computed(() => assetManagerStore.selectedObject)
const objectName = ref('')
const objectTags = ref<string[]>([])
const objectOriginX = ref(0)
const objectOriginY = ref(0)
const objectIsAnimated = ref(false)
const objectFrameRate = ref(0)
const objectFrameWidth = ref(0)
const objectFrameHeight = ref(0)
if (!selectedObject.value) {
console.error('No object selected')
}
if (selectedObject.value) {
objectName.value = selectedObject.value.name
objectTags.value = selectedObject.value.tags
objectOriginX.value = selectedObject.value.originX
objectOriginY.value = selectedObject.value.originY
objectIsAnimated.value = selectedObject.value.isAnimated
objectFrameRate.value = selectedObject.value.frameRate
objectFrameWidth.value = selectedObject.value.frameWidth
objectFrameHeight.value = selectedObject.value.frameHeight
}
function removeObject() {
gameStore.connection?.emit('gm:object:remove', { object: selectedObject.value?.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove object')
return
}
refreshObjectList()
})
}
function refreshObjectList(unsetSelectedObject = true) {
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
assetManagerStore.setObjectList(response)
if (unsetSelectedObject) {
assetManagerStore.setSelectedObject(null)
}
if (zoneEditorStore.active) {
zoneEditorStore.setObjectList(response)
}
})
}
function saveObject() {
if (!selectedObject.value) {
console.error('No object selected')
return
}
gameStore.connection?.emit(
'gm:object:update',
{
id: selectedObject.value.id,
name: objectName.value,
tags: objectTags.value,
originX: objectOriginX.value,
originY: objectOriginY.value,
isAnimated: objectIsAnimated.value,
frameRate: objectFrameRate.value,
frameWidth: objectFrameWidth.value,
frameHeight: objectFrameHeight.value
},
(response: boolean) => {
if (!response) {
console.error('Failed to save object')
return
}
refreshObjectList(false)
}
)
}
watch(selectedObject, (object: Object | null) => {
if (!object) return
objectName.value = object.name
objectTags.value = object.tags
objectOriginX.value = object.originX
objectOriginY.value = object.originY
objectIsAnimated.value = object.isAnimated
objectFrameRate.value = object.frameRate
objectFrameWidth.value = object.frameWidth
objectFrameHeight.value = object.frameHeight
})
onMounted(() => {
if (!selectedObject.value) return
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedObject(null)
})
</script>

View File

@ -8,20 +8,20 @@
</svg> </svg>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5"> <div v-bind="wrapperProps" ref="elementToScroll">
<a v-for="{ data: mapObject } in list" :key="mapObject.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedMapObject?.id === mapObject.id }" @click="assetManagerStore.setSelectedMapObject(mapObject as MapObject)"> <a v-for="{ data: object } in list" :key="object.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedObject?.id === object.id }" @click="assetManagerStore.setSelectedObject(object as Object)">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<div class="h-7 w-16 max-w-16 flex justify-center"> <div class="h-7 w-16 max-w-16 flex justify-center">
<img class="h-7" :src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`" alt="Object" /> <img class="h-7" :src="`${config.server_endpoint}/assets/objects/${object.id}.png`" alt="Object" />
</div> </div>
<span :class="{ 'text-white': assetManagerStore.selectedMapObject?.id === mapObject.id }">{{ mapObject.name }}</span> <span :class="{ 'text-white': assetManagerStore.selectedObject?.id === object.id }">{{ object.name }}</span>
</div> </div>
</a> </a>
</div> </div>
<div class="absolute w-12 h-12 bottom-2.5 right-2.5"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" /> <img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>
@ -29,7 +29,7 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import type { MapObject } from '@/application/types' import type { Object } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core' import { useVirtualList } from '@vueuse/core'
@ -47,14 +47,14 @@ const elementToScroll = ref()
const handleFileUpload = (e: Event) => { const handleFileUpload = (e: Event) => {
const files = (e.target as HTMLInputElement).files const files = (e.target as HTMLInputElement).files
if (!files) return if (!files) return
gameStore.connection?.emit('gm:mapObject:upload', files, (response: boolean) => { gameStore.connection?.emit('gm:object:upload', files, (response: boolean) => {
if (!response) { if (!response) {
if (config.development) console.error('Failed to upload object') if (config.development) console.error('Failed to upload object')
return return
} }
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => { gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
assetManagerStore.setMapObjectList(response) assetManagerStore.setObjectList(response)
}) })
}) })
} }
@ -66,9 +66,9 @@ const handleSearch = () => {
const filteredObjects = computed(() => { const filteredObjects = computed(() => {
if (!searchQuery.value) { if (!searchQuery.value) {
return assetManagerStore.mapObjectList return assetManagerStore.objectList
} }
return assetManagerStore.mapObjectList.filter((object) => object.name.toLowerCase().includes(searchQuery.value.toLowerCase())) return assetManagerStore.objectList.filter((object) => object.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
}) })
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredObjects, { const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredObjects, {
@ -92,8 +92,8 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => { gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
assetManagerStore.setMapObjectList(response) assetManagerStore.setObjectList(response)
}) })
}) })
</script> </script>

View File

@ -184,7 +184,6 @@ function addNewImage() {
} }
function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] { function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
if (!actions) return []
return [...actions].sort((a, b) => a.action.localeCompare(b.action)) return [...actions].sort((a, b) => a.action.localeCompare(b.action))
} }

View File

@ -7,8 +7,8 @@
</svg> </svg>
</button> </button>
</div> </div>
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5"> <div v-bind="wrapperProps" ref="elementToScroll">
<a v-for="{ data: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)"> <a v-for="{ data: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<span :class="{ 'text-white': assetManagerStore.selectedSprite?.id === sprite.id }">{{ sprite.name }}</span> <span :class="{ 'text-white': assetManagerStore.selectedSprite?.id === sprite.id }">{{ sprite.name }}</span>
@ -17,7 +17,7 @@
</div> </div>
<div class="absolute w-12 h-12 bottom-2.5 right-2.5"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" /> <img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="h-full overflow-auto"> <div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray"> <div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
<img class="max-h-72" :src="`${config.server_endpoint}/textures/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" /> <img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
</div> </div>
<div class="mt-5 block"> <div class="mt-5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile"> <form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
@ -28,12 +28,12 @@ import type { Tile } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue' import ChipsInput from '@/components/forms/ChipsInput.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore() const assetManagerStore = useAssetManagerStore()
const mapEditorStore = useMapEditorStore() const zoneEditorStore = useZoneEditorStore()
const selectedTile = computed(() => assetManagerStore.selectedTile) const selectedTile = computed(() => assetManagerStore.selectedTile)
@ -73,8 +73,8 @@ function refreshTileList(unsetSelectedTile = true) {
assetManagerStore.setSelectedTile(null) assetManagerStore.setSelectedTile(null)
} }
if (mapEditorStore.active) { if (zoneEditorStore.active) {
mapEditorStore.setTileList(response) zoneEditorStore.setTileList(response)
} }
}) })
} }

View File

@ -8,12 +8,12 @@
</svg> </svg>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5"> <div v-bind="wrapperProps" ref="elementToScroll">
<a v-for="{ data: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)"> <a v-for="{ data: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<div class="h-7 w-16 max-w-16 flex justify-center"> <div class="h-7 w-16 max-w-16 flex justify-center">
<img class="h-7" :src="`${config.server_endpoint}/textures/tiles/${tile.id}.png`" alt="Tile" /> <img class="h-7" :src="`${config.server_endpoint}/assets/tiles/${tile.id}.png`" alt="Tile" />
</div> </div>
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedTile?.id === tile.id }">{{ tile.name }}</span> <span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedTile?.id === tile.id }">{{ tile.name }}</span>
</div> </div>
@ -21,7 +21,7 @@
</div> </div>
<div class="absolute w-12 h-12 bottom-2.5 right-2.5"> <div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop"> <button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" /> <img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,72 +0,0 @@
<template>
<MapTiles @tileMap:create="tileMap = $event" />
<PlacedMapObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<MapEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Toolbar @save="save" @clear="clear" />
<MapList />
<TileList />
<ObjectList />
<MapSettings />
<TeleportModal />
</template>
<script setup lang="ts">
import { type Map } from '@/application/types'
import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue'
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
import ObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue'
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onUnmounted, shallowRef } from 'vue'
const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
function clear() {
if (!mapEditorStore.map) return
// Clear objects, event tiles and tiles
mapEditorStore.map.placedMapObjects = []
mapEditorStore.map.mapEventTiles = []
mapEditorStore.triggerClearTiles()
}
function save() {
if (!mapEditorStore.map) return
const data = {
mapId: mapEditorStore.map.id,
name: mapEditorStore.mapSettings.name,
width: mapEditorStore.mapSettings.width,
height: mapEditorStore.mapSettings.height,
tiles: mapEditorStore.map.tiles,
pvp: mapEditorStore.map.pvp,
mapEffects: mapEditorStore.map.mapEffects?.map(({ id, effect, strength }) => ({ id, effect, strength })) ?? [],
mapEventTiles: mapEditorStore.map.mapEventTiles?.map(({ id, type, positionX, positionY, teleport }) => ({ id, type, positionX, positionY, teleport })) ?? [],
placedMapObjects: mapEditorStore.map.placedMapObjects?.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? []
}
if (mapEditorStore.isSettingsModalShown) {
mapEditorStore.toggleSettingsModal()
}
gameStore.connection?.emit('gm:map:update', data, (response: Map) => {
mapEditorStore.setMap(response)
})
}
onUnmounted(() => {
mapEditorStore.reset()
})
</script>

View File

@ -1,231 +0,0 @@
<template>
<Controls v-if="tileLayer" :layer="tileLayer" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/application/config'
import Controls from '@/components/utilities/Controls.vue'
import { createTileArray, getTile, loadAllTilesIntoScene, placeTile, setLayerTiles } from '@/composables/mapComposable'
import { TileStorage } from '@/storage/storages'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { useScene } from 'phavuer'
import { onBeforeMount, onMounted, onUnmounted, shallowRef, watch } from 'vue'
import Tileset = Phaser.Tilemaps.Tileset
const emit = defineEmits(['tileMap:create'])
const scene = useScene()
const mapEditorStore = useMapEditorStore()
const tileStorage = new TileStorage()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
function createTileMap() {
const mapData = new Phaser.Tilemaps.MapData({
width: mapEditorStore.map?.width,
height: mapEditorStore.map?.height,
tileWidth: config.tile_size.width,
tileHeight: config.tile_size.height,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapData)
emit('tileMap:create', newTileMap)
return newTileMap
}
async function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap) {
const tiles = await tileStorage.getAll()
const tilesetImages = []
for (const tile of tiles) {
tilesetImages.push(currentTileMap.addTilesetImage(tile.id, tile.id, config.tile_size.width, config.tile_size.height, 1, 2, tilesetImages.length + 1, { x: 0, y: -config.tile_size.height }))
}
// Add blank tile
tilesetImages.push(currentTileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height }))
const layer = currentTileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
function pencil(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is tile
if (mapEditorStore.drawMode !== 'tile') return
// Check if there is a selected tile
if (!mapEditorStore.selectedTile) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditorStore.selectedTile)
// Adjust mapEditorStore.map.tiles
mapEditorStore.map.tiles[tile.y][tile.x] = mapEditorStore.selectedTile
}
function eraser(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'eraser') return
// Check if draw mode is tile
if (mapEditorStore.eraserMode !== 'tile') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile')
// Adjust mapEditorStore.map.tiles
mapEditorStore.map.tiles[tile.y][tile.x] = 'blank_tile'
}
function paint(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'paint') return
// Check if there is a selected tile
if (!mapEditorStore.selectedTile) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) return
// Set new tileArray with selected tile
setLayerTiles(tileMap.value, tileLayer.value, createTileArray(tileMap.value.width, tileMap.value.height, mapEditorStore.selectedTile))
// Adjust mapEditorStore.map.tiles
mapEditorStore.map.tiles = createTileArray(tileMap.value.width, tileMap.value.height, mapEditorStore.selectedTile)
}
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
function tilePicker(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is tile
if (mapEditorStore.drawMode !== 'tile') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (!pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Select the tile
mapEditorStore.setSelectedTile(mapEditorStore.map.tiles[tile.y][tile.x])
}
watch(
() => mapEditorStore.shouldClearTiles,
(shouldClear) => {
if (shouldClear && mapEditorStore.map && tileMap.value && tileLayer.value) {
const blankTiles = createTileArray(tileMap.value.width, tileMap.value.height, 'blank_tile')
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
mapEditorStore.map.tiles = blankTiles
mapEditorStore.resetClearTilesFlag()
}
}
)
onMounted(async () => {
if (!mapEditorStore.map?.tiles) return
tileMap.value = createTileMap()
tileLayer.value = await createTileLayer(tileMap.value)
// First fill the entire map with blank tiles using current map dimensions
const blankTiles = createTileArray(mapEditorStore.map.width, mapEditorStore.map.height, 'blank_tile')
// Then overlay the map tiles, but only within the current map dimensions
const mapTiles = mapEditorStore.map.tiles
for (let y = 0; y < mapEditorStore.map.height; y++) {
for (let x = 0; x < mapEditorStore.map.width; x++) {
if (mapTiles[y] && mapTiles[y][x] !== undefined) {
blankTiles[y][x] = mapTiles[y][x]
}
}
}
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
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_DOWN, paint)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, tilePicker)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
if (tileMap.value) {
tileMap.value.destroyLayer('tiles')
tileMap.value.removeAllLayers()
tileMap.value.destroy()
}
})
</script>

View File

@ -1,45 +0,0 @@
<template>
<Image v-if="gameStore.isTextureLoaded(props.placedMapObject.mapObject.id)" v-bind="imageProps" />
</template>
<script setup lang="ts">
import type { PlacedMapObject, TextureData } from '@/application/types'
import { loadTexture } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer'
import { computed } from 'vue'
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
placedMapObject: PlacedMapObject
selectedPlacedMapObject: PlacedMapObject | null
movingPlacedMapObject: PlacedMapObject | null
}>()
const gameStore = useGameStore()
const scene = useScene()
const imageProps = computed(() => ({
alpha: props.movingPlacedMapObject?.id === props.placedMapObject.id ? 0.5 : 1,
tint: props.selectedPlacedMapObject?.id === props.placedMapObject.id ? 0x00ff00 : 0xffffff,
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, props.placedMapObject.mapObject.frameWidth, props.placedMapObject.mapObject.frameHeight),
x: tileToWorldX(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
y: tileToWorldY(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
flipX: props.placedMapObject.isRotated,
texture: props.placedMapObject.mapObject.id,
originY: Number(props.placedMapObject.mapObject.originX),
originX: Number(props.placedMapObject.mapObject.originY)
}))
loadTexture(scene, {
key: props.placedMapObject.mapObject.id,
data: '/textures/map_objects/' + props.placedMapObject.mapObject.id + '.png',
group: 'map_objects',
updatedAt: props.placedMapObject.mapObject.updatedAt,
frameWidth: props.placedMapObject.mapObject.frameWidth,
frameHeight: props.placedMapObject.mapObject.frameHeight
} as TextureData).catch((error) => {
console.error('Error loading texture:', error)
})
</script>

View File

@ -1,246 +0,0 @@
<template>
<SelectedPlacedMapObjectComponent v-if="selectedPlacedMapObject" :placedMapObject="selectedPlacedMapObject" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
<PlacedMapObject v-for="placedMapObject in mapEditorStore.map?.placedMapObjects" :tilemap="tilemap" :placedMapObject :selectedPlacedMapObject :movingPlacedMapObject @pointerup="clickPlacedMapObject(placedMapObject)" />
</template>
<script setup lang="ts">
import type { PlacedMapObject as PlacedMapObjectT } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import PlacedMapObject from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObject.vue'
import SelectedPlacedMapObjectComponent from '@/components/gameMaster/mapEditor/partials/SelectedPlacedMapObject.vue'
import { getTile } from '@/composables/mapComposable'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, ref, watch } from 'vue'
const scene = useScene()
const mapEditorStore = useMapEditorStore()
const selectedPlacedMapObject = ref<PlacedMapObjectT | null>(null)
const movingPlacedMapObject = ref<PlacedMapObjectT | null>(null)
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
function pencil(pointer: Phaser.Input.Pointer) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is map_object
if (mapEditorStore.drawMode !== 'map_object') return
// Check if there is a selected object
if (!mapEditorStore.selectedMapObject) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
if (existingPlacedMapObject) return
const newPlacedMapObject = {
id: uuidv4(),
map: mapEditorStore.map,
mapObject: mapEditorStore.selectedMapObject,
depth: 0,
isRotated: false,
positionX: tile.x,
positionY: tile.y
}
// Add new object to mapObjects
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.concat(newPlacedMapObject as PlacedMapObjectT)
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is eraser
if (mapEditorStore.tool !== 'eraser') return
// Check if draw mode is map_object
if (mapEditorStore.eraserMode !== 'map_object') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
if (!existingPlacedMapObject) return
// Remove existing object
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id)
}
function objectPicker(pointer: Phaser.Input.Pointer) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is map_object
if (mapEditorStore.drawMode !== 'map_object') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// If alt is not pressed, return
if (!pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
if (!existingPlacedMapObject) return
// Select the object
mapEditorStore.setSelectedMapObject(existingPlacedMapObject.mapObject)
}
function moveMapObject(id: string) {
// Check if map is set
if (!mapEditorStore.map) return
movingPlacedMapObject.value = mapEditorStore.map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!movingPlacedMapObject.value) return
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
movingPlacedMapObject.value.positionX = tile.x
movingPlacedMapObject.value.positionY = tile.y
}
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
function handlePointerUp() {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
movingPlacedMapObject.value = null
}
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
}
function rotatePlacedMapObject(id: string) {
// Check if map is set
if (!mapEditorStore.map) return
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.map((placedMapObject) => {
if (placedMapObject.id === id) {
return {
...placedMapObject,
isRotated: !placedMapObject.isRotated
}
}
return placedMapObject
})
}
function deletePlacedMapObject(id: string) {
// Check if map is set
if (!mapEditorStore.map) return
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.filter((object) => object.id !== id)
selectedPlacedMapObject.value = null
}
function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) {
selectedPlacedMapObject.value = placedMapObject
// If alt is pressed, select the object
if (scene.input.activePointer.event.altKey) {
mapEditorStore.setSelectedMapObject(placedMapObject.mapObject)
}
}
onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, objectPicker)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, objectPicker)
})
// watch mapEditorStore.mapObjectList and update originX and originY of objects in mapObjects
watch(
() => mapEditorStore.mapObjectList,
(newMapObjects) => {
if (!mapEditorStore.map) return
const updatedMapObjects = mapEditorStore.map.placedMapObjects.map((mapObject) => {
const updatedMapObject = newMapObjects.find((obj) => obj.id === mapObject.mapObject.id)
if (updatedMapObject) {
return {
...mapObject,
mapObject: {
...mapObject.mapObject,
originX: updatedMapObject.originX,
originY: updatedMapObject.originY
}
}
}
return mapObject
})
// Update the map with the new mapObjects
mapEditorStore.setMap({
...mapEditorStore.map,
placedMapObjects: updatedMapObjects
})
// Update selectedMapObject if it's set
if (mapEditorStore.selectedMapObject) {
const updatedMapObject = newMapObjects.find((obj) => obj.id === mapEditorStore.selectedMapObject?.id)
if (updatedMapObject) {
mapEditorStore.setSelectedMapObject({
...mapEditorStore.selectedMapObject,
originX: updatedMapObject.originX,
originY: updatedMapObject.originY
})
}
}
}
// { deep: true }
)
</script>

View File

@ -1,61 +0,0 @@
<template>
<CreateMap v-if="mapEditorStore.isCreateMapModalShown" />
<Modal :is-modal-open="mapEditorStore.isMapListModalShown" @modal:close="() => mapEditorStore.toggleMapListModal()" :is-resizable="false" :modal-width="300" :modal-height="360" :bg-style="'none'">
<template #modalHeader>
<h3 class="text-lg text-white">Maps</h3>
</template>
<template #modalBody>
<div class="my-4 mx-auto">
<div class="text-center mb-4 px-2 flex gap-2.5">
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchMaps">Refresh</button>
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="() => mapEditorStore.toggleCreateMapModal()">New</button>
</div>
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(map, index) in mapEditorStore.mapList" :key="map.id">
<div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div>
<div class="flex gap-3 items-center w-full" @click="() => loadMap(map.id)">
<span>{{ map.name }}</span>
<span class="ml-auto gap-1 flex">
<button class="btn-red w-7 h-7 z-50 flex items-center justify-center" @click.stop="() => deleteMap(map.id)">x</button>
</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import type { Map, UUID } from '@/application/types'
import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue'
import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onMounted } from 'vue'
const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
onMounted(async () => {
fetchMaps()
})
function fetchMaps() {
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => {
mapEditorStore.setMapList(response)
})
}
function loadMap(id: UUID) {
gameStore.connection?.emit('gm:map:request', { mapId: id }, (response: Map) => {
mapEditorStore.setMap(response)
})
mapEditorStore.toggleMapListModal()
}
function deleteMap(id: UUID) {
gameStore.connection?.emit('gm:map:delete', { mapId: id }, () => {
fetchMaps()
})
}
</script>

View File

@ -0,0 +1,73 @@
<template>
<ZoneTiles @tileMap:create="tileMap = $event" />
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<ZoneEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Toolbar @save="save" @clear="clear" />
<ZoneList />
<TileList />
<ObjectList />
<ZoneSettings />
<TeleportModal />
</template>
<script setup lang="ts">
import { type Zone } from '@/application/types'
import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.vue'
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
// Components
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
import ZoneEventTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneEventTiles.vue'
import ZoneObjects from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObjects.vue'
import ZoneTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onUnmounted, ref } from 'vue'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
function clear() {
if (!zoneEditorStore.zone) return
// Clear objects, event tiles and tiles
zoneEditorStore.zone.zoneObjects = []
zoneEditorStore.zone.zoneEventTiles = []
zoneEditorStore.triggerClearTiles()
}
function save() {
if (!zoneEditorStore.zone) return
const data = {
zoneId: zoneEditorStore.zone.id,
name: zoneEditorStore.zoneSettings.name,
width: zoneEditorStore.zoneSettings.width,
height: zoneEditorStore.zoneSettings.height,
tiles: zoneEditorStore.zone.tiles,
pvp: zoneEditorStore.zone.pvp,
zoneEffects: zoneEditorStore.zone.zoneEffects.map(({ id, zoneId, effect, strength }) => ({ id, zoneId, effect, strength })),
zoneEventTiles: zoneEditorStore.zone.zoneEventTiles.map(({ id, zoneId, type, positionX, positionY, teleport }) => ({ id, zoneId, type, positionX, positionY, teleport })),
zoneObjects: zoneEditorStore.zone.zoneObjects.map(({ id, zoneId, objectId, depth, isRotated, positionX, positionY }) => ({ id, zoneId, objectId, depth, isRotated, positionX, positionY }))
}
if (zoneEditorStore.isSettingsModalShown) {
zoneEditorStore.toggleSettingsModal()
}
gameStore.connection?.emit('gm:zone_editor:zone:update', data, (response: Zone) => {
zoneEditorStore.setZone(response)
})
}
onUnmounted(() => {
zoneEditorStore.reset()
})
</script>

View File

@ -1,7 +1,7 @@
<template> <template>
<Modal :isModalOpen="true" @modal:close="() => mapEditorStore.toggleCreateMapModal()" :modal-width="300" :modal-height="420" :is-resizable="false" :bg-style="'none'"> <Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="420" :is-resizable="false" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Create new map</h3> <h3 class="m-0 font-medium shrink-0 text-white">Create new zone</h3>
</template> </template>
<template #modalBody> <template #modalBody>
@ -36,23 +36,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Map } from '@/application/types' import type { Zone } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { ref } from 'vue' import { ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore() const zoneEditorStore = useZoneEditorStore()
const name = ref('') const name = ref('')
const width = ref(0) const width = ref(0)
const height = ref(0) const height = ref(0)
function submit() { function submit() {
gameStore.connection?.emit('gm:map:create', { name: name.value, width: width.value, height: height.value }, (response: Map[]) => { gameStore.connection.emit('gm:zone_editor:zone:create', { name: name.value, width: width.value, height: height.value }, (response: Zone[]) => {
mapEditorStore.setMapList(response) zoneEditorStore.setZoneList(response)
}) })
mapEditorStore.toggleCreateMapModal() zoneEditorStore.toggleCreateZoneModal()
} }
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<Modal :isModalOpen="mapEditorStore.isMapObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (mapEditorStore.isMapObjectListModalShown = false)" :bg-style="'none'"> <Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="text-lg text-white">Map objects</h3> <h3 class="text-lg text-white">Objects</h3>
</template> </template>
<template #modalBody> <template #modalBody>
<div class="flex pt-4 pl-4"> <div class="flex pt-4 pl-4">
@ -20,16 +20,16 @@
</div> </div>
<div class="h-full overflow-auto"> <div class="h-full overflow-auto">
<div class="flex justify-between flex-wrap gap-2.5 items-center"> <div class="flex justify-between flex-wrap gap-2.5 items-center">
<div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block"> <div v-for="(object, index) in filteredObjects" :key="index" class="max-w-1/4 inline-block">
<img <img
class="border-2 border-solid max-w-full" class="border-2 border-solid max-w-full"
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`" :src="`${config.server_endpoint}/assets/objects/${object.id}.png`"
alt="Object" alt="Object"
@click="mapEditorStore.setSelectedMapObject(mapObject)" @click="zoneEditorStore.setSelectedObject(object)"
:class="{ :class="{
'cursor-pointer transition-all duration-300': true, 'cursor-pointer transition-all duration-300': true,
'border-cyan shadow-lg scale-105': mapEditorStore.selectedMapObject?.id === mapObject.id, 'border-cyan shadow-lg scale-105': zoneEditorStore.selectedObject?.id === object.id,
'border-transparent hover:border-gray-300': mapEditorStore.selectedMapObject?.id !== mapObject.id 'border-transparent hover:border-gray-300': zoneEditorStore.selectedObject?.id !== object.id
}" }"
/> />
</div> </div>
@ -42,25 +42,25 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import type { MapObject } from '@/application/types' import type { Object, ZoneObject } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const isModalOpen = ref(false) const isModalOpen = ref(false)
const mapEditorStore = useMapEditorStore() const zoneEditorStore = useZoneEditorStore()
const searchQuery = ref('') const searchQuery = ref('')
const selectedTags = ref<string[]>([]) const selectedTags = ref<string[]>([])
const uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = mapEditorStore.mapObjectList.flatMap((obj) => obj.tags || []) const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || [])
return Array.from(new Set(allTags)) return Array.from(new Set(allTags))
}) })
const filteredMapObjects = computed(() => { const filteredObjects = computed(() => {
return mapEditorStore.mapObjectList.filter((object) => { return zoneEditorStore.objectList.filter((object) => {
const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase()) const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag))) const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
return matchesSearch && matchesTags return matchesSearch && matchesTags
@ -77,8 +77,8 @@ const toggleTag = (tag: string) => {
onMounted(async () => { onMounted(async () => {
isModalOpen.value = true isModalOpen.value = true
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => { gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
mapEditorStore.setMapObjectList(response) zoneEditorStore.setObjectList(response)
}) })
}) })
</script> </script>

View File

@ -11,23 +11,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { PlacedMapObject } from '@/application/types' import type { ZoneObject } from '@/application/types'
const props = defineProps<{ const props = defineProps<{
placedMapObject: PlacedMapObject zoneObject: ZoneObject
}>() }>()
const emit = defineEmits(['move', 'rotate', 'delete']) const emit = defineEmits(['move', 'rotate', 'delete'])
const handleMove = () => { const handleMove = () => {
emit('move', props.placedMapObject.id) emit('move', props.zoneObject.id)
} }
const handleRotate = () => { const handleRotate = () => {
emit('rotate', props.placedMapObject.id) emit('rotate', props.zoneObject.id)
} }
const handleDelete = () => { const handleDelete = () => {
emit('delete', props.placedMapObject.id) emit('delete', props.zoneObject.id)
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal :is-modal-open="showTeleportModal" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'"> <Modal :is-modal-open="showTeleportModal" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3> <h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
</template> </template>
@ -25,10 +25,10 @@
</select> </select>
</div> </div>
<div class="form-field-full"> <div class="form-field-full">
<label for="toMap">Map to teleport to</label> <label for="toZoneId">Zone to teleport to</label>
<select v-model="toMap" class="input-field" name="toMap" id="toMap"> <select v-model="toZoneId" class="input-field" name="toZoneId" id="toZoneId">
<option :value="null">Select map</option> <option :value="0">Select zone</option>
<option v-for="map in mapEditorStore.mapList" :key="map.id" :value="map">{{ map.name }}</option> <option v-for="zone in zoneEditorStore.zoneList" :key="zone.id" :value="zone.id">{{ zone.name }}</option>
</select> </select>
</div> </div>
</div> </div>
@ -39,44 +39,44 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Map } from '@/application/types' import type { Zone } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport') const showTeleportModal = computed(() => zoneEditorStore.tool === 'pencil' && zoneEditorStore.drawMode === 'teleport')
const mapEditorStore = useMapEditorStore() const zoneEditorStore = useZoneEditorStore()
const gameStore = useGameStore() const gameStore = useGameStore()
onMounted(fetchMaps) onMounted(fetchZones)
function fetchMaps() { function fetchZones() {
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => { gameStore.connection?.emit('gm:zone_editor:zone:list', {}, (response: Zone[]) => {
mapEditorStore.setMapList(response) zoneEditorStore.setZoneList(response)
}) })
} }
const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings() const { toPositionX, toPositionY, toRotation, toZoneId } = useRefTeleportSettings()
function useRefTeleportSettings() { function useRefTeleportSettings() {
const settings = mapEditorStore.teleportSettings const settings = zoneEditorStore.teleportSettings
return { return {
toPositionX: ref(settings.toPositionX), toPositionX: ref(settings.toPositionX),
toPositionY: ref(settings.toPositionY), toPositionY: ref(settings.toPositionY),
toRotation: ref(settings.toRotation), toRotation: ref(settings.toRotation),
toMap: ref(settings.toMap) toZoneId: ref(settings.toZoneId)
} }
} }
watch([toPositionX, toPositionY, toRotation, toMap], updateTeleportSettings) watch([toPositionX, toPositionY, toRotation, toZoneId], updateTeleportSettings)
function updateTeleportSettings() { function updateTeleportSettings() {
mapEditorStore.setTeleportSettings({ zoneEditorStore.setTeleportSettings({
toPositionX: toPositionX.value, toPositionX: toPositionX.value,
toPositionY: toPositionY.value, toPositionY: toPositionY.value,
toRotation: toRotation.value, toRotation: toRotation.value,
toMap: toMap.value toZoneId: toZoneId.value
}) })
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal :isModalOpen="mapEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (mapEditorStore.isTileListModalShown = false)" :bg-style="'none'"> <Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="text-lg text-white">Tiles</h3> <h3 class="text-lg text-white">Tiles</h3>
</template> </template>
@ -24,7 +24,7 @@
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative"> <div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
<img <img
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300" class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`" :src="`${config.server_endpoint}/assets/tiles/${group.parent.id}.png`"
:alt="group.parent.name" :alt="group.parent.name"
@click="openGroup(group)" @click="openGroup(group)"
@load="() => processTile(group.parent)" @load="() => processTile(group.parent)"
@ -50,7 +50,7 @@
<div class="flex flex-col items-center justify-center"> <div class="flex flex-col items-center justify-center">
<img <img
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300" class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`" :src="`${config.server_endpoint}/assets/tiles/${selectedGroup.parent.id}.png`"
:alt="selectedGroup.parent.name" :alt="selectedGroup.parent.name"
@click="selectTile(selectedGroup.parent.id)" @click="selectTile(selectedGroup.parent.id)"
:class="{ :class="{
@ -63,7 +63,7 @@
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center"> <div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
<img <img
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300" class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`" :src="`${config.server_endpoint}/assets/tiles/${childTile.id}.png`"
:alt="childTile.name" :alt="childTile.name"
@click="selectTile(childTile.id)" @click="selectTile(childTile.id)"
:class="{ :class="{
@ -85,25 +85,25 @@ import config from '@/application/config'
import type { Tile } from '@/application/types' import type { Tile } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const isModalOpen = ref(false) const isModalOpen = ref(false)
const mapEditorStore = useMapEditorStore() const zoneEditorStore = useZoneEditorStore()
const searchQuery = ref('') const searchQuery = ref('')
const selectedTags = ref<string[]>([]) const selectedTags = ref<string[]>([])
const tileCategories = ref<Map<string, string>>(new Map()) const tileCategories = ref<Map<string, string>>(new Map())
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null) const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
const uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = mapEditorStore.tileList.flatMap((tile) => tile.tags || []) const allTags = zoneEditorStore.tileList.flatMap((tile) => tile.tags || [])
return Array.from(new Set(allTags)) return Array.from(new Set(allTags))
}) })
const groupedTiles = computed(() => { const groupedTiles = computed(() => {
const groups: { parent: Tile; children: Tile[] }[] = [] const groups: { parent: Tile; children: Tile[] }[] = []
const filteredTiles = mapEditorStore.tileList.filter((tile) => { const filteredTiles = zoneEditorStore.tileList.filter((tile) => {
const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase()) const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag))) const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag)))
return matchesSearch && matchesTags return matchesSearch && matchesTags
@ -169,7 +169,7 @@ function processTile(tile: Tile) {
tileColorData.value.set(tile.id, getDominantColor(imageData)) tileColorData.value.set(tile.id, getDominantColor(imageData))
tileEdgeData.value.set(tile.id, getEdgeComplexity(imageData)) tileEdgeData.value.set(tile.id, getEdgeComplexity(imageData))
} }
img.src = `${config.server_endpoint}/textures/tiles/${tile.id}.png` img.src = `${config.server_endpoint}/assets/tiles/${tile.id}.png`
} }
function getDominantColor(imageData: ImageData) { function getDominantColor(imageData: ImageData) {
@ -219,17 +219,17 @@ function closeGroup() {
} }
function selectTile(tile: string) { function selectTile(tile: string) {
mapEditorStore.setSelectedTile(tile) zoneEditorStore.setSelectedTile(tile)
} }
function isActiveTile(tile: Tile): boolean { function isActiveTile(tile: Tile): boolean {
return mapEditorStore.selectedTile === tile.id return zoneEditorStore.selectedTile === tile.id
} }
onMounted(async () => { onMounted(async () => {
isModalOpen.value = true isModalOpen.value = true
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => { gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
mapEditorStore.setTileList(response) zoneEditorStore.setTileList(response)
response.forEach((tile) => processTile(tile)) response.forEach((tile) => processTile(tile))
}) })
}) })

View File

@ -1,27 +1,27 @@
<template> <template>
<div class="flex justify-center p-5"> <div class="flex justify-center p-5">
<div class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10"> <div class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
<div ref="toolbar" class="tools flex gap-2.5" v-if="mapEditorStore.map"> <div ref="toolbar" class="tools flex gap-2.5" v-if="zoneEditorStore.zone">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'move' }" @click="handleClick('move')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'move' }" @click="handleClick('move')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'move' }">(M)</span> <img class="invert w-5 h-5" src="/assets/icons/zoneEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'move' }">(M)</span>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'pencil' }" @click="handleClick('pencil')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'pencil' }" @click="handleClick('pencil')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'pencil' }">(P)</span> <img class="invert w-5 h-5" src="/assets/icons/zoneEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'pencil' }">(P)</span>
<div class="select" v-if="mapEditorStore.tool === 'pencil'"> <div class="select" v-if="zoneEditorStore.tool === 'pencil'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }"> <div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
{{ mapEditorStore.drawMode.replace('_', ' ') }} {{ zoneEditorStore.drawMode }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" /> <img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
</div> </div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && mapEditorStore.tool === 'pencil'"> <div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && zoneEditorStore.tool === 'pencil'">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('tile')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('tile')">
Tile Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('map_object')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('object')">
Map object Object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('teleport')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('teleport')">
@ -35,20 +35,20 @@
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'eraser' }" @click="handleClick('eraser')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'eraser' }" @click="handleClick('eraser')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'eraser' }">(E)</span> <img class="invert w-5 h-5" src="/assets/icons/zoneEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'eraser' }">(E)</span>
<div class="select" v-if="mapEditorStore.tool === 'eraser'"> <div class="select" v-if="zoneEditorStore.tool === 'eraser'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }"> <div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
{{ mapEditorStore.eraserMode.replace('_', ' ') }} {{ zoneEditorStore.eraserMode }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" /> <img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
</div> </div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen"> <div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('tile')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('tile')">
Tile Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('map_object')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('object')">
Map object Object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('teleport')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('teleport')">
@ -62,31 +62,31 @@
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'paint' }" @click="handleClick('paint')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'paint' }" @click="handleClick('paint')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'paint' }">(B)</span> <img class="invert w-5 h-5" src="/assets/icons/zoneEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'paint' }">(B)</span>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')" v-if="mapEditorStore.map"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button> <button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')" v-if="zoneEditorStore.zone"><img class="invert w-5 h-5" src="/assets/icons/zoneEditor/gear.svg" alt="Zone settings" /> <span class="h-5 ml-2.5">(Z)</span></button>
</div> </div>
<div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2"> <div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
<button class="btn-cyan px-3.5" @click="() => mapEditorStore.toggleMapListModal()">Load</button> <button class="btn-cyan px-3.5" @click="() => zoneEditorStore.toggleZoneListModal()">Load</button>
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditorStore.map">Save</button> <button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="zoneEditorStore.zone">Save</button>
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditorStore.map">Clear</button> <button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="zoneEditorStore.zone">Clear</button>
<button class="btn-cyan px-3.5" @click="() => mapEditorStore.toggleActive()">Exit</button> <button class="btn-cyan px-3.5" @click="() => zoneEditorStore.toggleActive()">Exit</button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import { onBeforeUnmount, onMounted, ref } from 'vue' import { onBeforeUnmount, onMounted, ref } from 'vue'
const mapEditorStore = useMapEditorStore() const zoneEditorStore = useZoneEditorStore()
const emit = defineEmits(['save', 'clear']) const emit = defineEmits(['save', 'clear'])
@ -99,24 +99,24 @@ let selectEraserOpen = ref(false)
// drawMode // drawMode
function setDrawMode(value: string) { function setDrawMode(value: string) {
mapEditorStore.isTileListModalShown = value === 'tile' zoneEditorStore.isTileListModalShown = value === 'tile'
mapEditorStore.isMapObjectListModalShown = value === 'map_object' zoneEditorStore.isObjectListModalShown = value === 'object'
mapEditorStore.setDrawMode(value) zoneEditorStore.setDrawMode(value)
selectPencilOpen.value = false selectPencilOpen.value = false
} }
// drawMode // drawMode
function setEraserMode(value: string) { function setEraserMode(value: string) {
mapEditorStore.setEraserMode(value) zoneEditorStore.setEraserMode(value)
selectEraserOpen.value = false selectEraserOpen.value = false
} }
function handleClick(tool: string) { function handleClick(tool: string) {
if (tool === 'settings') { if (tool === 'settings') {
mapEditorStore.toggleSettingsModal() zoneEditorStore.toggleSettingsModal()
} else { } else {
mapEditorStore.setTool(tool) zoneEditorStore.setTool(tool)
} }
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
@ -125,7 +125,7 @@ function handleClick(tool: string) {
function cycleToolMode(tool: 'pencil' | 'eraser') { function cycleToolMode(tool: 'pencil' | 'eraser') {
const modes = ['tile', 'object', 'teleport', 'blocking tile'] const modes = ['tile', 'object', 'teleport', 'blocking tile']
const currentMode = tool === 'pencil' ? mapEditorStore.drawMode : mapEditorStore.eraserMode const currentMode = tool === 'pencil' ? zoneEditorStore.drawMode : zoneEditorStore.eraserMode
const currentIndex = modes.indexOf(currentMode) const currentIndex = modes.indexOf(currentMode)
const nextIndex = (currentIndex + 1) % modes.length const nextIndex = (currentIndex + 1) % modes.length
const nextMode = modes[nextIndex] const nextMode = modes[nextIndex]
@ -138,8 +138,8 @@ function cycleToolMode(tool: 'pencil' | 'eraser') {
} }
function initKeyShortcuts(event: KeyboardEvent) { function initKeyShortcuts(event: KeyboardEvent) {
// Check if map is set // Check if zone is set
if (!mapEditorStore.map) return if (!zoneEditorStore.zone) return
// prevent if focused on composables // prevent if focused on composables
if (document.activeElement?.tagName === 'INPUT') return if (document.activeElement?.tagName === 'INPUT') return
@ -154,7 +154,7 @@ function initKeyShortcuts(event: KeyboardEvent) {
if (keyActions.hasOwnProperty(event.key)) { if (keyActions.hasOwnProperty(event.key)) {
const tool = keyActions[event.key] const tool = keyActions[event.key]
if ((tool === 'pencil' || tool === 'eraser') && mapEditorStore.tool === tool) { if ((tool === 'pencil' || tool === 'eraser') && zoneEditorStore.tool === tool) {
cycleToolMode(tool) cycleToolMode(tool)
} else { } else {
handleClick(tool) handleClick(tool)

View File

@ -0,0 +1,61 @@
<template>
<CreateZone v-if="zoneEditorStore.isCreateZoneModalShown" />
<Modal :is-modal-open="zoneEditorStore.isZoneListModalShown" @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :modal-width="300" :modal-height="360" :bg-style="'none'">
<template #modalHeader>
<h3 class="text-lg text-white">Zones</h3>
</template>
<template #modalBody>
<div class="my-4 mx-auto">
<div class="text-center mb-4 px-2 flex gap-2.5">
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchZones">Refresh</button>
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="() => zoneEditorStore.toggleCreateZoneModal()">New</button>
</div>
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(zone, index) in zoneEditorStore.zoneList" :key="zone.id">
<div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div>
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
<span>{{ zone.name }}</span>
<span class="ml-auto gap-1 flex">
<button class="btn-red w-7 h-7 z-50 flex items-center justify-center" @click.stop="() => deleteZone(zone.id)">x</button>
</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import type { Zone } from '@/application/types'
import CreateZone from '@/components/gameMaster/zoneEditor/partials/CreateZone.vue'
import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onMounted } from 'vue'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
onMounted(async () => {
fetchZones()
})
function fetchZones() {
gameStore.connection?.emit('gm:zone_editor:zone:list', {}, (response: Zone[]) => {
zoneEditorStore.setZoneList(response)
})
}
function loadZone(id: number) {
gameStore.connection?.emit('gm:zone_editor:zone:request', { zoneId: id }, (response: Zone) => {
zoneEditorStore.setZone(response)
})
zoneEditorStore.toggleZoneListModal()
}
function deleteZone(id: number) {
gameStore.connection?.emit('gm:zone_editor:zone:delete', { zoneId: id }, () => {
fetchZones()
})
}
</script>

View File

@ -1,7 +1,7 @@
<template> <template>
<Modal :is-modal-open="mapEditorStore.isSettingsModalShown" @modal:close="() => mapEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="430" :bg-style="'none'"> <Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="430" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Map settings</h3> <h3 class="m-0 font-medium shrink-0 text-white">Zone settings</h3>
</template> </template>
<template #modalBody> <template #modalBody>
@ -34,7 +34,7 @@
</div> </div>
</form> </form>
<form method="post" @submit.prevent="" class="inline" v-if="screen === 'effects'"> <form method="post" @submit.prevent="" class="inline" v-if="screen === 'effects'">
<div v-for="(effect, index) in mapEffects" :key="effect.id" class="mb-2 flex items-center space-x-2 mt-4"> <div v-for="(effect, index) in zoneEffects" :key="effect.id" class="mb-2 flex items-center space-x-2 mt-4">
<input class="input-field flex-grow" v-model="effect.effect" placeholder="Effect name" /> <input class="input-field flex-grow" v-model="effect.effect" placeholder="Effect name" />
<input class="input-field w-20" v-model.number="effect.strength" type="number" placeholder="Strength" /> <input class="input-field w-20" v-model.number="effect.strength" type="number" placeholder="Strength" />
<button class="btn-red py-1 px-2" type="button" @click="removeEffect(index)">Delete</button> <button class="btn-red py-1 px-2" type="button" @click="removeEffect(index)">Delete</button>
@ -46,61 +46,61 @@
</Modal> </Modal>
</template> </template>
<script setup lang="ts"> <script setup>
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
const mapEditorStore = useMapEditorStore() const zoneEditorStore = useZoneEditorStore()
const screen = ref('settings') const screen = ref('settings')
mapEditorStore.setMapName(mapEditorStore.map?.name) zoneEditorStore.setZoneName(zoneEditorStore.zone?.name)
mapEditorStore.setMapWidth(mapEditorStore.map?.width) zoneEditorStore.setZoneWidth(zoneEditorStore.zone?.width)
mapEditorStore.setMapHeight(mapEditorStore.map?.height) zoneEditorStore.setZoneHeight(zoneEditorStore.zone?.height)
mapEditorStore.setMapPvp(mapEditorStore.map?.pvp) zoneEditorStore.setZonePvp(zoneEditorStore.zone?.pvp)
mapEditorStore.setMapEffects(mapEditorStore.map?.mapEffects) zoneEditorStore.setZoneEffects(zoneEditorStore.zone?.zoneEffects)
const name = ref(mapEditorStore.mapSettings?.name) const name = ref(zoneEditorStore.zoneSettings?.name)
const width = ref(mapEditorStore.mapSettings?.width) const width = ref(zoneEditorStore.zoneSettings?.width)
const height = ref(mapEditorStore.mapSettings?.height) const height = ref(zoneEditorStore.zoneSettings?.height)
const pvp = ref(mapEditorStore.mapSettings?.pvp) const pvp = ref(zoneEditorStore.zoneSettings?.pvp)
const mapEffects = ref(mapEditorStore.mapSettings?.mapEffects || []) const zoneEffects = ref(zoneEditorStore.zoneSettings?.zoneEffects || [])
watch(name, (value) => { watch(name, (value) => {
mapEditorStore.setMapName(value) zoneEditorStore.setZoneName(value)
}) })
watch(width, (value) => { watch(width, (value) => {
mapEditorStore.setMapWidth(value) zoneEditorStore.setZoneWidth(value)
}) })
watch(height, (value) => { watch(height, (value) => {
mapEditorStore.setMapHeight(value) zoneEditorStore.setZoneHeight(value)
}) })
watch(pvp, (value) => { watch(pvp, (value) => {
mapEditorStore.setMapPvp(value) zoneEditorStore.setZonePvp(value)
}) })
watch( watch(
mapEffects, zoneEffects,
(value) => { (value) => {
mapEditorStore.setMapEffects(value) zoneEditorStore.setZoneEffects(value)
}, },
{ deep: true } { deep: true }
) )
const addEffect = () => { const addEffect = () => {
mapEffects.value.push({ zoneEffects.value.push({
id: Date.now().toString(), // Simple unique id generation id: Date.now().toString(), // Simple unique id generation
mapId: mapEditorStore.map?.id, zoneId: zoneEditorStore.zone?.id,
map: mapEditorStore.map, zone: zoneEditorStore.zone,
effect: '', effect: '',
strength: 1 strength: 1
}) })
} }
const removeEffect = (index) => { const removeEffect = (index) => {
mapEffects.value.splice(index, 1) zoneEffects.value.splice(index, 1)
} }
</script> </script>

View File

@ -1,23 +1,23 @@
<template> <template>
<Image v-for="tile in mapEditorStore.map?.mapEventTiles" v-bind="getImageProps(tile)" /> <Image v-for="tile in zoneEditorStore.zone?.zoneEventTiles" v-bind="getImageProps(tile)" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { MapEventTileType, type MapEventTile } from '@/application/types' import { ZoneEventTileType, type ZoneEventTile } from '@/application/types'
import { uuidv4 } from '@/application/utilities' import { uuidv4 } from '@/application/utilities'
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/mapComposable' import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted } from 'vue'
const scene = useScene() const scene = useScene()
const mapEditorStore = useMapEditorStore() const zoneEditorStore = useZoneEditorStore()
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tilemap: Phaser.Tilemaps.Tilemap
}>() }>()
function getImageProps(tile: MapEventTile) { function getImageProps(tile: ZoneEventTile) {
return { return {
x: tileToWorldX(props.tilemap, tile.positionX, tile.positionY), x: tileToWorldX(props.tilemap, tile.positionX, tile.positionY),
y: tileToWorldY(props.tilemap, tile.positionX, tile.positionY), y: tileToWorldY(props.tilemap, tile.positionX, tile.positionY),
@ -27,14 +27,14 @@ function getImageProps(tile: MapEventTile) {
} }
function pencil(pointer: Phaser.Input.Pointer) { function pencil(pointer: Phaser.Input.Pointer) {
// Check if map is set // Check if zone is set
if (!mapEditorStore.map) return if (!zoneEditorStore.zone) return
// Check if tool is pencil // Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is blocking tile or teleport // Check if draw mode is blocking tile or teleport
if (mapEditorStore.drawMode !== 'blocking tile' && mapEditorStore.drawMode !== 'teleport') return if (zoneEditorStore.drawMode !== 'blocking tile' && zoneEditorStore.drawMode !== 'teleport') return
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
@ -47,42 +47,42 @@ function pencil(pointer: Phaser.Input.Pointer) {
if (!tile) return if (!tile) return
// Check if event tile already exists on position // Check if event tile already exists on position
const existingEventTile = mapEditorStore.map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y) const existingEventTile = zoneEditorStore.zone.zoneEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (existingEventTile) return if (existingEventTile) return
// If teleport, check if there is a selected map // If teleport, check if there is a selected zone
if (mapEditorStore.drawMode === 'teleport' && !mapEditorStore.teleportSettings.toMap) return if (zoneEditorStore.drawMode === 'teleport' && !zoneEditorStore.teleportSettings.toZoneId) return
const newEventTile = { const newEventTile = {
id: uuidv4(), id: uuidv4(),
mapId: mapEditorStore.map.id, zoneId: zoneEditorStore.zone.id,
map: mapEditorStore.map, zone: zoneEditorStore.zone,
type: mapEditorStore.drawMode === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT, type: zoneEditorStore.drawMode === 'blocking tile' ? ZoneEventTileType.BLOCK : ZoneEventTileType.TELEPORT,
positionX: tile.x, positionX: tile.x,
positionY: tile.y, positionY: tile.y,
teleport: teleport:
mapEditorStore.drawMode === 'teleport' zoneEditorStore.drawMode === 'teleport'
? { ? {
toMap: mapEditorStore.teleportSettings.toMap, toZoneId: zoneEditorStore.teleportSettings.toZoneId,
toPositionX: mapEditorStore.teleportSettings.toPositionX, toPositionX: zoneEditorStore.teleportSettings.toPositionX,
toPositionY: mapEditorStore.teleportSettings.toPositionY, toPositionY: zoneEditorStore.teleportSettings.toPositionY,
toRotation: mapEditorStore.teleportSettings.toRotation toRotation: zoneEditorStore.teleportSettings.toRotation
} }
: undefined : undefined
} }
mapEditorStore.map.mapEventTiles = mapEditorStore.map.mapEventTiles.concat(newEventTile as MapEventTile) zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.concat(newEventTile as ZoneEventTile)
} }
function eraser(pointer: Phaser.Input.Pointer) { function eraser(pointer: Phaser.Input.Pointer) {
// Check if map is set // Check if zone is set
if (!mapEditorStore.map) return if (!zoneEditorStore.zone) return
// Check if tool is pencil // Check if tool is pencil
if (mapEditorStore.tool !== 'eraser') return if (zoneEditorStore.tool !== 'eraser') return
// Check if draw mode is blocking tile or teleport // Check if draw mode is blocking tile or teleport
if (mapEditorStore.eraserMode !== 'blocking tile' && mapEditorStore.eraserMode !== 'teleport') return if (zoneEditorStore.eraserMode !== 'blocking tile' && zoneEditorStore.eraserMode !== 'teleport') return
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
@ -95,11 +95,11 @@ function eraser(pointer: Phaser.Input.Pointer) {
if (!tile) return if (!tile) return
// Check if event tile already exists on position // Check if event tile already exists on position
const existingEventTile = mapEditorStore.map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y) const existingEventTile = zoneEditorStore.zone.zoneEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (!existingEventTile) return if (!existingEventTile) return
// Remove existing event tile // Remove existing event tile
mapEditorStore.map.mapEventTiles = mapEditorStore.map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id) zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
} }
onMounted(() => { onMounted(() => {

View File

@ -0,0 +1,45 @@
<template>
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
</template>
<script setup lang="ts">
import type { AssetDataT, ZoneObject } from '@/application/types'
import { loadTexture } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer'
import { computed } from 'vue'
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
zoneObject: ZoneObject
selectedZoneObject: ZoneObject | null
movingZoneObject: ZoneObject | null
}>()
const gameStore = useGameStore()
const scene = useScene()
const imageProps = computed(() => ({
alpha: props.movingZoneObject?.id === props.zoneObject.id ? 0.5 : 1,
tint: props.selectedZoneObject?.id === props.zoneObject.id ? 0x00ff00 : 0xffffff,
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
flipX: props.zoneObject.isRotated,
texture: props.zoneObject.object.id,
originY: Number(props.zoneObject.object.originX),
originX: Number(props.zoneObject.object.originY)
}))
loadTexture(scene, {
key: props.zoneObject.object.id,
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
group: 'objects',
updatedAt: props.zoneObject.object.updatedAt,
frameWidth: props.zoneObject.object.frameWidth,
frameHeight: props.zoneObject.object.frameHeight
} as AssetDataT).catch((error) => {
console.error('Error loading texture:', error)
})
</script>

View File

@ -0,0 +1,248 @@
<template>
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" :movingZoneObject="movingZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" />
<ZoneObject v-for="zoneObject in zoneEditorStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject :selectedZoneObject :movingZoneObject @pointerup="clickZoneObject(zoneObject)" />
</template>
<script setup lang="ts">
import type { ZoneObject as ZoneObjectT } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
import ZoneObject from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObject.vue'
import { getTile } from '@/composables/zoneComposable'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, ref, watch } from 'vue'
const scene = useScene()
const zoneEditorStore = useZoneEditorStore()
const selectedZoneObject = ref<ZoneObjectT | null>(null)
const movingZoneObject = ref<ZoneObjectT | null>(null)
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
function pencil(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is object
if (zoneEditorStore.drawMode !== 'object') return
// Check if there is a selected object
if (!zoneEditorStore.selectedObject) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingObject = zoneEditorStore.zone?.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (existingObject) return
const newObject = {
id: uuidv4(),
zoneId: zoneEditorStore.zone.id,
zone: zoneEditorStore.zone,
objectId: zoneEditorStore.selectedObject.id,
object: zoneEditorStore.selectedObject,
depth: 0,
isRotated: false,
positionX: tile.x,
positionY: tile.y
}
// Add new object to zoneObjects
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.concat(newObject as ZoneObjectT)
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is eraser
if (zoneEditorStore.tool !== 'eraser') return
// Check if draw mode is object
if (zoneEditorStore.eraserMode !== 'object') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingObject = zoneEditorStore.zone.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (!existingObject) return
// Remove existing object
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== existingObject.id)
}
function objectPicker(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is object
if (zoneEditorStore.drawMode !== 'object') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// If alt is not pressed, return
if (!pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingObject = zoneEditorStore.zone.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (!existingObject) return
// Select the object
zoneEditorStore.setSelectedObject(existingObject)
}
function moveZoneObject(id: string) {
// Check if zone is set
if (!zoneEditorStore.zone) return
movingZoneObject.value = zoneEditorStore.zone.zoneObjects.find((object) => object.id === id) as ZoneObjectT
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!movingZoneObject.value) return
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
movingZoneObject.value.positionX = tile.x
movingZoneObject.value.positionY = tile.y
}
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
function handlePointerUp() {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
movingZoneObject.value = null
}
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
}
function rotateZoneObject(id: string) {
// Check if zone is set
if (!zoneEditorStore.zone) return
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.map((object) => {
if (object.id === id) {
return {
...object,
isRotated: !object.isRotated
}
}
return object
})
}
function deleteZoneObject(id: string) {
// Check if zone is set
if (!zoneEditorStore.zone) return
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== id)
selectedZoneObject.value = null
}
function clickZoneObject(zoneObject: ZoneObjectT) {
selectedZoneObject.value = zoneObject
// If alt is pressed, select the object
if (scene.input.activePointer.event.altKey) {
zoneEditorStore.setSelectedObject(zoneObject.object)
}
}
onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, objectPicker)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, objectPicker)
})
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
watch(
() => zoneEditorStore.objectList,
(newObjects) => {
if (!zoneEditorStore.zone) return
const updatedZoneObjects = zoneEditorStore.zone.zoneObjects.map((zoneObject) => {
const updatedObject = newObjects.find((obj) => obj.id === zoneObject.object.id)
if (updatedObject) {
return {
...zoneObject,
object: {
...zoneObject.object,
originX: updatedObject.originX,
originY: updatedObject.originY
}
}
}
return zoneObject
})
// Update the zone with the new zoneObjects
zoneEditorStore.setZone({
...zoneEditorStore.zone,
zoneObjects: updatedZoneObjects
})
// Update selectedObject if it's set
if (zoneEditorStore.selectedObject) {
const updatedObject = newObjects.find((obj) => obj.id === zoneEditorStore.selectedObject?.id)
if (updatedObject) {
zoneEditorStore.setSelectedObject({
...zoneEditorStore.selectedObject,
originX: updatedObject.originX,
originY: updatedObject.originY
})
}
}
},
{ deep: true }
)
</script>

View File

@ -0,0 +1,227 @@
<template>
<Controls :layer="tileLayer" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/application/config'
import type { AssetDataT } from '@/application/types'
import Controls from '@/components/utilities/Controls.vue'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, watch } from 'vue'
const emit = defineEmits(['tileMap:create'])
const scene = useScene()
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const tileMap = createTileMap()
const tileLayer = createTileLayer()
/**
* A Tilemap is a container for Tilemap data.
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
*/
function createTileMap() {
const zoneData = new Phaser.Tilemaps.MapData({
width: zoneEditorStore.zone?.width,
height: zoneEditorStore.zone?.height,
tileWidth: config.tile_size.x,
tileHeight: config.tile_size.y,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
emit('tileMap:create', newTileMap)
return newTileMap
}
/**
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
*/
function createTileLayer() {
const tilesArray = gameStore.getLoadedAssetsByGroup('tiles')
const tilesetImages = Array.from(tilesArray).map((tile: AssetDataT, index: number) => {
return tileMap.addTilesetImage(tile.key, tile.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
}) as any
// Add blank tile
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
function pencil(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is tile
if (zoneEditorStore.drawMode !== 'tile') return
// Check if there is a selected tile
if (!zoneEditorStore.selectedTile) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if there is a tile
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap, tileLayer, tile.x, tile.y, zoneEditorStore.selectedTile)
// Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'eraser') return
// Check if draw mode is tile
if (zoneEditorStore.eraserMode !== 'tile') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap, tileLayer, tile.x, tile.y, 'blank_tile')
// Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles[tile.y][tile.x] = 'blank_tile'
}
function paint(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'paint') return
// Check if there is a selected tile
if (!zoneEditorStore.selectedTile) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) return
// Set new tileArray with selected tile
setLayerTiles(tileMap, tileLayer, createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile))
// Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles = createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile)
}
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
function tilePicker(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is tile
if (zoneEditorStore.drawMode !== 'tile') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (!pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Select the tile
zoneEditorStore.setSelectedTile(zoneEditorStore.zone.tiles[tile.y][tile.x])
}
watch(
() => zoneEditorStore.shouldClearTiles,
(shouldClear) => {
if (shouldClear && zoneEditorStore.zone) {
const blankTiles = createTileArray(tileMap.width, tileMap.height, 'blank_tile')
setLayerTiles(tileMap, tileLayer, blankTiles)
zoneEditorStore.zone.tiles = blankTiles
zoneEditorStore.resetClearTilesFlag()
}
}
)
onMounted(() => {
if (!zoneEditorStore.zone?.tiles) {
return
}
// 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, eraser)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, tilePicker)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
tileMap.destroyLayer('tiles')
tileMap.removeAllLayers()
tileMap.destroy()
})
</script>

View File

@ -23,14 +23,14 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Chat } from '@/application/types' import type { Chat } from '@/application/types'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore' import { useZoneStore } from '@/stores/zoneStore'
import { onClickOutside, useFocus } from '@vueuse/core' import { onClickOutside, useFocus } from '@vueuse/core'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue' import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
const scene = useScene() const scene = useScene()
const gameStore = useGameStore() const gameStore = useGameStore()
const mapStore = useMapStore() const zoneStore = useZoneStore()
const message = ref('') const message = ref('')
const chats = ref([] as Chat[]) const chats = ref([] as Chat[])
@ -79,11 +79,11 @@ const scrollToBottom = () => {
}) })
} }
gameStore.connection?.on('chat:message', (data: Chat) => { gameStore.connection!.on('chat:message', (data: Chat) => {
chats.value.push(data) chats.value.push(data)
scrollToBottom() scrollToBottom()
if (!mapStore.characterLoaded) return if (!zoneStore.characterLoaded) return
const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
if (!charChatContainer) return if (!charChatContainer) return

View File

@ -11,7 +11,7 @@ import { onUnmounted } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
// Listen for new date from socket // Listen for new date from socket
gameStore.connection?.on('date', (data: Date) => { gameStore.connection!.on('date', (data: Date) => {
gameStore.world.date = new Date(data) gameStore.world.date = new Date(data)
}) })

View File

@ -30,10 +30,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import CharacterScreen from '@/components/game/gui/partials/CharacterScreen.vue' import CharacterScreen from '@/components/gui/partials/CharacterScreen.vue'
import Equipment from '@/components/game/gui/partials/Equipment.vue' import Equipment from '@/components/gui/partials/Equipment.vue'
import Inventory from '@/components/game/gui/partials/Inventory.vue' import Inventory from '@/components/gui/partials/Inventory.vue'
import Settings from '@/components/game/gui/partials/Settings.vue' import Settings from '@/components/gui/partials/Settings.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue' import { ref } from 'vue'

View File

@ -33,7 +33,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import CharacterSettings from '@/components/game/gui/partials/settings/CharacterSettings.vue' import CharacterSettings from '@/components/gui/partials/settings/CharacterSettings.vue'
import { ref } from 'vue' import { ref } from 'vue'
let settingCategory = ref('character') let settingCategory = ref('character')

View File

@ -1,8 +1,5 @@
<template> <template>
<div class="relative max-lg:h-dvh flex flex-row-reverse"> <div class="relative max-lg:h-dvh flex flex-row-reverse">
<div class="portrait-mode-notice hidden absolute h-[calc(100%_-_80px)] w-[calc(100%_-_80px)] left-0 top-0 bg-gray z-50 p-10 text-center">
<span class="text-lg">Noxious is not compatible with portrait mode on smaller screens. Please switch to landscape mode to play.</span>
</div>
<div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[65dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div> <div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[65dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
<div class="bg-[url('/assets/login/login-bg.png')] opacity-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center grayscale"></div> <div class="bg-[url('/assets/login/login-bg.png')] opacity-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center grayscale"></div>
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[35dvh] lg:h-dvh relative"></div> <div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[35dvh] lg:h-dvh relative"></div>
@ -37,23 +34,27 @@
<button class="ml-6 w-4 h-8 p-0"> <button class="ml-6 w-4 h-8 p-0">
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" /> <img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" />
</button> </button>
<img class="w-24 object-contain mb-3.5" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + characters.find((c) => c.id === selectedCharacterId)?.characterType + '/' + (selectedHairId ?? 'default')" /> <img class="w-24 object-contain mb-3.5" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + characters.find((c) => c.id === selectedCharacterId)?.characterType?.id + '/' + (selectedHairId ?? 'default')" />
<button class="mr-6 w-4 h-8 p-0"> <button class="mr-6 w-4 h-8 p-0">
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" /> <img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" />
</button> </button>
</div> </div>
<!-- <div class="flex justify-between w-[190px]">-->
<!-- &lt;!&ndash; TODO: replace with color swatches &ndash;&gt;-->
<!-- <button v-for="n in 9" class="w-4 h-4 rounded-sm bg-white"></button>-->
<!-- </div>-->
</div> </div>
<!-- TODO: update gender on (selected) character --> <!-- TODO: update gender on (selected) character -->
<!-- <div class="flex justify-between w-[190px]">--> <div class="flex justify-between w-[190px]">
<!-- <button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === 'MALE' }">--> <button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === 'MALE' }">
<!-- <img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />--> <img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
<!-- <span class="text-white">Male</span>--> <span class="text-white">Male</span>
<!-- </button>--> </button>
<!-- <button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === 'FEMALE' }">--> <button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === 'FEMALE' }">
<!-- <img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />--> <img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
<!-- <span class="text-white">Female</span>--> <span class="text-white">Female</span>
<!-- </button>--> </button>
<!-- </div>--> </div>
</div> </div>
</div> </div>
</div> </div>
@ -73,7 +74,7 @@
v-for="hair in characterHairs" v-for="hair in characterHairs"
class="relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent" class="relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent"
> >
<img class="h-4 object-contain" :src="config.server_endpoint + '/textures/sprites/' + hair.sprite + '/front.png'" alt="Hair sprite" /> <img class="h-4 object-contain" :src="config.server_endpoint + '/assets/sprites/' + hair.sprite.id + '/front.png'" alt="Hair sprite" />
<input type="radio" name="hair" :value="hair.id" v-model="selectedHairId" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" /> <input type="radio" name="hair" :value="hair.id" v-model="selectedHairId" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
</div> </div>
</div> </div>
@ -125,11 +126,10 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types' import { type CharacterHair, type Character as CharacterT, type Zone } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { CharacterHairStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue' import { onBeforeUnmount, ref, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const isLoading = ref<boolean>(true) const isLoading = ref<boolean>(true)
@ -145,9 +145,15 @@ setTimeout(() => {
gameStore.connection?.emit('character:list') gameStore.connection?.emit('character:list')
}, 750) }, 750)
gameStore.connection?.on('character:list', (data: any) => { gameStore.connection!.on('character:list', (data: any) => {
characters.value = data characters.value = data
isLoading.value = false isLoading.value = false
// Fetch hairs
// @TODO: This is hacky, we should have a better way to do this
gameStore.connection?.emit('character:hair:list', {}, (data: CharacterHair[]) => {
characterHairs.value = data
})
}) })
// Select character logics // Select character logics
@ -160,7 +166,7 @@ function loginWithCharacter() {
characterId: selectedCharacterId.value, characterId: selectedCharacterId.value,
characterHairId: selectedHairId.value characterHairId: selectedHairId.value
}, },
(response: { character: CharacterT; map: Map; characters: CharacterT[] }) => { (response: { character: CharacterT; zone: Zone; characters: CharacterT[] }) => {
gameStore.setCharacter(response.character) gameStore.setCharacter(response.character)
} }
) )
@ -168,7 +174,7 @@ function loginWithCharacter() {
// Create character logics // Create character logics
function createCharacter() { function createCharacter() {
gameStore.connection?.on('character:create:success', (data: CharacterT) => { gameStore.connection!.on('character:create:success', (data: CharacterT) => {
gameStore.setCharacter(data) gameStore.setCharacter(data)
isCreateNewCharacterModalOpen.value = false isCreateNewCharacterModalOpen.value = false
}) })
@ -178,12 +184,7 @@ function createCharacter() {
// Watch changes for selected character and update hairs // Watch changes for selected character and update hairs
watch(selectedCharacterId, (characterId) => { watch(selectedCharacterId, (characterId) => {
if (!characterId) return if (!characterId) return
// selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHairId ?? null selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHairId ?? null
})
onMounted(async () => {
const characterHairStorage = new CharacterHairStorage()
characterHairs.value = await characterHairStorage.getAll()
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@ -1,15 +1,12 @@
<template> <template>
<div class="flex justify-center items-center h-dvh relative"> <div class="flex justify-center items-center h-dvh relative">
<div class="portrait-mode-notice hidden absolute h-[calc(100%_-_80px)] w-[calc(100%_-_80px)] left-0 top-0 bg-gray z-50 p-10 text-center">
<span class="text-lg">Noxious is not compatible with portrait mode on smaller screens. Please switch to landscape mode to play.</span>
</div>
<Game :config="gameConfig" @create="createGame"> <Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene"> <Scene name="main" @preload="preloadScene" @create="createScene">
<Menu /> <Menu />
<Hud /> <Hud />
<Hotkeys /> <Hotkeys />
<Clock /> <Clock />
<Map /> <Zone />
<Chat /> <Chat />
<ExpBar /> <ExpBar />
@ -23,18 +20,19 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import 'phaser' import 'phaser'
import CharacterProfile from '@/components/game/gui/CharacterProfile.vue' import Effects from '@/components/Effects.vue'
import Chat from '@/components/game/gui/Chat.vue' import Zone from '@/components/game/zone/Zone.vue'
import Clock from '@/components/game/gui/Clock.vue' import CharacterProfile from '@/components/gui/CharacterProfile.vue'
import ExpBar from '@/components/game/gui/ExpBar.vue' import Chat from '@/components/gui/Chat.vue'
import Hotkeys from '@/components/game/gui/Hotkeys.vue' // import Minimap from '@/components/gui/Minimap.vue'
import Hud from '@/components/game/gui/Hud.vue' import Clock from '@/components/gui/Clock.vue'
import Menu from '@/components/game/gui/Menu.vue' import ExpBar from '@/components/gui/ExpBar.vue'
import Effects from '@/components/game/map/Effects.vue' import Hotkeys from '@/components/gui/Hotkeys.vue'
import Map from '@/components/game/map/Map.vue' import Hud from '@/components/gui/Hud.vue'
import Menu from '@/components/gui/Menu.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
import { Game, Scene } from 'phavuer' import { Game, Scene } from 'phavuer'
import { onBeforeUnmount } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
@ -43,11 +41,22 @@ const gameConfig = {
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
resolution: 5 resolution: 5,
plugins: {
global: [
{
key: 'rexAwaitLoader',
plugin: AwaitLoaderPlugin,
start: true
}
]
}
} }
const createGame = (game: Phaser.Game) => { const createGame = (game: Phaser.Game) => {
// Resize the game when the window is resized /**
* Resize the game when the window is resized
*/
addEventListener('resize', () => { addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight) game.scale.resize(window.innerWidth, window.innerHeight)
}) })
@ -63,12 +72,12 @@ const createGame = (game: Phaser.Game) => {
} }
function preloadScene(scene: Phaser.Scene) { function preloadScene(scene: Phaser.Scene) {
// Load the base assets into the Phaser scene /**
scene.load.image('blank_tile', '/assets/map/blank_tile.png') * Load the base assets into the Phaser scene
*/
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
scene.load.image('waypoint', '/assets/waypoint.png') scene.load.image('waypoint', '/assets/waypoint.png')
} }
function createScene(scene: Phaser.Scene) {} function createScene(scene: Phaser.Scene) {}
onBeforeUnmount(() => {})
</script> </script>

View File

@ -1,60 +1,25 @@
<template> <template>
<div class="flex flex-col justify-center items-center h-dvh relative col"> <div class="flex flex-col justify-center items-center h-dvh relative">
<svg width="40" height="40" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <button @click="continueBtnClick" class="w-32 h-12 rounded-full bg-gray-500 flex items-center justify-between px-4 hover:bg-gray-600 transition-colors">
<circle cx="4" cy="12" r="3" fill="white"> <span class="text-white text-lg flex-1 text-center">Play</span>
<animate id="spinner_qFRN" begin="0;spinner_OcgL.end+0.25s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" /> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
</circle> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
<circle cx="12" cy="12" r="3" fill="white">
<animate begin="spinner_qFRN.begin+0.1s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
</circle>
<circle cx="20" cy="12" r="3" fill="white">
<animate id="spinner_OcgL" begin="spinner_qFRN.begin+0.2s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
</circle>
</svg> </svg>
</button>
</div> </div>
</template> </template>
<script setup lang="ts" async> <script setup lang="ts" async>
import config from '@/application/config'
import type { HttpResponse, MapObject } from '@/application/types'
import type { BaseStorage } from '@/storage/baseStorage'
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages'
// import type { Map } from '@/application/types'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const totalItems = ref(0) function continueBtnClick() {
const currentItem = ref(0) // Play music
const audio = new Audio('/assets/music/login.mp3')
audio.play()
async function downloadAndStore<T extends { id: string }>(endpoint: string, storage: BaseStorage<T>) { // Set isLoaded to true
const request = await fetch(`${config.server_endpoint}/cache/${endpoint}`)
const response = (await request.json()) as HttpResponse<T[]>
if (!response.success) {
console.error(`Failed to download ${endpoint}:`, response.message)
return
}
const items = response.data ?? []
for (const item of items) {
await storage.add(item)
}
}
const tileStorage = new TileStorage()
const mapStorage = new MapStorage()
const mapObjectStorage = new MapObjectStorage()
Promise.all([
downloadAndStore('tiles', tileStorage),
downloadAndStore('maps', mapStorage),
downloadAndStore('map_objects', mapObjectStorage),
downloadAndStore('sprites', new SpriteStorage()),
downloadAndStore('character_types', new CharacterTypeStorage()),
downloadAndStore('character_hair', new CharacterHairStorage())
]).then(() => {
gameStore.game.isLoaded = true gameStore.game.isLoaded = true
}) }
</script> </script>

View File

@ -9,7 +9,7 @@
<!-- <img src="/assets/tlogo.png" class="mb-10 w-52" alt="Noxious logo" />--> <!-- <img src="/assets/tlogo.png" class="mb-10 w-52" alt="Noxious logo" />-->
<div class="relative"> <div class="relative">
<img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" /> <img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" />
<img src="/assets/ui-elements/login-ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)]" alt="UI box inner" /> <img src="/assets/ui-elements/login-ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" alt="UI box inner" />
<!-- Login Form --> <!-- Login Form -->
<LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" /> <LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" />

View File

@ -1,69 +0,0 @@
<template>
<div class="flex justify-center items-center h-dvh relative">
<Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene">
<MapEditor :key="JSON.stringify(`${mapEditorStore.map?.id}_${mapEditorStore.map?.createdAt}_${mapEditorStore.map?.updatedAt}`)" v-if="isLoaded" />
<div v-else class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div>
</Scene>
</Game>
</div>
</template>
<script setup lang="ts">
import config from '@/application/config'
import 'phaser'
import MapEditor from '@/components/gameMaster/mapEditor/MapEditor.vue'
import { loadAllTilesIntoScene } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { Game, Scene } from 'phavuer'
import { ref } from 'vue'
const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const isLoaded = ref(false)
const gameConfig = {
name: config.name,
width: window.innerWidth,
height: window.innerHeight,
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
resolution: 5
}
const createGame = (game: Phaser.Game) => {
// Resize the game when the window is resized
addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight)
})
// We don't support canvas mode, only WebGL
if (game.renderer.type === Phaser.CANVAS) {
gameStore.addNotification({
title: 'Warning',
message: 'Your browser does not support WebGL. Please use a modern browser like Chrome, Firefox, or Edge.'
})
gameStore.disconnectSocket()
}
}
const preloadScene = async (scene: Phaser.Scene) => {
// Load the base assets into the Phaser scene
scene.load.image('BLOCK', '/assets/map/bt_tile.png')
scene.load.image('TELEPORT', '/assets/map/tp_tile.png')
scene.load.image('blank_tile', '/assets/map/blank_tile.png')
scene.load.image('waypoint', '/assets/waypoint.png')
await loadAllTilesIntoScene(scene)
await new Promise<void>((resolve) => {
scene.load.on(Phaser.Loader.Events.COMPLETE, () => {
resolve()
})
isLoaded.value = true
})
}
const createScene = async (scene: Phaser.Scene) => {}
</script>

View File

@ -0,0 +1,85 @@
<template>
<div class="flex justify-center items-center h-dvh relative">
<Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene">
<ZoneEditor :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt ?? ''}`)" />
</Scene>
</Game>
</div>
</template>
<script setup lang="ts">
import config from '@/application/config'
import 'phaser'
import type { AssetDataT } from '@/application/types'
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
import { loadTexture } from '@/composables/gameComposable'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
import { Game, Scene } from 'phavuer'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const gameConfig = {
name: config.name,
width: window.innerWidth,
height: window.innerHeight,
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
resolution: 5,
plugins: {
global: [
{
key: 'rexAwaitLoader',
plugin: AwaitLoaderPlugin,
start: true
}
]
}
}
const createGame = (game: Phaser.Game) => {
/**
* Resize the game when the window is resized
*/
addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight)
})
// We don't support canvas mode, only WebGL
if (game.renderer.type === Phaser.CANVAS) {
gameStore.addNotification({
title: 'Warning',
message: 'Your browser does not support WebGL. Please use a modern browser like Chrome, Firefox, or Edge.'
})
gameStore.disconnectSocket()
}
}
const preloadScene = async (scene: Phaser.Scene) => {
/**
* Load the base assets into the Phaser scene
*/
scene.load.image('BLOCK', '/assets/zone/bt_tile.png')
scene.load.image('TELEPORT', '/assets/zone/tp_tile.png')
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
scene.load.image('waypoint', '/assets/waypoint.png')
/**
* Because Phaser can't load tiles after the scene with map in it is created,
* we need to load and cache all the tiles first.
* Then load them into the scene.
*/
scene.load.rexAwait(async function (successCallback: any) {
const tiles: AssetDataT[] = await fetch(config.server_endpoint + '/assets/list_tiles').then((response) => response.json())
for await (const tile of tiles) {
await loadTexture(scene, tile)
}
successCallback()
})
}
const createScene = async (scene: Phaser.Scene) => {}
</script>

View File

@ -1,46 +0,0 @@
<template></template>
<script setup lang="ts">
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages'
import { TextureStorage } from '@/storage/textureStorage'
import { onMounted, onUnmounted } from 'vue'
const mapStorage = new MapStorage()
const tileStorage = new TileStorage()
const mapObjectStorage = new MapObjectStorage()
const spriteStorage = new SpriteStorage()
const characterTypeStorage = new CharacterTypeStorage()
const characterHairStorage = new CharacterHairStorage()
const textureStorage = new TextureStorage()
let currentString = ''
async function handleKeyPress(event: KeyboardEvent) {
// Ignore if typing in input/textarea
if (document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') {
return
}
currentString += event.key
// Do something when string matches
if (currentString.includes('reset')) {
await mapStorage.destroy()
await tileStorage.destroy()
await mapObjectStorage.destroy()
await spriteStorage.destroy()
await characterTypeStorage.destroy()
await characterHairStorage.destroy()
await textureStorage.destroy()
currentString = '' // Reset
}
// Reset string after a certain amount of time
setTimeout(() => {
currentString = ''
}, 60000)
}
onMounted(() => window.addEventListener('keydown', handleKeyPress))
onUnmounted(() => window.removeEventListener('keydown', handleKeyPress))
</script>

View File

@ -0,0 +1,34 @@
export function createSceneLoader(scene: Phaser.Scene) {
const width = scene.cameras.main.width
const height = scene.cameras.main.height
const progressBox = scene.add.graphics()
const progressBar = scene.add.graphics()
progressBox.fillStyle(0x222222, 0.8)
progressBox.fillRect(width / 2 - 180, height / 2, 320, 50)
const loadingText = scene.make.text({
x: width / 2,
y: height / 2 - 50,
text: 'Loading...',
style: {
font: '20px monospace',
// @ts-ignore
fill: '#ffffff'
}
})
loadingText.setOrigin(0.5, 0.5)
scene.load.on(Phaser.Loader.Events.PROGRESS, function (value: any) {
progressBar.clear()
progressBar.fillStyle(0x368f8b, 1)
progressBar.fillRect(width / 2 - 180 + 10, height / 2 + 10, 300 * value, 30)
})
scene.load.on(Phaser.Loader.Events.COMPLETE, function () {
progressBar.destroy()
progressBox.destroy()
loadingText.destroy()
return true
})
}

View File

@ -1,103 +1,96 @@
import type { TextureData } from '@/application/types' import { Assets } from '@/application/assets'
import { SpriteStorage } from '@/storage/storages' import config from '@/application/config'
import { TextureStorage } from '@/storage/textureStorage' import type { AssetDataT, HttpResponse, Sprite, SpriteAction } from '@/application/types'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
const textureLoadingPromises = new Map<string, Promise<boolean>>() const textureLoadingPromises = new Map<string, Promise<boolean>>()
export async function loadTexture(scene: Phaser.Scene, textureData: TextureData): Promise<boolean> { export async function loadTexture(scene: Phaser.Scene, assetData: AssetDataT): Promise<boolean> {
const gameStore = useGameStore() const gameStore = useGameStore()
const textureStorage = new TextureStorage() const assetStorage = new Assets()
// Check if the texture is already loaded in Phaser // Check if the texture is already loaded in Phaser
if (gameStore.game.loadedTextures.find((texture) => texture === textureData.key)) { if (gameStore.game.loadedAssets.find((asset) => asset.key === assetData.key)) {
return true return Promise.resolve(true)
} }
// If there's already a loading promise for this texture, return it // If there's already a loading promise for this texture, return it
if (textureLoadingPromises.has(textureData.key)) { if (textureLoadingPromises.has(assetData.key)) {
return await textureLoadingPromises.get(textureData.key)! return await textureLoadingPromises.get(assetData.key)!
} }
// Create new loading promise // Create new loading promise
const loadingPromise = (async () => { const loadingPromise = (async () => {
// Check if the asset is already cached // Check if the asset is already cached
let texture = await textureStorage.get(textureData.key) let asset = await assetStorage.get(assetData.key)
// If asset is not found, download it // If asset is not found, download it
if (!texture) { if (!asset) {
await textureStorage.download(textureData) await assetStorage.download(assetData)
texture = await textureStorage.get(textureData.key) asset = await assetStorage.get(assetData.key)
} }
// If asset is found, add it to the scene // If asset is found, add it to the scene
if (texture) { if (asset) {
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
// Remove existing texture if it exists // Remove existing texture if it exists
if (scene.textures.exists(texture.key)) { if (scene.textures.exists(asset.key)) {
scene.textures.remove(texture.key) scene.textures.remove(asset.key)
} }
scene.textures.addBase64(texture.key, texture.data) scene.textures.addBase64(asset.key, asset.data)
scene.textures.once(`addtexture-${texture.key}`, () => { scene.textures.once(`addtexture-${asset.key}`, () => {
gameStore.game.loadedTextures.push(textureData.key) gameStore.game.loadedAssets.push(assetData)
textureLoadingPromises.delete(textureData.key) // Clean up the promise textureLoadingPromises.delete(assetData.key) // Clean up the promise
resolve(true) resolve(true)
}) })
}) })
} }
textureLoadingPromises.delete(textureData.key) // Clean up the promise textureLoadingPromises.delete(assetData.key) // Clean up the promise
return false return Promise.resolve(false)
})() })()
// Store the loading promise // Store the loading promise
textureLoadingPromises.set(textureData.key, loadingPromise) textureLoadingPromises.set(assetData.key, loadingPromise)
return loadingPromise return loadingPromise
} }
export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string) { export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string) {
if (!sprite_id) return false if (!sprite_id) return
// @TODO: Fix this // @TODO: Fix this
const spriteStorage = new SpriteStorage() const sprite_actions: HttpResponse<any[]> = await fetch(config.server_endpoint + '/assets/list_sprite_actions/' + sprite_id).then((response) => response.json())
const sprite = await spriteStorage.get(sprite_id)
if (!sprite) { for await (const sprite_action of sprite_actions.data ?? []) {
console.error('Failed to load sprite:', sprite_id)
return false
}
for await (const sprite_action of sprite.spriteActions) {
const key = sprite.id + '-' + sprite_action.action
await loadTexture(scene, { await loadTexture(scene, {
key, key: sprite_action.key,
data: '/textures/sprites/' + sprite.id + '/' + sprite_action.action + '.png', data: sprite_action.data,
group: sprite_action.isAnimated ? 'sprite_animations' : 'sprites', group: sprite_action.isAnimated ? 'sprite_animations' : 'sprites',
updatedAt: sprite_action.updatedAt, updatedAt: sprite_action.updatedAt,
originX: sprite_action.originX, originX: sprite_action.originX ?? 0,
originY: sprite_action.originY, originY: sprite_action.originY ?? 0,
isAnimated: sprite_action.isAnimated, isAnimated: sprite_action.isAnimated,
frameWidth: sprite_action.frameWidth, frameWidth: sprite_action.frameWidth,
frameHeight: sprite_action.frameHeight, frameHeight: sprite_action.frameHeight,
frameRate: sprite_action.frameRate frameRate: sprite_action.frameRate
} as TextureData) } as AssetDataT)
// If the sprite is not animated, skip // If the sprite is not animated, skip
if (!sprite_action.isAnimated) continue if (!sprite_action.isAnimated) continue
// Check if animation already exists // Check if animation already exists
if (scene.anims.get(key)) continue if (scene.anims.get(sprite_action.key)) continue
// Add the animation to the scene // Add the animation to the scene
const anim = scene.textures.get(key) const anim = scene.textures.get(sprite_action.key)
scene.textures.addSpriteSheet(key, anim, { frameWidth: sprite_action.frameWidth ?? 0, frameHeight: sprite_action.frameHeight ?? 0 }) scene.textures.addSpriteSheet(sprite_action.key, anim, { frameWidth: sprite_action.frameWidth ?? 0, frameHeight: sprite_action.frameHeight ?? 0 })
scene.anims.create({ scene.anims.create({
key: key, key: sprite_action.key,
frameRate: sprite_action.frameRate, frameRate: sprite_action.frameRate,
frames: scene.anims.generateFrameNumbers(key, { start: 0, end: sprite_action.frameCount! - 1 }), frames: scene.anims.generateFrameNumbers(sprite_action.key, { start: 0, end: sprite_action.frameCount! - 1 }),
repeat: -1 repeat: -1
}) })
} }
return true return Promise.resolve(true)
} }

View File

@ -1,142 +0,0 @@
import config from '@/application/config'
import type { HttpResponse, TextureData, UUID } from '@/application/types'
import { unduplicateArray } from '@/application/utilities'
import { loadTexture } from '@/composables/gameComposable'
import { MapStorage, TileStorage } from '@/storage/storages'
import Tilemap = Phaser.Tilemaps.Tilemap
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
import Tileset = Phaser.Tilemaps.Tileset
import Tile = Phaser.Tilemaps.Tile
export function getTile(layer: TilemapLayer | Tilemap, positionX: number, positionY: number): Tile | null {
const tile = layer?.getTileAtWorldXY(positionX, positionY)
if (!tile) return null
return tile
}
export function tileToWorldXY(layer: TilemapLayer | Tilemap, positionX: number, positionY: number) {
const worldPoint = layer.tileToWorldXY(positionX, positionY)
if (!worldPoint) return { worldPositionX: 0, worldPositionY: 0 }
const worldPositionX = worldPoint.x + config.tile_size.height
const worldPositionY = worldPoint.y
return { worldPositionX, worldPositionY }
}
export function tileToWorldX(layer: TilemapLayer | Tilemap, positionX: number, positionY: number): number {
const worldPoint = layer.tileToWorldXY(positionX, positionY)
if (!worldPoint) return 0
return worldPoint.x + config.tile_size.width / 2
}
export function tileToWorldY(layer: TilemapLayer | Tilemap, positionX: number, positionY: number): number {
const worldPoint = layer.tileToWorldXY(positionX, positionY)
if (!worldPoint) return 0
return worldPoint.y + config.tile_size.height * 1.5
}
/**
* Can also be used to replace tiles
* @param map
* @param layer
* @param positionX
* @param positionY
* @param tileName
*/
export function placeTile(map: Tilemap, layer: TilemapLayer, positionX: number, positionY: number, tileName: string) {
let tileImg = map.getTileset(tileName) as Tileset
if (!tileImg) {
tileImg = map.getTileset('blank_tile') as Tileset
}
layer.putTileAt(tileImg.firstgid, positionX, positionY)
}
export function setLayerTiles(map: Tilemap, layer: TilemapLayer, tiles: string[][]) {
if (!tiles) return
tiles.forEach((row: string[], y: number) => {
row.forEach((tile: string, x: number) => {
placeTile(map, layer, x, y, tile)
})
})
}
export function createTileArray(width: number, height: number, tile: string = 'blank_tile') {
return Array.from({ length: height }, () => Array.from({ length: width }, () => tile))
}
export const calculateIsometricDepth = (positionX: number, positionY: number, width: number = 0, height: number = 0, isCharacter: boolean = false) => {
const baseDepth = positionX + positionY
if (isCharacter) {
return baseDepth // @TODO: Fix collision, this is a hack
}
return baseDepth + (width + height) / (2 * config.tile_size.width)
}
export function FlattenMapArray(tiles: string[][]) {
const normalArray = []
for (const row of tiles) {
normalArray.push(...row)
}
return normalArray
}
export async function loadMapTilesIntoScene(map_id: UUID, scene: Phaser.Scene) {
const tileStorage = new TileStorage()
const mapStorage = new MapStorage()
const map = await mapStorage.get(map_id)
if (!map) return
const tileArray = unduplicateArray(FlattenMapArray(map.tiles))
const tiles = await tileStorage.getByIds(tileArray)
// Load each tile into the scene
for (const tile of tiles) {
const textureData = {
key: tile.id,
data: '/textures/tiles/' + tile.id + '.png',
group: 'tiles',
updatedAt: tile.updatedAt
} as TextureData
await loadTexture(scene, textureData)
}
}
export async function loadTilesIntoScene(tileIds: string[], scene: Phaser.Scene) {
const tileStorage = new TileStorage()
const tiles = await tileStorage.getByIds(tileIds)
// Load each tile into the scene
for (const tile of tiles) {
const textureData = {
key: tile.id,
data: '/textures/tiles/' + tile.id + '.png',
group: 'tiles',
updatedAt: tile.updatedAt
} as TextureData
await loadTexture(scene, textureData)
}
}
export async function loadAllTilesIntoScene(scene: Phaser.Scene) {
const tileStorage = new TileStorage()
const tiles = await tileStorage.getAll()
// Load each tile into the scene
for (const tile of tiles) {
const textureData = {
key: tile.id,
data: '/textures/tiles/' + tile.id + '.png',
group: 'tiles',
updatedAt: tile.updatedAt
} as TextureData
await loadTexture(scene, textureData)
}
}

View File

@ -1,5 +1,5 @@
import config from '@/application/config' import config from '@/application/config'
import { getTile, tileToWorldXY } from '@/composables/mapComposable' import { getTile, tileToWorldXY } from '@/composables/zoneComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { ref, type Ref } from 'vue' import { ref, type Ref } from 'vue'
@ -10,17 +10,15 @@ export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilema
function updateWaypoint(worldX: number, worldY: number) { function updateWaypoint(worldX: number, worldY: number) {
const pointerTile = getTile(layer, worldX, worldY) const pointerTile = getTile(layer, worldX, worldY)
if (!pointerTile) { if (pointerTile) {
waypoint.value.visible = false
return
}
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y) const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
if (!worldPoint.worldPositionX || !worldPoint.worldPositionX) return
waypoint.value = { waypoint.value = {
visible: true, visible: true,
x: worldPoint.worldPositionX, x: worldPoint.positionX,
y: worldPoint.worldPositionY + config.tile_size.height + 15 y: worldPoint.positionY + config.tile_size.y + 15
}
} else {
waypoint.value.visible = false
} }
} }
@ -30,36 +28,35 @@ export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilema
} }
function handlePointerMove(pointer: Phaser.Input.Pointer) { function handlePointerMove(pointer: Phaser.Input.Pointer) {
updateWaypoint(pointer.worldX, pointer.worldY) const { worldX, worldY } = pointer
updateWaypoint(worldX, worldY)
if (!gameStore.game.isPlayerDraggingCamera) return
if (gameStore.game.isPlayerDraggingCamera) {
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y) const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
// If the distance is less than the drag threshold, return if (distance > dragThreshold) {
// We do this to prevent the camera from scrolling too quickly const { x, y, prevPosition } = pointer
if (distance <= dragThreshold) return const { scrollX, scrollY, zoom } = camera
camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom)
camera.setScroll(camera.scrollX - (pointer.x - pointer.prevPosition.x) / camera.zoom, camera.scrollY - (pointer.y - pointer.prevPosition.y) / camera.zoom) }
}
} }
function handlePointerUp(pointer: Phaser.Input.Pointer) { function handlePointerUp(pointer: Phaser.Input.Pointer) {
gameStore.setPlayerDraggingCamera(false)
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y) const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
// If the distance is greater than the drag threshold, return if (distance <= dragThreshold) {
// We do this to prevent the camera from scrolling too quickly
if (distance > dragThreshold) return
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY) const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
if (!pointerTile) return if (pointerTile) {
gameStore.connection?.emit('zone:character:move', {
gameStore.connection?.emit('map:character:move', {
positionX: pointerTile.x, positionX: pointerTile.x,
positionY: pointerTile.y positionY: pointerTile.y
}) })
} }
}
gameStore.setPlayerDraggingCamera(false)
}
const setupPointerHandlers = () => { const setupPointerHandlers = () => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown) scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)

View File

@ -1,83 +0,0 @@
import config from '@/application/config'
import { getTile, tileToWorldXY } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, ref, type Ref } from 'vue'
export function useMapEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const isMoveTool = computed(() => mapEditorStore.tool === 'move')
const pointerStartPosition = ref({ x: 0, y: 0 })
const dragThreshold = 5 // pixels
function updateWaypoint(worldX: number, worldY: number) {
const pointerTile = getTile(layer, worldX, worldY)
if (!pointerTile) {
waypoint.value.visible = false
return
}
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
if (!worldPoint.worldPositionX || !worldPoint.worldPositionX) return
waypoint.value = {
visible: true,
x: worldPoint.worldPositionX,
y: worldPoint.worldPositionY + config.tile_size.height + 15
}
}
function handlePointerDown(pointer: Phaser.Input.Pointer) {
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
if (isMoveTool.value || pointer.event.shiftKey) {
gameStore.setPlayerDraggingCamera(true)
}
}
function dragMap(pointer: Phaser.Input.Pointer) {
if (!gameStore.game.isPlayerDraggingCamera) return
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
if (distance <= dragThreshold) return
camera.setScroll(camera.scrollX - (pointer.x - pointer.prevPosition.x) / camera.zoom, camera.scrollY - (pointer.y - pointer.prevPosition.y) / camera.zoom)
}
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (isMoveTool.value || pointer.event.shiftKey) {
dragMap(pointer)
}
updateWaypoint(pointer.worldX, pointer.worldY)
}
function handlePointerUp(pointer: Phaser.Input.Pointer) {
gameStore.setPlayerDraggingCamera(false)
}
function handleZoom(pointer: Phaser.Input.Pointer) {
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
const deltaY = pointer.event.deltaY
const zoomLevel = camera.zoom - deltaY * 0.005
if (zoomLevel > 0 && zoomLevel < 3) {
camera.setZoom(zoomLevel)
}
}
}
const setupPointerHandlers = () => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
}
const cleanupPointerHandlers = () => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
}
return { setupPointerHandlers, cleanupPointerHandlers }
}

View File

@ -0,0 +1,64 @@
import config from '@/application/config'
import { getTile, tileToWorldXY } from '@/composables/zoneComposable'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { computed, type Ref } from 'vue'
export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const isMoveTool = computed(() => zoneEditorStore.tool === 'move')
function updateWaypoint(pointer: Phaser.Input.Pointer) {
const { x: px, y: py } = camera.getWorldPoint(pointer.x, pointer.y)
const pointerTile = getTile(layer, px, py)
if (pointerTile) {
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
waypoint.value = {
visible: true,
x: worldPoint.positionX,
y: worldPoint.positionY + config.tile_size.y + 15
}
} else {
waypoint.value.visible = false
}
}
function dragZone(pointer: Phaser.Input.Pointer) {
if (gameStore.game.isPlayerDraggingCamera) {
const { x, y, prevPosition } = pointer
const { scrollX, scrollY, zoom } = camera
camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom)
}
}
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (isMoveTool.value || pointer.event.shiftKey) {
dragZone(pointer)
}
updateWaypoint(pointer)
}
function handleZoom(pointer: Phaser.Input.Pointer) {
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
const deltaY = pointer.event.deltaY
const zoomLevel = camera.zoom - deltaY * 0.005
if (zoomLevel > 0 && zoomLevel < 3) {
camera.setZoom(zoomLevel)
}
}
}
const setupPointerHandlers = () => {
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
}
const cleanupPointerHandlers = () => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
}
return { setupPointerHandlers, cleanupPointerHandlers }
}

View File

@ -1,5 +1,5 @@
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore' import { useZoneStore } from '@/stores/zoneStore'
export function useCameraControls(scene: Phaser.Scene) { export function useCameraControls(scene: Phaser.Scene) {
const gameStore = useGameStore() const gameStore = useGameStore()

View File

@ -1,20 +1,20 @@
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { computed, watch, type Ref } from 'vue' import { computed, watch, type Ref } from 'vue'
import { useGamePointerHandlers } from './pointerHandlers/useGamePointerHandlers' import { useGamePointerHandlers } from './pointerHandlers/useGamePointerHandlers'
import { useMapEditorPointerHandlers } from './pointerHandlers/useMapEditorPointerHandlers' import { useZoneEditorPointerHandlers } from './pointerHandlers/useZoneEditorPointerHandlers'
export function usePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) { export function usePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const mapEditorStore = useMapEditorStore() const zoneEditorStore = useZoneEditorStore()
const gameHandlers = useGamePointerHandlers(scene, layer, waypoint, camera) const gameHandlers = useGamePointerHandlers(scene, layer, waypoint, camera)
const mapEditorHandlers = useMapEditorPointerHandlers(scene, layer, waypoint, camera) const zoneEditorHandlers = useZoneEditorPointerHandlers(scene, layer, waypoint, camera)
const currentHandlers = computed(() => (mapEditorStore.active ? mapEditorHandlers : gameHandlers)) const currentHandlers = computed(() => (zoneEditorStore.active ? zoneEditorHandlers : gameHandlers))
const setupPointerHandlers = () => currentHandlers.value.setupPointerHandlers() const setupPointerHandlers = () => currentHandlers.value.setupPointerHandlers()
const cleanupPointerHandlers = () => currentHandlers.value.cleanupPointerHandlers() const cleanupPointerHandlers = () => currentHandlers.value.cleanupPointerHandlers()
watch( watch(
() => mapEditorStore.active, () => zoneEditorStore.active,
() => { () => {
cleanupPointerHandlers() cleanupPointerHandlers()
setupPointerHandlers() setupPointerHandlers()

View File

@ -0,0 +1,96 @@
import config from '@/application/config'
import type { AssetDataT, HttpResponse, Zone as ZoneT } from '@/application/types'
import { loadTexture } from '@/composables/gameComposable'
import Tilemap = Phaser.Tilemaps.Tilemap
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
import Tileset = Phaser.Tilemaps.Tileset
import Tile = Phaser.Tilemaps.Tile
export function getTile(layer: TilemapLayer | Tilemap, x: number, y: number): Tile | undefined {
const tile = layer.getTileAtWorldXY(x, y)
if (!tile) return undefined
return tile
}
export function tileToWorldXY(layer: TilemapLayer | Tilemap, pos_x: number, pos_y: number) {
const worldPoint = layer.tileToWorldXY(pos_x, pos_y)
if (!worldPoint) return { positionX: 0, positionY: 0 }
const positionX = worldPoint.x + config.tile_size.y
const positionY = worldPoint.y
return { positionX, positionY }
}
export function tileToWorldX(layer: TilemapLayer | Tilemap, pos_x: number, pos_y: number): number {
const worldPoint = layer.tileToWorldXY(pos_x, pos_y)
if (!worldPoint) return 0
return worldPoint.x + config.tile_size.x / 2
}
export function tileToWorldY(layer: TilemapLayer | Tilemap, pos_x: number, pos_y: number): number {
const worldPoint = layer.tileToWorldXY(pos_x, pos_y)
if (!worldPoint) return 0
return worldPoint.y + config.tile_size.y * 1.5
}
/**
* Can also be used to replace tiles
* @param zone
* @param layer
* @param x
* @param y
* @param tileName
*/
export function placeTile(zone: Tilemap, layer: TilemapLayer, x: number, y: number, tileName: string) {
let tileImg = zone.getTileset(tileName) as Tileset
if (!tileImg) {
tileImg = zone.getTileset('blank_tile') as Tileset
}
layer.putTileAt(tileImg.firstgid, x, y)
}
export function setLayerTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][]) {
if (!tiles) return
tiles.forEach((row: string[], y: number) => {
row.forEach((tile: string, x: number) => {
placeTile(zone, layer, x, y, tile)
})
})
}
export function createTileArray(width: number, height: number, tile: string = 'blank_tile') {
return Array.from({ length: height }, () => Array.from({ length: width }, () => tile))
}
export const calculateIsometricDepth = (x: number, y: number, width: number = 0, height: number = 0, isCharacter: boolean = false) => {
const baseDepth = x + y
if (isCharacter) {
return baseDepth // @TODO: Fix collision, this is a hack
}
return baseDepth + (width + height) / (2 * config.tile_size.x)
}
export function FlattenZoneArray(tiles: string[][]) {
const normalArray = []
for (const row of tiles) {
normalArray.push(...row)
}
return normalArray
}
export async function loadZoneTilesIntoScene(zone_id: number, scene: Phaser.Scene) {
// Fetch the list of tiles from the server
const tileArray: HttpResponse<AssetDataT[]> = await fetch(config.server_endpoint + '/assets/list_tiles/' + zone_id).then((response) => response.json())
// Load each tile into the scene
for (const tile of tileArray.data ?? []) {
await loadTexture(scene, tile)
}
}

Some files were not shown because too many files have changed in this diff Show More