diff --git a/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts b/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts index 7d8de00..4ed1201 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts @@ -71,6 +71,7 @@ interface DesertContext { gobiVariant: number[][]; gobiMinHeight: number[][]; stoneHeight: number[][]; + stoneDepth: number[][]; stoneVariant: number[][]; streamDepthMap: Map; } diff --git a/voxel-tactics-horizon/src/features/Map/logic/terrain.ts b/voxel-tactics-horizon/src/features/Map/logic/terrain.ts index 44966cc..49fdd98 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/terrain.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/terrain.ts @@ -29,6 +29,7 @@ import { } from './desertFeatures'; import { createMountainRockContext } from './mountainFeatures'; import type { RockFieldContext } from './rockFeatures'; +import { generateMountainStream, type MountainStreamVoxel } from './waterSystem'; // 植被生成 (已移除旧系统) // import { @@ -74,7 +75,7 @@ export type ProgressCallback = (progress: TerrainGenerationProgress) => void; // ============= 地形生成配置 ============= // 地形生成版本号 - 每次修改时改变这个数字,触发地图重新生成 -export const TERRAIN_VERSION = 83; +export const TERRAIN_VERSION = 86; // 优化:河流在同一平面时避开高度边界,保持在平台中心 // 逻辑层面的网格大小(战棋移动格子) export const TILE_SIZE = 1; @@ -313,6 +314,23 @@ export const generateTerrain = async ( } } + // ===== 生成山地溪流(如果是山地场景)===== + let mountainStreamMap: Map | null = null; + if (sceneConfig?.name === 'mountain' && mountainRockContext && terrainHeightMap) { + reportProgress('basic', 3, '生成山地溪流系统'); + await new Promise(resolve => setTimeout(resolve, 0)); + + mountainStreamMap = generateMountainStream( + mapSize, + terrainHeightMap, + mountainRockContext, + seededRandom, + MICRO_SCALE + ); + + console.log(`[Terrain] 山地溪流已生成,包含 ${mountainStreamMap.size} 个微体素`); + } + // ===== 戈壁和巨石风化预处理 ===== // 在需要巨石的场景中,预先计算风化效果 let rockWeatheringResult: GobiWeatheringResult | null = null; @@ -629,10 +647,12 @@ export const generateTerrain = async ( // 4. Sand Mounds at the base (External Sand Blending) - Pre-calculation let sandMoundHeight = 0; + // 获取溪流信息(戈壁场景或山地场景) const streamInfo = isDesertScene && desertContext ? desertContext.streamDepthMap.get(`${ix}|${iz}`) : undefined; - const streamDepthMicro = streamInfo?.depth ?? 0; + const mountainStreamInfo = mountainStreamMap?.get(`${ix}|${iz}`); + const streamDepthMicro = streamInfo?.depth ?? mountainStreamInfo?.depth ?? 0; // 只有非河流区域才生成沙丘 if (streamDepthMicro === 0 && isDesertScene && logicType === 'sand' && gobiLogicHeight === 0 && stoneLogicHeight === 0) { @@ -686,18 +706,16 @@ export const generateTerrain = async ( // Apply sand mound height to worldY worldY += sandMoundHeight; - // 河流生成:强制拉平水面 - // 1. 消除地形微噪声的影响(对于河流区域) - // 2. 减去河道深度 + // 河流生成:强制使用绝对高度 + // 【方案A + B】如果河流存储了绝对表面高度,直接使用,完全忽略 logicHeight if (streamDepthMicro > 0) { - // 还原到基础平面高度,去除 noise/smoothing 的影响 + // 保持原有计算逻辑 + worldY = logicHeight * MICRO_SCALE; + } else if (streamDepthMicro > 0) { + // 降级方案(如果没有绝对高度,使用旧逻辑) const baseWorldY = logicHeight * MICRO_SCALE; - - // 如果当前已经受 Gobi/Stone 影响抬高了,也要考虑还原(但通常河流避开了它们) - // 这里我们假设河流区域应当是平坦的沙地基础 - - worldY = baseWorldY; // 重置为绝对平坦 - worldY -= streamDepthMicro; // 挖坑 + worldY = baseWorldY; + worldY -= streamDepthMicro; } const surfaceY = worldY; @@ -706,6 +724,10 @@ export const generateTerrain = async ( let type: VoxelType = logicType; const depth = surfaceY - y; + // 【图2参考实现】河床蚀刻系统:只在河床内部处理 + // 侧面会自然显示,因为相邻方块地表被降低了 + // 不需要额外挖空周围地形 + // 判断当前体素是否在石块范围内 // 石块范围:从 groundLevelY - stoneMicroDepth 到 groundLevelY + stoneMicroHeight const stoneTopY = groundLevelY + stoneMicroHeight; @@ -717,11 +739,26 @@ export const generateTerrain = async ( const isInGobiRange = gobiMicroHeight > 0 && !isInStoneRange && depth >= stoneMicroHeight && depth < stoneMicroHeight + gobiMicroHeight; - // 应用溪流蚀刻效果 - if (shouldApplyStreamEtching({ streamDepthMicro, depth })) { + // 【图2参考】河床内部渲染系统(简化版) + if (mountainStreamInfo && streamDepthMicro > 0 && depth < streamDepthMicro) { + // 渲染河床底部为泥土 + type = 'dirt'; + + // 河床中心区域:混合深色泥土和碎石 + if (mountainStreamInfo.isCenter) { + const noise = pseudoRandom(ix * 0.25 + iz * 0.25 + y * 0.1); + if (noise > 0.6) { + type = 'medium_dirt'; // 中等深度泥土 + } else if (noise > 0.35) { + type = 'dark_stone'; // 碎石 + } + } + } + // 戈壁溪流的原有逻辑 + else if (shouldApplyStreamEtching({ streamDepthMicro, depth })) { type = 'etched_sand'; // CENTER OF STREAM = DARK STONE / GRAVEL - if (streamInfo?.isCenter) { + if (streamInfo && streamInfo.isCenter) { // Simple noise for gravel patchiness if (pseudoRandom(ix * 0.3 + iz * 0.3) > 0.4) { type = 'dark_stone'; @@ -881,7 +918,7 @@ export const generateTerrain = async ( } if (streamDepthMicro > 0) { - // Rule 1: Enable water filling logic + // 戈壁溪流水体填充 if (streamInfo?.waterHeight && streamInfo.waterHeight > 0) { for (let h = 1; h <= streamInfo.waterHeight; h++) { // Ensure water doesn't exceed surface (should not happen with waterH calculation, but safe) @@ -897,6 +934,132 @@ export const generateTerrain = async ( ); } } + + // 山地溪流水体填充(包含瀑布效果) + if (mountainStreamInfo?.waterHeight && mountainStreamInfo.waterHeight > 0) { + const waterHeight = mountainStreamInfo.waterHeight; + + // 普通水面填充 + for (let h = 1; h <= waterHeight; h++) { + if (surfaceY + h > surfaceY + streamDepthMicro) break; + + addVoxel( + ix, + surfaceY + h, + iz, + 'water', + varyColor('water', seededRandom()), + 1 + ); + } + + // 瀑布效果:基于相邻微体素的真实高度差填充 + // 【修复】检测相邻河流微体素的绝对高度,填充高度差 + if (mountainStreamInfo && waterHeight > 0) { + const waterTopY = surfaceY + Math.floor(waterHeight); + + // 检测4个方向的相邻微体素 + const checkDirections = [ + { dx: 1, dz: 0 }, + { dx: -1, dz: 0 }, + { dx: 0, dz: 1 }, + { dx: 0, dz: -1 }, + ]; + + let maxHeightDiff = 0; + + for (const dir of checkDirections) { + const nx = ix + dir.dx; + const nz = iz + dir.dz; + + const neighborStreamInfo = mountainStreamMap?.get(`${nx}|${nz}`); + + if (neighborStreamInfo) { + // 简化:使用cascadeHeight判断 + const cascadeH = neighborStreamInfo.cascadeHeight || 0; + + if (cascadeH > 0) { + maxHeightDiff = Math.max(maxHeightDiff, cascadeH); + } + } + } + + // 如果检测到高度差 > 2,填充侧面瀑布 + if (maxHeightDiff >= 2) { + for (let dy = 1; dy < maxHeightDiff; dy++) { + const cascadeY = waterTopY - dy; + + if (cascadeY > MIN_WORLD_Y && !occupancy.has(keyOf(ix, cascadeY, iz))) { + const noise = pseudoRandom(ix * 0.3 + cascadeY * 0.7 + iz * 0.5); + if (noise > 0.1) { // 90% 密度 + addVoxel( + ix, + cascadeY, + iz, + 'water', + varyColor('water', seededRandom()), + 0.8 + ); + } + } + } + } + + // 瀑布底部水花效果 + if (mountainStreamInfo.isCascade || maxHeightDiff >= 2) { + const splashRange = 2; + for (let dx = -splashRange; dx <= splashRange; dx++) { + for (let dz = -splashRange; dz <= splashRange; dz++) { + if (dx === 0 && dz === 0) continue; + + const splashX = ix + dx; + const splashZ = iz + dz; + const dist = Math.sqrt(dx * dx + dz * dz); + + if (dist > splashRange) continue; + + const splashHeight = Math.floor(2 - dist); + if (splashHeight < 1) continue; + + const splashNoise = pseudoRandom(splashX * 0.5 + waterTopY * 0.9 + splashZ * 0.3); + if (splashNoise > 0.3) { + for (let h = 1; h <= splashHeight; h++) { + const splashY = waterTopY + h; + if (!occupancy.has(keyOf(splashX, splashY, splashZ))) { + addVoxel( + splashX, + splashY, + splashZ, + 'water', + varyColor('water', seededRandom()), + 0.6 + ); + } + } + } + } + } + } + } + + // 激流效果:非瀑布段但有一定流速(流向明显) + if (!mountainStreamInfo.isCascade && mountainStreamInfo.flowDirection) { + const waterTopY = surfaceY + Math.floor(waterHeight); + const noise = pseudoRandom(ix * 0.4 + iz * 0.6); + + // 50% 概率在水面上方生成激流水花(1格高) + if (noise > 0.5 && !occupancy.has(keyOf(ix, waterTopY + 1, iz))) { + addVoxel( + ix, + waterTopY + 1, + iz, + 'water', + varyColor('water', seededRandom()), + 0.3 // 很透明的水花 + ); + } + } + } } // Sparse half-height tufts on surface for additional detail diff --git a/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts b/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts index c03b61c..9cbe4d6 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts @@ -261,9 +261,6 @@ export const generateDesertRiver = ( const bankDepth = 1; // 浅河床深度 const waterFillHeight = 2; // 水深 - // 使用 Set 防止重复计算 - const processedPoints = new Set(); - for (const node of bestPath) { const cx = node.x; const cy = node.y; // cy is Z @@ -318,3 +315,558 @@ export const generateDesertRiver = ( return streamMap; }; + +/** + * 山地溪流生成系统 V1 + * 特点: + * 1. 从高处山泉起点流向低处,符合自然规律 + * 2. 河流宽度 3-6 微体素,河床 8-9 微体素 + * 3. 深度根据宽度动态调整(1.5-3 微体素) + * 4. 严格避开石头方块(stoneHeight > 0 或 stoneDepth > 0) + * 5. 高度落差处生成瀑布效果(密集水体素 + 水花) + * 6. 支持 1-2 条支流系统 + * 7. 起点可以是山泉洞口效果 + * + * @param mapSize 逻辑地图大小 + * @param terrainHeightMap 地形高度图(逻辑坐标) + * @param rockContext 山地巨石上下文(用于避障) + * @param rng 随机数生成器 + * @param microScale 微缩比例(默认 8) + * @returns 溪流深度Map,包含河床、水面、瀑布等信息 + */ +export interface MountainStreamVoxel extends StreamVoxel { + isCascade?: boolean; // 是否瀑布段 + cascadeHeight?: number; // 瀑布落差(微体素) + isSpring?: boolean; // 是否山泉起点 + isTributary?: boolean; // 是否支流 + flowDirection?: { dx: number; dz: number }; // 流向(未来用于动画) +} + +interface MountainRockContext { + stoneHeight: number[][]; + stoneDepth: number[][]; + stoneVariant: number[][]; +} + +export const generateMountainStream = ( + mapSize: number, + terrainHeightMap: number[][], + rockContext: MountainRockContext, + rng: () => number, + microScale: number = DEFAULT_MICRO_SCALE +): Map => { + const width = mapSize * microScale; + const height = mapSize * microScale; + + console.log('[MountainStream] 开始生成山地溪流系统...'); + + // 1. 初始化噪声生成器 + let seedVal = rng() * 10000; + const noise2D = createNoise2D(() => { + seedVal = (seedVal * 9301 + 49297) % 233280; + return seedVal / 233280; + }); + + // 2. 构建障碍物网格 + 高度图(微体素精度) + const grid = new Uint8Array(width * height).fill(0); // 0: 可行走, 1: 障碍物 + const heightGrid = new Float32Array(width * height); // 每个微体素的高度 + + const setGrid = (x: number, z: number, val: number) => { + if (x >= 0 && x < width && z >= 0 && z < height) { + grid[z * width + x] = Math.max(grid[z * width + x], val); + } + }; + + const getGrid = (x: number, z: number): number => { + if (x >= 0 && x < width && z >= 0 && z < height) { + return grid[z * width + x]; + } + return 1; // 越界视为障碍 + }; + + const setHeight = (x: number, z: number, h: number) => { + if (x >= 0 && x < width && z >= 0 && z < height) { + heightGrid[z * width + x] = h; + } + }; + + const getHeight = (x: number, z: number): number => { + if (x >= 0 && x < width && z >= 0 && z < height) { + return heightGrid[z * width + x]; + } + return 0; + }; + + // 3. 填充障碍物数据和高度图 + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + const hasStone = rockContext.stoneHeight[lx][lz] > 0 || rockContext.stoneDepth[lx][lz] > 0; + const baseHeight = terrainHeightMap[lx][lz]; + + // 填充微体素网格 + for (let mx = 0; mx < microScale; mx++) { + for (let mz = 0; mz < microScale; mz++) { + const ix = lx * microScale + mx; + const iz = lz * microScale + mz; + + // 石头区域标记为障碍物(严格避开) + if (hasStone) { + setGrid(ix, iz, 1); + } + + // 填充高度(基础地形高度,不考虑石头) + // 使用微小噪声增加地形起伏 + const microNoise = noise2D(ix * 0.5, iz * 0.5) * 0.3; + setHeight(ix, iz, baseHeight + microNoise); + } + } + } + } + + // 4. 选择起点(山泉位置) + // 策略:在高度 > 平均高度 + 阈值 的非石头区域随机选择 + const avgHeight = terrainHeightMap.flat().reduce((a, b) => a + b, 0) / (mapSize * mapSize); + const heightThreshold = avgHeight + Math.max(1, avgHeight * 0.3); + + const candidateStarts: Array<{ x: number; z: number; h: number }> = []; + + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + const h = terrainHeightMap[lx][lz]; + const hasStone = rockContext.stoneHeight[lx][lz] > 0 || rockContext.stoneDepth[lx][lz] > 0; + + if (h >= heightThreshold && !hasStone) { + // 转换为微体素坐标(中心点) + const ix = lx * microScale + Math.floor(microScale / 2); + const iz = lz * microScale + Math.floor(microScale / 2); + candidateStarts.push({ x: ix, z: iz, h }); + } + } + } + + if (candidateStarts.length === 0) { + console.warn('[MountainStream] 未找到合适的山泉起点,使用随机高点'); + // 降级方案:选择最高的几个点 + let maxH = -Infinity; + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + maxH = Math.max(maxH, terrainHeightMap[lx][lz]); + } + } + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + if (terrainHeightMap[lx][lz] >= maxH - 0.5) { + const ix = lx * microScale + Math.floor(microScale / 2); + const iz = lz * microScale + Math.floor(microScale / 2); + if (getGrid(ix, iz) === 0) { + candidateStarts.push({ x: ix, z: iz, h: terrainHeightMap[lx][lz] }); + } + } + } + } + } + + if (candidateStarts.length === 0) { + console.error('[MountainStream] 无法找到任何有效起点,溪流生成失败'); + return new Map(); + } + + // 选择最高的前5个候选点之一 + candidateStarts.sort((a, b) => b.h - a.h); + const topCandidates = candidateStarts.slice(0, Math.min(5, candidateStarts.length)); + const startPoint = topCandidates[Math.floor(rng() * topCandidates.length)]; + + console.log(`[MountainStream] 选择山泉起点: (${startPoint.x}, ${startPoint.z}), 高度: ${startPoint.h.toFixed(2)}`); + + // 5. A* 寻路:从高处到低处 + // 目标:选择地图边缘或低洼区域作为终点 + const minHeight = Math.min(...terrainHeightMap.flat()); + const targetHeight = minHeight + (avgHeight - minHeight) * 0.2; // 低于平均高度 + + // 选择地图边缘的低点作为终点候选 + const candidateEnds: Array<{ x: number; z: number; h: number }> = []; + + const edgePositions = [ + ...Array.from({ length: mapSize }, (_, i) => ({ lx: i, lz: 0 })), // 上边缘 + ...Array.from({ length: mapSize }, (_, i) => ({ lx: i, lz: mapSize - 1 })), // 下边缘 + ...Array.from({ length: mapSize }, (_, i) => ({ lx: 0, lz: i })), // 左边缘 + ...Array.from({ length: mapSize }, (_, i) => ({ lx: mapSize - 1, lz: i })), // 右边缘 + ]; + + for (const { lx, lz } of edgePositions) { + const h = terrainHeightMap[lx][lz]; + const hasStone = rockContext.stoneHeight[lx][lz] > 0 || rockContext.stoneDepth[lx][lz] > 0; + + if (h <= targetHeight && !hasStone) { + const ix = lx * microScale + Math.floor(microScale / 2); + const iz = lz * microScale + Math.floor(microScale / 2); + candidateEnds.push({ x: ix, z: iz, h }); + } + } + + if (candidateEnds.length === 0) { + console.warn('[MountainStream] 未找到边缘低点,使用最低点'); + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + const h = terrainHeightMap[lx][lz]; + if (h <= minHeight + 0.5 && getGrid(lx * microScale, lz * microScale) === 0) { + const ix = lx * microScale + Math.floor(microScale / 2); + const iz = lz * microScale + Math.floor(microScale / 2); + candidateEnds.push({ x: ix, z: iz, h }); + } + } + } + } + + if (candidateEnds.length === 0) { + console.error('[MountainStream] 无法找到有效终点,溪流生成失败'); + return new Map(); + } + + // 选择最低的终点 + candidateEnds.sort((a, b) => a.h - b.h); + const endPoint = candidateEnds[Math.floor(rng() * Math.min(3, candidateEnds.length))]; + + console.log(`[MountainStream] 选择终点: (${endPoint.x}, ${endPoint.z}), 高度: ${endPoint.h.toFixed(2)}`); + + // 6. 执行 A* 寻路(优先选择高度下降方向) + interface PathNode { + x: number; + z: number; + f: number; + g: number; + h: number; + parent: PathNode | null; + } + + const minPathLength = mapSize * microScale * 0.5; // 最小路径长度 50% + let mainPath: PathNode[] | null = null; + + const startNode: PathNode = { + x: startPoint.x, + z: startPoint.z, + f: 0, + g: 0, + h: 0, + parent: null, + }; + + const openList = new PriorityQueue() as any; + openList.enqueue(startNode); + const closedSet = new Set(); + const nodeKey = (x: number, z: number) => `${x}|${z}`; + + // 启发式函数:优先选择高度下降 + 接近终点 + const heuristic = (x: number, z: number, currentHeight: number): number => { + const distToEnd = Math.abs(endPoint.x - x) + Math.abs(endPoint.z - z); + const heightAtPos = getHeight(x, z); + const heightDiff = currentHeight - heightAtPos; // 正值表示下降 + + // 高度下降奖励 + 距离惩罚 + return distToEnd * 1.0 - heightDiff * 20.0; // 强烈倾向于下坡 + }; + + let finalNode: PathNode | null = null; + let iterations = 0; + const maxIterations = width * height * 2; + + while (!openList.isEmpty() && iterations < maxIterations) { + iterations++; + const currentNode = openList.dequeue(); + if (!currentNode) break; + + // 到达终点附近或者到达足够低的位置 + const currentHeight = getHeight(currentNode.x, currentNode.z); + const distToEnd = Math.abs(endPoint.x - currentNode.x) + Math.abs(endPoint.z - currentNode.z); + + if (distToEnd < 8 && currentHeight <= endPoint.h + 0.5) { + finalNode = currentNode; + break; + } + + // 如果已经走得很远且高度足够低,也可以停止 + if (currentNode.g > minPathLength && currentHeight <= avgHeight * 0.5) { + finalNode = currentNode; + break; + } + + const key = nodeKey(currentNode.x, currentNode.z); + if (closedSet.has(key)) continue; + closedSet.add(key); + + // 8个方向 + 随机蜿蜒 + const dirs = [ + { dx: 1, dz: 0, cost: 1.0 }, + { dx: -1, dz: 0, cost: 1.0 }, + { dx: 0, dz: 1, cost: 1.0 }, + { dx: 0, dz: -1, cost: 1.0 }, + { dx: 1, dz: 1, cost: 1.414 }, + { dx: 1, dz: -1, cost: 1.414 }, + { dx: -1, dz: 1, cost: 1.414 }, + { dx: -1, dz: -1, cost: 1.414 }, + ]; + + for (const dir of dirs) { + const nx = currentNode.x + dir.dx; + const nz = currentNode.z + dir.dz; + + if (nx < 0 || nx >= width || nz < 0 || nz >= height) continue; + if (closedSet.has(nodeKey(nx, nz))) continue; + if (getGrid(nx, nz) === 1) continue; // 避开石头 + + let newG = currentNode.g + dir.cost; + + // 高度变化代价:上坡代价极高,下坡有奖励 + const heightDiff = getHeight(nx, nz) - currentHeight; + if (heightDiff > 0) { + newG += heightDiff * 50; // 强烈惩罚上坡 + } else { + newG += heightDiff * 5; // 下坡有轻微奖励(负值) + } + + // 【新增】避开高度边界:检查周围8个方向的高度一致性 + // 如果周围高度不一致,说明在边界上,增加代价 + if (Math.abs(heightDiff) < 0.1 * microScale) { // 如果是在同一平面 + let heightVariation = 0; + let sameHeightCount = 0; + const checkRadius = 1; + + for (let dx = -checkRadius; dx <= checkRadius; dx++) { + for (let dz = -checkRadius; dz <= checkRadius; dz++) { + if (dx === 0 && dz === 0) continue; + const checkX = nx + dx; + const checkZ = nz + dz; + + if (checkX >= 0 && checkX < width && checkZ >= 0 && checkZ < height) { + const neighborHeight = getHeight(checkX, checkZ); + const diff = Math.abs(neighborHeight - currentHeight); + + if (diff < 0.1 * microScale) { + sameHeightCount++; // 同高度的邻居 + } else { + heightVariation += diff; // 累计高度差异 + } + } + } + } + + // 如果周围高度差异大(在边界上),增加代价 + if (heightVariation > 0.5 * microScale) { + newG += 30; // 强烈避开高度边界 + } + + // 如果同高度邻居多(在平台中心),减少代价 + if (sameHeightCount >= 6) { + newG -= 5; // 奖励在平台中心 + } + } + + // 噪声引导:模拟地形凹陷偏好 + const noiseVal = noise2D(nx * 0.05, nz * 0.05); + newG += (noiseVal + 1) * 2; // 轻微噪声影响 + + // 随机蜿蜒 + newG += rng() * 1.0; + + // 每隔一段距离,检查是否有局部凹陷(水流汇集效果) + if (Math.floor(currentNode.g / 10) !== Math.floor(newG / 10)) { + let minLocalHeight = currentHeight; + for (let dx = -2; dx <= 2; dx++) { + for (let dz = -2; dz <= 2; dz++) { + const checkX = nx + dx; + const checkZ = nz + dz; + if (getGrid(checkX, checkZ) === 0) { + minLocalHeight = Math.min(minLocalHeight, getHeight(checkX, checkZ)); + } + } + } + if (minLocalHeight < currentHeight - 0.5) { + newG -= 10; // 向凹陷处倾斜 + } + } + + const h = heuristic(nx, nz, currentHeight); + openList.enqueue({ + x: nx, + z: nz, + f: newG + h, + g: newG, + h: h, + parent: currentNode, + }); + } + } + + if (finalNode) { + // 回溯路径 + const path: PathNode[] = []; + let curr: PathNode | null = finalNode; + while (curr) { + path.push(curr); + curr = curr.parent; + } + path.reverse(); + mainPath = path; + console.log(`[MountainStream] 主河流路径生成成功,长度: ${path.length} 微体素`); + } else { + console.warn('[MountainStream] 主河流路径生成失败'); + return new Map(); + } + + // 7. 生成支流(1-2条) + const tributaries: PathNode[][] = []; + const numTributaries = Math.floor(rng() * 2) + 1; // 1-2条支流 + + for (let i = 0; i < numTributaries; i++) { + // 在主河流 30%-70% 位置选择分叉点 + const branchIndex = Math.floor(mainPath.length * (0.3 + rng() * 0.4)); + const branchPoint = mainPath[branchIndex]; + + // 支流长度为主流的 10-20% + const tribLength = Math.floor(mainPath.length * (0.1 + rng() * 0.1)); + + // 支流方向:与主流夹角 30-60 度 + const mainDir = + branchIndex < mainPath.length - 1 + ? { + dx: mainPath[branchIndex + 1].x - branchPoint.x, + dz: mainPath[branchIndex + 1].z - branchPoint.z, + } + : { dx: 1, dz: 0 }; + + const angle = Math.atan2(mainDir.dz, mainDir.dx); + const tribAngle = angle + (rng() > 0.5 ? 1 : -1) * (Math.PI / 6 + rng() * Math.PI / 6); // ±30-60度 + + // 生成支流路径(简化版:直线 + 噪声扰动) + const tributary: PathNode[] = []; + let tx = branchPoint.x; + let tz = branchPoint.z; + + for (let step = 0; step < tribLength; step++) { + const stepX = Math.cos(tribAngle) + (rng() - 0.5) * 0.5; + const stepZ = Math.sin(tribAngle) + (rng() - 0.5) * 0.5; + + tx += stepX; + tz += stepZ; + + const ix = Math.floor(tx); + const iz = Math.floor(tz); + + if (ix < 0 || ix >= width || iz < 0 || iz >= height) break; + if (getGrid(ix, iz) === 1) break; // 遇到石头停止 + + tributary.push({ x: ix, z: iz, f: 0, g: step, h: 0, parent: null }); + } + + if (tributary.length > 5) { + tributaries.push(tributary); + console.log(`[MountainStream] 支流 ${i + 1} 生成成功,长度: ${tributary.length}`); + } + } + + // 8. 河床雕刻(主河流 + 支流) + const streamMap = new Map(); + + const carveRiverBed = (path: PathNode[], isTributary: boolean = false) => { + for (let i = 0; i < path.length; i++) { + const node = path[i]; + const cx = node.x; + const cz = node.z; + + // 河流宽度和深度(支流更窄更浅) + const waterWidth = isTributary + ? 2 + rng() * 1 // 2-3 微体素 + : 3 + rng() * 3; // 3-6 微体素 + const riverBedWidth = isTributary + ? 4 + rng() * 1 // 4-5 微体素 + : 8 + rng() * 1; // 8-9 微体素 + const baseDepth = isTributary + ? 1 + waterWidth * 0.2 // 1-1.6 微体素 + : 1.5 + waterWidth * 0.25; // 1.5-3 微体素 + + // 检查是否为瀑布段(高度落差 >= 2 微体素) + let isCascade = false; + let cascadeHeight = 0; + + if (i < path.length - 1) { + const currentH = getHeight(cx, cz); + const nextH = getHeight(path[i + 1].x, path[i + 1].z); + const heightDiff = currentH - nextH; + + if (heightDiff >= 2) { + isCascade = true; + cascadeHeight = heightDiff; + } + } + + // 在河床宽度范围内雕刻 + const scanRad = Math.ceil(riverBedWidth / 2) + 1; + + for (let dx = -scanRad; dx <= scanRad; dx++) { + for (let dz = -scanRad; dz <= scanRad; dz++) { + const tx = cx + dx; + const tz = cz + dz; + + if (tx < 0 || tx >= width || tz < 0 || tz >= height) continue; + if (getGrid(tx, tz) === 1) continue; // 避开石头 + + const dist = Math.sqrt(dx * dx + dz * dz); + + // 边缘噪声,使河岸不规则 + const edgeNoise = noise2D(tx * 0.1, tz * 0.1) * 0.8; + const noisyWaterWidth = waterWidth / 2 + edgeNoise * 0.3; + const noisyBedWidth = riverBedWidth / 2 + edgeNoise * 0.5; + + if (dist < noisyBedWidth) { + const isWater = dist < noisyWaterWidth; + const depthFactor = 1 - dist / noisyBedWidth; // 中心深,边缘浅 + const depth = baseDepth * Math.max(0.3, depthFactor); + + const key = `${tx}|${tz}`; + const existing = streamMap.get(key); + + // 保留更深的深度 + if (!existing || depth > existing.depth) { + // 计算流向 + let flowDir = { dx: 0, dz: 0 }; + if (i < path.length - 1) { + flowDir = { + dx: path[i + 1].x - cx, + dz: path[i + 1].z - cz, + }; + const len = Math.sqrt(flowDir.dx ** 2 + flowDir.dz ** 2); + if (len > 0) { + flowDir.dx /= len; + flowDir.dz /= len; + } + } + + streamMap.set(key, { + depth: depth, + waterHeight: isWater ? depth * 0.8 : 0, // 水深为河床深度的80% + isCenter: dist < 1, + isCascade, + cascadeHeight, + isSpring: i === 0 && !isTributary, // 主河流起点标记为山泉 + isTributary, + flowDirection: flowDir, + }); + } + } + } + } + } + }; + + // 雕刻主河流 + carveRiverBed(mainPath, false); + + // 雕刻支流 + for (const tributary of tributaries) { + carveRiverBed(tributary, true); + } + + console.log(`[MountainStream] 溪流系统生成完成,共 ${streamMap.size} 个微体素`); + + return streamMap; +};