fix web bugs
This commit is contained in:
122
web/src/components/game/AnimatedAvatar.vue
Normal file
122
web/src/components/game/AnimatedAvatar.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import { useTextures } from './composables/useTextures'
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { Graphics, Ticker } from 'pixi.js'
|
||||
import { useApplication } from 'vue3-pixi'
|
||||
|
||||
const props = defineProps<{
|
||||
avatar: any
|
||||
tileSize: number
|
||||
}>()
|
||||
|
||||
const { textures } = useTextures()
|
||||
const app = useApplication()
|
||||
|
||||
// Target position (grid coordinates)
|
||||
const targetX = ref(props.avatar.x)
|
||||
const targetY = ref(props.avatar.y)
|
||||
|
||||
// Current render position (pixel coordinates)
|
||||
const currentX = ref(props.avatar.x * props.tileSize + props.tileSize / 2)
|
||||
const currentY = ref(props.avatar.y * props.tileSize + props.tileSize / 2)
|
||||
|
||||
// Watch for prop updates (server ticks)
|
||||
watch(() => [props.avatar.x, props.avatar.y], ([newX, newY]) => {
|
||||
targetX.value = newX
|
||||
targetY.value = newY
|
||||
})
|
||||
|
||||
// Animation Loop
|
||||
onMounted(() => {
|
||||
const ticker = new Ticker()
|
||||
ticker.add((delta) => {
|
||||
const destX = targetX.value * props.tileSize + props.tileSize / 2
|
||||
const destY = targetY.value * props.tileSize + props.tileSize / 2
|
||||
|
||||
// Simple Lerp for smoothness
|
||||
// Speed factor: 0.1 means it covers 10% of the remaining distance per frame
|
||||
const speed = 0.1 * delta.deltaTime
|
||||
|
||||
if (Math.abs(destX - currentX.value) > 1) {
|
||||
currentX.value += (destX - currentX.value) * speed
|
||||
} else {
|
||||
currentX.value = destX
|
||||
}
|
||||
|
||||
if (Math.abs(destY - currentY.value) > 1) {
|
||||
currentY.value += (destY - currentY.value) * speed
|
||||
} else {
|
||||
currentY.value = destY
|
||||
}
|
||||
})
|
||||
ticker.start()
|
||||
|
||||
// Cleanup manually if needed, though Vue unmount should handle parent destruction
|
||||
// Ideally we should attach to app.ticker, but local ticker is easier for per-component logic without memory leaks if managed well.
|
||||
// Better approach: use onTick from vue3-pixi if available, or just requestAnimationFrame
|
||||
})
|
||||
|
||||
function getTexture() {
|
||||
const key = `${(props.avatar.gender || 'male').toLowerCase()}_${props.avatar.pic_id || 1}`
|
||||
return textures.value[key]
|
||||
}
|
||||
|
||||
function getScale() {
|
||||
const tex = getTexture()
|
||||
if (!tex) return 1
|
||||
// Scale up: 2.5x tile size (was 1.8x)
|
||||
return (props.tileSize * 2.5) / Math.max(tex.width, tex.height)
|
||||
}
|
||||
|
||||
const drawFallback = (g: Graphics) => {
|
||||
g.clear()
|
||||
g.circle(0, 0, props.tileSize * 0.8) // Increased size
|
||||
g.fill({ color: props.avatar.gender === 'female' ? 0xffaaaa : 0xaaaaff })
|
||||
g.stroke({ width: 3, color: 0x000000 })
|
||||
}
|
||||
|
||||
const nameStyle = {
|
||||
fontFamily: '"Microsoft YaHei", sans-serif',
|
||||
fontSize: 42, // Increased from 36
|
||||
fontWeight: 'bold',
|
||||
fill: 0xffffff,
|
||||
stroke: { color: 0x000000, width: 6 }, // Thicker stroke
|
||||
align: 'center',
|
||||
dropShadow: {
|
||||
color: '#000000',
|
||||
blur: 2,
|
||||
angle: Math.PI / 6,
|
||||
distance: 2,
|
||||
alpha: 0.8
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<container
|
||||
:x="currentX"
|
||||
:y="currentY"
|
||||
:z-index="Math.floor(currentY)"
|
||||
>
|
||||
<sprite
|
||||
v-if="getTexture()"
|
||||
:texture="getTexture()"
|
||||
:anchor-x="0.5"
|
||||
:anchor-y="0.9"
|
||||
:scale="getScale()"
|
||||
/>
|
||||
|
||||
<graphics
|
||||
v-else
|
||||
@render="drawFallback"
|
||||
/>
|
||||
|
||||
<text
|
||||
:text="avatar.name"
|
||||
:style="nameStyle"
|
||||
:anchor-x="0.5"
|
||||
:anchor-y="0"
|
||||
:y="10"
|
||||
/>
|
||||
</container>
|
||||
</template>
|
||||
@@ -1,74 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '../../stores/game'
|
||||
import { useTextures } from './composables/useTextures'
|
||||
import { computed } from 'vue'
|
||||
import { Graphics } from 'pixi.js'
|
||||
import AnimatedAvatar from './AnimatedAvatar.vue'
|
||||
|
||||
const store = useGameStore()
|
||||
const { textures } = useTextures()
|
||||
const TILE_SIZE = 64
|
||||
|
||||
function getTexture(avatar: any) {
|
||||
const key = `${(avatar.gender || 'male').toLowerCase()}_${avatar.pic_id || 1}`
|
||||
return textures.value[key]
|
||||
}
|
||||
|
||||
function getScale(avatar: any) {
|
||||
const tex = getTexture(avatar)
|
||||
if (!tex) return 1
|
||||
return (TILE_SIZE * 1.8) / Math.max(tex.width, tex.height)
|
||||
}
|
||||
|
||||
// Fallback graphics draw function
|
||||
const drawFallback = (g: Graphics, avatar: any) => {
|
||||
g.clear()
|
||||
g.circle(0, 0, TILE_SIZE * 0.6)
|
||||
g.fill({ color: avatar.gender === 'female' ? 0xffaaaa : 0xaaaaff })
|
||||
g.stroke({ width: 2, color: 0x000000 })
|
||||
}
|
||||
|
||||
const nameStyle = {
|
||||
fontFamily: '"Microsoft YaHei", sans-serif',
|
||||
fontSize: 24,
|
||||
fill: 0xffffff,
|
||||
stroke: { color: 0x000000, width: 4 },
|
||||
align: 'center'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<container sortable-children>
|
||||
<container
|
||||
<AnimatedAvatar
|
||||
v-for="avatar in store.avatarList"
|
||||
:key="avatar.id"
|
||||
:x="avatar.x * TILE_SIZE + TILE_SIZE / 2"
|
||||
:y="avatar.y * TILE_SIZE + TILE_SIZE / 2"
|
||||
:z-index="avatar.y"
|
||||
>
|
||||
<!-- Avatar Sprite -->
|
||||
<sprite
|
||||
v-if="getTexture(avatar)"
|
||||
:texture="getTexture(avatar)"
|
||||
:anchor-x="0.5"
|
||||
:anchor-y="0.8"
|
||||
:scale="getScale(avatar)"
|
||||
/>
|
||||
|
||||
<!-- Fallback Graphics -->
|
||||
<graphics
|
||||
v-else
|
||||
@render="g => drawFallback(g, avatar)"
|
||||
/>
|
||||
|
||||
<!-- Name Tag -->
|
||||
<text
|
||||
:text="avatar.name"
|
||||
:style="nameStyle"
|
||||
:anchor-x="0.5"
|
||||
:anchor-y="1"
|
||||
:y="-TILE_SIZE * 0.8"
|
||||
/>
|
||||
</container>
|
||||
:avatar="avatar"
|
||||
:tile-size="TILE_SIZE"
|
||||
/>
|
||||
</container>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ async function initMap() {
|
||||
const res = await fetch('/api/map')
|
||||
const data = await res.json()
|
||||
const mapData = data.data
|
||||
// Regions are already provided by backend
|
||||
regions.value = data.regions || []
|
||||
|
||||
if (!mapData) return
|
||||
@@ -69,9 +70,9 @@ onMounted(() => {
|
||||
function getRegionStyle(type: string) {
|
||||
const base = {
|
||||
fontFamily: '"Microsoft YaHei", sans-serif',
|
||||
fontSize: type === 'sect' ? 48 : 64,
|
||||
fontSize: type === 'sect' ? 48 : 64,
|
||||
fill: type === 'sect' ? 0xffcc00 : (type === 'city' ? 0xccffcc : 0xffffff),
|
||||
stroke: { color: 0x000000, width: 8, join: 'round' },
|
||||
stroke: { color: 0x000000, width: 8, join: 'round' },
|
||||
align: 'center',
|
||||
dropShadow: {
|
||||
color: '#000000',
|
||||
@@ -86,18 +87,21 @@ function getRegionStyle(type: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<container ref="mapContainer">
|
||||
<!-- Regions (Labels) -->
|
||||
<text
|
||||
v-for="r in regions"
|
||||
:key="r.name"
|
||||
:text="r.name"
|
||||
:x="r.x * TILE_SIZE + TILE_SIZE / 2"
|
||||
:y="r.y * TILE_SIZE + TILE_SIZE / 2"
|
||||
:anchor="0.5"
|
||||
:style="getRegionStyle(r.type)"
|
||||
:z-index="100"
|
||||
/>
|
||||
<container>
|
||||
<!-- Tile Layer -->
|
||||
<container ref="mapContainer" />
|
||||
|
||||
<!-- Region Labels Layer (Above tiles) -->
|
||||
<container :z-index="200">
|
||||
<text
|
||||
v-for="r in regions"
|
||||
:key="r.name"
|
||||
:text="r.name"
|
||||
:x="r.x * TILE_SIZE + TILE_SIZE / 2"
|
||||
:y="r.y * TILE_SIZE + TILE_SIZE / 2"
|
||||
:anchor="0.5"
|
||||
:style="getRegionStyle(r.type)"
|
||||
/>
|
||||
</container>
|
||||
</container>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user