add random tiles
BIN
assets/tiles/bamboo_1.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
assets/tiles/bamboo_2.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
assets/tiles/bamboo_3.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
assets/tiles/bamboo_4.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
assets/tiles/bamboo_5.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
assets/tiles/bamboo_6.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
assets/tiles/bamboo_7.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/tiles/bamboo_8.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
assets/tiles/bamboo_9.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
assets/tiles/gobi_1.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/tiles/gobi_2.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
assets/tiles/gobi_3.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
assets/tiles/gobi_4.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
assets/tiles/gobi_5.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
assets/tiles/gobi_6.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
assets/tiles/gobi_7.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
assets/tiles/gobi_8.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
assets/tiles/gobi_9.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
assets/tiles/island_1.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
assets/tiles/island_2.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
assets/tiles/island_3.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
assets/tiles/island_4.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
assets/tiles/island_5.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
assets/tiles/island_6.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
assets/tiles/island_7.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
assets/tiles/island_8.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
assets/tiles/island_9.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
assets/tiles/rainforest_1.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/tiles/rainforest_2.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
assets/tiles/rainforest_3.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
assets/tiles/rainforest_4.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
assets/tiles/rainforest_5.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
assets/tiles/rainforest_6.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
assets/tiles/rainforest_7.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
assets/tiles/rainforest_8.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
assets/tiles/rainforest_9.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
assets/tiles/swamp_1.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
assets/tiles/swamp_2.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
assets/tiles/swamp_3.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
assets/tiles/swamp_4.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
assets/tiles/swamp_5.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
assets/tiles/swamp_6.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
assets/tiles/swamp_7.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
assets/tiles/swamp_8.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
assets/tiles/swamp_9.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
@@ -8,7 +8,7 @@ import type { RegionSummary } from '../../types/core'
|
||||
|
||||
const TILE_SIZE = 64
|
||||
const mapContainer = ref<Container>()
|
||||
const { textures, isLoaded, loadSectTexture, loadCityTexture } = useTextures()
|
||||
const { textures, isLoaded, loadSectTexture, loadCityTexture, getTileTexture } = useTextures()
|
||||
const worldStore = useWorldStore()
|
||||
|
||||
// 动态水面相关变量
|
||||
@@ -121,7 +121,7 @@ async function renderMap() {
|
||||
}
|
||||
|
||||
// 处理普通地块
|
||||
let tex = textures.value[type]
|
||||
let tex = getTileTexture(type, x, y)
|
||||
|
||||
if (type === 'SECT') {
|
||||
// Legacy placeholder
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { ref } from 'vue'
|
||||
import { Assets, Texture, TextureStyle } from 'pixi.js'
|
||||
import { gameApi } from '@/api/game'
|
||||
import { getClusteredTileVariant } from '@/utils/procedural'
|
||||
|
||||
// 设置全局纹理缩放模式为 nearest (像素风)
|
||||
TextureStyle.defaultOptions.scaleMode = 'nearest'
|
||||
|
||||
// 地形变体配置
|
||||
const TILE_VARIANTS: Record<string, { prefix: string, count: number }> = {
|
||||
'RAINFOREST': { prefix: 'rainforest', count: 9 },
|
||||
'BAMBOO': { prefix: 'bamboo', count: 9 },
|
||||
'GOBI': { prefix: 'gobi', count: 9 },
|
||||
'ISLAND': { prefix: 'island', count: 9 },
|
||||
'SWAMP': { prefix: 'swamp', count: 9 },
|
||||
}
|
||||
|
||||
// 全局纹理缓存,避免重复加载
|
||||
const textures = ref<Record<string, Texture>>({})
|
||||
const isLoaded = ref(false)
|
||||
@@ -71,6 +81,20 @@ export function useTextures() {
|
||||
}
|
||||
})
|
||||
|
||||
// Load Tile Variants
|
||||
const variantPromises: Promise<void>[] = []
|
||||
Object.entries(TILE_VARIANTS).forEach(([key, { prefix, count }]) => {
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const variantKey = `${key}_${i}`
|
||||
const url = `/assets/tiles/${prefix}_${i}.png`
|
||||
variantPromises.push(
|
||||
Assets.load(url)
|
||||
.then(tex => { textures.value[variantKey] = tex })
|
||||
.catch(e => console.warn(`Failed to load variant ${variantKey}`, e))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Load Clouds
|
||||
const cloudPromises: Promise<void>[] = []
|
||||
for (let i = 0; i <= 8; i++) {
|
||||
@@ -100,7 +124,7 @@ export function useTextures() {
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all([...tilePromises, ...avatarPromises, ...cloudPromises])
|
||||
await Promise.all([...tilePromises, ...variantPromises, ...avatarPromises, ...cloudPromises])
|
||||
|
||||
isLoaded.value = true
|
||||
console.log('Base textures loaded')
|
||||
@@ -162,13 +186,30 @@ export function useTextures() {
|
||||
await Promise.all(slicePromises)
|
||||
}
|
||||
|
||||
// 获取地形纹理(支持随机变体)
|
||||
const getTileTexture = (type: string, x: number, y: number): Texture | undefined => {
|
||||
const variantConfig = TILE_VARIANTS[type]
|
||||
if (variantConfig) {
|
||||
// 使用噪声聚类算法替代纯随机 Hash
|
||||
// 让变体在地图上呈现自然的群落分布,减少视觉噪点
|
||||
const index = getClusteredTileVariant(x, y, variantConfig.count)
|
||||
const variantKey = `${type}_${index}`
|
||||
|
||||
if (textures.value[variantKey]) {
|
||||
return textures.value[variantKey]
|
||||
}
|
||||
}
|
||||
return textures.value[type]
|
||||
}
|
||||
|
||||
return {
|
||||
textures,
|
||||
isLoaded,
|
||||
loadBaseTextures,
|
||||
loadSectTexture,
|
||||
loadCityTexture,
|
||||
availableAvatars
|
||||
availableAvatars,
|
||||
getTileTexture
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
68
web/src/utils/procedural.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 地形生成相关的过程生成算法
|
||||
* 用于解决纯随机带来的视觉杂乱问题
|
||||
*/
|
||||
|
||||
/**
|
||||
* 伪随机数生成器 (基于坐标)
|
||||
* 返回 [0, 1] 之间的浮点数
|
||||
*/
|
||||
function random(x: number, y: number): number {
|
||||
const dot = x * 12.9898 + y * 78.233;
|
||||
const sin = Math.sin(dot);
|
||||
return (sin * 43758.5453) % 1; // frac
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取基于噪声聚类的地块变体索引
|
||||
*
|
||||
* 算法逻辑:
|
||||
* 1. 使用双重正弦波生成低频噪声(Biomes/群落感),让相邻的格子倾向于选择索引相近的变体。
|
||||
* 2. 叠加高频 Hash 扰动(Jitter),避免纹理过于死板或出现明显的人工波纹。
|
||||
* 3. 最终效果:变体会在地图上呈现自然的“斑块状”分布,而非杂乱的噪点。
|
||||
*
|
||||
* @param x 地图 X 坐标
|
||||
* @param y 地图 Y 坐标
|
||||
* @param count 变体总数 (例如 9)
|
||||
* @returns 1 到 count 之间的整数索引
|
||||
*/
|
||||
export function getClusteredTileVariant(x: number, y: number, count: number): number {
|
||||
if (count <= 1) return 1;
|
||||
|
||||
// 1. 低频噪声 (Large Scale Noise)
|
||||
// 决定区域的主色调。Scale 越小,斑块越大。
|
||||
// 0.15 意味着大约 2PI / 0.15 ~= 40 格一个完整周期,
|
||||
// 视觉上大约 10-20 格为一个明显的“群落”。
|
||||
const scale = 0.15;
|
||||
|
||||
// 使用两个不同频率和方向的波叠加,打破完美的对角线感
|
||||
const lowFreqNoise = Math.sin(x * scale + y * scale * 0.5) +
|
||||
Math.cos(y * scale * 0.8 - x * scale * 0.3);
|
||||
|
||||
// lowFreqNoise 范围约为 [-2, 2],归一化到 [0, 1]
|
||||
const normalizedNoise = (lowFreqNoise + 2) / 4;
|
||||
|
||||
// 2. 高频扰动 (Jitter)
|
||||
// 如果完全依赖低频噪声,变体变化会像等高线一样生硬(1->2->3...)。
|
||||
// 引入扰动可以让边缘交错,且让同一群落内部也有少许变化。
|
||||
// range: [-0.5, 0.5]
|
||||
const noiseVal = Math.abs(random(x, y)); // 0~1
|
||||
const jitter = (noiseVal - 0.5) * 0.5; // 强度系数 0.5
|
||||
|
||||
// 3. 混合计算
|
||||
let finalValue = normalizedNoise + jitter;
|
||||
|
||||
// 钳制到 [0, 1]
|
||||
finalValue = Math.max(0, Math.min(1, finalValue));
|
||||
|
||||
// 4. 映射到 [1, count]
|
||||
// 使用 Math.floor(val * count) 得到 0..count-1,再 +1
|
||||
let index = Math.floor(finalValue * count) + 1;
|
||||
|
||||
// 边界保护 (防止浮点误差导致溢出)
|
||||
if (index > count) index = count;
|
||||
if (index < 1) index = 1;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||