add offset to multi avatar layer
This commit is contained in:
@@ -8,6 +8,7 @@ import { useSharedTicker } from './composables/useSharedTicker'
|
||||
const props = defineProps<{
|
||||
avatar: AvatarSummary
|
||||
tileSize: number
|
||||
offset?: { x: number; y: number }
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -21,8 +22,12 @@ 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)
|
||||
// Initial position includes offset immediately to avoid "jumping" on spawn if possible,
|
||||
// but props.offset might be undefined initially.
|
||||
const initialOffsetX = props.offset?.x ?? 0
|
||||
const initialOffsetY = props.offset?.y ?? 0
|
||||
const currentX = ref((props.avatar.x + initialOffsetX) * props.tileSize + props.tileSize / 2)
|
||||
const currentY = ref((props.avatar.y + initialOffsetY) * props.tileSize + props.tileSize / 2)
|
||||
|
||||
// Watch for prop updates (server ticks)
|
||||
watch(() => [props.avatar.x, props.avatar.y], ([newX, newY]) => {
|
||||
@@ -31,8 +36,11 @@ watch(() => [props.avatar.x, props.avatar.y], ([newX, newY]) => {
|
||||
})
|
||||
|
||||
useSharedTicker((delta) => {
|
||||
const destX = targetX.value * props.tileSize + props.tileSize / 2
|
||||
const destY = targetY.value * props.tileSize + props.tileSize / 2
|
||||
const offsetX = props.offset?.x ?? 0
|
||||
const offsetY = props.offset?.y ?? 0
|
||||
|
||||
const destX = (targetX.value + offsetX) * props.tileSize + props.tileSize / 2
|
||||
const destY = (targetY.value + offsetY) * props.tileSize + props.tileSize / 2
|
||||
|
||||
const speed = 0.1 * delta
|
||||
|
||||
@@ -90,8 +98,8 @@ const nameStyle = {
|
||||
fontFamily: '"Microsoft YaHei", sans-serif',
|
||||
fontSize: 42, // Increased from 36
|
||||
fontWeight: 'bold',
|
||||
fill: 0xffffff,
|
||||
stroke: { color: 0x000000, width: 6 }, // Thicker stroke
|
||||
fill: '#ffffff',
|
||||
stroke: { color: '#000000', width: 6 }, // Thicker stroke
|
||||
align: 'center',
|
||||
dropShadow: {
|
||||
color: '#000000',
|
||||
@@ -100,7 +108,7 @@ const nameStyle = {
|
||||
distance: 2,
|
||||
alpha: 0.8
|
||||
}
|
||||
}
|
||||
} as any
|
||||
|
||||
function handlePointerTap() {
|
||||
emit('select', {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useWorldStore } from '../../stores/world'
|
||||
import AnimatedAvatar from './AnimatedAvatar.vue'
|
||||
import { computed } from 'vue'
|
||||
import { calculateVisualOffsets } from './utils/avatarLayout'
|
||||
|
||||
const worldStore = useWorldStore()
|
||||
const TILE_SIZE = 64
|
||||
@@ -9,6 +11,10 @@ const emit = defineEmits<{
|
||||
(e: 'avatarSelected', payload: { type: 'avatar'; id: string; name?: string }): void
|
||||
}>()
|
||||
|
||||
const avatarOffsets = computed(() => {
|
||||
return calculateVisualOffsets(worldStore.avatarList)
|
||||
})
|
||||
|
||||
function handleAvatarSelect(payload: { type: 'avatar'; id: string; name?: string }) {
|
||||
emit('avatarSelected', payload)
|
||||
}
|
||||
@@ -21,6 +27,7 @@ function handleAvatarSelect(payload: { type: 'avatar'; id: string; name?: string
|
||||
:key="avatar.id"
|
||||
:avatar="avatar"
|
||||
:tile-size="TILE_SIZE"
|
||||
:offset="avatarOffsets.get(avatar.id)"
|
||||
@select="handleAvatarSelect"
|
||||
/>
|
||||
</container>
|
||||
|
||||
59
web/src/components/game/utils/avatarLayout.ts
Normal file
59
web/src/components/game/utils/avatarLayout.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
export interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface PositionedObject {
|
||||
id: string
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算角色在格子内的视觉偏移量,避免重叠
|
||||
* @param items 包含坐标的对象列表
|
||||
* @param radius 偏移半径(相对于格子大小的比例,0.25 表示 1/4 格子宽)
|
||||
*/
|
||||
export function calculateVisualOffsets<T extends PositionedObject>(
|
||||
items: T[],
|
||||
radius: number = 0.25
|
||||
): Map<string, Point> {
|
||||
const groups = new Map<string, T[]>()
|
||||
const offsets = new Map<string, Point>()
|
||||
|
||||
// 1. 按坐标分组
|
||||
for (const item of items) {
|
||||
const key = `${item.x},${item.y}`
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, [])
|
||||
}
|
||||
groups.get(key)!.push(item)
|
||||
}
|
||||
|
||||
// 2. 计算每组的偏移
|
||||
for (const group of groups.values()) {
|
||||
const count = group.length
|
||||
|
||||
// 只有一个人时,不需要偏移,居中显示
|
||||
if (count <= 1) {
|
||||
offsets.set(group[0].id, { x: 0, y: 0 })
|
||||
continue
|
||||
}
|
||||
|
||||
// 多人时,按圆形分布
|
||||
// 无论多少人,都均匀分布在圆周上
|
||||
// 起始角度 -PI/2 (即正上方),让排列更符合直觉
|
||||
const angleStep = (Math.PI * 2) / count
|
||||
|
||||
group.forEach((item, index) => {
|
||||
const angle = index * angleStep - Math.PI / 2
|
||||
offsets.set(item.id, {
|
||||
x: Math.cos(angle) * radius,
|
||||
y: Math.sin(angle) * radius
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return offsets
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user