Moved game components into a new folder, working proof of concept hair customisation
This commit is contained in:
221
src/components/game/character/Character.vue
Normal file
221
src/components/game/character/Character.vue
Normal file
@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<!-- Chat bubble -->
|
||||
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
|
||||
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
|
||||
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
|
||||
</Container>
|
||||
<!-- Character name and health -->
|
||||
<Container :depth="999" :x="currentX" :y="currentY">
|
||||
<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="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
||||
</Container>
|
||||
<!-- Character sprite -->
|
||||
<Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY">
|
||||
<!-- HAIR-->
|
||||
<Image :origin-y="4.2" :depth="1" :texture="props.zoneCharacter.character.hair?.spriteId + '-front'" />
|
||||
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" :flipY="false" />
|
||||
</Container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import config from '@/config'
|
||||
import { type Sprite as SpriteT, type ZoneCharacter } from '@/types'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { Container, Image, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
|
||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||
|
||||
enum Direction {
|
||||
POSITIVE,
|
||||
NEGATIVE,
|
||||
UNCHANGED
|
||||
}
|
||||
const props = defineProps<{
|
||||
layer: Phaser.Tilemaps.TilemapLayer
|
||||
zoneCharacter: ZoneCharacter
|
||||
}>()
|
||||
|
||||
const charChatContainer = refObj<Phaser.GameObjects.Container>()
|
||||
const charContainer = refObj<Phaser.GameObjects.Container>()
|
||||
const charSprite = refObj<Phaser.GameObjects.Sprite>()
|
||||
|
||||
const game = useGame()
|
||||
const gameStore = useGameStore()
|
||||
const zoneStore = useZoneStore()
|
||||
const scene = useScene()
|
||||
|
||||
const currentX = ref(0)
|
||||
const currentY = ref(0)
|
||||
const isometricDepth = ref(1)
|
||||
const isInitialPosition = ref(true)
|
||||
const tween = ref<Phaser.Tweens.Tween | null>(null)
|
||||
|
||||
const updateIsometricDepth = (x: number, y: number) => {
|
||||
isometricDepth.value = calculateIsometricDepth(x, y, 28, 94, true)
|
||||
}
|
||||
|
||||
const updatePosition = (x: number, y: number, direction: Direction) => {
|
||||
const targetX = tileToWorldX(props.layer, x, y)
|
||||
const targetY = tileToWorldY(props.layer, x, y)
|
||||
|
||||
if (isInitialPosition.value) {
|
||||
currentX.value = targetX
|
||||
currentY.value = targetY
|
||||
isInitialPosition.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (tween.value?.isPlaying()) {
|
||||
tween.value.stop()
|
||||
}
|
||||
|
||||
const distance = Math.sqrt(Math.pow(targetX - currentX.value, 2) + Math.pow(targetY - currentY.value, 2))
|
||||
|
||||
if (distance >= config.tile_size.x / 1.1) {
|
||||
currentX.value = targetX
|
||||
currentY.value = targetY
|
||||
return
|
||||
}
|
||||
|
||||
const duration = distance * 6
|
||||
|
||||
tween.value = props.layer.scene.tweens.add({
|
||||
targets: { x: currentX.value, y: currentY.value },
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
duration,
|
||||
ease: 'Linear',
|
||||
onStart: () => {
|
||||
if (direction === Direction.POSITIVE) {
|
||||
updateIsometricDepth(x, y)
|
||||
}
|
||||
},
|
||||
onUpdate: (tween) => {
|
||||
currentX.value = tween.targets[0].x
|
||||
currentY.value = tween.targets[0].y
|
||||
},
|
||||
onComplete: () => {
|
||||
if (direction === Direction.NEGATIVE) {
|
||||
updateIsometricDepth(x, y)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const calcDirection = (oldX: number, oldY: number, newX: number, newY: number): Direction => {
|
||||
if (newY < oldY || newX < oldX) return Direction.NEGATIVE
|
||||
if (newX > oldX || newY > oldY) return Direction.POSITIVE
|
||||
return Direction.UNCHANGED
|
||||
}
|
||||
|
||||
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
|
||||
|
||||
const charTexture = computed(() => {
|
||||
const { rotation, characterType } = props.zoneCharacter.character
|
||||
const spriteId = characterType?.sprite?.id ?? 'idle_right_down'
|
||||
const action = props.zoneCharacter.isMoving ? 'walk' : 'idle'
|
||||
const direction = [0, 6].includes(rotation) ? 'left_up' : 'right_down'
|
||||
|
||||
return `${spriteId}-${action}_${direction}`
|
||||
})
|
||||
|
||||
const updateSprite = () => {
|
||||
if (props.zoneCharacter.isMoving) {
|
||||
charSprite.value!.anims.play(charTexture.value, true)
|
||||
return
|
||||
}
|
||||
|
||||
charSprite.value!.anims.stop()
|
||||
charSprite.value!.setFrame(0)
|
||||
charSprite.value!.setTexture(charTexture.value)
|
||||
}
|
||||
|
||||
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
||||
container.setName(`${props.zoneCharacter.character.name}_chatBubble`)
|
||||
}
|
||||
|
||||
const createChatText = (text: Phaser.GameObjects.Text) => {
|
||||
text.setName(`${props.zoneCharacter.character.name}_chatText`)
|
||||
text.setFontSize(13)
|
||||
text.setFontFamily('Arial')
|
||||
text.setOrigin(0.5, 10.9)
|
||||
|
||||
// Fix text alignment on Windows and Android
|
||||
if (game.device.os.windows || game.device.os.android) {
|
||||
text.setOrigin(0.5, 9.75)
|
||||
|
||||
if (game.device.browser.firefox) {
|
||||
text.setOrigin(0.5, 10.9)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createNicknameText = (text: Phaser.GameObjects.Text) => {
|
||||
text.setFontSize(13)
|
||||
text.setFontFamily('Arial')
|
||||
text.setOrigin(0.5, 9)
|
||||
|
||||
// Fix text alignment on Windows and Android
|
||||
if (game.device.os.windows || game.device.os.android) {
|
||||
text.setOrigin(0.5, 8)
|
||||
|
||||
if (game.device.browser.firefox) {
|
||||
text.setOrigin(0.5, 9)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.zoneCharacter.character,
|
||||
(newChar, oldChar) => {
|
||||
if (!newChar) return
|
||||
|
||||
if (!oldChar || newChar.positionX !== oldChar.positionX || newChar.positionY !== oldChar.positionY) {
|
||||
const direction = !oldChar ? Direction.POSITIVE : calcDirection(oldChar.positionX, oldChar.positionY, newChar.positionX, newChar.positionY)
|
||||
updatePosition(newChar.positionX, newChar.positionY, direction)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(() => props.zoneCharacter, updateSprite)
|
||||
|
||||
|
||||
// Hair demo
|
||||
loadSpriteTextures(scene, props.zoneCharacter.character.hair?.sprite as SpriteT)
|
||||
.then(() => {})
|
||||
.catch((error) => {
|
||||
console.error('Error loading texture:', error)
|
||||
})
|
||||
|
||||
loadSpriteTextures(scene, props.zoneCharacter.character.characterType?.sprite as SpriteT)
|
||||
.then(() => {
|
||||
charSprite.value!.setTexture(charTexture.value)
|
||||
charSprite.value!.setFlipX(isFlippedX.value)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error loading texture:', error)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
charChatContainer.value!.setName(`${props.zoneCharacter.character!.name}_chatContainer`)
|
||||
charChatContainer.value!.setVisible(false)
|
||||
charContainer.value!.setName(props.zoneCharacter.character!.name)
|
||||
|
||||
if (props.zoneCharacter.character.id === gameStore.character!.id) {
|
||||
zoneStore.setCharacterLoaded(true)
|
||||
|
||||
// #146 : Set camera position to character, need to be improved still
|
||||
scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
|
||||
scene.cameras.main.stopFollow()
|
||||
}
|
||||
|
||||
updatePosition(props.zoneCharacter.character.positionX, props.zoneCharacter.character.positionY, props.zoneCharacter.character.rotation)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
tween.value?.stop()
|
||||
})
|
||||
</script>
|
14
src/components/game/zone/Characters.vue
Normal file
14
src/components/game/zone/Characters.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<Character v-for="item in zoneStore.characters" :key="item.character.id" :layer="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>
|
72
src/components/game/zone/Zone.vue
Normal file
72
src/components/game/zone/Zone.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<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 { ref, onUnmounted } from 'vue'
|
||||
import { useScene } from 'phavuer'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { loadZoneTilesIntoScene } from '@/composables/zoneComposable'
|
||||
import type { Zone as ZoneT, ZoneCharacter } from '@/types'
|
||||
import ZoneTiles from '@/components/game/zone/ZoneTiles.vue'
|
||||
import ZoneObjects from '@/components/game/zone/ZoneObjects.vue'
|
||||
import Characters from '@/components/game/zone/Characters.vue'
|
||||
|
||||
const scene = useScene()
|
||||
const gameStore = useGameStore()
|
||||
const zoneStore = useZoneStore()
|
||||
|
||||
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
||||
|
||||
type zoneLoadData = {
|
||||
zone: ZoneT
|
||||
characters: ZoneCharacter[]
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => {
|
||||
/**
|
||||
* @TODO : Update character via global event server-side, remove this and listen for it somewhere not here
|
||||
*/
|
||||
gameStore.setCharacter({
|
||||
...gameStore.character!,
|
||||
zoneId: data.zone.id
|
||||
})
|
||||
|
||||
await loadZoneTilesIntoScene(data.zone, scene)
|
||||
zoneStore.setZone(data.zone)
|
||||
zoneStore.setCharacters(data.characters)
|
||||
})
|
||||
|
||||
gameStore.connection!.on('zone:character:join', async (data: ZoneCharacter) => {
|
||||
// If data is from the current user, don't add it to the store
|
||||
if (data.character.id === gameStore.character?.id) return
|
||||
zoneStore.addCharacter(data)
|
||||
})
|
||||
|
||||
gameStore.connection!.on('zone:character:leave', (characterId: number) => {
|
||||
zoneStore.removeCharacter(characterId)
|
||||
})
|
||||
|
||||
gameStore.connection!.on('character:move', (data: ZoneCharacter) => {
|
||||
zoneStore.updateCharacter(data)
|
||||
})
|
||||
|
||||
// Emit zone:character:join event to server and wait for response, then set zone and characters
|
||||
gameStore!.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
|
||||
await loadZoneTilesIntoScene(response.zone, scene)
|
||||
zoneStore.setZone(response.zone)
|
||||
zoneStore.setCharacters(response.characters)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
zoneStore.reset()
|
||||
gameStore.connection!.off('zone:character:teleport')
|
||||
gameStore.connection!.off('zone:character:join')
|
||||
gameStore.connection!.off('zone:character:leave')
|
||||
gameStore.connection!.off('character:move')
|
||||
})
|
||||
</script>
|
14
src/components/game/zone/ZoneObjects.vue
Normal file
14
src/components/game/zone/ZoneObjects.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<ZoneObject v-for="zoneObject in zoneStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import ZoneObject from '@/components/game/zone/partials/ZoneObject.vue'
|
||||
|
||||
const zoneStore = useZoneStore()
|
||||
|
||||
defineProps<{
|
||||
tilemap: Phaser.Tilemaps.Tilemap
|
||||
}>()
|
||||
</script>
|
69
src/components/game/zone/ZoneTiles.vue
Normal file
69
src/components/game/zone/ZoneTiles.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<Controls :layer="tileLayer" :depth="0" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/config'
|
||||
import { useScene } from 'phavuer'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
|
||||
import Controls from '@/components/utilities/Controls.vue'
|
||||
import { unduplicateArray } from '@/utilities'
|
||||
|
||||
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>
|
41
src/components/game/zone/partials/ZoneObject.vue
Normal file
41
src/components/game/zone/partials/ZoneObject.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import { loadTexture } from '@/composables/gameComposable'
|
||||
import type { AssetDataT, ZoneObject } from '@/types'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
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>
|
Reference in New Issue
Block a user