Files
cultivation-world-simulator/web/src/components/game/MapLayer.vue
2025-12-03 00:10:10 +08:00

158 lines
4.1 KiB
Vue

<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import { Container, Sprite } from 'pixi.js'
import { useTextures } from './composables/useTextures'
import { useWorldStore } from '../../stores/world'
import type { RegionSummary } from '../../types/core'
const TILE_SIZE = 64
const mapContainer = ref<Container>()
const { textures, isLoaded, loadSectTexture } = useTextures()
const worldStore = useWorldStore()
const regionStyleCache = new Map<string, Record<string, unknown>>()
const emit = defineEmits<{
(e: 'mapLoaded', payload: { width: number, height: number }): void
(e: 'regionSelected', payload: { type: 'region'; id: string; name?: string }): void
}>()
onMounted(() => {
// Map data is loaded by worldStore.initialize() in App.vue or similar
if (isLoaded.value && worldStore.isLoaded) {
renderMap()
}
})
watch(
() => [isLoaded.value, worldStore.isLoaded],
([texturesReady, mapReady]) => {
if (texturesReady && mapReady) {
renderMap()
}
}
)
async function renderMap() {
if (!mapContainer.value || !worldStore.mapData.length) return
await preloadSectTextures()
mapContainer.value.removeChildren()
const rows = worldStore.mapData.length
const cols = worldStore.mapData[0]?.length ?? 0
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const type = worldStore.mapData[y][x]
let tex = textures.value[type]
if (type === 'SECT') {
tex = resolveSectTexture(x, y) ?? textures.value['CITY']
}
if (!tex) {
tex = textures.value['PLAIN']
}
if (!tex) continue
const sprite = new Sprite(tex)
sprite.x = x * TILE_SIZE
sprite.y = y * TILE_SIZE
// 开启像素取整,消除 Tile 之间的黑边缝隙
sprite.roundPixels = true
sprite.width = TILE_SIZE
sprite.height = TILE_SIZE
sprite.eventMode = 'none'
mapContainer.value.addChild(sprite)
}
}
emit('mapLoaded', {
width: cols * TILE_SIZE,
height: rows * TILE_SIZE
})
}
async function preloadSectTextures() {
const regions = Array.from(worldStore.regions.values());
const sectNames = Array.from(
new Set(
regions
.filter(region => region.type === 'sect' && region.sect_name)
.map(region => region.sect_name as string)
)
)
await Promise.all(sectNames.map(name => loadSectTexture(name)))
}
function resolveSectTexture(x: number, y: number) {
const regions = Array.from(worldStore.regions.values());
const region = regions.find(r =>
r.type === 'sect' && Math.abs(r.x - x) < 3 && Math.abs(r.y - y) < 3
)
if (region?.sect_name) {
const key = `SECT_${region.sect_name}`
return textures.value[key] ?? null
}
return null
}
function getRegionStyle(type: string) {
if (regionStyleCache.has(type)) {
return regionStyleCache.get(type)
}
const style = {
fontFamily: '"Microsoft YaHei", sans-serif',
fontSize: type === 'sect' ? 85 : 100,
fill: type === 'sect' ? '#ffcc00' : (type === 'city' ? '#ccffcc' : '#ffffff'),
stroke: { color: '#000000', width: 7, join: 'round' },
align: 'center',
dropShadow: {
color: '#000000',
blur: 4,
angle: Math.PI / 6,
distance: 4,
alpha: 0.8
}
}
regionStyleCache.set(type, style)
return style
}
function handleRegionSelect(region: RegionSummary) {
emit('regionSelected', {
type: 'region',
id: String(region.id),
name: region.name
})
}
</script>
<template>
<container>
<!-- Tile Layer -->
<container ref="mapContainer" />
<!-- Region Labels Layer (Above tiles) -->
<container :z-index="200">
<!-- @vue-ignore -->
<text
v-for="r in Array.from(worldStore.regions.values())"
:key="r.name"
:text="r.name"
:x="r.x * TILE_SIZE + TILE_SIZE / 2"
:y="r.y * TILE_SIZE + TILE_SIZE * 1.5"
:anchor="0.5"
:style="getRegionStyle(r.type)"
event-mode="static"
cursor="pointer"
@pointertap="handleRegionSelect(r)"
/>
</container>
</container>
</template>